| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qscxmlcompiler_p.h" |
| 5 | #include "qscxmlexecutablecontent_p.h" |
| 6 | |
| 7 | #include <qxmlstream.h> |
| 8 | #include <qloggingcategory.h> |
| 9 | #include <qfile.h> |
| 10 | #include <qlist.h> |
| 11 | #include <qstring.h> |
| 12 | |
| 13 | #ifndef BUILD_QSCXMLC |
| 14 | #include "qscxmlinvokableservice_p.h" |
| 15 | #include "qscxmldatamodel_p.h" |
| 16 | #include "qscxmlstatemachine_p.h" |
| 17 | #include "qscxmlstatemachine.h" |
| 18 | #include "qscxmltabledata_p.h" |
| 19 | |
| 20 | #include <private/qmetaobjectbuilder_p.h> |
| 21 | #endif // BUILD_QSCXMLC |
| 22 | |
| 23 | #include <QtCore/qmap.h> |
| 24 | |
| 25 | #include <functional> |
| 26 | |
| 27 | namespace { |
| 28 | enum { |
| 29 | DebugHelper_NameTransitions = 0 |
| 30 | }; |
| 31 | } // anonymous namespace |
| 32 | |
| 33 | QT_BEGIN_NAMESPACE |
| 34 | |
| 35 | static QString scxmlNamespace = QStringLiteral("http://www.w3.org/2005/07/scxml" ); |
| 36 | static QString qtScxmlNamespace = QStringLiteral("http://theqtcompany.com/scxml/2015/06/" ); |
| 37 | |
| 38 | namespace { |
| 39 | |
| 40 | class ScxmlVerifier: public DocumentModel::NodeVisitor |
| 41 | { |
| 42 | public: |
| 43 | ScxmlVerifier(std::function<void (const DocumentModel::XmlLocation &, const QString &)> errorHandler) |
| 44 | : m_errorHandler(errorHandler) |
| 45 | , m_doc(nullptr) |
| 46 | , m_hasErrors(false) |
| 47 | {} |
| 48 | |
| 49 | bool verify(DocumentModel::ScxmlDocument *doc) |
| 50 | { |
| 51 | if (doc->isVerified) |
| 52 | return true; |
| 53 | |
| 54 | doc->isVerified = true; |
| 55 | m_doc = doc; |
| 56 | for (DocumentModel::AbstractState *state : std::as_const(t&: doc->allStates)) { |
| 57 | if (state->id.isEmpty()) { |
| 58 | continue; |
| 59 | #ifndef QT_NO_DEBUG |
| 60 | } else if (m_stateById.contains(key: state->id)) { |
| 61 | Q_ASSERT(!"Should be unreachable: the compiler should check for this case!" ); |
| 62 | #endif // QT_NO_DEBUG |
| 63 | } else { |
| 64 | m_stateById[state->id] = state; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | if (doc->root) |
| 69 | doc->root->accept(visitor: this); |
| 70 | return !m_hasErrors; |
| 71 | } |
| 72 | |
| 73 | private: |
| 74 | bool visit(DocumentModel::Scxml *scxml) override |
| 75 | { |
| 76 | if (!scxml->name.isEmpty() && !isValidToken(id: scxml->name, tokenType: XmlNmtoken)) { |
| 77 | error(location: scxml->xmlLocation, |
| 78 | QStringLiteral("scxml name '%1' is not a valid XML Nmtoken" ).arg(a: scxml->name)); |
| 79 | } |
| 80 | |
| 81 | if (scxml->initial.isEmpty()) { |
| 82 | if (auto firstChild = firstAbstractState(container: scxml)) { |
| 83 | scxml->initialTransition = createInitialTransition(states: {firstChild}); |
| 84 | } |
| 85 | } else { |
| 86 | QList<DocumentModel::AbstractState *> initialStates; |
| 87 | for (const QString &initial : std::as_const(t&: scxml->initial)) { |
| 88 | if (DocumentModel::AbstractState *s = m_stateById.value(key: initial)) |
| 89 | initialStates.append(t: s); |
| 90 | else |
| 91 | error(location: scxml->xmlLocation, QStringLiteral("initial state '%1' not found for <scxml> element" ).arg(a: initial)); |
| 92 | } |
| 93 | scxml->initialTransition = createInitialTransition(states: initialStates); |
| 94 | } |
| 95 | |
| 96 | m_parentNodes.append(t: scxml); |
| 97 | |
| 98 | return true; |
| 99 | } |
| 100 | |
| 101 | void endVisit(DocumentModel::Scxml *) override |
| 102 | { |
| 103 | m_parentNodes.removeLast(); |
| 104 | } |
| 105 | |
| 106 | bool visit(DocumentModel::State *state) override |
| 107 | { |
| 108 | if (!state->id.isEmpty() && !isValidToken(id: state->id, tokenType: XmlNCName)) { |
| 109 | error(location: state->xmlLocation, QStringLiteral("'%1' is not a valid XML ID" ).arg(a: state->id)); |
| 110 | } |
| 111 | |
| 112 | if (state->initialTransition == nullptr) { |
| 113 | if (state->initial.isEmpty()) { |
| 114 | if (state->type == DocumentModel::State::Parallel) { |
| 115 | auto allChildren = allAbstractStates(container: state); |
| 116 | state->initialTransition = createInitialTransition(states: allChildren); |
| 117 | } else { |
| 118 | if (auto firstChild = firstAbstractState(container: state)) { |
| 119 | state->initialTransition = createInitialTransition(states: {firstChild}); |
| 120 | } |
| 121 | } |
| 122 | } else { |
| 123 | Q_ASSERT(state->type == DocumentModel::State::Normal); |
| 124 | QList<DocumentModel::AbstractState *> initialStates; |
| 125 | for (const QString &initialState : std::as_const(t&: state->initial)) { |
| 126 | if (DocumentModel::AbstractState *s = m_stateById.value(key: initialState)) { |
| 127 | initialStates.append(t: s); |
| 128 | } else { |
| 129 | error(location: state->xmlLocation, |
| 130 | QStringLiteral("undefined initial state '%1' for state '%2'" ) |
| 131 | .arg(args: initialState, args&: state->id)); |
| 132 | } |
| 133 | } |
| 134 | state->initialTransition = createInitialTransition(states: initialStates); |
| 135 | } |
| 136 | } else { |
| 137 | if (state->initial.isEmpty()) { |
| 138 | visit(transition: state->initialTransition); |
| 139 | } else { |
| 140 | error(location: state->xmlLocation, |
| 141 | QStringLiteral("initial transition and initial attribute for state '%1'" ) |
| 142 | .arg(a: state->id)); |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | switch (state->type) { |
| 147 | case DocumentModel::State::Normal: |
| 148 | break; |
| 149 | case DocumentModel::State::Parallel: |
| 150 | if (!state->initial.isEmpty()) { |
| 151 | error(location: state->xmlLocation, |
| 152 | QStringLiteral("parallel states cannot have an initial state" )); |
| 153 | } |
| 154 | break; |
| 155 | case DocumentModel::State::Final: |
| 156 | break; |
| 157 | default: |
| 158 | Q_UNREACHABLE(); |
| 159 | } |
| 160 | |
| 161 | m_parentNodes.append(t: state); |
| 162 | return true; |
| 163 | } |
| 164 | |
| 165 | void endVisit(DocumentModel::State *) override |
| 166 | { |
| 167 | m_parentNodes.removeLast(); |
| 168 | } |
| 169 | |
| 170 | bool visit(DocumentModel::Transition *transition) override |
| 171 | { |
| 172 | Q_ASSERT(transition->targetStates.isEmpty()); |
| 173 | |
| 174 | if (int size = transition->targets.size()) |
| 175 | transition->targetStates.reserve(asize: size); |
| 176 | for (const QString &target : std::as_const(t&: transition->targets)) { |
| 177 | if (DocumentModel::AbstractState *s = m_stateById.value(key: target)) { |
| 178 | if (transition->targetStates.contains(t: s)) { |
| 179 | error(location: transition->xmlLocation, QStringLiteral("duplicate target '%1'" ).arg(a: target)); |
| 180 | } else { |
| 181 | transition->targetStates.append(t: s); |
| 182 | } |
| 183 | } else if (!target.isEmpty()) { |
| 184 | error(location: transition->xmlLocation, QStringLiteral("unknown state '%1' in target" ).arg(a: target)); |
| 185 | } |
| 186 | } |
| 187 | for (const QString &event : std::as_const(t&: transition->events)) |
| 188 | checkEvent(event, loc: transition->xmlLocation, wildCardMode: AllowWildCards); |
| 189 | |
| 190 | m_parentNodes.append(t: transition); |
| 191 | return true; |
| 192 | } |
| 193 | |
| 194 | void endVisit(DocumentModel::Transition *) override |
| 195 | { |
| 196 | m_parentNodes.removeLast(); |
| 197 | } |
| 198 | |
| 199 | bool visit(DocumentModel::HistoryState *state) override |
| 200 | { |
| 201 | bool seenTransition = false; |
| 202 | for (DocumentModel::StateOrTransition *sot : std::as_const(t&: state->children)) { |
| 203 | if (DocumentModel::State *s = sot->asState()) { |
| 204 | error(location: s->xmlLocation, QStringLiteral("history state cannot have substates" )); |
| 205 | } else if (DocumentModel::Transition *t = sot->asTransition()) { |
| 206 | if (seenTransition) { |
| 207 | error(location: t->xmlLocation, QStringLiteral("history state can only have one transition" )); |
| 208 | } else { |
| 209 | seenTransition = true; |
| 210 | m_parentNodes.append(t: state); |
| 211 | t->accept(visitor: this); |
| 212 | m_parentNodes.removeLast(); |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | return false; |
| 218 | } |
| 219 | |
| 220 | bool visit(DocumentModel::Send *node) override |
| 221 | { |
| 222 | checkEvent(event: node->event, loc: node->xmlLocation, wildCardMode: ForbidWildCards); |
| 223 | checkExpr(loc: node->xmlLocation, QStringLiteral("send" ), QStringLiteral("eventexpr" ), attrValue: node->eventexpr); |
| 224 | return true; |
| 225 | } |
| 226 | |
| 227 | void visit(DocumentModel::Cancel *node) override |
| 228 | { |
| 229 | checkExpr(loc: node->xmlLocation, QStringLiteral("cancel" ), QStringLiteral("sendidexpr" ), attrValue: node->sendidexpr); |
| 230 | } |
| 231 | |
| 232 | bool visit(DocumentModel::DoneData *node) override |
| 233 | { |
| 234 | checkExpr(loc: node->xmlLocation, QStringLiteral("donedata" ), QStringLiteral("expr" ), attrValue: node->expr); |
| 235 | return false; |
| 236 | } |
| 237 | |
| 238 | bool visit(DocumentModel::Invoke *node) override |
| 239 | { |
| 240 | if (!node->srcexpr.isEmpty()) |
| 241 | return false; |
| 242 | |
| 243 | if (node->content.isNull()) { |
| 244 | error(location: node->xmlLocation, QStringLiteral("no valid content found in <invoke> tag" )); |
| 245 | } else { |
| 246 | ScxmlVerifier subVerifier(m_errorHandler); |
| 247 | m_hasErrors = !subVerifier.verify(doc: node->content.data()); |
| 248 | } |
| 249 | return false; |
| 250 | } |
| 251 | |
| 252 | private: |
| 253 | enum TokenType { |
| 254 | XmlNCName, |
| 255 | XmlNmtoken, |
| 256 | }; |
| 257 | |
| 258 | static bool isValidToken(const QString &id, TokenType tokenType) |
| 259 | { |
| 260 | Q_ASSERT(!id.isEmpty()); |
| 261 | int i = 0; |
| 262 | if (tokenType == XmlNCName) { |
| 263 | const QChar c = id.at(i: i++); |
| 264 | if (!isLetter(c) && c != QLatin1Char('_')) |
| 265 | return false; |
| 266 | } |
| 267 | for (int ei = id.size(); i != ei; ++i) { |
| 268 | const QChar c = id.at(i); |
| 269 | if (isLetter(c) || c.isDigit() || c == QLatin1Char('.') || c == QLatin1Char('-') |
| 270 | || c == QLatin1Char('_') || isNameTail(c)) |
| 271 | continue; |
| 272 | else if (tokenType == XmlNmtoken && c == QLatin1Char(':')) |
| 273 | continue; |
| 274 | else |
| 275 | return false; |
| 276 | } |
| 277 | |
| 278 | return true; |
| 279 | } |
| 280 | |
| 281 | static bool isLetter(QChar c) |
| 282 | { |
| 283 | switch (c.category()) { |
| 284 | case QChar::Letter_Lowercase: |
| 285 | case QChar::Letter_Uppercase: |
| 286 | case QChar::Letter_Other: |
| 287 | case QChar::Letter_Titlecase: |
| 288 | case QChar::Number_Letter: |
| 289 | return true; |
| 290 | default: |
| 291 | return false; |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | static bool isNameTail(QChar c) |
| 296 | { |
| 297 | switch (c.category()) { |
| 298 | case QChar::Mark_SpacingCombining: |
| 299 | case QChar::Mark_Enclosing: |
| 300 | case QChar::Mark_NonSpacing: |
| 301 | case QChar::Letter_Modifier: |
| 302 | case QChar::Number_DecimalDigit: |
| 303 | return true; |
| 304 | default: |
| 305 | return false; |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | enum WildCardMode { |
| 310 | ForbidWildCards, |
| 311 | AllowWildCards |
| 312 | }; |
| 313 | |
| 314 | void checkEvent(const QString &event, const DocumentModel::XmlLocation &loc, |
| 315 | WildCardMode wildCardMode) |
| 316 | { |
| 317 | if (event.isEmpty()) |
| 318 | return; |
| 319 | |
| 320 | if (!isValidEvent(event, wildCardMode)) { |
| 321 | error(location: loc, QStringLiteral("'%1' is not a valid event" ).arg(a: event)); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | static bool isValidEvent(const QString &event, WildCardMode wildCardMode) |
| 326 | { |
| 327 | if (event.isEmpty()) |
| 328 | return false; |
| 329 | |
| 330 | if (wildCardMode == AllowWildCards && event == QLatin1String(".*" )) |
| 331 | return true; |
| 332 | |
| 333 | const QStringList parts = event.split(sep: QLatin1Char('.')); |
| 334 | |
| 335 | for (const QString &part : parts) { |
| 336 | if (part.isEmpty()) |
| 337 | return false; |
| 338 | |
| 339 | if (wildCardMode == AllowWildCards && part.size() == 1 |
| 340 | && part.at(i: 0) == QLatin1Char('*')) { |
| 341 | continue; |
| 342 | } |
| 343 | |
| 344 | for (int i = 0, ei = part.size(); i != ei; ++i) { |
| 345 | const QChar c = part.at(i); |
| 346 | if (!isLetter(c) && !c.isDigit() && c != QLatin1Char('-') && c != QLatin1Char('_') |
| 347 | && c != QLatin1Char(':')) { |
| 348 | return false; |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | return true; |
| 354 | } |
| 355 | |
| 356 | static const QList<DocumentModel::StateOrTransition *> &allChildrenOfContainer( |
| 357 | DocumentModel::StateContainer *container) |
| 358 | { |
| 359 | if (auto state = container->asState()) |
| 360 | return state->children; |
| 361 | else if (auto scxml = container->asScxml()) |
| 362 | return scxml->children; |
| 363 | else |
| 364 | Q_UNREACHABLE(); |
| 365 | } |
| 366 | |
| 367 | static DocumentModel::AbstractState *firstAbstractState(DocumentModel::StateContainer *container) |
| 368 | { |
| 369 | const auto &allChildren = allChildrenOfContainer(container); |
| 370 | |
| 371 | QList<DocumentModel::AbstractState *> childStates; |
| 372 | for (DocumentModel::StateOrTransition *child : std::as_const(t: allChildren)) { |
| 373 | if (DocumentModel::State *s = child->asState()) |
| 374 | return s; |
| 375 | else if (DocumentModel::HistoryState *h = child->asHistoryState()) |
| 376 | return h; |
| 377 | } |
| 378 | return nullptr; |
| 379 | } |
| 380 | |
| 381 | static QList<DocumentModel::AbstractState *> allAbstractStates( |
| 382 | DocumentModel::StateContainer *container) |
| 383 | { |
| 384 | const auto &allChildren = allChildrenOfContainer(container); |
| 385 | |
| 386 | QList<DocumentModel::AbstractState *> childStates; |
| 387 | for (DocumentModel::StateOrTransition *child : std::as_const(t: allChildren)) { |
| 388 | if (DocumentModel::State *s = child->asState()) |
| 389 | childStates.append(t: s); |
| 390 | else if (DocumentModel::HistoryState *h = child->asHistoryState()) |
| 391 | childStates.append(t: h); |
| 392 | } |
| 393 | return childStates; |
| 394 | } |
| 395 | |
| 396 | DocumentModel::Transition *createInitialTransition( |
| 397 | const QList<DocumentModel::AbstractState *> &states) |
| 398 | { |
| 399 | auto *newTransition = m_doc->newTransition(parent: nullptr, xmlLocation: DocumentModel::XmlLocation(-1, -1)); |
| 400 | newTransition->type = DocumentModel::Transition::Synthetic; |
| 401 | for (auto *s : states) { |
| 402 | newTransition->targets.append(t: s->id); |
| 403 | } |
| 404 | |
| 405 | newTransition->targetStates = states; |
| 406 | return newTransition; |
| 407 | } |
| 408 | |
| 409 | void checkExpr(const DocumentModel::XmlLocation &loc, const QString &tag, const QString &attrName, const QString &attrValue) |
| 410 | { |
| 411 | if (m_doc->root->dataModel == DocumentModel::Scxml::NullDataModel && !attrValue.isEmpty()) { |
| 412 | error(location: loc, QStringLiteral( |
| 413 | "%1 in <%2> cannot be used with data model 'null'" ).arg(args: attrName, args: tag)); |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | void error(const DocumentModel::XmlLocation &location, const QString &message) |
| 418 | { |
| 419 | m_hasErrors = true; |
| 420 | if (m_errorHandler) |
| 421 | m_errorHandler(location, message); |
| 422 | } |
| 423 | |
| 424 | private: |
| 425 | std::function<void (const DocumentModel::XmlLocation &, const QString &)> m_errorHandler; |
| 426 | DocumentModel::ScxmlDocument *m_doc; |
| 427 | bool m_hasErrors; |
| 428 | QHash<QString, DocumentModel::AbstractState *> m_stateById; |
| 429 | QList<DocumentModel::Node *> m_parentNodes; |
| 430 | }; |
| 431 | |
| 432 | #ifndef BUILD_QSCXMLC |
| 433 | class InvokeDynamicScxmlFactory: public QScxmlInvokableServiceFactory |
| 434 | { |
| 435 | Q_OBJECT |
| 436 | public: |
| 437 | InvokeDynamicScxmlFactory(const QScxmlExecutableContent::InvokeInfo &invokeInfo, |
| 438 | const QList<QScxmlExecutableContent::StringId> &namelist, |
| 439 | const QList<QScxmlExecutableContent::ParameterInfo> ¶ms) |
| 440 | : QScxmlInvokableServiceFactory(invokeInfo, namelist, params) |
| 441 | {} |
| 442 | |
| 443 | void setContent(const QSharedPointer<DocumentModel::ScxmlDocument> &content) |
| 444 | { m_content = content; } |
| 445 | |
| 446 | QScxmlInvokableService *invoke(QScxmlStateMachine *child) override; |
| 447 | |
| 448 | private: |
| 449 | QSharedPointer<DocumentModel::ScxmlDocument> m_content; |
| 450 | }; |
| 451 | |
| 452 | class DynamicStateMachinePrivate : public QScxmlStateMachinePrivate |
| 453 | { |
| 454 | struct DynamicMetaObject : public QAbstractDynamicMetaObject |
| 455 | { |
| 456 | QMetaObject *toDynamicMetaObject(QObject *) override |
| 457 | { |
| 458 | return this; |
| 459 | } |
| 460 | |
| 461 | int metaCall(QObject *o, QMetaObject::Call c, int id, void **a) override |
| 462 | { |
| 463 | return o->qt_metacall(c, id, a); |
| 464 | } |
| 465 | }; |
| 466 | |
| 467 | public: |
| 468 | DynamicStateMachinePrivate() : |
| 469 | QScxmlStateMachinePrivate(&QScxmlStateMachine::staticMetaObject) |
| 470 | { |
| 471 | metaObject = new DynamicMetaObject; |
| 472 | } |
| 473 | |
| 474 | void setDynamicMetaObject(const QMetaObject *m) { |
| 475 | // Prevent the QML engine from creating a property cache for this thing. |
| 476 | static_cast<DynamicMetaObject *>(metaObject)->d = m->d; |
| 477 | m_metaObject = m; |
| 478 | } |
| 479 | }; |
| 480 | |
| 481 | class DynamicStateMachine: public QScxmlStateMachine, public QScxmlInternal::GeneratedTableData |
| 482 | { |
| 483 | Q_DECLARE_PRIVATE(DynamicStateMachine) |
| 484 | // Manually expanded from Q_OBJECT macro: |
| 485 | public: |
| 486 | const QMetaObject *metaObject() const override |
| 487 | { return d_func()->m_metaObject; } |
| 488 | |
| 489 | int qt_metacall(QMetaObject::Call _c, int _id, void **_a) override |
| 490 | { |
| 491 | Q_D(DynamicStateMachine); |
| 492 | _id = QScxmlStateMachine::qt_metacall(_c, _id, _a); |
| 493 | if (_id < 0) |
| 494 | return _id; |
| 495 | int ownMethodCount = d->m_metaObject->methodCount() - d->m_metaObject->methodOffset(); |
| 496 | if (_c == QMetaObject::InvokeMetaMethod) { |
| 497 | if (_id < ownMethodCount) |
| 498 | qt_static_metacall(o: this, _c, _id, _a); |
| 499 | _id -= ownMethodCount; |
| 500 | } else if (_c == QMetaObject::ReadProperty || _c == QMetaObject::WriteProperty |
| 501 | || _c == QMetaObject::ResetProperty || _c == QMetaObject::RegisterPropertyMetaType) { |
| 502 | qt_static_metacall(o: this, _c, _id, _a); |
| 503 | _id -= d->m_metaObject->propertyCount(); |
| 504 | } |
| 505 | return _id; |
| 506 | } |
| 507 | |
| 508 | private: |
| 509 | static void qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) |
| 510 | { |
| 511 | if (_c == QMetaObject::RegisterPropertyMetaType) { |
| 512 | *reinterpret_cast<int*>(_a[0]) = qRegisterMetaType<bool>(); |
| 513 | } else if (_c == QMetaObject::ReadProperty) { |
| 514 | DynamicStateMachine *_t = static_cast<DynamicStateMachine *>(_o); |
| 515 | void *_v = _a[0]; |
| 516 | if (_id >= 0 && _id < _t->m_propertyCount) { |
| 517 | // getter for the state |
| 518 | *reinterpret_cast<bool*>(_v) = _t->isActive(stateIndex: _id); |
| 519 | } |
| 520 | } |
| 521 | } |
| 522 | // end of Q_OBJECT macro |
| 523 | |
| 524 | private: |
| 525 | DynamicStateMachine() |
| 526 | : QScxmlStateMachine(*new DynamicStateMachinePrivate) |
| 527 | , m_propertyCount(0) |
| 528 | { |
| 529 | // Temporarily wire up the QMetaObject |
| 530 | Q_D(DynamicStateMachine); |
| 531 | QMetaObjectBuilder b; |
| 532 | b.setClassName("DynamicStateMachine" ); |
| 533 | b.setSuperClass(&QScxmlStateMachine::staticMetaObject); |
| 534 | b.setStaticMetacallFunction(qt_static_metacall); |
| 535 | d->setDynamicMetaObject(b.toMetaObject()); |
| 536 | } |
| 537 | |
| 538 | void initDynamicParts(const MetaDataInfo &info) |
| 539 | { |
| 540 | Q_D(DynamicStateMachine); |
| 541 | // Release the temporary QMetaObject. |
| 542 | Q_ASSERT(d->m_metaObject != &QScxmlStateMachine::staticMetaObject); |
| 543 | free(ptr: const_cast<QMetaObject *>(d->m_metaObject)); |
| 544 | d->setDynamicMetaObject(&QScxmlStateMachine::staticMetaObject); |
| 545 | |
| 546 | // Build the real one. |
| 547 | QMetaObjectBuilder b; |
| 548 | b.setClassName("DynamicStateMachine" ); |
| 549 | b.setSuperClass(&QScxmlStateMachine::staticMetaObject); |
| 550 | b.setStaticMetacallFunction(qt_static_metacall); |
| 551 | |
| 552 | // signals |
| 553 | for (const QString &stateName : info.stateNames) { |
| 554 | auto name = stateName.toUtf8(); |
| 555 | const QByteArray signalName = name + "Changed(bool)" ; |
| 556 | QMetaMethodBuilder signalBuilder = b.addSignal(signature: signalName); |
| 557 | signalBuilder.setParameterNames(init(s: "active" )); |
| 558 | } |
| 559 | |
| 560 | // properties |
| 561 | int notifier = 0; |
| 562 | for (const QString &stateName : info.stateNames) { |
| 563 | QMetaPropertyBuilder prop = b.addProperty(name: stateName.toUtf8(), type: "bool" , notifierId: notifier); |
| 564 | prop.setWritable(false); |
| 565 | ++m_propertyCount; |
| 566 | ++notifier; |
| 567 | } |
| 568 | |
| 569 | // And we're done |
| 570 | d->setDynamicMetaObject(b.toMetaObject()); |
| 571 | } |
| 572 | |
| 573 | public: |
| 574 | ~DynamicStateMachine() |
| 575 | { |
| 576 | Q_D(DynamicStateMachine); |
| 577 | if (d->m_metaObject != &QScxmlStateMachine::staticMetaObject) { |
| 578 | free(ptr: const_cast<QMetaObject *>(d->m_metaObject)); |
| 579 | d->setDynamicMetaObject(&QScxmlStateMachine::staticMetaObject); |
| 580 | } |
| 581 | } |
| 582 | |
| 583 | QScxmlInvokableServiceFactory *serviceFactory(int id) const override final |
| 584 | { return m_allFactoriesById.at(i: id); } |
| 585 | |
| 586 | static DynamicStateMachine *build(DocumentModel::ScxmlDocument *doc) |
| 587 | { |
| 588 | auto stateMachine = new DynamicStateMachine; |
| 589 | MetaDataInfo info; |
| 590 | DataModelInfo dm; |
| 591 | auto factoryIdCreator = [stateMachine]( |
| 592 | const QScxmlExecutableContent::InvokeInfo &invokeInfo, |
| 593 | const QList<QScxmlExecutableContent::StringId> &namelist, |
| 594 | const QList<QScxmlExecutableContent::ParameterInfo> ¶ms, |
| 595 | const QSharedPointer<DocumentModel::ScxmlDocument> &content) -> int { |
| 596 | auto factory = new InvokeDynamicScxmlFactory(invokeInfo, namelist, params); |
| 597 | factory->setContent(content); |
| 598 | stateMachine->m_allFactoriesById.append(t: factory); |
| 599 | return stateMachine->m_allFactoriesById.size() - 1; |
| 600 | }; |
| 601 | |
| 602 | GeneratedTableData::build(doc, table: stateMachine, metaDataInfo: &info, dataModelInfo: &dm, func: factoryIdCreator); |
| 603 | stateMachine->setTableData(stateMachine); |
| 604 | stateMachine->initDynamicParts(info); |
| 605 | |
| 606 | return stateMachine; |
| 607 | } |
| 608 | |
| 609 | private: |
| 610 | static QList<QByteArray> init(const char *s) |
| 611 | { |
| 612 | #ifdef Q_COMPILER_INITIALIZER_LISTS |
| 613 | return QList<QByteArray>({ QByteArray::fromRawData(data: s, size: int(strlen(s: s))) }); |
| 614 | #else // insane compiler: |
| 615 | return QList<QByteArray>() << QByteArray::fromRawData(s, int(strlen(s))); |
| 616 | #endif |
| 617 | } |
| 618 | |
| 619 | private: |
| 620 | QList<QScxmlInvokableServiceFactory *> m_allFactoriesById; |
| 621 | int m_propertyCount; |
| 622 | }; |
| 623 | |
| 624 | inline QScxmlInvokableService *InvokeDynamicScxmlFactory::invoke( |
| 625 | QScxmlStateMachine *parentStateMachine) |
| 626 | { |
| 627 | bool ok = true; |
| 628 | auto srcexpr = calculateSrcexpr(parent: parentStateMachine, srcexpr: invokeInfo().expr, ok: &ok); |
| 629 | if (!ok) |
| 630 | return nullptr; |
| 631 | |
| 632 | if (!srcexpr.isEmpty()) |
| 633 | return invokeDynamicScxmlService(sourceUrl: srcexpr, parentStateMachine, factory: this); |
| 634 | |
| 635 | auto childStateMachine = DynamicStateMachine::build(doc: m_content.data()); |
| 636 | |
| 637 | auto dm = QScxmlDataModelPrivate::instantiateDataModel(type: m_content->root->dataModel); |
| 638 | dm->setParent(childStateMachine); |
| 639 | childStateMachine->setDataModel(dm); |
| 640 | |
| 641 | return invokeStaticScxmlService(childStateMachine, parentStateMachine, factory: this); |
| 642 | } |
| 643 | #endif // BUILD_QSCXMLC |
| 644 | |
| 645 | } // anonymous namespace |
| 646 | |
| 647 | #ifndef BUILD_QSCXMLC |
| 648 | QScxmlScxmlService *invokeDynamicScxmlService(const QString &sourceUrl, |
| 649 | QScxmlStateMachine *parentStateMachine, |
| 650 | QScxmlInvokableServiceFactory *factory) |
| 651 | { |
| 652 | QScxmlCompiler::Loader *loader = parentStateMachine->loader(); |
| 653 | |
| 654 | const QString baseDir = sourceUrl.isEmpty() ? QString() : QFileInfo(sourceUrl).path(); |
| 655 | QStringList errs; |
| 656 | const QByteArray data = loader->load(name: sourceUrl, baseDir, errors: &errs); |
| 657 | |
| 658 | if (!errs.isEmpty()) { |
| 659 | qWarning() << errs; |
| 660 | return nullptr; |
| 661 | } |
| 662 | |
| 663 | QXmlStreamReader reader(data); |
| 664 | QScxmlCompiler compiler(&reader); |
| 665 | compiler.setFileName(sourceUrl); |
| 666 | compiler.setLoader(parentStateMachine->loader()); |
| 667 | compiler.compile(); |
| 668 | if (!compiler.errors().isEmpty()) { |
| 669 | const auto errors = compiler.errors(); |
| 670 | for (const QScxmlError &error : errors) |
| 671 | qWarning().noquote() << error.toString(); |
| 672 | return nullptr; |
| 673 | } |
| 674 | |
| 675 | auto mainDoc = QScxmlCompilerPrivate::get(compiler: &compiler)->scxmlDocument(); |
| 676 | if (mainDoc == nullptr) { |
| 677 | Q_ASSERT(!compiler.errors().isEmpty()); |
| 678 | const auto errors = compiler.errors(); |
| 679 | for (const QScxmlError &error : errors) |
| 680 | qWarning().noquote() << error.toString(); |
| 681 | return nullptr; |
| 682 | } |
| 683 | |
| 684 | auto childStateMachine = DynamicStateMachine::build(doc: mainDoc); |
| 685 | |
| 686 | auto dm = QScxmlDataModelPrivate::instantiateDataModel(type: mainDoc->root->dataModel); |
| 687 | dm->setParent(childStateMachine); |
| 688 | childStateMachine->setDataModel(dm); |
| 689 | |
| 690 | return invokeStaticScxmlService(childStateMachine, parentStateMachine, factory); |
| 691 | } |
| 692 | #endif // BUILD_QSCXMLC |
| 693 | |
| 694 | /*! |
| 695 | * \class QScxmlCompiler |
| 696 | * \brief The QScxmlCompiler class is a compiler for SCXML files. |
| 697 | * \since 5.7 |
| 698 | * \inmodule QtScxml |
| 699 | * |
| 700 | * Parses an \l{SCXML Specification}{SCXML} file and dynamically instantiates a |
| 701 | * state machine for a successfully parsed SCXML file. If parsing fails, the |
| 702 | * new state machine cannot start. All errors are returned by |
| 703 | * QScxmlStateMachine::parseErrors(). |
| 704 | * |
| 705 | * To load an SCXML file, QScxmlStateMachine::fromFile or QScxmlStateMachine::fromData should be |
| 706 | * used. Using QScxmlCompiler directly is only needed when the compiler needs to use a custom |
| 707 | * QScxmlCompiler::Loader. |
| 708 | */ |
| 709 | |
| 710 | /*! |
| 711 | * Creates a new SCXML compiler for the specified \a reader. |
| 712 | */ |
| 713 | QScxmlCompiler::QScxmlCompiler(QXmlStreamReader *reader) |
| 714 | : d(new QScxmlCompilerPrivate(reader)) |
| 715 | { } |
| 716 | |
| 717 | /*! |
| 718 | * Destroys the SCXML compiler. |
| 719 | */ |
| 720 | QScxmlCompiler::~QScxmlCompiler() |
| 721 | { |
| 722 | delete d; |
| 723 | } |
| 724 | |
| 725 | /*! |
| 726 | * Returns the file name associated with the current input. |
| 727 | * |
| 728 | * \sa setFileName() |
| 729 | */ |
| 730 | QString QScxmlCompiler::fileName() const |
| 731 | { |
| 732 | return d->fileName(); |
| 733 | } |
| 734 | |
| 735 | /*! |
| 736 | * Sets the file name for the current input to \a fileName. |
| 737 | * |
| 738 | * The file name is used for error reporting and for resolving relative path URIs. |
| 739 | * |
| 740 | * \sa fileName() |
| 741 | */ |
| 742 | void QScxmlCompiler::setFileName(const QString &fileName) |
| 743 | { |
| 744 | d->setFileName(fileName); |
| 745 | } |
| 746 | |
| 747 | /*! |
| 748 | * Returns the loader that is currently used to resolve and load URIs for the |
| 749 | * SCXML compiler. |
| 750 | * |
| 751 | * \sa setLoader() |
| 752 | */ |
| 753 | QScxmlCompiler::Loader *QScxmlCompiler::loader() const |
| 754 | { |
| 755 | return d->loader(); |
| 756 | } |
| 757 | |
| 758 | /*! |
| 759 | * Sets \a newLoader to be used for resolving and loading URIs for the SCXML |
| 760 | * compiler. |
| 761 | * |
| 762 | * \sa loader() |
| 763 | */ |
| 764 | void QScxmlCompiler::setLoader(QScxmlCompiler::Loader *newLoader) |
| 765 | { |
| 766 | d->setLoader(newLoader); |
| 767 | } |
| 768 | |
| 769 | /*! |
| 770 | * Parses an SCXML file and creates a new state machine from it. |
| 771 | * |
| 772 | * If parsing is successful, the returned state machine can be initialized and started. If |
| 773 | * parsing fails, QScxmlStateMachine::parseErrors() can be used to retrieve a list of errors. |
| 774 | */ |
| 775 | QScxmlStateMachine *QScxmlCompiler::compile() |
| 776 | { |
| 777 | d->readDocument(); |
| 778 | if (d->errors().isEmpty()) { |
| 779 | // Only verify the document if there were no parse errors: if there were any, the document |
| 780 | // is incomplete and will contain errors for sure. There is no need to heap more errors on |
| 781 | // top of other errors. |
| 782 | d->verifyDocument(); |
| 783 | } |
| 784 | return d->instantiateStateMachine(); |
| 785 | } |
| 786 | |
| 787 | /*! |
| 788 | * \internal |
| 789 | * Instantiates a new state machine from the parsed SCXML. |
| 790 | * |
| 791 | * If parsing is successful, the returned state machine can be initialized and started. If |
| 792 | * parsing fails, QScxmlStateMachine::parseErrors() can be used to retrieve a list of errors. |
| 793 | * |
| 794 | * \note The instantiated state machine will not have an associated data model set. |
| 795 | * \sa QScxmlCompilerPrivate::instantiateDataModel |
| 796 | */ |
| 797 | QScxmlStateMachine *QScxmlCompilerPrivate::instantiateStateMachine() const |
| 798 | { |
| 799 | #ifdef BUILD_QSCXMLC |
| 800 | return nullptr; |
| 801 | #else // BUILD_QSCXMLC |
| 802 | DocumentModel::ScxmlDocument *doc = scxmlDocument(); |
| 803 | if (doc && doc->root) { |
| 804 | auto stateMachine = DynamicStateMachine::build(doc); |
| 805 | instantiateDataModel(stateMachine); |
| 806 | return stateMachine; |
| 807 | } else { |
| 808 | class InvalidStateMachine: public QScxmlStateMachine { |
| 809 | public: |
| 810 | InvalidStateMachine() : QScxmlStateMachine(&QScxmlStateMachine::staticMetaObject) |
| 811 | {} |
| 812 | }; |
| 813 | |
| 814 | auto stateMachine = new InvalidStateMachine; |
| 815 | QScxmlStateMachinePrivate::get(t: stateMachine)->parserData()->m_errors = errors(); |
| 816 | instantiateDataModel(stateMachine); |
| 817 | return stateMachine; |
| 818 | } |
| 819 | #endif // BUILD_QSCXMLC |
| 820 | } |
| 821 | |
| 822 | /*! |
| 823 | * \internal |
| 824 | * Instantiates the data model as described in the SCXML file. |
| 825 | * |
| 826 | * After instantiation, the \a stateMachine takes ownership of the data model. |
| 827 | */ |
| 828 | void QScxmlCompilerPrivate::instantiateDataModel(QScxmlStateMachine *stateMachine) const |
| 829 | { |
| 830 | #ifdef BUILD_QSCXMLC |
| 831 | Q_UNUSED(stateMachine); |
| 832 | #else |
| 833 | if (!m_errors.isEmpty()) { |
| 834 | qWarning() << "SCXML document has errors" ; |
| 835 | return; |
| 836 | } |
| 837 | |
| 838 | auto doc = scxmlDocument(); |
| 839 | auto root = doc ? doc->root : nullptr; |
| 840 | if (root == nullptr) { |
| 841 | qWarning() << "SCXML document has no root element" ; |
| 842 | } else { |
| 843 | QScxmlDataModel *dm = QScxmlDataModelPrivate::instantiateDataModel(type: root->dataModel); |
| 844 | QScxmlStateMachinePrivate::get(t: stateMachine)->parserData()->m_ownedDataModel.reset(other: dm); |
| 845 | stateMachine->setDataModel(dm); |
| 846 | if (dm == nullptr) |
| 847 | qWarning() << "No data-model instantiated" ; |
| 848 | } |
| 849 | #endif // BUILD_QSCXMLC |
| 850 | } |
| 851 | |
| 852 | /*! |
| 853 | * Returns the list of parse errors. |
| 854 | */ |
| 855 | QList<QScxmlError> QScxmlCompiler::errors() const |
| 856 | { |
| 857 | return d->errors(); |
| 858 | } |
| 859 | |
| 860 | bool QScxmlCompilerPrivate::ParserState::collectChars() { |
| 861 | switch (kind) { |
| 862 | case Content: |
| 863 | case Data: |
| 864 | case Script: |
| 865 | return true; |
| 866 | default: |
| 867 | break; |
| 868 | } |
| 869 | return false; |
| 870 | } |
| 871 | |
| 872 | bool QScxmlCompilerPrivate::ParserState::validChild(ParserState::Kind child) const { |
| 873 | return validChild(parent: kind, child); |
| 874 | } |
| 875 | |
| 876 | bool QScxmlCompilerPrivate::ParserState::validChild(ParserState::Kind parent, ParserState::Kind child) |
| 877 | { |
| 878 | switch (parent) { |
| 879 | case ParserState::Scxml: |
| 880 | switch (child) { |
| 881 | case ParserState::State: |
| 882 | case ParserState::Parallel: |
| 883 | case ParserState::Final: |
| 884 | case ParserState::DataModel: |
| 885 | case ParserState::Script: |
| 886 | case ParserState::Transition: |
| 887 | return true; |
| 888 | default: |
| 889 | break; |
| 890 | } |
| 891 | return false; |
| 892 | case ParserState::State: |
| 893 | switch (child) { |
| 894 | case ParserState::OnEntry: |
| 895 | case ParserState::OnExit: |
| 896 | case ParserState::Transition: |
| 897 | case ParserState::Initial: |
| 898 | case ParserState::State: |
| 899 | case ParserState::Parallel: |
| 900 | case ParserState::Final: |
| 901 | case ParserState::History: |
| 902 | case ParserState::DataModel: |
| 903 | case ParserState::Invoke: |
| 904 | return true; |
| 905 | default: |
| 906 | break; |
| 907 | } |
| 908 | return false; |
| 909 | case ParserState::Parallel: |
| 910 | switch (child) { |
| 911 | case ParserState::OnEntry: |
| 912 | case ParserState::OnExit: |
| 913 | case ParserState::Transition: |
| 914 | case ParserState::State: |
| 915 | case ParserState::Parallel: |
| 916 | case ParserState::History: |
| 917 | case ParserState::DataModel: |
| 918 | case ParserState::Invoke: |
| 919 | return true; |
| 920 | default: |
| 921 | break; |
| 922 | } |
| 923 | return false; |
| 924 | case ParserState::Transition: |
| 925 | return isExecutableContent(kind: child); |
| 926 | case ParserState::Initial: |
| 927 | return (child == ParserState::Transition); |
| 928 | case ParserState::Final: |
| 929 | switch (child) { |
| 930 | case ParserState::OnEntry: |
| 931 | case ParserState::OnExit: |
| 932 | case ParserState::DoneData: |
| 933 | return true; |
| 934 | default: |
| 935 | break; |
| 936 | } |
| 937 | return false; |
| 938 | case ParserState::OnEntry: |
| 939 | case ParserState::OnExit: |
| 940 | return isExecutableContent(kind: child); |
| 941 | case ParserState::History: |
| 942 | return child == ParserState::Transition; |
| 943 | case ParserState::Raise: |
| 944 | return false; |
| 945 | case ParserState::If: |
| 946 | return child == ParserState::ElseIf || child == ParserState::Else |
| 947 | || isExecutableContent(kind: child); |
| 948 | case ParserState::ElseIf: |
| 949 | case ParserState::Else: |
| 950 | return false; |
| 951 | case ParserState::Foreach: |
| 952 | return isExecutableContent(kind: child); |
| 953 | case ParserState::Log: |
| 954 | return false; |
| 955 | case ParserState::DataModel: |
| 956 | return (child == ParserState::Data); |
| 957 | case ParserState::Data: |
| 958 | return false; |
| 959 | case ParserState::Assign: |
| 960 | return false; |
| 961 | case ParserState::DoneData: |
| 962 | case ParserState::Send: |
| 963 | return child == ParserState::Content || child == ParserState::Param; |
| 964 | case ParserState::Content: |
| 965 | return child == ParserState::Scxml || isExecutableContent(kind: child); |
| 966 | case ParserState::Param: |
| 967 | case ParserState::Cancel: |
| 968 | return false; |
| 969 | case ParserState::Finalize: |
| 970 | return isExecutableContent(kind: child); |
| 971 | case ParserState::Invoke: |
| 972 | return child == ParserState::Content || child == ParserState::Finalize |
| 973 | || child == ParserState::Param; |
| 974 | case ParserState::Script: |
| 975 | case ParserState::None: |
| 976 | break; |
| 977 | } |
| 978 | return false; |
| 979 | } |
| 980 | |
| 981 | bool QScxmlCompilerPrivate::ParserState::isExecutableContent(ParserState::Kind kind) { |
| 982 | switch (kind) { |
| 983 | case Raise: |
| 984 | case Send: |
| 985 | case Log: |
| 986 | case Script: |
| 987 | case Assign: |
| 988 | case If: |
| 989 | case Foreach: |
| 990 | case Cancel: |
| 991 | case Invoke: |
| 992 | return true; |
| 993 | default: |
| 994 | break; |
| 995 | } |
| 996 | return false; |
| 997 | } |
| 998 | |
| 999 | QScxmlCompilerPrivate::ParserState::Kind QScxmlCompilerPrivate::ParserState::nameToParserStateKind(QStringView name) |
| 1000 | { |
| 1001 | static QMap<QString, ParserState::Kind> nameToKind; |
| 1002 | if (nameToKind.isEmpty()) { |
| 1003 | nameToKind.insert(key: QLatin1String("scxml" ), value: Scxml); |
| 1004 | nameToKind.insert(key: QLatin1String("state" ), value: State); |
| 1005 | nameToKind.insert(key: QLatin1String("parallel" ), value: Parallel); |
| 1006 | nameToKind.insert(key: QLatin1String("transition" ), value: Transition); |
| 1007 | nameToKind.insert(key: QLatin1String("initial" ), value: Initial); |
| 1008 | nameToKind.insert(key: QLatin1String("final" ), value: Final); |
| 1009 | nameToKind.insert(key: QLatin1String("onentry" ), value: OnEntry); |
| 1010 | nameToKind.insert(key: QLatin1String("onexit" ), value: OnExit); |
| 1011 | nameToKind.insert(key: QLatin1String("history" ), value: History); |
| 1012 | nameToKind.insert(key: QLatin1String("raise" ), value: Raise); |
| 1013 | nameToKind.insert(key: QLatin1String("if" ), value: If); |
| 1014 | nameToKind.insert(key: QLatin1String("elseif" ), value: ElseIf); |
| 1015 | nameToKind.insert(key: QLatin1String("else" ), value: Else); |
| 1016 | nameToKind.insert(key: QLatin1String("foreach" ), value: Foreach); |
| 1017 | nameToKind.insert(key: QLatin1String("log" ), value: Log); |
| 1018 | nameToKind.insert(key: QLatin1String("datamodel" ), value: DataModel); |
| 1019 | nameToKind.insert(key: QLatin1String("data" ), value: Data); |
| 1020 | nameToKind.insert(key: QLatin1String("assign" ), value: Assign); |
| 1021 | nameToKind.insert(key: QLatin1String("donedata" ), value: DoneData); |
| 1022 | nameToKind.insert(key: QLatin1String("content" ), value: Content); |
| 1023 | nameToKind.insert(key: QLatin1String("param" ), value: Param); |
| 1024 | nameToKind.insert(key: QLatin1String("script" ), value: Script); |
| 1025 | nameToKind.insert(key: QLatin1String("send" ), value: Send); |
| 1026 | nameToKind.insert(key: QLatin1String("cancel" ), value: Cancel); |
| 1027 | nameToKind.insert(key: QLatin1String("invoke" ), value: Invoke); |
| 1028 | nameToKind.insert(key: QLatin1String("finalize" ), value: Finalize); |
| 1029 | } |
| 1030 | QMap<QString, ParserState::Kind>::ConstIterator it = nameToKind.constBegin(); |
| 1031 | const QMap<QString, ParserState::Kind>::ConstIterator itEnd = nameToKind.constEnd(); |
| 1032 | while (it != itEnd) { |
| 1033 | if (it.key() == name) |
| 1034 | return it.value(); |
| 1035 | ++it; |
| 1036 | } |
| 1037 | return None; |
| 1038 | } |
| 1039 | |
| 1040 | QStringList QScxmlCompilerPrivate::ParserState::requiredAttributes(QScxmlCompilerPrivate::ParserState::Kind kind) |
| 1041 | { |
| 1042 | switch (kind) { |
| 1043 | case Scxml: return QStringList() << QStringLiteral("version" ); |
| 1044 | case State: return QStringList(); |
| 1045 | case Parallel: return QStringList(); |
| 1046 | case Transition: return QStringList(); |
| 1047 | case Initial: return QStringList(); |
| 1048 | case Final: return QStringList(); |
| 1049 | case OnEntry: return QStringList(); |
| 1050 | case OnExit: return QStringList(); |
| 1051 | case History: return QStringList(); |
| 1052 | case Raise: return QStringList() << QStringLiteral("event" ); |
| 1053 | case If: return QStringList() << QStringLiteral("cond" ); |
| 1054 | case ElseIf: return QStringList() << QStringLiteral("cond" ); |
| 1055 | case Else: return QStringList(); |
| 1056 | case Foreach: return QStringList() << QStringLiteral("array" ) |
| 1057 | << QStringLiteral("item" ); |
| 1058 | case Log: return QStringList(); |
| 1059 | case DataModel: return QStringList(); |
| 1060 | case Data: return QStringList() << QStringLiteral("id" ); |
| 1061 | case Assign: return QStringList() << QStringLiteral("location" ); |
| 1062 | case DoneData: return QStringList(); |
| 1063 | case Content: return QStringList(); |
| 1064 | case Param: return QStringList() << QStringLiteral("name" ); |
| 1065 | case Script: return QStringList(); |
| 1066 | case Send: return QStringList(); |
| 1067 | case Cancel: return QStringList(); |
| 1068 | case Invoke: return QStringList(); |
| 1069 | case Finalize: return QStringList(); |
| 1070 | default: return QStringList(); |
| 1071 | } |
| 1072 | return QStringList(); |
| 1073 | } |
| 1074 | |
| 1075 | QStringList QScxmlCompilerPrivate::ParserState::optionalAttributes(QScxmlCompilerPrivate::ParserState::Kind kind) |
| 1076 | { |
| 1077 | switch (kind) { |
| 1078 | case Scxml: return QStringList() << QStringLiteral("initial" ) |
| 1079 | << QStringLiteral("datamodel" ) |
| 1080 | << QStringLiteral("binding" ) |
| 1081 | << QStringLiteral("name" ); |
| 1082 | case State: return QStringList() << QStringLiteral("id" ) |
| 1083 | << QStringLiteral("initial" ); |
| 1084 | case Parallel: return QStringList() << QStringLiteral("id" ); |
| 1085 | case Transition: return QStringList() << QStringLiteral("event" ) |
| 1086 | << QStringLiteral("cond" ) |
| 1087 | << QStringLiteral("target" ) |
| 1088 | << QStringLiteral("type" ); |
| 1089 | case Initial: return QStringList(); |
| 1090 | case Final: return QStringList() << QStringLiteral("id" ); |
| 1091 | case OnEntry: return QStringList(); |
| 1092 | case OnExit: return QStringList(); |
| 1093 | case History: return QStringList() << QStringLiteral("id" ) |
| 1094 | << QStringLiteral("type" ); |
| 1095 | case Raise: return QStringList(); |
| 1096 | case If: return QStringList(); |
| 1097 | case ElseIf: return QStringList(); |
| 1098 | case Else: return QStringList(); |
| 1099 | case Foreach: return QStringList() << QStringLiteral("index" ); |
| 1100 | case Log: return QStringList() << QStringLiteral("label" ) |
| 1101 | << QStringLiteral("expr" ); |
| 1102 | case DataModel: return QStringList(); |
| 1103 | case Data: return QStringList() << QStringLiteral("src" ) |
| 1104 | << QStringLiteral("expr" ); |
| 1105 | case Assign: return QStringList() << QStringLiteral("expr" ); |
| 1106 | case DoneData: return QStringList(); |
| 1107 | case Content: return QStringList() << QStringLiteral("expr" ); |
| 1108 | case Param: return QStringList() << QStringLiteral("expr" ) |
| 1109 | << QStringLiteral("location" ); |
| 1110 | case Script: return QStringList() << QStringLiteral("src" ); |
| 1111 | case Send: return QStringList() << QStringLiteral("event" ) |
| 1112 | << QStringLiteral("eventexpr" ) |
| 1113 | << QStringLiteral("id" ) |
| 1114 | << QStringLiteral("idlocation" ) |
| 1115 | << QStringLiteral("type" ) |
| 1116 | << QStringLiteral("typeexpr" ) |
| 1117 | << QStringLiteral("namelist" ) |
| 1118 | << QStringLiteral("delay" ) |
| 1119 | << QStringLiteral("delayexpr" ) |
| 1120 | << QStringLiteral("target" ) |
| 1121 | << QStringLiteral("targetexpr" ); |
| 1122 | case Cancel: return QStringList() << QStringLiteral("sendid" ) |
| 1123 | << QStringLiteral("sendidexpr" ); |
| 1124 | case Invoke: return QStringList() << QStringLiteral("type" ) |
| 1125 | << QStringLiteral("typeexpr" ) |
| 1126 | << QStringLiteral("src" ) |
| 1127 | << QStringLiteral("srcexpr" ) |
| 1128 | << QStringLiteral("id" ) |
| 1129 | << QStringLiteral("idlocation" ) |
| 1130 | << QStringLiteral("namelist" ) |
| 1131 | << QStringLiteral("autoforward" ); |
| 1132 | case Finalize: return QStringList(); |
| 1133 | default: return QStringList(); |
| 1134 | } |
| 1135 | return QStringList(); |
| 1136 | } |
| 1137 | |
| 1138 | DocumentModel::Node::~Node() |
| 1139 | { |
| 1140 | } |
| 1141 | |
| 1142 | DocumentModel::AbstractState *DocumentModel::Node::asAbstractState() |
| 1143 | { |
| 1144 | if (State *state = asState()) |
| 1145 | return state; |
| 1146 | if (HistoryState *history = asHistoryState()) |
| 1147 | return history; |
| 1148 | return nullptr; |
| 1149 | } |
| 1150 | |
| 1151 | void DocumentModel::DataElement::accept(DocumentModel::NodeVisitor *visitor) |
| 1152 | { |
| 1153 | visitor->visit(this); |
| 1154 | } |
| 1155 | |
| 1156 | void DocumentModel::Param::accept(DocumentModel::NodeVisitor *visitor) |
| 1157 | { |
| 1158 | visitor->visit(this); |
| 1159 | } |
| 1160 | |
| 1161 | void DocumentModel::DoneData::accept(DocumentModel::NodeVisitor *visitor) |
| 1162 | { |
| 1163 | if (visitor->visit(this)) { |
| 1164 | for (Param *param : std::as_const(t&: params)) |
| 1165 | param->accept(visitor); |
| 1166 | } |
| 1167 | visitor->endVisit(this); |
| 1168 | } |
| 1169 | |
| 1170 | void DocumentModel::Send::accept(DocumentModel::NodeVisitor *visitor) |
| 1171 | { |
| 1172 | if (visitor->visit(this)) { |
| 1173 | visitor->visit(params); |
| 1174 | } |
| 1175 | visitor->endVisit(this); |
| 1176 | } |
| 1177 | |
| 1178 | void DocumentModel::Invoke::accept(DocumentModel::NodeVisitor *visitor) |
| 1179 | { |
| 1180 | if (visitor->visit(this)) { |
| 1181 | visitor->visit(params); |
| 1182 | visitor->visit(sequence: &finalize); |
| 1183 | } |
| 1184 | visitor->endVisit(this); |
| 1185 | } |
| 1186 | |
| 1187 | void DocumentModel::Raise::accept(DocumentModel::NodeVisitor *visitor) |
| 1188 | { |
| 1189 | visitor->visit(this); |
| 1190 | } |
| 1191 | |
| 1192 | void DocumentModel::Log::accept(DocumentModel::NodeVisitor *visitor) |
| 1193 | { |
| 1194 | visitor->visit(this); |
| 1195 | } |
| 1196 | |
| 1197 | void DocumentModel::Script::accept(DocumentModel::NodeVisitor *visitor) |
| 1198 | { |
| 1199 | visitor->visit(this); |
| 1200 | } |
| 1201 | |
| 1202 | void DocumentModel::Assign::accept(DocumentModel::NodeVisitor *visitor) |
| 1203 | { |
| 1204 | visitor->visit(this); |
| 1205 | } |
| 1206 | |
| 1207 | void DocumentModel::If::accept(DocumentModel::NodeVisitor *visitor) |
| 1208 | { |
| 1209 | if (visitor->visit(this)) { |
| 1210 | visitor->visit(sequences: blocks); |
| 1211 | } |
| 1212 | visitor->endVisit(this); |
| 1213 | } |
| 1214 | |
| 1215 | void DocumentModel::Foreach::accept(DocumentModel::NodeVisitor *visitor) |
| 1216 | { |
| 1217 | if (visitor->visit(this)) { |
| 1218 | visitor->visit(sequence: &block); |
| 1219 | } |
| 1220 | visitor->endVisit(this); |
| 1221 | } |
| 1222 | |
| 1223 | void DocumentModel::Cancel::accept(DocumentModel::NodeVisitor *visitor) |
| 1224 | { |
| 1225 | visitor->visit(this); |
| 1226 | } |
| 1227 | |
| 1228 | void DocumentModel::State::accept(DocumentModel::NodeVisitor *visitor) |
| 1229 | { |
| 1230 | if (visitor->visit(this)) { |
| 1231 | visitor->visit(dataElements); |
| 1232 | visitor->visit(children); |
| 1233 | visitor->visit(sequences: onEntry); |
| 1234 | visitor->visit(sequences: onExit); |
| 1235 | if (doneData) |
| 1236 | doneData->accept(visitor); |
| 1237 | for (Invoke *invoke : std::as_const(t&: invokes)) |
| 1238 | invoke->accept(visitor); |
| 1239 | } |
| 1240 | visitor->endVisit(this); |
| 1241 | } |
| 1242 | |
| 1243 | void DocumentModel::Transition::accept(DocumentModel::NodeVisitor *visitor) |
| 1244 | { |
| 1245 | if (visitor->visit(this)) { |
| 1246 | visitor->visit(sequence: &instructionsOnTransition); |
| 1247 | } |
| 1248 | visitor->endVisit(this); |
| 1249 | } |
| 1250 | |
| 1251 | void DocumentModel::HistoryState::accept(DocumentModel::NodeVisitor *visitor) |
| 1252 | { |
| 1253 | if (visitor->visit(this)) { |
| 1254 | if (Transition *t = defaultConfiguration()) |
| 1255 | t->accept(visitor); |
| 1256 | } |
| 1257 | visitor->endVisit(this); |
| 1258 | } |
| 1259 | |
| 1260 | void DocumentModel::Scxml::accept(DocumentModel::NodeVisitor *visitor) |
| 1261 | { |
| 1262 | if (visitor->visit(this)) { |
| 1263 | visitor->visit(children); |
| 1264 | visitor->visit(dataElements); |
| 1265 | if (script) |
| 1266 | script->accept(visitor); |
| 1267 | visitor->visit(sequence: &initialSetup); |
| 1268 | } |
| 1269 | visitor->endVisit(this); |
| 1270 | } |
| 1271 | |
| 1272 | DocumentModel::NodeVisitor::~NodeVisitor() |
| 1273 | {} |
| 1274 | |
| 1275 | /*! |
| 1276 | * \class QScxmlCompiler::Loader |
| 1277 | * \brief The Loader class is a URI resolver and resource loader for an SCXML compiler. |
| 1278 | * \since 5.8 |
| 1279 | * \inmodule QtScxml |
| 1280 | */ |
| 1281 | |
| 1282 | /*! |
| 1283 | * Creates a new loader. |
| 1284 | */ |
| 1285 | QScxmlCompiler::Loader::Loader() |
| 1286 | { |
| 1287 | } |
| 1288 | |
| 1289 | /*! |
| 1290 | * Destroys the loader. |
| 1291 | */ |
| 1292 | QScxmlCompiler::Loader::~Loader() |
| 1293 | {} |
| 1294 | |
| 1295 | /*! |
| 1296 | * \fn QScxmlCompiler::Loader::load(const QString &name, const QString &baseDir, QStringList *errors) |
| 1297 | * Resolves the URI \a name and loads an SCXML file from the directory |
| 1298 | * specified by \a baseDir. \a errors contains information about the errors that |
| 1299 | * might have occurred. |
| 1300 | * |
| 1301 | * Returns a QByteArray that stores the contents of the file. |
| 1302 | */ |
| 1303 | |
| 1304 | QScxmlCompilerPrivate *QScxmlCompilerPrivate::get(QScxmlCompiler *compiler) |
| 1305 | { |
| 1306 | return compiler->d; |
| 1307 | } |
| 1308 | |
| 1309 | QScxmlCompilerPrivate::QScxmlCompilerPrivate(QXmlStreamReader *reader) |
| 1310 | : m_currentState(nullptr) |
| 1311 | , m_loader(&m_defaultLoader) |
| 1312 | , m_reader(reader) |
| 1313 | {} |
| 1314 | |
| 1315 | bool QScxmlCompilerPrivate::verifyDocument() |
| 1316 | { |
| 1317 | if (!m_doc) |
| 1318 | return false; |
| 1319 | |
| 1320 | auto handler = [this](const DocumentModel::XmlLocation &location, const QString &msg) { |
| 1321 | this->addError(location, msg); |
| 1322 | }; |
| 1323 | |
| 1324 | if (ScxmlVerifier(handler).verify(doc: m_doc.get())) |
| 1325 | return true; |
| 1326 | else |
| 1327 | return false; |
| 1328 | } |
| 1329 | |
| 1330 | DocumentModel::ScxmlDocument *QScxmlCompilerPrivate::scxmlDocument() const |
| 1331 | { |
| 1332 | return m_doc && m_errors.isEmpty() ? m_doc.get() : nullptr; |
| 1333 | } |
| 1334 | |
| 1335 | QString QScxmlCompilerPrivate::fileName() const |
| 1336 | { |
| 1337 | return m_fileName; |
| 1338 | } |
| 1339 | |
| 1340 | void QScxmlCompilerPrivate::setFileName(const QString &fileName) |
| 1341 | { |
| 1342 | m_fileName = fileName; |
| 1343 | } |
| 1344 | |
| 1345 | QScxmlCompiler::Loader *QScxmlCompilerPrivate::loader() const |
| 1346 | { |
| 1347 | return m_loader; |
| 1348 | } |
| 1349 | |
| 1350 | void QScxmlCompilerPrivate::setLoader(QScxmlCompiler::Loader *loader) |
| 1351 | { |
| 1352 | m_loader = loader; |
| 1353 | } |
| 1354 | |
| 1355 | void QScxmlCompilerPrivate::parseSubDocument(DocumentModel::Invoke *parentInvoke, |
| 1356 | QXmlStreamReader *reader, |
| 1357 | const QString &fileName) |
| 1358 | { |
| 1359 | QScxmlCompiler p(reader); |
| 1360 | p.setFileName(fileName); |
| 1361 | p.setLoader(loader()); |
| 1362 | p.d->readDocument(); |
| 1363 | parentInvoke->content.reset(t: p.d->m_doc.release()); |
| 1364 | m_doc->allSubDocuments.append(t: parentInvoke->content.data()); |
| 1365 | m_errors.append(other: p.errors()); |
| 1366 | } |
| 1367 | |
| 1368 | bool QScxmlCompilerPrivate::parseSubElement(DocumentModel::Invoke *parentInvoke, |
| 1369 | QXmlStreamReader *reader, |
| 1370 | const QString &fileName) |
| 1371 | { |
| 1372 | QScxmlCompiler p(reader); |
| 1373 | p.setFileName(fileName); |
| 1374 | p.setLoader(loader()); |
| 1375 | p.d->resetDocument(); |
| 1376 | bool ok = p.d->readElement(); |
| 1377 | parentInvoke->content.reset(t: p.d->m_doc.release()); |
| 1378 | m_doc->allSubDocuments.append(t: parentInvoke->content.data()); |
| 1379 | m_errors.append(other: p.errors()); |
| 1380 | return ok; |
| 1381 | } |
| 1382 | |
| 1383 | bool QScxmlCompilerPrivate::preReadElementScxml() |
| 1384 | { |
| 1385 | if (m_doc->root) { |
| 1386 | addError(msg: QLatin1String("Doc root already allocated" )); |
| 1387 | return false; |
| 1388 | } |
| 1389 | m_doc->root = new DocumentModel::Scxml(xmlLocation()); |
| 1390 | |
| 1391 | auto scxml = m_doc->root; |
| 1392 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1393 | if (attributes.hasAttribute(QStringLiteral("initial" ))) { |
| 1394 | const QString initial = attributes.value(QStringLiteral("initial" )).toString(); |
| 1395 | scxml->initial += initial.split(sep: QChar::Space, behavior: Qt::SkipEmptyParts); |
| 1396 | } |
| 1397 | |
| 1398 | const QStringView datamodel = attributes.value(qualifiedName: QLatin1String("datamodel" )); |
| 1399 | if (datamodel.isEmpty() || datamodel == QLatin1String("null" )) { |
| 1400 | scxml->dataModel = DocumentModel::Scxml::NullDataModel; |
| 1401 | } else if (datamodel == QLatin1String("ecmascript" )) { |
| 1402 | scxml->dataModel = DocumentModel::Scxml::JSDataModel; |
| 1403 | } else if (datamodel.startsWith(s: QLatin1String("cplusplus" ))) { |
| 1404 | scxml->dataModel = DocumentModel::Scxml::CppDataModel; |
| 1405 | int firstColon = datamodel.indexOf(c: QLatin1Char(':')); |
| 1406 | if (firstColon == -1) { |
| 1407 | scxml->cppDataModelClassName = attributes.value(QStringLiteral("name" )).toString() + QStringLiteral("DataModel" ); |
| 1408 | scxml->cppDataModelHeaderName = scxml->cppDataModelClassName + QStringLiteral(".h" ); |
| 1409 | } else { |
| 1410 | int lastColon = datamodel.lastIndexOf(c: QLatin1Char(':')); |
| 1411 | if (lastColon == -1) { |
| 1412 | lastColon = datamodel.size(); |
| 1413 | } else { |
| 1414 | scxml->cppDataModelHeaderName = datamodel.mid(pos: lastColon + 1).toString(); |
| 1415 | } |
| 1416 | scxml->cppDataModelClassName = datamodel.mid(pos: firstColon + 1, n: lastColon - firstColon - 1).toString(); |
| 1417 | } |
| 1418 | } else { |
| 1419 | addError(QStringLiteral("Unsupported data model '%1' in scxml" ) |
| 1420 | .arg(a: datamodel.toString())); |
| 1421 | } |
| 1422 | const QStringView binding = attributes.value(qualifiedName: QLatin1String("binding" )); |
| 1423 | if (binding.isEmpty() || binding == QLatin1String("early" )) { |
| 1424 | scxml->binding = DocumentModel::Scxml::EarlyBinding; |
| 1425 | } else if (binding == QLatin1String("late" )) { |
| 1426 | scxml->binding = DocumentModel::Scxml::LateBinding; |
| 1427 | } else { |
| 1428 | addError(QStringLiteral("Unsupperted binding type '%1'" ) |
| 1429 | .arg(a: binding.toString())); |
| 1430 | return false; |
| 1431 | } |
| 1432 | const QStringView name = attributes.value(qualifiedName: QLatin1String("name" )); |
| 1433 | if (!name.isEmpty()) { |
| 1434 | scxml->name = name.toString(); |
| 1435 | } |
| 1436 | m_currentState = m_doc->root; |
| 1437 | current().instructionContainer = &m_doc->root->initialSetup; |
| 1438 | return true; |
| 1439 | } |
| 1440 | |
| 1441 | |
| 1442 | bool QScxmlCompilerPrivate::preReadElementState() |
| 1443 | { |
| 1444 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1445 | auto newState = m_doc->newState(parent: m_currentState, type: DocumentModel::State::Normal, xmlLocation: xmlLocation()); |
| 1446 | if (!maybeId(attributes, id: &newState->id)) |
| 1447 | return false; |
| 1448 | |
| 1449 | if (attributes.hasAttribute(QStringLiteral("initial" ))) { |
| 1450 | const QString initial = attributes.value(QStringLiteral("initial" )).toString(); |
| 1451 | newState->initial += initial.split(sep: QChar::Space, behavior: Qt::SkipEmptyParts); |
| 1452 | } |
| 1453 | m_currentState = newState; |
| 1454 | return true; |
| 1455 | } |
| 1456 | |
| 1457 | bool QScxmlCompilerPrivate::preReadElementParallel() |
| 1458 | { |
| 1459 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1460 | auto newState = m_doc->newState(parent: m_currentState, type: DocumentModel::State::Parallel, xmlLocation: xmlLocation()); |
| 1461 | if (!maybeId(attributes, id: &newState->id)) |
| 1462 | return false; |
| 1463 | |
| 1464 | m_currentState = newState; |
| 1465 | return true; |
| 1466 | } |
| 1467 | |
| 1468 | bool QScxmlCompilerPrivate::preReadElementInitial() |
| 1469 | { |
| 1470 | DocumentModel::AbstractState *parent = currentParent(); |
| 1471 | if (!parent) { |
| 1472 | addError(QStringLiteral("<initial> found outside a state" )); |
| 1473 | return false; |
| 1474 | } |
| 1475 | |
| 1476 | DocumentModel::State *parentState = parent->asState(); |
| 1477 | if (!parentState) { |
| 1478 | addError(QStringLiteral("<initial> found outside a state" )); |
| 1479 | return false; |
| 1480 | } |
| 1481 | |
| 1482 | if (parentState->type == DocumentModel::State::Parallel) { |
| 1483 | addError(QStringLiteral("Explicit initial state for parallel states not supported (only implicitly through the initial states of its substates)" )); |
| 1484 | return false; |
| 1485 | } |
| 1486 | return true; |
| 1487 | } |
| 1488 | |
| 1489 | bool QScxmlCompilerPrivate::preReadElementTransition() |
| 1490 | { |
| 1491 | // Parser stack at this point: |
| 1492 | // <transition> |
| 1493 | // <initial> |
| 1494 | // <state> or <scxml> |
| 1495 | // |
| 1496 | // Or: |
| 1497 | // <transition> |
| 1498 | // <state> or <scxml> |
| 1499 | |
| 1500 | DocumentModel::Transition *transition = nullptr; |
| 1501 | if (previous().kind == ParserState::Initial) { |
| 1502 | transition = m_doc->newTransition(parent: nullptr, xmlLocation: xmlLocation()); |
| 1503 | const auto &initialParentState = m_stack.at(i: m_stack.size() - 3); |
| 1504 | if (initialParentState.kind == ParserState::Scxml) { |
| 1505 | m_currentState->asScxml()->initialTransition = transition; |
| 1506 | } else if (initialParentState.kind == ParserState::State) { |
| 1507 | m_currentState->asState()->initialTransition = transition; |
| 1508 | } else { |
| 1509 | Q_UNREACHABLE(); |
| 1510 | } |
| 1511 | } else { |
| 1512 | transition = m_doc->newTransition(parent: m_currentState, xmlLocation: xmlLocation()); |
| 1513 | } |
| 1514 | |
| 1515 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1516 | transition->events = attributes.value(qualifiedName: QLatin1String("event" )).toString().split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts); |
| 1517 | transition->targets = attributes.value(qualifiedName: QLatin1String("target" )).toString().split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts); |
| 1518 | if (attributes.hasAttribute(QStringLiteral("cond" ))) |
| 1519 | transition->condition.reset(other: new QString(attributes.value(qualifiedName: QLatin1String("cond" )).toString())); |
| 1520 | QStringView type = attributes.value(qualifiedName: QLatin1String("type" )); |
| 1521 | if (type.isEmpty() || type == QLatin1String("external" )) { |
| 1522 | transition->type = DocumentModel::Transition::External; |
| 1523 | } else if (type == QLatin1String("internal" )) { |
| 1524 | transition->type = DocumentModel::Transition::Internal; |
| 1525 | } else { |
| 1526 | addError(QStringLiteral("invalid transition type '%1', valid values are 'external' and 'internal'" ).arg(a: type.toString())); |
| 1527 | return true; // TODO: verify me |
| 1528 | } |
| 1529 | current().instructionContainer = &transition->instructionsOnTransition; |
| 1530 | return true; |
| 1531 | } |
| 1532 | |
| 1533 | bool QScxmlCompilerPrivate::preReadElementFinal() |
| 1534 | { |
| 1535 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1536 | auto newState = m_doc->newState(parent: m_currentState, type: DocumentModel::State::Final, xmlLocation: xmlLocation()); |
| 1537 | if (!maybeId(attributes, id: &newState->id)) |
| 1538 | return false; |
| 1539 | m_currentState = newState; |
| 1540 | return true; |
| 1541 | } |
| 1542 | |
| 1543 | bool QScxmlCompilerPrivate::preReadElementHistory() |
| 1544 | { |
| 1545 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1546 | |
| 1547 | DocumentModel::AbstractState *parent = currentParent(); |
| 1548 | if (!parent) { |
| 1549 | addError(QStringLiteral("<history> found outside a state" )); |
| 1550 | return false; |
| 1551 | } |
| 1552 | auto newState = m_doc->newHistoryState(parent, xmlLocation: xmlLocation()); |
| 1553 | if (!maybeId(attributes, id: &newState->id)) |
| 1554 | return false; |
| 1555 | |
| 1556 | const QStringView type = attributes.value(qualifiedName: QLatin1String("type" )); |
| 1557 | if (type.isEmpty() || type == QLatin1String("shallow" )) { |
| 1558 | newState->type = DocumentModel::HistoryState::Shallow; |
| 1559 | } else if (type == QLatin1String("deep" )) { |
| 1560 | newState->type = DocumentModel::HistoryState::Deep; |
| 1561 | } else { |
| 1562 | addError(QStringLiteral("invalid history type %1, valid values are 'shallow' and 'deep'" ).arg(a: type.toString())); |
| 1563 | return false; |
| 1564 | } |
| 1565 | m_currentState = newState; |
| 1566 | return true; |
| 1567 | } |
| 1568 | |
| 1569 | bool QScxmlCompilerPrivate::preReadElementOnEntry() |
| 1570 | { |
| 1571 | const ParserState::Kind previousKind = previous().kind; |
| 1572 | switch (previousKind) { |
| 1573 | case ParserState::Final: |
| 1574 | case ParserState::State: |
| 1575 | case ParserState::Parallel: |
| 1576 | if (DocumentModel::State *s = m_currentState->asState()) { |
| 1577 | current().instructionContainer = m_doc->newSequence(container: &s->onEntry); |
| 1578 | break; |
| 1579 | } |
| 1580 | Q_FALLTHROUGH(); |
| 1581 | default: |
| 1582 | addError(QStringLiteral("unexpected container state for onentry" )); |
| 1583 | break; |
| 1584 | } |
| 1585 | return true; |
| 1586 | } |
| 1587 | |
| 1588 | bool QScxmlCompilerPrivate::preReadElementOnExit() |
| 1589 | { |
| 1590 | ParserState::Kind previousKind = previous().kind; |
| 1591 | switch (previousKind) { |
| 1592 | case ParserState::Final: |
| 1593 | case ParserState::State: |
| 1594 | case ParserState::Parallel: |
| 1595 | if (DocumentModel::State *s = m_currentState->asState()) { |
| 1596 | current().instructionContainer = m_doc->newSequence(container: &s->onExit); |
| 1597 | break; |
| 1598 | } |
| 1599 | Q_FALLTHROUGH(); |
| 1600 | default: |
| 1601 | addError(QStringLiteral("unexpected container state for onexit" )); |
| 1602 | break; |
| 1603 | } |
| 1604 | return true; |
| 1605 | } |
| 1606 | |
| 1607 | bool QScxmlCompilerPrivate::preReadElementRaise() |
| 1608 | { |
| 1609 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1610 | auto raise = m_doc->newNode<DocumentModel::Raise>(xmlLocation: xmlLocation()); |
| 1611 | raise->event = attributes.value(qualifiedName: QLatin1String("event" )).toString(); |
| 1612 | current().instruction = raise; |
| 1613 | return true; |
| 1614 | } |
| 1615 | |
| 1616 | bool QScxmlCompilerPrivate::preReadElementIf() |
| 1617 | { |
| 1618 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1619 | auto *ifI = m_doc->newNode<DocumentModel::If>(xmlLocation: xmlLocation()); |
| 1620 | current().instruction = ifI; |
| 1621 | ifI->conditions.append(t: attributes.value(qualifiedName: QLatin1String("cond" )).toString()); |
| 1622 | current().instructionContainer = m_doc->newSequence(container: &ifI->blocks); |
| 1623 | return true; |
| 1624 | } |
| 1625 | |
| 1626 | bool QScxmlCompilerPrivate::preReadElementElseIf() |
| 1627 | { |
| 1628 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1629 | |
| 1630 | DocumentModel::If *ifI = lastIf(); |
| 1631 | if (!ifI) |
| 1632 | return false; |
| 1633 | |
| 1634 | ifI->conditions.append(t: attributes.value(qualifiedName: QLatin1String("cond" )).toString()); |
| 1635 | previous().instructionContainer = m_doc->newSequence(container: &ifI->blocks); |
| 1636 | return true; |
| 1637 | } |
| 1638 | |
| 1639 | bool QScxmlCompilerPrivate::preReadElementElse() |
| 1640 | { |
| 1641 | DocumentModel::If *ifI = lastIf(); |
| 1642 | if (!ifI) |
| 1643 | return false; |
| 1644 | |
| 1645 | previous().instructionContainer = m_doc->newSequence(container: &ifI->blocks); |
| 1646 | return true; |
| 1647 | } |
| 1648 | |
| 1649 | bool QScxmlCompilerPrivate::preReadElementForeach() |
| 1650 | { |
| 1651 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1652 | auto foreachI = m_doc->newNode<DocumentModel::Foreach>(xmlLocation: xmlLocation()); |
| 1653 | foreachI->array = attributes.value(qualifiedName: QLatin1String("array" )).toString(); |
| 1654 | foreachI->item = attributes.value(qualifiedName: QLatin1String("item" )).toString(); |
| 1655 | foreachI->index = attributes.value(qualifiedName: QLatin1String("index" )).toString(); |
| 1656 | current().instruction = foreachI; |
| 1657 | current().instructionContainer = &foreachI->block; |
| 1658 | return true; |
| 1659 | } |
| 1660 | |
| 1661 | bool QScxmlCompilerPrivate::preReadElementLog() |
| 1662 | { |
| 1663 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1664 | auto logI = m_doc->newNode<DocumentModel::Log>(xmlLocation: xmlLocation()); |
| 1665 | logI->label = attributes.value(qualifiedName: QLatin1String("label" )).toString(); |
| 1666 | logI->expr = attributes.value(qualifiedName: QLatin1String("expr" )).toString(); |
| 1667 | current().instruction = logI; |
| 1668 | return true; |
| 1669 | } |
| 1670 | |
| 1671 | bool QScxmlCompilerPrivate::preReadElementDataModel() |
| 1672 | { |
| 1673 | return true; |
| 1674 | } |
| 1675 | |
| 1676 | bool QScxmlCompilerPrivate::preReadElementData() |
| 1677 | { |
| 1678 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1679 | auto data = m_doc->newNode<DocumentModel::DataElement>(xmlLocation: xmlLocation()); |
| 1680 | data->id = attributes.value(qualifiedName: QLatin1String("id" )).toString(); |
| 1681 | data->src = attributes.value(qualifiedName: QLatin1String("src" )).toString(); |
| 1682 | data->expr = attributes.value(qualifiedName: QLatin1String("expr" )).toString(); |
| 1683 | if (DocumentModel::Scxml *scxml = m_currentState->asScxml()) { |
| 1684 | scxml->dataElements.append(t: data); |
| 1685 | } else if (DocumentModel::State *state = m_currentState->asState()) { |
| 1686 | state->dataElements.append(t: data); |
| 1687 | } else { |
| 1688 | Q_UNREACHABLE(); |
| 1689 | } |
| 1690 | return true; |
| 1691 | } |
| 1692 | |
| 1693 | bool QScxmlCompilerPrivate::preReadElementAssign() |
| 1694 | { |
| 1695 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1696 | auto assign = m_doc->newNode<DocumentModel::Assign>(xmlLocation: xmlLocation()); |
| 1697 | assign->location = attributes.value(qualifiedName: QLatin1String("location" )).toString(); |
| 1698 | assign->expr = attributes.value(qualifiedName: QLatin1String("expr" )).toString(); |
| 1699 | current().instruction = assign; |
| 1700 | return true; |
| 1701 | } |
| 1702 | |
| 1703 | bool QScxmlCompilerPrivate::preReadElementDoneData() |
| 1704 | { |
| 1705 | DocumentModel::State *s = m_currentState->asState(); |
| 1706 | if (s && s->type == DocumentModel::State::Final) { |
| 1707 | if (s->doneData) { |
| 1708 | addError(msg: QLatin1String("state can only have one donedata" )); |
| 1709 | } else { |
| 1710 | s->doneData = m_doc->newNode<DocumentModel::DoneData>(xmlLocation: xmlLocation()); |
| 1711 | } |
| 1712 | } else { |
| 1713 | addError(QStringLiteral("donedata can only occur in a final state" )); |
| 1714 | } |
| 1715 | return true; |
| 1716 | } |
| 1717 | |
| 1718 | bool QScxmlCompilerPrivate::preReadElementContent() |
| 1719 | { |
| 1720 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1721 | ParserState::Kind previousKind = previous().kind; |
| 1722 | switch (previousKind) { |
| 1723 | case ParserState::DoneData: { |
| 1724 | DocumentModel::State *s = m_currentState->asState(); |
| 1725 | Q_ASSERT(s); |
| 1726 | s->doneData->expr = attributes.value(qualifiedName: QLatin1String("expr" )).toString(); |
| 1727 | } break; |
| 1728 | case ParserState::Send: { |
| 1729 | DocumentModel::Send *s = previous().instruction->asSend(); |
| 1730 | Q_ASSERT(s); |
| 1731 | s->contentexpr = attributes.value(qualifiedName: QLatin1String("expr" )).toString(); |
| 1732 | } break; |
| 1733 | case ParserState::Invoke: { |
| 1734 | DocumentModel::Invoke *i = previous().instruction->asInvoke(); |
| 1735 | Q_ASSERT(i); |
| 1736 | Q_UNUSED(i); |
| 1737 | if (attributes.hasAttribute(QStringLiteral("expr" ))) { |
| 1738 | addError(QStringLiteral("expr attribute in content of invoke is not supported" )); |
| 1739 | break; |
| 1740 | } |
| 1741 | } break; |
| 1742 | default: |
| 1743 | addError(QStringLiteral("unexpected parent of content %1" ).arg(a: previous().kind)); |
| 1744 | } |
| 1745 | return true; |
| 1746 | } |
| 1747 | |
| 1748 | bool QScxmlCompilerPrivate::preReadElementParam() |
| 1749 | { |
| 1750 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1751 | auto param = m_doc->newNode<DocumentModel::Param>(xmlLocation: xmlLocation()); |
| 1752 | param->name = attributes.value(qualifiedName: QLatin1String("name" )).toString(); |
| 1753 | param->expr = attributes.value(qualifiedName: QLatin1String("expr" )).toString(); |
| 1754 | param->location = attributes.value(qualifiedName: QLatin1String("location" )).toString(); |
| 1755 | |
| 1756 | ParserState::Kind previousKind = previous().kind; |
| 1757 | switch (previousKind) { |
| 1758 | case ParserState::DoneData: { |
| 1759 | DocumentModel::State *s = m_currentState->asState(); |
| 1760 | Q_ASSERT(s); |
| 1761 | Q_ASSERT(s->doneData); |
| 1762 | s->doneData->params.append(t: param); |
| 1763 | } break; |
| 1764 | case ParserState::Send: { |
| 1765 | DocumentModel::Send *s = previous().instruction->asSend(); |
| 1766 | Q_ASSERT(s); |
| 1767 | s->params.append(t: param); |
| 1768 | } break; |
| 1769 | case ParserState::Invoke: { |
| 1770 | DocumentModel::Invoke *i = previous().instruction->asInvoke(); |
| 1771 | Q_ASSERT(i); |
| 1772 | i->params.append(t: param); |
| 1773 | } break; |
| 1774 | default: |
| 1775 | addError(QStringLiteral("unexpected parent of param %1" ).arg(a: previous().kind)); |
| 1776 | } |
| 1777 | return true; |
| 1778 | } |
| 1779 | |
| 1780 | bool QScxmlCompilerPrivate::preReadElementScript() |
| 1781 | { |
| 1782 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1783 | auto *script = m_doc->newNode<DocumentModel::Script>(xmlLocation: xmlLocation()); |
| 1784 | script->src = attributes.value(qualifiedName: QLatin1String("src" )).toString(); |
| 1785 | current().instruction = script; |
| 1786 | return true; |
| 1787 | } |
| 1788 | |
| 1789 | bool QScxmlCompilerPrivate::preReadElementSend() |
| 1790 | { |
| 1791 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1792 | auto *send = m_doc->newNode<DocumentModel::Send>(xmlLocation: xmlLocation()); |
| 1793 | send->event = attributes.value(qualifiedName: QLatin1String("event" )).toString(); |
| 1794 | send->eventexpr = attributes.value(qualifiedName: QLatin1String("eventexpr" )).toString(); |
| 1795 | send->delay = attributes.value(qualifiedName: QLatin1String("delay" )).toString(); |
| 1796 | send->delayexpr = attributes.value(qualifiedName: QLatin1String("delayexpr" )).toString(); |
| 1797 | send->id = attributes.value(qualifiedName: QLatin1String("id" )).toString(); |
| 1798 | send->idLocation = attributes.value(qualifiedName: QLatin1String("idlocation" )).toString(); |
| 1799 | send->type = attributes.value(qualifiedName: QLatin1String("type" )).toString(); |
| 1800 | send->typeexpr = attributes.value(qualifiedName: QLatin1String("typeexpr" )).toString(); |
| 1801 | send->target = attributes.value(qualifiedName: QLatin1String("target" )).toString(); |
| 1802 | send->targetexpr = attributes.value(qualifiedName: QLatin1String("targetexpr" )).toString(); |
| 1803 | if (attributes.hasAttribute(qualifiedName: QLatin1String("namelist" ))) |
| 1804 | send->namelist = attributes.value(qualifiedName: QLatin1String("namelist" )).toString().split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts); |
| 1805 | current().instruction = send; |
| 1806 | return true; |
| 1807 | } |
| 1808 | |
| 1809 | bool QScxmlCompilerPrivate::preReadElementCancel() |
| 1810 | { |
| 1811 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1812 | auto *cancel = m_doc->newNode<DocumentModel::Cancel>(xmlLocation: xmlLocation()); |
| 1813 | cancel->sendid = attributes.value(qualifiedName: QLatin1String("sendid" )).toString(); |
| 1814 | cancel->sendidexpr = attributes.value(qualifiedName: QLatin1String("sendidexpr" )).toString(); |
| 1815 | current().instruction = cancel; |
| 1816 | return true; |
| 1817 | } |
| 1818 | |
| 1819 | bool QScxmlCompilerPrivate::preReadElementInvoke() |
| 1820 | { |
| 1821 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 1822 | DocumentModel::State *parentState = m_currentState->asState(); |
| 1823 | if (!parentState || |
| 1824 | (parentState->type != DocumentModel::State::Normal && parentState->type != DocumentModel::State::Parallel)) { |
| 1825 | addError(QStringLiteral("invoke can only occur in <state> or <parallel>" )); |
| 1826 | return true; // TODO: verify me |
| 1827 | } |
| 1828 | auto *invoke = m_doc->newNode<DocumentModel::Invoke>(xmlLocation: xmlLocation()); |
| 1829 | parentState->invokes.append(t: invoke); |
| 1830 | invoke->src = attributes.value(qualifiedName: QLatin1String("src" )).toString(); |
| 1831 | invoke->srcexpr = attributes.value(qualifiedName: QLatin1String("srcexpr" )).toString(); |
| 1832 | invoke->id = attributes.value(qualifiedName: QLatin1String("id" )).toString(); |
| 1833 | invoke->idLocation = attributes.value(qualifiedName: QLatin1String("idlocation" )).toString(); |
| 1834 | invoke->type = attributes.value(qualifiedName: QLatin1String("type" )).toString(); |
| 1835 | invoke->typeexpr = attributes.value(qualifiedName: QLatin1String("typeexpr" )).toString(); |
| 1836 | QStringView autoforwardS = attributes.value(qualifiedName: QLatin1String("autoforward" )); |
| 1837 | if (autoforwardS.compare(s: QLatin1String("true" ), cs: Qt::CaseInsensitive) == 0 |
| 1838 | || autoforwardS.compare(s: QLatin1String("yes" ), cs: Qt::CaseInsensitive) == 0 |
| 1839 | || autoforwardS.compare(s: QLatin1String("t" ), cs: Qt::CaseInsensitive) == 0 |
| 1840 | || autoforwardS.compare(s: QLatin1String("y" ), cs: Qt::CaseInsensitive) == 0 |
| 1841 | || autoforwardS == QLatin1String("1" )) |
| 1842 | invoke->autoforward = true; |
| 1843 | else |
| 1844 | invoke->autoforward = false; |
| 1845 | invoke->namelist = attributes.value(qualifiedName: QLatin1String("namelist" )).toString().split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts); |
| 1846 | current().instruction = invoke; |
| 1847 | return true; |
| 1848 | } |
| 1849 | |
| 1850 | bool QScxmlCompilerPrivate::preReadElementFinalize() |
| 1851 | { |
| 1852 | auto instr = previous().instruction; |
| 1853 | if (!instr) { |
| 1854 | addError(QStringLiteral("no previous instruction found for <finalize>" )); |
| 1855 | return false; |
| 1856 | } |
| 1857 | auto invoke = instr->asInvoke(); |
| 1858 | if (!invoke) { |
| 1859 | addError(QStringLiteral("instruction before <finalize> is not <invoke>" )); |
| 1860 | return false; |
| 1861 | } |
| 1862 | current().instructionContainer = &invoke->finalize; |
| 1863 | return true; |
| 1864 | } |
| 1865 | |
| 1866 | bool QScxmlCompilerPrivate::postReadElementScxml() |
| 1867 | { |
| 1868 | return true; |
| 1869 | } |
| 1870 | |
| 1871 | bool QScxmlCompilerPrivate::postReadElementState() |
| 1872 | { |
| 1873 | currentStateUp(); |
| 1874 | return true; |
| 1875 | } |
| 1876 | |
| 1877 | bool QScxmlCompilerPrivate::postReadElementParallel() |
| 1878 | { |
| 1879 | currentStateUp(); |
| 1880 | return true; |
| 1881 | } |
| 1882 | |
| 1883 | bool QScxmlCompilerPrivate::postReadElementInitial() |
| 1884 | { |
| 1885 | return true; |
| 1886 | } |
| 1887 | |
| 1888 | bool QScxmlCompilerPrivate::postReadElementTransition() |
| 1889 | { |
| 1890 | return true; |
| 1891 | } |
| 1892 | |
| 1893 | bool QScxmlCompilerPrivate::postReadElementFinal() |
| 1894 | { |
| 1895 | currentStateUp(); |
| 1896 | return true; |
| 1897 | } |
| 1898 | |
| 1899 | bool QScxmlCompilerPrivate::postReadElementHistory() |
| 1900 | { |
| 1901 | currentStateUp(); |
| 1902 | return true; |
| 1903 | } |
| 1904 | |
| 1905 | bool QScxmlCompilerPrivate::postReadElementOnEntry() |
| 1906 | { |
| 1907 | return true; |
| 1908 | } |
| 1909 | |
| 1910 | bool QScxmlCompilerPrivate::postReadElementOnExit() |
| 1911 | { |
| 1912 | return true; |
| 1913 | } |
| 1914 | |
| 1915 | bool QScxmlCompilerPrivate::postReadElementRaise() |
| 1916 | { |
| 1917 | return flushInstruction(); |
| 1918 | } |
| 1919 | |
| 1920 | bool QScxmlCompilerPrivate::postReadElementIf() |
| 1921 | { |
| 1922 | return flushInstruction(); |
| 1923 | } |
| 1924 | |
| 1925 | bool QScxmlCompilerPrivate::postReadElementElseIf() |
| 1926 | { |
| 1927 | return true; |
| 1928 | } |
| 1929 | |
| 1930 | bool QScxmlCompilerPrivate::postReadElementElse() |
| 1931 | { |
| 1932 | return true; |
| 1933 | } |
| 1934 | |
| 1935 | bool QScxmlCompilerPrivate::postReadElementForeach() |
| 1936 | { |
| 1937 | return flushInstruction(); |
| 1938 | } |
| 1939 | |
| 1940 | bool QScxmlCompilerPrivate::postReadElementLog() |
| 1941 | { |
| 1942 | return flushInstruction(); |
| 1943 | } |
| 1944 | |
| 1945 | bool QScxmlCompilerPrivate::postReadElementDataModel() |
| 1946 | { |
| 1947 | return true; |
| 1948 | } |
| 1949 | |
| 1950 | bool QScxmlCompilerPrivate::postReadElementData() |
| 1951 | { |
| 1952 | const ParserState parserState = current(); |
| 1953 | DocumentModel::DataElement *data = nullptr; |
| 1954 | if (auto state = m_currentState->asState()) { |
| 1955 | data = state->dataElements.last(); |
| 1956 | } else if (auto scxml = m_currentState->asScxml()) { |
| 1957 | data = scxml->dataElements.last(); |
| 1958 | } else { |
| 1959 | Q_UNREACHABLE(); |
| 1960 | } |
| 1961 | if (!data->src.isEmpty() && !data->expr.isEmpty()) { |
| 1962 | addError(QStringLiteral("data element with both 'src' and 'expr' attributes" )); |
| 1963 | return false; |
| 1964 | } |
| 1965 | if (!parserState.chars.trimmed().isEmpty()) { |
| 1966 | if (!data->src.isEmpty()) { |
| 1967 | addError(QStringLiteral("data element with both 'src' attribute and CDATA" )); |
| 1968 | return false; |
| 1969 | } else if (!data->expr.isEmpty()) { |
| 1970 | addError(QStringLiteral("data element with both 'expr' attribute and CDATA" )); |
| 1971 | return false; |
| 1972 | } else { |
| 1973 | // w3c-ecma/test558 - "if a child element of <data> is not a XML, |
| 1974 | // treat it as a string with whitespace normalization" |
| 1975 | // We've modified the test, so that a string is enclosed with quotes. |
| 1976 | data->expr = parserState.chars; |
| 1977 | } |
| 1978 | } else if (!data->src.isEmpty()) { |
| 1979 | if (!m_loader) { |
| 1980 | addError(QStringLiteral("cannot parse a document with external dependencies without a loader" )); |
| 1981 | } else { |
| 1982 | bool ok; |
| 1983 | const QByteArray ba = load(name: data->src, ok: &ok); |
| 1984 | if (!ok) { |
| 1985 | addError(QStringLiteral("failed to load external dependency" )); |
| 1986 | } else { |
| 1987 | // w3c-ecma/test558 - "if XML is loaded via "src" attribute, |
| 1988 | // treat it as a string with whitespace normalization" |
| 1989 | // We've enclosed the text in file with quotes. |
| 1990 | data->expr = QString::fromUtf8(ba); |
| 1991 | } |
| 1992 | } |
| 1993 | } |
| 1994 | return true; |
| 1995 | } |
| 1996 | |
| 1997 | bool QScxmlCompilerPrivate::postReadElementAssign() |
| 1998 | { |
| 1999 | return flushInstruction(); |
| 2000 | } |
| 2001 | |
| 2002 | bool QScxmlCompilerPrivate::postReadElementDoneData() |
| 2003 | { |
| 2004 | return true; |
| 2005 | } |
| 2006 | |
| 2007 | bool QScxmlCompilerPrivate::postReadElementContent() |
| 2008 | { |
| 2009 | const ParserState parserState = current(); |
| 2010 | if (!parserState.chars.trimmed().isEmpty()) { |
| 2011 | |
| 2012 | switch (previous().kind) { |
| 2013 | case ParserState::DoneData: // see test529 |
| 2014 | m_currentState->asState()->doneData->contents = parserState.chars.simplified(); |
| 2015 | break; |
| 2016 | case ParserState::Send: // see test179 |
| 2017 | previous().instruction->asSend()->content = parserState.chars.simplified(); |
| 2018 | break; |
| 2019 | default: |
| 2020 | break; |
| 2021 | } |
| 2022 | } |
| 2023 | return true; |
| 2024 | } |
| 2025 | |
| 2026 | bool QScxmlCompilerPrivate::postReadElementParam() |
| 2027 | { |
| 2028 | return true; |
| 2029 | } |
| 2030 | |
| 2031 | bool QScxmlCompilerPrivate::postReadElementScript() |
| 2032 | { |
| 2033 | const ParserState parserState = current(); |
| 2034 | DocumentModel::Script *scriptI = parserState.instruction->asScript(); |
| 2035 | if (!parserState.chars.trimmed().isEmpty()) { |
| 2036 | scriptI->content = parserState.chars.trimmed(); |
| 2037 | if (!scriptI->src.isEmpty()) |
| 2038 | addError(QStringLiteral("both src and source content given to script, will ignore external content" )); |
| 2039 | } else if (!scriptI->src.isEmpty()) { |
| 2040 | if (!m_loader) { |
| 2041 | addError(QStringLiteral("cannot parse a document with external dependencies without a loader" )); |
| 2042 | } else { |
| 2043 | bool ok; |
| 2044 | const QByteArray data = load(name: scriptI->src, ok: &ok); |
| 2045 | if (!ok) { |
| 2046 | addError(QStringLiteral("failed to load external dependency" )); |
| 2047 | } else { |
| 2048 | scriptI->content = QString::fromUtf8(ba: data); |
| 2049 | } |
| 2050 | } |
| 2051 | } else { |
| 2052 | addError(location: scriptI->xmlLocation, |
| 2053 | QStringLiteral("neither src nor any content has been given in the script tag" )); |
| 2054 | } |
| 2055 | return flushInstruction(); |
| 2056 | } |
| 2057 | |
| 2058 | bool QScxmlCompilerPrivate::postReadElementSend() |
| 2059 | { |
| 2060 | return flushInstruction(); |
| 2061 | } |
| 2062 | |
| 2063 | bool QScxmlCompilerPrivate::postReadElementCancel() |
| 2064 | { |
| 2065 | return flushInstruction(); |
| 2066 | } |
| 2067 | |
| 2068 | bool QScxmlCompilerPrivate::postReadElementInvoke() |
| 2069 | { |
| 2070 | DocumentModel::Invoke *i = current().instruction->asInvoke(); |
| 2071 | const QString fileName = i->src; |
| 2072 | if (!i->content.data()) { |
| 2073 | if (!fileName.isEmpty()) { |
| 2074 | bool ok = true; |
| 2075 | const QByteArray data = load(name: fileName, ok: &ok); |
| 2076 | if (!ok) { |
| 2077 | addError(QStringLiteral("failed to load external dependency" )); |
| 2078 | } else { |
| 2079 | QXmlStreamReader reader(data); |
| 2080 | parseSubDocument(parentInvoke: i, reader: &reader, fileName); |
| 2081 | } |
| 2082 | } |
| 2083 | } else if (!fileName.isEmpty()) { |
| 2084 | addError(QStringLiteral("both src and content given to invoke" )); |
| 2085 | } |
| 2086 | |
| 2087 | return true; |
| 2088 | } |
| 2089 | |
| 2090 | bool QScxmlCompilerPrivate::postReadElementFinalize() |
| 2091 | { |
| 2092 | return true; |
| 2093 | } |
| 2094 | |
| 2095 | void QScxmlCompilerPrivate::resetDocument() |
| 2096 | { |
| 2097 | m_doc.reset(p: new DocumentModel::ScxmlDocument(fileName())); |
| 2098 | } |
| 2099 | |
| 2100 | bool QScxmlCompilerPrivate::readDocument() |
| 2101 | { |
| 2102 | resetDocument(); |
| 2103 | m_currentState = m_doc->root; |
| 2104 | for (bool finished = false; !finished && !m_reader->hasError();) { |
| 2105 | switch (m_reader->readNext()) { |
| 2106 | case QXmlStreamReader::StartElement : { |
| 2107 | const QStringView newTag = m_reader->name(); |
| 2108 | const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(name: newTag); |
| 2109 | |
| 2110 | auto ns = m_reader->namespaceUri(); |
| 2111 | |
| 2112 | if (ns != scxmlNamespace) { |
| 2113 | m_reader->skipCurrentElement(); |
| 2114 | } else if (newElementKind == ParserState::None) { |
| 2115 | addError(QStringLiteral("Unknown element %1" ).arg(a: newTag.toString())); |
| 2116 | m_reader->skipCurrentElement(); |
| 2117 | } else if (newElementKind == ParserState::Scxml) { |
| 2118 | if (readElement() == false) |
| 2119 | return false; |
| 2120 | } else { |
| 2121 | addError(QStringLiteral("Unexpected element %1" ).arg(a: newTag.toString())); |
| 2122 | m_reader->skipCurrentElement(); |
| 2123 | } |
| 2124 | } |
| 2125 | break; |
| 2126 | case QXmlStreamReader::EndElement : |
| 2127 | finished = true; |
| 2128 | break; |
| 2129 | default : |
| 2130 | break; |
| 2131 | } |
| 2132 | } |
| 2133 | if (!m_doc->root) { |
| 2134 | addError(QStringLiteral("Missing root element" )); |
| 2135 | return false; |
| 2136 | } |
| 2137 | |
| 2138 | if (m_reader->hasError() && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError) { |
| 2139 | addError(QStringLiteral("Error parsing SCXML file: %1" ).arg(a: m_reader->errorString())); |
| 2140 | return false; |
| 2141 | } |
| 2142 | |
| 2143 | return true; |
| 2144 | } |
| 2145 | |
| 2146 | bool QScxmlCompilerPrivate::readElement() |
| 2147 | { |
| 2148 | const QStringView currentTag = m_reader->name(); |
| 2149 | const QXmlStreamAttributes attributes = m_reader->attributes(); |
| 2150 | |
| 2151 | const ParserState::Kind elementKind = ParserState::nameToParserStateKind(name: currentTag); |
| 2152 | |
| 2153 | if (!checkAttributes(attributes, kind: elementKind)) |
| 2154 | return false; |
| 2155 | |
| 2156 | if (elementKind == ParserState::Scxml && m_doc->root) { |
| 2157 | if (!hasPrevious()) { |
| 2158 | addError(QStringLiteral("misplaced scxml" )); |
| 2159 | return false; |
| 2160 | } |
| 2161 | |
| 2162 | DocumentModel::Invoke *i = previous().instruction->asInvoke(); |
| 2163 | if (!i) { |
| 2164 | addError(QStringLiteral("misplaced scxml" )); |
| 2165 | return false; |
| 2166 | } |
| 2167 | |
| 2168 | return parseSubElement(parentInvoke: i, reader: m_reader, fileName: m_fileName); |
| 2169 | } |
| 2170 | |
| 2171 | if (elementKind != ParserState::Scxml && !m_stack.size()) { |
| 2172 | addError(QStringLiteral("misplaced %1" ).arg(a: currentTag.toString())); |
| 2173 | return false; |
| 2174 | } |
| 2175 | |
| 2176 | ParserState pNew = ParserState(elementKind); |
| 2177 | |
| 2178 | m_stack.append(t: pNew); |
| 2179 | |
| 2180 | switch (elementKind) { |
| 2181 | case ParserState::Scxml: if (!preReadElementScxml()) return false; break; |
| 2182 | case ParserState::State: if (!preReadElementState()) return false; break; |
| 2183 | case ParserState::Parallel: if (!preReadElementParallel()) return false; break; |
| 2184 | case ParserState::Initial: if (!preReadElementInitial()) return false; break; |
| 2185 | case ParserState::Transition: if (!preReadElementTransition()) return false; break; |
| 2186 | case ParserState::Final: if (!preReadElementFinal()) return false; break; |
| 2187 | case ParserState::History: if (!preReadElementHistory()) return false; break; |
| 2188 | case ParserState::OnEntry: if (!preReadElementOnEntry()) return false; break; |
| 2189 | case ParserState::OnExit: if (!preReadElementOnExit()) return false; break; |
| 2190 | case ParserState::Raise: if (!preReadElementRaise()) return false; break; |
| 2191 | case ParserState::If: if (!preReadElementIf()) return false; break; |
| 2192 | case ParserState::ElseIf: if (!preReadElementElseIf()) return false; break; |
| 2193 | case ParserState::Else: if (!preReadElementElse()) return false; break; |
| 2194 | case ParserState::Foreach: if (!preReadElementForeach()) return false; break; |
| 2195 | case ParserState::Log: if (!preReadElementLog()) return false; break; |
| 2196 | case ParserState::DataModel: if (!preReadElementDataModel()) return false; break; |
| 2197 | case ParserState::Data: if (!preReadElementData()) return false; break; |
| 2198 | case ParserState::Assign: if (!preReadElementAssign()) return false; break; |
| 2199 | case ParserState::DoneData: if (!preReadElementDoneData()) return false; break; |
| 2200 | case ParserState::Content: if (!preReadElementContent()) return false; break; |
| 2201 | case ParserState::Param: if (!preReadElementParam()) return false; break; |
| 2202 | case ParserState::Script: if (!preReadElementScript()) return false; break; |
| 2203 | case ParserState::Send: if (!preReadElementSend()) return false; break; |
| 2204 | case ParserState::Cancel: if (!preReadElementCancel()) return false; break; |
| 2205 | case ParserState::Invoke: if (!preReadElementInvoke()) return false; break; |
| 2206 | case ParserState::Finalize: if (!preReadElementFinalize()) return false; break; |
| 2207 | default: addError(QStringLiteral("Unknown element %1" ).arg(a: currentTag.toString())); return false; |
| 2208 | } |
| 2209 | |
| 2210 | for (bool finished = false; !finished && !m_reader->hasError();) { |
| 2211 | switch (m_reader->readNext()) { |
| 2212 | case QXmlStreamReader::StartElement : { |
| 2213 | const QStringView newTag = m_reader->name(); |
| 2214 | const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(name: newTag); |
| 2215 | |
| 2216 | auto ns = m_reader->namespaceUri(); |
| 2217 | |
| 2218 | if (ns != scxmlNamespace) { |
| 2219 | m_reader->skipCurrentElement(); |
| 2220 | } else if (newElementKind == ParserState::None) { |
| 2221 | addError(QStringLiteral("Unknown element %1" ).arg(a: newTag.toString())); |
| 2222 | m_reader->skipCurrentElement(); |
| 2223 | } else if (pNew.validChild(child: newElementKind)) { |
| 2224 | if (readElement() == false) |
| 2225 | return false; |
| 2226 | } else { |
| 2227 | addError(QStringLiteral("Unexpected element %1" ).arg(a: newTag.toString())); |
| 2228 | m_reader->skipCurrentElement(); |
| 2229 | } |
| 2230 | } |
| 2231 | break; |
| 2232 | case QXmlStreamReader::EndElement : |
| 2233 | finished = true; |
| 2234 | break; |
| 2235 | case QXmlStreamReader::Characters : |
| 2236 | if (m_stack.isEmpty()) |
| 2237 | break; |
| 2238 | if (current().collectChars()) |
| 2239 | current().chars.append(v: m_reader->text()); |
| 2240 | break; |
| 2241 | default : |
| 2242 | break; |
| 2243 | } |
| 2244 | } |
| 2245 | |
| 2246 | switch (elementKind) { |
| 2247 | case ParserState::Scxml: if (!postReadElementScxml()) return false; break; |
| 2248 | case ParserState::State: if (!postReadElementState()) return false; break; |
| 2249 | case ParserState::Parallel: if (!postReadElementParallel()) return false; break; |
| 2250 | case ParserState::Initial: if (!postReadElementInitial()) return false; break; |
| 2251 | case ParserState::Transition: if (!postReadElementTransition()) return false; break; |
| 2252 | case ParserState::Final: if (!postReadElementFinal()) return false; break; |
| 2253 | case ParserState::History: if (!postReadElementHistory()) return false; break; |
| 2254 | case ParserState::OnEntry: if (!postReadElementOnEntry()) return false; break; |
| 2255 | case ParserState::OnExit: if (!postReadElementOnExit()) return false; break; |
| 2256 | case ParserState::Raise: if (!postReadElementRaise()) return false; break; |
| 2257 | case ParserState::If: if (!postReadElementIf()) return false; break; |
| 2258 | case ParserState::ElseIf: if (!postReadElementElseIf()) return false; break; |
| 2259 | case ParserState::Else: if (!postReadElementElse()) return false; break; |
| 2260 | case ParserState::Foreach: if (!postReadElementForeach()) return false; break; |
| 2261 | case ParserState::Log: if (!postReadElementLog()) return false; break; |
| 2262 | case ParserState::DataModel: if (!postReadElementDataModel()) return false; break; |
| 2263 | case ParserState::Data: if (!postReadElementData()) return false; break; |
| 2264 | case ParserState::Assign: if (!postReadElementAssign()) return false; break; |
| 2265 | case ParserState::DoneData: if (!postReadElementDoneData()) return false; break; |
| 2266 | case ParserState::Content: if (!postReadElementContent()) return false; break; |
| 2267 | case ParserState::Param: if (!postReadElementParam()) return false; break; |
| 2268 | case ParserState::Script: if (!postReadElementScript()) return false; break; |
| 2269 | case ParserState::Send: if (!postReadElementSend()) return false; break; |
| 2270 | case ParserState::Cancel: if (!postReadElementCancel()) return false; break; |
| 2271 | case ParserState::Invoke: if (!postReadElementInvoke()) return false; break; |
| 2272 | case ParserState::Finalize: if (!postReadElementFinalize()) return false; break; |
| 2273 | default: break; |
| 2274 | } |
| 2275 | |
| 2276 | m_stack.removeLast(); |
| 2277 | |
| 2278 | if (m_reader->hasError()/* && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError*/) { |
| 2279 | addError(QStringLiteral("Error parsing SCXML file: %1" ).arg(a: m_reader->errorString())); |
| 2280 | return false; |
| 2281 | } |
| 2282 | |
| 2283 | return true; |
| 2284 | } |
| 2285 | |
| 2286 | void QScxmlCompilerPrivate::currentStateUp() |
| 2287 | { |
| 2288 | Q_ASSERT(m_currentState->parent); |
| 2289 | m_currentState = m_currentState->parent; |
| 2290 | } |
| 2291 | |
| 2292 | bool QScxmlCompilerPrivate::flushInstruction() |
| 2293 | { |
| 2294 | if (!hasPrevious()) { |
| 2295 | addError(QStringLiteral("missing instructionContainer" )); |
| 2296 | return false; |
| 2297 | } |
| 2298 | DocumentModel::InstructionSequence *instructions = previous().instructionContainer; |
| 2299 | if (!instructions) { |
| 2300 | addError(QStringLiteral("got executable content within an element that did not set instructionContainer" )); |
| 2301 | return false; |
| 2302 | } |
| 2303 | instructions->append(t: current().instruction); |
| 2304 | return true; |
| 2305 | } |
| 2306 | |
| 2307 | |
| 2308 | QByteArray QScxmlCompilerPrivate::load(const QString &name, bool *ok) |
| 2309 | { |
| 2310 | QStringList errs; |
| 2311 | const QByteArray result = m_loader->load(name, baseDir: m_fileName.isEmpty() ? |
| 2312 | QString() : QFileInfo(m_fileName).path(), errors: &errs); |
| 2313 | for (const QString &err : errs) |
| 2314 | addError(msg: err); |
| 2315 | |
| 2316 | *ok = errs.isEmpty(); |
| 2317 | |
| 2318 | return result; |
| 2319 | } |
| 2320 | |
| 2321 | QList<QScxmlError> QScxmlCompilerPrivate::errors() const |
| 2322 | { |
| 2323 | return m_errors; |
| 2324 | } |
| 2325 | |
| 2326 | void QScxmlCompilerPrivate::addError(const QString &msg) |
| 2327 | { |
| 2328 | m_errors.append(t: QScxmlError(m_fileName, m_reader->lineNumber(), m_reader->columnNumber(), msg)); |
| 2329 | } |
| 2330 | |
| 2331 | void QScxmlCompilerPrivate::addError(const DocumentModel::XmlLocation &location, const QString &msg) |
| 2332 | { |
| 2333 | m_errors.append(t: QScxmlError(m_fileName, location.line, location.column, msg)); |
| 2334 | } |
| 2335 | |
| 2336 | DocumentModel::AbstractState *QScxmlCompilerPrivate::currentParent() const |
| 2337 | { |
| 2338 | return m_currentState ? m_currentState->asAbstractState() : nullptr; |
| 2339 | } |
| 2340 | |
| 2341 | DocumentModel::XmlLocation QScxmlCompilerPrivate::xmlLocation() const |
| 2342 | { |
| 2343 | return DocumentModel::XmlLocation(m_reader->lineNumber(), m_reader->columnNumber()); |
| 2344 | } |
| 2345 | |
| 2346 | bool QScxmlCompilerPrivate::maybeId(const QXmlStreamAttributes &attributes, QString *id) |
| 2347 | { |
| 2348 | Q_ASSERT(id); |
| 2349 | QString idStr = attributes.value(qualifiedName: QLatin1String("id" )).toString(); |
| 2350 | if (!idStr.isEmpty()) { |
| 2351 | if (m_allIds.contains(value: idStr)) { |
| 2352 | addError(location: xmlLocation(), QStringLiteral("duplicate id '%1'" ).arg(a: idStr)); |
| 2353 | } else { |
| 2354 | m_allIds.insert(value: idStr); |
| 2355 | *id = idStr; |
| 2356 | } |
| 2357 | } |
| 2358 | return true; |
| 2359 | } |
| 2360 | |
| 2361 | DocumentModel::If *QScxmlCompilerPrivate::lastIf() |
| 2362 | { |
| 2363 | if (!hasPrevious()) { |
| 2364 | addError(QStringLiteral("No previous instruction found for else block" )); |
| 2365 | return nullptr; |
| 2366 | } |
| 2367 | |
| 2368 | DocumentModel::Instruction *lastI = previous().instruction; |
| 2369 | if (!lastI) { |
| 2370 | addError(QStringLiteral("No previous instruction found for else block" )); |
| 2371 | return nullptr; |
| 2372 | } |
| 2373 | DocumentModel::If *ifI = lastI->asIf(); |
| 2374 | if (!ifI) { |
| 2375 | addError(QStringLiteral("Previous instruction for else block is not an 'if'" )); |
| 2376 | return nullptr; |
| 2377 | } |
| 2378 | return ifI; |
| 2379 | } |
| 2380 | |
| 2381 | QScxmlCompilerPrivate::ParserState &QScxmlCompilerPrivate::current() |
| 2382 | { |
| 2383 | return m_stack.last(); |
| 2384 | } |
| 2385 | |
| 2386 | QScxmlCompilerPrivate::ParserState &QScxmlCompilerPrivate::previous() |
| 2387 | { |
| 2388 | return m_stack[m_stack.size() - 2]; |
| 2389 | } |
| 2390 | |
| 2391 | bool QScxmlCompilerPrivate::hasPrevious() const |
| 2392 | { |
| 2393 | return m_stack.size() > 1; |
| 2394 | } |
| 2395 | |
| 2396 | bool QScxmlCompilerPrivate::checkAttributes(const QXmlStreamAttributes &attributes, |
| 2397 | QScxmlCompilerPrivate::ParserState::Kind kind) |
| 2398 | { |
| 2399 | return checkAttributes(attributes, |
| 2400 | requiredNames: ParserState::requiredAttributes(kind), |
| 2401 | optionalNames: ParserState::optionalAttributes(kind)); |
| 2402 | } |
| 2403 | |
| 2404 | bool QScxmlCompilerPrivate::checkAttributes(const QXmlStreamAttributes &attributes, |
| 2405 | const QStringList &requiredNames, |
| 2406 | const QStringList &optionalNames) |
| 2407 | { |
| 2408 | QStringList required = requiredNames; |
| 2409 | for (const QXmlStreamAttribute &attribute : attributes) { |
| 2410 | const QStringView ns = attribute.namespaceUri(); |
| 2411 | if (!ns.isEmpty() && ns != scxmlNamespace && ns != qtScxmlNamespace) |
| 2412 | continue; |
| 2413 | |
| 2414 | const QString name = attribute.name().toString(); |
| 2415 | if (!required.removeOne(t: name) && !optionalNames.contains(str: name)) { |
| 2416 | addError(QStringLiteral("Unexpected attribute '%1'" ).arg(a: name)); |
| 2417 | return false; |
| 2418 | } |
| 2419 | } |
| 2420 | if (!required.isEmpty()) { |
| 2421 | addError(QStringLiteral("Missing required attributes: '%1'" ) |
| 2422 | .arg(a: required.join(sep: QLatin1String("', '" )))); |
| 2423 | return false; |
| 2424 | } |
| 2425 | return true; |
| 2426 | } |
| 2427 | |
| 2428 | QScxmlCompilerPrivate::DefaultLoader::DefaultLoader() |
| 2429 | : Loader() |
| 2430 | {} |
| 2431 | |
| 2432 | QByteArray QScxmlCompilerPrivate::DefaultLoader::load(const QString &name, const QString &baseDir, QStringList *errors) |
| 2433 | { |
| 2434 | QStringList errs; |
| 2435 | QByteArray contents; |
| 2436 | #ifdef BUILD_QSCXMLC |
| 2437 | QString cleanName = name; |
| 2438 | if (name.startsWith(QStringLiteral("file:" ))) |
| 2439 | cleanName = name.mid(5); |
| 2440 | QFileInfo fInfo(cleanName); |
| 2441 | #else |
| 2442 | const QUrl url(name); |
| 2443 | if (!url.isLocalFile() && !url.isRelative()) |
| 2444 | errs << QStringLiteral("src attribute is not a local file (%1)" ).arg(a: name); |
| 2445 | QFileInfo fInfo(url.isLocalFile() ? url.toLocalFile() : name); |
| 2446 | #endif // BUILD_QSCXMLC |
| 2447 | if (fInfo.isRelative()) |
| 2448 | fInfo = QFileInfo(QDir(baseDir).filePath(fileName: fInfo.filePath())); |
| 2449 | |
| 2450 | if (!fInfo.exists()) { |
| 2451 | errs << QStringLiteral("src attribute resolves to non existing file (%1)" ).arg(a: fInfo.filePath()); |
| 2452 | } else { |
| 2453 | QFile f(fInfo.filePath()); |
| 2454 | if (f.open(flags: QFile::ReadOnly)) |
| 2455 | contents = f.readAll(); |
| 2456 | else |
| 2457 | errs << QStringLiteral("Failure opening file %1: %2" ) |
| 2458 | .arg(args: fInfo.filePath(), args: f.errorString()); |
| 2459 | } |
| 2460 | if (errors) |
| 2461 | *errors = errs; |
| 2462 | |
| 2463 | return contents; |
| 2464 | } |
| 2465 | |
| 2466 | QScxmlCompilerPrivate::ParserState::ParserState(QScxmlCompilerPrivate::ParserState::Kind someKind) |
| 2467 | : kind(someKind) |
| 2468 | , instruction(0) |
| 2469 | , instructionContainer(0) |
| 2470 | {} |
| 2471 | |
| 2472 | QT_END_NAMESPACE |
| 2473 | |
| 2474 | #ifndef BUILD_QSCXMLC |
| 2475 | #include "qscxmlcompiler.moc" |
| 2476 | #endif |
| 2477 | |