/** * CnrTvcProfileHandler - reads TetraChannel and TvcConfig data from the database and publishes the data to dds, * reacts to TvcConfig related Function topics * $Id$ * $HeadURL$ * $Author$ * Created by Gerlinde Rudolphus, SLSE, 2017-10-20 */ #include "cnr_tvc_profile_handler.h" #include "cnr_handler.h" #include "tvc_handler.h" #include #include #include #include namespace AudioVideoManagement { namespace { CnrTvcProfileData toDds(const Database::AvmCnrTvcProfileData &circuitDb) { // either cnrConfigId or tvcConfigId must be set if (!circuitDb.cnrConfigId.isNull()) { Q_ASSERT(circuitDb.tvcConfigId.isNull()); return CnrTvcProfileData(circuitDb.cnrConfigId.toInt(), RadioSystem::Cnr); } Q_ASSERT(!circuitDb.tvcConfigId.isNull()); return CnrTvcProfileData(circuitDb.tvcConfigId.toInt(), RadioSystem::Tvc); } template CnrTvcProfile toDds(const Database::AvmCnrTvcProfile &circuitContainerDb, T circuitsDb) { auto lessRadioCircuitsContent = [](const Database::AvmCnrTvcProfileData &lhs, const Database::AvmCnrTvcProfileData &rhs) { return lhs.sortIndex < rhs.sortIndex; }; // Bring the elements in the correct order as indicated by 'sort_index' column. std::sort(circuitsDb.begin(), circuitsDb.end(), lessRadioCircuitsContent); CnrTvcProfile circuitContainerDds(circuitContainerDb.id, circuitContainerDb.operatorId, circuitContainerDb.exerciseId, {}); circuitContainerDds.data().reserve(circuitsDb.size()); for (const auto ¤tCircuitDb : qAsConst(circuitsDb)) circuitContainerDds.data().push_back(toDds(currentCircuitDb)); return circuitContainerDds; } Database::AvmCnrTvcProfile toDb(const CnrTvcProfile &profileDds) { Database::AvmCnrTvcProfile profileDb; profileDb.id = profileDds.id(); profileDb.operatorId = profileDds.operatorId(); profileDb.exerciseId = profileDds.exerciseId(); return profileDb; } Database::AvmCnrTvcProfileData toDb(const CnrTvcProfileData &circuitDds, int32_t containerId, int32_t sortIndex) { Database::AvmCnrTvcProfileData circuitDb; circuitDb.containerId = containerId; circuitDb.sortIndex = sortIndex; switch (circuitDds.radioSystemId().underlying()) { case RadioSystem::Cnr: circuitDb.cnrConfigId = circuitDds.radioCircuitId(); return circuitDb; case RadioSystem::Tvc: circuitDb.tvcConfigId = circuitDds.radioCircuitId(); return circuitDb; } Q_ASSERT(false); return circuitDb; } /** * Helper function for CnrTvcProfileHandler::isValidCnr() and CnrTvcProfileHandler::isValidTvc(). * Handler is either CnrHandler or TvcHandler. */ template Core::FunctionReplyObject isValid(int configId, int64_t exerciseId, Handler *handler, int requestId, const QString &configTypeAsString) { Q_ASSERT(handler); Core::FunctionReplyObject reply; if (requestId > 0) reply.setId(requestId); if (!handler->hasConfig(configId)) return reply.setStr(tr_lts("dds", "%1 config %2 does not exist")).arg(configTypeAsString).arg(configId); const auto &config = handler->config(configId); if (config.exerciseId().is_set() && config.exerciseId().get() != exerciseId) return reply.setStr(tr_lts("dds", "%1 config %2 cannot be used in exercise %3")).arg(configTypeAsString).arg(configId).arg(exerciseId); return reply; } } // anonymous namespace CnrTvcProfileHandler::CnrTvcProfileHandler(DatabaseInterface *db, Interface::Dts *dts, Interface::Fts *fts, CnrHandler *cnrHandler, TvcHandler *tvcHandler) : ModifyCnrTvcProfileRequestHandler(nullptr) , RemoveCnrTvcProfileRequestHandler(nullptr) , m_db(db) , m_dts(dts) , m_fts(fts) , m_cnrHandler(cnrHandler) , m_tvcHandler(tvcHandler) , m_logger(getLogger("CnrTvcProfileHandler")) { Q_ASSERT(db); Q_ASSERT(dts); Q_ASSERT(fts); Q_ASSERT(cnrHandler); Q_ASSERT(tvcHandler); m_fts->setModifyCnrTvcProfileRequestHandler(this); m_fts->setRemoveCnrTvcProfileRequestHandler(this); connect(m_cnrHandler, &CnrHandler::cnrConfigChanged, this, &CnrTvcProfileHandler::onCnrConfigChanged); connect(m_tvcHandler, &TvcHandler::tvcConfigChanged, this, &CnrTvcProfileHandler::onTvcConfigChanged); connect(db, &DatabaseInterface::notifyDeleteCnrTvcProfile, this, &CnrTvcProfileHandler::onDbDeletedCnrTvcProfile); connect(db, &DatabaseInterface::notifyDeleteCnrTvcProfileData, this, &CnrTvcProfileHandler::onDbDeletedCnrTvcProfileData); } CnrTvcProfileHandler::~CnrTvcProfileHandler() { m_fts->setModifyCnrTvcProfileRequestHandler(nullptr); m_fts->setRemoveCnrTvcProfileRequestHandler(nullptr); } bool CnrTvcProfileHandler::init() { QVector circuitContainersDb; bool status = m_db->select(circuitContainersDb); if (!status) { ERROR_LOG(m_logger) << "Error reading data from database 'avm.avm_cnr_tvc_profile'"; return false; } for (const auto ¤tCircuitContainer: qAsConst(circuitContainersDb)) status &= collectAndPublish(currentCircuitContainer); return status; } /** * Fetch the entries from avm_cnr_tvc_profile_data for the given container, * build the corresponding CnrTvcProfile-Topic and publish. * * This method can be called in two cases: * 1. due to init() (isReload == false) * 2. as a consequence of the delete notification or a modification of a Cnr-/TvcConfig (isReload == true) */ bool CnrTvcProfileHandler::collectAndPublish(const Database::AvmCnrTvcProfile &circuitContainerDb, bool isReload) { if (isReload) { Q_ASSERT(m_cache.contains(circuitContainerDb.id)); Q_ASSERT(OperatorExerciseKey(m_cache.value(circuitContainerDb.id)) == OperatorExerciseKey(circuitContainerDb)); } QVector circuitsContentDb; bool status = m_db->selectByContainerId(circuitsContentDb, circuitContainerDb.id); if (!status) { ERROR_LOG(m_logger) << "Error reading data from database 'avm.avm_cnr_tvc_profile_data'"; return false; } // Check for invalid entries (where the exercise does not match) and remove those. const QVector invalidEntries = determineInvalidEntries(circuitsContentDb, circuitContainerDb.exerciseId); for (const auto &invalid : invalidEntries) { status = m_db->removeAvmCnrTvcProfileData(invalid.id); if (!status) ERROR_LOG(m_logger) << "Failed to remove from 'avm.avm_cnr_tvc_profile_data' with id " << invalid.id; auto newEnd = std::remove_if(circuitsContentDb.begin(), circuitsContentDb.end(), [&invalid] (const Database::AvmCnrTvcProfileData &circuit) { return circuit.id == invalid.id; }); circuitsContentDb.erase(newEnd, circuitsContentDb.end()); } // Check for possibly empty containers in the DB. // Those are deleted and not published. In case of a reload, the removal is published. if (circuitsContentDb.empty()) { status = m_db->removeAvmCnrTvcProfile(circuitContainerDb.id); if (!status) { ERROR_LOG(m_logger) << "Failed to remove from 'avm.avm_cnr_tvc_profile' with id " << circuitContainerDb.id; return false; } if (isReload) removeProfile(circuitContainerDb.id); return true; } CnrTvcProfile profileDds = toDds(circuitContainerDb, circuitsContentDb); if (isReload) updateProfile(profileDds); else addProfile(profileDds); return true; } /** * Modify is used for both: to create and modify a data set. * A request with request.id <= 0 is a create-request. */ void CnrTvcProfileHandler::onModifyCnrTvcProfileRequest(const rti::core::SampleIdentity &cookie, const CnrTvcProfile &request) { Core::FunctionReplyObject rep; if (request.id() <= 0) rep = createProfile(request); else rep = modifyProfile(request); if (rep.error()) WARNING_LOG(m_logger) << rep.str(); m_fts->modifyCnrTvcProfileReply(cookie, rep); } void CnrTvcProfileHandler::onRemoveCnrTvcProfileRequest(const rti::core::SampleIdentity &cookie, const Core::RemoveElementRequest &request) { Core::FunctionReplyObject rep = removeProfile(request); if (rep.error()) WARNING_LOG(m_logger) << rep.str(); m_fts->removeCnrTvcProfileReply(cookie, rep); } Core::FunctionReplyObject CnrTvcProfileHandler::createProfile(const CnrTvcProfile &createRequest) { Q_ASSERT(createRequest.id() <= 0); Core::FunctionReplyObject reply = validateRequest(createRequest); if (reply.error()) return reply; Database::AvmCnrTvcProfile dbEntry = toDb(createRequest); DBTransaction dbTransaction(m_db->database()); bool status = m_db->insert(dbEntry); if (!status) return reply.setStr(tr_lts("dds", "Failed to insert into 'avm.avm_cnr_tvc_profile'")); reply.setId(dbEntry.id); const auto circuitDataDb = insert(createRequest.data(), dbEntry.id, status); if (!status) return reply.setStr(tr_lts("dds", "Failed to insert into 'avm.avm_cnr_tvc_profile_data'")); dbTransaction.commit(); CnrTvcProfile toPublish = toDds(dbEntry, circuitDataDb); addProfile(toPublish); return reply; } Core::FunctionReplyObject CnrTvcProfileHandler::modifyProfile(const CnrTvcProfile &modifyRequest) { Q_ASSERT(modifyRequest.id() > 0); Core::FunctionReplyObject reply = validateRequest(modifyRequest); if (reply.error()) return reply; DBTransaction dbTransaction(m_db->database()); Database::AvmCnrTvcProfile dbEntry = toDb(modifyRequest); bool status = m_db->update(dbEntry); if (!status) return reply.setStr(tr_lts("dds", "Failed to update 'avm.avm_cnr_tvc_profile' data with id %1")).arg(modifyRequest.id()); status = m_db->removeAvmCnrTvcProfileDataByContainerId(modifyRequest.id()); if (!status) return reply.setStr(tr_lts("dds", "Failed to remove from 'avm.avm_cnr_tvc_profile_data' with container_id %1")).arg(modifyRequest.id()); insert(modifyRequest.data(), modifyRequest.id(), status); if (!status) return reply.setStr(tr_lts("dds", "Failed to insert into 'avm.avm_cnr_tvc_profile_data'")); dbTransaction.commit(); updateProfile(modifyRequest); return reply; } Core::FunctionReplyObject CnrTvcProfileHandler::removeProfile(const Core::RemoveElementRequest &request) { Core::FunctionReplyObject reply = validateRequest(request); if (reply.error()) return reply; DBTransaction dbTransaction(m_db->database()); bool status = m_db->removeAvmCnrTvcProfileDataByContainerId(request.id()); if (!status) return reply.setStr(tr_lts("dds", "Failed to remove from 'avm.avm_cnr_tvc_profile_data' with container_id %1")).arg(request.id()); status = m_db->removeAvmCnrTvcProfile(request.id()); if (!status) return reply.setStr(tr_lts("dds", "Failed to remove from 'avm.avm_cnr_tvc_profile' with id %1")).arg(request.id()); dbTransaction.commit(); removeProfile(request.id()); return reply; } /** * In case the bulk-insert into the DB fails, the returned vector is empty * and status is set to false. */ std::vector CnrTvcProfileHandler::insert(const std::vector &circuitsDds, qint64 containerId, bool &status) { status = false; std::vector circuitsDb; circuitsDb.reserve(circuitsDds.size()); // The order of circuits in the vector also indicates the user's requested sort order. int sortIndex = 0; for (const auto &circuitData: circuitsDds) circuitsDb.push_back(toDb(circuitData, containerId, sortIndex++)); status = m_db->insert(circuitsDb); if (!status) return {}; return circuitsDb; } void CnrTvcProfileHandler::addProfile(const CnrTvcProfile &profile) { Q_ASSERT(profile.id() > 0); Q_ASSERT(!m_cache.contains(profile.id())); OperatorExerciseKey key(profile); Q_ASSERT(!m_uniqueKeys.contains(key)); m_cache[profile.id()] = profile; m_uniqueKeys.append(key); bool status = m_dts->publish(profile); if (!status) ERROR_LOG(m_logger) << QStringLiteral("Failed to publish 'CnrTvcProfile' data with id %1 (create)").arg(profile.id()); } void CnrTvcProfileHandler::removeProfile(int64_t id) { Q_ASSERT(m_cache.contains(id)); CnrTvcProfile profile = m_cache.value(id); OperatorExerciseKey key(profile); m_cache.remove(id); int count = m_uniqueKeys.removeAll(key); Q_ASSERT(count == 1); bool status = m_dts->removeCnrTvcProfileByKey(id); if (!status) ERROR_LOG(m_logger) << QStringLiteral("Failed to publish 'CnrTvcProfile' with id %1 (remove)").arg(id); } void CnrTvcProfileHandler::updateProfile(const CnrTvcProfile &profile) { Q_ASSERT(profile.id() > 0); Q_ASSERT(m_cache.contains(profile.id())); OperatorExerciseKey key(profile); Q_ASSERT(m_uniqueKeys.contains(key)); if (m_cache[profile.id()] == profile) return; m_cache[profile.id()] = profile; bool status = m_dts->publish(profile); if (!status) ERROR_LOG(m_logger) << QStringLiteral("Failed to publish 'CnrTvcProfile' data with id %1 (modify)").arg(profile.id()); } void CnrTvcProfileHandler::onCnrConfigChanged(const CnrConfig &cnrConfig) { onConfigChanged(cnrConfig.id(), RadioSystem::Cnr); } void CnrTvcProfileHandler::onTvcConfigChanged(const TvcConfig &tvcConfig) { onConfigChanged(tvcConfig.id(), RadioSystem::Tvc); } /** * Will be called whenever a row from avm_cnr_tvc_profile is removed. * The remove-operation itself can be a result of the following: * 1. we ourselves are about to remove a row as a result of a RemoveRadioCircuitsRequest * 2. one of our foreign key dependencies was removed (exercise or operator) and our row * was removed as a result of a cascading delete * 3. someone removed the row by hand. */ void CnrTvcProfileHandler::onDbDeletedCnrTvcProfile(const QVariant &id) { bool couldConvert = false; qint64 dbId = id.toLongLong(&couldConvert); Q_ASSERT(couldConvert); if (!m_cache.contains(dbId)) return; // We don't have the id in our cache. We probably just removed the Row ourselves. => nothing to do. // We still have the id in our cache. Someone else must have deleted this row. // We remove the entry from our cache and publish the removal. removeProfile(dbId); } /** * Will be called whenever a row from avm_cnr_tvc_profile_data is removed. * Besides the causes already mentioned above (onDbDeletedCnrTvcProfile()) * this method can also be called as a result of a ModifyRadioCircuitRequest. * This is because the modify is implemented via deletion of all previous entries * and insertion of all new entries. * * @param containerId The id of the row in avm_cnr_tvc_profile where the deleted row * belongs to, not the primary key of the deleted row itself. */ void CnrTvcProfileHandler::onDbDeletedCnrTvcProfileData(const QVariant &containerId) { bool couldConvert = false; qint64 dbId = containerId.toLongLong(&couldConvert); Q_ASSERT(couldConvert); if (!m_cache.contains(dbId)) return; // We don't have the id in our cache. We probably just removed the Row ourselves. => nothing to do. // We still have the id in our cache. Someone else must have deleted this row. // We need to reload the whole container and publish the modification. // In case the container is now empty, we need to delete the container and publish the removal. Database::AvmCnrTvcProfile profileDb = toDb(m_cache.value(dbId)); const bool isReload = true; (void) collectAndPublish(profileDb, isReload); } Core::FunctionReplyObject CnrTvcProfileHandler::validateRequest(const CnrTvcProfile &request) const { Core::FunctionReplyObject reply; if (request.id() > 0) { // modify reply.setId(request.id()); if (!m_cache.contains(request.id())) return reply.setStr(tr_lts("dds", "CnrTvcProfile for operator %1 in exercise %2 does not exist")).arg(request.operatorId()).arg(request.exerciseId()); if (OperatorExerciseKey(m_cache.value(request.id())) != OperatorExerciseKey(request)) return reply.setStr(tr_lts("dds", "operator or exercise may not be changed")); } else { // create if (m_uniqueKeys.contains(OperatorExerciseKey(request))) return reply.setStr(tr_lts("dds", "CnrTvcProfile for operator %1 already exists in exercise %2")).arg(request.operatorId()).arg(request.exerciseId()); } if (request.data().size() == 0) return reply.setStr(tr_lts("dds", "CnrTvcProfile must not be empty")); for (const auto &entry : request.data()) { if (entry.radioSystemId() == RadioSystem::Tvc) { auto reply2 = isValidTvc(entry.radioCircuitId(), request.exerciseId(), request.id()); if (reply2.error()) return reply2; } else if (entry.radioSystemId() == RadioSystem::Cnr) { auto reply2 = isValidCnr(entry.radioCircuitId(), request.exerciseId(), request.id()); if (reply2.error()) return reply2; } } return reply; } Core::FunctionReplyObject CnrTvcProfileHandler::validateRequest(const Core::RemoveElementRequest &request) const { Q_ASSERT(request.id() > 0); Core::FunctionReplyObject reply(request.id()); if (!m_cache.contains(request.id())) return reply.setStr(tr_lts("dds", "CnrTvcProfile %1 does not exist")).arg(request.id()); return reply; } /** * Check whether all entries are valid in the context of the given exercise. * Return all invalid entries. */ QVector CnrTvcProfileHandler::determineInvalidEntries(const QVector &entries, int64_t exerciseId) const { QVector invalid; invalid.reserve(entries.size()); for (const auto &circuit : entries) { Q_ASSERT(circuit.cnrConfigId.isNull() ^ circuit.tvcConfigId.isNull()); if (!circuit.tvcConfigId.isNull()) { if (!isValidTvc(circuit.tvcConfigId.toInt(), exerciseId)) invalid.append(circuit); } else { if (!isValidCnr(circuit.cnrConfigId.toInt(), exerciseId)) invalid.append(circuit); } } return invalid; } bool CnrTvcProfileHandler::isValidCnr(int32_t cnrConfigId, int64_t exerciseId) const { int dummyRequestId = 0; return !isValidCnr(cnrConfigId, exerciseId, dummyRequestId).error(); } bool CnrTvcProfileHandler::isValidTvc(int32_t tvcConfigId, int64_t exerciseId) const { int dummyRequestId = 0; return !isValidTvc(tvcConfigId, exerciseId, dummyRequestId).error(); } Core::FunctionReplyObject CnrTvcProfileHandler::isValidCnr(int32_t cnrConfigId, int64_t exerciseId, int requestId) const { static const QString configType = QStringLiteral("CNR"); return isValid(cnrConfigId, exerciseId, m_cnrHandler, requestId, configType); } Core::FunctionReplyObject CnrTvcProfileHandler::isValidTvc(int32_t tvcConfigId, int64_t exerciseId, int requestId) const { static const QString configType = QStringLiteral("TVC"); return isValid(tvcConfigId, exerciseId, m_tvcHandler, requestId, configType); } void CnrTvcProfileHandler::onConfigChanged(int32_t configId, RadioSystem type) { QVector toBeReloaded; // Find all CnrTvcProfiles where this config is referenced and reload those. // Hint: reloading might modify m_cache. Thus the reloading is done in a separate loop. for (const auto &profile : qAsConst(m_cache)) for (const auto &data : profile.data()) if (data.radioSystemId() == type && data.radioCircuitId() == configId) { toBeReloaded.append(profile); break; } for (const auto &profile : qAsConst(toBeReloaded)) { Database::AvmCnrTvcProfile profileDb = toDb(profile); const bool isReload = true; (void) collectAndPublish(profileDb, isReload); } } } // namespace AudioVideoManagement