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

source code of qtscxml/src/scxml/qscxmlecmascriptdatamodel.cpp