Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhelpcollectionhandler.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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// Qt-Security score:critical reason:data-parser
4
6#include "qhelp_global.h"
9#include "qhelplink.h"
10
11#include <QtCore/qdatastream.h>
12#include <QtCore/qdatetime.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qfile.h>
15#include <QtCore/qfileinfo.h>
16#include <QtCore/qset.h>
17#include <QtCore/qtimer.h>
18#include <QtCore/qversionnumber.h>
19#include <QtSql/qsqldriver.h>
20#include <QtSql/qsqlerror.h>
21#include <QtSql/qsqlquery.h>
22
23#include <algorithm>
24#include <iterator>
25#include <limits>
26#include <optional>
27
28QT_BEGIN_NAMESPACE
29
30using namespace Qt::StringLiterals;
31
32// Stored as PRAGMA user_version. Increase when the format of the collection
33// file changes in an incompatible way. Collection files without a version
34// (0) were created before the version was introduced.
35static constexpr int CollectionSchemaVersion = 1;
36
38{
39 return "PRAGMA user_version = "_L1 + QString::number(CollectionSchemaVersion);
40}
41
43{
44public:
46
47 Transaction(const QString &connectionName)
50 {
51 if (m_inTransaction)
52 m_inTransaction = m_db.transaction();
53 }
54
56 {
57 if (m_inTransaction)
58 m_db.rollback();
59 }
60
61 [[nodiscard]] bool commit()
62 {
63 if (!m_inTransaction)
64 return true;
65
66 // On failure, the transaction stays open and is rolled back in the destructor.
67 if (!m_db.commit())
68 return false;
69
70 m_inTransaction = false;
71 return true;
72 }
73
74private:
75 QSqlDatabase m_db;
76 bool m_inTransaction;
77};
78
79// Reads the highest id stored in the given column. The ids of the rows inserted afterwards
80// are bound explicitly, starting with the returned id + 1. Returns std::nullopt if the ids
81// of count such rows wouldn't fit into qint64.
82static std::optional<qint64> readMaxId(QSqlQuery *query, const QString &tableName,
83 const QString &columnName, int count)
84{
85 if (!query->exec("SELECT MAX(%1) FROM %2"_L1.arg(columnName, tableName)) || !query->next())
86 return {};
87
88 const qint64 maxId = query->value(0).toLongLong();
89 if (maxId > std::numeric_limits<qint64>::max() - count)
90 return {};
91 return maxId;
92}
93
94QHelpCollectionHandler::QHelpCollectionHandler(const QString &collectionFile, QObject *parent)
95 : QObject(parent)
96 , m_collectionFile(collectionFile)
97{
98 const QFileInfo fi(m_collectionFile);
99 if (!fi.isAbsolute())
100 m_collectionFile = fi.absoluteFilePath();
101}
102
107
108bool QHelpCollectionHandler::isDBOpened() const
109{
110 if (m_query)
111 return true;
112 auto *that = const_cast<QHelpCollectionHandler *>(this);
113 emit that->error(tr("The collection file \"%1\" is not set up yet.").arg(m_collectionFile));
114 return false;
115}
116
117bool QHelpCollectionHandler::isDBWritable()
118{
119 if (!isDBOpened())
120 return false;
121 if (!m_readOnly)
122 return true;
123 emit error(tr("The collection file \"%1\" is opened in read-only mode.").arg(m_collectionFile));
124 return false;
125}
126
127void QHelpCollectionHandler::closeDB()
128{
129 if (!m_query)
130 return;
131
132 m_query.reset();
133 QSqlDatabase::removeDatabase(m_connectionName);
134 m_connectionName.clear();
135}
136
138{
139 if (m_query)
140 return true;
141
142 if (m_readOnly) {
143 // A missing or empty file has no tables, as these are only created in
144 // fully writable mode. Either way the collection is unusable, so report
145 // it here with a clearer message than the failures further below.
146 const QFileInfo fi(m_collectionFile);
147 if (!fi.exists()) {
148 emit error(tr("The collection file \"%1\" does not exist.").arg(m_collectionFile));
149 return false;
150 }
151 if (fi.size() == 0) {
152 emit error(tr("The collection file \"%1\" is empty.").arg(m_collectionFile));
153 return false;
154 }
155 }
156
157 m_connectionName = QHelpGlobal::uniquifyConnectionName("QHelpCollectionHandler"_L1, this);
158 {
159 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_connectionName);
160 if (db.driver()
161 && db.driver()->lastError().type() == QSqlError::ConnectionError) {
162 emit error(tr("Cannot load sqlite database driver."));
163 return false;
164 }
165
166 if (m_readOnly)
167 db.setConnectOptions("QSQLITE_OPEN_READONLY"_L1);
168 db.setDatabaseName(collectionFile());
169 if (db.open())
170 m_query.reset(new QSqlQuery(db));
171
172 if (!m_query) {
173 QSqlDatabase::removeDatabase(m_connectionName);
174 emit error(tr("Cannot open collection file: %1").arg(collectionFile()));
175 return false;
176 }
177 }
178
179 if (!m_query->exec("PRAGMA user_version"_L1) || !m_query->next()) {
180 closeDB();
181 emit error(tr("Cannot read the version of collection file %1.").arg(collectionFile()));
182 return false;
183 }
184
185 const int schemaVersion = m_query->value(0).toInt();
186 if (schemaVersion > CollectionSchemaVersion) {
187 closeDB();
188 emit error(tr("The collection file \"%1\" was created by a newer version of Qt Help.")
189 .arg(m_collectionFile));
190 return false;
191 }
192
193 if (m_readOnly) {
194 // Nothing is created in read-only mode, so the tables every collection
195 // file has must already exist. Older collection files lack the index
196 // tables, which are only added in fully writable mode.
197 const QStringList tables = {
198 "NamespaceTable"_L1,
199 "FolderTable"_L1,
200 "FilterAttributeTable"_L1,
201 "FilterNameTable"_L1,
202 "FilterTable"_L1,
203 "SettingsTable"_L1
204 };
205 const QString queryString = "SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\' "
206 "AND (Name=\'"_L1 + tables.join("\' OR Name=\'"_L1) + "\')"_L1;
207 if (!m_query->exec(queryString) || !m_query->next()
208 || m_query->value(0).toInt() != tables.size()) {
209 closeDB();
210 emit error(tr("The collection file \"%1\" is not a valid help collection file.")
211 .arg(m_collectionFile));
212 return false;
213 }
214 return true;
215 }
216
217 m_query->exec("PRAGMA synchronous=OFF"_L1);
218 m_query->exec("PRAGMA cache_size=3000"_L1);
219
220 m_query->exec("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\' "
221 "AND Name=\'NamespaceTable\'"_L1);
222 m_query->next();
223
224 const bool tablesExist = m_query->value(0).toInt() > 0;
225 if (!tablesExist) {
226 if (!createTables(m_query.get())) {
227 closeDB();
228 emit error(tr("Cannot create tables in file %1.").arg(collectionFile()));
229 return false;
230 }
231 }
232
233 bool indexAndNamespaceFilterTablesMissing = false;
234
235 const QStringList newTables = {
236 "IndexTable"_L1,
237 "FileNameTable"_L1,
238 "ContentsTable"_L1,
239 "FileFilterTable"_L1,
240 "IndexFilterTable"_L1,
241 "ContentsFilterTable"_L1,
242 "FileAttributeSetTable"_L1,
243 "OptimizedFilterTable"_L1,
244 "TimeStampTable"_L1,
245 "VersionTable"_L1,
246 "Filter"_L1,
247 "ComponentTable"_L1,
248 "ComponentMapping"_L1,
249 "ComponentFilter"_L1,
250 "VersionFilter"_L1
251 };
252
253 QString queryString = "SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"_L1;
254 queryString.append(" AND (Name=\'"_L1);
255 queryString.append(newTables.join("\' OR Name=\'"_L1));
256 queryString.append("\')"_L1);
257
258 m_query->exec(queryString);
259 m_query->next();
260 if (m_query->value(0).toInt() != newTables.size()) {
261 if (!recreateIndexAndNamespaceFilterTables(m_query.get())) {
262 emit error(tr("Cannot create index tables in file %1.").arg(collectionFile()));
263 return false;
264 }
265
266 // Old tables exist, index tables didn't, recreate index tables only in this case
267 indexAndNamespaceFilterTablesMissing = tablesExist;
268 }
269
270 if (schemaVersion < CollectionSchemaVersion
271 && !m_query->exec(writeSchemaVersionStatement())) {
272 emit error(tr("Cannot write the version of collection file %1.").arg(collectionFile()));
273 return false;
274 }
275
277 if (indexAndNamespaceFilterTablesMissing) {
278 for (const QHelpCollectionHandler::FileInfo &info : docList) {
279 if (!registerIndexAndNamespaceFilterTables(info.namespaceName, true)) {
280 emit error(tr("Cannot register index tables in file %1.").arg(collectionFile()));
281 return false;
282 }
283 }
284 return true;
285 }
286
287 QList<TimeStamp> timeStamps;
288 m_query->exec("SELECT NamespaceId, FolderId, FilePath, Size, TimeStamp FROM TimeStampTable"_L1);
289 while (m_query->next()) {
290 TimeStamp timeStamp;
291 timeStamp.namespaceId = m_query->value(0).toLongLong();
292 timeStamp.folderId = m_query->value(1).toLongLong();
293 timeStamp.fileName = m_query->value(2).toString();
294 timeStamp.size = m_query->value(3).toLongLong();
295 timeStamp.timeStamp = m_query->value(4).toDateTime();
296 timeStamps.append(timeStamp);
297 }
298
299 QList<TimeStamp> toRemove;
300 for (const TimeStamp &timeStamp : timeStamps) {
301 if (!isTimeStampCorrect(timeStamp))
302 toRemove.append(timeStamp);
303 }
304
305 // TODO: we may optimize when toRemove.size() == timeStamps.size().
306 // In this case we remove all records from tables.
307 Transaction transaction(m_connectionName);
308 for (const TimeStamp &timeStamp : toRemove) {
309 if (!unregisterIndexTable(timeStamp.namespaceId, timeStamp.folderId)) {
310 emit error(tr("Cannot unregister index tables in file %1.").arg(collectionFile()));
311 return false;
312 }
313 }
314 if (!transaction.commit()) {
315 emit error(tr("Cannot unregister index tables in file %1.").arg(collectionFile()));
316 return false;
317 }
318
319 for (const QHelpCollectionHandler::FileInfo &info : docList) {
320 if (!hasTimeStampInfo(info.namespaceName)
321 && !registerIndexAndNamespaceFilterTables(info.namespaceName)) {
322 // we may have a doc registered without a timestamp
323 // and the doc may be missing currently
324 unregisterDocumentation(info.namespaceName);
325 }
326 }
327 return true;
328}
329
330QString QHelpCollectionHandler::absoluteDocPath(const QString &fileName) const
331{
332 const QFileInfo fi(collectionFile());
333 return QDir::isAbsolutePath(fileName)
334 ? fileName
335 : QFileInfo(fi.absolutePath() + u'/' + fileName).absoluteFilePath();
336}
337
338bool QHelpCollectionHandler::isTimeStampCorrect(const TimeStamp &timeStamp) const
339{
340 const QFileInfo fi(absoluteDocPath(timeStamp.fileName));
341
342 if (!fi.exists())
343 return false;
344
345 if (fi.size() != timeStamp.size)
346 return false;
347
348 if (fi.lastModified(QTimeZone::UTC) != timeStamp.timeStamp)
349 return false;
350
351 m_query->prepare("SELECT FilePath FROM NamespaceTable WHERE Id = ?"_L1);
352 m_query->bindValue(0, timeStamp.namespaceId);
353 if (!m_query->exec() || !m_query->next())
354 return false;
355
356 const QString oldFileName = m_query->value(0).toString();
357 m_query->clear();
358 if (oldFileName != timeStamp.fileName)
359 return false;
360
361 return true;
362}
363
364bool QHelpCollectionHandler::hasTimeStampInfo(const QString &nameSpace) const
365{
366 m_query->prepare(
367 "SELECT "
368 "TimeStampTable.NamespaceId "
369 "FROM "
370 "NamespaceTable, "
371 "TimeStampTable "
372 "WHERE NamespaceTable.Id = TimeStampTable.NamespaceId "
373 "AND NamespaceTable.Name = ? LIMIT 1"_L1);
374 m_query->bindValue(0, nameSpace);
375 if (!m_query->exec())
376 return false;
377
378 if (!m_query->next())
379 return false;
380
381 m_query->clear();
382 return true;
383}
384
385void QHelpCollectionHandler::scheduleVacuum()
386{
387 if (m_vacuumScheduled)
388 return;
389
390 m_vacuumScheduled = true;
391 QTimer::singleShot(0, this, &QHelpCollectionHandler::execVacuum);
392}
393
394void QHelpCollectionHandler::execVacuum()
395{
396 if (!m_query)
397 return;
398
399 m_query->exec("VACUUM"_L1);
400 m_vacuumScheduled = false;
401}
402
403bool QHelpCollectionHandler::copyCollectionFile(const QString &fileName)
404{
405 if (!m_query)
406 return false;
407
408 const QFileInfo fi(fileName);
409 if (fi.exists()) {
410 emit error(tr("The collection file \"%1\" already exists.").arg(fileName));
411 return false;
412 }
413
414 if (!fi.absoluteDir().exists() && !QDir().mkpath(fi.absolutePath())) {
415 emit error(tr("Cannot create directory: %1").arg(fi.absolutePath()));
416 return false;
417 }
418
419 const QString &colFile = fi.absoluteFilePath();
420 const QString &connectionName =
421 QHelpGlobal::uniquifyConnectionName("QHelpCollectionHandlerCopy"_L1, this);
422 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, connectionName);
423 db.setDatabaseName(colFile);
424 if (!db.open()) {
425 emit error(tr("Cannot open collection file: %1").arg(colFile));
426 return false;
427 }
428
429 const auto copyTables = [&] {
430 QSqlQuery copyQuery(db);
431 copyQuery.exec("PRAGMA synchronous=OFF"_L1);
432 copyQuery.exec("PRAGMA cache_size=3000"_L1);
433
434 Transaction transaction(connectionName);
435
436 if (!createTables(&copyQuery) || !recreateIndexAndNamespaceFilterTables(&copyQuery)
437 || !copyQuery.exec(writeSchemaVersionStatement())) {
438 return false;
439 }
440
441 const QString &oldBaseDir = QFileInfo(collectionFile()).absolutePath();
442 const QFileInfo newColFi(colFile);
443 m_query->exec("SELECT Name, FilePath FROM NamespaceTable"_L1);
444 while (m_query->next()) {
445 copyQuery.prepare("INSERT INTO NamespaceTable VALUES(NULL, ?, ?)"_L1);
446 copyQuery.bindValue(0, m_query->value(0).toString());
447 QString oldFilePath = m_query->value(1).toString();
448 if (!QDir::isAbsolutePath(oldFilePath))
449 oldFilePath = oldBaseDir + u'/' + oldFilePath;
450 copyQuery.bindValue(1, newColFi.absoluteDir().relativeFilePath(oldFilePath));
451 if (!copyQuery.exec())
452 return false;
453 }
454
455 m_query->exec("SELECT NamespaceId, Name FROM FolderTable"_L1);
456 while (m_query->next()) {
457 copyQuery.prepare("INSERT INTO FolderTable VALUES(NULL, ?, ?)"_L1);
458 copyQuery.bindValue(0, m_query->value(0).toString());
459 copyQuery.bindValue(1, m_query->value(1).toString());
460 if (!copyQuery.exec())
461 return false;
462 }
463
464 m_query->exec("SELECT Name FROM FilterAttributeTable"_L1);
465 while (m_query->next()) {
466 copyQuery.prepare("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"_L1);
467 copyQuery.bindValue(0, m_query->value(0).toString());
468 if (!copyQuery.exec())
469 return false;
470 }
471
472 m_query->exec("SELECT Name FROM FilterNameTable"_L1);
473 while (m_query->next()) {
474 copyQuery.prepare("INSERT INTO FilterNameTable VALUES(NULL, ?)"_L1);
475 copyQuery.bindValue(0, m_query->value(0).toString());
476 if (!copyQuery.exec())
477 return false;
478 }
479
480 m_query->exec("SELECT NameId, FilterAttributeId FROM FilterTable"_L1);
481 while (m_query->next()) {
482 copyQuery.prepare("INSERT INTO FilterTable VALUES(?, ?)"_L1);
483 copyQuery.bindValue(0, m_query->value(0).toLongLong());
484 copyQuery.bindValue(1, m_query->value(1).toLongLong());
485 if (!copyQuery.exec())
486 return false;
487 }
488
489 m_query->exec("SELECT Key, Value FROM SettingsTable"_L1);
490 while (m_query->next()) {
491 if (m_query->value(0).toString() == "FTS5IndexedNamespaces"_L1)
492 continue;
493 copyQuery.prepare("INSERT INTO SettingsTable VALUES(?, ?)"_L1);
494 copyQuery.bindValue(0, m_query->value(0).toString());
495 copyQuery.bindValue(1, m_query->value(1));
496 if (!copyQuery.exec())
497 return false;
498 }
499
500 return transaction.commit();
501 };
502
503 const bool copied = copyTables();
504 db.close();
505 db = QSqlDatabase();
506 QSqlDatabase::removeDatabase(connectionName);
507 if (!copied) {
508 QFile::remove(colFile);
509 emit error(tr("Cannot copy collection file: %1").arg(colFile));
510 return false;
511 }
512 return true;
513}
514
515bool QHelpCollectionHandler::createTables(QSqlQuery *query)
516{
517 const QStringList tables = {
518 "CREATE TABLE NamespaceTable ("
519 "Id INTEGER PRIMARY KEY, "
520 "Name TEXT, "
521 "FilePath TEXT )"_L1,
522 "CREATE TABLE FolderTable ("
523 "Id INTEGER PRIMARY KEY, "
524 "NamespaceId INTEGER, "
525 "Name TEXT )"_L1,
526 "CREATE TABLE FilterAttributeTable ("
527 "Id INTEGER PRIMARY KEY, "
528 "Name TEXT )"_L1,
529 "CREATE TABLE FilterNameTable ("
530 "Id INTEGER PRIMARY KEY, "
531 "Name TEXT )"_L1,
532 "CREATE TABLE FilterTable ("
533 "NameId INTEGER, "
534 "FilterAttributeId INTEGER )"_L1,
535 "CREATE TABLE SettingsTable ("
536 "Key TEXT PRIMARY KEY, "
537 "Value BLOB )"_L1
538 };
539
540 for (const QString &q : tables) {
541 if (!query->exec(q))
542 return false;
543 }
544 return true;
545}
546
547bool QHelpCollectionHandler::recreateIndexAndNamespaceFilterTables(QSqlQuery *query)
548{
549 const QStringList tables = {
550 "DROP TABLE IF EXISTS FileNameTable"_L1,
551 "DROP TABLE IF EXISTS IndexTable"_L1,
552 "DROP TABLE IF EXISTS ContentsTable"_L1,
553 "DROP TABLE IF EXISTS FileFilterTable"_L1, // legacy
554 "DROP TABLE IF EXISTS IndexFilterTable"_L1, // legacy
555 "DROP TABLE IF EXISTS ContentsFilterTable"_L1, // legacy
556 "DROP TABLE IF EXISTS FileAttributeSetTable"_L1, // legacy
557 "DROP TABLE IF EXISTS OptimizedFilterTable"_L1, // legacy
558 "DROP TABLE IF EXISTS TimeStampTable"_L1,
559 "DROP TABLE IF EXISTS VersionTable"_L1,
560 "DROP TABLE IF EXISTS Filter"_L1,
561 "DROP TABLE IF EXISTS ComponentTable"_L1,
562 "DROP TABLE IF EXISTS ComponentMapping"_L1,
563 "DROP TABLE IF EXISTS ComponentFilter"_L1,
564 "DROP TABLE IF EXISTS VersionFilter"_L1,
565 "CREATE TABLE FileNameTable ("
566 "FolderId INTEGER, "
567 "Name TEXT, "
568 "FileId INTEGER PRIMARY KEY, "
569 "Title TEXT)"_L1,
570 "CREATE TABLE IndexTable ("
571 "Id INTEGER PRIMARY KEY, "
572 "Name TEXT, "
573 "Identifier TEXT, "
574 "NamespaceId INTEGER, "
575 "FileId INTEGER, "
576 "Anchor TEXT)"_L1,
577 "CREATE TABLE ContentsTable ("
578 "Id INTEGER PRIMARY KEY, "
579 "NamespaceId INTEGER, "
580 "Data BLOB)"_L1,
581 "CREATE TABLE FileFilterTable ("
582 "FilterAttributeId INTEGER, "
583 "FileId INTEGER)"_L1,
584 "CREATE TABLE IndexFilterTable ("
585 "FilterAttributeId INTEGER, "
586 "IndexId INTEGER)"_L1,
587 "CREATE TABLE ContentsFilterTable ("
588 "FilterAttributeId INTEGER, "
589 "ContentsId INTEGER )"_L1,
590 "CREATE TABLE FileAttributeSetTable ("
591 "NamespaceId INTEGER, "
592 "FilterAttributeSetId INTEGER, "
593 "FilterAttributeId INTEGER)"_L1,
594 "CREATE TABLE OptimizedFilterTable ("
595 "NamespaceId INTEGER, "
596 "FilterAttributeId INTEGER)"_L1,
597 "CREATE TABLE TimeStampTable ("
598 "NamespaceId INTEGER, "
599 "FolderId INTEGER, "
600 "FilePath TEXT, "
601 "Size INTEGER, "
602 "TimeStamp TEXT)"_L1,
603 "CREATE TABLE VersionTable ("
604 "NamespaceId INTEGER, "
605 "Version TEXT)"_L1,
606 "CREATE TABLE Filter ("
607 "FilterId INTEGER PRIMARY KEY, "
608 "Name TEXT)"_L1,
609 "CREATE TABLE ComponentTable ("
610 "ComponentId INTEGER PRIMARY KEY, "
611 "Name TEXT)"_L1,
612 "CREATE TABLE ComponentMapping ("
613 "ComponentId INTEGER, "
614 "NamespaceId INTEGER)"_L1,
615 "CREATE TABLE ComponentFilter ("
616 "ComponentName TEXT, "
617 "FilterId INTEGER)"_L1,
618 "CREATE TABLE VersionFilter ("
619 "Version TEXT, "
620 "FilterId INTEGER)"_L1
621 };
622
623 for (const QString &q : tables) {
624 if (!query->exec(q))
625 return false;
626 }
627 return true;
628}
629
631{
632 QStringList list;
633 if (m_query) {
634 m_query->exec("SELECT Name FROM FilterNameTable"_L1);
635 while (m_query->next())
636 list.append(m_query->value(0).toString());
637 }
638 return list;
639}
640
642{
643 QStringList list;
644 if (m_query) {
645 m_query->exec("SELECT Name FROM Filter ORDER BY Name"_L1);
646 while (m_query->next())
647 list.append(m_query->value(0).toString());
648 }
649 return list;
650}
651
653{
654 QStringList list;
655 if (m_query) {
656 m_query->exec("SELECT DISTINCT Name FROM ComponentTable ORDER BY Name"_L1);
657 while (m_query->next())
658 list.append(m_query->value(0).toString());
659 }
660 return list;
661}
662
664{
665 QList<QVersionNumber> list;
666 if (m_query) {
667 m_query->exec("SELECT DISTINCT Version FROM VersionTable ORDER BY Version"_L1);
668 while (m_query->next())
669 list.append(QVersionNumber::fromString(m_query->value(0).toString()));
670 }
671 return list;
672}
673
675{
676 QMap<QString, QString> result;
677 if (m_query) {
678 m_query->exec(
679 "SELECT "
680 "NamespaceTable.Name, "
681 "ComponentTable.Name "
682 "FROM NamespaceTable, "
683 "ComponentTable, "
684 "ComponentMapping "
685 "WHERE NamespaceTable.Id = ComponentMapping.NamespaceId "
686 "AND ComponentMapping.ComponentId = ComponentTable.ComponentId"_L1);
687 while (m_query->next())
688 result.insert(m_query->value(0).toString(), m_query->value(1).toString());
689 }
690 return result;
691}
692
694{
695 QMap<QString, QVersionNumber> result;
696 if (m_query) {
697 m_query->exec(
698 "SELECT "
699 "NamespaceTable.Name, "
700 "VersionTable.Version "
701 "FROM NamespaceTable, "
702 "VersionTable "
703 "WHERE NamespaceTable.Id = VersionTable.NamespaceId"_L1);
704 while (m_query->next()) {
705 result.insert(m_query->value(0).toString(),
706 QVersionNumber::fromString(m_query->value(1).toString()));
707 }
708 }
709 return result;
710}
711
712QHelpFilterData QHelpCollectionHandler::filterData(const QString &filterName) const
713{
714 QStringList components;
715 QList<QVersionNumber> versions;
716 if (m_query) {
717 m_query->prepare(
718 "SELECT ComponentFilter.ComponentName "
719 "FROM ComponentFilter, Filter "
720 "WHERE ComponentFilter.FilterId = Filter.FilterId "
721 "AND Filter.Name = ? "
722 "ORDER BY ComponentFilter.ComponentName"_L1);
723 m_query->bindValue(0, filterName);
724 m_query->exec();
725 while (m_query->next())
726 components.append(m_query->value(0).toString());
727
728 m_query->prepare(
729 "SELECT VersionFilter.Version "
730 "FROM VersionFilter, Filter "
731 "WHERE VersionFilter.FilterId = Filter.FilterId "
732 "AND Filter.Name = ? "
733 "ORDER BY VersionFilter.Version"_L1);
734 m_query->bindValue(0, filterName);
735 m_query->exec();
736 while (m_query->next())
737 versions.append(QVersionNumber::fromString(m_query->value(0).toString()));
738
739 }
740 QHelpFilterData data;
741 data.setComponents(components);
742 data.setVersions(versions);
743 return data;
744}
745
746bool QHelpCollectionHandler::setFilterData(const QString &filterName,
747 const QHelpFilterData &filterData)
748{
749 if (!isDBWritable() || !removeFilter(filterName))
750 return false;
751
752 m_query->prepare("INSERT INTO Filter VALUES (NULL, ?)"_L1);
753 m_query->bindValue(0, filterName);
754 if (!m_query->exec())
755 return false;
756
757 const qint64 filterId = m_query->lastInsertId().toLongLong();
758
759 QVariantList componentList;
760 QVariantList versionList;
761 QVariantList filterIdList;
762
763 const auto &components = filterData.components();
764 for (const QString &component : components) {
765 componentList.append(component);
766 filterIdList.append(filterId);
767 }
768
769 m_query->prepare("INSERT INTO ComponentFilter VALUES (?, ?)"_L1);
770 m_query->addBindValue(componentList);
771 m_query->addBindValue(filterIdList);
772 if (!m_query->execBatch())
773 return false;
774
775 filterIdList.clear();
776 const auto &versions = filterData.versions();
777 for (const QVersionNumber &version : versions) {
778 versionList.append(version.isNull() ? QString() : version.toString());
779 filterIdList.append(filterId);
780 }
781
782 m_query->prepare("INSERT INTO VersionFilter VALUES (?, ?)"_L1);
783 m_query->addBindValue(versionList);
784 m_query->addBindValue(filterIdList);
785 if (!m_query->execBatch())
786 return false;
787
788 return true;
789}
790
791bool QHelpCollectionHandler::removeFilter(const QString &filterName)
792{
793 if (!isDBWritable())
794 return false;
795
796 m_query->prepare("SELECT FilterId FROM Filter WHERE Name = ?"_L1);
797 m_query->bindValue(0, filterName);
798 if (!m_query->exec())
799 return false;
800
801 if (!m_query->next())
802 return true; // no filter in DB
803
804 const qint64 filterId = m_query->value(0).toLongLong();
805
806 m_query->prepare("DELETE FROM Filter WHERE Filter.Name = ?"_L1);
807 m_query->bindValue(0, filterName);
808 if (!m_query->exec())
809 return false;
810
811 m_query->prepare("DELETE FROM ComponentFilter WHERE ComponentFilter.FilterId = ?"_L1);
812 m_query->bindValue(0, filterId);
813 if (!m_query->exec())
814 return false;
815
816 m_query->prepare("DELETE FROM VersionFilter WHERE VersionFilter.FilterId = ?"_L1);
817 m_query->bindValue(0, filterId);
818 if (!m_query->exec())
819 return false;
820
821 return true;
822}
823
824bool QHelpCollectionHandler::removeCustomFilter(const QString &filterName)
825{
826 if (!isDBWritable() || filterName.isEmpty())
827 return false;
828
829 qint64 filterNameId = -1;
830 m_query->prepare("SELECT Id FROM FilterNameTable WHERE Name=?"_L1);
831 m_query->bindValue(0, filterName);
832 m_query->exec();
833 if (m_query->next())
834 filterNameId = m_query->value(0).toLongLong();
835
836 if (filterNameId < 0) {
837 emit error(tr("Unknown filter \"%1\".").arg(filterName));
838 return false;
839 }
840
841 Transaction transaction(m_connectionName);
842
843 m_query->prepare("DELETE FROM FilterTable WHERE NameId=?"_L1);
844 m_query->bindValue(0, filterNameId);
845 if (!m_query->exec())
846 return false;
847
848 m_query->prepare("DELETE FROM FilterNameTable WHERE Id=?"_L1);
849 m_query->bindValue(0, filterNameId);
850 if (!m_query->exec())
851 return false;
852
853 return transaction.commit();
854}
855
856bool QHelpCollectionHandler::addCustomFilter(const QString &filterName,
857 const QStringList &attributes)
858{
859 if (!isDBWritable() || filterName.isEmpty())
860 return false;
861
862 qint64 nameId = -1;
863 m_query->prepare("SELECT Id FROM FilterNameTable WHERE Name=?"_L1);
864 m_query->bindValue(0, filterName);
865 m_query->exec();
866 if (m_query->next())
867 nameId = m_query->value(0).toLongLong();
868
869 m_query->exec("SELECT Id, Name FROM FilterAttributeTable"_L1);
870 QStringList idsToInsert = attributes;
871 QMap<QString, qint64> attributeMap;
872 while (m_query->next()) {
873 // all old attributes
874 const QString attributeName = m_query->value(1).toString();
875 attributeMap.insert(attributeName, m_query->value(0).toLongLong());
876 idsToInsert.removeAll(attributeName);
877 }
878
879 Transaction transaction(m_connectionName);
880
881 for (const QString &id : std::as_const(idsToInsert)) {
882 m_query->prepare("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"_L1);
883 m_query->bindValue(0, id);
884 if (!m_query->exec())
885 return false;
886 attributeMap.insert(id, m_query->lastInsertId().toLongLong());
887 }
888
889 if (nameId < 0) {
890 m_query->prepare("INSERT INTO FilterNameTable VALUES(NULL, ?)"_L1);
891 m_query->bindValue(0, filterName);
892 if (m_query->exec())
893 nameId = m_query->lastInsertId().toLongLong();
894 }
895
896 if (nameId < 0) {
897 emit error(tr("Cannot register filter %1.").arg(filterName));
898 return false;
899 }
900
901 m_query->prepare("DELETE FROM FilterTable WHERE NameId=?"_L1);
902 m_query->bindValue(0, nameId);
903 if (!m_query->exec())
904 return false;
905
906 for (const QString &att : attributes) {
907 m_query->prepare("INSERT INTO FilterTable VALUES(?, ?)"_L1);
908 m_query->bindValue(0, nameId);
909 m_query->bindValue(1, attributeMap[att]);
910 if (!m_query->exec())
911 return false;
912 }
913 return transaction.commit();
914}
915
916QHelpCollectionHandler::FileInfo QHelpCollectionHandler::registeredDocumentation(
917 const QString &namespaceName) const
918{
919 FileInfo fileInfo;
920
921 if (!m_query)
922 return fileInfo;
923
924 m_query->prepare(
925 "SELECT "
926 "NamespaceTable.Name, "
927 "NamespaceTable.FilePath, "
928 "FolderTable.Name "
929 "FROM "
930 "NamespaceTable, "
931 "FolderTable "
932 "WHERE NamespaceTable.Id = FolderTable.NamespaceId "
933 "AND NamespaceTable.Name = ? LIMIT 1"_L1);
934 m_query->bindValue(0, namespaceName);
935 if (!m_query->exec() || !m_query->next())
936 return fileInfo;
937
938 fileInfo.namespaceName = m_query->value(0).toString();
939 fileInfo.fileName = m_query->value(1).toString();
940 fileInfo.folderName = m_query->value(2).toString();
941
942 m_query->clear();
943
944 return fileInfo;
945}
946
948{
949 FileInfoList list;
950 if (!m_query)
951 return list;
952
953 m_query->exec(
954 "SELECT "
955 "NamespaceTable.Name, "
956 "NamespaceTable.FilePath, "
957 "FolderTable.Name "
958 "FROM "
959 "NamespaceTable, "
960 "FolderTable "
961 "WHERE NamespaceTable.Id = FolderTable.NamespaceId"_L1);
962
963 while (m_query->next()) {
964 FileInfo fileInfo;
965 fileInfo.namespaceName = m_query->value(0).toString();
966 fileInfo.fileName = m_query->value(1).toString();
967 fileInfo.folderName = m_query->value(2).toString();
968 list.append(fileInfo);
969 }
970
971 return list;
972}
973
974bool QHelpCollectionHandler::registerDocumentation(const QString &fileName)
975{
976 if (!isDBWritable())
977 return false;
978
979 QHelpDBReader reader(fileName, QHelpGlobal::uniquifyConnectionName(
980 "QHelpCollectionHandler"_L1, this), nullptr);
981 if (!reader.init()) {
982 emit error(tr("Cannot open documentation file %1.").arg(fileName));
983 return false;
984 }
985
986 const QString &ns = reader.namespaceName();
987 if (ns.isEmpty()) {
988 emit error(tr("Invalid documentation file \"%1\".").arg(fileName));
989 return false;
990 }
991
992 // A file holds exactly one namespace. A registration of the same file under
993 // a different namespace is stale (e.g. the file was regenerated for another
994 // version), so drop it. Files at other paths are never affected.
995 const QString absFileName = QFileInfo(fileName).absoluteFilePath();
996 const auto &registered = registeredDocumentations();
997 for (const FileInfo &info : registered) {
998 if (info.namespaceName != ns && absoluteDocPath(info.fileName) == absFileName)
999 unregisterDocumentation(info.namespaceName);
1000 }
1001
1002 Transaction transaction(m_connectionName);
1003
1004 const qint64 nsId = registerNamespace(ns, fileName);
1005 if (nsId < 1)
1006 return false;
1007
1008 const qint64 vfId = registerVirtualFolder(reader.virtualFolder(), nsId);
1009 if (vfId < 1)
1010 return false;
1011
1012 registerVersion(reader.version(), nsId);
1013 registerFilterAttributes(reader.filterAttributeSets(), nsId); // qset, what happens when removing documentation?
1014 const auto &customFilters = reader.customFilters();
1015 for (const QString &filterName : customFilters)
1016 addCustomFilter(filterName, reader.filterAttributes(filterName));
1017
1018 if (!registerIndexTable(reader.indexTable(), nsId, vfId, registeredDocumentation(ns).fileName))
1019 return false;
1020
1021 return transaction.commit();
1022}
1023
1024bool QHelpCollectionHandler::unregisterDocumentation(const QString &namespaceName)
1025{
1026 if (!isDBWritable())
1027 return false;
1028
1029 m_query->prepare("SELECT Id FROM NamespaceTable WHERE Name = ?"_L1);
1030 m_query->bindValue(0, namespaceName);
1031 m_query->exec();
1032
1033 if (!m_query->next()) {
1034 emit error(tr("The namespace %1 was not registered.").arg(namespaceName));
1035 return false;
1036 }
1037
1038 const qint64 nsId = m_query->value(0).toLongLong();
1039
1040 m_query->prepare("SELECT Id FROM FolderTable WHERE NamespaceId = ?"_L1);
1041 m_query->bindValue(0, nsId);
1042 m_query->exec();
1043
1044 if (!m_query->next()) {
1045 emit error(tr("The namespace %1 was not registered.").arg(namespaceName));
1046 return false;
1047 }
1048
1049 const qint64 vfId = m_query->value(0).toLongLong();
1050
1051 Transaction transaction(m_connectionName);
1052
1053 m_query->prepare("DELETE FROM NamespaceTable WHERE Id = ?"_L1);
1054 m_query->bindValue(0, nsId);
1055 if (!m_query->exec())
1056 return false;
1057
1058 m_query->prepare("DELETE FROM FolderTable WHERE NamespaceId = ?"_L1);
1059 m_query->bindValue(0, nsId);
1060 if (!m_query->exec())
1061 return false;
1062
1063 if (!unregisterIndexTable(nsId, vfId) || !transaction.commit())
1064 return false;
1065
1066 scheduleVacuum();
1067
1068 return true;
1069}
1070
1072{
1073 QHelpCollectionHandler::FileInfo fileInfo;
1074
1075 if (!url.isValid() || url.toString().count(u'/') < 4
1076 || url.scheme() != "qthelp"_L1) {
1077 return fileInfo;
1078 }
1079
1080 fileInfo.namespaceName = url.authority();
1081 fileInfo.fileName = url.path();
1082 if (fileInfo.fileName.startsWith(u'/'))
1083 fileInfo.fileName = fileInfo.fileName.mid(1);
1084 fileInfo.folderName = fileInfo.fileName.mid(0, fileInfo.fileName.indexOf(u'/', 1));
1085 fileInfo.fileName.remove(0, fileInfo.folderName.size() + 1);
1086
1087 return fileInfo;
1088}
1089
1090bool QHelpCollectionHandler::fileExists(const QUrl &url) const
1091{
1092 if (!isDBOpened())
1093 return false;
1094
1095 const FileInfo fileInfo = extractFileInfo(url);
1096 if (fileInfo.namespaceName.isEmpty())
1097 return false;
1098
1099 m_query->prepare(
1100 "SELECT COUNT (DISTINCT NamespaceTable.Id) "
1101 "FROM "
1102 "FileNameTable, "
1103 "NamespaceTable, "
1104 "FolderTable "
1105 "WHERE FolderTable.Name = ? "
1106 "AND FileNameTable.Name = ? "
1107 "AND FileNameTable.FolderId = FolderTable.Id "
1108 "AND FolderTable.NamespaceId = NamespaceTable.Id"_L1);
1109 m_query->bindValue(0, fileInfo.folderName);
1110 m_query->bindValue(1, fileInfo.fileName);
1111 if (!m_query->exec() || !m_query->next())
1112 return false;
1113
1114 const int count = m_query->value(0).toInt();
1115 m_query->clear();
1116
1117 return count;
1118}
1119
1120static QString prepareFilterQuery(const QString &filterName)
1121{
1122 if (filterName.isEmpty())
1123 return {};
1124
1125 return " AND EXISTS(SELECT * FROM Filter WHERE Filter.Name = ?) "
1126 "AND ("
1127 "(NOT EXISTS(" // 1. filter by component
1128 "SELECT * FROM "
1129 "ComponentFilter, "
1130 "Filter "
1131 "WHERE ComponentFilter.FilterId = Filter.FilterId "
1132 "AND Filter.Name = ?) "
1133 "OR NamespaceTable.Id IN ("
1134 "SELECT "
1135 "NamespaceTable.Id "
1136 "FROM "
1137 "NamespaceTable, "
1138 "ComponentTable, "
1139 "ComponentMapping, "
1140 "ComponentFilter, "
1141 "Filter "
1142 "WHERE ComponentMapping.NamespaceId = NamespaceTable.Id "
1143 "AND ComponentTable.ComponentId = ComponentMapping.ComponentId "
1144 "AND ((ComponentTable.Name = ComponentFilter.ComponentName) "
1145 "OR (ComponentTable.Name IS NULL AND ComponentFilter.ComponentName IS NULL)) "
1146 "AND ComponentFilter.FilterId = Filter.FilterId "
1147 "AND Filter.Name = ?))"
1148 " AND "
1149 "(NOT EXISTS(" // 2. filter by version
1150 "SELECT * FROM "
1151 "VersionFilter, "
1152 "Filter "
1153 "WHERE VersionFilter.FilterId = Filter.FilterId "
1154 "AND Filter.Name = ?) "
1155 "OR NamespaceTable.Id IN ("
1156 "SELECT "
1157 "NamespaceTable.Id "
1158 "FROM "
1159 "NamespaceTable, "
1160 "VersionFilter, "
1161 "VersionTable, "
1162 "Filter "
1163 "WHERE VersionFilter.FilterId = Filter.FilterId "
1164 "AND ((VersionFilter.Version = VersionTable.Version) "
1165 "OR (VersionFilter.Version IS NULL AND VersionTable.Version IS NULL)) "
1166 "AND VersionTable.NamespaceId = NamespaceTable.Id "
1167 "AND Filter.Name = ?))"
1168 ")"_L1;
1169}
1170
1171static void bindFilterQuery(QSqlQuery *query, int bindStart, const QString &filterName)
1172{
1173 if (filterName.isEmpty())
1174 return;
1175
1176 query->bindValue(bindStart, filterName);
1177 query->bindValue(bindStart + 1, filterName);
1178 query->bindValue(bindStart + 2, filterName);
1179 query->bindValue(bindStart + 3, filterName);
1180 query->bindValue(bindStart + 4, filterName);
1181}
1182
1183static QString prepareFilterQuery(int attributesCount,
1184 const QString &idTableName,
1185 const QString &idColumnName,
1186 const QString &filterTableName,
1187 const QString &filterColumnName)
1188{
1189 if (!attributesCount)
1190 return {};
1191
1192 QString filterQuery = " AND (%1.%2 IN ("_L1.arg(idTableName, idColumnName);
1193
1194 const QString filterQueryTemplate =
1195 "SELECT %1.%2 "
1196 "FROM %1, FilterAttributeTable "
1197 "WHERE %1.FilterAttributeId = FilterAttributeTable.Id "
1198 "AND FilterAttributeTable.Name = ?"_L1.arg(filterTableName, filterColumnName);
1199
1200 for (int i = 0; i < attributesCount; ++i) {
1201 if (i > 0)
1202 filterQuery.append(" INTERSECT "_L1);
1203 filterQuery.append(filterQueryTemplate);
1204 }
1205
1206 filterQuery.append(") OR NamespaceTable.Id IN ("_L1);
1207
1208 const QString optimizedFilterQueryTemplate =
1209 "SELECT OptimizedFilterTable.NamespaceId "
1210 "FROM OptimizedFilterTable, FilterAttributeTable "
1211 "WHERE OptimizedFilterTable.FilterAttributeId = FilterAttributeTable.Id "
1212 "AND FilterAttributeTable.Name = ?"_L1;
1213
1214 for (int i = 0; i < attributesCount; ++i) {
1215 if (i > 0)
1216 filterQuery.append(" INTERSECT "_L1);
1217 filterQuery.append(optimizedFilterQueryTemplate);
1218 }
1219
1220 filterQuery.append("))"_L1);
1221
1222 return filterQuery;
1223}
1224
1225static void bindFilterQuery(QSqlQuery *query, int startingBindPos, const QStringList &filterAttributes)
1226{
1227 for (int i = 0; i < 2; ++i) {
1228 for (int j = 0; j < filterAttributes.size(); j++) {
1229 query->bindValue(i * filterAttributes.size() + j + startingBindPos,
1230 filterAttributes.at(j));
1231 }
1232 }
1233}
1234
1236 const QStringList &filterAttributes) const
1237{
1238 if (!isDBOpened())
1239 return {};
1240
1241 const FileInfo fileInfo = extractFileInfo(url);
1242 if (fileInfo.namespaceName.isEmpty())
1243 return {};
1244
1245 const QString filterlessQuery =
1246 "SELECT DISTINCT "
1247 "NamespaceTable.Name "
1248 "FROM "
1249 "FileNameTable, "
1250 "NamespaceTable, "
1251 "FolderTable "
1252 "WHERE FolderTable.Name = ? "
1253 "AND FileNameTable.Name = ? "
1254 "AND FileNameTable.FolderId = FolderTable.Id "
1255 "AND FolderTable.NamespaceId = NamespaceTable.Id"_L1;
1256
1257 const QString filterQuery = filterlessQuery
1258 + prepareFilterQuery(filterAttributes.size(), "FileNameTable"_L1, "FileId"_L1,
1259 "FileFilterTable"_L1, "FileId"_L1);
1260
1261 m_query->prepare(filterQuery);
1262 m_query->bindValue(0, fileInfo.folderName);
1263 m_query->bindValue(1, fileInfo.fileName);
1264 bindFilterQuery(m_query.get(), 2, filterAttributes);
1265
1266 if (!m_query->exec())
1267 return {};
1268
1269 QStringList namespaceList;
1270 while (m_query->next())
1271 namespaceList.append(m_query->value(0).toString());
1272
1273 if (namespaceList.isEmpty())
1274 return {};
1275
1276 if (namespaceList.contains(fileInfo.namespaceName))
1277 return fileInfo.namespaceName;
1278
1279 const QString originalVersion = namespaceVersion(fileInfo.namespaceName);
1280
1281 // Otherwise, fall back to the highest available version, with ties broken by name,
1282 // so that the result doesn't depend on the order of rows in the collection file.
1283 QString fallbackNamespace;
1284 QVersionNumber fallbackVersion;
1285 for (const QString &ns : std::as_const(namespaceList)) {
1286 const QString nsVersion = namespaceVersion(ns);
1287 if (originalVersion == nsVersion)
1288 return ns;
1289
1290 const QVersionNumber version = QVersionNumber::fromString(nsVersion);
1291 const int compare = QVersionNumber::compare(version, fallbackVersion);
1292 if (fallbackNamespace.isEmpty() || compare > 0
1293 || (compare == 0 && ns < fallbackNamespace)) {
1294 fallbackNamespace = ns;
1295 fallbackVersion = version;
1296 }
1297 }
1298 return fallbackNamespace;
1299}
1300
1301QString QHelpCollectionHandler::namespaceForFile(const QUrl &url,
1302 const QString &filterName) const
1303{
1304 if (!isDBOpened())
1305 return {};
1306
1307 const FileInfo fileInfo = extractFileInfo(url);
1308 if (fileInfo.namespaceName.isEmpty())
1309 return {};
1310
1311 const QString filterlessQuery =
1312 "SELECT DISTINCT "
1313 "NamespaceTable.Name "
1314 "FROM "
1315 "FileNameTable, "
1316 "NamespaceTable, "
1317 "FolderTable "
1318 "WHERE FolderTable.Name = ? "
1319 "AND FileNameTable.Name = ? "
1320 "AND FileNameTable.FolderId = FolderTable.Id "
1321 "AND FolderTable.NamespaceId = NamespaceTable.Id"_L1;
1322
1323 const QString filterQuery = filterlessQuery
1324 + prepareFilterQuery(filterName);
1325
1326 m_query->prepare(filterQuery);
1327 m_query->bindValue(0, fileInfo.folderName);
1328 m_query->bindValue(1, fileInfo.fileName);
1329 bindFilterQuery(m_query.get(), 2, filterName);
1330
1331 if (!m_query->exec())
1332 return {};
1333
1334 QStringList namespaceList;
1335 while (m_query->next())
1336 namespaceList.append(m_query->value(0).toString());
1337
1338 if (namespaceList.isEmpty())
1339 return {};
1340
1341 if (namespaceList.contains(fileInfo.namespaceName))
1342 return fileInfo.namespaceName;
1343
1344 const QString originalVersion = namespaceVersion(fileInfo.namespaceName);
1345
1346 // Otherwise, fall back to the highest available version, with ties broken by name,
1347 // so that the result doesn't depend on the order of rows in the collection file.
1348 QString fallbackNamespace;
1349 QVersionNumber fallbackVersion;
1350 for (const QString &ns : std::as_const(namespaceList)) {
1351 const QString nsVersion = namespaceVersion(ns);
1352 if (originalVersion == nsVersion)
1353 return ns;
1354
1355 const QVersionNumber version = QVersionNumber::fromString(nsVersion);
1356 const int compare = QVersionNumber::compare(version, fallbackVersion);
1357 if (fallbackNamespace.isEmpty() || compare > 0
1358 || (compare == 0 && ns < fallbackNamespace)) {
1359 fallbackNamespace = ns;
1360 fallbackVersion = version;
1361 }
1362 }
1363 return fallbackNamespace;
1364}
1365
1366QStringList QHelpCollectionHandler::files(const QString &namespaceName,
1367 const QStringList &filterAttributes,
1368 const QString &extensionFilter) const
1369{
1370 if (!isDBOpened())
1371 return {};
1372
1373 const QString extensionQuery = extensionFilter.isEmpty()
1374 ? QString() : " AND FileNameTable.Name LIKE ?"_L1;
1375 const QString filterlessQuery =
1376 "SELECT "
1377 "FolderTable.Name, "
1378 "FileNameTable.Name "
1379 "FROM "
1380 "FileNameTable, "
1381 "FolderTable, "
1382 "NamespaceTable "
1383 "WHERE FileNameTable.FolderId = FolderTable.Id "
1384 "AND FolderTable.NamespaceId = NamespaceTable.Id "
1385 "AND NamespaceTable.Name = ?"_L1 + extensionQuery;
1386
1387 const QString filterQuery = filterlessQuery
1388 + prepareFilterQuery(filterAttributes.size(), "FileNameTable"_L1, "FileId"_L1,
1389 "FileFilterTable"_L1, "FileId"_L1);
1390
1391 m_query->prepare(filterQuery);
1392 m_query->bindValue(0, namespaceName);
1393 int bindCount = 1;
1394 if (!extensionFilter.isEmpty()) {
1395 m_query->bindValue(bindCount, "%.%1"_L1.arg(extensionFilter));
1396 ++bindCount;
1397 }
1398 bindFilterQuery(m_query.get(), bindCount, filterAttributes);
1399
1400 if (!m_query->exec())
1401 return {};
1402
1403 QStringList fileNames;
1404 while (m_query->next())
1405 fileNames.append(m_query->value(0).toString() + u'/' + m_query->value(1).toString());
1406 return fileNames;
1407}
1408
1409QStringList QHelpCollectionHandler::files(const QString &namespaceName,
1410 const QString &filterName,
1411 const QString &extensionFilter) const
1412{
1413 if (!isDBOpened())
1414 return {};
1415
1416 const QString extensionQuery = extensionFilter.isEmpty()
1417 ? QString() : " AND FileNameTable.Name LIKE ?"_L1;
1418 const QString filterlessQuery =
1419 "SELECT "
1420 "FolderTable.Name, "
1421 "FileNameTable.Name "
1422 "FROM "
1423 "FileNameTable, "
1424 "FolderTable, "
1425 "NamespaceTable "
1426 "WHERE FileNameTable.FolderId = FolderTable.Id "
1427 "AND FolderTable.NamespaceId = NamespaceTable.Id "
1428 "AND NamespaceTable.Name = ?"_L1 + extensionQuery;
1429
1430 const QString filterQuery = filterlessQuery
1431 + prepareFilterQuery(filterName);
1432
1433 m_query->prepare(filterQuery);
1434 m_query->bindValue(0, namespaceName);
1435 int bindCount = 1;
1436 if (!extensionFilter.isEmpty()) {
1437 m_query->bindValue(bindCount, "%.%1"_L1.arg(extensionFilter));
1438 ++bindCount;
1439 }
1440
1441 bindFilterQuery(m_query.get(), bindCount, filterName);
1442
1443 if (!m_query->exec())
1444 return{};
1445
1446 QStringList fileNames;
1447 while (m_query->next())
1448 fileNames.append(m_query->value(0).toString() + u'/' + m_query->value(1).toString());
1449 return fileNames;
1450}
1451
1452QUrl QHelpCollectionHandler::findFile(const QUrl &url, const QStringList &filterAttributes) const
1453{
1454 if (!isDBOpened())
1455 return {};
1456
1457 const QString namespaceName = namespaceForFile(url, filterAttributes);
1458 if (namespaceName.isEmpty())
1459 return {};
1460
1461 QUrl result = url;
1462 result.setAuthority(namespaceName);
1463 return result;
1464}
1465
1466QUrl QHelpCollectionHandler::findFile(const QUrl &url, const QString &filterName) const
1467{
1468 if (!isDBOpened())
1469 return {};
1470
1471 const QString namespaceName = namespaceForFile(url, filterName);
1472 if (namespaceName.isEmpty())
1473 return {};
1474
1475 QUrl result = url;
1476 result.setAuthority(namespaceName);
1477 return result;
1478}
1479
1481{
1482 if (!isDBOpened())
1483 return {};
1484
1485 const QString namespaceName = namespaceForFile(url, QString());
1486 if (namespaceName.isEmpty())
1487 return {};
1488
1489 const FileInfo fileInfo = extractFileInfo(url);
1490
1491 const FileInfo docInfo = registeredDocumentation(namespaceName);
1492 const QString absFileName = absoluteDocPath(docInfo.fileName);
1493
1494 QHelpDBReader reader(absFileName, QHelpGlobal::uniquifyConnectionName(
1495 docInfo.fileName, const_cast<QHelpCollectionHandler *>(this)), nullptr);
1496 if (!reader.init())
1497 return {};
1498
1499 return reader.fileData(fileInfo.folderName, fileInfo.fileName);
1500}
1501
1502QStringList QHelpCollectionHandler::indicesForFilter(const QStringList &filterAttributes) const
1503{
1504 QStringList indices;
1505
1506 if (!isDBOpened())
1507 return indices;
1508
1509 const QString filterlessQuery =
1510 "SELECT DISTINCT "
1511 "IndexTable.Name "
1512 "FROM "
1513 "IndexTable, "
1514 "FileNameTable, "
1515 "FolderTable, "
1516 "NamespaceTable "
1517 "WHERE IndexTable.FileId = FileNameTable.FileId "
1518 "AND FileNameTable.FolderId = FolderTable.Id "
1519 "AND IndexTable.NamespaceId = NamespaceTable.Id"_L1;
1520
1521 const QString filterQuery = filterlessQuery
1522 + prepareFilterQuery(filterAttributes.size(), "IndexTable"_L1, "Id"_L1,
1523 "IndexFilterTable"_L1, "IndexId"_L1)
1524 + " ORDER BY LOWER(IndexTable.Name), IndexTable.Name"_L1;
1525 // this doesn't work: ASC COLLATE NOCASE
1526
1527 m_query->prepare(filterQuery);
1528 bindFilterQuery(m_query.get(), 0, filterAttributes);
1529
1530 m_query->exec();
1531
1532 while (m_query->next())
1533 indices.append(m_query->value(0).toString());
1534
1535 return indices;
1536}
1537
1538QStringList QHelpCollectionHandler::indicesForFilter(const QString &filterName) const
1539{
1540 QStringList indices;
1541
1542 if (!isDBOpened())
1543 return indices;
1544
1545 const QString filterlessQuery =
1546 "SELECT DISTINCT "
1547 "IndexTable.Name "
1548 "FROM "
1549 "IndexTable, "
1550 "FileNameTable, "
1551 "FolderTable, "
1552 "NamespaceTable "
1553 "WHERE IndexTable.FileId = FileNameTable.FileId "
1554 "AND FileNameTable.FolderId = FolderTable.Id "
1555 "AND IndexTable.NamespaceId = NamespaceTable.Id"_L1;
1556
1557 const QString filterQuery = filterlessQuery
1558 + prepareFilterQuery(filterName)
1559 + " ORDER BY LOWER(IndexTable.Name), IndexTable.Name"_L1;
1560
1561 m_query->prepare(filterQuery);
1562 bindFilterQuery(m_query.get(), 0, filterName);
1563
1564 m_query->exec();
1565
1566 while (m_query->next())
1567 indices.append(m_query->value(0).toString());
1568
1569 return indices;
1570}
1571
1572static QString getTitle(const QByteArray &contents)
1573{
1574 if (!contents.size())
1575 return {};
1576
1577 int depth = 0;
1578 QString link;
1579 QString title;
1580
1581 QDataStream s(contents);
1582 s >> depth;
1583 s >> link;
1584 s >> title;
1585
1586 return title;
1587}
1588
1590 const QStringList &filterAttributes) const
1591{
1592 if (!isDBOpened())
1593 return {};
1594
1595 const QString filterlessQuery =
1596 "SELECT DISTINCT "
1597 "NamespaceTable.Name, "
1598 "FolderTable.Name, "
1599 "ContentsTable.Data, "
1600 "VersionTable.Version "
1601 "FROM "
1602 "FolderTable, "
1603 "NamespaceTable, "
1604 "ContentsTable, "
1605 "VersionTable "
1606 "WHERE ContentsTable.NamespaceId = NamespaceTable.Id "
1607 "AND NamespaceTable.Id = FolderTable.NamespaceId "
1608 "AND ContentsTable.NamespaceId = NamespaceTable.Id "
1609 "AND VersionTable.NamespaceId = NamespaceTable.Id"_L1;
1610
1611 const QString filterQuery = filterlessQuery
1612 + prepareFilterQuery(filterAttributes.size(), "ContentsTable"_L1, "Id"_L1,
1613 "ContentsFilterTable"_L1, "ContentsId"_L1);
1614
1615 m_query->prepare(filterQuery);
1616 bindFilterQuery(m_query.get(), 0, filterAttributes);
1617
1618 m_query->exec();
1619
1620 QMap<QString, QMap<QVersionNumber, ContentsData>> contentsMap;
1621
1622 while (m_query->next()) {
1623 const QString namespaceName = m_query->value(0).toString();
1624 const QByteArray contents = m_query->value(2).toByteArray();
1625 const QString versionString = m_query->value(3).toString();
1626
1627 const QString title = getTitle(contents);
1628 const QVersionNumber version = QVersionNumber::fromString(versionString);
1629 // get existing or insert a new one otherwise
1630 ContentsData &contentsData = contentsMap[title][version];
1631 contentsData.namespaceName = namespaceName;
1632 contentsData.folderName = m_query->value(1).toString();
1633 contentsData.contentsList.append(contents);
1634 }
1635
1636 QList<QHelpCollectionHandler::ContentsData> result;
1637 for (const auto &versionContents : std::as_const(contentsMap)) {
1638 // insert items in the reverse order of version number
1639 const auto itBegin = versionContents.constBegin();
1640 auto it = versionContents.constEnd();
1641 while (it != itBegin) {
1642 --it;
1643 result.append(it.value());
1644 }
1645 }
1646 return result;
1647}
1648
1649QList<QHelpCollectionHandler::ContentsData> QHelpCollectionHandler::contentsForFilter(const QString &filterName) const
1650{
1651 if (!isDBOpened())
1652 return {};
1653
1654 const QString filterlessQuery =
1655 "SELECT DISTINCT "
1656 "NamespaceTable.Name, "
1657 "FolderTable.Name, "
1658 "ContentsTable.Data, "
1659 "VersionTable.Version "
1660 "FROM "
1661 "FolderTable, "
1662 "NamespaceTable, "
1663 "ContentsTable, "
1664 "VersionTable "
1665 "WHERE ContentsTable.NamespaceId = NamespaceTable.Id "
1666 "AND NamespaceTable.Id = FolderTable.NamespaceId "
1667 "AND ContentsTable.NamespaceId = NamespaceTable.Id "
1668 "AND VersionTable.NamespaceId = NamespaceTable.Id"_L1;
1669
1670 const QString filterQuery = filterlessQuery + prepareFilterQuery(filterName);
1671
1672 m_query->prepare(filterQuery);
1673 bindFilterQuery(m_query.get(), 0, filterName);
1674
1675 m_query->exec();
1676
1677 QMap<QString, QMap<QVersionNumber, ContentsData>> contentsMap;
1678
1679 while (m_query->next()) {
1680 const QString namespaceName = m_query->value(0).toString();
1681 const QByteArray contents = m_query->value(2).toByteArray();
1682 const QString versionString = m_query->value(3).toString();
1683
1684 const QString title = getTitle(contents);
1685 const QVersionNumber version = QVersionNumber::fromString(versionString);
1686 // get existing or insert a new one otherwise
1687 ContentsData &contentsData = contentsMap[title][version];
1688 contentsData.namespaceName = namespaceName;
1689 contentsData.folderName = m_query->value(1).toString();
1690 contentsData.contentsList.append(contents);
1691 }
1692
1693 QList<QHelpCollectionHandler::ContentsData> result;
1694 for (const auto &versionContents : std::as_const(contentsMap)) {
1695 // insert items in the reverse order of version number
1696 const auto itBegin = versionContents.constBegin();
1697 auto it = versionContents.constEnd();
1698 while (it != itBegin) {
1699 --it;
1700 result.append(it.value());
1701 }
1702 }
1703 return result;
1704}
1705
1706bool QHelpCollectionHandler::removeCustomValue(const QString &key)
1707{
1708 if (!isDBWritable())
1709 return false;
1710
1711 m_query->prepare("DELETE FROM SettingsTable WHERE Key=?"_L1);
1712 m_query->bindValue(0, key);
1713 return m_query->exec();
1714}
1715
1716QVariant QHelpCollectionHandler::customValue(const QString &key,
1717 const QVariant &defaultValue) const
1718{
1719 if (!m_query)
1720 return defaultValue;
1721
1722 m_query->prepare("SELECT COUNT(Key) FROM SettingsTable WHERE Key=?"_L1);
1723 m_query->bindValue(0, key);
1724 if (!m_query->exec() || !m_query->next() || !m_query->value(0).toInt()) {
1725 m_query->clear();
1726 return defaultValue;
1727 }
1728
1729 m_query->clear();
1730 m_query->prepare("SELECT Value FROM SettingsTable WHERE Key=?"_L1);
1731 m_query->bindValue(0, key);
1732 if (m_query->exec() && m_query->next()) {
1733 const QVariant &value = m_query->value(0);
1734 m_query->clear();
1735 return value;
1736 }
1737 return defaultValue;
1738}
1739
1740bool QHelpCollectionHandler::setCustomValue(const QString &key,
1741 const QVariant &value)
1742{
1743 if (!isDBWritable())
1744 return false;
1745
1746 m_query->prepare("SELECT Value FROM SettingsTable WHERE Key=?"_L1);
1747 m_query->bindValue(0, key);
1748 m_query->exec();
1749 if (m_query->next()) {
1750 m_query->prepare("UPDATE SettingsTable SET Value=? where Key=?"_L1);
1751 m_query->bindValue(0, value);
1752 m_query->bindValue(1, key);
1753 } else {
1754 m_query->prepare("INSERT INTO SettingsTable VALUES(?, ?)"_L1);
1755 m_query->bindValue(0, key);
1756 m_query->bindValue(1, value);
1757 }
1758 return m_query->exec();
1759}
1760
1761bool QHelpCollectionHandler::registerFilterAttributes(const QList<QStringList> &attributeSets,
1762 qint64 nsId)
1763{
1764 if (!isDBOpened())
1765 return false;
1766
1767 m_query->exec("SELECT Name FROM FilterAttributeTable"_L1);
1768 QSet<QString> atts;
1769 while (m_query->next())
1770 atts.insert(m_query->value(0).toString());
1771
1772 for (const QStringList &attributeSet : attributeSets) {
1773 for (const QString &attribute : attributeSet) {
1774 if (!atts.contains(attribute)) {
1775 m_query->prepare("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"_L1);
1776 m_query->bindValue(0, attribute);
1777 m_query->exec();
1778 }
1779 }
1780 }
1781 return registerFileAttributeSets(attributeSets, nsId);
1782}
1783
1784bool QHelpCollectionHandler::registerFileAttributeSets(const QList<QStringList> &attributeSets,
1785 qint64 nsId)
1786{
1787 if (!isDBOpened())
1788 return false;
1789
1790 if (attributeSets.isEmpty())
1791 return true;
1792
1793 QVariantList nsIds;
1794 QVariantList attributeSetIds;
1795 QVariantList filterAttributeIds;
1796
1797 const std::optional<qint64> maxAttributeSetId
1798 = readMaxId(m_query.get(), "FileAttributeSetTable"_L1, "FilterAttributeSetId"_L1,
1799 attributeSets.size());
1800 if (!maxAttributeSetId)
1801 return false;
1802
1803 qint64 attributeSetId = *maxAttributeSetId;
1804
1805 for (const QStringList &attributeSet : attributeSets) {
1806 ++attributeSetId;
1807
1808 for (const QString &attribute : attributeSet) {
1809 m_query->prepare("SELECT Id FROM FilterAttributeTable WHERE Name=?"_L1);
1810 m_query->bindValue(0, attribute);
1811
1812 if (!m_query->exec() || !m_query->next())
1813 return false;
1814
1815 nsIds.append(nsId);
1816 attributeSetIds.append(attributeSetId);
1817 filterAttributeIds.append(m_query->value(0).toLongLong());
1818 }
1819 }
1820
1821 m_query->prepare("INSERT INTO FileAttributeSetTable "
1822 "(NamespaceId, FilterAttributeSetId, FilterAttributeId) "
1823 "VALUES(?, ?, ?)"_L1);
1824 m_query->addBindValue(nsIds);
1825 m_query->addBindValue(attributeSetIds);
1826 m_query->addBindValue(filterAttributeIds);
1827 return m_query->execBatch();
1828}
1829
1831{
1832 QStringList list;
1833 if (m_query) {
1834 m_query->exec("SELECT Name FROM FilterAttributeTable"_L1);
1835 while (m_query->next())
1836 list.append(m_query->value(0).toString());
1837 }
1838 return list;
1839}
1840
1841QStringList QHelpCollectionHandler::filterAttributes(const QString &filterName) const
1842{
1843 QStringList list;
1844 if (m_query) {
1845 m_query->prepare(
1846 "SELECT "
1847 "FilterAttributeTable.Name "
1848 "FROM "
1849 "FilterAttributeTable, "
1850 "FilterTable, "
1851 "FilterNameTable "
1852 "WHERE FilterAttributeTable.Id = FilterTable.FilterAttributeId "
1853 "AND FilterTable.NameId = FilterNameTable.Id "
1854 "AND FilterNameTable.Name=?"_L1);
1855 m_query->bindValue(0, filterName);
1856 m_query->exec();
1857 while (m_query->next())
1858 list.append(m_query->value(0).toString());
1859 }
1860 return list;
1861}
1862
1863QList<QStringList> QHelpCollectionHandler::filterAttributeSets(const QString &namespaceName) const
1864{
1865 if (!isDBOpened())
1866 return {};
1867
1868 m_query->prepare(
1869 "SELECT "
1870 "FileAttributeSetTable.FilterAttributeSetId, "
1871 "FilterAttributeTable.Name "
1872 "FROM "
1873 "FileAttributeSetTable, "
1874 "FilterAttributeTable, "
1875 "NamespaceTable "
1876 "WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
1877 "AND FileAttributeSetTable.NamespaceId = NamespaceTable.Id "
1878 "AND NamespaceTable.Name = ? "
1879 "ORDER BY FileAttributeSetTable.FilterAttributeSetId"_L1);
1880 m_query->bindValue(0, namespaceName);
1881 m_query->exec();
1882 qint64 oldId = -1;
1883 QList<QStringList> result;
1884 while (m_query->next()) {
1885 const qint64 id = m_query->value(0).toLongLong();
1886 if (id != oldId) {
1887 result.append(QStringList());
1888 oldId = id;
1889 }
1890 result.last().append(m_query->value(1).toString());
1891 }
1892
1893 if (result.isEmpty())
1894 result.append(QStringList());
1895 return result;
1896}
1897
1898QString QHelpCollectionHandler::namespaceVersion(const QString &namespaceName) const
1899{
1900 if (!m_query)
1901 return {};
1902
1903 m_query->prepare(
1904 "SELECT "
1905 "VersionTable.Version "
1906 "FROM "
1907 "NamespaceTable, "
1908 "VersionTable "
1909 "WHERE NamespaceTable.Name = ? "
1910 "AND NamespaceTable.Id = VersionTable.NamespaceId"_L1);
1911 m_query->bindValue(0, namespaceName);
1912 if (!m_query->exec() || !m_query->next())
1913 return {};
1914
1915 const QString ret = m_query->value(0).toString();
1916 m_query->clear();
1917 return ret;
1918}
1919
1920qint64 QHelpCollectionHandler::registerNamespace(const QString &nspace, const QString &fileName)
1921{
1922 const int errorValue = -1;
1923 if (!m_query)
1924 return errorValue;
1925
1926 m_query->prepare("SELECT COUNT(Id) FROM NamespaceTable WHERE Name=?"_L1);
1927 m_query->bindValue(0, nspace);
1928 m_query->exec();
1929 while (m_query->next()) {
1930 if (m_query->value(0).toInt() > 0) {
1931 emit error(tr("Namespace %1 already exists.").arg(nspace));
1932 return errorValue;
1933 }
1934 }
1935
1936 QFileInfo fi(m_collectionFile);
1937 m_query->prepare("INSERT INTO NamespaceTable VALUES(NULL, ?, ?)"_L1);
1938 m_query->bindValue(0, nspace);
1939 m_query->bindValue(1, fi.absoluteDir().relativeFilePath(fileName));
1940 qint64 namespaceId = errorValue;
1941 if (m_query->exec()) {
1942 namespaceId = m_query->lastInsertId().toLongLong();
1943 m_query->clear();
1944 }
1945 if (namespaceId < 1) {
1946 emit error(tr("Cannot register namespace \"%1\".").arg(nspace));
1947 return errorValue;
1948 }
1949 return namespaceId;
1950}
1951
1952qint64 QHelpCollectionHandler::registerVirtualFolder(const QString &folderName, qint64 namespaceId)
1953{
1954 if (!m_query)
1955 return false;
1956
1957 m_query->prepare("INSERT INTO FolderTable VALUES(NULL, ?, ?)"_L1);
1958 m_query->bindValue(0, namespaceId);
1959 m_query->bindValue(1, folderName);
1960
1961 qint64 virtualId = -1;
1962 if (m_query->exec()) {
1963 virtualId = m_query->lastInsertId().toLongLong();
1964 m_query->clear();
1965 }
1966 if (virtualId < 1) {
1967 emit error(tr("Cannot register virtual folder '%1'.").arg(folderName));
1968 return -1;
1969 }
1970 if (registerComponent(folderName, namespaceId) < 0) {
1971 emit error(tr("Cannot register component '%1'.").arg(folderName));
1972 return -1;
1973 }
1974 return virtualId;
1975}
1976
1977qint64 QHelpCollectionHandler::registerComponent(const QString &componentName, qint64 namespaceId)
1978{
1979 m_query->prepare("SELECT ComponentId FROM ComponentTable WHERE Name = ?"_L1);
1980 m_query->bindValue(0, componentName);
1981 if (!m_query->exec())
1982 return -1;
1983
1984 if (!m_query->next()) {
1985 m_query->prepare("INSERT INTO ComponentTable VALUES(NULL, ?)"_L1);
1986 m_query->bindValue(0, componentName);
1987 if (!m_query->exec())
1988 return -1;
1989
1990 m_query->prepare("SELECT ComponentId FROM ComponentTable WHERE Name = ?"_L1);
1991 m_query->bindValue(0, componentName);
1992 if (!m_query->exec() || !m_query->next())
1993 return -1;
1994 }
1995
1996 const qint64 componentId = m_query->value(0).toLongLong();
1997
1998 m_query->prepare("INSERT INTO ComponentMapping VALUES(?, ?)"_L1);
1999 m_query->bindValue(0, componentId);
2000 m_query->bindValue(1, namespaceId);
2001 if (!m_query->exec())
2002 return -1;
2003
2004 return componentId;
2005}
2006
2007bool QHelpCollectionHandler::registerVersion(const QString &version, qint64 namespaceId)
2008{
2009 if (!m_query)
2010 return false;
2011
2012 m_query->prepare("INSERT INTO VersionTable (NamespaceId, Version) VALUES(?, ?)"_L1);
2013 m_query->addBindValue(namespaceId);
2014 m_query->addBindValue(version);
2015 return m_query->exec();
2016}
2017
2018bool QHelpCollectionHandler::registerIndexAndNamespaceFilterTables(
2019 const QString &nameSpace, bool createDefaultVersionFilter)
2020{
2021 if (!isDBOpened())
2022 return false;
2023
2024 m_query->prepare("SELECT Id, FilePath FROM NamespaceTable WHERE Name=?"_L1);
2025 m_query->bindValue(0, nameSpace);
2026 m_query->exec();
2027 if (!m_query->next())
2028 return false;
2029
2030 const qint64 nsId = m_query->value(0).toLongLong();
2031 const QString fileName = m_query->value(1).toString();
2032
2033 m_query->prepare("SELECT Id, Name FROM FolderTable WHERE NamespaceId=?"_L1);
2034 m_query->bindValue(0, nsId);
2035 m_query->exec();
2036 if (!m_query->next())
2037 return false;
2038
2039 const qint64 vfId = m_query->value(0).toLongLong();
2040 const QString vfName = m_query->value(1).toString();
2041
2042 // Drop the index which may be left over for this documentation, e.g. when its time stamp
2043 // was missing or broken, so that it isn't registered twice.
2044 if (!unregisterIndexTable(nsId, vfId))
2045 return false;
2046
2047 const QString absFileName = absoluteDocPath(fileName);
2048 QHelpDBReader reader(absFileName, QHelpGlobal::uniquifyConnectionName(
2049 fileName, this), this);
2050 if (!reader.init())
2051 return false;
2052
2053 registerComponent(vfName, nsId);
2054 registerVersion(reader.version(), nsId);
2055 if (!registerFileAttributeSets(reader.filterAttributeSets(), nsId))
2056 return false;
2057
2058 if (!registerIndexTable(reader.indexTable(), nsId, vfId, fileName))
2059 return false;
2060
2061 if (createDefaultVersionFilter)
2062 createVersionFilter(reader.version());
2063 return true;
2064}
2065
2066void QHelpCollectionHandler::createVersionFilter(const QString &version)
2067{
2068 if (version.isEmpty())
2069 return;
2070
2071 const QVersionNumber versionNumber = QVersionNumber::fromString(version);
2072 if (versionNumber.isNull())
2073 return;
2074
2075 const QString filterName = tr("Version %1").arg(version);
2076 if (filters().contains(filterName))
2077 return;
2078
2079 QHelpFilterData filterData;
2080 filterData.setVersions({versionNumber});
2081 setFilterData(filterName, filterData);
2082}
2083
2084bool QHelpCollectionHandler::registerIndexTable(const QHelpDBReader::IndexTable &indexTable,
2085 qint64 nsId, qint64 vfId, const QString &fileName)
2086{
2087 const auto fail = [this, &fileName] {
2088 emit error(tr("Cannot register index tables of documentation %1.").arg(fileName));
2089 return false;
2090 };
2091
2092 if (!indexTable.valid)
2093 return fail();
2094
2095 Transaction transaction(m_connectionName);
2096
2097 QMap<QString, QVariantList> filterAttributeToNewFileId;
2098
2099 QVariantList fileFolderIds;
2100 QVariantList fileNames;
2101 QVariantList fileTitles;
2102 const int fileSize = indexTable.fileItems.size();
2103 fileFolderIds.reserve(fileSize);
2104 fileNames.reserve(fileSize);
2105 fileTitles.reserve(fileSize);
2106
2107 const std::optional<qint64> maxFileIdOpt
2108 = readMaxId(m_query.get(), "FileNameTable"_L1, "FileId"_L1, fileSize);
2109 if (!maxFileIdOpt)
2110 return fail();
2111
2112 const qint64 maxFileId = *maxFileIdOpt;
2113 QVariantList fileIds;
2114 fileIds.reserve(fileSize);
2115 int newFileId = 0;
2116 for (const QHelpDBReader::FileItem &item : indexTable.fileItems) {
2117 fileIds.append(maxFileId + newFileId + 1);
2118 fileFolderIds.append(vfId);
2119 fileNames.append(item.name);
2120 fileTitles.append(item.title);
2121
2122 for (const QString &filterAttribute : item.filterAttributes)
2123 filterAttributeToNewFileId[filterAttribute].append(maxFileId + newFileId + 1);
2124 ++newFileId;
2125 }
2126
2127 m_query->prepare("INSERT INTO FileNameTable VALUES(?, ?, ?, ?)"_L1);
2128 m_query->addBindValue(fileFolderIds);
2129 m_query->addBindValue(fileNames);
2130 m_query->addBindValue(fileIds);
2131 m_query->addBindValue(fileTitles);
2132 if (!m_query->execBatch())
2133 return fail();
2134
2135 for (auto it = filterAttributeToNewFileId.cbegin(),
2136 end = filterAttributeToNewFileId.cend(); it != end; ++it) {
2137 const QString filterAttribute = it.key();
2138 m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
2139 m_query->bindValue(0, filterAttribute);
2140 if (!m_query->exec() || !m_query->next())
2141 return fail();
2142
2143 const qint64 attributeId = m_query->value(0).toLongLong();
2144
2145 QVariantList attributeIds;
2146 for (int i = 0; i < it.value().size(); i++)
2147 attributeIds.append(attributeId);
2148
2149 m_query->prepare("INSERT INTO FileFilterTable VALUES(?, ?)"_L1);
2150 m_query->addBindValue(attributeIds);
2151 m_query->addBindValue(it.value());
2152 if (!m_query->execBatch())
2153 return fail();
2154 }
2155
2156 QMap<QString, QVariantList> filterAttributeToNewIndexId;
2157
2158 QVariantList indexIds;
2159 QVariantList indexNames;
2160 QVariantList indexIdentifiers;
2161 QVariantList indexNamespaceIds;
2162 QVariantList indexFileIds;
2163 QVariantList indexAnchors;
2164 const int indexSize = indexTable.indexItems.size();
2165 indexIds.reserve(indexSize);
2166 indexNames.reserve(indexSize);
2167 indexIdentifiers.reserve(indexSize);
2168 indexNamespaceIds.reserve(indexSize);
2169 indexFileIds.reserve(indexSize);
2170 indexAnchors.reserve(indexSize);
2171
2172 const std::optional<qint64> maxIndexIdOpt
2173 = readMaxId(m_query.get(), "IndexTable"_L1, "Id"_L1, indexSize);
2174 if (!maxIndexIdOpt)
2175 return fail();
2176
2177 const qint64 maxIndexId = *maxIndexIdOpt;
2178 int newIndexId = 0;
2179 for (const QHelpDBReader::IndexItem &item : indexTable.indexItems) {
2180 // The file id is an index into the fileItems of this documentation. Anything else
2181 // would make the keyword point to a file of other documentation.
2182 if (item.fileId < 0 || item.fileId >= fileSize) {
2183 emit error(tr("Invalid file reference for keyword '%1' in '%2'.")
2184 .arg(item.name, fileName));
2185 return false;
2186 }
2187 indexIds.append(maxIndexId + newIndexId + 1);
2188 indexNames.append(item.name);
2189 indexIdentifiers.append(item.identifier);
2190 indexNamespaceIds.append(nsId);
2191 indexFileIds.append(maxFileId + item.fileId + 1);
2192 indexAnchors.append(item.anchor);
2193
2194 for (const QString &filterAttribute : item.filterAttributes)
2195 filterAttributeToNewIndexId[filterAttribute].append(maxIndexId + newIndexId + 1);
2196 ++newIndexId;
2197 }
2198
2199 m_query->prepare("INSERT INTO IndexTable VALUES(?, ?, ?, ?, ?, ?)"_L1);
2200 m_query->addBindValue(indexIds);
2201 m_query->addBindValue(indexNames);
2202 m_query->addBindValue(indexIdentifiers);
2203 m_query->addBindValue(indexNamespaceIds);
2204 m_query->addBindValue(indexFileIds);
2205 m_query->addBindValue(indexAnchors);
2206 if (!m_query->execBatch())
2207 return fail();
2208
2209 for (auto it = filterAttributeToNewIndexId.cbegin(),
2210 end = filterAttributeToNewIndexId.cend(); it != end; ++it) {
2211 const QString filterAttribute = it.key();
2212 m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
2213 m_query->bindValue(0, filterAttribute);
2214 if (!m_query->exec() || !m_query->next())
2215 return fail();
2216
2217 const qint64 attributeId = m_query->value(0).toLongLong();
2218
2219 QVariantList attributeIds;
2220 for (int i = 0; i < it.value().size(); i++)
2221 attributeIds.append(attributeId);
2222
2223 m_query->prepare("INSERT INTO IndexFilterTable VALUES(?, ?)"_L1);
2224 m_query->addBindValue(attributeIds);
2225 m_query->addBindValue(it.value());
2226 if (!m_query->execBatch())
2227 return fail();
2228 }
2229
2230 QMap<QString, QVariantList> filterAttributeToNewContentsId;
2231
2232 QVariantList contentsIds;
2233 QVariantList contentsNsIds;
2234 QVariantList contentsData;
2235 const int contentsSize = indexTable.contentsItems.size();
2236 contentsIds.reserve(contentsSize);
2237 contentsNsIds.reserve(contentsSize);
2238 contentsData.reserve(contentsSize);
2239
2240 const std::optional<qint64> maxContentsIdOpt
2241 = readMaxId(m_query.get(), "ContentsTable"_L1, "Id"_L1, contentsSize);
2242 if (!maxContentsIdOpt)
2243 return fail();
2244
2245 const qint64 maxContentsId = *maxContentsIdOpt;
2246 int newContentsId = 0;
2247 for (const QHelpDBReader::ContentsItem &item : indexTable.contentsItems) {
2248 contentsIds.append(maxContentsId + newContentsId + 1);
2249 contentsNsIds.append(nsId);
2250 contentsData.append(item.data);
2251
2252 for (const QString &filterAttribute : item.filterAttributes) {
2253 filterAttributeToNewContentsId[filterAttribute]
2254 .append(maxContentsId + newContentsId + 1);
2255 }
2256 ++newContentsId;
2257 }
2258
2259 m_query->prepare("INSERT INTO ContentsTable VALUES(?, ?, ?)"_L1);
2260 m_query->addBindValue(contentsIds);
2261 m_query->addBindValue(contentsNsIds);
2262 m_query->addBindValue(contentsData);
2263 if (!m_query->execBatch())
2264 return fail();
2265
2266 for (auto it = filterAttributeToNewContentsId.cbegin(),
2267 end = filterAttributeToNewContentsId.cend(); it != end; ++it) {
2268 const QString filterAttribute = it.key();
2269 m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
2270 m_query->bindValue(0, filterAttribute);
2271 if (!m_query->exec() || !m_query->next())
2272 return fail();
2273
2274 const qint64 attributeId = m_query->value(0).toLongLong();
2275
2276 QVariantList attributeIds;
2277 for (int i = 0; i < it.value().size(); i++)
2278 attributeIds.append(attributeId);
2279
2280 m_query->prepare("INSERT INTO ContentsFilterTable VALUES(?, ?)"_L1);
2281 m_query->addBindValue(attributeIds);
2282 m_query->addBindValue(it.value());
2283 if (!m_query->execBatch())
2284 return fail();
2285 }
2286
2287 QVariantList filterNsIds;
2288 QVariantList filterAttributeIds;
2289 for (const QString &filterAttribute : indexTable.usedFilterAttributes) {
2290 filterNsIds.append(nsId);
2291
2292 m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
2293 m_query->bindValue(0, filterAttribute);
2294 if (!m_query->exec() || !m_query->next())
2295 return fail();
2296
2297 filterAttributeIds.append(m_query->value(0).toLongLong());
2298 }
2299
2300 m_query->prepare("INSERT INTO OptimizedFilterTable "
2301 "(NamespaceId, FilterAttributeId) VALUES(?, ?)"_L1);
2302 m_query->addBindValue(filterNsIds);
2303 m_query->addBindValue(filterAttributeIds);
2304 if (!m_query->execBatch())
2305 return fail();
2306
2307 m_query->prepare("INSERT INTO TimeStampTable "
2308 "(NamespaceId, FolderId, FilePath, Size, TimeStamp) "
2309 "VALUES(?, ?, ?, ?, ?)"_L1);
2310 m_query->addBindValue(nsId);
2311 m_query->addBindValue(vfId);
2312 m_query->addBindValue(fileName);
2313 const QFileInfo fi(absoluteDocPath(fileName));
2314 m_query->addBindValue(fi.size());
2315 QDateTime lastModified = fi.lastModified(QTimeZone::UTC);
2316 if (qEnvironmentVariableIsSet("SOURCE_DATE_EPOCH")) {
2317 const QString sourceDateEpochStr = qEnvironmentVariable("SOURCE_DATE_EPOCH");
2318 bool ok;
2319 const qlonglong sourceDateEpoch = sourceDateEpochStr.toLongLong(&ok);
2320 if (ok && sourceDateEpoch < lastModified.toSecsSinceEpoch())
2321 lastModified.setSecsSinceEpoch(sourceDateEpoch);
2322 }
2323 m_query->addBindValue(lastModified);
2324 if (!m_query->exec())
2325 return fail();
2326
2327 if (!transaction.commit())
2328 return fail();
2329 return true;
2330}
2331
2332bool QHelpCollectionHandler::unregisterIndexTable(qint64 nsId, qint64 vfId)
2333{
2334 m_query->prepare("DELETE FROM IndexFilterTable WHERE IndexId IN "
2335 "(SELECT Id FROM IndexTable WHERE NamespaceId = ?)"_L1);
2336 m_query->bindValue(0, nsId);
2337 if (!m_query->exec())
2338 return false;
2339
2340 m_query->prepare("DELETE FROM IndexTable WHERE NamespaceId = ?"_L1);
2341 m_query->bindValue(0, nsId);
2342 if (!m_query->exec())
2343 return false;
2344
2345 m_query->prepare("DELETE FROM FileFilterTable WHERE FileId IN "
2346 "(SELECT FileId FROM FileNameTable WHERE FolderId = ?)"_L1);
2347 m_query->bindValue(0, vfId);
2348 if (!m_query->exec())
2349 return false;
2350
2351 m_query->prepare("DELETE FROM FileNameTable WHERE FolderId = ?"_L1);
2352 m_query->bindValue(0, vfId);
2353 if (!m_query->exec())
2354 return false;
2355
2356 m_query->prepare("DELETE FROM ContentsFilterTable WHERE ContentsId IN "
2357 "(SELECT Id FROM ContentsTable WHERE NamespaceId = ?)"_L1);
2358 m_query->bindValue(0, nsId);
2359 if (!m_query->exec())
2360 return false;
2361
2362 m_query->prepare("DELETE FROM ContentsTable WHERE NamespaceId = ?"_L1);
2363 m_query->bindValue(0, nsId);
2364 if (!m_query->exec())
2365 return false;
2366
2367 m_query->prepare("DELETE FROM FileAttributeSetTable WHERE NamespaceId = ?"_L1);
2368 m_query->bindValue(0, nsId);
2369 if (!m_query->exec())
2370 return false;
2371
2372 m_query->prepare("DELETE FROM OptimizedFilterTable WHERE NamespaceId = ?"_L1);
2373 m_query->bindValue(0, nsId);
2374 if (!m_query->exec())
2375 return false;
2376
2377 m_query->prepare("DELETE FROM TimeStampTable WHERE NamespaceId = ?"_L1);
2378 m_query->bindValue(0, nsId);
2379 if (!m_query->exec())
2380 return false;
2381
2382 m_query->prepare("DELETE FROM VersionTable WHERE NamespaceId = ?"_L1);
2383 m_query->bindValue(0, nsId);
2384 if (!m_query->exec())
2385 return false;
2386
2387 m_query->prepare("SELECT ComponentId FROM ComponentMapping WHERE NamespaceId = ?"_L1);
2388 m_query->bindValue(0, nsId);
2389 if (!m_query->exec())
2390 return false;
2391
2392 if (!m_query->next())
2393 return true; // the component mapping is already removed
2394
2395 const qint64 componentId = m_query->value(0).toLongLong();
2396
2397 m_query->prepare("DELETE FROM ComponentMapping WHERE NamespaceId = ?"_L1);
2398 m_query->bindValue(0, nsId);
2399 if (!m_query->exec())
2400 return false;
2401
2402 m_query->prepare("SELECT ComponentId FROM ComponentMapping WHERE ComponentId = ?"_L1);
2403 m_query->bindValue(0, componentId);
2404 if (!m_query->exec())
2405 return false;
2406
2407 if (!m_query->next()) { // no more namespaces refer to the componentId
2408 m_query->prepare("DELETE FROM ComponentTable WHERE ComponentId = ?"_L1);
2409 m_query->bindValue(0, componentId);
2410 if (!m_query->exec())
2411 return false;
2412 }
2413
2414 return true;
2415}
2416
2417QUrl QHelpCollectionHandler::buildQUrl(const QString &ns, const QString &folder,
2418 const QString &relFileName, const QString &anchor)
2419{
2420 QUrl url;
2421 url.setScheme("qthelp"_L1);
2422 url.setAuthority(ns);
2423 url.setPath(u'/' + folder + u'/' + relFileName);
2424 url.setFragment(anchor);
2425 return url;
2426}
2427
2428QList<QHelpLink> QHelpCollectionHandler::documentsForIdentifier(
2429 const QString &id, const QStringList &filterAttributes) const
2430{
2431 return documentsForField("Identifier"_L1, id, filterAttributes);
2432}
2433
2434QList<QHelpLink> QHelpCollectionHandler::documentsForKeyword(
2435 const QString &keyword, const QStringList &filterAttributes) const
2436{
2437 return documentsForField("Name"_L1, keyword, filterAttributes);
2438}
2439
2440QList<QHelpLink> QHelpCollectionHandler::documentsForField(const QString &fieldName,
2441 const QString &fieldValue, const QStringList &filterAttributes) const
2442{
2443 if (!isDBOpened())
2444 return {};
2445
2446 const QString filterlessQuery =
2447 "SELECT "
2448 "FileNameTable.Title, "
2449 "NamespaceTable.Name, "
2450 "FolderTable.Name, "
2451 "FileNameTable.Name, "
2452 "IndexTable.Anchor "
2453 "FROM "
2454 "IndexTable, "
2455 "FileNameTable, "
2456 "FolderTable, "
2457 "NamespaceTable "
2458 "WHERE IndexTable.FileId = FileNameTable.FileId "
2459 "AND FileNameTable.FolderId = FolderTable.Id "
2460 "AND IndexTable.NamespaceId = NamespaceTable.Id "
2461 "AND IndexTable.%1 = ?"_L1.arg(fieldName);
2462
2463 const QString filterQuery = filterlessQuery
2464 + prepareFilterQuery(filterAttributes.size(), "IndexTable"_L1, "Id"_L1,
2465 "IndexFilterTable"_L1, "IndexId"_L1);
2466
2467 m_query->prepare(filterQuery);
2468 m_query->bindValue(0, fieldValue);
2469 bindFilterQuery(m_query.get(), 1, filterAttributes);
2470
2471 m_query->exec();
2472
2473 QList<QHelpLink> docList;
2474 while (m_query->next()) {
2475 QString title = m_query->value(0).toString();
2476 if (title.isEmpty()) // generate a title + corresponding path
2477 title = fieldValue + " : "_L1 + m_query->value(3).toString();
2478
2479 const QUrl url = buildQUrl(m_query->value(1).toString(),
2480 m_query->value(2).toString(),
2481 m_query->value(3).toString(),
2482 m_query->value(4).toString());
2483 docList.append(QHelpLink {url, title});
2484 }
2485 return docList;
2486}
2487
2488QList<QHelpLink> QHelpCollectionHandler::documentsForIdentifier(
2489 const QString &id, const QString &filterName) const
2490{
2491 return documentsForField("Identifier"_L1, id, filterName);
2492}
2493
2494QList<QHelpLink> QHelpCollectionHandler::documentsForKeyword(
2495 const QString &keyword, const QString &filterName) const
2496{
2497 return documentsForField("Name"_L1, keyword, filterName);
2498}
2499
2500QMultiMap<QString, QUrl> QHelpCollectionHandler::linksForField(const QString &fieldName,
2501 const QString &fieldValue, const QString &filterName) const
2502{
2503 QMultiMap<QString, QUrl> linkMap;
2504 const auto documents = documentsForField(fieldName, fieldValue, filterName);
2505 for (const auto &document : documents)
2506 linkMap.insert(document.title, document.url);
2507 return linkMap;
2508}
2509
2510struct IndexEntry
2511{
2512 QString title;
2515
2516private:
2517 friend bool comparesEqual(const IndexEntry &lhs, const IndexEntry &rhs) noexcept
2518 {
2519 return lhs.title == rhs.title && lhs.url == rhs.url && lhs.version == rhs.version;
2520 }
2522};
2523
2524static inline bool hasVersion(const IndexEntry &e)
2525{
2526 return !e.version.isNull();
2527}
2528
2529static bool versionLessThan(const IndexEntry &lhs, const IndexEntry &rhs)
2530{
2531 return lhs.version < rhs.version;
2532}
2533
2535{
2536 return { e.url, e.title };
2537}
2538
2539QList<QHelpLink> QHelpCollectionHandler::documentsForField(const QString &fieldName,
2540 const QString &fieldValue, const QString &filterName) const
2541{
2542 if (!isDBOpened())
2543 return {};
2544
2545 const QString filterlessQuery =
2546 "SELECT "
2547 "FileNameTable.Title, "
2548 "NamespaceTable.Name, "
2549 "FolderTable.Name, "
2550 "FileNameTable.Name, "
2551 "IndexTable.Anchor "
2552 "FROM "
2553 "IndexTable, "
2554 "FileNameTable, "
2555 "FolderTable, "
2556 "NamespaceTable "
2557 "WHERE IndexTable.FileId = FileNameTable.FileId "
2558 "AND FileNameTable.FolderId = FolderTable.Id "
2559 "AND IndexTable.NamespaceId = NamespaceTable.Id "
2560 "AND IndexTable.%1 = ?"_L1.arg(fieldName);
2561
2562 const QString filterQuery = filterlessQuery
2563 + prepareFilterQuery(filterName)
2564 + " ORDER BY LOWER(FileNameTable.Title), FileNameTable.Title"_L1;
2565
2566 m_query->prepare(filterQuery);
2567 m_query->bindValue(0, fieldValue);
2568 bindFilterQuery(m_query.get(), 1, filterName);
2569
2570 m_query->exec();
2571
2572 QList<IndexEntry> entries;
2573 while (m_query->next()) {
2574 QString title = m_query->value(0).toString();
2575 if (title.isEmpty()) // generate a title + corresponding path
2576 title = fieldValue + " : "_L1 + m_query->value(3).toString();
2577 const QString nameSpace = m_query->value(1).toString();
2578 const QUrl url = buildQUrl(nameSpace,
2579 m_query->value(2).toString(),
2580 m_query->value(3).toString(),
2581 m_query->value(4).toString());
2582 IndexEntry newEntry{ title, url, QHelpDBReader::versionHeuristic(nameSpace) };
2583 if (!entries.contains(newEntry))
2584 entries.emplaceBack(newEntry);
2585 }
2586
2587 // Reverse sort by trailing version number on the namespace, newest first
2588 if (std::all_of(entries.cbegin(), entries.cend(), hasVersion))
2589 std::stable_sort(entries.rbegin(), entries.rend(), versionLessThan);
2590
2591 QList<QHelpLink> docList;
2592 docList.reserve(entries.size());
2593 std::transform(entries.cbegin(), entries.cend(), std::back_inserter(docList), toHelpLink);
2594 return docList;
2595}
2596
2597QStringList QHelpCollectionHandler::namespacesForFilter(const QString &filterName) const
2598{
2599 QStringList namespaceList;
2600
2601 if (!isDBOpened())
2602 return namespaceList;
2603
2604 const QString filterlessQuery =
2605 "SELECT "
2606 "NamespaceTable.Name "
2607 "FROM "
2608 "NamespaceTable "
2609 "WHERE TRUE"_L1;
2610
2611 const QString filterQuery = filterlessQuery
2612 + prepareFilterQuery(filterName);
2613
2614 m_query->prepare(filterQuery);
2615 bindFilterQuery(m_query.get(), 0, filterName);
2616
2617 m_query->exec();
2618
2619 while (m_query->next())
2620 namespaceList.append(m_query->value(0).toString());
2621 return namespaceList;
2622}
2623
2624QT_END_NAMESPACE
QUrl findFile(const QUrl &url, const QStringList &filterAttributes) const
QByteArray fileData(const QUrl &url) const
FileInfoList registeredDocumentations() const
QList< ContentsData > contentsForFilter(const QStringList &filterAttributes) const
QMap< QString, QVersionNumber > namespaceToVersion() const
QList< QVersionNumber > availableVersions() const
QString namespaceForFile(const QUrl &url, const QStringList &filterAttributes) const
QMap< QString, QString > namespaceToComponent() const
QStringList availableComponents() const
QStringList indicesForFilter(const QStringList &filterAttributes) const
bool fileExists(const QUrl &url) const
\inmodule QtCore
Transaction(const QString &connectionName)
static bool hasVersion(const IndexEntry &e)
static std::optional< qint64 > readMaxId(QSqlQuery *query, const QString &tableName, const QString &columnName, int count)
static QString getTitle(const QByteArray &contents)
static QHelpCollectionHandler::FileInfo extractFileInfo(const QUrl &url)
static QString prepareFilterQuery(int attributesCount, const QString &idTableName, const QString &idColumnName, const QString &filterTableName, const QString &filterColumnName)
static void bindFilterQuery(QSqlQuery *query, int bindStart, const QString &filterName)
static bool versionLessThan(const IndexEntry &lhs, const IndexEntry &rhs)
static void bindFilterQuery(QSqlQuery *query, int startingBindPos, const QStringList &filterAttributes)
static constexpr int CollectionSchemaVersion
static QString writeSchemaVersionStatement()
static QString prepareFilterQuery(const QString &filterName)
static QHelpLink toHelpLink(const IndexEntry &e)
friend bool comparesEqual(const IndexEntry &lhs, const IndexEntry &rhs) noexcept