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#if QT_CONFIG(qml_network)
684void QQmlXmlListModel::requestFinished()
685{
686 if (m_reply->error() != QNetworkReply::NoError) {
687 m_errorString = m_reply->errorString();
688 deleteReply();
689
690 if (m_size > 0) {
691 beginRemoveRows(parent: QModelIndex(), first: 0, last: m_size - 1);
692 m_data.clear();
693 m_size = 0;
694 endRemoveRows();
695 Q_EMIT countChanged();
696 }
697
698 m_status = Error;
699 m_queryId = -1;
700 Q_EMIT statusChanged(m_status);
701 } else {
702 QByteArray data = m_reply->readAll();
703 if (data.isEmpty()) {
704 m_queryId = 0;
705 QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared);
706 } else {
707 tryExecuteQuery(data);
708 }
709 deleteReply();
710
711 m_progress = 1.0;
712 Q_EMIT progressChanged(progress: m_progress);
713 }
714}
715
716void QQmlXmlListModel::deleteReply()
717{
718 if (m_reply) {
719 QObject::disconnect(sender: m_reply, signal: 0, receiver: this, member: 0);
720 m_reply->deleteLater();
721 m_reply = nullptr;
722 }
723}
724#endif
725
726void QQmlXmlListModel::requestProgress(qint64 received, qint64 total)
727{
728 if (m_status == Loading && total > 0) {
729 m_progress = qreal(received) / total;
730 Q_EMIT progressChanged(progress: m_progress);
731 }
732}
733
734void QQmlXmlListModel::dataCleared()
735{
736 QQmlXmlListModelQueryResult r;
737 r.queryId = 0;
738 queryCompleted(r);
739}
740
741void QQmlXmlListModel::queryError(void *object, const QString &error)
742{
743 for (int i = 0; i < m_roleObjects.size(); i++) {
744 if (m_roleObjects.at(i) == static_cast<QQmlXmlListModelRole *>(object)) {
745 qmlWarning(me: m_roleObjects.at(i))
746 << QQmlXmlListModel::tr(s: "Query error: \"%1\"").arg(a: error);
747 return;
748 }
749 }
750 qmlWarning(me: this) << QQmlXmlListModel::tr(s: "Query error: \"%1\"").arg(a: error);
751}
752
753void QQmlXmlListModel::queryCompleted(const QQmlXmlListModelQueryResult &result)
754{
755 if (result.queryId != m_queryId)
756 return;
757
758 int origCount = m_size;
759 bool sizeChanged = result.data.size() != m_size;
760
761 if (m_source.isEmpty())
762 m_status = Null;
763 else
764 m_status = Ready;
765 m_errorString.clear();
766 m_queryId = -1;
767
768 if (origCount > 0) {
769 beginRemoveRows(parent: QModelIndex(), first: 0, last: origCount - 1);
770 endRemoveRows();
771 }
772 m_size = result.data.size();
773 m_data = result.data;
774
775 if (m_size > 0) {
776 beginInsertRows(parent: QModelIndex(), first: 0, last: m_size - 1);
777 endInsertRows();
778 }
779
780 if (sizeChanged)
781 Q_EMIT countChanged();
782
783 Q_EMIT statusChanged(m_status);
784}
785
786void QQmlXmlListModel::notifyQueryStarted(bool remoteSource)
787{
788 m_progress = remoteSource ? 0.0 : 1.0;
789 m_status = QQmlXmlListModel::Loading;
790 m_errorString.clear();
791 Q_EMIT progressChanged(progress: m_progress);
792 Q_EMIT statusChanged(m_status);
793}
794
795static qsizetype findIndexOfName(const QStringList &elementNames, const QStringView &name,
796 qsizetype startIndex = 0)
797{
798 for (auto idx = startIndex; idx < elementNames.size(); ++idx) {
799 if (elementNames[idx].startsWith(s: name))
800 return idx;
801 }
802 return -1;
803}
804
805QQmlXmlListModelQueryRunnable::QQmlXmlListModelQueryRunnable(QQmlXmlListModelQueryJob &&job)
806 : m_job(std::move(job))
807{
808 setAutoDelete(true);
809}
810
811void QQmlXmlListModelQueryRunnable::run()
812{
813 m_promise.start();
814 if (!m_promise.isCanceled()) {
815 QQmlXmlListModelQueryResult result;
816 result.queryId = m_job.queryId;
817 doQueryJob(currentResult: &result);
818 m_promise.addResult(result: std::move(result));
819 }
820 m_promise.finish();
821}
822
823QFuture<QQmlXmlListModelQueryResult> QQmlXmlListModelQueryRunnable::future() const
824{
825 return m_promise.future();
826}
827
828void QQmlXmlListModelQueryRunnable::doQueryJob(QQmlXmlListModelQueryResult *currentResult)
829{
830 Q_ASSERT(m_job.queryId != -1);
831
832 QByteArray data(m_job.data);
833 QXmlStreamReader reader;
834 reader.addData(data);
835
836 QStringList items = m_job.query.split(sep: QLatin1Char('/'), behavior: Qt::SkipEmptyParts);
837
838 while (!reader.atEnd() && !m_promise.isCanceled()) {
839 int i = 0;
840 while (i < items.size()) {
841 if (reader.readNextStartElement()) {
842 if (reader.name() == items.at(i)) {
843 if (i != items.size() - 1) {
844 i++;
845 continue;
846 } else {
847 processElement(currentResult, element: items.at(i), reader);
848 }
849 } else {
850 reader.skipCurrentElement();
851 }
852 }
853 if (reader.tokenType() == QXmlStreamReader::Invalid) {
854 reader.readNext();
855 break;
856 } else if (reader.hasError()) {
857 reader.raiseError();
858 break;
859 }
860 }
861 }
862}
863
864void QQmlXmlListModelQueryRunnable::processElement(QQmlXmlListModelQueryResult *currentResult,
865 const QString &element, QXmlStreamReader &reader)
866{
867 if (!reader.isStartElement() || reader.name() != element)
868 return;
869
870 const QStringList &elementNames = m_job.elementNames;
871 const QStringList &attributes = m_job.elementAttributes;
872 QFlatMap<int, QString> results;
873
874 // First of all check all the empty element names. They might have
875 // attributes to be read from the current element
876 if (!reader.attributes().isEmpty()) {
877 for (auto index = 0; index < elementNames.size(); ++index) {
878 if (elementNames.at(i: index).isEmpty() && !attributes.at(i: index).isEmpty()) {
879 const QString &attribute = attributes.at(i: index);
880 if (reader.attributes().hasAttribute(qualifiedName: attribute))
881 results[index] = reader.attributes().value(qualifiedName: attribute).toString();
882 }
883 }
884 }
885
886 // After that we recursively search for the elements, considering that we
887 // can have nested element names in our model, and that the same element
888 // can be used multiple types (with different attributes, for example)
889 readSubTree(prefix: QString(), reader, results, errors: &currentResult->errors);
890
891 if (reader.hasError())
892 currentResult->errors.push_back(t: qMakePair(value1: this, value2: reader.errorString()));
893
894 currentResult->data << results;
895}
896
897void QQmlXmlListModelQueryRunnable::readSubTree(const QString &prefix, QXmlStreamReader &reader,
898 QFlatMap<int, QString> &results,
899 QList<QPair<void *, QString>> *errors)
900{
901 const QStringList &elementNames = m_job.elementNames;
902 const QStringList &attributes = m_job.elementAttributes;
903 while (reader.readNextStartElement()) {
904 const auto name = reader.name();
905 const QString fullName =
906 prefix.isEmpty() ? name.toString() : (prefix + QLatin1Char('/') + name.toString());
907 qsizetype index = name.isEmpty() ? -1 : findIndexOfName(elementNames, name: fullName);
908 if (index >= 0) {
909 // We can have multiple roles with the same element name, but
910 // different attributes, so we need to cache the attributes and
911 // element text.
912 const auto elementAttributes = reader.attributes();
913 // We can read text only when the element actually contains it,
914 // otherwise it will be an error. It can also be used to check that
915 // we've reached the bottom level.
916 QString elementText;
917 bool elementTextRead = false;
918 while (index >= 0) {
919 // if the path matches completely, not just starts with, we
920 // need to actually extract value
921 if (elementNames[index] == fullName) {
922 QString roleResult;
923 const QString &attribute = attributes.at(i: index);
924 if (!attribute.isEmpty()) {
925 if (elementAttributes.hasAttribute(qualifiedName: attribute)) {
926 roleResult = elementAttributes.value(qualifiedName: attributes.at(i: index)).toString();
927 } else {
928 errors->push_back(t: qMakePair(value1: m_job.roleQueryErrorId.at(i: index),
929 value2: QLatin1String("Attribute %1 not found")
930 .arg(args: attributes[index])));
931 }
932 } else if (!elementNames.at(i: index).isEmpty()) {
933 if (!elementTextRead) {
934 elementText =
935 reader.readElementText(behaviour: QXmlStreamReader::IncludeChildElements);
936 elementTextRead = true;
937 }
938 roleResult = elementText;
939 }
940 results[index] = roleResult;
941 }
942 // search for the next role with the same element name
943 index = findIndexOfName(elementNames, name: fullName, startIndex: index + 1);
944 }
945 if (!elementTextRead)
946 readSubTree(prefix: fullName, reader, results, errors);
947 } else {
948 reader.skipCurrentElement();
949 }
950 }
951}
952
953QT_END_NAMESPACE
954
955#include "moc_qqmlxmllistmodel_p.cpp"
956

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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