1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2019 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtQml module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include <private/qqmldatablob_p.h> |
41 | #include <private/qqmlglobal_p.h> |
42 | #include <private/qqmlprofiler_p.h> |
43 | #include <private/qqmltypeloader_p.h> |
44 | #include <private/qqmltypeloaderthread_p.h> |
45 | #include <private/qqmlsourcecoordinate_p.h> |
46 | |
47 | #include <QtQml/qqmlengine.h> |
48 | |
49 | #include <qtqml_tracepoints_p.h> |
50 | |
51 | #ifdef DATABLOB_DEBUG |
52 | #define ASSERT_CALLBACK() do { if (!m_typeLoader || !m_typeLoader->m_thread->isThisThread()) qFatal("QQmlDataBlob: An API call was made outside a callback"); } while (false) |
53 | #else |
54 | #define ASSERT_CALLBACK() |
55 | #endif |
56 | |
57 | DEFINE_BOOL_CONFIG_OPTION(dumpErrors, QML_DUMP_ERRORS); |
58 | |
59 | QT_BEGIN_NAMESPACE |
60 | |
61 | /*! |
62 | \class QQmlDataBlob |
63 | \brief The QQmlDataBlob encapsulates a data request that can be issued to a QQmlTypeLoader. |
64 | \internal |
65 | |
66 | QQmlDataBlob's are loaded by a QQmlTypeLoader. The user creates the QQmlDataBlob |
67 | and then calls QQmlTypeLoader::load() or QQmlTypeLoader::loadWithStaticData() to load it. |
68 | The QQmlTypeLoader invokes callbacks on the QQmlDataBlob as data becomes available. |
69 | */ |
70 | |
71 | /*! |
72 | \enum QQmlDataBlob::Status |
73 | |
74 | This enum describes the status of the data blob. |
75 | |
76 | \list |
77 | \li Null The blob has not yet been loaded by a QQmlTypeLoader |
78 | \li Loading The blob is loading network data. The QQmlDataBlob::setData() callback has not yet been |
79 | invoked or has not yet returned. |
80 | \li WaitingForDependencies The blob is waiting for dependencies to be done before continuing. |
81 | This status only occurs after the QQmlDataBlob::setData() callback has been made, and when the |
82 | blob has outstanding dependencies. |
83 | \li Complete The blob's data has been loaded and all dependencies are done. |
84 | \li Error An error has been set on this blob. |
85 | \endlist |
86 | */ |
87 | |
88 | /*! |
89 | \enum QQmlDataBlob::Type |
90 | |
91 | This enum describes the type of the data blob. |
92 | |
93 | \list |
94 | \li QmlFile This is a QQmlTypeData |
95 | \li JavaScriptFile This is a QQmlScriptData |
96 | \li QmldirFile This is a QQmlQmldirData |
97 | \endlist |
98 | */ |
99 | |
100 | /*! |
101 | Create a new QQmlDataBlob for \a url and of the provided \a type. |
102 | */ |
103 | QQmlDataBlob::QQmlDataBlob(const QUrl &url, Type type, QQmlTypeLoader *manager) |
104 | : m_typeLoader(manager), m_type(type), m_url(url), m_finalUrl(url), m_redirectCount(0), |
105 | m_inCallback(false), m_isDone(false) |
106 | { |
107 | //Set here because we need to get the engine from the manager |
108 | if (m_typeLoader->engine() && m_typeLoader->engine()->urlInterceptor()) |
109 | m_url = m_typeLoader->engine()->urlInterceptor()->intercept(path: m_url, |
110 | type: (QQmlAbstractUrlInterceptor::DataType)m_type); |
111 | } |
112 | |
113 | /*! \internal */ |
114 | QQmlDataBlob::~QQmlDataBlob() |
115 | { |
116 | Q_ASSERT(m_waitingOnMe.isEmpty()); |
117 | |
118 | cancelAllWaitingFor(); |
119 | } |
120 | |
121 | /*! |
122 | Must be called before loading can occur. |
123 | */ |
124 | void QQmlDataBlob::startLoading() |
125 | { |
126 | Q_ASSERT(status() == QQmlDataBlob::Null); |
127 | m_data.setStatus(QQmlDataBlob::Loading); |
128 | } |
129 | |
130 | /*! |
131 | Returns the type provided to the constructor. |
132 | */ |
133 | QQmlDataBlob::Type QQmlDataBlob::type() const |
134 | { |
135 | return m_type; |
136 | } |
137 | |
138 | /*! |
139 | Returns the blob's status. |
140 | */ |
141 | QQmlDataBlob::Status QQmlDataBlob::status() const |
142 | { |
143 | return m_data.status(); |
144 | } |
145 | |
146 | /*! |
147 | Returns true if the status is Null. |
148 | */ |
149 | bool QQmlDataBlob::isNull() const |
150 | { |
151 | return status() == Null; |
152 | } |
153 | |
154 | /*! |
155 | Returns true if the status is Loading. |
156 | */ |
157 | bool QQmlDataBlob::isLoading() const |
158 | { |
159 | return status() == Loading; |
160 | } |
161 | |
162 | /*! |
163 | Returns true if the status is WaitingForDependencies. |
164 | */ |
165 | bool QQmlDataBlob::isWaiting() const |
166 | { |
167 | return status() == WaitingForDependencies || |
168 | status() == ResolvingDependencies; |
169 | } |
170 | |
171 | /*! |
172 | Returns true if the status is Complete. |
173 | */ |
174 | bool QQmlDataBlob::isComplete() const |
175 | { |
176 | return status() == Complete; |
177 | } |
178 | |
179 | /*! |
180 | Returns true if the status is Error. |
181 | */ |
182 | bool QQmlDataBlob::isError() const |
183 | { |
184 | return status() == Error; |
185 | } |
186 | |
187 | /*! |
188 | Returns true if the status is Complete or Error. |
189 | */ |
190 | bool QQmlDataBlob::isCompleteOrError() const |
191 | { |
192 | Status s = status(); |
193 | return s == Error || s == Complete; |
194 | } |
195 | |
196 | /*! |
197 | Returns the data download progress from 0 to 1. |
198 | */ |
199 | qreal QQmlDataBlob::progress() const |
200 | { |
201 | quint8 p = m_data.progress(); |
202 | if (p == 0xFF) return 1.; |
203 | else return qreal(p) / qreal(0xFF); |
204 | } |
205 | |
206 | /*! |
207 | Returns the physical url of the data. Initially this is the same as |
208 | finalUrl(), but if a URL interceptor is set, it will work on this URL |
209 | and leave finalUrl() alone. |
210 | |
211 | \sa finalUrl() |
212 | */ |
213 | QUrl QQmlDataBlob::url() const |
214 | { |
215 | return m_url; |
216 | } |
217 | |
218 | QString QQmlDataBlob::urlString() const |
219 | { |
220 | if (m_urlString.isEmpty()) |
221 | m_urlString = m_url.toString(); |
222 | |
223 | return m_urlString; |
224 | } |
225 | |
226 | /*! |
227 | Returns the logical URL to be used for resolving further URLs referred to in |
228 | the code. |
229 | |
230 | This is the blob url passed to the constructor. If a URL interceptor rewrites |
231 | the URL, this one stays the same. If a network redirect happens while fetching |
232 | the data, this url is updated to reflect the new location. Therefore, if both |
233 | an interception and a redirection happen, the final url will indirectly |
234 | incorporate the result of the interception, potentially breaking further |
235 | lookups. |
236 | |
237 | \sa url() |
238 | */ |
239 | QUrl QQmlDataBlob::finalUrl() const |
240 | { |
241 | return m_finalUrl; |
242 | } |
243 | |
244 | /*! |
245 | Returns the finalUrl() as a string. |
246 | */ |
247 | QString QQmlDataBlob::finalUrlString() const |
248 | { |
249 | if (m_finalUrlString.isEmpty()) |
250 | m_finalUrlString = m_finalUrl.toString(); |
251 | |
252 | return m_finalUrlString; |
253 | } |
254 | |
255 | /*! |
256 | Return the errors on this blob. |
257 | |
258 | May only be called from the load thread, or after the blob isCompleteOrError(). |
259 | */ |
260 | QList<QQmlError> QQmlDataBlob::errors() const |
261 | { |
262 | Q_ASSERT(isCompleteOrError() || (m_typeLoader && m_typeLoader->m_thread->isThisThread())); |
263 | return m_errors; |
264 | } |
265 | |
266 | /*! |
267 | Mark this blob as having \a errors. |
268 | |
269 | All outstanding dependencies will be cancelled. Requests to add new dependencies |
270 | will be ignored. Entry into the Error state is irreversable. |
271 | |
272 | The setError() method may only be called from within a QQmlDataBlob callback. |
273 | */ |
274 | void QQmlDataBlob::setError(const QQmlError &errors) |
275 | { |
276 | ASSERT_CALLBACK(); |
277 | |
278 | QList<QQmlError> l; |
279 | l << errors; |
280 | setError(l); |
281 | } |
282 | |
283 | /*! |
284 | \overload |
285 | */ |
286 | void QQmlDataBlob::setError(const QList<QQmlError> &errors) |
287 | { |
288 | ASSERT_CALLBACK(); |
289 | |
290 | Q_ASSERT(status() != Error); |
291 | Q_ASSERT(m_errors.isEmpty()); |
292 | |
293 | m_errors = errors; // Must be set before the m_data fence |
294 | m_data.setStatus(Error); |
295 | |
296 | if (dumpErrors()) { |
297 | qWarning().nospace() << "Errors for " << urlString(); |
298 | for (int ii = 0; ii < errors.count(); ++ii) |
299 | qWarning().nospace() << " " << qPrintable(errors.at(ii).toString()); |
300 | } |
301 | cancelAllWaitingFor(); |
302 | |
303 | if (!m_inCallback) |
304 | tryDone(); |
305 | } |
306 | |
307 | void QQmlDataBlob::setError(const QQmlJS::DiagnosticMessage &error) |
308 | { |
309 | QQmlError e; |
310 | e.setColumn(qmlConvertSourceCoordinate<quint32, int>(n: error.loc.startColumn)); |
311 | e.setLine(qmlConvertSourceCoordinate<quint32, int>(n: error.loc.startLine)); |
312 | e.setDescription(error.message); |
313 | e.setUrl(url()); |
314 | setError(e); |
315 | } |
316 | |
317 | void QQmlDataBlob::setError(const QVector<QQmlError> &errors) |
318 | { |
319 | QList<QQmlError> finalErrors; |
320 | finalErrors.reserve(alloc: errors.count()); |
321 | for (const auto &error : errors) { |
322 | QQmlError e = error; |
323 | e.setUrl(url()); |
324 | finalErrors << e; |
325 | } |
326 | setError(finalErrors); |
327 | } |
328 | |
329 | void QQmlDataBlob::setError(const QString &description) |
330 | { |
331 | QQmlError e; |
332 | e.setDescription(description); |
333 | e.setUrl(url()); |
334 | setError(e); |
335 | } |
336 | |
337 | /*! |
338 | Wait for \a blob to become complete or to error. If \a blob is already |
339 | complete or in error, or this blob is already complete, this has no effect. |
340 | |
341 | The setError() method may only be called from within a QQmlDataBlob callback. |
342 | */ |
343 | void QQmlDataBlob::addDependency(QQmlDataBlob *blob) |
344 | { |
345 | ASSERT_CALLBACK(); |
346 | |
347 | Q_ASSERT(status() != Null); |
348 | |
349 | if (!blob || |
350 | blob->status() == Error || blob->status() == Complete || |
351 | status() == Error || status() == Complete || m_isDone) |
352 | return; |
353 | |
354 | for (const auto &existingDep: qAsConst(t&: m_waitingFor)) |
355 | if (existingDep.data() == blob) |
356 | return; |
357 | |
358 | m_data.setStatus(WaitingForDependencies); |
359 | |
360 | m_waitingFor.append(t: blob); |
361 | blob->m_waitingOnMe.append(t: this); |
362 | |
363 | // Check circular dependency |
364 | if (m_waitingOnMe.indexOf(t: blob) >= 0) { |
365 | qWarning() << "Cyclic dependency detected between" << this->url().toString() << "and" << blob->url().toString(); |
366 | m_data.setStatus(Error); |
367 | } |
368 | } |
369 | |
370 | /*! |
371 | \fn void QQmlDataBlob::dataReceived(const Data &data) |
372 | |
373 | Invoked when data for the blob is received. Implementors should use this callback |
374 | to determine a blob's dependencies. Within this callback you may call setError() |
375 | or addDependency(). |
376 | */ |
377 | |
378 | /*! |
379 | Invoked once data has either been received or a network error occurred, and all |
380 | dependencies are complete. |
381 | |
382 | You can set an error in this method, but you cannot add new dependencies. Implementors |
383 | should use this callback to finalize processing of data. |
384 | |
385 | The default implementation does nothing. |
386 | |
387 | XXX Rename processData() or some such to avoid confusion between done() (processing thread) |
388 | and completed() (main thread) |
389 | */ |
390 | void QQmlDataBlob::done() |
391 | { |
392 | } |
393 | |
394 | #if QT_CONFIG(qml_network) |
395 | /*! |
396 | Invoked if there is a network error while fetching this blob. |
397 | |
398 | The default implementation sets an appropriate QQmlError. |
399 | */ |
400 | void QQmlDataBlob::networkError(QNetworkReply::NetworkError networkError) |
401 | { |
402 | Q_UNUSED(networkError); |
403 | |
404 | QQmlError error; |
405 | error.setUrl(m_url); |
406 | |
407 | const char *errorString = nullptr; |
408 | switch (networkError) { |
409 | default: |
410 | errorString = "Network error" ; |
411 | break; |
412 | case QNetworkReply::ConnectionRefusedError: |
413 | errorString = "Connection refused" ; |
414 | break; |
415 | case QNetworkReply::RemoteHostClosedError: |
416 | errorString = "Remote host closed the connection" ; |
417 | break; |
418 | case QNetworkReply::HostNotFoundError: |
419 | errorString = "Host not found" ; |
420 | break; |
421 | case QNetworkReply::TimeoutError: |
422 | errorString = "Timeout" ; |
423 | break; |
424 | case QNetworkReply::ProxyConnectionRefusedError: |
425 | case QNetworkReply::ProxyConnectionClosedError: |
426 | case QNetworkReply::ProxyNotFoundError: |
427 | case QNetworkReply::ProxyTimeoutError: |
428 | case QNetworkReply::ProxyAuthenticationRequiredError: |
429 | case QNetworkReply::UnknownProxyError: |
430 | errorString = "Proxy error" ; |
431 | break; |
432 | case QNetworkReply::ContentAccessDenied: |
433 | errorString = "Access denied" ; |
434 | break; |
435 | case QNetworkReply::ContentNotFoundError: |
436 | errorString = "File not found" ; |
437 | break; |
438 | case QNetworkReply::AuthenticationRequiredError: |
439 | errorString = "Authentication required" ; |
440 | break; |
441 | }; |
442 | |
443 | error.setDescription(QLatin1String(errorString)); |
444 | |
445 | setError(error); |
446 | } |
447 | #endif // qml_network |
448 | |
449 | /*! |
450 | Called if \a blob, which was previously waited for, has an error. |
451 | |
452 | The default implementation does nothing. |
453 | */ |
454 | void QQmlDataBlob::dependencyError(QQmlDataBlob *blob) |
455 | { |
456 | Q_UNUSED(blob); |
457 | } |
458 | |
459 | /*! |
460 | Called if \a blob, which was previously waited for, has completed. |
461 | |
462 | The default implementation does nothing. |
463 | */ |
464 | void QQmlDataBlob::dependencyComplete(QQmlDataBlob *blob) |
465 | { |
466 | Q_UNUSED(blob); |
467 | } |
468 | |
469 | /*! |
470 | Called when all blobs waited for have completed. This occurs regardless of |
471 | whether they are in error, or complete state. |
472 | |
473 | The default implementation does nothing. |
474 | */ |
475 | void QQmlDataBlob::allDependenciesDone() |
476 | { |
477 | m_data.setStatus(QQmlDataBlob::ResolvingDependencies); |
478 | } |
479 | |
480 | /*! |
481 | Called when the download progress of this blob changes. \a progress goes |
482 | from 0 to 1. |
483 | |
484 | This callback is only invoked if an asynchronous load for this blob is |
485 | made. An asynchronous load is one in which the Asynchronous mode is |
486 | specified explicitly, or one that is implicitly delayed due to a network |
487 | operation. |
488 | |
489 | The default implementation does nothing. |
490 | */ |
491 | void QQmlDataBlob::downloadProgressChanged(qreal progress) |
492 | { |
493 | Q_UNUSED(progress); |
494 | } |
495 | |
496 | /*! |
497 | Invoked on the main thread sometime after done() was called on the load thread. |
498 | |
499 | You cannot modify the blobs state at all in this callback and cannot depend on the |
500 | order or timeliness of these callbacks. Implementors should use this callback to notify |
501 | dependencies on the main thread that the blob is done and not a lot else. |
502 | |
503 | This callback is only invoked if an asynchronous load for this blob is |
504 | made. An asynchronous load is one in which the Asynchronous mode is |
505 | specified explicitly, or one that is implicitly delayed due to a network |
506 | operation. |
507 | |
508 | The default implementation does nothing. |
509 | */ |
510 | void QQmlDataBlob::completed() |
511 | { |
512 | } |
513 | |
514 | |
515 | void QQmlDataBlob::tryDone() |
516 | { |
517 | if (status() != Loading && m_waitingFor.isEmpty() && !m_isDone) { |
518 | m_isDone = true; |
519 | addref(); |
520 | |
521 | #ifdef DATABLOB_DEBUG |
522 | qWarning("QQmlDataBlob::done() %s" , qPrintable(urlString())); |
523 | #endif |
524 | done(); |
525 | |
526 | if (status() != Error) |
527 | m_data.setStatus(Complete); |
528 | |
529 | notifyAllWaitingOnMe(); |
530 | |
531 | // Locking is not required here, as anyone expecting callbacks must |
532 | // already be protected against the blob being completed (as set above); |
533 | #ifdef DATABLOB_DEBUG |
534 | qWarning("QQmlDataBlob: Dispatching completed" ); |
535 | #endif |
536 | m_typeLoader->m_thread->callCompleted(b: this); |
537 | |
538 | release(); |
539 | } |
540 | } |
541 | |
542 | void QQmlDataBlob::cancelAllWaitingFor() |
543 | { |
544 | while (m_waitingFor.count()) { |
545 | QQmlRefPointer<QQmlDataBlob> blob = m_waitingFor.takeLast(); |
546 | |
547 | Q_ASSERT(blob->m_waitingOnMe.contains(this)); |
548 | |
549 | blob->m_waitingOnMe.removeOne(t: this); |
550 | } |
551 | } |
552 | |
553 | void QQmlDataBlob::notifyAllWaitingOnMe() |
554 | { |
555 | while (m_waitingOnMe.count()) { |
556 | QQmlDataBlob *blob = m_waitingOnMe.takeLast(); |
557 | |
558 | Q_ASSERT(std::any_of(blob->m_waitingFor.constBegin(), blob->m_waitingFor.constEnd(), |
559 | [this](const QQmlRefPointer<QQmlDataBlob> &waiting) { return waiting.data() == this; })); |
560 | |
561 | blob->notifyComplete(this); |
562 | } |
563 | } |
564 | |
565 | void QQmlDataBlob::notifyComplete(QQmlDataBlob *blob) |
566 | { |
567 | Q_ASSERT(blob->status() == Error || blob->status() == Complete); |
568 | Q_TRACE_SCOPE(QQmlCompiling, blob->url()); |
569 | QQmlCompilingProfiler prof(typeLoader()->profiler(), blob); |
570 | |
571 | m_inCallback = true; |
572 | |
573 | QQmlRefPointer<QQmlDataBlob> blobRef; |
574 | for (int i = 0; i < m_waitingFor.count(); ++i) { |
575 | if (m_waitingFor.at(i).data() == blob) { |
576 | blobRef = m_waitingFor.takeAt(i); |
577 | break; |
578 | } |
579 | } |
580 | Q_ASSERT(blobRef); |
581 | |
582 | if (blob->status() == Error) { |
583 | dependencyError(blob); |
584 | } else if (blob->status() == Complete) { |
585 | dependencyComplete(blob); |
586 | } |
587 | |
588 | if (!isError() && m_waitingFor.isEmpty()) |
589 | allDependenciesDone(); |
590 | |
591 | m_inCallback = false; |
592 | |
593 | tryDone(); |
594 | } |
595 | |
596 | QString QQmlDataBlob::SourceCodeData::readAll(QString *error) const |
597 | { |
598 | error->clear(); |
599 | if (hasInlineSourceCode) |
600 | return inlineSourceCode; |
601 | |
602 | QFile f(fileInfo.absoluteFilePath()); |
603 | if (!f.open(flags: QIODevice::ReadOnly)) { |
604 | *error = f.errorString(); |
605 | return QString(); |
606 | } |
607 | |
608 | const qint64 fileSize = fileInfo.size(); |
609 | |
610 | if (uchar *mappedData = f.map(offset: 0, size: fileSize)) { |
611 | QString source = QString::fromUtf8(str: reinterpret_cast<const char *>(mappedData), size: fileSize); |
612 | f.unmap(address: mappedData); |
613 | return source; |
614 | } |
615 | |
616 | QByteArray data(fileSize, Qt::Uninitialized); |
617 | if (f.read(data: data.data(), maxlen: data.length()) != data.length()) { |
618 | *error = f.errorString(); |
619 | return QString(); |
620 | } |
621 | return QString::fromUtf8(str: data); |
622 | } |
623 | |
624 | QDateTime QQmlDataBlob::SourceCodeData::sourceTimeStamp() const |
625 | { |
626 | if (hasInlineSourceCode) |
627 | return QDateTime(); |
628 | |
629 | return fileInfo.lastModified(); |
630 | } |
631 | |
632 | bool QQmlDataBlob::SourceCodeData::exists() const |
633 | { |
634 | if (hasInlineSourceCode) |
635 | return true; |
636 | return fileInfo.exists(); |
637 | } |
638 | |
639 | bool QQmlDataBlob::SourceCodeData::isEmpty() const |
640 | { |
641 | if (hasInlineSourceCode) |
642 | return inlineSourceCode.isEmpty(); |
643 | return fileInfo.size() == 0; |
644 | } |
645 | |
646 | QT_END_NAMESPACE |
647 | |