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