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