1// Copyright (C) 2021 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 "qqmlxmllistmodel_p.h"
5
6#include <QtQml/qqmlcontext.h>
7#include <QtQml/qqmlengine.h>
8#include <QtQml/qqmlinfo.h>
9#include <QtQml/qqmlfile.h>
10
11#include <QtCore/qcoreapplication.h>
12#include <QtCore/qfile.h>
13#include <QtCore/qfuturewatcher.h>
14#include <QtCore/qtimer.h>
15#include <QtCore/qxmlstream.h>
16
17#if QT_CONFIG(qml_network)
18#include <QtNetwork/qnetworkreply.h>
19#include <QtNetwork/qnetworkrequest.h>
20#endif
21
22Q_DECLARE_METATYPE(QQmlXmlListModelQueryResult)
23
24QT_BEGIN_NAMESPACE
25
26/*!
27 \qmlmodule QtQml.XmlListModel
28 \title Qt XmlListModel QML Types
29 \keyword Qt XmlListModel QML Types
30 \ingroup qmlmodules
31 \brief Provides QML types for creating models from XML data
32
33 This QML module contains types for creating models from XML data.
34
35 To use the types in this module, import the module with the following line:
36
37 \qml
38 import QtQml.XmlListModel
39 \endqml
40*/
41
42/*!
43 \qmltype XmlListModelRole
44 \inqmlmodule QtQml.XmlListModel
45 \brief For specifying a role to an \l XmlListModel.
46
47 \sa {All QML Types}{Qt QML}
48*/
49
50/*!
51 \qmlproperty string QtQml.XmlListModel::XmlListModelRole::name
52
53 The name for the role. This name is used to access the model data for this
54 role.
55
56 For example, the following model has a role named "title", which can be
57 accessed from the view's delegate:
58
59 \qml
60 XmlListModel {
61 id: xmlModel
62 source: "file.xml"
63 query: "/documents/document"
64 XmlListModelRole { name: "title"; elementName: "title" }
65 }
66 \endqml
67
68 \qml
69 ListView {
70 model: xmlModel
71 delegate: Text { text: title }
72 }
73 \endqml
74*/
75QString QQmlXmlListModelRole::name() const
76{
77 return m_name;
78}
79
80void QQmlXmlListModelRole::setName(const QString &name)
81{
82 if (name == m_name)
83 return;
84 m_name = name;
85 Q_EMIT nameChanged();
86}
87
88/*!
89 \qmlproperty string QtQml.XmlListModel::XmlListModelRole::elementName
90
91 The name of the XML element, or a path to the XML element, that will be
92 used to read the data. The element must actually contain text.
93
94 Optionally the \l attributeName property can be specified to extract
95 the data.
96
97//! [basic-example]
98 For example, the following model has a role named "title", which reads the
99 data from the XML element \c {<title>}. It also has another role named
100 "timestamp", which uses the same XML element \c {<title>}, but reads its
101 "created" attribute to extract the actual value.
102
103 \qml
104 XmlListModel {
105 id: xmlModel
106 source: "file.xml"
107 query: "/documents/document"
108 XmlListModelRole { name: "title"; elementName: "title" }
109 XmlListModelRole {
110 name: "timestamp"
111 elementName: "title"
112 attributeName: "created"
113 }
114 }
115
116 ListView {
117 anchors.fill: parent
118 model: xmlModel
119 delegate: Text { text: title + " created on " + timestamp }
120 }
121 \endqml
122//! [basic-example]
123
124//! [empty-elementName-example]
125 When the \l attributeName is specified, the \l elementName can be left
126 empty. In this case the attribute of the top level XML element of the query
127 will be read.
128
129 For example, if you have the following xml document:
130
131 \code
132 <documents>
133 <document title="Title1"/>
134 <document title="Title2"/>
135 </documents>
136 \endcode
137
138 To extract the document titles you need the following model:
139
140 \qml
141 XmlListModel {
142 id: xmlModel
143 source: "file.xml"
144 query: "/documents/document"
145 XmlListModelRole {
146 name: "title"
147 elementName: ""
148 attributeName: "title"
149 }
150 }
151 \endqml
152//! [empty-elementName-example]
153
154 The elementName property can actually contain a path to the nested xml
155 element. All the elements in the path must be joined with the \c {'/'}
156 character.
157
158 For example, if you have the following xml document:
159 \code
160 <documents>
161 <document>
162 <title>Title1</title>
163 <info>
164 <num_pages>10</num_pages>
165 </info>
166 </document>
167 <document>
168 <title>Title2</title>
169 <info>
170 <num_pages>20</num_pages>
171 </info>
172 </document>
173 </documents>
174 \endcode
175
176 You can extract the number of pages with the following role:
177
178 \qml
179 XmlListModel {
180 id: xmlModel
181 source: "file.xml"
182 query: "/documents/document"
183 // ...
184 XmlListModelRole {
185 name: "pages"
186 elementName: "info/num_pages"
187 }
188 }
189 \endqml
190
191 \note The path to the element must not start or end with \c {'/'}.
192
193 \sa attributeName
194*/
195QString QQmlXmlListModelRole::elementName() const
196{
197 return m_elementName;
198}
199
200void QQmlXmlListModelRole::setElementName(const QString &name)
201{
202 if (name.startsWith(c: QLatin1Char('/'))) {
203 qmlWarning(me: this) << tr(s: "An XML element must not start with '/'");
204 return;
205 } else if (name.endsWith(c: QLatin1Char('/'))) {
206 qmlWarning(me: this) << tr(s: "An XML element must not end with '/'");
207 return;
208 } else if (name.contains(QStringLiteral("//"))) {
209 qmlWarning(me: this) << tr(s: "An XML element must not contain \"//\"");
210 return;
211 }
212
213 if (name == m_elementName)
214 return;
215 m_elementName = name;
216 Q_EMIT elementNameChanged();
217}
218
219/*!
220 \qmlproperty string QtQml.XmlListModel::XmlListModelRole::attributeName
221
222 The attribute of the XML element that will be used to read the data.
223 The XML element is specified by \l elementName property.
224
225 \include qqmlxmllistmodel.cpp basic-example
226
227 \include qqmlxmllistmodel.cpp empty-elementName-example
228
229 If you do not need to parse any attributes for the specified XML element,
230 simply leave this property blank.
231
232 \sa elementName
233*/
234QString QQmlXmlListModelRole::attributeName() const
235{
236 return m_attributeName;
237}
238
239void QQmlXmlListModelRole::setAttributeName(const QString &attributeName)
240{
241 if (m_attributeName == attributeName)
242 return;
243 m_attributeName = attributeName;
244 Q_EMIT attributeNameChanged();
245}
246
247bool QQmlXmlListModelRole::isValid() const
248{
249 return !m_name.isEmpty();
250}
251
252/*!
253 \qmltype XmlListModel
254 \inqmlmodule QtQml.XmlListModel
255 \brief For specifying a read-only model using XML data.
256
257 To use this element, you will need to import the module with the following line:
258 \code
259 import QtQml.XmlListModel
260 \endcode
261
262 XmlListModel is used to create a read-only model from XML data. It can be
263 used as a data source for view elements (such as ListView, PathView,
264 GridView) and other elements that interact with model data (such as
265 Repeater).
266
267 \note This model \b {does not} support the XPath queries. It supports simple
268 slash-separated paths and, optionally, one attribute for each element.
269
270 For example, if there is an XML document at https://www.qt.io/blog/rss.xml
271 like this:
272
273 \code
274 <?xml version="1.0" encoding="UTF-8"?>
275 <rss version="2.0">
276 ...
277 <channel>
278 <item>
279 <title>Qt 6.0.2 Released</title>
280 <link>https://www.qt.io/blog/qt-6.0.2-released</link>
281 <pubDate>Wed, 03 Mar 2021 12:40:43 GMT</pubDate>
282 </item>
283 <item>
284 <title>Qt 6.1 Beta Released</title>
285 <link>https://www.qt.io/blog/qt-6.1-beta-released</link>
286 <pubDate>Tue, 02 Mar 2021 13:05:47 GMT</pubDate>
287 </item>
288 <item>
289 <title>Qt Creator 4.14.1 released</title>
290 <link>https://www.qt.io/blog/qt-creator-4.14.1-released</link>
291 <pubDate>Wed, 24 Feb 2021 13:53:21 GMT</pubDate>
292 </item>
293 </channel>
294 </rss>
295 \endcode
296
297 A XmlListModel could create a model from this data, like this:
298
299 \qml
300 import QtQml.XmlListModel
301
302 XmlListModel {
303 id: xmlModel
304 source: "https://www.qt.io/blog/rss.xml"
305 query: "/rss/channel/item"
306
307 XmlListModelRole { name: "title"; elementName: "title" }
308 XmlListModelRole { name: "pubDate"; elementName: "pubDate" }
309 XmlListModelRole { name: "link"; elementName: "link" }
310 }
311 \endqml
312
313 The \l {XmlListModel::query}{query} value of "/rss/channel/item" specifies
314 that the XmlListModel should generate a model item for each \c {<item>} in
315 the XML document.
316
317 The \l [QML] {XmlListModelRole} objects define the model item attributes.
318 Here, each model item will have \c title, \c pubDate and \c link attributes
319 that match the \c title, \c pubDate and \c link values of its corresponding
320 \c {<item>}.
321 (See \l [QML] {XmlListModelRole} documentation for more examples.)
322
323 The model could be used in a ListView, like this:
324
325 \qml
326 ListView {
327 width: 180; height: 300
328 model: xmlModel
329 delegate: Text { text: title + ": " + pubDate + "; link: " + link }
330 }
331 \endqml
332
333 The \l XmlListModel data is loaded asynchronously, and \l status
334 is set to \c XmlListModel.Ready when loading is complete.
335 Note this means when \l XmlListModel is used for a view, the view is not
336 populated until the model is loaded.
337*/
338
339QQmlXmlListModel::QQmlXmlListModel(QObject *parent) : QAbstractListModel(parent) { }
340
341QQmlXmlListModel::~QQmlXmlListModel()
342{
343 // Cancel all objects
344 for (auto &w : m_watchers.values())
345 w->cancel();
346 // Wait until all objects are finished
347 while (!m_watchers.isEmpty()) {
348 auto it = m_watchers.begin();
349 it.value()->waitForFinished();
350 // Explicitly delete the watcher here, because the connected lambda
351 // would not be called until processEvents() is called
352 delete it.value();
353 m_watchers.erase(it);
354 }
355}
356
357QModelIndex QQmlXmlListModel::index(int row, int column, const QModelIndex &parent) const
358{
359 return !parent.isValid() && column == 0 && row >= 0 && m_size ? createIndex(arow: row, acolumn: column)
360 : QModelIndex();
361}
362
363int QQmlXmlListModel::rowCount(const QModelIndex &parent) const
364{
365 return !parent.isValid() ? m_size : 0;
366}
367
368QVariant QQmlXmlListModel::data(const QModelIndex &index, int role) const
369{
370 const int roleIndex = m_roles.indexOf(t: role);
371 return (roleIndex == -1 || !index.isValid()) ? QVariant()
372 : m_data.value(i: index.row()).value(key: roleIndex);
373}
374
375QHash<int, QByteArray> QQmlXmlListModel::roleNames() const
376{
377 QHash<int, QByteArray> roleNames;
378 for (int i = 0; i < m_roles.size(); ++i)
379 roleNames.insert(key: m_roles.at(i), value: m_roleNames.at(i).toUtf8());
380 return roleNames;
381}
382
383/*!
384 \qmlproperty int QtQml.XmlListModel::XmlListModel::count
385 The number of data entries in the model.
386*/
387int QQmlXmlListModel::count() const
388{
389 return m_size;
390}
391
392/*!
393 \qmlproperty url QtQml.XmlListModel::XmlListModel::source
394 The location of the XML data source.
395*/
396QUrl QQmlXmlListModel::source() const
397{
398 return m_source;
399}
400
401void QQmlXmlListModel::setSource(const QUrl &src)
402{
403 if (m_source != src) {
404 m_source = src;
405 reload();
406 Q_EMIT sourceChanged();
407 }
408}
409
410/*!
411 \qmlproperty string QtQml.XmlListModel::XmlListModel::query
412 A string representing the base path for creating model items from this
413 model's \l [QML] {XmlListModelRole} objects. The query should start with
414 \c {'/'}.
415*/
416QString QQmlXmlListModel::query() const
417{
418 return m_query;
419}
420
421void QQmlXmlListModel::setQuery(const QString &query)
422{
423 if (!query.startsWith(c: QLatin1Char('/'))) {
424 qmlWarning(me: this) << QCoreApplication::translate(
425 context: "XmlListModelRoleList", key: "An XmlListModel query must start with '/'");
426 return;
427 }
428
429 if (m_query != query) {
430 m_query = query;
431 reload();
432 Q_EMIT queryChanged();
433 }
434}
435
436/*!
437 \qmlproperty list<XmlListModelRole> QtQml.XmlListModel::XmlListModel::roles
438
439 The roles to make available for this model.
440*/
441QQmlListProperty<QQmlXmlListModelRole> QQmlXmlListModel::roleObjects()
442{
443 QQmlListProperty<QQmlXmlListModelRole> list(this, &m_roleObjects);
444 list.append = &QQmlXmlListModel::appendRole;
445 list.clear = &QQmlXmlListModel::clearRole;
446 return list;
447}
448
449void QQmlXmlListModel::appendRole(QQmlXmlListModelRole *role)
450{
451 if (role) {
452 int i = m_roleObjects.size();
453 m_roleObjects.append(t: role);
454 if (m_roleNames.contains(str: role->name())) {
455 qmlWarning(me: role)
456 << QQmlXmlListModel::tr(
457 s: "\"%1\" duplicates a previous role name and will be disabled.")
458 .arg(a: role->name());
459 return;
460 }
461 m_roles.insert(i, t: m_highestRole);
462 m_roleNames.insert(i, t: role->name());
463 ++m_highestRole;
464 }
465}
466
467void QQmlXmlListModel::clearRole()
468{
469 m_roles.clear();
470 m_roleNames.clear();
471 m_roleObjects.clear();
472}
473
474void QQmlXmlListModel::appendRole(QQmlListProperty<QQmlXmlListModelRole> *list,
475 QQmlXmlListModelRole *role)
476{
477 auto object = qobject_cast<QQmlXmlListModel *>(object: list->object);
478 if (object) // role is checked inside appendRole
479 object->appendRole(role);
480}
481
482void QQmlXmlListModel::clearRole(QQmlListProperty<QQmlXmlListModelRole> *list)
483{
484 auto object = qobject_cast<QQmlXmlListModel *>(object: list->object);
485 if (object)
486 object->clearRole();
487}
488
489void QQmlXmlListModel::tryExecuteQuery(const QByteArray &data)
490{
491 auto job = createJob(data);
492 m_queryId = job.queryId;
493 QQmlXmlListModelQueryRunnable *runnable = new QQmlXmlListModelQueryRunnable(std::move(job));
494 if (runnable) {
495 auto future = runnable->future();
496 auto *watcher = new ResultFutureWatcher();
497 // No need to connect to canceled signal, because it just notifies that
498 // QFuture::cancel() was called. We will get the finished() signal in
499 // both cases.
500 connect(sender: watcher, signal: &ResultFutureWatcher::finished, context: this, slot: [id = m_queryId, this]() {
501 auto *watcher = static_cast<ResultFutureWatcher *>(sender());
502 if (watcher) {
503 if (!watcher->isCanceled()) {
504 QQmlXmlListModelQueryResult result = watcher->result();
505 // handle errors
506 for (const auto &errorInfo : result.errors)
507 queryError(object: errorInfo.first, error: errorInfo.second);
508 // fill results
509 queryCompleted(result);
510 }
511 // remove from watchers
512 m_watchers.remove(key: id);
513 watcher->deleteLater();
514 }
515 });
516 m_watchers[m_queryId] = watcher;
517 watcher->setFuture(future);
518 QThreadPool::globalInstance()->start(runnable);
519 } else {
520 m_errorString = tr(s: "Failed to create an instance of QRunnable query object");
521 m_status = QQmlXmlListModel::Error;
522 m_queryId = -1;
523 Q_EMIT statusChanged(m_status);
524 }
525}
526
527QQmlXmlListModelQueryJob QQmlXmlListModel::createJob(const QByteArray &data)
528{
529 QQmlXmlListModelQueryJob job;
530 job.queryId = nextQueryId();
531 job.data = data;
532 job.query = m_query;
533
534 for (int i = 0; i < m_roleObjects.size(); i++) {
535 if (!m_roleObjects.at(i)->isValid()) {
536 job.roleNames << QString();
537 job.elementNames << QString();
538 job.elementAttributes << QString();
539 continue;
540 }
541 job.roleNames << m_roleObjects.at(i)->name();
542 job.elementNames << m_roleObjects.at(i)->elementName();
543 job.elementAttributes << m_roleObjects.at(i)->attributeName();
544 job.roleQueryErrorId << static_cast<void *>(m_roleObjects.at(i));
545 }
546
547 return job;
548}
549
550int QQmlXmlListModel::nextQueryId()
551{
552 m_nextQueryIdGenerator++;
553 if (m_nextQueryIdGenerator <= 0)
554 m_nextQueryIdGenerator = 1;
555 return m_nextQueryIdGenerator;
556}
557
558/*!
559 \qmlproperty enumeration QtQml.XmlListModel::XmlListModel::status
560 Specifies the model loading status, which can be one of the following:
561
562 \value XmlListModel.Null No XML data has been set for this model.
563 \value XmlListModel.Ready The XML data has been loaded into the model.
564 \value XmlListModel.Loading The model is in the process of reading and
565 loading XML data.
566 \value XmlListModel.Error An error occurred while the model was loading. See
567 \l errorString() for details about the error.
568
569 \sa progress
570*/
571QQmlXmlListModel::Status QQmlXmlListModel::status() const
572{
573 return m_status;
574}
575
576/*!
577 \qmlproperty real QtQml.XmlListModel::XmlListModel::progress
578
579 This indicates the current progress of the downloading of the XML data
580 source. This value ranges from 0.0 (no data downloaded) to
581 1.0 (all data downloaded). If the XML data is not from a remote source,
582 the progress becomes 1.0 as soon as the data is read.
583
584 Note that when the progress is 1.0, the XML data has been downloaded, but
585 it is yet to be loaded into the model at this point. Use the status
586 property to find out when the XML data has been read and loaded into
587 the model.
588
589 \sa status, source
590*/
591qreal QQmlXmlListModel::progress() const
592{
593 return m_progress;
594}
595
596/*!
597 \qmlmethod QtQml.XmlListModel::XmlListModel::errorString()
598
599 Returns a string description of the last error that occurred
600 if \l status is \l {XmlListModel}.Error.
601*/
602QString QQmlXmlListModel::errorString() const
603{
604 return m_errorString;
605}
606
607void QQmlXmlListModel::classBegin()
608{
609 m_isComponentComplete = false;
610}
611
612void QQmlXmlListModel::componentComplete()
613{
614 m_isComponentComplete = true;
615 reload();
616}
617
618/*!
619 \qmlmethod QtQml.XmlListModel::XmlListModel::reload()
620
621 Reloads the model.
622*/
623void QQmlXmlListModel::reload()
624{
625 if (!m_isComponentComplete)
626 return;
627
628 if (m_queryId > 0 && m_watchers.contains(key: m_queryId))
629 m_watchers[m_queryId]->cancel();
630
631 m_queryId = -1;
632
633 if (m_size < 0)
634 m_size = 0;
635
636#if QT_CONFIG(qml_network)
637 if (m_reply) {
638 m_reply->abort();
639 deleteReply();
640 }
641#endif
642
643 const QQmlContext *context = qmlContext(this);
644 const auto resolvedSource = context ? context->resolvedUrl(m_source) : m_source;
645
646 if (resolvedSource.isEmpty()) {
647 m_queryId = 0;
648 notifyQueryStarted(remoteSource: false);
649 QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared);
650 } else if (QQmlFile::isLocalFile(url: resolvedSource)) {
651 QFile file(QQmlFile::urlToLocalFileOrQrc(resolvedSource));
652 const bool opened = file.open(flags: QIODevice::ReadOnly);
653 if (!opened)
654 qWarning(msg: "Failed to open file %s: %s", qPrintable(file.fileName()),
655 qPrintable(file.errorString()));
656 QByteArray data = opened ? file.readAll() : QByteArray();
657 notifyQueryStarted(remoteSource: false);
658 if (data.isEmpty()) {
659 m_queryId = 0;
660 QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared);
661 } else {
662 tryExecuteQuery(data);
663 }
664 } else {
665#if QT_CONFIG(qml_network)
666 notifyQueryStarted(remoteSource: true);
667 QNetworkRequest req(resolvedSource);
668 req.setRawHeader(headerName: "Accept", value: "application/xml,*/*");
669 m_reply = qmlContext(this)->engine()->networkAccessManager()->get(request: req);
670
671 QObject::connect(sender: m_reply, signal: &QNetworkReply::finished, context: this,
672 slot: &QQmlXmlListModel::requestFinished);
673 QObject::connect(sender: m_reply, signal: &QNetworkReply::downloadProgress, context: this,
674 slot: &QQmlXmlListModel::requestProgress);
675#else
676 m_queryId = 0;
677 notifyQueryStarted(false);
678 QTimer::singleShot(0, this, &QQmlXmlListModel::dataCleared);
679#endif
680 }
681}
682
683#define XMLLISTMODEL_MAX_REDIRECT 16
684
685#if QT_CONFIG(qml_network)
686void QQmlXmlListModel::requestFinished()
687{
688 m_redirectCount++;
689 if (m_redirectCount < XMLLISTMODEL_MAX_REDIRECT) {
690 QVariant redirect = m_reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute);
691 if (redirect.isValid()) {
692 QUrl url = m_reply->url().resolved(relative: redirect.toUrl());
693 deleteReply();
694 setSource(url);
695 return;
696 }
697 }
698 m_redirectCount = 0;
699
700 if (m_reply->error() != QNetworkReply::NoError) {
701 m_errorString = m_reply->errorString();
702 deleteReply();
703
704 if (m_size > 0) {
705 beginRemoveRows(parent: QModelIndex(), first: 0, last: m_size - 1);
706 m_data.clear();
707 m_size = 0;
708 endRemoveRows();
709 Q_EMIT countChanged();
710 }
711
712 m_status = Error;
713 m_queryId = -1;
714 Q_EMIT statusChanged(m_status);
715 } else {
716 QByteArray data = m_reply->readAll();
717 if (data.isEmpty()) {
718 m_queryId = 0;
719 QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared);
720 } else {
721 tryExecuteQuery(data);
722 }
723 deleteReply();
724
725 m_progress = 1.0;
726 Q_EMIT progressChanged(progress: m_progress);
727 }
728}
729
730void QQmlXmlListModel::deleteReply()
731{
732 if (m_reply) {
733 QObject::disconnect(sender: m_reply, signal: 0, receiver: this, member: 0);
734 m_reply->deleteLater();
735 m_reply = nullptr;
736 }
737}
738#endif
739
740void QQmlXmlListModel::requestProgress(qint64 received, qint64 total)
741{
742 if (m_status == Loading && total > 0) {
743 m_progress = qreal(received) / total;
744 Q_EMIT progressChanged(progress: m_progress);
745 }
746}
747
748void QQmlXmlListModel::dataCleared()
749{
750 QQmlXmlListModelQueryResult r;
751 r.queryId = 0;
752 queryCompleted(r);
753}
754
755void QQmlXmlListModel::queryError(void *object, const QString &error)
756{
757 for (int i = 0; i < m_roleObjects.size(); i++) {
758 if (m_roleObjects.at(i) == static_cast<QQmlXmlListModelRole *>(object)) {
759 qmlWarning(me: m_roleObjects.at(i))
760 << QQmlXmlListModel::tr(s: "Query error: \"%1\"").arg(a: error);
761 return;
762 }
763 }
764 qmlWarning(me: this) << QQmlXmlListModel::tr(s: "Query error: \"%1\"").arg(a: error);
765}
766
767void QQmlXmlListModel::queryCompleted(const QQmlXmlListModelQueryResult &result)
768{
769 if (result.queryId != m_queryId)
770 return;
771
772 int origCount = m_size;
773 bool sizeChanged = result.data.size() != m_size;
774
775 if (m_source.isEmpty())
776 m_status = Null;
777 else
778 m_status = Ready;
779 m_errorString.clear();
780 m_queryId = -1;
781
782 if (origCount > 0) {
783 beginRemoveRows(parent: QModelIndex(), first: 0, last: origCount - 1);
784 endRemoveRows();
785 }
786 m_size = result.data.size();
787 m_data = result.data;
788
789 if (m_size > 0) {
790 beginInsertRows(parent: QModelIndex(), first: 0, last: m_size - 1);
791 endInsertRows();
792 }
793
794 if (sizeChanged)
795 Q_EMIT countChanged();
796
797 Q_EMIT statusChanged(m_status);
798}
799
800void QQmlXmlListModel::notifyQueryStarted(bool remoteSource)
801{
802 m_progress = remoteSource ? 0.0 : 1.0;
803 m_status = QQmlXmlListModel::Loading;
804 m_errorString.clear();
805 Q_EMIT progressChanged(progress: m_progress);
806 Q_EMIT statusChanged(m_status);
807}
808
809static qsizetype findIndexOfName(const QStringList &elementNames, const QStringView &name,
810 qsizetype startIndex = 0)
811{
812 for (auto idx = startIndex; idx < elementNames.size(); ++idx) {
813 if (elementNames[idx].startsWith(s: name))
814 return idx;
815 }
816 return -1;
817}
818
819QQmlXmlListModelQueryRunnable::QQmlXmlListModelQueryRunnable(QQmlXmlListModelQueryJob &&job)
820 : m_job(std::move(job))
821{
822 setAutoDelete(true);
823}
824
825void QQmlXmlListModelQueryRunnable::run()
826{
827 m_promise.start();
828 if (!m_promise.isCanceled()) {
829 QQmlXmlListModelQueryResult result;
830 result.queryId = m_job.queryId;
831 doQueryJob(currentResult: &result);
832 m_promise.addResult(result: std::move(result));
833 }
834 m_promise.finish();
835}
836
837QFuture<QQmlXmlListModelQueryResult> QQmlXmlListModelQueryRunnable::future() const
838{
839 return m_promise.future();
840}
841
842void QQmlXmlListModelQueryRunnable::doQueryJob(QQmlXmlListModelQueryResult *currentResult)
843{
844 Q_ASSERT(m_job.queryId != -1);
845
846 QByteArray data(m_job.data);
847 QXmlStreamReader reader;
848 reader.addData(data);
849
850 QStringList items = m_job.query.split(sep: QLatin1Char('/'), behavior: Qt::SkipEmptyParts);
851
852 while (!reader.atEnd() && !m_promise.isCanceled()) {
853 int i = 0;
854 while (i < items.size()) {
855 if (reader.readNextStartElement()) {
856 if (reader.name() == items.at(i)) {
857 if (i != items.size() - 1) {
858 i++;
859 continue;
860 } else {
861 processElement(currentResult, element: items.at(i), reader);
862 }
863 } else {
864 reader.skipCurrentElement();
865 }
866 }
867 if (reader.tokenType() == QXmlStreamReader::Invalid) {
868 reader.readNext();
869 break;
870 } else if (reader.hasError()) {
871 reader.raiseError();
872 break;
873 }
874 }
875 }
876}
877
878void QQmlXmlListModelQueryRunnable::processElement(QQmlXmlListModelQueryResult *currentResult,
879 const QString &element, QXmlStreamReader &reader)
880{
881 if (!reader.isStartElement() || reader.name() != element)
882 return;
883
884 const QStringList &elementNames = m_job.elementNames;
885 const QStringList &attributes = m_job.elementAttributes;
886 QFlatMap<int, QString> results;
887
888 // First of all check all the empty element names. They might have
889 // attributes to be read from the current element
890 if (!reader.attributes().isEmpty()) {
891 for (auto index = 0; index < elementNames.size(); ++index) {
892 if (elementNames.at(i: index).isEmpty() && !attributes.at(i: index).isEmpty()) {
893 const QString &attribute = attributes.at(i: index);
894 if (reader.attributes().hasAttribute(qualifiedName: attribute))
895 results[index] = reader.attributes().value(qualifiedName: attribute).toString();
896 }
897 }
898 }
899
900 // After that we recursively search for the elements, considering that we
901 // can have nested element names in our model, and that the same element
902 // can be used multiple types (with different attributes, for example)
903 readSubTree(prefix: QString(), reader, results, errors: &currentResult->errors);
904
905 if (reader.hasError())
906 currentResult->errors.push_back(t: qMakePair(value1: this, value2: reader.errorString()));
907
908 currentResult->data << results;
909}
910
911void QQmlXmlListModelQueryRunnable::readSubTree(const QString &prefix, QXmlStreamReader &reader,
912 QFlatMap<int, QString> &results,
913 QList<QPair<void *, QString>> *errors)
914{
915 const QStringList &elementNames = m_job.elementNames;
916 const QStringList &attributes = m_job.elementAttributes;
917 while (reader.readNextStartElement()) {
918 const auto name = reader.name();
919 const QString fullName =
920 prefix.isEmpty() ? name.toString() : (prefix + QLatin1Char('/') + name.toString());
921 qsizetype index = name.isEmpty() ? -1 : findIndexOfName(elementNames, name: fullName);
922 if (index >= 0) {
923 // We can have multiple roles with the same element name, but
924 // different attributes, so we need to cache the attributes and
925 // element text.
926 const auto elementAttributes = reader.attributes();
927 // We can read text only when the element actually contains it,
928 // otherwise it will be an error. It can also be used to check that
929 // we've reached the bottom level.
930 QString elementText;
931 bool elementTextRead = false;
932 while (index >= 0) {
933 // if the path matches completely, not just starts with, we
934 // need to actually extract value
935 if (elementNames[index] == fullName) {
936 QString roleResult;
937 const QString &attribute = attributes.at(i: index);
938 if (!attribute.isEmpty()) {
939 if (elementAttributes.hasAttribute(qualifiedName: attribute)) {
940 roleResult = elementAttributes.value(qualifiedName: attributes.at(i: index)).toString();
941 } else {
942 errors->push_back(t: qMakePair(value1: m_job.roleQueryErrorId.at(i: index),
943 value2: QLatin1String("Attribute %1 not found")
944 .arg(args: attributes[index])));
945 }
946 } else if (!elementNames.at(i: index).isEmpty()) {
947 if (!elementTextRead) {
948 elementText =
949 reader.readElementText(behaviour: QXmlStreamReader::IncludeChildElements);
950 elementTextRead = true;
951 }
952 roleResult = elementText;
953 }
954 results[index] = roleResult;
955 }
956 // search for the next role with the same element name
957 index = findIndexOfName(elementNames, name: fullName, startIndex: index + 1);
958 }
959 if (!elementTextRead)
960 readSubTree(prefix: fullName, reader, results, errors);
961 } else {
962 reader.skipCurrentElement();
963 }
964 }
965}
966
967QT_END_NAMESPACE
968
969#include "moc_qqmlxmllistmodel_p.cpp"
970

source code of qtdeclarative/src/qmlxmllistmodel/qqmlxmllistmodel.cpp