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