1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQml module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qqmlxmllistmodel_p.h"
41
42#include <qqmlcontext.h>
43#include <qqmlfile.h>
44#include <private/qqmlengine_p.h>
45#include <private/qv4value_p.h>
46#include <private/qv4engine_p.h>
47#include <private/qv4object_p.h>
48
49#include <QDebug>
50#include <QStringList>
51#include <QMap>
52#include <QThread>
53#include <QXmlQuery>
54#include <QXmlResultItems>
55#include <QXmlNodeModelIndex>
56#include <QBuffer>
57#if QT_CONFIG(qml_network)
58#include <QNetworkRequest>
59#include <QNetworkReply>
60#endif
61#include <QTimer>
62#include <QMutex>
63
64#include <private/qabstractitemmodel_p.h>
65
66Q_DECLARE_METATYPE(QQuickXmlQueryResult)
67
68QT_BEGIN_NAMESPACE
69
70using namespace QV4;
71
72typedef QPair<int, int> QQuickXmlListRange;
73
74#define XMLLISTMODEL_CLEAR_ID 0
75
76/*!
77 \qmlmodule QtQuick.XmlListModel 2.\QtMinorVersion
78 \title Qt XML Patterns QML Types
79 \keyword Qt Quick XmlListModel QML Types
80 \ingroup qmlmodules
81 \brief Provides QML types for creating models from XML data
82
83 This QML module contains types for creating models from XML data.
84
85 To use the types in this module, import the module with the following line:
86
87 \qml \QtMinorVersion
88 import QtQuick.XmlListModel 2.\1
89 \endqml
90*/
91
92/*!
93 \qmltype XmlRole
94 \inqmlmodule QtQuick.XmlListModel
95 \brief For specifying a role to an XmlListModel.
96 \ingroup qtquick-models
97
98 \sa {All QML Types}{Qt QML}
99*/
100
101/*!
102 \qmlproperty string QtQuick.XmlListModel::XmlRole::name
103
104 The name for the role. This name is used to access the model data for this role.
105
106 For example, the following model has a role named "title", which can be accessed
107 from the view's delegate:
108
109 \qml
110 XmlListModel {
111 id: xmlModel
112 // ...
113 XmlRole {
114 name: "title"
115 query: "title/string()"
116 }
117 }
118 \endqml
119
120 \qml
121 ListView {
122 model: xmlModel
123 delegate: Text { text: title }
124 }
125 \endqml
126*/
127
128/*!
129 \qmlproperty string QtQuick.XmlListModel::XmlRole::query
130 The relative XPath expression query for this role. The query must be relative; it cannot start
131 with a '/'.
132
133 For example, if there is an XML document like this:
134
135 \quotefile qml/xmlrole.xml
136 Here are some valid XPath expressions for XmlRole queries on this document:
137
138 \snippet qml/xmlrole.qml 0
139 \dots 4
140 \snippet qml/xmlrole.qml 1
141
142 Accessing the model data for the above roles from a delegate:
143
144 \snippet qml/xmlrole.qml 2
145
146 See the \l{http://www.w3.org/TR/xpath20/}{W3C XPath 2.0 specification} for more information.
147*/
148
149/*!
150 \qmlproperty bool QtQuick.XmlListModel::XmlRole::isKey
151 Defines whether this is a key role.
152 Key roles are used to determine whether a set of values should
153 be updated or added to the XML list model when XmlListModel::reload()
154 is called.
155
156 \sa XmlListModel
157*/
158
159struct XmlQueryJob
160{
161 int queryId;
162 QByteArray data;
163 QString query;
164 QString namespaces;
165 QStringList roleQueries;
166 QList<void*> roleQueryErrorId; // the ptr to send back if there is an error
167 QStringList keyRoleQueries;
168 QStringList keyRoleResultsCache;
169 QString prefix;
170};
171
172
173class QQuickXmlQueryEngine;
174class QQuickXmlQueryThreadObject : public QObject
175{
176 Q_OBJECT
177public:
178 QQuickXmlQueryThreadObject(QQuickXmlQueryEngine *);
179
180 void processJobs();
181 bool event(QEvent *e) override;
182
183private:
184 QQuickXmlQueryEngine *m_queryEngine;
185};
186
187
188class QQuickXmlQueryEngine : public QThread
189{
190 Q_OBJECT
191public:
192 QQuickXmlQueryEngine(QQmlEngine *eng);
193 ~QQuickXmlQueryEngine();
194
195 int doQuery(QString query, QString namespaces, QByteArray data, QList<QQuickXmlListModelRole *>* roleObjects, QStringList keyRoleResultsCache);
196 void abort(int id);
197
198 void processJobs();
199
200 static QQuickXmlQueryEngine *instance(QQmlEngine *engine);
201
202signals:
203 void queryCompleted(const QQuickXmlQueryResult &);
204 void error(void*, const QString&);
205
206protected:
207 void run() override;
208
209private:
210 void processQuery(XmlQueryJob *job);
211 void doQueryJob(XmlQueryJob *job, QQuickXmlQueryResult *currentResult);
212 void doSubQueryJob(XmlQueryJob *job, QQuickXmlQueryResult *currentResult);
213 void getValuesOfKeyRoles(const XmlQueryJob& currentJob, QStringList *values, QXmlQuery *query) const;
214 void addIndexToRangeList(QList<QQuickXmlListRange> *ranges, int index) const;
215
216 QMutex m_mutex;
217 QQuickXmlQueryThreadObject *m_threadObject;
218 QList<XmlQueryJob> m_jobs;
219 QSet<int> m_cancelledJobs;
220 QAtomicInt m_queryIds;
221
222 QQmlEngine *m_engine;
223 QObject *m_eventLoopQuitHack;
224
225 static QHash<QQmlEngine *,QQuickXmlQueryEngine*> queryEngines;
226 static QMutex queryEnginesMutex;
227};
228QHash<QQmlEngine *,QQuickXmlQueryEngine*> QQuickXmlQueryEngine::queryEngines;
229QMutex QQuickXmlQueryEngine::queryEnginesMutex;
230
231
232QQuickXmlQueryThreadObject::QQuickXmlQueryThreadObject(QQuickXmlQueryEngine *e)
233 : m_queryEngine(e)
234{
235}
236
237void QQuickXmlQueryThreadObject::processJobs()
238{
239 QCoreApplication::postEvent(receiver: this, event: new QEvent(QEvent::User));
240}
241
242bool QQuickXmlQueryThreadObject::event(QEvent *e)
243{
244 if (e->type() == QEvent::User) {
245 m_queryEngine->processJobs();
246 return true;
247 } else {
248 return QObject::event(event: e);
249 }
250}
251
252
253
254QQuickXmlQueryEngine::QQuickXmlQueryEngine(QQmlEngine *eng)
255: QThread(eng), m_threadObject(0), m_queryIds(XMLLISTMODEL_CLEAR_ID + 1), m_engine(eng), m_eventLoopQuitHack(0)
256{
257 qRegisterMetaType<QQuickXmlQueryResult>(typeName: "QQuickXmlQueryResult");
258
259 m_eventLoopQuitHack = new QObject;
260 m_eventLoopQuitHack->moveToThread(thread: this);
261 connect(asender: m_eventLoopQuitHack, SIGNAL(destroyed(QObject*)), SLOT(quit()), atype: Qt::DirectConnection);
262 start(QThread::IdlePriority);
263}
264
265QQuickXmlQueryEngine::~QQuickXmlQueryEngine()
266{
267 queryEnginesMutex.lock();
268 queryEngines.remove(key: m_engine);
269 queryEnginesMutex.unlock();
270
271 m_eventLoopQuitHack->deleteLater();
272 wait();
273}
274
275int QQuickXmlQueryEngine::doQuery(QString query, QString namespaces, QByteArray data, QList<QQuickXmlListModelRole *>* roleObjects, QStringList keyRoleResultsCache) {
276 {
277 QMutexLocker m1(&m_mutex);
278 m_queryIds.ref();
279 if (m_queryIds.loadRelaxed() <= 0)
280 m_queryIds.storeRelaxed(newValue: 1);
281 }
282
283 XmlQueryJob job;
284 job.queryId = m_queryIds.loadRelaxed();
285 job.data = data;
286 job.query = QLatin1String("doc($src)") + query;
287 job.namespaces = namespaces;
288 job.keyRoleResultsCache = keyRoleResultsCache;
289
290 for (int i=0; i<roleObjects->count(); i++) {
291 if (!roleObjects->at(i)->isValid()) {
292 job.roleQueries << QString();
293 continue;
294 }
295 job.roleQueries << roleObjects->at(i)->query();
296 job.roleQueryErrorId << static_cast<void*>(roleObjects->at(i));
297 if (roleObjects->at(i)->isKey())
298 job.keyRoleQueries << job.roleQueries.last();
299 }
300
301 {
302 QMutexLocker ml(&m_mutex);
303 m_jobs.append(t: job);
304 if (m_threadObject)
305 m_threadObject->processJobs();
306 }
307
308 return job.queryId;
309}
310
311void QQuickXmlQueryEngine::abort(int id)
312{
313 QMutexLocker ml(&m_mutex);
314 if (id != -1)
315 m_cancelledJobs.insert(value: id);
316}
317
318void QQuickXmlQueryEngine::run()
319{
320 m_mutex.lock();
321 m_threadObject = new QQuickXmlQueryThreadObject(this);
322 m_mutex.unlock();
323
324 processJobs();
325 exec();
326
327 delete m_threadObject;
328 m_threadObject = 0;
329}
330
331void QQuickXmlQueryEngine::processJobs()
332{
333 QMutexLocker locker(&m_mutex);
334
335 while (true) {
336 if (m_jobs.isEmpty())
337 return;
338
339 XmlQueryJob currentJob = m_jobs.takeLast();
340 while (m_cancelledJobs.remove(value: currentJob.queryId)) {
341 if (m_jobs.isEmpty())
342 return;
343 currentJob = m_jobs.takeLast();
344 }
345
346 locker.unlock();
347 processQuery(job: &currentJob);
348 locker.relock();
349 }
350}
351
352QQuickXmlQueryEngine *QQuickXmlQueryEngine::instance(QQmlEngine *engine)
353{
354 queryEnginesMutex.lock();
355 QQuickXmlQueryEngine *queryEng = queryEngines.value(key: engine);
356 if (!queryEng) {
357 queryEng = new QQuickXmlQueryEngine(engine);
358 queryEngines.insert(key: engine, value: queryEng);
359 }
360 queryEnginesMutex.unlock();
361
362 return queryEng;
363}
364
365void QQuickXmlQueryEngine::processQuery(XmlQueryJob *job)
366{
367 QQuickXmlQueryResult result;
368 result.queryId = job->queryId;
369 doQueryJob(job, currentResult: &result);
370 doSubQueryJob(job, currentResult: &result);
371
372 {
373 QMutexLocker ml(&m_mutex);
374 if (m_cancelledJobs.contains(value: job->queryId)) {
375 m_cancelledJobs.remove(value: job->queryId);
376 } else {
377 emit queryCompleted(result);
378 }
379 }
380}
381
382void QQuickXmlQueryEngine::doQueryJob(XmlQueryJob *currentJob, QQuickXmlQueryResult *currentResult)
383{
384 Q_ASSERT(currentJob->queryId != -1);
385
386 QString r;
387 QXmlQuery query;
388 QBuffer buffer(&currentJob->data);
389 buffer.open(openMode: QIODevice::ReadOnly);
390 query.bindVariable(localName: QLatin1String("src"), &buffer);
391 query.setQuery(sourceCode: currentJob->namespaces + currentJob->query);
392 query.evaluateTo(output: &r);
393
394 //always need a single root element
395 QByteArray xml = "<dummy:items xmlns:dummy=\"http://qtsotware.com/dummy\">\n" + r.toUtf8() + "</dummy:items>";
396 QBuffer b(&xml);
397 b.open(openMode: QIODevice::ReadOnly);
398
399 QString namespaces = QLatin1String("declare namespace dummy=\"http://qtsotware.com/dummy\";\n") + currentJob->namespaces;
400 QString prefix = QLatin1String("doc($inputDocument)/dummy:items/*");
401
402 //figure out how many items we are dealing with
403 int count = -1;
404 {
405 QXmlResultItems result;
406 QXmlQuery countquery;
407 countquery.bindVariable(localName: QLatin1String("inputDocument"), &b);
408 countquery.setQuery(sourceCode: namespaces + QLatin1String("count(") + prefix + QLatin1Char(')'));
409 countquery.evaluateTo(result: &result);
410 QXmlItem item(result.next());
411 if (item.isAtomicValue())
412 count = item.toAtomicValue().toInt();
413 }
414
415 currentJob->data = xml;
416 currentJob->prefix = namespaces + prefix + QLatin1Char('/');
417 currentResult->size = (count > 0 ? count : 0);
418}
419
420void QQuickXmlQueryEngine::getValuesOfKeyRoles(const XmlQueryJob& currentJob, QStringList *values, QXmlQuery *query) const
421{
422 const QStringList &keysQueries = currentJob.keyRoleQueries;
423 QString keysQuery;
424 if (keysQueries.count() == 1)
425 keysQuery = currentJob.prefix + keysQueries[0];
426 else if (keysQueries.count() > 1)
427 keysQuery = currentJob.prefix + QLatin1String("concat(") + keysQueries.join(sep: QLatin1Char(',')) + QLatin1Char(')');
428
429 if (!keysQuery.isEmpty()) {
430 query->setQuery(sourceCode: keysQuery);
431 QXmlResultItems resultItems;
432 query->evaluateTo(result: &resultItems);
433 QXmlItem item(resultItems.next());
434 while (!item.isNull()) {
435 values->append(t: item.toAtomicValue().toString());
436 item = resultItems.next();
437 }
438 }
439}
440
441void QQuickXmlQueryEngine::addIndexToRangeList(QList<QQuickXmlListRange> *ranges, int index) const {
442 if (ranges->isEmpty())
443 ranges->append(t: qMakePair(x: index, y: 1));
444 else if (ranges->last().first + ranges->last().second == index)
445 ranges->last().second += 1;
446 else
447 ranges->append(t: qMakePair(x: index, y: 1));
448}
449
450void QQuickXmlQueryEngine::doSubQueryJob(XmlQueryJob *currentJob, QQuickXmlQueryResult *currentResult)
451{
452 Q_ASSERT(currentJob->queryId != -1);
453
454 QBuffer b(&currentJob->data);
455 b.open(openMode: QIODevice::ReadOnly);
456
457 QXmlQuery subquery;
458 subquery.bindVariable(localName: QLatin1String("inputDocument"), &b);
459
460 QStringList keyRoleResults;
461 getValuesOfKeyRoles(currentJob: *currentJob, values: &keyRoleResults, query: &subquery);
462
463 // See if any values of key roles have been inserted or removed.
464
465 if (currentJob->keyRoleResultsCache.isEmpty()) {
466 currentResult->inserted << qMakePair(x: 0, y: currentResult->size);
467 } else {
468 if (keyRoleResults != currentJob->keyRoleResultsCache) {
469 QStringList temp;
470 for (int i=0; i<currentJob->keyRoleResultsCache.count(); i++) {
471 if (!keyRoleResults.contains(str: currentJob->keyRoleResultsCache[i]))
472 addIndexToRangeList(ranges: &currentResult->removed, index: i);
473 else
474 temp << currentJob->keyRoleResultsCache[i];
475 }
476 for (int i=0; i<keyRoleResults.count(); i++) {
477 if (temp.count() == i || keyRoleResults[i] != temp[i]) {
478 temp.insert(i, t: keyRoleResults[i]);
479 addIndexToRangeList(ranges: &currentResult->inserted, index: i);
480 }
481 }
482 }
483 }
484 currentResult->keyRoleResultsCache = keyRoleResults;
485
486 // Get the new values for each role.
487 //### we might be able to condense even further (query for everything in one go)
488 const QStringList &queries = currentJob->roleQueries;
489 for (int i = 0; i < queries.size(); ++i) {
490 QList<QVariant> resultList;
491 if (!queries[i].isEmpty()) {
492 subquery.setQuery(sourceCode: currentJob->prefix + QLatin1String("(let $v := string(") + queries[i] + QLatin1String(") return if ($v) then ") + queries[i] + QLatin1String(" else \"\")"));
493 if (subquery.isValid()) {
494 QXmlResultItems resultItems;
495 subquery.evaluateTo(result: &resultItems);
496 QXmlItem item(resultItems.next());
497 while (!item.isNull()) {
498 resultList << item.toAtomicValue(); //### we used to trim strings
499 item = resultItems.next();
500 }
501 } else {
502 emit error(currentJob->roleQueryErrorId.at(i), queries[i]);
503 }
504 }
505 //### should warn here if things have gone wrong.
506 while (resultList.count() < currentResult->size)
507 resultList << QVariant();
508 currentResult->data << resultList;
509 b.seek(off: 0);
510 }
511
512 //this method is much slower, but works better for incremental loading
513 /*for (int j = 0; j < m_size; ++j) {
514 QList<QVariant> resultList;
515 for (int i = 0; i < m_roleObjects->size(); ++i) {
516 QQuickXmlListModelRole *role = m_roleObjects->at(i);
517 subquery.setQuery(m_prefix.arg(j+1) + role->query());
518 if (role->isStringList()) {
519 QStringList data;
520 subquery.evaluateTo(&data);
521 resultList << QVariant(data);
522 //qDebug() << data;
523 } else {
524 QString s;
525 subquery.evaluateTo(&s);
526 if (role->isCData()) {
527 //un-escape
528 s.replace(QLatin1String("&lt;"), QLatin1String("<"));
529 s.replace(QLatin1String("&gt;"), QLatin1String(">"));
530 s.replace(QLatin1String("&amp;"), QLatin1String("&"));
531 }
532 resultList << s.trimmed();
533 //qDebug() << s;
534 }
535 b.seek(0);
536 }
537 m_modelData << resultList;
538 }*/
539}
540
541class QQuickXmlListModelPrivate : public QAbstractItemModelPrivate
542{
543 Q_DECLARE_PUBLIC(QQuickXmlListModel)
544public:
545 QQuickXmlListModelPrivate()
546 : isComponentComplete(true), size(0), highestRole(Qt::UserRole)
547#if QT_CONFIG(qml_network)
548 , reply(0)
549#endif
550 , status(QQuickXmlListModel::Null), progress(0.0)
551 , queryId(-1), roleObjects(), redirectCount(0) {}
552
553
554 void notifyQueryStarted(bool remoteSource) {
555 Q_Q(QQuickXmlListModel);
556 progress = remoteSource ? 0.0 : 1.0;
557 status = QQuickXmlListModel::Loading;
558 errorString.clear();
559 emit q->progressChanged(progress);
560 emit q->statusChanged(status);
561 }
562
563#if QT_CONFIG(qml_network)
564 void deleteReply() {
565 Q_Q(QQuickXmlListModel);
566 if (reply) {
567 QObject::disconnect(sender: reply, signal: 0, receiver: q, member: 0);
568 reply->deleteLater();
569 reply = 0;
570 }
571 }
572#endif
573
574 bool isComponentComplete;
575 QUrl src;
576 QString xml;
577 QString query;
578 QString namespaces;
579 int size;
580 QList<int> roles;
581 QStringList roleNames;
582 int highestRole;
583
584#if QT_CONFIG(qml_network)
585 QNetworkReply *reply;
586#endif
587 QQuickXmlListModel::Status status;
588 QString errorString;
589 qreal progress;
590 int queryId;
591 QStringList keyRoleResultsCache;
592 QList<QQuickXmlListModelRole *> roleObjects;
593
594 static void append_role(QQmlListProperty<QQuickXmlListModelRole> *list, QQuickXmlListModelRole *role);
595 static void clear_role(QQmlListProperty<QQuickXmlListModelRole> *list);
596 QList<QList<QVariant> > data;
597 int redirectCount;
598};
599
600
601void QQuickXmlListModelPrivate::append_role(QQmlListProperty<QQuickXmlListModelRole> *list, QQuickXmlListModelRole *role)
602{
603 QQuickXmlListModel *_this = qobject_cast<QQuickXmlListModel *>(object: list->object);
604 if (_this && role) {
605 int i = _this->d_func()->roleObjects.count();
606 _this->d_func()->roleObjects.append(t: role);
607 if (_this->d_func()->roleNames.contains(str: role->name())) {
608 qmlWarning(me: role) << QQuickXmlListModel::tr(s: "\"%1\" duplicates a previous role name and will be disabled.").arg(a: role->name());
609 return;
610 }
611 _this->d_func()->roles.insert(i, t: _this->d_func()->highestRole);
612 _this->d_func()->roleNames.insert(i, t: role->name());
613 ++_this->d_func()->highestRole;
614 }
615}
616
617//### clear needs to invalidate any cached data (in data table) as well
618// (and the model should emit the appropriate signals)
619void QQuickXmlListModelPrivate::clear_role(QQmlListProperty<QQuickXmlListModelRole> *list)
620{
621 QQuickXmlListModel *_this = static_cast<QQuickXmlListModel *>(list->object);
622 _this->d_func()->roles.clear();
623 _this->d_func()->roleNames.clear();
624 _this->d_func()->roleObjects.clear();
625}
626
627/*!
628 \qmltype XmlListModel
629 \inqmlmodule QtQuick.XmlListModel
630 \brief For specifying a read-only model using XPath expressions.
631 \ingroup qtquick-models
632
633
634 To use this element, you will need to import the module with the following line:
635 \code
636 import QtQuick.XmlListModel 2.0
637 \endcode
638
639 XmlListModel is used to create a read-only model from XML data. It can be used as a data source
640 for view elements (such as ListView, PathView, GridView) and other elements that interact with model
641 data (such as \l [QML]{Repeater}).
642
643 For example, if there is a XML document at http://www.mysite.com/feed.xml like this:
644
645 \code
646 <?xml version="1.0" encoding="utf-8"?>
647 <rss version="2.0">
648 ...
649 <channel>
650 <item>
651 <title>A blog post</title>
652 <pubDate>Sat, 07 Sep 2010 10:00:01 GMT</pubDate>
653 </item>
654 <item>
655 <title>Another blog post</title>
656 <pubDate>Sat, 07 Sep 2010 15:35:01 GMT</pubDate>
657 </item>
658 </channel>
659 </rss>
660 \endcode
661
662 A XmlListModel could create a model from this data, like this:
663
664 \qml
665 import QtQuick 2.0
666 import QtQuick.XmlListModel 2.0
667
668 XmlListModel {
669 id: xmlModel
670 source: "http://www.mysite.com/feed.xml"
671 query: "/rss/channel/item"
672
673 XmlRole { name: "title"; query: "title/string()" }
674 XmlRole { name: "pubDate"; query: "pubDate/string()" }
675 }
676 \endqml
677
678 The \l {XmlListModel::query}{query} value of "/rss/channel/item" specifies that the XmlListModel should generate
679 a model item for each \c <item> in the XML document.
680
681 The XmlRole objects define the
682 model item attributes. Here, each model item will have \c title and \c pubDate
683 attributes that match the \c title and \c pubDate values of its corresponding \c <item>.
684 (See \l XmlRole::query for more examples of valid XPath expressions for XmlRole.)
685
686 The model could be used in a ListView, like this:
687
688 \qml
689 ListView {
690 width: 180; height: 300
691 model: xmlModel
692 delegate: Text { text: title + ": " + pubDate }
693 }
694 \endqml
695
696 \image qml-xmllistmodel-example.png
697
698 The XmlListModel data is loaded asynchronously, and \l status
699 is set to \c XmlListModel.Ready when loading is complete.
700 Note this means when XmlListModel is used for a view, the view is not
701 populated until the model is loaded.
702
703
704 \section2 Using Key XML Roles
705
706 You can define certain roles as "keys" so that when reload() is called,
707 the model will only add and refresh data that contains new values for
708 these keys.
709
710 For example, if above role for "pubDate" was defined like this instead:
711
712 \qml
713 XmlRole { name: "pubDate"; query: "pubDate/string()"; isKey: true }
714 \endqml
715
716 Then when reload() is called, the model will only add and reload
717 items with a "pubDate" value that is not already
718 present in the model.
719
720 This is useful when displaying the contents of XML documents that
721 are incrementally updated (such as RSS feeds) to avoid repainting the
722 entire contents of a model in a view.
723
724 If multiple key roles are specified, the model only adds and reload items
725 with a combined value of all key roles that is not already present in
726 the model.
727
728 \sa {Qt Quick Demo - RSS News}
729*/
730
731QQuickXmlListModel::QQuickXmlListModel(QObject *parent)
732 : QAbstractListModel(*(new QQuickXmlListModelPrivate), parent)
733{
734}
735
736QQuickXmlListModel::~QQuickXmlListModel()
737{
738}
739
740/*!
741 \qmlproperty list<XmlRole> QtQuick.XmlListModel::XmlListModel::roles
742
743 The roles to make available for this model.
744*/
745QQmlListProperty<QQuickXmlListModelRole> QQuickXmlListModel::roleObjects()
746{
747 Q_D(QQuickXmlListModel);
748 QQmlListProperty<QQuickXmlListModelRole> list(this, &d->roleObjects);
749 list.append = &QQuickXmlListModelPrivate::append_role;
750 list.clear = &QQuickXmlListModelPrivate::clear_role;
751 return list;
752}
753
754QModelIndex QQuickXmlListModel::index(int row, int column, const QModelIndex &parent) const
755{
756 Q_D(const QQuickXmlListModel);
757 return !parent.isValid() && column == 0 && row >= 0 && row < d->size
758 ? createIndex(arow: row, acolumn: column)
759 : QModelIndex();
760}
761
762int QQuickXmlListModel::rowCount(const QModelIndex &parent) const
763{
764 Q_D(const QQuickXmlListModel);
765 return !parent.isValid() ? d->size : 0;
766}
767
768QVariant QQuickXmlListModel::data(const QModelIndex &index, int role) const
769{
770 Q_D(const QQuickXmlListModel);
771 const int roleIndex = d->roles.indexOf(t: role);
772 return (roleIndex == -1 || !index.isValid())
773 ? QVariant()
774 : d->data.value(i: roleIndex).value(i: index.row());
775}
776
777QHash<int, QByteArray> QQuickXmlListModel::roleNames() const
778{
779 Q_D(const QQuickXmlListModel);
780 QHash<int,QByteArray> roleNames;
781 for (int i = 0; i < d->roles.count(); ++i)
782 roleNames.insert(key: d->roles.at(i), value: d->roleNames.at(i).toUtf8());
783 return roleNames;
784}
785
786/*!
787 \qmlproperty int QtQuick.XmlListModel::XmlListModel::count
788 The number of data entries in the model.
789*/
790int QQuickXmlListModel::count() const
791{
792 Q_D(const QQuickXmlListModel);
793 return d->size;
794}
795
796/*!
797 \qmlproperty url QtQuick.XmlListModel::XmlListModel::source
798 The location of the XML data source.
799
800 If both \c source and \l xml are set, \l xml is used.
801*/
802QUrl QQuickXmlListModel::source() const
803{
804 Q_D(const QQuickXmlListModel);
805 return d->src;
806}
807
808void QQuickXmlListModel::setSource(const QUrl &src)
809{
810 Q_D(QQuickXmlListModel);
811 if (d->src != src) {
812 d->src = src;
813 if (d->xml.isEmpty()) // src is only used if d->xml is not set
814 reload();
815 emit sourceChanged();
816 }
817}
818
819/*!
820 \qmlproperty string QtQuick.XmlListModel::XmlListModel::xml
821 This property holds the XML data for this model, if set.
822
823 The text is assumed to be UTF-8 encoded.
824
825 If both \l source and \c xml are set, \c xml is used.
826*/
827QString QQuickXmlListModel::xml() const
828{
829 Q_D(const QQuickXmlListModel);
830 return d->xml;
831}
832
833void QQuickXmlListModel::setXml(const QString &xml)
834{
835 Q_D(QQuickXmlListModel);
836 if (d->xml != xml) {
837 d->xml = xml;
838 reload();
839 emit xmlChanged();
840 }
841}
842
843/*!
844 \qmlproperty string QtQuick.XmlListModel::XmlListModel::query
845 An absolute XPath query representing the base query for creating model items
846 from this model's XmlRole objects. The query should start with '/' or '//'.
847*/
848QString QQuickXmlListModel::query() const
849{
850 Q_D(const QQuickXmlListModel);
851 return d->query;
852}
853
854void QQuickXmlListModel::setQuery(const QString &query)
855{
856 Q_D(QQuickXmlListModel);
857 if (!query.startsWith(c: QLatin1Char('/'))) {
858 qmlWarning(me: this) << QCoreApplication::translate(context: "QQuickXmlRoleList", key: "An XmlListModel query must start with '/' or \"//\"");
859 return;
860 }
861
862 if (d->query != query) {
863 d->query = query;
864 reload();
865 emit queryChanged();
866 }
867}
868
869/*!
870 \qmlproperty string QtQuick.XmlListModel::XmlListModel::namespaceDeclarations
871 The namespace declarations to be used in the XPath queries.
872
873 The namespaces should be declared as in XQuery. For example, if a requested document
874 at http://mysite.com/feed.xml uses the namespace "http://www.w3.org/2005/Atom",
875 this can be declared as the default namespace:
876
877 \qml
878 XmlListModel {
879 source: "http://mysite.com/feed.xml"
880 query: "/feed/entry"
881 namespaceDeclarations: "declare default element namespace 'http://www.w3.org/2005/Atom';"
882
883 XmlRole { name: "title"; query: "title/string()" }
884 }
885 \endqml
886*/
887QString QQuickXmlListModel::namespaceDeclarations() const
888{
889 Q_D(const QQuickXmlListModel);
890 return d->namespaces;
891}
892
893void QQuickXmlListModel::setNamespaceDeclarations(const QString &declarations)
894{
895 Q_D(QQuickXmlListModel);
896 if (d->namespaces != declarations) {
897 d->namespaces = declarations;
898 reload();
899 emit namespaceDeclarationsChanged();
900 }
901}
902
903/*!
904 \qmlmethod object QtQuick.XmlListModel::XmlListModel::get(int index)
905
906 Returns the item at \a index in the model.
907
908 For example, for a model like this:
909
910 \qml
911 XmlListModel {
912 id: model
913 source: "http://mysite.com/feed.xml"
914 query: "/feed/entry"
915 XmlRole { name: "title"; query: "title/string()" }
916 }
917 \endqml
918
919 This will access the \c title value for the first item in the model:
920
921 \js
922 var title = model.get(0).title;
923 \endjs
924*/
925QJSValue QQuickXmlListModel::get(int index) const
926{
927 // Must be called with a context and handle scope
928 Q_D(const QQuickXmlListModel);
929
930 if (index < 0 || index >= count())
931 return QJSValue(QJSValue::UndefinedValue);
932
933 QQmlEngine *engine = qmlContext(this)->engine();
934 ExecutionEngine *v4engine = engine->handle();
935 Scope scope(v4engine);
936 Scoped<Object> o(scope, v4engine->newObject());
937 ScopedString name(scope);
938 ScopedValue value(scope);
939 for (int ii = 0; ii < d->roleObjects.count(); ++ii) {
940 name = v4engine->newIdentifier(text: d->roleObjects[ii]->name());
941 value = v4engine->fromVariant(d->data.value(i: ii).value(i: index));
942 o->insertMember(s: name.getPointer(), v: value);
943 }
944
945 return QJSValue(v4engine, o->asReturnedValue());
946}
947
948/*!
949 \qmlproperty enumeration QtQuick.XmlListModel::XmlListModel::status
950 Specifies the model loading status, which can be one of the following:
951
952 \list
953 \li XmlListModel.Null - No XML data has been set for this model.
954 \li XmlListModel.Ready - The XML data has been loaded into the model.
955 \li XmlListModel.Loading - The model is in the process of reading and loading XML data.
956 \li XmlListModel.Error - An error occurred while the model was loading. See errorString() for details
957 about the error.
958 \endlist
959
960 \sa progress
961
962*/
963QQuickXmlListModel::Status QQuickXmlListModel::status() const
964{
965 Q_D(const QQuickXmlListModel);
966 return d->status;
967}
968
969/*!
970 \qmlproperty real QtQuick.XmlListModel::XmlListModel::progress
971
972 This indicates the current progress of the downloading of the XML data
973 source. This value ranges from 0.0 (no data downloaded) to
974 1.0 (all data downloaded). If the XML data is not from a remote source,
975 the progress becomes 1.0 as soon as the data is read.
976
977 Note that when the progress is 1.0, the XML data has been downloaded, but
978 it is yet to be loaded into the model at this point. Use the status
979 property to find out when the XML data has been read and loaded into
980 the model.
981
982 \sa status, source
983*/
984qreal QQuickXmlListModel::progress() const
985{
986 Q_D(const QQuickXmlListModel);
987 return d->progress;
988}
989
990/*!
991 \qmlmethod QtQuick.XmlListModel::XmlListModel::errorString()
992
993 Returns a string description of the last error that occurred
994 if \l status is XmlListModel::Error.
995*/
996QString QQuickXmlListModel::errorString() const
997{
998 Q_D(const QQuickXmlListModel);
999 return d->errorString;
1000}
1001
1002void QQuickXmlListModel::classBegin()
1003{
1004 Q_D(QQuickXmlListModel);
1005 d->isComponentComplete = false;
1006
1007 QQuickXmlQueryEngine *queryEngine = QQuickXmlQueryEngine::instance(engine: qmlEngine(this));
1008 connect(asender: queryEngine, SIGNAL(queryCompleted(QQuickXmlQueryResult)),
1009 SLOT(queryCompleted(QQuickXmlQueryResult)));
1010 connect(asender: queryEngine, SIGNAL(error(void*,QString)),
1011 SLOT(queryError(void*,QString)));
1012}
1013
1014void QQuickXmlListModel::componentComplete()
1015{
1016 Q_D(QQuickXmlListModel);
1017 d->isComponentComplete = true;
1018 reload();
1019}
1020
1021/*!
1022 \qmlmethod QtQuick.XmlListModel::XmlListModel::reload()
1023
1024 Reloads the model.
1025
1026 If no key roles have been specified, all existing model
1027 data is removed, and the model is rebuilt from scratch.
1028
1029 Otherwise, items are only added if the model does not already
1030 contain items with matching key role values.
1031
1032 \sa {Using key XML roles}, XmlRole::isKey
1033*/
1034void QQuickXmlListModel::reload()
1035{
1036 Q_D(QQuickXmlListModel);
1037
1038 if (!d->isComponentComplete)
1039 return;
1040
1041 QQuickXmlQueryEngine::instance(engine: qmlEngine(this))->abort(id: d->queryId);
1042 d->queryId = -1;
1043
1044 if (d->size < 0)
1045 d->size = 0;
1046
1047#if QT_CONFIG(qml_network)
1048 if (d->reply) {
1049 d->reply->abort();
1050 d->deleteReply();
1051 }
1052#endif
1053
1054 if (!d->xml.isEmpty()) {
1055 d->queryId = QQuickXmlQueryEngine::instance(engine: qmlEngine(this))->doQuery(query: d->query, namespaces: d->namespaces, data: d->xml.toUtf8(), roleObjects: &d->roleObjects, keyRoleResultsCache: d->keyRoleResultsCache);
1056 d->notifyQueryStarted(remoteSource: false);
1057
1058 } else if (d->src.isEmpty()) {
1059 d->queryId = XMLLISTMODEL_CLEAR_ID;
1060 d->notifyQueryStarted(remoteSource: false);
1061 QTimer::singleShot(msec: 0, receiver: this, SLOT(dataCleared()));
1062
1063 } else if (QQmlFile::isLocalFile(url: d->src)) {
1064 QFile file(QQmlFile::urlToLocalFileOrQrc(d->src));
1065 QByteArray data = file.open(flags: QIODevice::ReadOnly) ? file.readAll() : QByteArray();
1066 d->notifyQueryStarted(remoteSource: false);
1067 if (data.isEmpty()) {
1068 d->queryId = XMLLISTMODEL_CLEAR_ID;
1069 QTimer::singleShot(msec: 0, receiver: this, SLOT(dataCleared()));
1070 } else {
1071 d->queryId = QQuickXmlQueryEngine::instance(engine: qmlEngine(this))->doQuery(
1072 query: d->query, namespaces: d->namespaces, data, roleObjects: &d->roleObjects, keyRoleResultsCache: d->keyRoleResultsCache);
1073 }
1074 } else {
1075#if QT_CONFIG(qml_network)
1076 d->notifyQueryStarted(remoteSource: true);
1077 QNetworkRequest req(d->src);
1078 req.setRawHeader(headerName: "Accept", value: "application/xml,*/*");
1079 d->reply = qmlContext(this)->engine()->networkAccessManager()->get(request: req);
1080 QObject::connect(sender: d->reply, SIGNAL(finished()), receiver: this, SLOT(requestFinished()));
1081 QObject::connect(sender: d->reply, SIGNAL(downloadProgress(qint64,qint64)),
1082 receiver: this, SLOT(requestProgress(qint64,qint64)));
1083#else
1084 d->queryId = XMLLISTMODEL_CLEAR_ID;
1085 d->notifyQueryStarted(false);
1086 QTimer::singleShot(0, this, SLOT(dataCleared()));
1087#endif
1088 }
1089}
1090
1091#define XMLLISTMODEL_MAX_REDIRECT 16
1092
1093#if QT_CONFIG(qml_network)
1094void QQuickXmlListModel::requestFinished()
1095{
1096 Q_D(QQuickXmlListModel);
1097
1098 d->redirectCount++;
1099 if (d->redirectCount < XMLLISTMODEL_MAX_REDIRECT) {
1100 QVariant redirect = d->reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute);
1101 if (redirect.isValid()) {
1102 QUrl url = d->reply->url().resolved(relative: redirect.toUrl());
1103 d->deleteReply();
1104 setSource(url);
1105 return;
1106 }
1107 }
1108 d->redirectCount = 0;
1109
1110 if (d->reply->error() != QNetworkReply::NoError) {
1111 d->errorString = d->reply->errorString();
1112 d->deleteReply();
1113
1114 if (d->size > 0) {
1115 beginRemoveRows(parent: QModelIndex(), first: 0, last: d->size - 1);
1116 d->data.clear();
1117 d->size = 0;
1118 endRemoveRows();
1119 emit countChanged();
1120 }
1121
1122 d->status = Error;
1123 d->queryId = -1;
1124 emit statusChanged(d->status);
1125 } else {
1126 QByteArray data = d->reply->readAll();
1127 if (data.isEmpty()) {
1128 d->queryId = XMLLISTMODEL_CLEAR_ID;
1129 QTimer::singleShot(msec: 0, receiver: this, SLOT(dataCleared()));
1130 } else {
1131 d->queryId = QQuickXmlQueryEngine::instance(engine: qmlEngine(this))->doQuery(query: d->query, namespaces: d->namespaces, data, roleObjects: &d->roleObjects, keyRoleResultsCache: d->keyRoleResultsCache);
1132 }
1133 d->deleteReply();
1134
1135 d->progress = 1.0;
1136 emit progressChanged(progress: d->progress);
1137 }
1138}
1139#endif
1140
1141void QQuickXmlListModel::requestProgress(qint64 received, qint64 total)
1142{
1143 Q_D(QQuickXmlListModel);
1144 if (d->status == Loading && total > 0) {
1145 d->progress = qreal(received)/total;
1146 emit progressChanged(progress: d->progress);
1147 }
1148}
1149
1150void QQuickXmlListModel::dataCleared()
1151{
1152 Q_D(QQuickXmlListModel);
1153 QQuickXmlQueryResult r;
1154 r.queryId = XMLLISTMODEL_CLEAR_ID;
1155 r.size = 0;
1156 r.removed << qMakePair(x: 0, y: count());
1157 r.keyRoleResultsCache = d->keyRoleResultsCache;
1158 queryCompleted(r);
1159}
1160
1161void QQuickXmlListModel::queryError(void* object, const QString& error)
1162{
1163 // Be extra careful, object may no longer exist, it's just an ID.
1164 Q_D(QQuickXmlListModel);
1165 for (int i=0; i<d->roleObjects.count(); i++) {
1166 if (d->roleObjects.at(i) == static_cast<QQuickXmlListModelRole*>(object)) {
1167 qmlWarning(me: d->roleObjects.at(i)) << QQuickXmlListModel::tr(s: "invalid query: \"%1\"").arg(a: error);
1168 return;
1169 }
1170 }
1171 qmlWarning(me: this) << QQuickXmlListModel::tr(s: "invalid query: \"%1\"").arg(a: error);
1172}
1173
1174void QQuickXmlListModel::queryCompleted(const QQuickXmlQueryResult &result)
1175{
1176 Q_D(QQuickXmlListModel);
1177 if (result.queryId != d->queryId)
1178 return;
1179
1180 int origCount = d->size;
1181 bool sizeChanged = result.size != d->size;
1182
1183 d->keyRoleResultsCache = result.keyRoleResultsCache;
1184 if (d->src.isEmpty() && d->xml.isEmpty())
1185 d->status = Null;
1186 else
1187 d->status = Ready;
1188 d->errorString.clear();
1189 d->queryId = -1;
1190
1191 bool hasKeys = false;
1192 for (int i=0; i<d->roleObjects.count(); i++) {
1193 if (d->roleObjects[i]->isKey()) {
1194 hasKeys = true;
1195 break;
1196 }
1197 }
1198 if (!hasKeys) {
1199 if (origCount > 0) {
1200 beginRemoveRows(parent: QModelIndex(), first: 0, last: origCount - 1);
1201 endRemoveRows();
1202 }
1203 d->size = result.size;
1204 d->data = result.data;
1205 if (d->size > 0) {
1206 beginInsertRows(parent: QModelIndex(), first: 0, last: d->size - 1);
1207 endInsertRows();
1208 }
1209 } else {
1210 for (int i=0; i<result.removed.count(); i++) {
1211 const int index = result.removed[i].first;
1212 const int count = result.removed[i].second;
1213 if (count > 0) {
1214 beginRemoveRows(parent: QModelIndex(), first: index, last: index + count - 1);
1215 endRemoveRows();
1216 }
1217 }
1218 d->size = result.size;
1219 d->data = result.data;
1220 for (int i=0; i<result.inserted.count(); i++) {
1221 const int index = result.inserted[i].first;
1222 const int count = result.inserted[i].second;
1223 if (count > 0) {
1224 beginInsertRows(parent: QModelIndex(), first: index, last: index + count - 1);
1225 endInsertRows();
1226 }
1227 }
1228 }
1229 if (sizeChanged)
1230 emit countChanged();
1231
1232 emit statusChanged(d->status);
1233}
1234
1235QT_END_NAMESPACE
1236
1237#include <qqmlxmllistmodel.moc>
1238

source code of qtxmlpatterns/src/imports/xmllistmodel/qqmlxmllistmodel.cpp