1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Assistant of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qhelpenginecore.h"
41 #include "qhelpfilterengine.h"
42 #include "qhelpsearchindexreader_default_p.h"
43 
44 #include <QtCore/QSet>
45 #include <QtSql/QSqlDatabase>
46 #include <QtSql/QSqlQuery>
47 
48 QT_BEGIN_NAMESPACE
49 
50 namespace fulltextsearch {
51 namespace qt {
52 
setIndexPath(const QString & path)53 void Reader::setIndexPath(const QString &path)
54 {
55     m_indexPath = path;
56     m_namespaceAttributes.clear();
57     m_filterEngineNamespaceList.clear();
58     m_useFilterEngine = false;
59 }
60 
addNamespaceAttributes(const QString & namespaceName,const QStringList & attributes)61 void Reader::addNamespaceAttributes(const QString &namespaceName, const QStringList &attributes)
62 {
63     m_namespaceAttributes.insert(namespaceName, attributes);
64 }
65 
setFilterEngineNamespaceList(const QStringList & namespaceList)66 void Reader::setFilterEngineNamespaceList(const QStringList &namespaceList)
67 {
68     m_useFilterEngine = true;
69     m_filterEngineNamespaceList = namespaceList;
70 }
71 
namespacePlaceholders(const QMultiMap<QString,QStringList> & namespaces)72 static QString namespacePlaceholders(const QMultiMap<QString, QStringList> &namespaces)
73 {
74     QString placeholders;
75     const auto &namespaceList = namespaces.uniqueKeys();
76     bool firstNS = true;
77     for (const QString &ns : namespaceList) {
78         if (firstNS)
79             firstNS = false;
80         else
81             placeholders += QLatin1String(" OR ");
82         placeholders += QLatin1String("(namespace = ?");
83 
84         const QList<QStringList> &attributeSets = namespaces.values(ns);
85         bool firstAS = true;
86         for (const QStringList &attributeSet : attributeSets) {
87             if (!attributeSet.isEmpty()) {
88                 if (firstAS) {
89                     firstAS = false;
90                     placeholders += QLatin1String(" AND (");
91                 } else {
92                     placeholders += QLatin1String(" OR ");
93                 }
94                 placeholders += QLatin1String("attributes = ?");
95             }
96         }
97         if (!firstAS)
98             placeholders += QLatin1Char(')'); // close "AND ("
99         placeholders += QLatin1Char(')');
100     }
101     return placeholders;
102 }
103 
bindNamespacesAndAttributes(QSqlQuery * query,const QMultiMap<QString,QStringList> & namespaces)104 static void bindNamespacesAndAttributes(QSqlQuery *query, const QMultiMap<QString, QStringList> &namespaces)
105 {
106     const auto &namespaceList = namespaces.uniqueKeys();
107     for (const QString &ns : namespaceList) {
108         query->addBindValue(ns);
109 
110         const QList<QStringList> &attributeSets = namespaces.values(ns);
111         for (const QStringList &attributeSet : attributeSets) {
112             if (!attributeSet.isEmpty())
113                 query->addBindValue(attributeSet.join(QLatin1Char('|')));
114         }
115     }
116 }
117 
namespacePlaceholders(const QStringList & namespaceList)118 static QString namespacePlaceholders(const QStringList &namespaceList)
119 {
120     QString placeholders;
121     bool firstNS = true;
122     for (int i = namespaceList.count(); i; --i) {
123         if (firstNS)
124             firstNS = false;
125         else
126             placeholders += QLatin1String(" OR ");
127         placeholders += QLatin1String("namespace = ?");
128     }
129     return placeholders;
130 }
131 
bindNamespacesAndAttributes(QSqlQuery * query,const QStringList & namespaceList)132 static void bindNamespacesAndAttributes(QSqlQuery *query, const QStringList &namespaceList)
133 {
134     for (const QString &ns : namespaceList)
135         query->addBindValue(ns);
136 }
137 
queryTable(const QSqlDatabase & db,const QString & tableName,const QString & searchInput) const138 QVector<QHelpSearchResult> Reader::queryTable(const QSqlDatabase &db,
139                                          const QString &tableName,
140                                          const QString &searchInput) const
141 {
142     const QString nsPlaceholders = m_useFilterEngine
143             ? namespacePlaceholders(m_filterEngineNamespaceList)
144             : namespacePlaceholders(m_namespaceAttributes);
145     QSqlQuery query(db);
146     query.prepare(QLatin1String("SELECT url, title, snippet(") + tableName +
147                   QLatin1String(", -1, '<b>', '</b>', '...', '10') FROM ") + tableName +
148                   QLatin1String(" WHERE (") + nsPlaceholders +
149                   QLatin1String(") AND ") + tableName +
150                   QLatin1String(" MATCH ? ORDER BY rank"));
151     m_useFilterEngine
152             ? bindNamespacesAndAttributes(&query, m_filterEngineNamespaceList)
153             : bindNamespacesAndAttributes(&query, m_namespaceAttributes);
154     query.addBindValue(searchInput);
155     query.exec();
156 
157     QVector<QHelpSearchResult> results;
158 
159     while (query.next()) {
160         const QString &url = query.value(QLatin1String("url")).toString();
161         const QString &title = query.value(QLatin1String("title")).toString();
162         const QString &snippet = query.value(2).toString();
163         results.append(QHelpSearchResult(url, title, snippet));
164     }
165 
166     return results;
167 }
168 
searchInDB(const QString & searchInput)169 void Reader::searchInDB(const QString &searchInput)
170 {
171     const QString &uniqueId = QHelpGlobal::uniquifyConnectionName(QLatin1String("QHelpReader"), this);
172     {
173         QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), uniqueId);
174         db.setConnectOptions(QLatin1String("QSQLITE_OPEN_READONLY"));
175         db.setDatabaseName(m_indexPath + QLatin1String("/fts"));
176 
177         if (db.open()) {
178             const QVector<QHelpSearchResult> titleResults = queryTable(db,
179                                              QLatin1String("titles"), searchInput);
180             const QVector<QHelpSearchResult> contentResults = queryTable(db,
181                                              QLatin1String("contents"), searchInput);
182 
183             // merge results form title and contents searches
184             m_searchResults = QVector<QHelpSearchResult>();
185 
186             QSet<QUrl> urls;
187 
188             for (const QHelpSearchResult &result : titleResults) {
189                 const QUrl &url = result.url();
190                 if (!urls.contains(url)) {
191                     urls.insert(url);
192                     m_searchResults.append(result);
193                 }
194             }
195 
196             for (const QHelpSearchResult &result : contentResults) {
197                 const QUrl &url = result.url();
198                 if (!urls.contains(url)) {
199                     urls.insert(url);
200                     m_searchResults.append(result);
201                 }
202             }
203         }
204     }
205     QSqlDatabase::removeDatabase(uniqueId);
206 }
207 
searchResults() const208 QVector<QHelpSearchResult> Reader::searchResults() const
209 {
210     return m_searchResults;
211 }
212 
attributesMatchFilter(const QStringList & attributes,const QStringList & filter)213 static bool attributesMatchFilter(const QStringList &attributes,
214                                   const QStringList &filter)
215 {
216     for (const QString &attribute : filter) {
217         if (!attributes.contains(attribute, Qt::CaseInsensitive))
218             return false;
219     }
220 
221     return true;
222 }
223 
run()224 void QHelpSearchIndexReaderDefault::run()
225 {
226     QMutexLocker lock(&m_mutex);
227 
228     if (m_cancel)
229         return;
230 
231     const QString searchInput = m_searchInput;
232     const QString collectionFile = m_collectionFile;
233     const QString indexPath = m_indexFilesFolder;
234     const bool usesFilterEngine = m_usesFilterEngine;
235 
236     lock.unlock();
237 
238     QHelpEngineCore engine(collectionFile, nullptr);
239     if (!engine.setupData())
240         return;
241 
242     emit searchingStarted();
243 
244     // setup the reader
245     m_reader.setIndexPath(indexPath);
246 
247     if (usesFilterEngine) {
248         m_reader.setFilterEngineNamespaceList(
249                     engine.filterEngine()->namespacesForFilter(
250                         engine.filterEngine()->activeFilter()));
251     } else {
252         const QStringList &registeredDocs = engine.registeredDocumentations();
253         const QStringList &currentFilter = engine.filterAttributes(engine.currentFilter());
254 
255         for (const QString &namespaceName : registeredDocs) {
256             const QList<QStringList> &attributeSets =
257                     engine.filterAttributeSets(namespaceName);
258 
259             for (const QStringList &attributes : attributeSets) {
260                 if (attributesMatchFilter(attributes, currentFilter)) {
261                     m_reader.addNamespaceAttributes(namespaceName, attributes);
262                 }
263             }
264         }
265     }
266 
267     lock.relock();
268     if (m_cancel) {
269         emit searchingFinished(0);   // TODO: check this, speed issue while locking???
270         return;
271     }
272     lock.unlock();
273 
274     m_searchResults.clear();
275     m_reader.searchInDB(searchInput);    // TODO: should this be interruptible as well ???
276 
277     lock.relock();
278     m_searchResults = m_reader.searchResults();
279     lock.unlock();
280 
281     emit searchingFinished(m_searchResults.count());
282 }
283 
284 }   // namespace std
285 }   // namespace fulltextsearch
286 
287 QT_END_NAMESPACE
288