1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtScxml/private/qscxmlglobals_p.h>
5#include "qscxmlecmascriptdatamodel_p.h"
6#include "qscxmlecmascriptplatformproperties_p.h"
7#include <QtScxml/private/qscxmlexecutablecontent_p.h>
8#include <QtScxml/private/qscxmlstatemachine_p.h>
9#include <QtScxml/private/qscxmldatamodel_p.h>
10
11#include <qjsengine.h>
12#include <qjsondocument.h>
13#include <QtQml/private/qjsvalue_p.h>
14#include <QtQml/private/qv4scopedvalue_p.h>
15
16#include <functional>
17
18QT_BEGIN_NAMESPACE
19
20Q_LOGGING_CATEGORY(qscxmlEsLog, "qt.scxml.statemachine")
21
22using namespace QScxmlExecutableContent;
23
24typedef std::function<QString (bool *)> ToStringEvaluator;
25typedef std::function<bool (bool *)> ToBoolEvaluator;
26typedef std::function<QVariant (bool *)> ToVariantEvaluator;
27typedef std::function<void (bool *)> ToVoidEvaluator;
28typedef std::function<bool (bool *, std::function<bool ()>)> ForeachEvaluator;
29
30class QScxmlEcmaScriptDataModelPrivate : public QScxmlDataModelPrivate
31{
32 Q_DECLARE_PUBLIC(QScxmlEcmaScriptDataModel)
33public:
34 QScxmlEcmaScriptDataModelPrivate()
35 : jsEngine(nullptr)
36 {}
37
38 QString evalStr(const QString &expr, const QString &context, bool *ok)
39 {
40 QString script = QStringLiteral("(%1).toString()").arg(a: expr);
41 QJSValue v = eval(script, context, ok);
42 if (*ok)
43 return v.toString();
44 else
45 return QString();
46 }
47
48 bool evalBool(const QString &expr, const QString &context, bool *ok)
49 {
50 QString script = QStringLiteral("(function(){return !!(%1); })()").arg(a: expr);
51 QJSValue v = eval(script, context, ok);
52 if (*ok)
53 return v.toBool();
54 else
55 return false;
56 }
57
58 QJSValue evalJSValue(const QString &expr, const QString &context, bool *ok)
59 {
60 assertEngine();
61
62 QString script = QStringLiteral("(function(){'use strict'; return (\n%1\n); })()").arg(a: expr);
63 return eval(script, context, ok);
64 }
65
66 QJSValue eval(const QString &script, const QString &context, bool *ok)
67 {
68 Q_ASSERT(ok);
69 QJSEngine *engine = assertEngine();
70
71 // TODO: copy QJSEngine::evaluate and handle the case of v4->catchException() "our way"
72
73 QJSValue v = engine->evaluate(QStringLiteral("'use strict'; ") + script, QStringLiteral("<expr>"), lineNumber: 0);
74 if (v.isError()) {
75 *ok = false;
76 submitError(QStringLiteral("error.execution"),
77 QStringLiteral("%1 in %2").arg(args: v.toString(), args: context));
78 return QJSValue(QJSValue::UndefinedValue);
79 } else {
80 *ok = true;
81 return v;
82 }
83 }
84
85 void setupDataModel()
86 {
87 QJSEngine *engine = assertEngine();
88 dataModel = engine->globalObject();
89
90 qCDebug(qscxmlEsLog) << m_stateMachine << "initializing the datamodel";
91 setupSystemVariables();
92 }
93
94 void setupSystemVariables()
95 {
96 setReadonlyProperty(object: &dataModel, QStringLiteral("_sessionid"),
97 value: m_stateMachine->sessionId());
98
99 setReadonlyProperty(object: &dataModel, QStringLiteral("_name"), value: m_stateMachine->name());
100
101 QJSEngine *engine = assertEngine();
102 auto scxml = engine->newObject();
103 scxml.setProperty(QStringLiteral("location"), QStringLiteral("#_scxml_%1")
104 .arg(a: m_stateMachine->sessionId()));
105 auto ioProcs = engine->newObject();
106 setReadonlyProperty(object: &ioProcs, QStringLiteral("scxml"), value: scxml);
107 setReadonlyProperty(object: &dataModel, QStringLiteral("_ioprocessors"), value: ioProcs);
108
109 auto platformVars = QScxmlPlatformProperties::create(engine, stateMachine: m_stateMachine);
110 dataModel.setProperty(QStringLiteral("_x"), value: platformVars->jsValue());
111
112 dataModel.setProperty(QStringLiteral("In"), value: engine->evaluate(
113 QStringLiteral("(function(id){return _x.inState(id);})")));
114 }
115
116 void assignEvent(const QScxmlEvent &event)
117 {
118 if (event.name().isEmpty())
119 return;
120
121 QJSEngine *engine = assertEngine();
122 QJSValue _event = engine->newObject();
123 QJSValue dataValue = eventDataAsJSValue(eventData: event.data());
124 _event.setProperty(QStringLiteral("data"), value: dataValue.isUndefined() ? QJSValue(QJSValue::UndefinedValue)
125 : dataValue);
126 _event.setProperty(QStringLiteral("invokeid"), value: event.invokeId().isEmpty() ? QJSValue(QJSValue::UndefinedValue)
127 : engine->toScriptValue(value: event.invokeId()));
128 if (!event.originType().isEmpty())
129 _event.setProperty(QStringLiteral("origintype"), value: engine->toScriptValue(value: event.originType()));
130 _event.setProperty(QStringLiteral("origin"), value: event.origin().isEmpty() ? QJSValue(QJSValue::UndefinedValue)
131 : engine->toScriptValue(value: event.origin()) );
132 _event.setProperty(QStringLiteral("sendid"), value: event.sendId().isEmpty() ? QJSValue(QJSValue::UndefinedValue)
133 : engine->toScriptValue(value: event.sendId()));
134 _event.setProperty(QStringLiteral("type"), value: engine->toScriptValue(value: event.scxmlType()));
135 _event.setProperty(QStringLiteral("name"), value: engine->toScriptValue(value: event.name()));
136 _event.setProperty(QStringLiteral("raw"), QStringLiteral("unsupported")); // See test178
137 if (event.isErrorEvent())
138 _event.setProperty(QStringLiteral("errorMessage"), value: event.errorMessage());
139
140 setReadonlyProperty(object: &dataModel, QStringLiteral("_event"), value: _event);
141 }
142
143 QJSValue eventDataAsJSValue(const QVariant &eventData)
144 {
145 if (!eventData.isValid()) {
146 return QJSValue(QJSValue::UndefinedValue);
147 }
148
149 QJSEngine *engine = assertEngine();
150 if (eventData.canConvert<QVariantMap>()) {
151 auto keyValues = eventData.value<QVariantMap>();
152 auto data = engine->newObject();
153
154 for (QVariantMap::const_iterator it = keyValues.begin(), eit = keyValues.end(); it != eit; ++it) {
155 data.setProperty(name: it.key(), value: engine->toScriptValue(value: it.value()));
156 }
157
158 return data;
159 }
160
161 if (eventData == QVariant(QMetaType(QMetaType::VoidStar), nullptr)) {
162 return QJSValue(QJSValue::NullValue);
163 }
164
165 QString data = eventData.toString();
166 QJsonParseError err;
167 QJsonDocument doc = QJsonDocument::fromJson(json: data.toUtf8(), error: &err);
168 if (err.error == QJsonParseError::NoError)
169 return engine->toScriptValue(value: doc.toVariant());
170 else
171 return engine->toScriptValue(value: data);
172 }
173
174 QJSEngine *assertEngine()
175 {
176 if (!jsEngine) {
177 Q_Q(QScxmlEcmaScriptDataModel);
178 setEngine(new QJSEngine(q->stateMachine()));
179 }
180
181 return jsEngine;
182 }
183
184 QJSEngine *engine() const
185 {
186 return jsEngine;
187 }
188
189 void setEngine(QJSEngine *engine)
190 { jsEngine = engine; }
191
192 QString string(StringId id) const
193 {
194 return m_stateMachine->tableData()->string(id);
195 }
196
197 bool hasProperty(const QString &name) const
198 { return dataModel.hasProperty(name); }
199
200 QJSValue property(const QString &name) const
201 { return dataModel.property(name); }
202
203 bool setProperty(const QString &name, const QJSValue &value, const QString &context)
204 {
205 QString msg;
206 switch (setProperty(object: &dataModel, name, value)) {
207 case SetPropertySucceeded:
208 return true;
209 case SetReadOnlyPropertyFailed:
210 msg = QStringLiteral("cannot assign to read-only property %1 in %2");
211 break;
212 case SetUnknownPropertyFailed:
213 msg = QStringLiteral("cannot assign to unknown propety %1 in %2");
214 break;
215 case SetPropertyFailedForAnotherReason:
216 msg = QStringLiteral("assignment to property %1 failed in %2");
217 break;
218 default:
219 Q_UNREACHABLE();
220 }
221
222 submitError(QStringLiteral("error.execution"), msg: msg.arg(args: name, args: context));
223 return false;
224 }
225
226 void submitError(const QString &type, const QString &msg, const QString &sendid = QString())
227 {
228 QScxmlStateMachinePrivate::get(t: m_stateMachine)->submitError(type, msg, sendid);
229 }
230
231public:
232 QStringList initialDataNames;
233
234private: // Uses private API
235 static void setReadonlyProperty(QJSValue *object, const QString &name, const QJSValue &value)
236 {
237 qCDebug(qscxmlEsLog) << "setting read-only property" << name;
238 QV4::ExecutionEngine *engine = QJSValuePrivate::engine(jsval: object);
239 Q_ASSERT(engine);
240 QV4::Scope scope(engine);
241
242 QV4::ScopedObject o(scope, QJSValuePrivate::asManagedType<QV4::Object>(jsval: object));
243 if (!o)
244 return;
245
246 if (!QJSValuePrivate::checkEngine(e: engine, jsval: value)) {
247 qCWarning(qscxmlEsLog, "EcmaScriptDataModel::setReadonlyProperty(%s) failed: cannot set value created in a different engine", name.toUtf8().constData());
248 return;
249 }
250
251 QV4::ScopedString s(scope, engine->newString(s: name));
252 QV4::ScopedPropertyKey key(scope, s->toPropertyKey());
253 if (key->isArrayIndex()) {
254 Q_UNIMPLEMENTED();
255 return;
256 }
257
258 QV4::ScopedValue v(scope, QJSValuePrivate::convertToReturnedValue(e: engine, jsval: value));
259 o->defineReadonlyProperty(name: s, value: v);
260 if (engine->hasException)
261 engine->catchException();
262 }
263
264 enum SetPropertyResult {
265 SetPropertySucceeded,
266 SetReadOnlyPropertyFailed,
267 SetUnknownPropertyFailed,
268 SetPropertyFailedForAnotherReason,
269 };
270
271 static SetPropertyResult setProperty(QJSValue *object, const QString &name, const QJSValue &value)
272 {
273 QV4::ExecutionEngine *engine = QJSValuePrivate::engine(jsval: object);
274 Q_ASSERT(engine);
275 if (engine->hasException)
276 return SetPropertyFailedForAnotherReason;
277
278 QV4::Scope scope(engine);
279 QV4::ScopedObject o(scope, QJSValuePrivate::asManagedType<QV4::Object>(jsval: object));
280 if (o == nullptr) {
281 return SetPropertyFailedForAnotherReason;
282 }
283
284 QV4::ScopedString s(scope, engine->newString(s: name));
285 QV4::ScopedPropertyKey key(scope, s->toPropertyKey());
286 if (key->isArrayIndex()) {
287 Q_UNIMPLEMENTED();
288 return SetPropertyFailedForAnotherReason;
289 }
290
291 QV4::PropertyAttributes attrs = o->getOwnProperty(id: s->toPropertyKey());
292 if (attrs.isWritable() || attrs.isEmpty()) {
293 QV4::ScopedValue v(scope, QJSValuePrivate::convertToReturnedValue(e: engine, jsval: value));
294 o->insertMember(s, v);
295 if (engine->hasException) {
296 engine->catchException();
297 return SetPropertyFailedForAnotherReason;
298 } else {
299 return SetPropertySucceeded;
300 }
301 } else {
302 return SetReadOnlyPropertyFailed;
303 }
304 }
305
306private:
307 QJSEngine *jsEngine;
308 QJSValue dataModel;
309};
310
311/*
312 * The QScxmlEcmaScriptDataModel class is the ECMAScript data model for
313 * a Qt SCXML state machine.
314 *
315 * This class implements the ECMAScript data model as described in
316 * "SCXML Specification - B.2 The ECMAScript Data Model". It can be
317 * subclassed to perform custom initialization.
318 *
319 * See also QScxmlStateMachine QScxmlDataModel
320 */
321
322/*
323 * Creates a new ECMAScript data model, with the parent object parent.
324 */
325QScxmlEcmaScriptDataModel::QScxmlEcmaScriptDataModel(QObject *parent)
326 : QScxmlDataModel(*(new QScxmlEcmaScriptDataModelPrivate), parent)
327{}
328
329bool QScxmlEcmaScriptDataModel::setup(const QVariantMap &initialDataValues)
330{
331 Q_D(QScxmlEcmaScriptDataModel);
332 d->setupDataModel();
333
334 bool ok = true;
335 QJSValue undefined(QJSValue::UndefinedValue); // See B.2.1, and test456.
336 int count;
337 StringId *names = d->m_stateMachine->tableData()->dataNames(count: &count);
338 for (int i = 0; i < count; ++i) {
339 auto name = d->string(id: names[i]);
340 QJSValue v = undefined;
341 QVariantMap::const_iterator it = initialDataValues.find(key: name);
342 if (it != initialDataValues.end()) {
343 QJSEngine *engine = d->assertEngine();
344 v = engine->toScriptValue(value: it.value());
345 }
346 if (!d->setProperty(name, value: v, QStringLiteral("<data>"))) {
347 ok = false;
348 }
349 }
350 d->initialDataNames = initialDataValues.keys();
351
352 return ok;
353}
354
355QString QScxmlEcmaScriptDataModel::evaluateToString(QScxmlExecutableContent::EvaluatorId id,
356 bool *ok)
357{
358 Q_D(QScxmlEcmaScriptDataModel);
359 const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(evaluatorId: id);
360
361 return d->evalStr(expr: d->string(id: info.expr), context: d->string(id: info.context), ok);
362}
363
364bool QScxmlEcmaScriptDataModel::evaluateToBool(QScxmlExecutableContent::EvaluatorId id,
365 bool *ok)
366{
367 Q_D(QScxmlEcmaScriptDataModel);
368 const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(evaluatorId: id);
369
370 return d->evalBool(expr: d->string(id: info.expr), context: d->string(id: info.context), ok);
371}
372
373QVariant QScxmlEcmaScriptDataModel::evaluateToVariant(QScxmlExecutableContent::EvaluatorId id,
374 bool *ok)
375{
376 Q_D(QScxmlEcmaScriptDataModel);
377 const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(evaluatorId: id);
378
379 return d->evalJSValue(expr: d->string(id: info.expr), context: d->string(id: info.context), ok).toVariant();
380}
381
382void QScxmlEcmaScriptDataModel::evaluateToVoid(QScxmlExecutableContent::EvaluatorId id,
383 bool *ok)
384{
385 Q_D(QScxmlEcmaScriptDataModel);
386 const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(evaluatorId: id);
387
388 d->eval(script: d->string(id: info.expr), context: d->string(id: info.context), ok);
389}
390
391void QScxmlEcmaScriptDataModel::evaluateAssignment(QScxmlExecutableContent::EvaluatorId id,
392 bool *ok)
393{
394 Q_D(QScxmlEcmaScriptDataModel);
395 Q_ASSERT(ok);
396
397 const AssignmentInfo &info = d->m_stateMachine->tableData()->assignmentInfo(assignmentId: id);
398
399 QString dest = d->string(id: info.dest);
400
401 if (hasScxmlProperty(name: dest)) {
402 QJSValue v = d->evalJSValue(expr: d->string(id: info.expr), context: d->string(id: info.context), ok);
403 if (*ok)
404 *ok = d->setProperty(name: dest, value: v, context: d->string(id: info.context));
405 } else {
406 *ok = false;
407 d->submitError(QStringLiteral("error.execution"),
408 QStringLiteral("%1 in %2 does not exist").arg(args&: dest, args: d->string(id: info.context)));
409 }
410}
411
412void QScxmlEcmaScriptDataModel::evaluateInitialization(QScxmlExecutableContent::EvaluatorId id,
413 bool *ok)
414{
415 Q_D(QScxmlEcmaScriptDataModel);
416 const AssignmentInfo &info = d->m_stateMachine->tableData()->assignmentInfo(assignmentId: id);
417 QString dest = d->string(id: info.dest);
418 if (d->initialDataNames.contains(str: dest)) {
419 *ok = true; // silently ignore the <data> tag
420 return;
421 }
422
423 evaluateAssignment(id, ok);
424}
425
426void QScxmlEcmaScriptDataModel::evaluateForeach(QScxmlExecutableContent::EvaluatorId id, bool *ok,
427 ForeachLoopBody *body)
428{
429 Q_D(QScxmlEcmaScriptDataModel);
430 Q_ASSERT(ok);
431 Q_ASSERT(body);
432 const ForeachInfo &info = d->m_stateMachine->tableData()->foreachInfo(foreachId: id);
433
434 QJSValue jsArray = d->property(name: d->string(id: info.array));
435 if (!jsArray.isArray()) {
436 d->submitError(QStringLiteral("error.execution"), QStringLiteral("invalid array '%1' in %2").arg(args: d->string(id: info.array), args: d->string(id: info.context)));
437 *ok = false;
438 return;
439 }
440
441 QString item = d->string(id: info.item);
442
443 QJSEngine *engine = d->assertEngine();
444 if (engine->evaluate(QStringLiteral("(function(){var %1 = 0})()").arg(a: item)).isError()) {
445 d->submitError(QStringLiteral("error.execution"), QStringLiteral("invalid item '%1' in %2")
446 .arg(args: d->string(id: info.item), args: d->string(id: info.context)));
447 *ok = false;
448 return;
449 }
450
451 const int length = jsArray.property(QStringLiteral("length")).toInt();
452 QString idx = d->string(id: info.index);
453 QString context = d->string(id: info.context);
454 const bool hasIndex = !idx.isEmpty();
455
456 for (int currentIndex = 0; currentIndex < length; ++currentIndex) {
457 QJSValue currentItem = jsArray.property(arrayIndex: static_cast<quint32>(currentIndex));
458 *ok = d->setProperty(name: item, value: currentItem, context);
459 if (!*ok)
460 return;
461 if (hasIndex) {
462 *ok = d->setProperty(name: idx, value: currentIndex, context);
463 if (!*ok)
464 return;
465 }
466 body->run(ok);
467 if (!*ok)
468 return;
469 }
470 *ok = true;
471}
472
473void QScxmlEcmaScriptDataModel::setScxmlEvent(const QScxmlEvent &event)
474{
475 Q_D(QScxmlEcmaScriptDataModel);
476 d->assignEvent(event);
477}
478
479QVariant QScxmlEcmaScriptDataModel::scxmlProperty(const QString &name) const
480{
481 Q_D(const QScxmlEcmaScriptDataModel);
482 return d->property(name).toVariant();
483}
484
485bool QScxmlEcmaScriptDataModel::hasScxmlProperty(const QString &name) const
486{
487 Q_D(const QScxmlEcmaScriptDataModel);
488 return d->hasProperty(name);
489}
490
491bool QScxmlEcmaScriptDataModel::setScxmlProperty(const QString &name, const QVariant &value,
492 const QString &context)
493{
494 Q_D(QScxmlEcmaScriptDataModel);
495 Q_ASSERT(hasScxmlProperty(name));
496
497 QJSEngine *engine = d->assertEngine();
498 QJSValue v = engine->toScriptValue(
499 value: value.canConvert<QJSValue>() ? value.value<QJSValue>().toVariant() : value);
500 return d->setProperty(name, value: v, context);
501}
502
503QT_END_NAMESPACE
504

source code of qtscxml/src/plugins/ecmascriptdatamodel/qscxmlecmascriptdatamodel.cpp