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