KItinerary

extractorscriptengine.cpp
1/*
2 SPDX-FileCopyrightText: 2017-2021 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "extractorscriptengine_p.h"
8#include "extractordocumentnode.h"
9#include "extractordocumentprocessor.h"
10#include "extractorengine.h"
11#include "extractorresult.h"
12#include "scriptextractor.h"
13#include "logging.h"
14
15#include "jsapi/barcode.h"
16#include "jsapi/bytearray.h"
17#include "jsapi/extractorengine.h"
18#include "jsapi/jsonld.h"
19
20#include <QFile>
21#include <QJSEngine>
22#include <QJSValueIterator>
23#include <QScopeGuard>
24#include <QThread>
25#include <QTimer>
26
27using namespace KItinerary;
28
29namespace KItinerary {
30class ExtractorScriptEnginePrivate {
31public:
32 ~ExtractorScriptEnginePrivate();
33 bool loadScript(const QString &fileName);
34
35 JsApi::Barcode *m_barcodeApi = nullptr;
36 JsApi::JsonLd *m_jsonLdApi = nullptr;
37 JsApi::ExtractorEngine *m_engineApi = nullptr;
38 QJSEngine m_engine;
39
40 QThread m_watchdogThread;
41 QTimer *m_watchdogTimer = nullptr;
42};
43}
44
45ExtractorScriptEnginePrivate::~ExtractorScriptEnginePrivate()
46{
47 m_watchdogTimer->deleteLater();
48 m_watchdogThread.quit();
49 m_watchdogThread.wait();
50}
51
52ExtractorScriptEngine::ExtractorScriptEngine() = default;
53ExtractorScriptEngine::~ExtractorScriptEngine() = default;
54
55void ExtractorScriptEngine::ensureInitialized()
56{
57 if (d) {
58 return;
59 }
60
61 d = std::make_unique<ExtractorScriptEnginePrivate>();
62 d->m_engine.installExtensions(QJSEngine::ConsoleExtension);
63 d->m_jsonLdApi = new JsApi::JsonLd(&d->m_engine);
64 d->m_engine.globalObject().setProperty(QStringLiteral("JsonLd"), d->m_engine.newQObject(d->m_jsonLdApi));
65 d->m_barcodeApi = new JsApi::Barcode;
66 d->m_engine.globalObject().setProperty(QStringLiteral("Barcode"), d->m_engine.newQObject(d->m_barcodeApi));
67 d->m_engine.globalObject().setProperty(QStringLiteral("ByteArray"), d->m_engine.newQObject(new JsApi::ByteArray));
68 d->m_engineApi = new JsApi::ExtractorEngine(&d->m_engine);
69 d->m_engine.globalObject().setProperty(QStringLiteral("ExtractorEngine"), d->m_engine.newQObject(d->m_engineApi));
70
71 d->m_watchdogThread.start();
72 d->m_watchdogTimer = new QTimer;
73 d->m_watchdogTimer->setInterval(std::chrono::seconds(1));
74 d->m_watchdogTimer->setSingleShot(true);
75 d->m_watchdogTimer->moveToThread(&d->m_watchdogThread);
76 QObject::connect(d->m_watchdogTimer, &QTimer::timeout, &d->m_engine, [this]() { d->m_engine.setInterrupted(true); }, Qt::DirectConnection);
77}
78
79void ExtractorScriptEngine::setExtractorEngine(ExtractorEngine *engine)
80{
81 ensureInitialized();
82 d->m_engineApi->setEngine(engine);
83 d->m_barcodeApi->setDecoder(engine->barcodeDecoder());
84}
85
86// produce the same output as the JS engine error result fileName property would have
87static QString fileNameToUrl(const QString &fileName)
88{
89 if (fileName.startsWith(QLatin1Char(':'))) {
90 return QLatin1StringView("qrc:/") + QStringView(fileName).mid(1);
91 }
92 return QUrl::fromLocalFile(fileName).toString();
93}
94
95static void printScriptError(const QJSValue &result, const QString &fileNameFallback)
96{
97 const auto fileName = result.property(QStringLiteral("fileName"));
98 // don't change the formatting without adjusting KItinerary Workbench too!
99 qCWarning(Log).noquote().nospace() << "JS ERROR: [" << (fileName.isString() ? fileName.toString() : fileNameToUrl(fileNameFallback))
100 << "]:" << result.property(QStringLiteral("lineNumber")).toInt() << ": " << result.toString();
101}
102
103bool ExtractorScriptEnginePrivate::loadScript(const QString &fileName)
104{
105 // TODO we could skip this is if the right script is already loaded
106 // we cannot do this unconditionally however without breaking KItinerary Workbench's live editing
107 if (fileName.isEmpty()) {
108 return false;
109 }
110
111 QFile f(fileName);
112 if (!f.open(QFile::ReadOnly)) {
113 qCWarning(Log) << "Failed to open extractor script" << f.fileName() << f.errorString();
114 return false;
115 }
116
117 auto result = m_engine.evaluate(QString::fromUtf8(f.readAll()), f.fileName());
118 if (result.isError()) {
119 printScriptError(result, fileName);
120 return false;
121 }
122
123 return true;
124}
125
126ExtractorResult ExtractorScriptEngine::execute(const ScriptExtractor *extractor, const ExtractorDocumentNode &node, const ExtractorDocumentNode &triggerNode) const
127{
128 const_cast<ExtractorScriptEngine*>(this)->ensureInitialized();
129
130 // watchdog setup
131 QMetaObject::invokeMethod(d->m_watchdogTimer, qOverload<>(&QTimer::start));
132 const auto scopeCleanup = qScopeGuard([this]() {
133 QMetaObject::invokeMethod(d->m_watchdogTimer, qOverload<>(&QTimer::stop));
134 });
135 d->m_engine.setInterrupted(false);
136
137 if (!d->loadScript(extractor->scriptFileName())) {
138 return {};
139 }
140
141 auto mainFunc = d->m_engine.globalObject().property(extractor->scriptFunction());
142 if (!mainFunc.isCallable()) {
143 qCWarning(Log) << "Script entry point not found!" << extractor->scriptFunction();
144 return {};
145 }
146
147 qCDebug(Log) << "Running script extractor" << extractor->scriptFileName() << extractor->scriptFunction();
148 node.setScriptEngine(&d->m_engine);
149 triggerNode.setScriptEngine(&d->m_engine);
150 const auto engineReset = qScopeGuard([&node, &triggerNode, this]{
151 d->m_engineApi->clear();
152 node.setScriptEngine(nullptr);
153 triggerNode.setScriptEngine(nullptr);
154 });
155
156 d->m_jsonLdApi->setContextDate(node.contextDateTime());
157 d->m_engineApi->setCurrentNode(node);
158
159 const auto nodeArg = d->m_engine.toScriptValue(node);
160 const auto dataArg = nodeArg.property(QLatin1StringView("content"));
161 const auto triggerArg = d->m_engine.toScriptValue(triggerNode);
162 QJSValueList args{ dataArg, nodeArg, triggerArg };
163
164 const auto result = mainFunc.call(args);
165 if (result.isError()) {
166 printScriptError(result, extractor->scriptFileName());
167 return {};
168 }
169
170 QJsonArray out;
171 if (result.isArray()) {
172 QJSValueIterator it(result);
173 while (it.hasNext()) {
174 it.next();
175 if (it.value().isObject()) {
176 out.push_back(QJsonValue::fromVariant(it.value().toVariant()));
177 }
178 }
179 } else if (result.isObject()) {
181 } else {
182 qCWarning(Log) << "Invalid result type from script";
183 }
184
185 return out;
186}
A node in the extracted document object tree.
QDateTime contextDateTime
The best known context date/time at this point in the document tree.
Semantic data extraction engine.
const BarcodeDecoder * barcodeDecoder() const
Barcode decoder for use by KItinerary::ExtractorDocumentProcessor.
Generic extraction result.
Barcode decoding functions.
Definition barcode.h:23
API for dealing with QByteArray and/or JS ArrayBuffer objects.
Definition bytearray.h:21
API to access the extractor engine for JS extractor scripts.
Methods to create JSON-LD objects.
A single unstructured data extraction rule set.
QString scriptFunction() const
The JS function entry point for this extractor, main if empty.
QString scriptFileName() const
The JS script containing the code of the extractor.
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
QJSValue evaluate(const QString &program, const QString &fileName, int lineNumber, QStringList *exceptionStackTrace)
void push_back(const QJsonValue &value)
QJsonValue fromVariant(const QVariant &variant)
QJSValue call(const QJSValueList &args) const const
bool isArray() const const
bool isError() const const
bool isObject() const const
QJSValue property(const QString &name) const const
QString toString() const const
QVariant toVariant() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool setProperty(const char *name, QVariant &&value)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QStringView mid(qsizetype start, qsizetype length) const const
DirectConnection
void quit()
bool wait(QDeadlineTimer deadline)
void setInterval(int msec)
void start()
void stop()
void timeout()
QUrl fromLocalFile(const QString &localFile)
QString toString(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:49 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.