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 | |
22 | Q_DECLARE_METATYPE(QQmlXmlListModelQueryResult) |
23 | |
24 | QT_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 | */ |
75 | QString QQmlXmlListModelRole::name() const |
76 | { |
77 | return m_name; |
78 | } |
79 | |
80 | void 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 | */ |
195 | QString QQmlXmlListModelRole::elementName() const |
196 | { |
197 | return m_elementName; |
198 | } |
199 | |
200 | void 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 | */ |
234 | QString QQmlXmlListModelRole::attributeName() const |
235 | { |
236 | return m_attributeName; |
237 | } |
238 | |
239 | void QQmlXmlListModelRole::setAttributeName(const QString &attributeName) |
240 | { |
241 | if (m_attributeName == attributeName) |
242 | return; |
243 | m_attributeName = attributeName; |
244 | Q_EMIT attributeNameChanged(); |
245 | } |
246 | |
247 | bool 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 | |
339 | QQmlXmlListModel::QQmlXmlListModel(QObject *parent) : QAbstractListModel(parent) { } |
340 | |
341 | QQmlXmlListModel::~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 | |
357 | QModelIndex 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 | |
363 | int QQmlXmlListModel::rowCount(const QModelIndex &parent) const |
364 | { |
365 | return !parent.isValid() ? m_size : 0; |
366 | } |
367 | |
368 | QVariant 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 | |
375 | QHash<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 | */ |
387 | int 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 | */ |
396 | QUrl QQmlXmlListModel::source() const |
397 | { |
398 | return m_source; |
399 | } |
400 | |
401 | void 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 | */ |
416 | QString QQmlXmlListModel::query() const |
417 | { |
418 | return m_query; |
419 | } |
420 | |
421 | void 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 | */ |
441 | QQmlListProperty<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 | |
449 | void 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 | |
467 | void QQmlXmlListModel::clearRole() |
468 | { |
469 | m_roles.clear(); |
470 | m_roleNames.clear(); |
471 | m_roleObjects.clear(); |
472 | } |
473 | |
474 | void 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 | |
482 | void QQmlXmlListModel::clearRole(QQmlListProperty<QQmlXmlListModelRole> *list) |
483 | { |
484 | auto object = qobject_cast<QQmlXmlListModel *>(object: list->object); |
485 | if (object) |
486 | object->clearRole(); |
487 | } |
488 | |
489 | void 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 | |
527 | QQmlXmlListModelQueryJob 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 | |
550 | int 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 | */ |
571 | QQmlXmlListModel::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 | */ |
591 | qreal 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 | */ |
602 | QString QQmlXmlListModel::errorString() const |
603 | { |
604 | return m_errorString; |
605 | } |
606 | |
607 | void QQmlXmlListModel::classBegin() |
608 | { |
609 | m_isComponentComplete = false; |
610 | } |
611 | |
612 | void QQmlXmlListModel::componentComplete() |
613 | { |
614 | m_isComponentComplete = true; |
615 | reload(); |
616 | } |
617 | |
618 | /*! |
619 | \qmlmethod QtQml.XmlListModel::XmlListModel::reload() |
620 | |
621 | Reloads the model. |
622 | */ |
623 | void QQmlXmlListModel::reload() |
624 | { |
625 | if (!m_isComponentComplete) |
626 | return; |
627 | |
628 | if (m_queryId > 0 && m_watchers.contains(key: m_queryId)) |
629 | m_watchers[m_queryId]->cancel(); |
630 | |
631 | m_queryId = -1; |
632 | |
633 | if (m_size < 0) |
634 | m_size = 0; |
635 | |
636 | #if QT_CONFIG(qml_network) |
637 | if (m_reply) { |
638 | m_reply->abort(); |
639 | deleteReply(); |
640 | } |
641 | #endif |
642 | |
643 | const QQmlContext *context = qmlContext(this); |
644 | const auto resolvedSource = context ? context->resolvedUrl(m_source) : m_source; |
645 | |
646 | if (resolvedSource.isEmpty()) { |
647 | m_queryId = 0; |
648 | notifyQueryStarted(remoteSource: false); |
649 | QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared); |
650 | } else if (QQmlFile::isLocalFile(url: resolvedSource)) { |
651 | QFile file(QQmlFile::urlToLocalFileOrQrc(resolvedSource)); |
652 | const bool opened = file.open(flags: QIODevice::ReadOnly); |
653 | if (!opened) |
654 | qWarning(msg: "Failed to open file %s: %s" , qPrintable(file.fileName()), |
655 | qPrintable(file.errorString())); |
656 | QByteArray data = opened ? file.readAll() : QByteArray(); |
657 | notifyQueryStarted(remoteSource: false); |
658 | if (data.isEmpty()) { |
659 | m_queryId = 0; |
660 | QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared); |
661 | } else { |
662 | tryExecuteQuery(data); |
663 | } |
664 | } else { |
665 | #if QT_CONFIG(qml_network) |
666 | notifyQueryStarted(remoteSource: true); |
667 | QNetworkRequest req(resolvedSource); |
668 | req.setRawHeader(headerName: "Accept" , value: "application/xml,*/*" ); |
669 | m_reply = qmlContext(this)->engine()->networkAccessManager()->get(request: req); |
670 | |
671 | QObject::connect(sender: m_reply, signal: &QNetworkReply::finished, context: this, |
672 | slot: &QQmlXmlListModel::requestFinished); |
673 | QObject::connect(sender: m_reply, signal: &QNetworkReply::downloadProgress, context: this, |
674 | slot: &QQmlXmlListModel::requestProgress); |
675 | #else |
676 | m_queryId = 0; |
677 | notifyQueryStarted(false); |
678 | QTimer::singleShot(0, this, &QQmlXmlListModel::dataCleared); |
679 | #endif |
680 | } |
681 | } |
682 | |
683 | #define XMLLISTMODEL_MAX_REDIRECT 16 |
684 | |
685 | #if QT_CONFIG(qml_network) |
686 | void QQmlXmlListModel::requestFinished() |
687 | { |
688 | m_redirectCount++; |
689 | if (m_redirectCount < XMLLISTMODEL_MAX_REDIRECT) { |
690 | QVariant redirect = m_reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute); |
691 | if (redirect.isValid()) { |
692 | QUrl url = m_reply->url().resolved(relative: redirect.toUrl()); |
693 | deleteReply(); |
694 | setSource(url); |
695 | return; |
696 | } |
697 | } |
698 | m_redirectCount = 0; |
699 | |
700 | if (m_reply->error() != QNetworkReply::NoError) { |
701 | m_errorString = m_reply->errorString(); |
702 | deleteReply(); |
703 | |
704 | if (m_size > 0) { |
705 | beginRemoveRows(parent: QModelIndex(), first: 0, last: m_size - 1); |
706 | m_data.clear(); |
707 | m_size = 0; |
708 | endRemoveRows(); |
709 | Q_EMIT countChanged(); |
710 | } |
711 | |
712 | m_status = Error; |
713 | m_queryId = -1; |
714 | Q_EMIT statusChanged(m_status); |
715 | } else { |
716 | QByteArray data = m_reply->readAll(); |
717 | if (data.isEmpty()) { |
718 | m_queryId = 0; |
719 | QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlXmlListModel::dataCleared); |
720 | } else { |
721 | tryExecuteQuery(data); |
722 | } |
723 | deleteReply(); |
724 | |
725 | m_progress = 1.0; |
726 | Q_EMIT progressChanged(progress: m_progress); |
727 | } |
728 | } |
729 | |
730 | void QQmlXmlListModel::deleteReply() |
731 | { |
732 | if (m_reply) { |
733 | QObject::disconnect(sender: m_reply, signal: 0, receiver: this, member: 0); |
734 | m_reply->deleteLater(); |
735 | m_reply = nullptr; |
736 | } |
737 | } |
738 | #endif |
739 | |
740 | void QQmlXmlListModel::requestProgress(qint64 received, qint64 total) |
741 | { |
742 | if (m_status == Loading && total > 0) { |
743 | m_progress = qreal(received) / total; |
744 | Q_EMIT progressChanged(progress: m_progress); |
745 | } |
746 | } |
747 | |
748 | void QQmlXmlListModel::dataCleared() |
749 | { |
750 | QQmlXmlListModelQueryResult r; |
751 | r.queryId = 0; |
752 | queryCompleted(r); |
753 | } |
754 | |
755 | void QQmlXmlListModel::queryError(void *object, const QString &error) |
756 | { |
757 | for (int i = 0; i < m_roleObjects.size(); i++) { |
758 | if (m_roleObjects.at(i) == static_cast<QQmlXmlListModelRole *>(object)) { |
759 | qmlWarning(me: m_roleObjects.at(i)) |
760 | << QQmlXmlListModel::tr(s: "Query error: \"%1\"" ).arg(a: error); |
761 | return; |
762 | } |
763 | } |
764 | qmlWarning(me: this) << QQmlXmlListModel::tr(s: "Query error: \"%1\"" ).arg(a: error); |
765 | } |
766 | |
767 | void QQmlXmlListModel::queryCompleted(const QQmlXmlListModelQueryResult &result) |
768 | { |
769 | if (result.queryId != m_queryId) |
770 | return; |
771 | |
772 | int origCount = m_size; |
773 | bool sizeChanged = result.data.size() != m_size; |
774 | |
775 | if (m_source.isEmpty()) |
776 | m_status = Null; |
777 | else |
778 | m_status = Ready; |
779 | m_errorString.clear(); |
780 | m_queryId = -1; |
781 | |
782 | if (origCount > 0) { |
783 | beginRemoveRows(parent: QModelIndex(), first: 0, last: origCount - 1); |
784 | endRemoveRows(); |
785 | } |
786 | m_size = result.data.size(); |
787 | m_data = result.data; |
788 | |
789 | if (m_size > 0) { |
790 | beginInsertRows(parent: QModelIndex(), first: 0, last: m_size - 1); |
791 | endInsertRows(); |
792 | } |
793 | |
794 | if (sizeChanged) |
795 | Q_EMIT countChanged(); |
796 | |
797 | Q_EMIT statusChanged(m_status); |
798 | } |
799 | |
800 | void QQmlXmlListModel::notifyQueryStarted(bool remoteSource) |
801 | { |
802 | m_progress = remoteSource ? 0.0 : 1.0; |
803 | m_status = QQmlXmlListModel::Loading; |
804 | m_errorString.clear(); |
805 | Q_EMIT progressChanged(progress: m_progress); |
806 | Q_EMIT statusChanged(m_status); |
807 | } |
808 | |
809 | static qsizetype findIndexOfName(const QStringList &elementNames, const QStringView &name, |
810 | qsizetype startIndex = 0) |
811 | { |
812 | for (auto idx = startIndex; idx < elementNames.size(); ++idx) { |
813 | if (elementNames[idx].startsWith(s: name)) |
814 | return idx; |
815 | } |
816 | return -1; |
817 | } |
818 | |
819 | QQmlXmlListModelQueryRunnable::QQmlXmlListModelQueryRunnable(QQmlXmlListModelQueryJob &&job) |
820 | : m_job(std::move(job)) |
821 | { |
822 | setAutoDelete(true); |
823 | } |
824 | |
825 | void QQmlXmlListModelQueryRunnable::run() |
826 | { |
827 | m_promise.start(); |
828 | if (!m_promise.isCanceled()) { |
829 | QQmlXmlListModelQueryResult result; |
830 | result.queryId = m_job.queryId; |
831 | doQueryJob(currentResult: &result); |
832 | m_promise.addResult(result: std::move(result)); |
833 | } |
834 | m_promise.finish(); |
835 | } |
836 | |
837 | QFuture<QQmlXmlListModelQueryResult> QQmlXmlListModelQueryRunnable::future() const |
838 | { |
839 | return m_promise.future(); |
840 | } |
841 | |
842 | void QQmlXmlListModelQueryRunnable::doQueryJob(QQmlXmlListModelQueryResult *currentResult) |
843 | { |
844 | Q_ASSERT(m_job.queryId != -1); |
845 | |
846 | QByteArray data(m_job.data); |
847 | QXmlStreamReader reader; |
848 | reader.addData(data); |
849 | |
850 | QStringList items = m_job.query.split(sep: QLatin1Char('/'), behavior: Qt::SkipEmptyParts); |
851 | |
852 | while (!reader.atEnd() && !m_promise.isCanceled()) { |
853 | int i = 0; |
854 | while (i < items.size()) { |
855 | if (reader.readNextStartElement()) { |
856 | if (reader.name() == items.at(i)) { |
857 | if (i != items.size() - 1) { |
858 | i++; |
859 | continue; |
860 | } else { |
861 | processElement(currentResult, element: items.at(i), reader); |
862 | } |
863 | } else { |
864 | reader.skipCurrentElement(); |
865 | } |
866 | } |
867 | if (reader.tokenType() == QXmlStreamReader::Invalid) { |
868 | reader.readNext(); |
869 | break; |
870 | } else if (reader.hasError()) { |
871 | reader.raiseError(); |
872 | break; |
873 | } |
874 | } |
875 | } |
876 | } |
877 | |
878 | void QQmlXmlListModelQueryRunnable::processElement(QQmlXmlListModelQueryResult *currentResult, |
879 | const QString &element, QXmlStreamReader &reader) |
880 | { |
881 | if (!reader.isStartElement() || reader.name() != element) |
882 | return; |
883 | |
884 | const QStringList &elementNames = m_job.elementNames; |
885 | const QStringList &attributes = m_job.elementAttributes; |
886 | QFlatMap<int, QString> results; |
887 | |
888 | // First of all check all the empty element names. They might have |
889 | // attributes to be read from the current element |
890 | if (!reader.attributes().isEmpty()) { |
891 | for (auto index = 0; index < elementNames.size(); ++index) { |
892 | if (elementNames.at(i: index).isEmpty() && !attributes.at(i: index).isEmpty()) { |
893 | const QString &attribute = attributes.at(i: index); |
894 | if (reader.attributes().hasAttribute(qualifiedName: attribute)) |
895 | results[index] = reader.attributes().value(qualifiedName: attribute).toString(); |
896 | } |
897 | } |
898 | } |
899 | |
900 | // After that we recursively search for the elements, considering that we |
901 | // can have nested element names in our model, and that the same element |
902 | // can be used multiple types (with different attributes, for example) |
903 | readSubTree(prefix: QString(), reader, results, errors: ¤tResult->errors); |
904 | |
905 | if (reader.hasError()) |
906 | currentResult->errors.push_back(t: qMakePair(value1: this, value2: reader.errorString())); |
907 | |
908 | currentResult->data << results; |
909 | } |
910 | |
911 | void QQmlXmlListModelQueryRunnable::readSubTree(const QString &prefix, QXmlStreamReader &reader, |
912 | QFlatMap<int, QString> &results, |
913 | QList<QPair<void *, QString>> *errors) |
914 | { |
915 | const QStringList &elementNames = m_job.elementNames; |
916 | const QStringList &attributes = m_job.elementAttributes; |
917 | while (reader.readNextStartElement()) { |
918 | const auto name = reader.name(); |
919 | const QString fullName = |
920 | prefix.isEmpty() ? name.toString() : (prefix + QLatin1Char('/') + name.toString()); |
921 | qsizetype index = name.isEmpty() ? -1 : findIndexOfName(elementNames, name: fullName); |
922 | if (index >= 0) { |
923 | // We can have multiple roles with the same element name, but |
924 | // different attributes, so we need to cache the attributes and |
925 | // element text. |
926 | const auto elementAttributes = reader.attributes(); |
927 | // We can read text only when the element actually contains it, |
928 | // otherwise it will be an error. It can also be used to check that |
929 | // we've reached the bottom level. |
930 | QString elementText; |
931 | bool elementTextRead = false; |
932 | while (index >= 0) { |
933 | // if the path matches completely, not just starts with, we |
934 | // need to actually extract value |
935 | if (elementNames[index] == fullName) { |
936 | QString roleResult; |
937 | const QString &attribute = attributes.at(i: index); |
938 | if (!attribute.isEmpty()) { |
939 | if (elementAttributes.hasAttribute(qualifiedName: attribute)) { |
940 | roleResult = elementAttributes.value(qualifiedName: attributes.at(i: index)).toString(); |
941 | } else { |
942 | errors->push_back(t: qMakePair(value1: m_job.roleQueryErrorId.at(i: index), |
943 | value2: QLatin1String("Attribute %1 not found" ) |
944 | .arg(args: attributes[index]))); |
945 | } |
946 | } else if (!elementNames.at(i: index).isEmpty()) { |
947 | if (!elementTextRead) { |
948 | elementText = |
949 | reader.readElementText(behaviour: QXmlStreamReader::IncludeChildElements); |
950 | elementTextRead = true; |
951 | } |
952 | roleResult = elementText; |
953 | } |
954 | results[index] = roleResult; |
955 | } |
956 | // search for the next role with the same element name |
957 | index = findIndexOfName(elementNames, name: fullName, startIndex: index + 1); |
958 | } |
959 | if (!elementTextRead) |
960 | readSubTree(prefix: fullName, reader, results, errors); |
961 | } else { |
962 | reader.skipCurrentElement(); |
963 | } |
964 | } |
965 | } |
966 | |
967 | QT_END_NAMESPACE |
968 | |
969 | #include "moc_qqmlxmllistmodel_p.cpp" |
970 | |