blob: 76c70d8a48d9e3bb2c45b5bebb8069ec78e29f63 [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 "qscxmlcompiler_p.h"
#include "qscxmlexecutablecontent_p.h"
#include <qxmlstream.h>
#include <qloggingcategory.h>
#include <qfile.h>
#include <qvector.h>
#include <qstring.h>
#ifndef BUILD_QSCXMLC
#include "qscxmlinvokableservice_p.h"
#include "qscxmldatamodel_p.h"
#include "qscxmlstatemachine_p.h"
#include "qscxmlstatemachine.h"
#include "qscxmltabledata_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(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) 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 *) override
{
m_parentNodes.removeLast();
}
bool visit(DocumentModel::State *state) 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 *) override
{
m_parentNodes.removeLast();
}
bool visit(DocumentModel::Transition *transition) 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 *) override
{
m_parentNodes.removeLast();
}
bool visit(DocumentModel::HistoryState *state) 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) override
{
checkEvent(node->event, node->xmlLocation, ForbidWildCards);
checkExpr(node->xmlLocation, QStringLiteral("send"), QStringLiteral("eventexpr"), node->eventexpr);
return true;
}
void visit(DocumentModel::Cancel *node) override
{
checkExpr(node->xmlLocation, QStringLiteral("cancel"), QStringLiteral("sendidexpr"), node->sendidexpr);
}
bool visit(DocumentModel::DoneData *node) override
{
checkExpr(node->xmlLocation, QStringLiteral("donedata"), QStringLiteral("expr"), node->expr);
return false;
}
bool visit(DocumentModel::Invoke *node) 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 QScxmlInvokableServiceFactory
{
Q_OBJECT
public:
InvokeDynamicScxmlFactory(const QScxmlExecutableContent::InvokeInfo &invokeInfo,
const QVector<QScxmlExecutableContent::StringId> &namelist,
const QVector<QScxmlExecutableContent::ParameterInfo> &params)
: QScxmlInvokableServiceFactory(invokeInfo, namelist, params)
{}
void setContent(const QSharedPointer<DocumentModel::ScxmlDocument> &content)
{ m_content = content; }
QScxmlInvokableService *invoke(QScxmlStateMachine *child) 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:
const QMetaObject *metaObject() const override
{ return d_func()->m_metaObject; }
int qt_metacall(QMetaObject::Call _c, int _id, void **_a) 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 override final
{ return m_allFactoriesById.at(id); }
static DynamicStateMachine *build(DocumentModel::ScxmlDocument *doc)
{
auto stateMachine = new DynamicStateMachine;
MetaDataInfo info;
DataModelInfo dm;
auto factoryIdCreator = [stateMachine](
const QScxmlExecutableContent::InvokeInfo &invokeInfo,
const QVector<QScxmlExecutableContent::StringId> &namelist,
const QVector<QScxmlExecutableContent::ParameterInfo> &params,
const QSharedPointer<DocumentModel::ScxmlDocument> &content) -> int {
auto factory = new InvokeDynamicScxmlFactory(invokeInfo, namelist, params);
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, invokeInfo().expr, &ok);
if (!ok)
return nullptr;
if (!srcexpr.isEmpty())
return invokeDynamicScxmlService(srcexpr, parentStateMachine, this);
auto childStateMachine = DynamicStateMachine::build(m_content.data());
auto dm = QScxmlDataModelPrivate::instantiateDataModel(m_content->root->dataModel);
dm->setParent(childStateMachine);
childStateMachine->setDataModel(dm);
return invokeStaticScxmlService(childStateMachine, parentStateMachine, this);
}
#endif // BUILD_QSCXMLC
} // anonymous namespace
#ifndef BUILD_QSCXMLC
QScxmlScxmlService *invokeDynamicScxmlService(const QString &sourceUrl,
QScxmlStateMachine *parentStateMachine,
QScxmlInvokableServiceFactory *factory)
{
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 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().noquote() << error.toString();
return 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().noquote() << error.toString();
return nullptr;
}
auto childStateMachine = DynamicStateMachine::build(mainDoc);
auto dm = QScxmlDataModelPrivate::instantiateDataModel(mainDoc->root->dataModel);
dm->setParent(childStateMachine);
childStateMachine->setDataModel(dm);
return invokeStaticScxmlService(childStateMachine, parentStateMachine, factory);
}
#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 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
if (!m_errors.isEmpty()) {
qWarning() << "SCXML document has errors";
return;
}
auto doc = scxmlDocument();
auto root = doc ? doc->root : nullptr;
if (root == 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 == 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 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(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() : 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, Qt::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, Qt::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(' '), Qt::SkipEmptyParts);
transition->targets = attributes.value(QLatin1String("target")).toString().split(QLatin1Char(' '), Qt::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;
}
Q_FALLTHROUGH();
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;
}
Q_FALLTHROUGH();
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(' '), Qt::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(' '), Qt::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 = 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() : 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 nullptr;
}
DocumentModel::Instruction *lastI = previous().instruction;
if (!lastI) {
addError(QStringLiteral("No previous instruction found for else block"));
return nullptr;
}
DocumentModel::If *ifI = lastI->asIf();
if (!ifI) {
addError(QStringLiteral("Previous instruction for else block is not an 'if'"));
return 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
#ifndef BUILD_QSCXMLC
#include "qscxmlcompiler.moc"
#endif