File indexing completed on 2024-05-12 03:47:43

0001 /*
0002     File        : MQTTTopic.cpp
0003     Project     : LabPlot
0004     Description : Represents a topic of a MQTTSubscription
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2018 Kovacs Ferencz <kferike98@gmail.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include "backend/datasources/MQTTTopic.h"
0011 
0012 #include "backend/datasources/MQTTClient.h"
0013 #include "backend/datasources/MQTTSubscription.h"
0014 #include "backend/datasources/filters/AsciiFilter.h"
0015 #include "backend/lib/XmlStreamReader.h"
0016 #include "commonfrontend/spreadsheet/SpreadsheetView.h"
0017 #include "kdefrontend/spreadsheet/PlotDataDialog.h"
0018 
0019 #include <KLocalizedString>
0020 #include <QAction>
0021 #include <QIcon>
0022 #include <QMenu>
0023 
0024 /*!
0025   \class MQTTTopic
0026   \brief  Represents a topic of a subscription made in MQTTClient.
0027 
0028   \ingroup datasources
0029 */
0030 MQTTTopic::MQTTTopic(const QString& name, MQTTSubscription* subscription, bool loading)
0031     : Spreadsheet(name, loading, AspectType::MQTTTopic)
0032     , m_topicName(name)
0033     , m_MQTTClient(subscription->mqttClient())
0034     , m_filter(new AsciiFilter) {
0035     auto mainFilter = m_MQTTClient->filter();
0036 
0037     m_filter->setAutoModeEnabled(mainFilter->isAutoModeEnabled());
0038     if (!mainFilter->isAutoModeEnabled()) {
0039         m_filter->setCommentCharacter(mainFilter->commentCharacter());
0040         m_filter->setSeparatingCharacter(mainFilter->separatingCharacter());
0041         m_filter->setDateTimeFormat(mainFilter->dateTimeFormat());
0042         m_filter->setCreateIndexEnabled(mainFilter->createIndexEnabled());
0043         m_filter->setCreateTimestampEnabled(mainFilter->createTimestampEnabled());
0044         m_filter->setSimplifyWhitespacesEnabled(mainFilter->simplifyWhitespacesEnabled());
0045         m_filter->setNaNValueToZero(mainFilter->NaNValueToZeroEnabled());
0046         m_filter->setRemoveQuotesEnabled(mainFilter->removeQuotesEnabled());
0047         m_filter->setSkipEmptyParts(mainFilter->skipEmptyParts());
0048         m_filter->setHeaderEnabled(mainFilter->isHeaderEnabled());
0049         QString vectorNames;
0050         const QStringList& filterVectorNames = mainFilter->vectorNames();
0051         for (int i = 0; i < filterVectorNames.size(); ++i) {
0052             vectorNames.append(filterVectorNames.at(i));
0053             if (i != vectorNames.size() - 1)
0054                 vectorNames.append(QLatin1String(" "));
0055         }
0056 
0057         m_filter->setVectorNames(vectorNames);
0058         m_filter->setStartRow(mainFilter->startRow());
0059         m_filter->setEndRow(mainFilter->endRow());
0060         m_filter->setStartColumn(mainFilter->startColumn());
0061         m_filter->setEndColumn(mainFilter->endColumn());
0062     }
0063 
0064     connect(m_MQTTClient, &MQTTClient::readFromTopics, this, &MQTTTopic::read);
0065     qDebug() << "New MqttTopic: " << m_topicName;
0066     initActions();
0067 }
0068 
0069 MQTTTopic::~MQTTTopic() {
0070     qDebug() << "MqttTopic destructor:" << m_topicName;
0071     delete m_filter;
0072 }
0073 
0074 /*!
0075  *\brief Sets the MQTTTopic's filter
0076  * The ownership of the filter is passed to MQTTTopic.
0077  *
0078  * \param filter
0079  */
0080 void MQTTTopic::setFilter(AsciiFilter* f) {
0081     delete m_filter;
0082     m_filter = f;
0083 }
0084 
0085 /*!
0086  *\brief Returns the MQTTTopic's filter
0087  */
0088 AsciiFilter* MQTTTopic::filter() const {
0089     return m_filter;
0090 }
0091 
0092 /*!
0093  *\brief Returns the MQTTTopic's icon
0094  */
0095 QIcon MQTTTopic::icon() const {
0096     return QIcon::fromTheme(QStringLiteral("text-plain"));
0097 }
0098 
0099 /*!
0100  *\brief Adds an action to the MQTTTopic's context menu in the project explorer
0101  */
0102 QMenu* MQTTTopic::createContextMenu() {
0103     QMenu* menu = AbstractPart::createContextMenu();
0104 
0105     QAction* firstAction = nullptr;
0106     // if we're populating the context menu for the project explorer, then
0107     // there're already actions available there. Skip the first title-action
0108     // and insert the action at the beginning of the menu.
0109     if (menu->actions().size() > 1)
0110         firstAction = menu->actions().at(1);
0111 
0112     menu->insertAction(firstAction, m_plotDataAction);
0113     menu->insertSeparator(firstAction);
0114 
0115     return menu;
0116 }
0117 
0118 QWidget* MQTTTopic::view() const {
0119     if (!m_partView)
0120         m_partView = new SpreadsheetView(const_cast<MQTTTopic*>(this), true);
0121     return m_partView;
0122 }
0123 
0124 /*!
0125  *\brief Adds a message received by the topic to the message puffer
0126  */
0127 void MQTTTopic::newMessage(const QString& message) {
0128     m_messagePuffer.push_back(message);
0129 }
0130 
0131 /*!
0132  *\brief Returns the name of the MQTTTopic
0133  */
0134 QString MQTTTopic::topicName() const {
0135     return m_topicName;
0136 }
0137 
0138 /*!
0139  *\brief Initializes the actions of MQTTTopic
0140  */
0141 void MQTTTopic::initActions() {
0142     m_plotDataAction = new QAction(QIcon::fromTheme(QStringLiteral("office-chart-line")), i18n("Plot data"), this);
0143     connect(m_plotDataAction, &QAction::triggered, this, &MQTTTopic::plotData);
0144 }
0145 
0146 /*!
0147  *\brief Returns the MQTTClient the topic belongs to
0148  */
0149 MQTTClient* MQTTTopic::mqttClient() const {
0150     return m_MQTTClient;
0151 }
0152 
0153 // ##############################################################################
0154 // #################################  SLOTS  ####################################
0155 // ##############################################################################
0156 
0157 /*!
0158  *\brief Plots the data stored in MQTTTopic
0159  */
0160 void MQTTTopic::plotData() {
0161     auto* dlg = new PlotDataDialog(this);
0162     dlg->exec();
0163 }
0164 
0165 /*!
0166  *\brief Reads every message from the message puffer
0167  */
0168 void MQTTTopic::read() {
0169     while (!m_messagePuffer.isEmpty()) {
0170         qDebug() << "Reading from topic " << m_topicName;
0171         const QString tempMessage = m_messagePuffer.takeFirst();
0172         m_filter->readMQTTTopic(tempMessage, this);
0173     }
0174 }
0175 
0176 // ##############################################################################
0177 // ##################  Serialization/Deserialization  ###########################
0178 // ##############################################################################
0179 /*!
0180   Saves as XML.
0181  */
0182 void MQTTTopic::save(QXmlStreamWriter* writer) const {
0183     writer->writeStartElement(QStringLiteral("MQTTTopic"));
0184     writeBasicAttributes(writer);
0185     writeCommentElement(writer);
0186 
0187     // general
0188     writer->writeStartElement(QStringLiteral("general"));
0189     writer->writeAttribute(QStringLiteral("topicName"), m_topicName);
0190     writer->writeAttribute(QStringLiteral("filterPrepared"), QString::number(m_filter->isPrepared()));
0191     writer->writeAttribute(QStringLiteral("filterSeparator"), m_filter->separator());
0192     writer->writeAttribute(QStringLiteral("messagePufferSize"), QString::number(m_messagePuffer.size()));
0193     for (int i = 0; i < m_messagePuffer.count(); ++i)
0194         writer->writeAttribute(QStringLiteral("message") + QString::number(i), m_messagePuffer[i]);
0195     writer->writeEndElement();
0196 
0197     // filter
0198     m_filter->save(writer);
0199 
0200     // Columns
0201     for (auto* col : children<Column>(AbstractAspect::ChildIndexFlag::IncludeHidden))
0202         col->save(writer);
0203 
0204     writer->writeEndElement(); // MQTTTopic
0205 }
0206 
0207 /*!
0208   Loads from XML.
0209 */
0210 bool MQTTTopic::load(XmlStreamReader* reader, bool preview) {
0211     removeColumns(0, columnCount());
0212     if (!readBasicAttributes(reader))
0213         return false;
0214 
0215     bool isFilterPrepared = false;
0216     QString separator;
0217 
0218     QString attributeWarning = i18n("Attribute '%1' missing or empty, default value is used");
0219     QXmlStreamAttributes attribs;
0220     QString str;
0221 
0222     while (!reader->atEnd()) {
0223         reader->readNext();
0224         if (reader->isEndElement() && reader->name() == QLatin1String("MQTTTopic"))
0225             break;
0226 
0227         if (!reader->isStartElement())
0228             continue;
0229 
0230         if (reader->name() == QLatin1String("comment")) {
0231             if (!readCommentElement(reader))
0232                 return false;
0233         } else if (reader->name() == QLatin1String("general")) {
0234             attribs = reader->attributes();
0235 
0236             str = attribs.value(QStringLiteral("topicName")).toString();
0237             if (str.isEmpty())
0238                 reader->raiseWarning(attributeWarning.arg(QStringLiteral("'topicName'")));
0239             else {
0240                 m_topicName = str;
0241                 setName(str);
0242             }
0243 
0244             str = attribs.value(QStringLiteral("filterPrepared")).toString();
0245             if (str.isEmpty())
0246                 reader->raiseWarning(attributeWarning.arg(QStringLiteral("'filterPrepared'")));
0247             else {
0248                 isFilterPrepared = str.toInt();
0249             }
0250 
0251             str = attribs.value(QStringLiteral("filterSeparator")).toString();
0252             if (str.isEmpty())
0253                 reader->raiseWarning(attributeWarning.arg(QStringLiteral("'filterSeparator'")));
0254             else {
0255                 separator = str;
0256             }
0257 
0258             int pufferSize = 0;
0259             str = attribs.value(QStringLiteral("messagePufferSize")).toString();
0260             if (str.isEmpty())
0261                 reader->raiseWarning(attributeWarning.arg(QStringLiteral("'messagePufferSize'")));
0262             else
0263                 pufferSize = str.toInt();
0264             for (int i = 0; i < pufferSize; ++i) {
0265                 str = attribs.value(QStringLiteral("message") + QString::number(i)).toString();
0266                 if (str.isEmpty())
0267                     reader->raiseWarning(attributeWarning.arg(QStringLiteral("'message") + QString::number(i) + QLatin1Char('\'')));
0268                 else
0269                     m_messagePuffer.push_back(str);
0270             }
0271         } else if (reader->name() == QLatin1String("asciiFilter")) {
0272             if (!m_filter->load(reader))
0273                 return false;
0274         } else if (reader->name() == QLatin1String("column")) {
0275             Column* column = new Column(QString(), AbstractColumn::ColumnMode::Text);
0276             if (!column->load(reader, preview)) {
0277                 delete column;
0278                 setColumnCount(0);
0279                 return false;
0280             }
0281             addChild(column);
0282         } else { // unknown element
0283             reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString()));
0284             if (!reader->skipToEndElement())
0285                 return false;
0286         }
0287     }
0288 
0289     // prepare filter for reading
0290     m_filter->setPreparedForMQTT(isFilterPrepared, this, separator);
0291 
0292     return !reader->hasError();
0293 }