File indexing completed on 2024-05-19 05:17:46

0001 /*
0002     SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "extractoreditorwidget.h"
0008 #include "ui_extractoreditorwidget.h"
0009 
0010 #include "metaenumcombobox.h"
0011 
0012 #include <KItinerary/ExtractorFilter>
0013 #include <KItinerary/ExtractorRepository>
0014 #include <KItinerary/ScriptExtractor>
0015 
0016 #include <KTextEditor/Document>
0017 #include <KTextEditor/Editor>
0018 #include <KTextEditor/View>
0019 
0020 #include <KActionCollection>
0021 #include <KLocalizedString>
0022 
0023 #include <QAbstractTableModel>
0024 #include <QDebug>
0025 #include <QFile>
0026 #include <QFileInfo>
0027 #include <QFileDialog>
0028 #include <QItemEditorFactory>
0029 #include <QJsonArray>
0030 #include <QJsonDocument>
0031 #include <QJsonObject>
0032 #include <QMessageBox>
0033 #include <QMetaEnum>
0034 #include <QSettings>
0035 #include <QStandardPaths>
0036 #include <QStyledItemDelegate>
0037 
0038 using namespace KItinerary;
0039 
0040 class ExtractorFilterModel : public QAbstractTableModel
0041 {
0042     Q_OBJECT
0043 public:
0044     explicit ExtractorFilterModel(QObject *parent = nullptr);
0045     ~ExtractorFilterModel() = default;
0046 
0047     const std::vector<ExtractorFilter>& filters() const;
0048     void setFilters(std::vector<ExtractorFilter> filters);
0049     void addFilter();
0050     void removeFilter(int row);
0051 
0052     bool isReadOnly() const;
0053     void setReadOnly(bool ro);
0054 
0055     int columnCount(const QModelIndex &parent = {}) const override;
0056     int rowCount(const QModelIndex &parent = {}) const override;
0057     Qt::ItemFlags flags(const QModelIndex &index) const override;
0058     QVariant data(const QModelIndex &index, int role) const override;
0059     bool setData(const QModelIndex &index, const QVariant &value, int role) override;
0060     QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
0061 
0062 private:
0063     std::vector<ExtractorFilter> m_filters;
0064     bool m_readOnly = false;
0065 };
0066 
0067 ExtractorFilterModel::ExtractorFilterModel(QObject *parent)
0068     : QAbstractTableModel(parent)
0069 {
0070 }
0071 
0072 const std::vector<ExtractorFilter>& ExtractorFilterModel::filters() const
0073 {
0074     return m_filters;
0075 }
0076 
0077 void ExtractorFilterModel::setFilters(std::vector<ExtractorFilter> filters)
0078 {
0079     beginResetModel();
0080     m_filters = std::move(filters);
0081     endResetModel();
0082 }
0083 
0084 void ExtractorFilterModel::addFilter()
0085 {
0086     beginInsertRows({}, m_filters.size(), m_filters.size());
0087     ExtractorFilter f;
0088     f.setMimeType(QStringLiteral("text/plain"));
0089     f.setFieldName(i18n("<field>"));
0090     f.setPattern(i18n("<pattern>"));
0091     m_filters.push_back(f);
0092     endInsertRows();
0093 }
0094 
0095 void ExtractorFilterModel::removeFilter(int row)
0096 {
0097     beginRemoveRows({}, row, row);
0098     m_filters.erase(m_filters.begin() + row);
0099     endRemoveRows();
0100 }
0101 
0102 bool ExtractorFilterModel::isReadOnly() const
0103 {
0104     return m_readOnly;
0105 }
0106 
0107 void ExtractorFilterModel::setReadOnly(bool ro)
0108 {
0109     m_readOnly = ro;
0110     if (!m_filters.empty()) {
0111         Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
0112     }
0113 }
0114 
0115 int ExtractorFilterModel::columnCount(const QModelIndex &parent) const
0116 {
0117     Q_UNUSED(parent);
0118     return 4;
0119 }
0120 
0121 int ExtractorFilterModel::rowCount(const QModelIndex &parent) const
0122 {
0123     if (parent.isValid()) {
0124         return 0;
0125     }
0126     return m_filters.size();
0127 }
0128 
0129 Qt::ItemFlags ExtractorFilterModel::flags(const QModelIndex &index) const
0130 {
0131     if (m_readOnly) {
0132         return QAbstractTableModel::flags(index);
0133     }
0134     return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
0135 }
0136 
0137 QVariant ExtractorFilterModel::data(const QModelIndex &index, int role) const
0138 {
0139     if (role == Qt::DisplayRole) {
0140         const auto &filter = m_filters[index.row()];
0141         switch (index.column()) {
0142             case 0: return filter.mimeType();
0143             case 1: return QString::fromUtf8(QMetaEnum::fromType<ExtractorFilter::Scope>().valueToKey(filter.scope()));
0144             case 2: return filter.fieldName();
0145             case 3: return filter.pattern();
0146         }
0147     } else if (role == Qt::EditRole) {
0148         const auto &filter = m_filters[index.row()];
0149         switch (index.column()) {
0150             case 0: return filter.mimeType();
0151             case 1: return QVariant::fromValue(filter.scope());
0152             case 2: return filter.fieldName();
0153             case 3: return filter.pattern();
0154         }
0155     }
0156     return {};
0157 }
0158 
0159 bool ExtractorFilterModel::setData(const QModelIndex &index, const QVariant &value, int role)
0160 {
0161     if (!index.isValid() || role != Qt::EditRole) {
0162         return false;
0163     }
0164 
0165     auto &filter = m_filters[index.row()];
0166     switch (index.column()) {
0167         case 0:
0168             filter.setMimeType(value.toString());
0169             break;
0170         case 1:
0171             filter.setScope(static_cast<ExtractorFilter::Scope>(value.toInt()));
0172             break;
0173         case 2:
0174             filter.setFieldName(value.toString());
0175             break;
0176         case 3:
0177             filter.setPattern(value.toString());
0178             break;
0179     }
0180 
0181     Q_EMIT dataChanged(index, index);
0182     return true;
0183 }
0184 
0185 QVariant ExtractorFilterModel::headerData(int section, Qt::Orientation orientation, int role) const
0186 {
0187     if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
0188         switch (section) {
0189             case 0: return i18n("Type");
0190             case 1: return i18n("Scope");
0191             case 2: return i18n("Value");
0192             case 3: return i18n("Pattern");
0193         }
0194     }
0195     return QAbstractTableModel::headerData(section, orientation, role);
0196 }
0197 
0198 
0199 ExtractorEditorWidget::ExtractorEditorWidget(QWidget *parent)
0200     : QWidget(parent)
0201     , ui(new Ui::ExtractorEditorWidget)
0202     , m_filterModel(new ExtractorFilterModel(this))
0203 {
0204     ui->setupUi(this);
0205     ui->inputType->addItems({
0206         QStringLiteral("application/ld+json"),
0207         QStringLiteral("application/octet-stream"),
0208         QStringLiteral("application/pdf"),
0209         QStringLiteral("application/vnd.apple.pkpass"),
0210         QStringLiteral("internal/era-elb"),
0211         QStringLiteral("internal/era-ssb"),
0212         QStringLiteral("internal/event"),
0213         QStringLiteral("internal/uic9183"),
0214         QStringLiteral("internal/vdv"),
0215         QStringLiteral("message/rfc822"),
0216         QStringLiteral("text/calendar"),
0217         QStringLiteral("text/html"),
0218         QStringLiteral("text/plain")
0219     });
0220     ui->filterView->setModel(m_filterModel);
0221 
0222     QSettings settings;
0223     settings.beginGroup(QLatin1String("Extractor Repository"));
0224     ExtractorRepository repo;
0225     repo.setAdditionalSearchPaths(settings.value(QLatin1String("SearchPaths"), QStringList()).toStringList());
0226     repo.reload();
0227 
0228     connect(ui->extractorCombobox, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
0229         ExtractorRepository repo;
0230         const auto extId = ui->extractorCombobox->currentText();
0231         const auto extractor = dynamic_cast<const ScriptExtractor*>(repo.extractorByName(extId));
0232         if (!extractor) {
0233             return;
0234         }
0235         ui->scriptEdit->setText(extractor->scriptFileName());
0236         ui->functionEdit->setText(extractor->scriptFunction());
0237         ui->inputType->setCurrentIndex(ui->inputType->findText(extractor->mimeType()));
0238         m_filterModel->setFilters(extractor->filters());
0239         m_scriptDoc->openUrl(QUrl::fromLocalFile(extractor->scriptFileName()));
0240 
0241         QFileInfo scriptFi(extractor->fileName());
0242         m_scriptDoc->setReadWrite(scriptFi.isWritable());
0243         QFileInfo metaFi(extractor->fileName());
0244         setMetaDataReadOnly(!metaFi.isWritable());
0245         validateInput();
0246     });
0247 
0248     auto editor = KTextEditor::Editor::instance();
0249     m_scriptDoc = editor->createDocument(nullptr);
0250     m_scriptDoc->setHighlightingMode(QStringLiteral("JavaScript"));
0251     m_scriptView = m_scriptDoc->createView(nullptr);
0252     ui->topLayout->addWidget(m_scriptView);
0253     reloadExtractors();
0254 
0255     connect(m_scriptDoc, &KTextEditor::Document::modifiedChanged, this, [this]() {
0256         if (!m_scriptDoc->isModified()) { // approximation for "document has been saved"
0257             Q_EMIT extractorChanged();
0258         }
0259     });
0260 
0261     connect(ui->addFilterButton, &QToolButton::clicked, m_filterModel, &ExtractorFilterModel::addFilter);
0262     connect(ui->removeFilterButton, &QToolButton::clicked, this, [this]() {
0263         const auto sel = ui->filterView->selectionModel()->selection();
0264         if (sel.isEmpty()) {
0265             return;
0266         }
0267         m_filterModel->removeFilter(sel.first().topLeft().row());
0268         validateInput();
0269     });
0270 
0271     connect(ui->scriptEdit, &QLineEdit::textChanged, this, &ExtractorEditorWidget::validateInput);
0272     connect(ui->functionEdit, &QLineEdit::textChanged, this, &ExtractorEditorWidget::validateInput);
0273     connect(m_filterModel, &ExtractorFilterModel::rowsInserted, this, &ExtractorEditorWidget::validateInput);
0274     connect(m_filterModel, &ExtractorFilterModel::dataChanged, this, &ExtractorEditorWidget::validateInput);
0275     connect(ui->filterView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ExtractorEditorWidget::validateInput);
0276 
0277     auto factory = new QItemEditorFactory;
0278     factory->registerEditor(qMetaTypeId<ExtractorFilter::Scope>(), new QStandardItemEditorCreator<MetaEnumComboBox>());
0279     qobject_cast<QStyledItemDelegate*>(ui->filterView->itemDelegate())->setItemEditorFactory(factory);
0280 }
0281 
0282 ExtractorEditorWidget::~ExtractorEditorWidget() = default;
0283 
0284 void ExtractorEditorWidget::registerActions(KActionCollection *ac)
0285 {
0286     connect(ui->actionFileNewExtractor, &QAction::triggered, this, &ExtractorEditorWidget::create);
0287     connect(ui->actionFileSaveExtractor, &QAction::triggered, this, &ExtractorEditorWidget::save);
0288 
0289     ac->addAction(QStringLiteral("file_new_extractor"), ui->actionFileNewExtractor);
0290     ac->addAction(QStringLiteral("file_save_extractor"), ui->actionFileSaveExtractor);
0291 }
0292 
0293 void ExtractorEditorWidget::reloadExtractors()
0294 {
0295     ui->extractorCombobox->clear();
0296     ExtractorRepository repo;
0297     for (const auto &ext : repo.extractors()) {
0298         if (dynamic_cast<ScriptExtractor*>(ext.get())) {
0299             ui->extractorCombobox->addItem(ext->name());
0300         }
0301     }
0302 }
0303 
0304 void ExtractorEditorWidget::showExtractor(const QString &extractorId)
0305 {
0306     const auto idx = ui->extractorCombobox->findText(extractorId);
0307     if (idx >= 0 && idx != ui->extractorCombobox->currentIndex()) {
0308         ui->extractorCombobox->setCurrentIndex(idx);
0309     }
0310 }
0311 
0312 void ExtractorEditorWidget::navigateToSource(const QString &fileName, int line)
0313 {
0314     // TODO find the extractor this file belongs to and select it?
0315     if (m_scriptDoc->url().toString() != fileName) {
0316         m_scriptDoc->openUrl(QUrl(fileName));
0317     }
0318     m_scriptView->setCursorPosition(KTextEditor::Cursor(line - 1, 0));
0319 }
0320 
0321 void ExtractorEditorWidget::setMetaDataReadOnly(bool readOnly)
0322 {
0323     ui->inputType->setEnabled(!readOnly);
0324     ui->scriptEdit->setEnabled(!readOnly);
0325     ui->functionEdit->setEnabled(!readOnly);
0326     m_filterModel->setReadOnly(readOnly);
0327     ui->addFilterButton->setEnabled(!readOnly);
0328     ui->removeFilterButton->setEnabled(!readOnly);
0329 }
0330 
0331 void ExtractorEditorWidget::save()
0332 {
0333     ExtractorRepository repo;
0334     const auto extId = ui->extractorCombobox->currentText();
0335     auto extractor = const_cast<ScriptExtractor*>(dynamic_cast<const ScriptExtractor*>(repo.extractorByName(extId)));
0336     Q_ASSERT(extractor);
0337 
0338     extractor->setMimeType(ui->inputType->currentText());
0339     extractor->setScriptFileName(ui->scriptEdit->text());
0340     extractor->setScriptFunction(ui->functionEdit->text());
0341     extractor->setFilters(m_filterModel->filters());
0342 
0343     const auto val = repo.extractorToJson(extractor);
0344 
0345     QFile f(extractor->fileName());
0346     if (!f.open(QFile::WriteOnly)) {
0347         QMessageBox::critical(this, i18n("Saving Failed"), i18n("Failed to open file %1 for saving: %2", f.fileName(), f.errorString()));
0348         return;
0349     }
0350     f.write((val.isArray() ? QJsonDocument(val.toArray()) : QJsonDocument(val.toObject())).toJson());
0351     f.close();
0352     m_scriptDoc->save();
0353 
0354     repo.reload();
0355 }
0356 
0357 void ExtractorEditorWidget::create()
0358 {
0359     ExtractorRepository repo;
0360     QString startDir;
0361     if (!repo.additionalSearchPaths().empty()) {
0362         startDir = repo.additionalSearchPaths().at(0);
0363     } else {
0364         startDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kitinerary/extractors"), QStandardPaths::LocateDirectory);
0365     }
0366 
0367     const auto metaFileName = QFileDialog::getSaveFileName(this, i18n("Create New Extractor"), startDir, i18n("JSON (*.json)"), nullptr, QFileDialog::DontConfirmOverwrite);
0368     if (metaFileName.isEmpty()) {
0369         return;
0370     }
0371 
0372     QFileInfo metaFi(metaFileName);
0373     const QString scriptFileName = metaFi.path() + QLatin1Char('/') + metaFi.baseName() + QLatin1String(".js");
0374     QFile scriptFile(scriptFileName);
0375     if (!scriptFile.open(QFile::WriteOnly | QFile::Append)) {
0376         QMessageBox::critical(this, i18n("Creation Failed"), i18n("Failed to create file %1: %2", scriptFile.fileName(), scriptFile.errorString()));
0377         return;
0378     }
0379     scriptFile.write(R"(
0380 function main(content) {
0381     console.log(content);
0382 }
0383 )");
0384     scriptFile.close();
0385 
0386     ScriptExtractor extractor;
0387     extractor.load({}, metaFileName, std::numeric_limits<int>::max()); // use a certainly unused index, so this doesn't clash with existing ones in a multi-extractor file
0388     extractor.setScriptFileName(scriptFileName);
0389     extractor.setMimeType(QStringLiteral("text/plain"));
0390     ExtractorFilter filter;
0391     filter.setMimeType(QStringLiteral("message/rfc822"));
0392     filter.setFieldName(QStringLiteral("From"));
0393     filter.setPattern(QStringLiteral("@change-me.com"));
0394     extractor.setFilters({filter});
0395     QFile metaFile(metaFileName);
0396     if (!metaFile.open(QFile::WriteOnly)) {
0397         QMessageBox::critical(this, i18n("Creation Failed"), i18n("Failed to create file %1: %2", metaFile.fileName(), metaFile.errorString()));
0398         return;
0399     }
0400     const auto json = repo.extractorToJson(&extractor);
0401     metaFile.write((json.isArray() ? QJsonDocument(json.toArray()) : QJsonDocument(json.toObject())).toJson());
0402     metaFile.close();
0403 
0404     repo.reload();
0405     reloadExtractors();
0406     showExtractor(metaFi.baseName());
0407 }
0408 
0409 void ExtractorEditorWidget::validateInput()
0410 {
0411     bool valid = !ui->scriptEdit->text().isEmpty() && !ui->functionEdit->text().isEmpty();
0412     ui->actionFileSaveExtractor->setEnabled(valid);
0413     ui->removeFilterButton->setEnabled(m_filterModel->rowCount() > 1 && !m_filterModel->isReadOnly());
0414 }
0415 
0416 #include "extractoreditorwidget.moc"