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 "qhelpdbreader_p.h"
5#include "qhelp_global.h"
6
7#include <QtCore/QFile>
8#include <QtCore/QList>
9#include <QtCore/QMultiMap>
10#include <QtCore/QVariant>
11#include <QtSql/QSqlError>
12#include <QtSql/QSqlQuery>
13
14QT_BEGIN_NAMESPACE
15
16QHelpDBReader::QHelpDBReader(const QString &dbName)
17 : QObject(nullptr),
18 m_dbName(dbName),
19 m_uniqueId(QHelpGlobal::uniquifyConnectionName(name: QLatin1String("QHelpDBReader"),
20 pointer: this))
21{
22}
23
24QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId,
25 QObject *parent)
26 : QObject(parent),
27 m_dbName(dbName),
28 m_uniqueId(uniqueId)
29{
30}
31
32QHelpDBReader::~QHelpDBReader()
33{
34 if (m_initDone) {
35 delete m_query;
36 QSqlDatabase::removeDatabase(connectionName: m_uniqueId);
37 }
38}
39
40bool QHelpDBReader::init()
41{
42 if (m_initDone)
43 return true;
44
45 if (!QFile::exists(fileName: m_dbName))
46 return false;
47
48 if (!initDB()) {
49 QSqlDatabase::removeDatabase(connectionName: m_uniqueId);
50 return false;
51 }
52
53 m_initDone = true;
54 m_query = new QSqlQuery(QSqlDatabase::database(connectionName: m_uniqueId));
55
56 return true;
57}
58
59bool QHelpDBReader::initDB()
60{
61 QSqlDatabase db = QSqlDatabase::addDatabase(type: QLatin1String("QSQLITE"), connectionName: m_uniqueId);
62 db.setConnectOptions(QLatin1String("QSQLITE_OPEN_READONLY"));
63 db.setDatabaseName(m_dbName);
64 if (!db.open()) {
65 /*: The placeholders are: %1 - The name of the database which cannot be opened
66 %2 - The unique id for the connection
67 %3 - The actual error string */
68 m_error = tr(s: "Cannot open database \"%1\" \"%2\": %3").arg(args&: m_dbName, args&: m_uniqueId, args: db.lastError().text());
69 return false;
70 }
71 return true;
72}
73
74QString QHelpDBReader::namespaceName() const
75{
76 if (!m_namespace.isEmpty())
77 return m_namespace;
78 if (m_query) {
79 m_query->exec(query: QLatin1String("SELECT Name FROM NamespaceTable"));
80 if (m_query->next())
81 m_namespace = m_query->value(i: 0).toString();
82 }
83 return m_namespace;
84}
85
86QString QHelpDBReader::virtualFolder() const
87{
88 if (m_query) {
89 m_query->exec(query: QLatin1String("SELECT Name FROM FolderTable WHERE Id=1"));
90 if (m_query->next())
91 return m_query->value(i: 0).toString();
92 }
93 return QString();
94}
95
96QString QHelpDBReader::version() const
97{
98 const QString versionString = metaData(name: QLatin1String("version")).toString();
99 if (versionString.isEmpty())
100 return qtVersionHeuristic();
101 return versionString;
102}
103
104QString QHelpDBReader::qtVersionHeuristic() const
105{
106 const QString nameSpace = namespaceName();
107 if (!nameSpace.startsWith(s: QLatin1String("org.qt-project.")))
108 return QString();
109
110 // We take the namespace tail, starting from the last letter in namespace name.
111 // We drop any non digit characters.
112 const QChar dot(QLatin1Char('.'));
113 QString tail;
114 for (int i = nameSpace.size(); i > 0; --i) {
115 const QChar c = nameSpace.at(i: i - 1);
116 if (c.isDigit() || c == dot)
117 tail.prepend(c);
118
119 if (c.isLetter())
120 break;
121 }
122
123 if (!tail.startsWith(c: dot) && tail.count(c: dot) == 1) {
124 // The org.qt-project.qtquickcontrols2.5120 case,
125 // tail = 2.5120 here. We need to cut "2." here.
126 const int dotIndex = tail.indexOf(c: dot);
127 if (dotIndex > 0)
128 tail = tail.mid(position: dotIndex);
129 }
130
131 // Drop beginning dots
132 while (tail.startsWith(c: dot))
133 tail = tail.mid(position: 1);
134
135 // Drop ending dots
136 while (tail.endsWith(c: dot))
137 tail.chop(n: 1);
138
139 if (tail.count(c: dot) == 0) {
140 if (tail.size() > 5)
141 return tail;
142
143 // When we have 3 digits, we split it like: ABC -> A.B.C
144 // When we have 4 digits, we split it like: ABCD -> A.BC.D
145 // When we have 5 digits, we split it like: ABCDE -> A.BC.DE
146 const int major = tail.left(n: 1).toInt();
147 const int minor = tail.size() == 3
148 ? tail.mid(position: 1, n: 1).toInt() : tail.mid(position: 1, n: 2).toInt();
149 const int patch = tail.size() == 5
150 ? tail.right(n: 2).toInt() : tail.right(n: 1).toInt();
151
152 return QString::fromUtf8(utf8: "%1.%2.%3").arg(a: major).arg(a: minor).arg(a: patch);
153 }
154
155 return tail;
156}
157
158static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
159{
160 query->prepare(query: QString::fromLatin1(ba: "SELECT FilterAttributeId "
161 "FROM %1 "
162 "WHERE FilterAttributeId = ? "
163 "LIMIT 1").arg(a: tableName));
164 query->bindValue(pos: 0, val: attributeId);
165 query->exec();
166 return query->next(); // if we got a result it means it was used
167}
168
169static int filterDataCount(QSqlQuery *query, const QString &tableName)
170{
171 query->exec(query: QString::fromLatin1(ba: "SELECT COUNT(*) FROM"
172 "(SELECT DISTINCT * FROM %1)").arg(a: tableName));
173 query->next();
174 return query->value(i: 0).toInt();
175}
176
177QHelpDBReader::IndexTable QHelpDBReader::indexTable() const
178{
179 IndexTable table;
180 if (!m_query)
181 return table;
182
183 QMap<int, QString> attributeIds;
184 m_query->exec(query: QLatin1String("SELECT DISTINCT Id, Name FROM FilterAttributeTable ORDER BY Id"));
185 while (m_query->next())
186 attributeIds.insert(key: m_query->value(i: 0).toInt(), value: m_query->value(i: 1).toString());
187
188 // Maybe some are unused and specified erroneously in the named filter only,
189 // like it was in case of qtlocation.qch <= qt 5.9
190 QList<int> usedAttributeIds;
191 for (auto it = attributeIds.cbegin(), end = attributeIds.cend(); it != end; ++it) {
192 const int attributeId = it.key();
193 if (isAttributeUsed(query: m_query, tableName: QLatin1String("IndexFilterTable"), attributeId)
194 || isAttributeUsed(query: m_query, tableName: QLatin1String("ContentsFilterTable"), attributeId)
195 || isAttributeUsed(query: m_query, tableName: QLatin1String("FileFilterTable"), attributeId)) {
196 usedAttributeIds.append(t: attributeId);
197 }
198 }
199
200 bool legacy = false;
201 m_query->exec(query: QLatin1String("SELECT * FROM pragma_table_info('IndexTable') "
202 "WHERE name='ContextName'"));
203 if (m_query->next())
204 legacy = true;
205
206 const QString identifierColumnName = legacy
207 ? QLatin1String("ContextName")
208 : QLatin1String("Identifier");
209
210 const int usedAttributeCount = usedAttributeIds.size();
211
212 QMap<int, IndexItem> idToIndexItem;
213
214 m_query->exec(query: QString::fromLatin1(ba: "SELECT Name, %1, FileId, Anchor, Id "
215 "FROM IndexTable "
216 "ORDER BY Id").arg(a: identifierColumnName));
217 while (m_query->next()) {
218 IndexItem indexItem;
219 indexItem.name = m_query->value(i: 0).toString();
220 indexItem.identifier = m_query->value(i: 1).toString();
221 indexItem.fileId = m_query->value(i: 2).toInt();
222 indexItem.anchor = m_query->value(i: 3).toString();
223 const int indexId = m_query->value(i: 4).toInt();
224
225 idToIndexItem.insert(key: indexId, value: indexItem);
226 }
227
228 QMap<int, FileItem> idToFileItem;
229 QMap<int, int> originalFileIdToNewFileId;
230
231 int filesCount = 0;
232 m_query->exec(query: QLatin1String("SELECT "
233 "FileNameTable.FileId, "
234 "FileNameTable.Name, "
235 "FileNameTable.Title "
236 "FROM FileNameTable, FolderTable "
237 "WHERE FileNameTable.FolderId = FolderTable.Id "
238 "ORDER BY FileId"));
239 while (m_query->next()) {
240 const int fileId = m_query->value(i: 0).toInt();
241 FileItem fileItem;
242 fileItem.name = m_query->value(i: 1).toString();
243 fileItem.title = m_query->value(i: 2).toString();
244
245 idToFileItem.insert(key: fileId, value: fileItem);
246 originalFileIdToNewFileId.insert(key: fileId, value: filesCount);
247 ++filesCount;
248 }
249
250 QMap<int, ContentsItem> idToContentsItem;
251
252 m_query->exec(query: QLatin1String("SELECT Data, Id "
253 "FROM ContentsTable "
254 "ORDER BY Id"));
255 while (m_query->next()) {
256 ContentsItem contentsItem;
257 contentsItem.data = m_query->value(i: 0).toByteArray();
258 const int contentsId = m_query->value(i: 1).toInt();
259
260 idToContentsItem.insert(key: contentsId, value: contentsItem);
261 }
262
263 bool optimized = true;
264
265 if (usedAttributeCount) {
266 // May optimize only when all usedAttributes are attached to every
267 // index and file. It means the number of rows in the
268 // IndexTable multiplied by number of used attributes
269 // must equal the number of rows inside IndexFilterTable
270 // (yes, we have a combinatorial explosion of data in IndexFilterTable,
271 // which we want to optimize). The same with FileNameTable and
272 // FileFilterTable.
273
274 const bool mayOptimizeIndexTable
275 = filterDataCount(query: m_query, tableName: QLatin1String("IndexFilterTable"))
276 == idToIndexItem.size() * usedAttributeCount;
277 const bool mayOptimizeFileTable
278 = filterDataCount(query: m_query, tableName: QLatin1String("FileFilterTable"))
279 == idToFileItem.size() * usedAttributeCount;
280 const bool mayOptimizeContentsTable
281 = filterDataCount(query: m_query, tableName: QLatin1String("ContentsFilterTable"))
282 == idToContentsItem.size() * usedAttributeCount;
283 optimized = mayOptimizeIndexTable && mayOptimizeFileTable && mayOptimizeContentsTable;
284
285 if (!optimized) {
286 m_query->exec(query: QLatin1String(
287 "SELECT "
288 "IndexFilterTable.IndexId, "
289 "FilterAttributeTable.Name "
290 "FROM "
291 "IndexFilterTable, "
292 "FilterAttributeTable "
293 "WHERE "
294 "IndexFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
295 while (m_query->next()) {
296 const int indexId = m_query->value(i: 0).toInt();
297 auto it = idToIndexItem.find(key: indexId);
298 if (it != idToIndexItem.end())
299 it.value().filterAttributes.append(t: m_query->value(i: 1).toString());
300 }
301
302 m_query->exec(query: QLatin1String(
303 "SELECT "
304 "FileFilterTable.FileId, "
305 "FilterAttributeTable.Name "
306 "FROM "
307 "FileFilterTable, "
308 "FilterAttributeTable "
309 "WHERE "
310 "FileFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
311 while (m_query->next()) {
312 const int fileId = m_query->value(i: 0).toInt();
313 auto it = idToFileItem.find(key: fileId);
314 if (it != idToFileItem.end())
315 it.value().filterAttributes.append(t: m_query->value(i: 1).toString());
316 }
317
318 m_query->exec(query: QLatin1String(
319 "SELECT "
320 "ContentsFilterTable.ContentsId, "
321 "FilterAttributeTable.Name "
322 "FROM "
323 "ContentsFilterTable, "
324 "FilterAttributeTable "
325 "WHERE "
326 "ContentsFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
327 while (m_query->next()) {
328 const int contentsId = m_query->value(i: 0).toInt();
329 auto it = idToContentsItem.find(key: contentsId);
330 if (it != idToContentsItem.end())
331 it.value().filterAttributes.append(t: m_query->value(i: 1).toString());
332 }
333 }
334 }
335
336 // reindex fileId references
337 for (auto it = idToIndexItem.cbegin(), end = idToIndexItem.cend(); it != end; ++it) {
338 IndexItem item = it.value();
339 item.fileId = originalFileIdToNewFileId.value(key: item.fileId);
340 table.indexItems.append(t: item);
341 }
342
343 table.fileItems = idToFileItem.values();
344 table.contentsItems = idToContentsItem.values();
345
346 if (optimized) {
347 for (int attributeId : usedAttributeIds)
348 table.usedFilterAttributes.append(t: attributeIds.value(key: attributeId));
349 }
350
351 return table;
352}
353
354QList<QStringList> QHelpDBReader::filterAttributeSets() const
355{
356 QList<QStringList> result;
357 if (m_query) {
358 m_query->exec(query: QLatin1String(
359 "SELECT "
360 "FileAttributeSetTable.Id, "
361 "FilterAttributeTable.Name "
362 "FROM "
363 "FileAttributeSetTable, "
364 "FilterAttributeTable "
365 "WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
366 "ORDER BY FileAttributeSetTable.Id"));
367 int oldId = -1;
368 while (m_query->next()) {
369 const int id = m_query->value(i: 0).toInt();
370 if (id != oldId) {
371 result.append(t: QStringList());
372 oldId = id;
373 }
374 result.last().append(t: m_query->value(i: 1).toString());
375 }
376 }
377 return result;
378}
379
380QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
381 const QString &filePath) const
382{
383 QByteArray ba;
384 if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
385 return ba;
386
387 namespaceName();
388 m_query->prepare(query: QLatin1String(
389 "SELECT "
390 "FileDataTable.Data "
391 "FROM "
392 "FileDataTable, "
393 "FileNameTable, "
394 "FolderTable, "
395 "NamespaceTable "
396 "WHERE FileDataTable.Id = FileNameTable.FileId "
397 "AND (FileNameTable.Name = ? OR FileNameTable.Name = ?) "
398 "AND FileNameTable.FolderId = FolderTable.Id "
399 "AND FolderTable.Name = ? "
400 "AND FolderTable.NamespaceId = NamespaceTable.Id "
401 "AND NamespaceTable.Name = ?"));
402 m_query->bindValue(pos: 0, val: filePath);
403 m_query->bindValue(pos: 1, val: QString(QLatin1String("./") + filePath));
404 m_query->bindValue(pos: 2, val: virtualFolder);
405 m_query->bindValue(pos: 3, val: m_namespace);
406 m_query->exec();
407 if (m_query->next() && m_query->isValid())
408 ba = qUncompress(data: m_query->value(i: 0).toByteArray());
409 return ba;
410}
411
412QStringList QHelpDBReader::customFilters() const
413{
414 QStringList lst;
415 if (m_query) {
416 m_query->exec(query: QLatin1String("SELECT Name FROM FilterNameTable"));
417 while (m_query->next())
418 lst.append(t: m_query->value(i: 0).toString());
419 }
420 return lst;
421}
422
423QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
424{
425 QStringList lst;
426 if (m_query) {
427 if (filterName.isEmpty()) {
428 m_query->prepare(query: QLatin1String("SELECT Name FROM FilterAttributeTable"));
429 } else {
430 m_query->prepare(query: QLatin1String(
431 "SELECT "
432 "FilterAttributeTable.Name "
433 "FROM "
434 "FilterAttributeTable, "
435 "FilterTable, "
436 "FilterNameTable "
437 "WHERE FilterNameTable.Name = ? "
438 "AND FilterNameTable.Id = FilterTable.NameId "
439 "AND FilterTable.FilterAttributeId = FilterAttributeTable.Id"));
440 m_query->bindValue(pos: 0, val: filterName);
441 }
442 m_query->exec();
443 while (m_query->next())
444 lst.append(t: m_query->value(i: 0).toString());
445 }
446 return lst;
447}
448
449QMultiMap<QString, QByteArray> QHelpDBReader::filesData(const QStringList &filterAttributes,
450 const QString &extensionFilter) const
451{
452 QMultiMap<QString, QByteArray> result;
453 if (!m_query)
454 return result;
455
456 QString query;
457 QString extension;
458 if (!extensionFilter.isEmpty())
459 extension = QString(QLatin1String("AND FileNameTable.Name "
460 "LIKE \'%.%1\'")).arg(a: extensionFilter);
461
462 if (filterAttributes.isEmpty()) {
463 query = QString(QLatin1String("SELECT "
464 "FileNameTable.Name, "
465 "FileDataTable.Data "
466 "FROM "
467 "FolderTable, "
468 "FileNameTable, "
469 "FileDataTable "
470 "WHERE FileDataTable.Id = FileNameTable.FileId "
471 "AND FileNameTable.FolderId = FolderTable.Id %1"))
472 .arg(a: extension);
473 } else {
474 for (int i = 0; i < filterAttributes.size(); ++i) {
475 if (i > 0)
476 query.append(s: QLatin1String(" INTERSECT "));
477 query.append(s: QString(QLatin1String(
478 "SELECT "
479 "FileNameTable.Name, "
480 "FileDataTable.Data "
481 "FROM "
482 "FolderTable, "
483 "FileNameTable, "
484 "FileDataTable, "
485 "FileFilterTable, "
486 "FilterAttributeTable "
487 "WHERE FileDataTable.Id = FileNameTable.FileId "
488 "AND FileNameTable.FolderId = FolderTable.Id "
489 "AND FileNameTable.FileId = FileFilterTable.FileId "
490 "AND FileFilterTable.FilterAttributeId = FilterAttributeTable.Id "
491 "AND FilterAttributeTable.Name = \'%1\' %2"))
492 .arg(args: quote(string: filterAttributes.at(i)), args&: extension));
493 }
494 }
495 m_query->exec(query);
496 while (m_query->next())
497 result.insert(key: m_query->value(i: 0).toString(), value: qUncompress(data: m_query->value(i: 1).toByteArray()));
498
499 return result;
500}
501
502QVariant QHelpDBReader::metaData(const QString &name) const
503{
504 QVariant v;
505 if (!m_query)
506 return v;
507
508 m_query->prepare(query: QLatin1String("SELECT COUNT(Value), Value FROM MetaDataTable "
509 "WHERE Name=?"));
510 m_query->bindValue(pos: 0, val: name);
511 if (m_query->exec() && m_query->next()
512 && m_query->value(i: 0).toInt() == 1)
513 v = m_query->value(i: 1);
514 return v;
515}
516
517QString QHelpDBReader::quote(const QString &string) const
518{
519 QString s = string;
520 s.replace(c: QLatin1Char('\''), after: QLatin1String("\'\'"));
521 return s;
522}
523
524QT_END_NAMESPACE
525

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