diff options
author | Ulf Hermann <ulf.hermann@qt.io> | 2016-09-20 18:10:22 +0200 |
---|---|---|
committer | Ulf Hermann <ulf.hermann@qt.io> | 2016-10-14 10:53:05 +0000 |
commit | 1080cf471231fdde6daca775bbf77a04620f89e7 (patch) | |
tree | fbd0a736628b560d15a1f08dba08dafa3b197be7 /src/scxml/qscxmlcompiler.cpp | |
parent | bde17badd31d4990b1cc57f155c74c62b2ec9c72 (diff) |
Rename QScxmlParser to QScxmlCompiler
It creates an executable representation from an XML description, so
it is a compiler and not merely a parser. The name should reflect this.
Change-Id: I459445706207b3afae64b3f803c9beae83114bb6
Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io>
Reviewed-by: Erik Verbruggen <erik.verbruggen@qt.io>
Diffstat (limited to 'src/scxml/qscxmlcompiler.cpp')
-rw-r--r-- | src/scxml/qscxmlcompiler.cpp | 2505 |
1 files changed, 2505 insertions, 0 deletions
diff --git a/src/scxml/qscxmlcompiler.cpp b/src/scxml/qscxmlcompiler.cpp new file mode 100644 index 0000000..9a3af4a --- /dev/null +++ b/src/scxml/qscxmlcompiler.cpp @@ -0,0 +1,2505 @@ +/**************************************************************************** +** +** 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 "qscxmlcompiler_p.h" +#include "qscxmlexecutablecontent_p.h" + +#include <QXmlStreamReader> +#include <QLoggingCategory> +#include <QJsonDocument> +#include <QJsonObject> +#include <QFile> +#include <QVector> +#include <QString> + +#ifndef BUILD_QSCXMLC +#include "qscxmlecmascriptdatamodel.h" +#include "qscxmlinvokableservice.h" +#include "qscxmldatamodel_p.h" +#include "qscxmlstatemachine_p.h" +#include "qscxmlstatemachine.h" +#include "qscxmltabledata_p.h" + +#include <QState> +#include <QHistoryState> +#include <QEventTransition> +#include <QSignalTransition> +#include <QJSValue> +#include <private/qabstracttransition_p.h> +#include <private/qmetaobjectbuilder_p.h> +#endif // BUILD_QSCXMLC + +#include <functional> + +namespace { +enum { + DebugHelper_NameTransitions = 0 +}; +} // anonymous namespace + +QT_BEGIN_NAMESPACE + +static QString scxmlNamespace = QStringLiteral("http://www.w3.org/2005/07/scxml"); +static QString qtScxmlNamespace = QStringLiteral("http://theqtcompany.com/scxml/2015/06/"); + +namespace { + +class ScxmlVerifier: public DocumentModel::NodeVisitor +{ +public: + ScxmlVerifier(std::function<void (const DocumentModel::XmlLocation &, const QString &)> errorHandler) + : m_errorHandler(errorHandler) + , m_doc(Q_NULLPTR) + , m_hasErrors(false) + {} + + bool verify(DocumentModel::ScxmlDocument *doc) + { + if (doc->isVerified) + return true; + + doc->isVerified = true; + m_doc = doc; + for (DocumentModel::AbstractState *state : qAsConst(doc->allStates)) { + if (state->id.isEmpty()) { + continue; +#ifndef QT_NO_DEBUG + } else if (m_stateById.contains(state->id)) { + Q_ASSERT(!"Should be unreachable: the compiler should check for this case!"); +#endif // QT_NO_DEBUG + } else { + m_stateById[state->id] = state; + } + } + + if (doc->root) + doc->root->accept(this); + return !m_hasErrors; + } + +private: + bool visit(DocumentModel::Scxml *scxml) Q_DECL_OVERRIDE + { + if (!scxml->name.isEmpty() && !isValidToken(scxml->name, XmlNmtoken)) { + error(scxml->xmlLocation, + QStringLiteral("scxml name '%1' is not a valid XML Nmtoken").arg(scxml->name)); + } + + if (scxml->initial.isEmpty()) { + if (auto firstChild = firstAbstractState(scxml)) { + scxml->initialTransition = createInitialTransition({firstChild}); + } + } else { + QVector<DocumentModel::AbstractState *> initialStates; + for (const QString &initial : qAsConst(scxml->initial)) { + if (DocumentModel::AbstractState *s = m_stateById.value(initial)) + initialStates.append(s); + else + error(scxml->xmlLocation, QStringLiteral("initial state '%1' not found for <scxml> element").arg(initial)); + } + scxml->initialTransition = createInitialTransition(initialStates); + } + + m_parentNodes.append(scxml); + + return true; + } + + void endVisit(DocumentModel::Scxml *) Q_DECL_OVERRIDE + { + m_parentNodes.removeLast(); + } + + bool visit(DocumentModel::State *state) Q_DECL_OVERRIDE + { + if (!state->id.isEmpty() && !isValidToken(state->id, XmlNCName)) { + error(state->xmlLocation, QStringLiteral("'%1' is not a valid XML ID").arg(state->id)); + } + + if (state->initialTransition == nullptr) { + if (state->initial.isEmpty()) { + if (state->type == DocumentModel::State::Parallel) { + auto allChildren = allAbstractStates(state); + state->initialTransition = createInitialTransition(allChildren); + } else { + if (auto firstChild = firstAbstractState(state)) { + state->initialTransition = createInitialTransition({firstChild}); + } + } + } else { + Q_ASSERT(state->type == DocumentModel::State::Normal); + QVector<DocumentModel::AbstractState *> initialStates; + for (const QString &initialState : qAsConst(state->initial)) { + if (DocumentModel::AbstractState *s = m_stateById.value(initialState)) { + initialStates.append(s); + } else { + error(state->xmlLocation, + QStringLiteral("undefined initial state '%1' for state '%2'") + .arg(initialState, state->id)); + } + } + state->initialTransition = createInitialTransition(initialStates); + } + } else { + if (state->initial.isEmpty()) { + visit(state->initialTransition); + } else { + error(state->xmlLocation, + QStringLiteral("initial transition and initial attribute for state '%1'") + .arg(state->id)); + } + } + + switch (state->type) { + case DocumentModel::State::Normal: + break; + case DocumentModel::State::Parallel: + if (!state->initial.isEmpty()) { + error(state->xmlLocation, + QStringLiteral("parallel states cannot have an initial state")); + } + break; + case DocumentModel::State::Final: + break; + default: + Q_UNREACHABLE(); + } + + m_parentNodes.append(state); + return true; + } + + void endVisit(DocumentModel::State *) Q_DECL_OVERRIDE + { + m_parentNodes.removeLast(); + } + + bool visit(DocumentModel::Transition *transition) Q_DECL_OVERRIDE + { + Q_ASSERT(transition->targetStates.isEmpty()); + + if (int size = transition->targets.size()) + transition->targetStates.reserve(size); + for (const QString &target : qAsConst(transition->targets)) { + if (DocumentModel::AbstractState *s = m_stateById.value(target)) { + if (transition->targetStates.contains(s)) { + error(transition->xmlLocation, QStringLiteral("duplicate target '%1'").arg(target)); + } else { + transition->targetStates.append(s); + } + } else if (!target.isEmpty()) { + error(transition->xmlLocation, QStringLiteral("unknown state '%1' in target").arg(target)); + } + } + for (const QString &event : qAsConst(transition->events)) + checkEvent(event, transition->xmlLocation, AllowWildCards); + + m_parentNodes.append(transition); + return true; + } + + void endVisit(DocumentModel::Transition *) Q_DECL_OVERRIDE + { + m_parentNodes.removeLast(); + } + + bool visit(DocumentModel::HistoryState *state) Q_DECL_OVERRIDE + { + bool seenTransition = false; + for (DocumentModel::StateOrTransition *sot : qAsConst(state->children)) { + if (DocumentModel::State *s = sot->asState()) { + error(s->xmlLocation, QStringLiteral("history state cannot have substates")); + } else if (DocumentModel::Transition *t = sot->asTransition()) { + if (seenTransition) { + error(t->xmlLocation, QStringLiteral("history state can only have one transition")); + } else { + seenTransition = true; + m_parentNodes.append(state); + t->accept(this); + m_parentNodes.removeLast(); + } + } + } + + return false; + } + + bool visit(DocumentModel::Send *node) Q_DECL_OVERRIDE + { + checkEvent(node->event, node->xmlLocation, ForbidWildCards); + checkExpr(node->xmlLocation, QStringLiteral("send"), QStringLiteral("eventexpr"), node->eventexpr); + return true; + } + + void visit(DocumentModel::Cancel *node) Q_DECL_OVERRIDE + { + checkExpr(node->xmlLocation, QStringLiteral("cancel"), QStringLiteral("sendidexpr"), node->sendidexpr); + } + + bool visit(DocumentModel::DoneData *node) Q_DECL_OVERRIDE + { + checkExpr(node->xmlLocation, QStringLiteral("donedata"), QStringLiteral("expr"), node->expr); + return false; + } + + bool visit(DocumentModel::Invoke *node) Q_DECL_OVERRIDE + { + if (!node->srcexpr.isEmpty()) + return false; + + if (node->content.isNull()) { + error(node->xmlLocation, QStringLiteral("no valid content found in <invoke> tag")); + } else { + ScxmlVerifier subVerifier(m_errorHandler); + m_hasErrors = !subVerifier.verify(node->content.data()); + } + return false; + } + +private: + enum TokenType { + XmlNCName, + XmlNmtoken, + }; + + static bool isValidToken(const QString &id, TokenType tokenType) + { + Q_ASSERT(!id.isEmpty()); + int i = 0; + if (tokenType == XmlNCName) { + const QChar c = id.at(i++); + if (!isLetter(c) && c != QLatin1Char('_')) + return false; + } + for (int ei = id.length(); i != ei; ++i) { + const QChar c = id.at(i); + if (isLetter(c) || c.isDigit() || c == QLatin1Char('.') || c == QLatin1Char('-') + || c == QLatin1Char('_') || isNameTail(c)) + continue; + else if (tokenType == XmlNmtoken && c == QLatin1Char(':')) + continue; + else + return false; + } + + return true; + } + + static bool isLetter(QChar c) + { + switch (c.category()) { + case QChar::Letter_Lowercase: + case QChar::Letter_Uppercase: + case QChar::Letter_Other: + case QChar::Letter_Titlecase: + case QChar::Number_Letter: + return true; + default: + return false; + } + } + + static bool isNameTail(QChar c) + { + switch (c.category()) { + case QChar::Mark_SpacingCombining: + case QChar::Mark_Enclosing: + case QChar::Mark_NonSpacing: + case QChar::Letter_Modifier: + case QChar::Number_DecimalDigit: + return true; + default: + return false; + } + } + + enum WildCardMode { + ForbidWildCards, + AllowWildCards + }; + + void checkEvent(const QString &event, const DocumentModel::XmlLocation &loc, + WildCardMode wildCardMode) + { + if (event.isEmpty()) + return; + + if (!isValidEvent(event, wildCardMode)) { + error(loc, QStringLiteral("'%1' is not a valid event").arg(event)); + } + } + + static bool isValidEvent(const QString &event, WildCardMode wildCardMode) + { + if (event.isEmpty()) + return false; + + if (wildCardMode == AllowWildCards && event == QLatin1String(".*")) + return true; + + const QStringList parts = event.split(QLatin1Char('.')); + + for (const QString &part : parts) { + if (part.isEmpty()) + return false; + + if (wildCardMode == AllowWildCards && part.length() == 1 + && part.at(0) == QLatin1Char('*')) { + continue; + } + + for (int i = 0, ei = part.length(); i != ei; ++i) { + const QChar c = part.at(i); + if (!isLetter(c) && !c.isDigit() && c != QLatin1Char('-') && c != QLatin1Char('_') + && c != QLatin1Char(':')) { + return false; + } + } + } + + return true; + } + + static const QVector<DocumentModel::StateOrTransition *> &allChildrenOfContainer( + DocumentModel::StateContainer *container) + { + if (auto state = container->asState()) + return state->children; + else if (auto scxml = container->asScxml()) + return scxml->children; + else + Q_UNREACHABLE(); + } + + static DocumentModel::AbstractState *firstAbstractState(DocumentModel::StateContainer *container) + { + const auto &allChildren = allChildrenOfContainer(container); + + QVector<DocumentModel::AbstractState *> childStates; + for (DocumentModel::StateOrTransition *child : qAsConst(allChildren)) { + if (DocumentModel::State *s = child->asState()) + return s; + else if (DocumentModel::HistoryState *h = child->asHistoryState()) + return h; + } + return nullptr; + } + + static QVector<DocumentModel::AbstractState *> allAbstractStates( + DocumentModel::StateContainer *container) + { + const auto &allChildren = allChildrenOfContainer(container); + + QVector<DocumentModel::AbstractState *> childStates; + for (DocumentModel::StateOrTransition *child : qAsConst(allChildren)) { + if (DocumentModel::State *s = child->asState()) + childStates.append(s); + else if (DocumentModel::HistoryState *h = child->asHistoryState()) + childStates.append(h); + } + return childStates; + } + + DocumentModel::Transition *createInitialTransition( + const QVector<DocumentModel::AbstractState *> &states) + { + auto *newTransition = m_doc->newTransition(nullptr, DocumentModel::XmlLocation(-1, -1)); + newTransition->type = DocumentModel::Transition::Synthetic; + for (auto *s : states) { + newTransition->targets.append(s->id); + } + + newTransition->targetStates = states; + return newTransition; + } + + void checkExpr(const DocumentModel::XmlLocation &loc, const QString &tag, const QString &attrName, const QString &attrValue) + { + if (m_doc->root->dataModel == DocumentModel::Scxml::NullDataModel && !attrValue.isEmpty()) { + error(loc, QStringLiteral( + "%1 in <%2> cannot be used with data model 'null'").arg(attrName, tag)); + } + } + + void error(const DocumentModel::XmlLocation &location, const QString &message) + { + m_hasErrors = true; + if (m_errorHandler) + m_errorHandler(location, message); + } + +private: + std::function<void (const DocumentModel::XmlLocation &, const QString &)> m_errorHandler; + DocumentModel::ScxmlDocument *m_doc; + bool m_hasErrors; + QHash<QString, DocumentModel::AbstractState *> m_stateById; + QVector<DocumentModel::Node *> m_parentNodes; +}; + +#ifndef BUILD_QSCXMLC +class InvokeDynamicScxmlFactory: public QScxmlScxmlServiceFactory +{ +public: + InvokeDynamicScxmlFactory(QScxmlExecutableContent::StringId invokeLocation, + QScxmlExecutableContent::EvaluatorId srcexpr, + QScxmlExecutableContent::StringId id, + QScxmlExecutableContent::StringId idPrefix, + QScxmlExecutableContent::StringId idlocation, + const QVector<QScxmlExecutableContent::StringId> &namelist, + bool autoforward, + const QVector<QScxmlExecutableContent::ParameterInfo> ¶ms, + QScxmlExecutableContent::ContainerId finalize) + : QScxmlScxmlServiceFactory(invokeLocation, srcexpr, id, idPrefix, idlocation, + namelist, autoforward, params, finalize) + {} + + void setContent(const QSharedPointer<DocumentModel::ScxmlDocument> &content) + { m_content = content; } + + QScxmlInvokableService *invoke(QScxmlStateMachine *child) Q_DECL_OVERRIDE; + +private: + QSharedPointer<DocumentModel::ScxmlDocument> m_content; +}; + +class DynamicStateMachinePrivate : public QScxmlStateMachinePrivate +{ +public: + DynamicStateMachinePrivate() : + QScxmlStateMachinePrivate(&QScxmlStateMachine::staticMetaObject) {} +}; + +class DynamicStateMachine: public QScxmlStateMachine, public QScxmlInternal::GeneratedTableData +{ + Q_DECLARE_PRIVATE(DynamicStateMachine) + // Manually expanded from Q_OBJECT macro: +public: + Q_OBJECT_CHECK + + const QMetaObject *metaObject() const Q_DECL_OVERRIDE + { return d_func()->m_metaObject; } + + int qt_metacall(QMetaObject::Call _c, int _id, void **_a) Q_DECL_OVERRIDE + { + Q_D(DynamicStateMachine); + _id = QScxmlStateMachine::qt_metacall(_c, _id, _a); + if (_id < 0) + return _id; + int ownMethodCount = d->m_metaObject->methodCount() - d->m_metaObject->methodOffset(); + if (_c == QMetaObject::InvokeMetaMethod) { + if (_id < ownMethodCount) + qt_static_metacall(this, _c, _id, _a); + _id -= ownMethodCount; + } else if (_c == QMetaObject::ReadProperty || _c == QMetaObject::WriteProperty + || _c == QMetaObject::ResetProperty || _c == QMetaObject::RegisterPropertyMetaType) { + qt_static_metacall(this, _c, _id, _a); + _id -= d->m_metaObject->propertyCount(); + } + return _id; + } + +private: + static void qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) + { + if (_c == QMetaObject::RegisterPropertyMetaType) { + *reinterpret_cast<int*>(_a[0]) = qRegisterMetaType<bool>(); + } else if (_c == QMetaObject::ReadProperty) { + DynamicStateMachine *_t = static_cast<DynamicStateMachine *>(_o); + void *_v = _a[0]; + if (_id >= 0 && _id < _t->m_propertyCount) { + // getter for the state + *reinterpret_cast<bool*>(_v) = _t->isActive(_id); + } + } + } + // end of Q_OBJECT macro + +private: + DynamicStateMachine() + : QScxmlStateMachine(*new DynamicStateMachinePrivate) + , m_propertyCount(0) + { + // Temporarily wire up the QMetaObject + Q_D(DynamicStateMachine); + QMetaObjectBuilder b; + b.setClassName("DynamicStateMachine"); + b.setSuperClass(&QScxmlStateMachine::staticMetaObject); + b.setStaticMetacallFunction(qt_static_metacall); + d->m_metaObject = b.toMetaObject(); + } + + void initDynamicParts(const MetaDataInfo &info) + { + Q_D(DynamicStateMachine); + // Release the temporary QMetaObject. + Q_ASSERT(d->m_metaObject != &QScxmlStateMachine::staticMetaObject); + free(const_cast<QMetaObject *>(d->m_metaObject)); + d->m_metaObject = &QScxmlStateMachine::staticMetaObject; + + // Build the real one. + QMetaObjectBuilder b; + b.setClassName("DynamicStateMachine"); + b.setSuperClass(&QScxmlStateMachine::staticMetaObject); + b.setStaticMetacallFunction(qt_static_metacall); + + // signals + for (const QString &stateName : info.stateNames) { + auto name = stateName.toUtf8(); + const QByteArray signalName = name + "Changed(bool)"; + QMetaMethodBuilder signalBuilder = b.addSignal(signalName); + signalBuilder.setParameterNames(init("active")); + } + + // properties + int notifier = 0; + for (const QString &stateName : info.stateNames) { + QMetaPropertyBuilder prop = b.addProperty(stateName.toUtf8(), "bool", notifier); + prop.setWritable(false); + ++m_propertyCount; + ++notifier; + } + + // And we're done + d->m_metaObject = b.toMetaObject(); + } + +public: + ~DynamicStateMachine() + { + Q_D(DynamicStateMachine); + if (d->m_metaObject != &QScxmlStateMachine::staticMetaObject) { + free(const_cast<QMetaObject *>(d->m_metaObject)); + d->m_metaObject = &QScxmlStateMachine::staticMetaObject; + } + } + + QScxmlInvokableServiceFactory *serviceFactory(int id) const Q_DECL_OVERRIDE Q_DECL_FINAL + { return m_allFactoriesById.at(id); } + + static DynamicStateMachine *build(DocumentModel::ScxmlDocument *doc) + { + auto stateMachine = new DynamicStateMachine; + MetaDataInfo info; + DataModelInfo dm; + auto factoryIdCreator = [stateMachine](QScxmlExecutableContent::StringId invokeLocation, + QScxmlExecutableContent::EvaluatorId srcexpr, + QScxmlExecutableContent::StringId id, + QScxmlExecutableContent::StringId idPrefix, + QScxmlExecutableContent::StringId idlocation, + const QVector<QScxmlExecutableContent::StringId> &namelist, + bool autoforward, + const QVector<QScxmlExecutableContent::ParameterInfo> ¶ms, + QScxmlExecutableContent::ContainerId finalize, + const QSharedPointer<DocumentModel::ScxmlDocument> &content) -> int { + auto factory = new InvokeDynamicScxmlFactory(invokeLocation, + srcexpr, + id, + idPrefix, + idlocation, + namelist, + autoforward, + params, + finalize); + factory->setContent(content); + stateMachine->m_allFactoriesById.append(factory); + return stateMachine->m_allFactoriesById.size() - 1; + }; + + GeneratedTableData::build(doc, stateMachine, &info, &dm, factoryIdCreator); + stateMachine->setTableData(stateMachine); + stateMachine->initDynamicParts(info); + + return stateMachine; + } + +private: + static QList<QByteArray> init(const char *s) + { +#ifdef Q_COMPILER_INITIALIZER_LISTS + return QList<QByteArray>({ QByteArray::fromRawData(s, int(strlen(s))) }); +#else // insane compiler: + return QList<QByteArray>() << QByteArray::fromRawData(s, int(strlen(s))); +#endif + } + +private: + QVector<QScxmlInvokableServiceFactory *> m_allFactoriesById; + int m_propertyCount; +}; + +inline QScxmlInvokableService *InvokeDynamicScxmlFactory::invoke( + QScxmlStateMachine *parentStateMachine) +{ + bool ok = true; + auto srcexpr = calculateSrcexpr(parentStateMachine, &ok); + if (!ok) + return Q_NULLPTR; + + if (!srcexpr.isEmpty()) + return invokeDynamic(parentStateMachine, srcexpr); + + auto childStateMachine = DynamicStateMachine::build(m_content.data()); + + auto dm = QScxmlDataModelPrivate::instantiateDataModel(m_content->root->dataModel); + dm->setParent(childStateMachine); + childStateMachine->setDataModel(dm); + + return invokeStatic(childStateMachine, parentStateMachine); +} +#endif // BUILD_QSCXMLC + +} // anonymous namespace + +#ifndef BUILD_QSCXMLC +QScxmlScxmlService *QScxmlScxmlServiceFactory::invokeDynamic( + QScxmlStateMachine *parentStateMachine, const QString &sourceUrl) +{ + QScxmlCompiler::Loader *loader = parentStateMachine->loader(); + + const QString baseDir = sourceUrl.isEmpty() ? QString() : QFileInfo(sourceUrl).path(); + QStringList errs; + const QByteArray data = loader->load(sourceUrl, baseDir, &errs); + + if (!errs.isEmpty()) { + qWarning() << errs; + return Q_NULLPTR; + } + + QXmlStreamReader reader(data); + QScxmlCompiler compiler(&reader); + compiler.setFileName(sourceUrl); + compiler.setLoader(parentStateMachine->loader()); + compiler.compile(); + if (!compiler.errors().isEmpty()) { + const auto errors = compiler.errors(); + for (const QScxmlError &error : errors) + qWarning() << error.toString(); + return Q_NULLPTR; + } + + auto mainDoc = QScxmlCompilerPrivate::get(&compiler)->scxmlDocument(); + if (mainDoc == nullptr) { + Q_ASSERT(!compiler.errors().isEmpty()); + const auto errors = compiler.errors(); + for (const QScxmlError &error : errors) + qWarning() << error.toString(); + return Q_NULLPTR; + } + + auto childStateMachine = DynamicStateMachine::build(mainDoc); + + auto dm = QScxmlDataModelPrivate::instantiateDataModel(mainDoc->root->dataModel); + dm->setParent(childStateMachine); + childStateMachine->setDataModel(dm); + + return invokeStatic(childStateMachine, parentStateMachine); +} +#endif // BUILD_QSCXMLC + +/*! + * \class QScxmlCompiler + * \brief The QScxmlCompiler class is a compiler for SCXML files. + * \since 5.7 + * \inmodule QtScxml + * + * Parses an \l{SCXML Specification}{SCXML} file and dynamically instantiates a + * state machine for a successfully parsed SCXML file. If parsing fails, the + * new state machine cannot start. All errors are returned by + * QScxmlStateMachine::parseErrors(). + * + * To load an SCXML file, QScxmlStateMachine::fromFile or QScxmlStateMachine::fromData should be + * used. Using QScxmlCompiler directly is only needed when the compiler needs to use a custom + * QScxmlCompiler::Loader. + */ + +/*! + * Creates a new SCXML compiler for the specified \a reader. + */ +QScxmlCompiler::QScxmlCompiler(QXmlStreamReader *reader) + : d(new QScxmlCompilerPrivate(reader)) +{ } + +/*! + * Destroys the SCXML compiler. + */ +QScxmlCompiler::~QScxmlCompiler() +{ + delete d; +} + +/*! + * Returns the file name associated with the current input. + * + * \sa setFileName() + */ +QString QScxmlCompiler::fileName() const +{ + return d->fileName(); +} + +/*! + * Sets the file name for the current input to \a fileName. + * + * The file name is used for error reporting and for resolving relative path URIs. + * + * \sa fileName() + */ +void QScxmlCompiler::setFileName(const QString &fileName) +{ + d->setFileName(fileName); +} + +/*! + * Returns the loader that is currently used to resolve and load URIs for the + * SCXML compiler. + * + * \sa setLoader() + */ +QScxmlCompiler::Loader *QScxmlCompiler::loader() const +{ + return d->loader(); +} + +/*! + * Sets \a newLoader to be used for resolving and loading URIs for the SCXML + * compiler. + * + * \sa loader() + */ +void QScxmlCompiler::setLoader(QScxmlCompiler::Loader *newLoader) +{ + d->setLoader(newLoader); +} + +/*! + * Parses an SCXML file and creates a new state machine from it. + * + * If parsing is successful, the returned state machine can be initialized and started. If + * parsing fails, QScxmlStateMachine::parseErrors() can be used to retrieve a list of errors. + */ +QScxmlStateMachine *QScxmlCompiler::compile() +{ + d->readDocument(); + if (d->errors().isEmpty()) { + // Only verify the document if there were no parse errors: if there were any, the document + // is incomplete and will contain errors for sure. There is no need to heap more errors on + // top of other errors. + d->verifyDocument(); + } + return d->instantiateStateMachine(); +} + +/*! + * \internal + * Instantiates a new state machine from the parsed SCXML. + * + * If parsing is successful, the returned state machine can be initialized and started. If + * parsing fails, QScxmlStateMachine::parseErrors() can be used to retrieve a list of errors. + * + * \note The instantiated state machine will not have an associated data model set. + * \sa QScxmlCompilerPrivate::instantiateDataModel + */ +QScxmlStateMachine *QScxmlCompilerPrivate::instantiateStateMachine() const +{ +#ifdef BUILD_QSCXMLC + return Q_NULLPTR; +#else // BUILD_QSCXMLC + DocumentModel::ScxmlDocument *doc = scxmlDocument(); + if (doc && doc->root) { + auto stateMachine = DynamicStateMachine::build(doc); + instantiateDataModel(stateMachine); + return stateMachine; + } else { + class InvalidStateMachine: public QScxmlStateMachine { + public: + InvalidStateMachine() : QScxmlStateMachine(&QScxmlStateMachine::staticMetaObject) + {} + }; + + auto stateMachine = new InvalidStateMachine; + QScxmlStateMachinePrivate::get(stateMachine)->parserData()->m_errors = errors(); + instantiateDataModel(stateMachine); + return stateMachine; + } +#endif // BUILD_QSCXMLC +} + +/*! + * \internal + * Instantiates the data model as described in the SCXML file. + * + * After instantiation, the \a stateMachine takes ownership of the data model. + */ +void QScxmlCompilerPrivate::instantiateDataModel(QScxmlStateMachine *stateMachine) const +{ +#ifdef BUILD_QSCXMLC + Q_UNUSED(stateMachine) +#else + auto doc = scxmlDocument(); + auto root = doc ? doc->root : Q_NULLPTR; + if (root == Q_NULLPTR) { + qWarning() << "SCXML document has no root element"; + } else { + QScxmlDataModel *dm = QScxmlDataModelPrivate::instantiateDataModel(root->dataModel); + QScxmlStateMachinePrivate::get(stateMachine)->parserData()->m_ownedDataModel.reset(dm); + stateMachine->setDataModel(dm); + if (dm == Q_NULLPTR) + qWarning() << "No data-model instantiated"; + } +#endif // BUILD_QSCXMLC +} + +/*! + * Returns the list of parse errors. + */ +QVector<QScxmlError> QScxmlCompiler::errors() const +{ + return d->errors(); +} + +bool QScxmlCompilerPrivate::ParserState::collectChars() { + switch (kind) { + case Content: + case Data: + case Script: + return true; + default: + break; + } + return false; +} + +bool QScxmlCompilerPrivate::ParserState::validChild(ParserState::Kind child) const { + return validChild(kind, child); +} + +bool QScxmlCompilerPrivate::ParserState::validChild(ParserState::Kind parent, ParserState::Kind child) +{ + switch (parent) { + case ParserState::Scxml: + switch (child) { + case ParserState::State: + case ParserState::Parallel: + case ParserState::Final: + case ParserState::DataModel: + case ParserState::Script: + case ParserState::Transition: + return true; + default: + break; + } + return false; + case ParserState::State: + switch (child) { + case ParserState::OnEntry: + case ParserState::OnExit: + case ParserState::Transition: + case ParserState::Initial: + case ParserState::State: + case ParserState::Parallel: + case ParserState::Final: + case ParserState::History: + case ParserState::DataModel: + case ParserState::Invoke: + return true; + default: + break; + } + return false; + case ParserState::Parallel: + switch (child) { + case ParserState::OnEntry: + case ParserState::OnExit: + case ParserState::Transition: + case ParserState::State: + case ParserState::Parallel: + case ParserState::History: + case ParserState::DataModel: + case ParserState::Invoke: + return true; + default: + break; + } + return false; + case ParserState::Transition: + return isExecutableContent(child); + case ParserState::Initial: + return (child == ParserState::Transition); + case ParserState::Final: + switch (child) { + case ParserState::OnEntry: + case ParserState::OnExit: + case ParserState::DoneData: + return true; + default: + break; + } + return false; + case ParserState::OnEntry: + case ParserState::OnExit: + return isExecutableContent(child); + case ParserState::History: + return child == ParserState::Transition; + case ParserState::Raise: + return false; + case ParserState::If: + return child == ParserState::ElseIf || child == ParserState::Else + || isExecutableContent(child); + case ParserState::ElseIf: + case ParserState::Else: + return false; + case ParserState::Foreach: + return isExecutableContent(child); + case ParserState::Log: + return false; + case ParserState::DataModel: + return (child == ParserState::Data); + case ParserState::Data: + return false; + case ParserState::Assign: + return false; + case ParserState::DoneData: + case ParserState::Send: + return child == ParserState::Content || child == ParserState::Param; + case ParserState::Content: + return child == ParserState::Scxml || isExecutableContent(child); + case ParserState::Param: + case ParserState::Cancel: + return false; + case ParserState::Finalize: + return isExecutableContent(child); + case ParserState::Invoke: + return child == ParserState::Content || child == ParserState::Finalize + || child == ParserState::Param; + case ParserState::Script: + case ParserState::None: + break; + } + return false; +} + +bool QScxmlCompilerPrivate::ParserState::isExecutableContent(ParserState::Kind kind) { + switch (kind) { + case Raise: + case Send: + case Log: + case Script: + case Assign: + case If: + case Foreach: + case Cancel: + case Invoke: + return true; + default: + break; + } + return false; +} + +QScxmlCompilerPrivate::ParserState::Kind QScxmlCompilerPrivate::ParserState::nameToParserStateKind(const QStringRef &name) +{ + static QMap<QString, ParserState::Kind> nameToKind; + if (nameToKind.isEmpty()) { + nameToKind.insert(QLatin1String("scxml"), Scxml); + nameToKind.insert(QLatin1String("state"), State); + nameToKind.insert(QLatin1String("parallel"), Parallel); + nameToKind.insert(QLatin1String("transition"), Transition); + nameToKind.insert(QLatin1String("initial"), Initial); + nameToKind.insert(QLatin1String("final"), Final); + nameToKind.insert(QLatin1String("onentry"), OnEntry); + nameToKind.insert(QLatin1String("onexit"), OnExit); + nameToKind.insert(QLatin1String("history"), History); + nameToKind.insert(QLatin1String("raise"), Raise); + nameToKind.insert(QLatin1String("if"), If); + nameToKind.insert(QLatin1String("elseif"), ElseIf); + nameToKind.insert(QLatin1String("else"), Else); + nameToKind.insert(QLatin1String("foreach"), Foreach); + nameToKind.insert(QLatin1String("log"), Log); + nameToKind.insert(QLatin1String("datamodel"), DataModel); + nameToKind.insert(QLatin1String("data"), Data); + nameToKind.insert(QLatin1String("assign"), Assign); + nameToKind.insert(QLatin1String("donedata"), DoneData); + nameToKind.insert(QLatin1String("content"), Content); + nameToKind.insert(QLatin1String("param"), Param); + nameToKind.insert(QLatin1String("script"), Script); + nameToKind.insert(QLatin1String("send"), Send); + nameToKind.insert(QLatin1String("cancel"), Cancel); + nameToKind.insert(QLatin1String("invoke"), Invoke); + nameToKind.insert(QLatin1String("finalize"), Finalize); + } + QMap<QString, ParserState::Kind>::ConstIterator it = nameToKind.constBegin(); + const QMap<QString, ParserState::Kind>::ConstIterator itEnd = nameToKind.constEnd(); + while (it != itEnd) { + if (it.key() == name) + return it.value(); + ++it; + } + return None; +} + +QStringList QScxmlCompilerPrivate::ParserState::requiredAttributes(QScxmlCompilerPrivate::ParserState::Kind kind) +{ + switch (kind) { + case Scxml: return QStringList() << QStringLiteral("version"); + case State: return QStringList(); + case Parallel: return QStringList(); + case Transition: return QStringList(); + case Initial: return QStringList(); + case Final: return QStringList(); + case OnEntry: return QStringList(); + case OnExit: return QStringList(); + case History: return QStringList(); + case Raise: return QStringList() << QStringLiteral("event"); + case If: return QStringList() << QStringLiteral("cond"); + case ElseIf: return QStringList() << QStringLiteral("cond"); + case Else: return QStringList(); + case Foreach: return QStringList() << QStringLiteral("array") + << QStringLiteral("item"); + case Log: return QStringList(); + case DataModel: return QStringList(); + case Data: return QStringList() << QStringLiteral("id"); + case Assign: return QStringList() << QStringLiteral("location"); + case DoneData: return QStringList(); + case Content: return QStringList(); + case Param: return QStringList() << QStringLiteral("name"); + case Script: return QStringList(); + case Send: return QStringList(); + case Cancel: return QStringList(); + case Invoke: return QStringList(); + case Finalize: return QStringList(); + default: return QStringList(); + } + return QStringList(); +} + +QStringList QScxmlCompilerPrivate::ParserState::optionalAttributes(QScxmlCompilerPrivate::ParserState::Kind kind) +{ + switch (kind) { + case Scxml: return QStringList() << QStringLiteral("initial") + << QStringLiteral("datamodel") + << QStringLiteral("binding") + << QStringLiteral("name"); + case State: return QStringList() << QStringLiteral("id") + << QStringLiteral("initial"); + case Parallel: return QStringList() << QStringLiteral("id"); + case Transition: return QStringList() << QStringLiteral("event") + << QStringLiteral("cond") + << QStringLiteral("target") + << QStringLiteral("type"); + case Initial: return QStringList(); + case Final: return QStringList() << QStringLiteral("id"); + case OnEntry: return QStringList(); + case OnExit: return QStringList(); + case History: return QStringList() << QStringLiteral("id") + << QStringLiteral("type"); + case Raise: return QStringList(); + case If: return QStringList(); + case ElseIf: return QStringList(); + case Else: return QStringList(); + case Foreach: return QStringList() << QStringLiteral("index"); + case Log: return QStringList() << QStringLiteral("label") + << QStringLiteral("expr"); + case DataModel: return QStringList(); + case Data: return QStringList() << QStringLiteral("src") + << QStringLiteral("expr"); + case Assign: return QStringList() << QStringLiteral("expr"); + case DoneData: return QStringList(); + case Content: return QStringList() << QStringLiteral("expr"); + case Param: return QStringList() << QStringLiteral("expr") + << QStringLiteral("location"); + case Script: return QStringList() << QStringLiteral("src"); + case Send: return QStringList() << QStringLiteral("event") + << QStringLiteral("eventexpr") + << QStringLiteral("id") + << QStringLiteral("idlocation") + << QStringLiteral("type") + << QStringLiteral("typeexpr") + << QStringLiteral("namelist") + << QStringLiteral("delay") + << QStringLiteral("delayexpr") + << QStringLiteral("target") + << QStringLiteral("targetexpr"); + case Cancel: return QStringList() << QStringLiteral("sendid") + << QStringLiteral("sendidexpr"); + case Invoke: return QStringList() << QStringLiteral("type") + << QStringLiteral("typeexpr") + << QStringLiteral("src") + << QStringLiteral("srcexpr") + << QStringLiteral("id") + << QStringLiteral("idlocation") + << QStringLiteral("namelist") + << QStringLiteral("autoforward"); + case Finalize: return QStringList(); + default: return QStringList(); + } + return QStringList(); +} + +DocumentModel::Node::~Node() +{ +} + +DocumentModel::AbstractState *DocumentModel::Node::asAbstractState() +{ + if (State *state = asState()) + return state; + if (HistoryState *history = asHistoryState()) + return history; + return Q_NULLPTR; +} + +void DocumentModel::DataElement::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::Param::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::DoneData::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + for (Param *param : qAsConst(params)) + param->accept(visitor); + } + visitor->endVisit(this); +} + +void DocumentModel::Send::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(params); + } + visitor->endVisit(this); +} + +void DocumentModel::Invoke::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(params); + visitor->visit(&finalize); + } + visitor->endVisit(this); +} + +void DocumentModel::Raise::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::Log::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::Script::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::Assign::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::If::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(blocks); + } + visitor->endVisit(this); +} + +void DocumentModel::Foreach::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(&block); + } + visitor->endVisit(this); +} + +void DocumentModel::Cancel::accept(DocumentModel::NodeVisitor *visitor) +{ + visitor->visit(this); +} + +void DocumentModel::State::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(dataElements); + visitor->visit(children); + visitor->visit(onEntry); + visitor->visit(onExit); + if (doneData) + doneData->accept(visitor); + for (Invoke *invoke : qAsConst(invokes)) + invoke->accept(visitor); + } + visitor->endVisit(this); +} + +void DocumentModel::Transition::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(&instructionsOnTransition); + } + visitor->endVisit(this); +} + +void DocumentModel::HistoryState::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + if (Transition *t = defaultConfiguration()) + t->accept(visitor); + } + visitor->endVisit(this); +} + +void DocumentModel::Scxml::accept(DocumentModel::NodeVisitor *visitor) +{ + if (visitor->visit(this)) { + visitor->visit(children); + visitor->visit(dataElements); + if (script) + script->accept(visitor); + visitor->visit(&initialSetup); + } + visitor->endVisit(this); +} + +DocumentModel::NodeVisitor::~NodeVisitor() +{} + +/*! + * \class QScxmlCompiler::Loader + * \brief The Loader class is a URI resolver and resource loader for an SCXML compiler. + * \since 5.8 + * \inmodule QtScxml + */ + +/*! + * Creates a new loader. + */ +QScxmlCompiler::Loader::Loader() +{ +} + +/*! + * Destroys the loader. + */ +QScxmlCompiler::Loader::~Loader() +{} + +/*! + * \fn QScxmlCompiler::Loader::load(const QString &name, const QString &baseDir, QStringList *errors) + * Resolves the URI \a name and loads an SCXML file from the directory + * specified by \a baseDir. \a errors contains information about the errors that + * might have occurred. + * + * Returns a QByteArray that stores the contents of the file. + */ + +QScxmlCompilerPrivate *QScxmlCompilerPrivate::get(QScxmlCompiler *compiler) +{ + return compiler->d; +} + +QScxmlCompilerPrivate::QScxmlCompilerPrivate(QXmlStreamReader *reader) + : m_currentState(Q_NULLPTR) + , m_loader(&m_defaultLoader) + , m_reader(reader) +{} + +bool QScxmlCompilerPrivate::verifyDocument() +{ + if (!m_doc) + return false; + + auto handler = [this](const DocumentModel::XmlLocation &location, const QString &msg) { + this->addError(location, msg); + }; + + if (ScxmlVerifier(handler).verify(m_doc.data())) + return true; + else + return false; +} + +DocumentModel::ScxmlDocument *QScxmlCompilerPrivate::scxmlDocument() const +{ + return m_doc && m_errors.isEmpty() ? m_doc.data() : Q_NULLPTR; +} + +QString QScxmlCompilerPrivate::fileName() const +{ + return m_fileName; +} + +void QScxmlCompilerPrivate::setFileName(const QString &fileName) +{ + m_fileName = fileName; +} + +QScxmlCompiler::Loader *QScxmlCompilerPrivate::loader() const +{ + return m_loader; +} + +void QScxmlCompilerPrivate::setLoader(QScxmlCompiler::Loader *loader) +{ + m_loader = loader; +} + +void QScxmlCompilerPrivate::parseSubDocument(DocumentModel::Invoke *parentInvoke, + QXmlStreamReader *reader, + const QString &fileName) +{ + QScxmlCompiler p(reader); + p.setFileName(fileName); + p.setLoader(loader()); + p.d->readDocument(); + parentInvoke->content.reset(p.d->m_doc.take()); + m_doc->allSubDocuments.append(parentInvoke->content.data()); + m_errors.append(p.errors()); +} + +bool QScxmlCompilerPrivate::parseSubElement(DocumentModel::Invoke *parentInvoke, + QXmlStreamReader *reader, + const QString &fileName) +{ + QScxmlCompiler p(reader); + p.setFileName(fileName); + p.setLoader(loader()); + p.d->resetDocument(); + bool ok = p.d->readElement(); + parentInvoke->content.reset(p.d->m_doc.take()); + m_doc->allSubDocuments.append(parentInvoke->content.data()); + m_errors.append(p.errors()); + return ok; +} + +bool QScxmlCompilerPrivate::preReadElementScxml() +{ + if (m_doc->root) { + addError(QLatin1String("Doc root already allocated")); + return false; + } + m_doc->root = new DocumentModel::Scxml(xmlLocation()); + + auto scxml = m_doc->root; + const QXmlStreamAttributes attributes = m_reader->attributes(); + if (attributes.hasAttribute(QStringLiteral("initial"))) { + const QString initial = attributes.value(QStringLiteral("initial")).toString(); + scxml->initial += initial.split(QChar::Space, QString::SkipEmptyParts); + } + + const QStringRef datamodel = attributes.value(QLatin1String("datamodel")); + if (datamodel.isEmpty() || datamodel == QLatin1String("null")) { + scxml->dataModel = DocumentModel::Scxml::NullDataModel; + } else if (datamodel == QLatin1String("ecmascript")) { + scxml->dataModel = DocumentModel::Scxml::JSDataModel; + } else if (datamodel.startsWith(QLatin1String("cplusplus"))) { + scxml->dataModel = DocumentModel::Scxml::CppDataModel; + int firstColon = datamodel.indexOf(QLatin1Char(':')); + if (firstColon == -1) { + scxml->cppDataModelClassName = attributes.value(QStringLiteral("name")).toString() + QStringLiteral("DataModel"); + scxml->cppDataModelHeaderName = scxml->cppDataModelClassName + QStringLiteral(".h"); + } else { + int lastColon = datamodel.lastIndexOf(QLatin1Char(':')); + if (lastColon == -1) { + lastColon = datamodel.length(); + } else { + scxml->cppDataModelHeaderName = datamodel.mid(lastColon + 1).toString(); + } + scxml->cppDataModelClassName = datamodel.mid(firstColon + 1, lastColon - firstColon - 1).toString(); + } + } else { + addError(QStringLiteral("Unsupported data model '%1' in scxml") + .arg(datamodel.toString())); + } + const QStringRef binding = attributes.value(QLatin1String("binding")); + if (binding.isEmpty() || binding == QLatin1String("early")) { + scxml->binding = DocumentModel::Scxml::EarlyBinding; + } else if (binding == QLatin1String("late")) { + scxml->binding = DocumentModel::Scxml::LateBinding; + } else { + addError(QStringLiteral("Unsupperted binding type '%1'") + .arg(binding.toString())); + return false; + } + const QStringRef name = attributes.value(QLatin1String("name")); + if (!name.isEmpty()) { + scxml->name = name.toString(); + } + m_currentState = m_doc->root; + current().instructionContainer = &m_doc->root->initialSetup; + return true; +} + + +bool QScxmlCompilerPrivate::preReadElementState() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Normal, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + + if (attributes.hasAttribute(QStringLiteral("initial"))) { + const QString initial = attributes.value(QStringLiteral("initial")).toString(); + newState->initial += initial.split(QChar::Space, QString::SkipEmptyParts); + } + m_currentState = newState; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementParallel() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Parallel, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + + m_currentState = newState; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementInitial() +{ + DocumentModel::AbstractState *parent = currentParent(); + if (!parent) { + addError(QStringLiteral("<initial> found outside a state")); + return false; + } + + DocumentModel::State *parentState = parent->asState(); + if (!parentState) { + addError(QStringLiteral("<initial> found outside a state")); + return false; + } + + if (parentState->type == DocumentModel::State::Parallel) { + addError(QStringLiteral("Explicit initial state for parallel states not supported (only implicitly through the initial states of its substates)")); + return false; + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementTransition() +{ + // Parser stack at this point: + // <transition> + // <initial> + // <state> or <scxml> + // + // Or: + // <transition> + // <state> or <scxml> + + DocumentModel::Transition *transition = nullptr; + if (previous().kind == ParserState::Initial) { + transition = m_doc->newTransition(nullptr, xmlLocation()); + const auto &initialParentState = m_stack.at(m_stack.size() - 3); + if (initialParentState.kind == ParserState::Scxml) { + m_currentState->asScxml()->initialTransition = transition; + } else if (initialParentState.kind == ParserState::State) { + m_currentState->asState()->initialTransition = transition; + } else { + Q_UNREACHABLE(); + } + } else { + transition = m_doc->newTransition(m_currentState, xmlLocation()); + } + + const QXmlStreamAttributes attributes = m_reader->attributes(); + transition->events = attributes.value(QLatin1String("event")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + transition->targets = attributes.value(QLatin1String("target")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + if (attributes.hasAttribute(QStringLiteral("cond"))) + transition->condition.reset(new QString(attributes.value(QLatin1String("cond")).toString())); + QStringRef type = attributes.value(QLatin1String("type")); + if (type.isEmpty() || type == QLatin1String("external")) { + transition->type = DocumentModel::Transition::External; + } else if (type == QLatin1String("internal")) { + transition->type = DocumentModel::Transition::Internal; + } else { + addError(QStringLiteral("invalid transition type '%1', valid values are 'external' and 'internal'").arg(type.toString())); + return true; // TODO: verify me + } + current().instructionContainer = &transition->instructionsOnTransition; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementFinal() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Final, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + m_currentState = newState; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementHistory() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + + DocumentModel::AbstractState *parent = currentParent(); + if (!parent) { + addError(QStringLiteral("<history> found outside a state")); + return false; + } + auto newState = m_doc->newHistoryState(parent, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + + const QStringRef type = attributes.value(QLatin1String("type")); + if (type.isEmpty() || type == QLatin1String("shallow")) { + newState->type = DocumentModel::HistoryState::Shallow; + } else if (type == QLatin1String("deep")) { + newState->type = DocumentModel::HistoryState::Deep; + } else { + addError(QStringLiteral("invalid history type %1, valid values are 'shallow' and 'deep'").arg(type.toString())); + return false; + } + m_currentState = newState; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementOnEntry() +{ + const ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::Final: + case ParserState::State: + case ParserState::Parallel: + if (DocumentModel::State *s = m_currentState->asState()) { + current().instructionContainer = m_doc->newSequence(&s->onEntry); + break; + } + // intentional fall-through + default: + addError(QStringLiteral("unexpected container state for onentry")); + break; + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementOnExit() +{ + ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::Final: + case ParserState::State: + case ParserState::Parallel: + if (DocumentModel::State *s = m_currentState->asState()) { + current().instructionContainer = m_doc->newSequence(&s->onExit); + break; + } + // intentional fall-through + default: + addError(QStringLiteral("unexpected container state for onexit")); + break; + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementRaise() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto raise = m_doc->newNode<DocumentModel::Raise>(xmlLocation()); + raise->event = attributes.value(QLatin1String("event")).toString(); + current().instruction = raise; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementIf() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *ifI = m_doc->newNode<DocumentModel::If>(xmlLocation()); + current().instruction = ifI; + ifI->conditions.append(attributes.value(QLatin1String("cond")).toString()); + current().instructionContainer = m_doc->newSequence(&ifI->blocks); + return true; +} + +bool QScxmlCompilerPrivate::preReadElementElseIf() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + + DocumentModel::If *ifI = lastIf(); + if (!ifI) + return false; + + ifI->conditions.append(attributes.value(QLatin1String("cond")).toString()); + previous().instructionContainer = m_doc->newSequence(&ifI->blocks); + return true; +} + +bool QScxmlCompilerPrivate::preReadElementElse() +{ + DocumentModel::If *ifI = lastIf(); + if (!ifI) + return false; + + previous().instructionContainer = m_doc->newSequence(&ifI->blocks); + return true; +} + +bool QScxmlCompilerPrivate::preReadElementForeach() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto foreachI = m_doc->newNode<DocumentModel::Foreach>(xmlLocation()); + foreachI->array = attributes.value(QLatin1String("array")).toString(); + foreachI->item = attributes.value(QLatin1String("item")).toString(); + foreachI->index = attributes.value(QLatin1String("index")).toString(); + current().instruction = foreachI; + current().instructionContainer = &foreachI->block; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementLog() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto logI = m_doc->newNode<DocumentModel::Log>(xmlLocation()); + logI->label = attributes.value(QLatin1String("label")).toString(); + logI->expr = attributes.value(QLatin1String("expr")).toString(); + current().instruction = logI; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementDataModel() +{ + return true; +} + +bool QScxmlCompilerPrivate::preReadElementData() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto data = m_doc->newNode<DocumentModel::DataElement>(xmlLocation()); + data->id = attributes.value(QLatin1String("id")).toString(); + data->src = attributes.value(QLatin1String("src")).toString(); + data->expr = attributes.value(QLatin1String("expr")).toString(); + if (DocumentModel::Scxml *scxml = m_currentState->asScxml()) { + scxml->dataElements.append(data); + } else if (DocumentModel::State *state = m_currentState->asState()) { + state->dataElements.append(data); + } else { + Q_UNREACHABLE(); + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementAssign() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto assign = m_doc->newNode<DocumentModel::Assign>(xmlLocation()); + assign->location = attributes.value(QLatin1String("location")).toString(); + assign->expr = attributes.value(QLatin1String("expr")).toString(); + current().instruction = assign; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementDoneData() +{ + DocumentModel::State *s = m_currentState->asState(); + if (s && s->type == DocumentModel::State::Final) { + if (s->doneData) { + addError(QLatin1String("state can only have one donedata")); + } else { + s->doneData = m_doc->newNode<DocumentModel::DoneData>(xmlLocation()); + } + } else { + addError(QStringLiteral("donedata can only occur in a final state")); + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementContent() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::DoneData: { + DocumentModel::State *s = m_currentState->asState(); + Q_ASSERT(s); + s->doneData->expr = attributes.value(QLatin1String("expr")).toString(); + } break; + case ParserState::Send: { + DocumentModel::Send *s = previous().instruction->asSend(); + Q_ASSERT(s); + s->contentexpr = attributes.value(QLatin1String("expr")).toString(); + } break; + case ParserState::Invoke: { + DocumentModel::Invoke *i = previous().instruction->asInvoke(); + Q_ASSERT(i); + Q_UNUSED(i); + if (attributes.hasAttribute(QStringLiteral("expr"))) { + addError(QStringLiteral("expr attribute in content of invoke is not supported")); + break; + } + } break; + default: + addError(QStringLiteral("unexpected parent of content %1").arg(previous().kind)); + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementParam() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto param = m_doc->newNode<DocumentModel::Param>(xmlLocation()); + param->name = attributes.value(QLatin1String("name")).toString(); + param->expr = attributes.value(QLatin1String("expr")).toString(); + param->location = attributes.value(QLatin1String("location")).toString(); + + ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::DoneData: { + DocumentModel::State *s = m_currentState->asState(); + Q_ASSERT(s); + Q_ASSERT(s->doneData); + s->doneData->params.append(param); + } break; + case ParserState::Send: { + DocumentModel::Send *s = previous().instruction->asSend(); + Q_ASSERT(s); + s->params.append(param); + } break; + case ParserState::Invoke: { + DocumentModel::Invoke *i = previous().instruction->asInvoke(); + Q_ASSERT(i); + i->params.append(param); + } break; + default: + addError(QStringLiteral("unexpected parent of param %1").arg(previous().kind)); + } + return true; +} + +bool QScxmlCompilerPrivate::preReadElementScript() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *script = m_doc->newNode<DocumentModel::Script>(xmlLocation()); + script->src = attributes.value(QLatin1String("src")).toString(); + current().instruction = script; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementSend() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *send = m_doc->newNode<DocumentModel::Send>(xmlLocation()); + send->event = attributes.value(QLatin1String("event")).toString(); + send->eventexpr = attributes.value(QLatin1String("eventexpr")).toString(); + send->delay = attributes.value(QLatin1String("delay")).toString(); + send->delayexpr = attributes.value(QLatin1String("delayexpr")).toString(); + send->id = attributes.value(QLatin1String("id")).toString(); + send->idLocation = attributes.value(QLatin1String("idlocation")).toString(); + send->type = attributes.value(QLatin1String("type")).toString(); + send->typeexpr = attributes.value(QLatin1String("typeexpr")).toString(); + send->target = attributes.value(QLatin1String("target")).toString(); + send->targetexpr = attributes.value(QLatin1String("targetexpr")).toString(); + if (attributes.hasAttribute(QLatin1String("namelist"))) + send->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + current().instruction = send; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementCancel() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *cancel = m_doc->newNode<DocumentModel::Cancel>(xmlLocation()); + cancel->sendid = attributes.value(QLatin1String("sendid")).toString(); + cancel->sendidexpr = attributes.value(QLatin1String("sendidexpr")).toString(); + current().instruction = cancel; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementInvoke() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + DocumentModel::State *parentState = m_currentState->asState(); + if (!parentState || + (parentState->type != DocumentModel::State::Normal && parentState->type != DocumentModel::State::Parallel)) { + addError(QStringLiteral("invoke can only occur in <state> or <parallel>")); + return true; // TODO: verify me + } + auto *invoke = m_doc->newNode<DocumentModel::Invoke>(xmlLocation()); + parentState->invokes.append(invoke); + invoke->src = attributes.value(QLatin1String("src")).toString(); + invoke->srcexpr = attributes.value(QLatin1String("srcexpr")).toString(); + invoke->id = attributes.value(QLatin1String("id")).toString(); + invoke->idLocation = attributes.value(QLatin1String("idlocation")).toString(); + invoke->type = attributes.value(QLatin1String("type")).toString(); + invoke->typeexpr = attributes.value(QLatin1String("typeexpr")).toString(); + QStringRef autoforwardS = attributes.value(QLatin1String("autoforward")); + if (QStringRef::compare(autoforwardS, QLatin1String("true"), Qt::CaseInsensitive) == 0 + || QStringRef::compare(autoforwardS, QLatin1String("yes"), Qt::CaseInsensitive) == 0 + || QStringRef::compare(autoforwardS, QLatin1String("t"), Qt::CaseInsensitive) == 0 + || QStringRef::compare(autoforwardS, QLatin1String("y"), Qt::CaseInsensitive) == 0 + || autoforwardS == QLatin1String("1")) + invoke->autoforward = true; + else + invoke->autoforward = false; + invoke->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + current().instruction = invoke; + return true; +} + +bool QScxmlCompilerPrivate::preReadElementFinalize() +{ + auto instr = previous().instruction; + if (!instr) { + addError(QStringLiteral("no previous instruction found for <finalize>")); + return false; + } + auto invoke = instr->asInvoke(); + if (!invoke) { + addError(QStringLiteral("instruction before <finalize> is not <invoke>")); + return false; + } + current().instructionContainer = &invoke->finalize; + return true; +} + +bool QScxmlCompilerPrivate::postReadElementScxml() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementState() +{ + currentStateUp(); + return true; +} + +bool QScxmlCompilerPrivate::postReadElementParallel() +{ + currentStateUp(); + return true; +} + +bool QScxmlCompilerPrivate::postReadElementInitial() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementTransition() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementFinal() +{ + currentStateUp(); + return true; +} + +bool QScxmlCompilerPrivate::postReadElementHistory() +{ + currentStateUp(); + return true; +} + +bool QScxmlCompilerPrivate::postReadElementOnEntry() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementOnExit() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementRaise() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementIf() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementElseIf() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementElse() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementForeach() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementLog() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementDataModel() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementData() +{ + const ParserState parserState = current(); + DocumentModel::DataElement *data = Q_NULLPTR; + if (auto state = m_currentState->asState()) { + data = state->dataElements.last(); + } else if (auto scxml = m_currentState->asScxml()) { + data = scxml->dataElements.last(); + } else { + Q_UNREACHABLE(); + } + if (!data->src.isEmpty() && !data->expr.isEmpty()) { + addError(QStringLiteral("data element with both 'src' and 'expr' attributes")); + return false; + } + if (!parserState.chars.trimmed().isEmpty()) { + if (!data->src.isEmpty()) { + addError(QStringLiteral("data element with both 'src' attribute and CDATA")); + return false; + } else if (!data->expr.isEmpty()) { + addError(QStringLiteral("data element with both 'expr' attribute and CDATA")); + return false; + } else { + // w3c-ecma/test558 - "if a child element of <data> is not a XML, + // treat it as a string with whitespace normalization" + // We've modified the test, so that a string is enclosed with quotes. + data->expr = parserState.chars; + } + } else if (!data->src.isEmpty()) { + if (!m_loader) { + addError(QStringLiteral("cannot parse a document with external dependencies without a loader")); + } else { + bool ok; + const QByteArray ba = load(data->src, &ok); + if (!ok) { + addError(QStringLiteral("failed to load external dependency")); + } else { + // w3c-ecma/test558 - "if XML is loaded via "src" attribute, + // treat it as a string with whitespace normalization" + // We've enclosed the text in file with quotes. + data->expr = QString::fromUtf8(ba); + } + } + } + return true; +} + +bool QScxmlCompilerPrivate::postReadElementAssign() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementDoneData() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementContent() +{ + const ParserState parserState = current(); + if (!parserState.chars.trimmed().isEmpty()) { + + switch (previous().kind) { + case ParserState::DoneData: // see test529 + m_currentState->asState()->doneData->contents = parserState.chars.simplified(); + break; + case ParserState::Send: // see test179 + previous().instruction->asSend()->content = parserState.chars.simplified(); + break; + default: + break; + } + } + return true; +} + +bool QScxmlCompilerPrivate::postReadElementParam() +{ + return true; +} + +bool QScxmlCompilerPrivate::postReadElementScript() +{ + const ParserState parserState = current(); + DocumentModel::Script *scriptI = parserState.instruction->asScript(); + if (!parserState.chars.trimmed().isEmpty()) { + scriptI->content = parserState.chars.trimmed(); + if (!scriptI->src.isEmpty()) + addError(QStringLiteral("both src and source content given to script, will ignore external content")); + } else if (!scriptI->src.isEmpty()) { + if (!m_loader) { + addError(QStringLiteral("cannot parse a document with external dependencies without a loader")); + } else { + bool ok; + const QByteArray data = load(scriptI->src, &ok); + if (!ok) { + addError(QStringLiteral("failed to load external dependency")); + } else { + scriptI->content = QString::fromUtf8(data); + } + } + } + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementSend() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementCancel() +{ + return flushInstruction(); +} + +bool QScxmlCompilerPrivate::postReadElementInvoke() +{ + DocumentModel::Invoke *i = current().instruction->asInvoke(); + const QString fileName = i->src; + if (!i->content.data()) { + if (!fileName.isEmpty()) { + bool ok = true; + const QByteArray data = load(fileName, &ok); + if (!ok) { + addError(QStringLiteral("failed to load external dependency")); + } else { + QXmlStreamReader reader(data); + parseSubDocument(i, &reader, fileName); + } + } + } else if (!fileName.isEmpty()) { + addError(QStringLiteral("both src and content given to invoke")); + } + + return true; +} + +bool QScxmlCompilerPrivate::postReadElementFinalize() +{ + return true; +} + +void QScxmlCompilerPrivate::resetDocument() +{ + m_doc.reset(new DocumentModel::ScxmlDocument(fileName())); +} + +bool QScxmlCompilerPrivate::readDocument() +{ + resetDocument(); + m_currentState = m_doc->root; + for (bool finished = false; !finished && !m_reader->hasError();) { + switch (m_reader->readNext()) { + case QXmlStreamReader::StartElement : { + const QStringRef newTag = m_reader->name(); + const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(newTag); + + auto ns = m_reader->namespaceUri(); + + if (ns != scxmlNamespace) { + m_reader->skipCurrentElement(); + } else if (newElementKind == ParserState::None) { + addError(QStringLiteral("Unknown element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); + } else if (newElementKind == ParserState::Scxml) { + if (readElement() == false) + return false; + } else { + addError(QStringLiteral("Unexpected element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); + } + } + break; + case QXmlStreamReader::EndElement : + finished = true; + break; + default : + break; + } + } + if (!m_doc->root) { + addError(QStringLiteral("Missing root element")); + return false; + } + + if (m_reader->hasError() && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError) { + addError(QStringLiteral("Error parsing SCXML file: %1").arg(m_reader->errorString())); + return false; + } + + return true; +} + +bool QScxmlCompilerPrivate::readElement() +{ + const QStringRef currentTag = m_reader->name(); + const QXmlStreamAttributes attributes = m_reader->attributes(); + + const ParserState::Kind elementKind = ParserState::nameToParserStateKind(currentTag); + + if (!checkAttributes(attributes, elementKind)) + return false; + + if (elementKind == ParserState::Scxml && m_doc->root) { + if (!hasPrevious()) { + addError(QStringLiteral("misplaced scxml")); + return false; + } + + DocumentModel::Invoke *i = previous().instruction->asInvoke(); + if (!i) { + addError(QStringLiteral("misplaced scxml")); + return false; + } + + return parseSubElement(i, m_reader, m_fileName); + } + + if (elementKind != ParserState::Scxml && !m_stack.count()) { + addError(QStringLiteral("misplaced %1").arg(currentTag.toString())); + return false; + } + + ParserState pNew = ParserState(elementKind); + + m_stack.append(pNew); + + switch (elementKind) { + case ParserState::Scxml: if (!preReadElementScxml()) return false; break; + case ParserState::State: if (!preReadElementState()) return false; break; + case ParserState::Parallel: if (!preReadElementParallel()) return false; break; + case ParserState::Initial: if (!preReadElementInitial()) return false; break; + case ParserState::Transition: if (!preReadElementTransition()) return false; break; + case ParserState::Final: if (!preReadElementFinal()) return false; break; + case ParserState::History: if (!preReadElementHistory()) return false; break; + case ParserState::OnEntry: if (!preReadElementOnEntry()) return false; break; + case ParserState::OnExit: if (!preReadElementOnExit()) return false; break; + case ParserState::Raise: if (!preReadElementRaise()) return false; break; + case ParserState::If: if (!preReadElementIf()) return false; break; + case ParserState::ElseIf: if (!preReadElementElseIf()) return false; break; + case ParserState::Else: if (!preReadElementElse()) return false; break; + case ParserState::Foreach: if (!preReadElementForeach()) return false; break; + case ParserState::Log: if (!preReadElementLog()) return false; break; + case ParserState::DataModel: if (!preReadElementDataModel()) return false; break; + case ParserState::Data: if (!preReadElementData()) return false; break; + case ParserState::Assign: if (!preReadElementAssign()) return false; break; + case ParserState::DoneData: if (!preReadElementDoneData()) return false; break; + case ParserState::Content: if (!preReadElementContent()) return false; break; + case ParserState::Param: if (!preReadElementParam()) return false; break; + case ParserState::Script: if (!preReadElementScript()) return false; break; + case ParserState::Send: if (!preReadElementSend()) return false; break; + case ParserState::Cancel: if (!preReadElementCancel()) return false; break; + case ParserState::Invoke: if (!preReadElementInvoke()) return false; break; + case ParserState::Finalize: if (!preReadElementFinalize()) return false; break; + default: addError(QStringLiteral("Unknown element %1").arg(currentTag.toString())); return false; + } + + for (bool finished = false; !finished && !m_reader->hasError();) { + switch (m_reader->readNext()) { + case QXmlStreamReader::StartElement : { + const QStringRef newTag = m_reader->name(); + const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(newTag); + + auto ns = m_reader->namespaceUri(); + + if (ns != scxmlNamespace) { + m_reader->skipCurrentElement(); + } else if (newElementKind == ParserState::None) { + addError(QStringLiteral("Unknown element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); + } else if (pNew.validChild(newElementKind)) { + if (readElement() == false) + return false; + } else { + addError(QStringLiteral("Unexpected element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); + } + } + break; + case QXmlStreamReader::EndElement : + finished = true; + break; + case QXmlStreamReader::Characters : + if (m_stack.isEmpty()) + break; + if (current().collectChars()) + current().chars.append(m_reader->text()); + break; + default : + break; + } + } + + switch (elementKind) { + case ParserState::Scxml: if (!postReadElementScxml()) return false; break; + case ParserState::State: if (!postReadElementState()) return false; break; + case ParserState::Parallel: if (!postReadElementParallel()) return false; break; + case ParserState::Initial: if (!postReadElementInitial()) return false; break; + case ParserState::Transition: if (!postReadElementTransition()) return false; break; + case ParserState::Final: if (!postReadElementFinal()) return false; break; + case ParserState::History: if (!postReadElementHistory()) return false; break; + case ParserState::OnEntry: if (!postReadElementOnEntry()) return false; break; + case ParserState::OnExit: if (!postReadElementOnExit()) return false; break; + case ParserState::Raise: if (!postReadElementRaise()) return false; break; + case ParserState::If: if (!postReadElementIf()) return false; break; + case ParserState::ElseIf: if (!postReadElementElseIf()) return false; break; + case ParserState::Else: if (!postReadElementElse()) return false; break; + case ParserState::Foreach: if (!postReadElementForeach()) return false; break; + case ParserState::Log: if (!postReadElementLog()) return false; break; + case ParserState::DataModel: if (!postReadElementDataModel()) return false; break; + case ParserState::Data: if (!postReadElementData()) return false; break; + case ParserState::Assign: if (!postReadElementAssign()) return false; break; + case ParserState::DoneData: if (!postReadElementDoneData()) return false; break; + case ParserState::Content: if (!postReadElementContent()) return false; break; + case ParserState::Param: if (!postReadElementParam()) return false; break; + case ParserState::Script: if (!postReadElementScript()) return false; break; + case ParserState::Send: if (!postReadElementSend()) return false; break; + case ParserState::Cancel: if (!postReadElementCancel()) return false; break; + case ParserState::Invoke: if (!postReadElementInvoke()) return false; break; + case ParserState::Finalize: if (!postReadElementFinalize()) return false; break; + default: break; + } + + m_stack.removeLast(); + + if (m_reader->hasError()/* && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError*/) { + addError(QStringLiteral("Error parsing SCXML file: %1").arg(m_reader->errorString())); + return false; + } + + return true; +} + +void QScxmlCompilerPrivate::currentStateUp() +{ + Q_ASSERT(m_currentState->parent); + m_currentState = m_currentState->parent; +} + +bool QScxmlCompilerPrivate::flushInstruction() +{ + if (!hasPrevious()) { + addError(QStringLiteral("missing instructionContainer")); + return false; + } + DocumentModel::InstructionSequence *instructions = previous().instructionContainer; + if (!instructions) { + addError(QStringLiteral("got executable content within an element that did not set instructionContainer")); + return false; + } + instructions->append(current().instruction); + return true; +} + + +QByteArray QScxmlCompilerPrivate::load(const QString &name, bool *ok) +{ + QStringList errs; + const QByteArray result = m_loader->load(name, m_fileName.isEmpty() ? + QString() : QFileInfo(m_fileName).path(), &errs); + for (const QString &err : errs) + addError(err); + + *ok = errs.isEmpty(); + + return result; +} + +QVector<QScxmlError> QScxmlCompilerPrivate::errors() const +{ + return m_errors; +} + +void QScxmlCompilerPrivate::addError(const QString &msg) +{ + m_errors.append(QScxmlError(m_fileName, m_reader->lineNumber(), m_reader->columnNumber(), msg)); +} + +void QScxmlCompilerPrivate::addError(const DocumentModel::XmlLocation &location, const QString &msg) +{ + m_errors.append(QScxmlError(m_fileName, location.line, location.column, msg)); +} + +DocumentModel::AbstractState *QScxmlCompilerPrivate::currentParent() const +{ + return m_currentState ? m_currentState->asAbstractState() : Q_NULLPTR; +} + +DocumentModel::XmlLocation QScxmlCompilerPrivate::xmlLocation() const +{ + return DocumentModel::XmlLocation(m_reader->lineNumber(), m_reader->columnNumber()); +} + +bool QScxmlCompilerPrivate::maybeId(const QXmlStreamAttributes &attributes, QString *id) +{ + Q_ASSERT(id); + QString idStr = attributes.value(QLatin1String("id")).toString(); + if (!idStr.isEmpty()) { + if (m_allIds.contains(idStr)) { + addError(xmlLocation(), QStringLiteral("duplicate id '%1'").arg(idStr)); + } else { + m_allIds.insert(idStr); + *id = idStr; + } + } + return true; +} + +DocumentModel::If *QScxmlCompilerPrivate::lastIf() +{ + if (!hasPrevious()) { + addError(QStringLiteral("No previous instruction found for else block")); + return Q_NULLPTR; + } + + DocumentModel::Instruction *lastI = previous().instruction; + if (!lastI) { + addError(QStringLiteral("No previous instruction found for else block")); + return Q_NULLPTR; + } + DocumentModel::If *ifI = lastI->asIf(); + if (!ifI) { + addError(QStringLiteral("Previous instruction for else block is not an 'if'")); + return Q_NULLPTR; + } + return ifI; +} + +QScxmlCompilerPrivate::ParserState &QScxmlCompilerPrivate::current() +{ + return m_stack.last(); +} + +QScxmlCompilerPrivate::ParserState &QScxmlCompilerPrivate::previous() +{ + return m_stack[m_stack.count() - 2]; +} + +bool QScxmlCompilerPrivate::hasPrevious() const +{ + return m_stack.count() > 1; +} + +bool QScxmlCompilerPrivate::checkAttributes(const QXmlStreamAttributes &attributes, + QScxmlCompilerPrivate::ParserState::Kind kind) +{ + return checkAttributes(attributes, + ParserState::requiredAttributes(kind), + ParserState::optionalAttributes(kind)); +} + +bool QScxmlCompilerPrivate::checkAttributes(const QXmlStreamAttributes &attributes, + const QStringList &requiredNames, + const QStringList &optionalNames) +{ + QStringList required = requiredNames; + for (const QXmlStreamAttribute &attribute : attributes) { + const QStringRef ns = attribute.namespaceUri(); + if (!ns.isEmpty() && ns != scxmlNamespace && ns != qtScxmlNamespace) + continue; + + const QString name = attribute.name().toString(); + if (!required.removeOne(name) && !optionalNames.contains(name)) { + addError(QStringLiteral("Unexpected attribute '%1'").arg(name)); + return false; + } + } + if (!required.isEmpty()) { + addError(QStringLiteral("Missing required attributes: '%1'") + .arg(required.join(QLatin1String("', '")))); + return false; + } + return true; +} + +QScxmlCompilerPrivate::DefaultLoader::DefaultLoader() + : Loader() +{} + +QByteArray QScxmlCompilerPrivate::DefaultLoader::load(const QString &name, const QString &baseDir, QStringList *errors) +{ + QStringList errs; + QByteArray contents; +#ifdef BUILD_QSCXMLC + QString cleanName = name; + if (name.startsWith(QStringLiteral("file:"))) + cleanName = name.mid(5); + QFileInfo fInfo(cleanName); +#else + const QUrl url(name); + if (!url.isLocalFile() && !url.isRelative()) + errs << QStringLiteral("src attribute is not a local file (%1)").arg(name); + QFileInfo fInfo = url.isLocalFile() ? url.toLocalFile() : name; +#endif // BUILD_QSCXMLC + if (fInfo.isRelative()) + fInfo = QFileInfo(QDir(baseDir).filePath(fInfo.filePath())); + + if (!fInfo.exists()) { + errs << QStringLiteral("src attribute resolves to non existing file (%1)").arg(fInfo.filePath()); + } else { + QFile f(fInfo.filePath()); + if (f.open(QFile::ReadOnly)) + contents = f.readAll(); + else + errs << QStringLiteral("Failure opening file %1: %2") + .arg(fInfo.filePath(), f.errorString()); + } + if (errors) + *errors = errs; + + return contents; +} + +QScxmlCompilerPrivate::ParserState::ParserState(QScxmlCompilerPrivate::ParserState::Kind someKind) + : kind(someKind) + , instruction(0) + , instructionContainer(0) +{} + +QT_END_NAMESPACE |