Files
qt5-legatus-legacy/Lts/Services/AudioVideoManagement/cnr_tvc_profile_handler.cpp
T
2026-08-13 18:33:10 +02:00

590 lines
19 KiB
C++
Executable File

/**
* 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 <AudioVideoManagement/avm_dts.h>
#include <ngldb/dbtransaction.h>
#include <QtGlobal>
#include <algorithm>
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<typename T>
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 &currentCircuitDb : 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<class Handler>
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<Database::AvmCnrTvcProfile> 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 &currentCircuitContainer: 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<Database::AvmCnrTvcProfileData> 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<Database::AvmCnrTvcProfileData> 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<Database::AvmCnrTvcProfileData> CnrTvcProfileHandler::insert(const std::vector<CnrTvcProfileData> &circuitsDds, qint64 containerId, bool &status)
{
status = false;
std::vector<Database::AvmCnrTvcProfileData> 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<Database::AvmCnrTvcProfileData> CnrTvcProfileHandler::determineInvalidEntries(const QVector<Database::AvmCnrTvcProfileData> &entries, int64_t exerciseId) const
{
QVector<Database::AvmCnrTvcProfileData> 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<CnrTvcProfile> 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