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

source code of qttools/src/assistant/help/qhelpcollectionhandler.cpp