/** * @RDE_HEADER@ * The DataManager class takes strucure element data from the HandlerStructureElement, * offers an interface to get and set structure data * $Id$ * $HeadURL$ * $Author$ * Created by Philipp Dassow, SLST, 2017-12-05 */ #include "WsBaseDataInterface/data_manager.h" #include #include #include #include #include #include #include "main_handler.h" #include #include #include #include #include #include namespace WarehouseGuez { namespace WsBaseDataInterface { static constexpr int s_exerciseOffset = 1000000; static inline QString tlnTable(int exerciseId) { return QStringLiteral("tln_") + QString::number(exerciseId); } static inline QString tlnZuordnungTable(int exerciseId) { return QStringLiteral("tln_zuordnung_") + QString::number(exerciseId); } static inline QString triggerName(int exerciseId) { if (exerciseId <= 0) return QStringLiteral("whg_notifyInsertParticipantAssignment"); return QStringLiteral("whg_notifyInsertParticipantAssignment_") + QString::number(exerciseId); } //--------------------------------------------------------------------------------------------------------- DataManager::DataManager(MainHandler *mainHandler) : m_mainHandler(mainHandler) , m_webSocketServer(nullptr) , m_logger(getLogger(QStringLiteral("DataManager"))) { } //--------------------------------------------------------------------------------------------------------- bool DataManager::init() { reconnectSignals(); return true; } //--------------------------------------------------------------------------------------------------------- LtsWebSocket::ReplyMessage *DataManager::equipmentTypes(const QString &uuid) const { using namespace Messages; IWMS2ECC::equipTypeMap etMap; auto cbEqTypeWh = [&etMap](const WarehouseGuez::Database::EquipmentTypeWh &data) { IWMS2ECC::EquipmentType et; et.id = data.id; et.name = data.name; et.minEquipNo = data.ausstgNrMin; et.maxEquipNo = data.ausstgNrMax; et.specialType = 0; // not used in GUEZ et.taVulnerabilityModelId = data.vbAgdus; etMap.insert(et.id, et); }; m_mainHandler->db()->select(cbEqTypeWh); EquipmentTypes eTypes; eTypes.setEquipmentTypes(etMap); QJsonObject obj = eTypes.toJsonAll(); INFO_LOG(m_logger) << "equipTypeMap.size: " << etMap.size() << ", " << QJsonDocument(obj).toJson(); return new EquipmentTypesMessage(obj, uuid); } void DataManager::newExerciseInformationsFromEXCON(const EXCON::Exercises &ex) { const IWMS2ECC::exerciseMap exMap = ex.getExercises(); DEBUG_LOG(m_logger) << "Exercises map.size: " << exMap.size(); QVector dataVec; m_mainHandler->db()->select(dataVec); for (const IWMS2ECC::Exercise &exercise : exMap) { WarehouseGuez::Database::Exercise ex; ex.id = exercise.id; ex.name = exercise.name; ex.beginn = QDateTime::fromMSecsSinceEpoch(exercise.start); ex.aktUebung = exercise.actExercise; ex.tln = tlnTable(exercise.id); ex.tlnZuordnung = tlnZuordnungTable(exercise.id); bool ok; if (findExercise(exercise.id, dataVec)) ok = m_mainHandler->db()->update(ex); else ok = m_mainHandler->db()->insert(ex); if (!ok) { ERROR_LOG(m_logger) << "Error while insert/update in exercise"; break; } } } bool DataManager::findExercise(int exerciseId, const QVector &dataVec) { for (const Database::Exercise &exercise : dataVec) { if (exercise.id == exerciseId) return true; } return false; } static inline QString fixupString(const QString &in) { // Warehouse in GUEZ loves SQL-Injection QString ret; ret.reserve(in.size()); for (const QChar &c : in) { if (c == QLatin1Char('\"') || c == QLatin1Char('\'') || c.unicode() == 0x60 || // GRAVE ACCENT - ` c.unicode() == 0xb4 || // ACUTE ACCENT - ยด c == QLatin1Char(';')) continue; ret += c; } return ret; } static inline void bindPlayer(NSqlQuery &q, const IWMS2ECC::PlayerInfo &pl) { q.bindValue(QStringLiteral(":id"), pl.playerId); q.bindValue(QStringLiteral(":hierarchie"), fixupString(pl.hierarchy)); q.bindValue(QStringLiteral(":typ_id"), pl.typeId); q.bindValue(QStringLiteral(":ausstg_typ_id"), pl.equipTypeId); q.bindValue(QStringLiteral(":ausstg_nr"), pl.equipNo); q.bindValue(QStringLiteral(":zuordnung"), pl.assignState); q.bindValue(QStringLiteral(":y_nr"), fixupString(pl.alias)); q.bindValue(QStringLiteral(":info"), fixupString(pl.info)); q.bindValue(QStringLiteral(":loeschkennung"), pl.isDeleted ? 1 : 0); // bool -> smallint :( q.bindValue(QStringLiteral(":zeit"), NSqlQuery::qDateTimeToQVariant(QDateTime::currentDateTime())); } void DataManager::participantsFromEXCON(const PlayerInfo &pl) { const QMap plMap = pl.getAllPlayerInfo(); if (plMap.isEmpty()) { DEBUG_LOG(m_logger) << "No valid exercise -> no participants"; return; } // first get exerciseId int exerciseId = plMap.firstKey() / s_exerciseOffset; DEBUG_LOG(m_logger) << "ExerciseId: " << exerciseId; DEBUG_LOG(m_logger) << "plMap.size(): " << plMap.size(); // table is created at exercise creation QString tlnOut = tlnTable(exerciseId); NSqlQuery q(m_mainHandler->db()->database()); // If entries exist delete them QString stmt = QStringLiteral("DELETE FROM ") + tlnOut; DEBUG_LOG(m_logger) << "Executing delete statement: " << stmt; q.exec(stmt); stmt = QStringLiteral("INSERT INTO ") + tlnOut + QStringLiteral("( id, hierarchie, typ_id, ausstg_typ_id, ausstg_nr, zuordnung, y_nr, info, loeschkennung, zeit ) VALUES (" ) + QStringLiteral(":id, :hierarchie, :typ_id, :ausstg_typ_id, :ausstg_nr, :zuordnung, :y_nr, :info, :loeschkennung, :zeit )"); DEBUG_LOG(m_logger) << "Preparing insert statement: " << stmt; q.prepare(stmt); for (const IWMS2ECC::PlayerInfo &pl : plMap) { bindPlayer(q, pl); DEBUG_LOG(m_logger) << "Executing insert statement for participant: " << pl.playerId; if (!q.exec()) ERROR_LOG(m_logger) << "Error inserting data for participantId: " << pl.playerId; } } bool DataManager::participantFromEXCON(const IWMS2ECC::PlayerInfo &pl, int modType) { DEBUG_LOG(m_logger) << "IN participantFromEXCON: modType: " << modType << ", participantId: " << pl.playerId; DataModification modificationType = static_cast(modType); int exerciseId = pl.playerId / s_exerciseOffset; QString tlnOut = tlnTable(exerciseId); NSqlQuery q(m_mainHandler->db()->database()); DEBUG_LOG(m_logger) << "Executing delete statement: " << QStringLiteral("DELETE FROM %1 WHERE id = %2").arg(tlnOut).arg(pl.playerId); q.exec(QStringLiteral("DELETE FROM %1 WHERE id = %2").arg(tlnOut).arg(pl.playerId)); switch(modificationType) { case DataModification::eModified: case DataModification::eAdded: { QString stmt = QStringLiteral("INSERT INTO ") + tlnOut + QStringLiteral("( id, hierarchie, typ_id, ausstg_typ_id, ausstg_nr, zuordnung, y_nr, info, loeschkennung, zeit ) VALUES (" ) + QStringLiteral(":id, :hierarchie, :typ_id, :ausstg_typ_id, :ausstg_nr, :zuordnung, :y_nr, :info, :loeschkennung, :zeit )"); if (!q.prepare(stmt)) { ERROR_LOG(m_logger) << "Error while preparing insert stmt: " << stmt; return false; } bindPlayer(q, pl); DEBUG_LOG(m_logger) << "Executing insert statement: " << stmt; if (!q.exec()) { ERROR_LOG(m_logger) << "Error inserting data for participantId: " << pl.playerId; return false; } break; } case DataModification::eRemoved: break; } // end switch return true; } bool DataManager::exerciseChanged(const IWMS2ECC::Exercise &data, DataModification modType) { DEBUG_LOG(m_logger) << "IN exerciseChanged: id = " << data.id << ", actExercise: " << data.actExercise; Database::Exercise exData; exData.id = data.id; exData.name = data.name; exData.beginn = QDateTime::fromMSecsSinceEpoch(data.start); exData.tln = tlnTable(data.id); exData.tlnZuordnung = tlnZuordnungTable(data.id); exData.aktUebung = data.actExercise; switch(modType) { case DataModification::eAdded: { Database::Exercise tmp; if (m_mainHandler->db()->selectExercise(exData.id, tmp)) { if (!m_mainHandler->db()->update(exData)) { ERROR_LOG(m_logger) << "Error while updating exerciseData for exercise: " << data.id; return false; } } else { if (!m_mainHandler->db()->insert(exData)) { ERROR_LOG(m_logger) << "Error while inserting exerciseData for exercise: " << data.id; return false; } createTableTlnOut(exData.id); createTableTlnZuordnung(exData.id); } return true; } case DataModification::eModified: { if (!m_mainHandler->db()->update(exData)) { ERROR_LOG(m_logger) << "Error while updating exerciseData for exercise: " << data.id; return false; } return true; } case DataModification::eRemoved: { if (!m_mainHandler->db()->removeExercise(data.id)) { ERROR_LOG(m_logger) << "Error while removing exerciseData for exercise: " << data.id; return false; } return true; } } ERROR_LOG(m_logger) << "DataModification Type: " << modType << " not handled in switch"; return false; } void DataManager::createTableTlnOut(int exerciseId) { ERROR_LOG(m_logger) << "createTableTlnOut"; // xmlPath const QString xmlPath = NGL::directory(NGL::FileType::ConfigFile, QLatin1String("xml")); QString xmlFile = xmlPath + QStringLiteral("/whg.xml"); CodeGen::SqlGenerator sqlGen(xmlFile); QString tlnOut = tlnTable(exerciseId); ERROR_LOG(m_logger) << "tlnOut: " << tlnOut; sqlGen.createStatementsForTable(QStringLiteral("tln"), tlnOut); const CodeGen::SqlGenerator::SqlStatements stmts = sqlGen.getStatements(CodeGen::Global); QSqlRecord record = m_mainHandler->db()->database().record(tlnOut); NSqlQuery q(m_mainHandler->db()->database()); for (const QString &stmt : stmts.createTables) { ERROR_LOG(m_logger) << "stmt: " << stmt; if (record.isEmpty()) { if (!q.exec(stmt)) ERROR_LOG(m_logger) << "Error executing stmt: " << stmt; } } } void DataManager::createTableTlnZuordnung(int exerciseId) { ERROR_LOG(m_logger) << "in createTableTlnZuordnung"; // xmlPath const QString xmlPath = NGL::directory(NGL::FileType::ConfigFile, QLatin1String("xml")); QString xmlFile = xmlPath + QStringLiteral("/whg.xml"); CodeGen::SqlGenerator sqlGen(xmlFile); const QString tlnZuordnung = tlnZuordnungTable(exerciseId); ERROR_LOG(m_logger) << "tlnZuordnung: " << tlnZuordnung; sqlGen.createStatementsForTable(QStringLiteral("tln_zuordnung"), tlnZuordnung); const CodeGen::SqlGenerator::SqlStatements stmts = sqlGen.getStatements(CodeGen::Global); QSqlRecord record = m_mainHandler->db()->database().record(tlnZuordnung); NSqlQuery q(m_mainHandler->db()->database()); for (const QString &stmt : stmts.createTables) { DEBUG_LOG(m_logger) << "stmt: " << stmt; if (record.isEmpty()) { if (!q.exec(stmt)) ERROR_LOG(m_logger) << "Error executing stmt: " << stmt; } } for (const QString &stmt : stmts.createSequences) { DEBUG_LOG(m_logger) << "stmt: " << stmt; if (record.isEmpty()) { if (!q.exec(stmt)) ERROR_LOG(m_logger) << "Error executing stmt: " << stmt; } } createTriggerForExercise(q, exerciseId); } void DataManager::createTriggerForExercise(NSqlQuery &q, int exerciseId) { QString stmt; const QString triggerNameExercise(triggerName(exerciseId)); const QString tlnZuordnungTableExercise(tlnZuordnungTable(exerciseId)); // drop possible old trigger stmt = QLatin1String("DROP TRIGGER IF EXISTS ") + triggerNameExercise + QLatin1String(" ON ") + tlnZuordnungTableExercise; if (!q.exec(stmt)) ERROR_LOG(m_logger) << "Error dropping possible old trigger: " << stmt; // Create trigger stmt = QLatin1String("CREATE OR REPLACE FUNCTION public.") + triggerNameExercise + QLatin1String("() RETURNS TRIGGER AS $$ " " BEGIN " " PERFORM pg_notify('") + triggerNameExercise + QLatin1String("', CAST(NEW.id AS text));" " RETURN NEW;" "END;" "$$ LANGUAGE plpgsql;"); if (!q.exec(stmt)) ERROR_LOG(m_logger) << "Error while creating function in statement: " << stmt; stmt = QLatin1String("CREATE TRIGGER ") + triggerNameExercise + QLatin1String(" AFTER INSERT ON ") + tlnZuordnungTableExercise + QLatin1String(" FOR EACH ROW EXECUTE PROCEDURE public.") + triggerNameExercise + QLatin1String("();"); if (!q.exec(stmt)) ERROR_LOG(m_logger) << "Error while creating trigger in statement: " << stmt; QSqlDriver *driver = m_mainHandler->db()->database().driver(); // create subscription connect(driver, QOverload::of(&QSqlDriver::notification), this, &DataManager::onNotification, Qt::UniqueConnection); if (!driver->subscribeToNotification(triggerNameExercise)) WARNING_LOG(m_logger) << "Error subscribing to DB notification " << triggerNameExercise; else DEBUG_LOG(m_logger) << "Sucessfully subscribed to DB notification " << triggerNameExercise; } void DataManager::onInsertParticipantAssignment(int participantId, int exerciseId) { INFO_LOG(m_logger) << "Reading ParticipantAssignment from database!"; NSqlQuery q(m_mainHandler->db()->database()); QString stmt = QLatin1String("SELECT id, ausstg_nr, zuordnung, y_nr, info FROM ") + m_mainHandler->db()->schemaName() + QLatin1Char('.') + tlnZuordnungTable(exerciseId) + QLatin1String(" WHERE gesendet = false"); if (participantId > 0) stmt += QLatin1String(" AND id = ") + QString::number(participantId); stmt += QLatin1String(" ORDER BY zeit"); if (!q.exec(stmt)) { ERROR_LOG(m_logger) << "Error selecting data for participantId: " << participantId << ", exerciseId: " << exerciseId; return; } // we may get back more than one row, esp. in case participantId == 0 while (q.next()) { // use the 'correct' participant id in case participantId == 0 was given (happens during startup) participantId = q.value(0).toInt() - (exerciseId * s_exerciseOffset); DEBUG_LOG(m_logger) << "participantId: " << participantId; Registration reg; reg.setPlayerId(participantId); reg.setAssignment(IWMS2ECC::int2PlayerAssignState(q.value(2).toInt())); if (!q.value(1).isNull()) reg.setEquipNo(q.value(1).toInt()); if (!q.value(3).isNull()) reg.setAlias(q.value(3).toString()); if (!q.value(4).isNull()) reg.setInfo(q.value(4).toString()); reg.setExerciseId(exerciseId); reg.setOk(true); Messages::ParticipantRegisterInExconRequest *msg = new Messages::ParticipantRegisterInExconRequest(reg.toJson(), QLatin1String("blub")); INFO_LOG(m_logger) << "registration: " << msg->toJsonString(); m_webSocketServer->sendToAllClients(msg); } } void DataManager::onNotification(const QString &name, QSqlDriver::NotificationSource source, const QVariant &payload) { Q_UNUSED(source); // search coresponding signal INFO_LOG(m_logger) << "IN onNotification name: " << name; const QString triggerNameEx0 = triggerName(0); if (name.contains(triggerNameEx0)) { QStringRef name_(&name); name_ = name_.mid(triggerNameEx0.size() + 1); int exerciseId = name_.toInt(); int participantId = payload.toInt(); INFO_LOG(m_logger) << "exerciseId: " << exerciseId << ", participantId: " << participantId; onInsertParticipantAssignment(participantId, exerciseId); } } void DataManager::reconnectSignals() { NSqlQuery q(m_mainHandler->db()->database()); QVector vecExercises; m_mainHandler->db()->select(vecExercises); for (const auto &exercise : qAsConst(vecExercises)) { createTriggerForExercise(q, exercise.id); const QString tn = triggerName(exercise.id); QTimer::singleShot(5000, this, [this, tn]() { onNotification(tn, QSqlDriver::UnknownSource, QLatin1String("0")); }); } } void DataManager::registerReply(int exerciseId, qint32 participantId, int returnValue) { DEBUG_LOG(m_logger) << "registerReply for exercise id = " << exerciseId << ", participant id = " << participantId << ", return value = " << returnValue; NSqlQuery q(m_mainHandler->db()->database()); qint32 newParticipantId = participantId + (exerciseId * s_exerciseOffset); QString stmt = QLatin1String("UPDATE ") + m_mainHandler->db()->schemaName() + QLatin1Char('.') + tlnZuordnungTable(exerciseId) + QLatin1String(" SET gesendet = true, error_code = ") + QString::number(returnValue) + QLatin1String(" WHERE id = ") + QString::number(newParticipantId) + QLatin1String(" AND gesendet = false"); DEBUG_LOG(m_logger) << "registerReply: stmt = " << stmt; if (!q.exec(stmt)) { ERROR_LOG(m_logger) << "Cannot update reply entry for participant: " << participantId << " in exercise: " << exerciseId << ", returnValue: " << returnValue; } else { INFO_LOG(m_logger) << "Update successful for participant " << participantId << " in exercise: " << exerciseId; } if (returnValue != 0) { QString stmt1 = QLatin1String("UPDATE ") + m_mainHandler->db()->schemaName() + QLatin1String(".") + tlnTable(exerciseId) + QLatin1String(" SET ausstg_nr = -1, zeit = ") + QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()) + QLatin1String(" WHERE id = ") + QString::number(newParticipantId); DEBUG_LOG(m_logger) << "registerReply: stmt1 = " << stmt1; if (!q.exec(stmt1)) { ERROR_LOG(m_logger) << "Cannot update austtg_nr for participant: " << participantId << " in exercise: " << exerciseId << ", returnValue: " << returnValue; } else { INFO_LOG(m_logger) << "Update ausstg_nr successful for participant " << participantId << " in exercise: " << exerciseId; } } } } // namespace WsBaseDataInterface } // namespace WarehouseGuez