Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhelpsearchindexwriter.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6#include "qhelp_global.h"
9
10#include <QtTools/qttools-config.h>
11#include <QtCore/qdatastream.h>
12#include <QtCore/qdatetime.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qstringconverter.h>
15#include <QtCore/qtextstream.h>
16#include <QtCore/qurl.h>
17#include <QtCore/qvariant.h>
18#if QT_CONFIG(fullqthelp)
19# include <QtGui/qtextdocument.h>
20#endif
21#include <QtSql/qsqldatabase.h>
22#include <QtSql/qsqldriver.h>
23#include <QtSql/qsqlerror.h>
24#include <QtSql/qsqlquery.h>
25
27
28using namespace Qt::StringLiterals;
29
30namespace fulltextsearch {
31
32const char FTS_DB_NAME[] = "fts";
33
34class Writer
35{
36public:
37 Writer(const QString &path);
39
40 bool tryInit(bool reindex);
41 void flush();
42
43 void removeNamespace(const QString &namespaceName);
44 bool hasNamespace(const QString &namespaceName);
45 void insertDoc(const QString &namespaceName,
46 const QString &attributes,
47 const QString &url,
48 const QString &title,
49 const QString &contents);
52
53private:
54 void init(bool reindex);
55 bool hasDB();
56 void clearLegacyIndex();
57
58 const QString m_dbDir;
59 QString m_uniqueId;
60
61 bool m_needOptimize = false;
62 QSqlDatabase m_db;
63 QVariantList m_namespaces;
64 QVariantList m_attributes;
65 QVariantList m_urls;
66 QVariantList m_titles;
67 QVariantList m_contents;
68};
69
70Writer::Writer(const QString &path)
71 : m_dbDir(path)
72{
73 clearLegacyIndex();
74 QDir().mkpath(m_dbDir);
75 m_uniqueId = QHelpGlobal::uniquifyConnectionName("QHelpWriter"_L1, this);
76 m_db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_uniqueId);
77 const QString dbPath = m_dbDir + u'/' + QLatin1StringView(FTS_DB_NAME);
78 m_db.setDatabaseName(dbPath);
79 if (!m_db.open()) {
80 const QString &error = QHelpSearchIndexWriter::tr(
81 "Cannot open database \"%1\" using connection \"%2\": %3")
82 .arg(dbPath, m_uniqueId, m_db.lastError().text());
83 qWarning("%s", qUtf8Printable(error));
84 m_db = {};
85 QSqlDatabase::removeDatabase(m_uniqueId);
86 m_uniqueId.clear();
87 } else {
89 }
90}
91
92bool Writer::tryInit(bool reindex)
93{
94 if (!m_db.isValid())
95 return true;
96
97 QSqlQuery query(m_db);
98 // HACK: we try to perform any modifying command just to check if
99 // we don't get SQLITE_BUSY code (SQLITE_BUSY is defined to 5 in sqlite driver)
100 if (!query.exec("CREATE TABLE foo ();"_L1) && query.lastError().nativeErrorCode() == "5"_L1) // db is locked
101 return false;
102
103 // HACK: clear what we have created
104 query.exec("DROP TABLE foo;"_L1);
105
106 init(reindex);
107 return true;
108}
109
110bool Writer::hasDB()
111{
112 if (!m_db.isValid())
113 return false;
114
115 QSqlQuery query(m_db);
116 query.prepare("SELECT id FROM info LIMIT 1"_L1);
117 query.exec();
118 return query.next();
119}
120
121void Writer::clearLegacyIndex()
122{
123 // Clear old legacy clucene index.
124 // More important in case of Creator, since
125 // the index folder is common for all Creator versions
126 QDir dir(m_dbDir);
127 if (!dir.exists())
128 return;
129
130 const QStringList &list = dir.entryList(QDir::Files | QDir::Hidden);
131 if (!list.contains(QLatin1StringView(FTS_DB_NAME))) {
132 for (const QString &item : list)
133 dir.remove(item);
134 }
135}
136
137void Writer::init(bool reindex)
138{
139 if (!m_db.isValid())
140 return;
141
142 QSqlQuery query(m_db);
143
144 if (reindex && hasDB()) {
145 m_needOptimize = true;
146
147 query.exec("DROP TABLE titles;"_L1);
148 query.exec("DROP TABLE contents;"_L1);
149 query.exec("DROP TABLE info;"_L1);
150 }
151
152 query.exec("CREATE TABLE info (id INTEGER PRIMARY KEY, namespace, attributes, url, title, data);"_L1);
153
154 query.exec("CREATE VIRTUAL TABLE titles USING fts5("
155 "namespace UNINDEXED, attributes UNINDEXED, "
156 "url UNINDEXED, title, "
157 "tokenize = 'porter unicode61', content = 'info', content_rowid='id');"_L1);
158 query.exec("CREATE TRIGGER titles_insert AFTER INSERT ON info BEGIN "
159 "INSERT INTO titles(rowid, namespace, attributes, url, title) "
160 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title); "
161 "END;"_L1);
162 query.exec("CREATE TRIGGER titles_delete AFTER DELETE ON info BEGIN "
163 "INSERT INTO titles(titles, rowid, namespace, attributes, url, title) "
164 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title); "
165 "END;"_L1);
166 query.exec("CREATE TRIGGER titles_update AFTER UPDATE ON info BEGIN "
167 "INSERT INTO titles(titles, rowid, namespace, attributes, url, title) "
168 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title); "
169 "INSERT INTO titles(rowid, namespace, attributes, url, title) "
170 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title); "
171 "END;"_L1);
172
173 query.exec("CREATE VIRTUAL TABLE contents USING fts5("
174 "namespace UNINDEXED, attributes UNINDEXED, "
175 "url UNINDEXED, title, data, "
176 "tokenize = 'porter unicode61', content = 'info', content_rowid='id');"_L1);
177 query.exec("CREATE TRIGGER contents_insert AFTER INSERT ON info BEGIN "
178 "INSERT INTO contents(rowid, namespace, attributes, url, title, data) "
179 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title, new.data); "
180 "END;"_L1);
181 query.exec("CREATE TRIGGER contents_delete AFTER DELETE ON info BEGIN "
182 "INSERT INTO contents(contents, rowid, namespace, attributes, url, title, data) "
183 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title, old.data); "
184 "END;"_L1);
185 query.exec("CREATE TRIGGER contents_update AFTER UPDATE ON info BEGIN "
186 "INSERT INTO contents(contents, rowid, namespace, attributes, url, title, data) "
187 "VALUES('delete', old.id, old.namespace, old.attributes, old.url, old.title, old.data); "
188 "INSERT INTO contents(rowid, namespace, attributes, url, title, data) "
189 "VALUES(new.id, new.namespace, new.attributes, new.url, new.title, new.data); "
190 "END;"_L1);
191}
192
194{
195 if (m_db.isValid())
196 m_db.close();
197 m_db = {};
198 if (!m_uniqueId.isEmpty())
199 QSqlDatabase::removeDatabase(m_uniqueId);
200}
201
203{
204 if (!m_db.isValid())
205 return;
206
207 QSqlQuery query(m_db);
208 query.prepare("INSERT INTO info (namespace, attributes, url, title, data) VALUES (?, ?, ?, ?, ?)"_L1);
209 query.addBindValue(m_namespaces);
210 query.addBindValue(m_attributes);
211 query.addBindValue(m_urls);
212 query.addBindValue(m_titles);
213 query.addBindValue(m_contents);
214 query.execBatch();
215
216 m_namespaces.clear();
217 m_attributes.clear();
218 m_urls.clear();
219 m_titles.clear();
220 m_contents.clear();
221}
222
223void Writer::removeNamespace(const QString &namespaceName)
224{
225 if (!m_db.isValid() || !hasNamespace(namespaceName)) // no data to delete
226 return;
227
228 m_needOptimize = true;
229 QSqlQuery query(m_db);
230 query.prepare("DELETE FROM info WHERE namespace = ?"_L1);
231 query.addBindValue(namespaceName);
232 query.exec();
233}
234
235bool Writer::hasNamespace(const QString &namespaceName)
236{
237 if (!m_db.isValid())
238 return false;
239
240 QSqlQuery query(m_db);
241 query.prepare("SELECT id FROM info WHERE namespace = ? LIMIT 1"_L1);
242 query.addBindValue(namespaceName);
243 query.exec();
244 return query.next();
245}
246
247void Writer::insertDoc(const QString &namespaceName,
248 const QString &attributes,
249 const QString &url,
250 const QString &title,
251 const QString &contents)
252{
253 m_namespaces.append(namespaceName);
254 m_attributes.append(attributes);
255 m_urls.append(url);
256 m_titles.append(title);
257 m_contents.append(contents);
258}
259
261{
262 if (!m_db.isValid())
263 return;
264
265 m_needOptimize = false;
266 if (m_db.driver()->hasFeature(QSqlDriver::Transactions))
267 m_db.transaction();
268}
269
271{
272 if (!m_db.isValid())
273 return;
274
275 QSqlQuery query(m_db);
276
277 if (m_needOptimize) {
278 query.exec("INSERT INTO titles(titles) VALUES('rebuild')"_L1);
279 query.exec("INSERT INTO contents(contents) VALUES('rebuild')"_L1);
280 }
281
282 if (m_db.driver()->hasFeature(QSqlDriver::Transactions))
283 m_db.commit();
284
285 if (m_needOptimize)
286 query.exec("VACUUM"_L1);
287}
288
289QHelpSearchIndexWriter::~QHelpSearchIndexWriter()
290{
291 m_mutex.lock();
292 this->m_cancel = true;
293 m_mutex.unlock();
294 wait();
295}
296
298{
299 QMutexLocker lock(&m_mutex);
300 m_cancel = true;
301}
302
303void QHelpSearchIndexWriter::updateIndex(const QString &collectionFile,
304 const QString &indexFilesFolder, bool reindex)
305{
306 wait();
307 QMutexLocker lock(&m_mutex);
308
309 m_cancel = false;
310 m_reindex = reindex;
311 m_collectionFile = collectionFile;
312 m_indexFilesFolder = indexFilesFolder;
313
314 lock.unlock();
315
316 start(QThread::LowestPriority);
317}
318
319static const char IndexedNamespacesKey[] = "FTS5IndexedNamespaces";
320
321static QMap<QString, QDateTime> readIndexMap(const QHelpEngineCore &engine)
322{
323 QMap<QString, QDateTime> indexMap;
324 QDataStream dataStream(
325 engine.customValue(QLatin1StringView(IndexedNamespacesKey)).toByteArray());
326 dataStream >> indexMap;
327 return indexMap;
328}
329
337
342
344{
345 QMutexLocker lock(&m_mutex);
346
347 if (m_cancel)
348 return;
349
350 const bool reindex(m_reindex);
351 const QString collectionFile(m_collectionFile);
352 const QString indexPath(m_indexFilesFolder);
353
354 lock.unlock();
355
356 QHelpEngineCore engine(collectionFile, nullptr);
357 if (!engine.setupData())
358 return;
359
360 if (reindex)
361 clearIndexMap(&engine);
362
363 emit indexingStarted();
364
365 Writer writer(indexPath);
366
367 while (!writer.tryInit(reindex))
368 sleep(1);
369
370 const QStringList &registeredDocs = engine.registeredDocumentations();
371 QMap<QString, QDateTime> indexMap = readIndexMap(engine);
372
373 if (!reindex) {
374 for (const QString &namespaceName : registeredDocs) {
375 const auto it = indexMap.constFind(namespaceName);
376 if (it != indexMap.constEnd()) {
377 const QString path = engine.documentationFileName(namespaceName);
378 if (*it < QFileInfo(path).lastModified()) {
379 // Remove some outdated indexed stuff
380 indexMap.erase(it);
381 writer.removeNamespace(namespaceName);
382 } else if (!writer.hasNamespace(namespaceName)) {
383 // No data in fts db for namespace.
384 // The namespace could have been removed from fts db
385 // or the whole fts db have been removed
386 // without removing it from indexMap.
387 indexMap.erase(it);
388 }
389 } else {
390 // Needed in case namespaceName was removed from indexMap
391 // without removing it from fts db.
392 // May happen when e.g. qch file was removed manually
393 // without removing fts db.
394 writer.removeNamespace(namespaceName);
395 }
396 // TODO: we may also detect if there are any other data
397 // and remove it
398 }
399 } else {
400 indexMap.clear();
401 }
402
403 auto it = indexMap.begin();
404 while (it != indexMap.end()) {
405 if (!registeredDocs.contains(it.key())) {
406 writer.removeNamespace(it.key());
407 it = indexMap.erase(it);
408 } else {
409 ++it;
410 }
411 }
412
413 for (const QString &namespaceName : registeredDocs) {
414 lock.relock();
415 if (m_cancel) {
416 // store what we have done so far
417 writeIndexMap(&engine, indexMap);
418 writer.endTransaction();
419 emit indexingFinished();
420 return;
421 }
422 lock.unlock();
423
424 // if indexed, continue
425 if (indexMap.contains(namespaceName))
426 continue;
427
428 const QString fileName = engine.documentationFileName(namespaceName);
429 QHelpDBReader reader(fileName, QHelpGlobal::uniquifyConnectionName(
430 fileName, this), nullptr);
431 if (!reader.init())
432 continue;
433
434 const QString virtualFolder = reader.virtualFolder();
435
436 const QList<QStringList> &attributeSets =
437 engine.filterAttributeSets(namespaceName);
438
439 for (const QStringList &attributes : attributeSets) {
440 const QString &attributesString = attributes.join(u'|');
441
442 const auto htmlFiles = reader.filesData(attributes, "html"_L1);
443 const auto htmFiles = reader.filesData(attributes, "htm"_L1);
444 const auto txtFiles = reader.filesData(attributes, "txt"_L1);
445
446 auto files = htmlFiles;
447 files.unite(htmFiles);
448 files.unite(txtFiles);
449
450 for (auto it = files.cbegin(), end = files.cend(); it != end ; ++it) {
451 lock.relock();
452 if (m_cancel) {
453 // store what we have done so far
454 writeIndexMap(&engine, indexMap);
455 writer.endTransaction();
456 emit indexingFinished();
457 return;
458 }
459 lock.unlock();
460
461 const QString &file = it.key();
462 const QByteArray &data = it.value();
463
464 if (data.isEmpty())
465 continue;
466
467 QUrl url;
468 url.setScheme("qthelp"_L1);
469 url.setAuthority(namespaceName);
470 url.setPath(u'/' + virtualFolder + u'/' + file);
471
472 if (url.hasFragment())
473 url.setFragment({});
474
475 const QString &fullFileName = url.toString();
476 if (!fullFileName.endsWith(".html"_L1) && !fullFileName.endsWith(".htm"_L1)
477 && !fullFileName.endsWith(".txt"_L1)) {
478 continue;
479 }
480
481 QTextStream s(data);
482 auto encoding = QStringDecoder::encodingForHtml(data);
483 if (encoding)
484 s.setEncoding(*encoding);
485
486 const QString &text = s.readAll();
487 if (text.isEmpty())
488 continue;
489
490 QString title;
491 QString contents;
492 if (fullFileName.endsWith(".txt"_L1)) {
493 title = fullFileName.mid(fullFileName.lastIndexOf(u'/') + 1);
494 contents = text.toHtmlEscaped();
495#if QT_CONFIG(fullqthelp)
496 } else {
497 QTextDocument doc;
498 doc.setHtml(text);
499
500 title = doc.metaInformation(QTextDocument::DocumentTitle).toHtmlEscaped();
501 contents = doc.toPlainText().toHtmlEscaped();
502#endif
503 }
504
505 writer.insertDoc(namespaceName, attributesString, fullFileName, title, contents);
506 }
507 }
508 writer.flush();
509 const QString &path = engine.documentationFileName(namespaceName);
510 indexMap.insert(namespaceName, QFileInfo(path).lastModified());
511 }
512
513 writeIndexMap(&engine, indexMap);
514
515 writer.endTransaction();
516 emit indexingFinished();
517}
518
519} // namespace fulltextsearch
520
521QT_END_NAMESPACE
The QHelpEngineCore class provides the core functionality of the help system.
void updateIndex(const QString &collectionFile, const QString &indexFilesFolder, bool reindex)
void removeNamespace(const QString &namespaceName)
void insertDoc(const QString &namespaceName, const QString &attributes, const QString &url, const QString &title, const QString &contents)
bool hasNamespace(const QString &namespaceName)
Combined button and popup list for selecting options.
static bool writeIndexMap(QHelpEngineCore *engine, const QMap< QString, QDateTime > &indexMap)
static const char IndexedNamespacesKey[]
static bool clearIndexMap(QHelpEngineCore *engine)
static QMap< QString, QDateTime > readIndexMap(const QHelpEngineCore &engine)