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 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | QHelpDBReader::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 | |
24 | QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId, |
25 | QObject *parent) |
26 | : QObject(parent), |
27 | m_dbName(dbName), |
28 | m_uniqueId(uniqueId) |
29 | { |
30 | } |
31 | |
32 | QHelpDBReader::~QHelpDBReader() |
33 | { |
34 | if (m_initDone) { |
35 | delete m_query; |
36 | QSqlDatabase::removeDatabase(connectionName: m_uniqueId); |
37 | } |
38 | } |
39 | |
40 | bool 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 | |
59 | bool 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 | |
74 | QString 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 | |
86 | QString 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 | |
96 | QString 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 | |
104 | QString 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 | |
158 | static 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 | |
169 | static 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 | |
177 | QHelpDBReader::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 | |
354 | QList<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 | |
380 | QByteArray 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 | |
412 | QStringList 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 | |
423 | QStringList 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 | |
449 | QMultiMap<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 | |
502 | QVariant 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 | |
517 | QString QHelpDBReader::quote(const QString &string) const |
518 | { |
519 | QString s = string; |
520 | s.replace(c: QLatin1Char('\''), after: QLatin1String("\'\'" )); |
521 | return s; |
522 | } |
523 | |
524 | QT_END_NAMESPACE |
525 | |