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