blob: da4fe8412f582f67861cf6066dc894bd8144900b [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtScxml module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qscxmlglobals_p.h"
#include "qscxmlecmascriptdatamodel.h"
#include "qscxmlecmascriptplatformproperties_p.h"
#include "qscxmlexecutablecontent_p.h"
#include "qscxmlstatemachine_p.h"
#include "qscxmldatamodel_p.h"
#include <qjsengine.h>
#include <qjsondocument.h>
#include <QtQml/private/qjsvalue_p.h>
#include <QtQml/private/qv4scopedvalue_p.h>
#include <functional>
QT_BEGIN_NAMESPACE
using namespace QScxmlExecutableContent;
typedef std::function<QString (bool *)> ToStringEvaluator;
typedef std::function<bool (bool *)> ToBoolEvaluator;
typedef std::function<QVariant (bool *)> ToVariantEvaluator;
typedef std::function<void (bool *)> ToVoidEvaluator;
typedef std::function<bool (bool *, std::function<bool ()>)> ForeachEvaluator;
class QScxmlEcmaScriptDataModelPrivate : public QScxmlDataModelPrivate
{
Q_DECLARE_PUBLIC(QScxmlEcmaScriptDataModel)
public:
QScxmlEcmaScriptDataModelPrivate()
: jsEngine(nullptr)
{}
QString evalStr(const QString &expr, const QString &context, bool *ok)
{
QString script = QStringLiteral("(%1).toString()").arg(expr);
QJSValue v = eval(script, context, ok);
if (*ok)
return v.toString();
else
return QString();
}
bool evalBool(const QString &expr, const QString &context, bool *ok)
{
QString script = QStringLiteral("(function(){return !!(%1); })()").arg(expr);
QJSValue v = eval(script, context, ok);
if (*ok)
return v.toBool();
else
return false;
}
QJSValue evalJSValue(const QString &expr, const QString &context, bool *ok)
{
assertEngine();
QString script = QStringLiteral("(function(){'use strict'; return (\n%1\n); })()").arg(expr);
return eval(script, context, ok);
}
QJSValue eval(const QString &script, const QString &context, bool *ok)
{
Q_ASSERT(ok);
QJSEngine *engine = assertEngine();
// TODO: copy QJSEngine::evaluate and handle the case of v4->catchException() "our way"
QJSValue v = engine->evaluate(QStringLiteral("'use strict'; ") + script, QStringLiteral("<expr>"), 0);
if (v.isError()) {
*ok = false;
submitError(QStringLiteral("error.execution"),
QStringLiteral("%1 in %2").arg(v.toString(), context));
return QJSValue(QJSValue::UndefinedValue);
} else {
*ok = true;
return v;
}
}
void setupDataModel()
{
QJSEngine *engine = assertEngine();
dataModel = engine->globalObject();
qCDebug(qscxmlLog) << m_stateMachine << "initializing the datamodel";
setupSystemVariables();
}
void setupSystemVariables()
{
setReadonlyProperty(&dataModel, QStringLiteral("_sessionid"),
m_stateMachine->sessionId());
setReadonlyProperty(&dataModel, QStringLiteral("_name"), m_stateMachine->name());
QJSEngine *engine = assertEngine();
auto scxml = engine->newObject();
scxml.setProperty(QStringLiteral("location"), QStringLiteral("#_scxml_%1")
.arg(m_stateMachine->sessionId()));
auto ioProcs = engine->newObject();
setReadonlyProperty(&ioProcs, QStringLiteral("scxml"), scxml);
setReadonlyProperty(&dataModel, QStringLiteral("_ioprocessors"), ioProcs);
auto platformVars = QScxmlPlatformProperties::create(engine, m_stateMachine);
dataModel.setProperty(QStringLiteral("_x"), platformVars->jsValue());
dataModel.setProperty(QStringLiteral("In"), engine->evaluate(
QStringLiteral("(function(id){return _x.inState(id);})")));
}
void assignEvent(const QScxmlEvent &event)
{
if (event.name().isEmpty())
return;
QJSEngine *engine = assertEngine();
QJSValue _event = engine->newObject();
QJSValue dataValue = eventDataAsJSValue(event.data());
_event.setProperty(QStringLiteral("data"), dataValue.isUndefined() ? QJSValue(QJSValue::UndefinedValue)
: dataValue);
_event.setProperty(QStringLiteral("invokeid"), event.invokeId().isEmpty() ? QJSValue(QJSValue::UndefinedValue)
: engine->toScriptValue(event.invokeId()));
if (!event.originType().isEmpty())
_event.setProperty(QStringLiteral("origintype"), engine->toScriptValue(event.originType()));
_event.setProperty(QStringLiteral("origin"), event.origin().isEmpty() ? QJSValue(QJSValue::UndefinedValue)
: engine->toScriptValue(event.origin()) );
_event.setProperty(QStringLiteral("sendid"), event.sendId().isEmpty() ? QJSValue(QJSValue::UndefinedValue)
: engine->toScriptValue(event.sendId()));
_event.setProperty(QStringLiteral("type"), engine->toScriptValue(event.scxmlType()));
_event.setProperty(QStringLiteral("name"), engine->toScriptValue(event.name()));
_event.setProperty(QStringLiteral("raw"), QStringLiteral("unsupported")); // See test178
if (event.isErrorEvent())
_event.setProperty(QStringLiteral("errorMessage"), event.errorMessage());
setReadonlyProperty(&dataModel, QStringLiteral("_event"), _event);
}
QJSValue eventDataAsJSValue(const QVariant &eventData)
{
if (!eventData.isValid()) {
return QJSValue(QJSValue::UndefinedValue);
}
QJSEngine *engine = assertEngine();
if (eventData.canConvert<QVariantMap>()) {
auto keyValues = eventData.value<QVariantMap>();
auto data = engine->newObject();
for (QVariantMap::const_iterator it = keyValues.begin(), eit = keyValues.end(); it != eit; ++it) {
data.setProperty(it.key(), engine->toScriptValue(it.value()));
}
return data;
}
if (eventData == QVariant(QMetaType::VoidStar, 0)) {
return QJSValue(QJSValue::NullValue);
}
QString data = eventData.toString();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &err);
if (err.error == QJsonParseError::NoError)
return engine->toScriptValue(doc.toVariant());
else
return engine->toScriptValue(data);
}
QJSEngine *assertEngine()
{
if (!jsEngine) {
Q_Q(QScxmlEcmaScriptDataModel);
setEngine(new QJSEngine(q->stateMachine()));
}
return jsEngine;
}
QJSEngine *engine() const
{
return jsEngine;
}
void setEngine(QJSEngine *engine)
{ jsEngine = engine; }
QString string(StringId id) const
{
return m_stateMachine->tableData()->string(id);
}
bool hasProperty(const QString &name) const
{ return dataModel.hasProperty(name); }
QJSValue property(const QString &name) const
{ return dataModel.property(name); }
bool setProperty(const QString &name, const QJSValue &value, const QString &context)
{
QString msg;
switch (setProperty(&dataModel, name, value)) {
case SetPropertySucceeded:
return true;
case SetReadOnlyPropertyFailed:
msg = QStringLiteral("cannot assign to read-only property %1 in %2");
break;
case SetUnknownPropertyFailed:
msg = QStringLiteral("cannot assign to unknown propety %1 in %2");
break;
case SetPropertyFailedForAnotherReason:
msg = QStringLiteral("assignment to property %1 failed in %2");
break;
default:
Q_UNREACHABLE();
}
submitError(QStringLiteral("error.execution"), msg.arg(name, context));
return false;
}
void submitError(const QString &type, const QString &msg, const QString &sendid = QString())
{
QScxmlStateMachinePrivate::get(m_stateMachine)->submitError(type, msg, sendid);
}
public:
QStringList initialDataNames;
private: // Uses private API
static void setReadonlyProperty(QJSValue *object, const QString &name, const QJSValue &value)
{
qCDebug(qscxmlLog) << "setting read-only property" << name;
QV4::ExecutionEngine *engine = QJSValuePrivate::engine(object);
Q_ASSERT(engine);
QV4::Scope scope(engine);
QV4::ScopedObject o(scope, QJSValuePrivate::getValue(object));
if (!o)
return;
if (!QJSValuePrivate::checkEngine(engine, value)) {
qCWarning(qscxmlLog, "EcmaScriptDataModel::setReadonlyProperty(%s) failed: cannot set value created in a different engine", name.toUtf8().constData());
return;
}
QV4::ScopedString s(scope, engine->newString(name));
QV4::ScopedPropertyKey key(scope, s->toPropertyKey());
if (key->isArrayIndex()) {
Q_UNIMPLEMENTED();
return;
}
QV4::ScopedValue v(scope, QJSValuePrivate::convertedToValue(engine, value));
o->defineReadonlyProperty(s, v);
if (engine->hasException)
engine->catchException();
}
enum SetPropertyResult {
SetPropertySucceeded,
SetReadOnlyPropertyFailed,
SetUnknownPropertyFailed,
SetPropertyFailedForAnotherReason,
};
static SetPropertyResult setProperty(QJSValue *object, const QString &name, const QJSValue &value)
{
QV4::ExecutionEngine *engine = QJSValuePrivate::engine(object);
Q_ASSERT(engine);
if (engine->hasException)
return SetPropertyFailedForAnotherReason;
QV4::Scope scope(engine);
QV4::ScopedObject o(scope, QJSValuePrivate::getValue(object));
if (o == nullptr) {
return SetPropertyFailedForAnotherReason;
}
QV4::ScopedString s(scope, engine->newString(name));
QV4::ScopedPropertyKey key(scope, s->toPropertyKey());
if (key->isArrayIndex()) {
Q_UNIMPLEMENTED();
return SetPropertyFailedForAnotherReason;
}
QV4::PropertyAttributes attrs = o->getOwnProperty(s->toPropertyKey());
if (attrs.isWritable() || attrs.isEmpty()) {
QV4::ScopedValue v(scope, QJSValuePrivate::convertedToValue(engine, value));
o->insertMember(s, v);
if (engine->hasException) {
engine->catchException();
return SetPropertyFailedForAnotherReason;
} else {
return SetPropertySucceeded;
}
} else {
return SetReadOnlyPropertyFailed;
}
}
private:
QJSEngine *jsEngine;
QJSValue dataModel;
};
/*!
* \class QScxmlEcmaScriptDataModel
* \brief The QScxmlEcmaScriptDataModel class is the ECMAScript data model for
* a Qt SCXML state machine.
* \since 5.7
* \inmodule QtScxml
*
* This class implements the ECMAScript data model as described in
* \l {SCXML Specification - B.2 The ECMAScript Data Model}. It can be
* subclassed to perform custom initialization.
*
* \sa QScxmlStateMachine QScxmlDataModel
*/
/*!
* Creates a new ECMAScript data model, with the parent object \a parent.
*/
QScxmlEcmaScriptDataModel::QScxmlEcmaScriptDataModel(QObject *parent)
: QScxmlDataModel(*(new QScxmlEcmaScriptDataModelPrivate), parent)
{}
/*!
\reimp
*/
bool QScxmlEcmaScriptDataModel::setup(const QVariantMap &initialDataValues)
{
Q_D(QScxmlEcmaScriptDataModel);
d->setupDataModel();
bool ok = true;
QJSValue undefined(QJSValue::UndefinedValue); // See B.2.1, and test456.
int count;
StringId *names = d->m_stateMachine->tableData()->dataNames(&count);
for (int i = 0; i < count; ++i) {
auto name = d->string(names[i]);
QJSValue v = undefined;
QVariantMap::const_iterator it = initialDataValues.find(name);
if (it != initialDataValues.end()) {
QJSEngine *engine = d->assertEngine();
v = engine->toScriptValue(it.value());
}
if (!d->setProperty(name, v, QStringLiteral("<data>"))) {
ok = false;
}
}
d->initialDataNames = initialDataValues.keys();
return ok;
}
/*!
\reimp
*/
QString QScxmlEcmaScriptDataModel::evaluateToString(QScxmlExecutableContent::EvaluatorId id,
bool *ok)
{
Q_D(QScxmlEcmaScriptDataModel);
const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(id);
return d->evalStr(d->string(info.expr), d->string(info.context), ok);
}
/*!
\reimp
*/
bool QScxmlEcmaScriptDataModel::evaluateToBool(QScxmlExecutableContent::EvaluatorId id,
bool *ok)
{
Q_D(QScxmlEcmaScriptDataModel);
const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(id);
return d->evalBool(d->string(info.expr), d->string(info.context), ok);
}
/*!
\reimp
*/
QVariant QScxmlEcmaScriptDataModel::evaluateToVariant(QScxmlExecutableContent::EvaluatorId id,
bool *ok)
{
Q_D(QScxmlEcmaScriptDataModel);
const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(id);
return d->evalJSValue(d->string(info.expr), d->string(info.context), ok).toVariant();
}
/*!
\reimp
*/
void QScxmlEcmaScriptDataModel::evaluateToVoid(QScxmlExecutableContent::EvaluatorId id,
bool *ok)
{
Q_D(QScxmlEcmaScriptDataModel);
const EvaluatorInfo &info = d->m_stateMachine->tableData()->evaluatorInfo(id);
d->eval(d->string(info.expr), d->string(info.context), ok);
}
/*!
\reimp
*/
void QScxmlEcmaScriptDataModel::evaluateAssignment(QScxmlExecutableContent::EvaluatorId id,
bool *ok)
{
Q_D(QScxmlEcmaScriptDataModel);
Q_ASSERT(ok);
const AssignmentInfo &info = d->m_stateMachine->tableData()->assignmentInfo(id);
QString dest = d->string(info.dest);
if (hasScxmlProperty(dest)) {
QJSValue v = d->evalJSValue(d->string(info.expr), d->string(info.context), ok);
if (*ok)
*ok = d->setProperty(dest, v, d->string(info.context));
} else {
*ok = false;
d->submitError(QStringLiteral("error.execution"),
QStringLiteral("%1 in %2 does not exist").arg(dest, d->string(info.context)));
}
}
/*!
\reimp
*/
void QScxmlEcmaScriptDataModel::evaluateInitialization(QScxmlExecutableContent::EvaluatorId id,
bool *ok)
{
Q_D(QScxmlEcmaScriptDataModel);
const AssignmentInfo &info = d->m_stateMachine->tableData()->assignmentInfo(id);
QString dest = d->string(info.dest);
if (d->initialDataNames.contains(dest)) {
*ok = true; // silently ignore the <data> tag
return;
}
evaluateAssignment(id, ok);
}
/*!
\reimp
*/
void QScxmlEcmaScriptDataModel::evaluateForeach(QScxmlExecutableContent::EvaluatorId id, bool *ok,
ForeachLoopBody *body)
{
Q_D(QScxmlEcmaScriptDataModel);
Q_ASSERT(ok);
Q_ASSERT(body);
const ForeachInfo &info = d->m_stateMachine->tableData()->foreachInfo(id);
QJSValue jsArray = d->property(d->string(info.array));
if (!jsArray.isArray()) {
d->submitError(QStringLiteral("error.execution"), QStringLiteral("invalid array '%1' in %2").arg(d->string(info.array), d->string(info.context)));
*ok = false;
return;
}
QString item = d->string(info.item);
QJSEngine *engine = d->assertEngine();
if (engine->evaluate(QStringLiteral("(function(){var %1 = 0})()").arg(item)).isError()) {
d->submitError(QStringLiteral("error.execution"), QStringLiteral("invalid item '%1' in %2")
.arg(d->string(info.item), d->string(info.context)));
*ok = false;
return;
}
const int length = jsArray.property(QStringLiteral("length")).toInt();
QString idx = d->string(info.index);
QString context = d->string(info.context);
const bool hasIndex = !idx.isEmpty();
for (int currentIndex = 0; currentIndex < length; ++currentIndex) {
QJSValue currentItem = jsArray.property(static_cast<quint32>(currentIndex));
*ok = d->setProperty(item, currentItem, context);
if (!*ok)
return;
if (hasIndex) {
*ok = d->setProperty(idx, currentIndex, context);
if (!*ok)
return;
}
body->run(ok);
if (!*ok)
return;
}
*ok = true;
}
/*!
* \reimp
*/
void QScxmlEcmaScriptDataModel::setScxmlEvent(const QScxmlEvent &event)
{
Q_D(QScxmlEcmaScriptDataModel);
d->assignEvent(event);
}
/*!
* \reimp
*/
QVariant QScxmlEcmaScriptDataModel::scxmlProperty(const QString &name) const
{
Q_D(const QScxmlEcmaScriptDataModel);
return d->property(name).toVariant();
}
/*!
* \reimp
*/
bool QScxmlEcmaScriptDataModel::hasScxmlProperty(const QString &name) const
{
Q_D(const QScxmlEcmaScriptDataModel);
return d->hasProperty(name);
}
/*!
* \reimp
*/
bool QScxmlEcmaScriptDataModel::setScxmlProperty(const QString &name, const QVariant &value,
const QString &context)
{
Q_D(QScxmlEcmaScriptDataModel);
Q_ASSERT(hasScxmlProperty(name));
QJSEngine *engine = d->assertEngine();
QJSValue v = engine->toScriptValue(
value.canConvert<QJSValue>() ? value.value<QJSValue>().toVariant() : value);
return d->setProperty(name, v, context);
}
QT_END_NAMESPACE