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 "qhelpsearchengine.h"
42 #include "qhelpsearchquerywidget.h"
43 #include "qhelpsearchresultwidget.h"
44 
45 #include "qhelpsearchindexreader_p.h"
46 #include "qhelpsearchindexreader_default_p.h"
47 #include "qhelpsearchindexwriter_default_p.h"
48 
49 #include <QtCore/QDir>
50 #include <QtCore/QFile>
51 #include <QtCore/QFileInfo>
52 #include <QtCore/QVariant>
53 #include <QtCore/QThread>
54 #include <QtCore/QPointer>
55 #include <QtCore/QTimer>
56 
57 QT_BEGIN_NAMESPACE
58 
59 using namespace fulltextsearch::qt;
60 
61 class QHelpSearchResultData : public QSharedData
62 {
63 public:
64     QUrl m_url;
65     QString m_title;
66     QString m_snippet;
67 };
68 
69 /*!
70     \class QHelpSearchResult
71     \since 5.9
72     \inmodule QtHelp
73     \brief The QHelpSearchResult class provides the data associated with the
74     search result.
75 
76     The QHelpSearchResult object is a data object that describes a single search result.
77     The vector of search result objects is returned by QHelpSearchEngine::searchResults().
78     The description of the search result contains the document title and URL
79     that the search input matched. It also contains the snippet from
80     the document content containing the best match of the search input.
81     \sa QHelpSearchEngine
82 */
83 
84 /*!
85     Constructs a new empty QHelpSearchResult.
86 */
QHelpSearchResult()87 QHelpSearchResult::QHelpSearchResult()
88     : d(new QHelpSearchResultData)
89 {
90 }
91 
92 /*!
93     Constructs a copy of \a other.
94 */
QHelpSearchResult(const QHelpSearchResult & other)95 QHelpSearchResult::QHelpSearchResult(const QHelpSearchResult &other)
96     : d(other.d)
97 {
98 }
99 
100 /*!
101     Constructs the search result containing \a url, \a title and \a snippet
102     as the description of the result.
103 */
QHelpSearchResult(const QUrl & url,const QString & title,const QString & snippet)104 QHelpSearchResult::QHelpSearchResult(const QUrl &url, const QString &title, const QString &snippet)
105     : d(new QHelpSearchResultData)
106 {
107     d->m_url = url;
108     d->m_title = title;
109     d->m_snippet = snippet;
110 }
111 
112 /*!
113     Destroys the search result.
114 */
~QHelpSearchResult()115 QHelpSearchResult::~QHelpSearchResult()
116 {
117 }
118 
119 /*!
120     Assigns \a other to this search result and returns a reference to this search result.
121 */
operator =(const QHelpSearchResult & other)122 QHelpSearchResult &QHelpSearchResult::operator=(const QHelpSearchResult &other)
123 {
124     d = other.d;
125     return *this;
126 }
127 
128 /*!
129     Returns the document title of the search result.
130 */
title() const131 QString QHelpSearchResult::title() const
132 {
133     return d->m_title;
134 }
135 
136 /*!
137     Returns the document URL of the search result.
138 */
url() const139 QUrl QHelpSearchResult::url() const
140 {
141     return d->m_url;
142 }
143 
144 /*!
145     Returns the document snippet containing the search phrase of the search result.
146 */
snippet() const147 QString QHelpSearchResult::snippet() const
148 {
149     return d->m_snippet;
150 }
151 
152 
153 class QHelpSearchEnginePrivate : public QObject
154 {
155     Q_OBJECT
156 
157 signals:
158     void indexingStarted();
159     void indexingFinished();
160 
161     void searchingStarted();
162     void searchingFinished(int searchResultCount);
163 
164 private:
QHelpSearchEnginePrivate(QHelpEngineCore * helpEngine)165     QHelpSearchEnginePrivate(QHelpEngineCore *helpEngine)
166         : helpEngine(helpEngine)
167     {
168     }
169 
~QHelpSearchEnginePrivate()170     ~QHelpSearchEnginePrivate()
171     {
172         delete indexReader;
173         delete indexWriter;
174     }
175 
searchResultCount() const176     int searchResultCount() const
177     {
178         return indexReader ? indexReader->searchResultCount() : 0;
179     }
180 
searchResults(int start,int end) const181     QVector<QHelpSearchResult> searchResults(int start, int end) const
182     {
183         return indexReader ?
184                indexReader->searchResults(start, end) :
185                QVector<QHelpSearchResult>();
186     }
187 
updateIndex(bool reindex=false)188     void updateIndex(bool reindex = false)
189     {
190         if (helpEngine.isNull())
191             return;
192 
193         if (!QFile::exists(QFileInfo(helpEngine->collectionFile()).path()))
194             return;
195 
196         if (!indexWriter) {
197             indexWriter = new QHelpSearchIndexWriter();
198 
199             connect(indexWriter, &QHelpSearchIndexWriter::indexingStarted,
200                     this, &QHelpSearchEnginePrivate::indexingStarted);
201             connect(indexWriter, &QHelpSearchIndexWriter::indexingFinished,
202                     this, &QHelpSearchEnginePrivate::indexingFinished);
203         }
204 
205         indexWriter->cancelIndexing();
206         indexWriter->updateIndex(helpEngine->collectionFile(),
207                                  indexFilesFolder(), reindex);
208     }
209 
cancelIndexing()210     void cancelIndexing()
211     {
212         if (indexWriter)
213             indexWriter->cancelIndexing();
214     }
215 
search(const QString & searchInput)216     void search(const QString &searchInput)
217     {
218         if (helpEngine.isNull())
219             return;
220 
221         if (!QFile::exists(QFileInfo(helpEngine->collectionFile()).path()))
222             return;
223 
224         if (!indexReader) {
225             indexReader = new QHelpSearchIndexReaderDefault();
226             connect(indexReader, &fulltextsearch::QHelpSearchIndexReader::searchingStarted,
227                     this, &QHelpSearchEnginePrivate::searchingStarted);
228             connect(indexReader, &fulltextsearch::QHelpSearchIndexReader::searchingFinished,
229                     this, &QHelpSearchEnginePrivate::searchingFinished);
230         }
231 
232         m_searchInput = searchInput;
233         indexReader->cancelSearching();
234         indexReader->search(helpEngine->collectionFile(), indexFilesFolder(),
235                             searchInput, helpEngine->usesFilterEngine());
236     }
237 
cancelSearching()238     void cancelSearching()
239     {
240         if (indexReader)
241             indexReader->cancelSearching();
242     }
243 
indexFilesFolder() const244     QString indexFilesFolder() const
245     {
246         QString indexFilesFolder = QLatin1String(".fulltextsearch");
247         if (helpEngine && !helpEngine->collectionFile().isEmpty()) {
248             QFileInfo fi(helpEngine->collectionFile());
249             indexFilesFolder = fi.absolutePath() + QDir::separator()
250                 + QLatin1Char('.')
251                 + fi.fileName().left(fi.fileName().lastIndexOf(QLatin1String(".qhc")));
252         }
253         return indexFilesFolder;
254     }
255 
256 private:
257     friend class QHelpSearchEngine;
258 
259     bool m_isIndexingScheduled = false;
260 
261     QHelpSearchQueryWidget *queryWidget = nullptr;
262     QHelpSearchResultWidget *resultWidget = nullptr;
263 
264     fulltextsearch::QHelpSearchIndexReader *indexReader = nullptr;
265     QHelpSearchIndexWriter *indexWriter = nullptr;
266 
267     QPointer<QHelpEngineCore> helpEngine;
268 
269     QString m_searchInput;
270 };
271 
272 #include "qhelpsearchengine.moc"
273 
274 /*!
275     \class QHelpSearchQuery
276     \obsolete
277     \since 4.4
278     \inmodule QtHelp
279     \brief The QHelpSearchQuery class contains the field name and the associated
280     search term.
281 
282     The QHelpSearchQuery class contains the field name and the associated search
283     term. Depending on the field the search term might get split up into separate
284     terms to be parsed differently by the search engine.
285 
286     \note This class has been deprecated in favor of QString.
287 
288     \sa QHelpSearchQueryWidget
289 */
290 
291 /*!
292     \fn QHelpSearchQuery::QHelpSearchQuery()
293 
294     Constructs a new empty QHelpSearchQuery.
295 */
296 
297 /*!
298     \fn QHelpSearchQuery::QHelpSearchQuery(FieldName field, const QStringList &wordList)
299 
300     Constructs a new QHelpSearchQuery and initializes it with the given \a field and \a wordList.
301 */
302 
303 /*!
304     \enum QHelpSearchQuery::FieldName
305     This enum type specifies the field names that are handled by the search engine.
306 
307     \value DEFAULT  the default field provided by the search widget, several terms should be
308                     split and stored in the word list except search terms enclosed in quotes.
309     \value FUZZY    \obsolete Terms should be split in separate
310                     words and passed to the search engine.
311     \value WITHOUT  \obsolete  Terms should be split in separate
312                     words and passed to the search engine.
313     \value PHRASE   \obsolete  Terms should not be split in separate words.
314     \value ALL      \obsolete  Terms should be split in separate
315                     words and passed to the search engine
316     \value ATLEAST  \obsolete  Terms should be split in separate
317                     words and passed to the search engine
318 */
319 
320 /*!
321     \class QHelpSearchEngine
322     \since 4.4
323     \inmodule QtHelp
324     \brief The QHelpSearchEngine class provides access to widgets reusable
325     to integrate fulltext search as well as to index and search documentation.
326 
327     Before the search engine can be used, one has to instantiate at least a
328     QHelpEngineCore object that needs to be passed to the search engines constructor.
329     This is required as the search engine needs to be connected to the help
330     engines setupFinished() signal to know when it can start to index documentation.
331 
332     After starting the indexing process the signal indexingStarted() is emitted and
333     on the end of the indexing process the indexingFinished() is emitted. To stop
334     the indexing one can call cancelIndexing().
335 
336     When the indexing process has finished, the search engine can be used to
337     search through the index for a given term using the search() function. When
338     the search input is passed to the search engine, the searchingStarted()
339     signal is emitted. When the search finishes, the searchingFinished() signal
340     is emitted. The search process can be stopped by calling cancelSearching().
341 
342     If the search succeeds, searchingFinished() is called with the search result
343     count to fetch the search results from the search engine. Calling the
344     searchResults() function with a range returns a list of QHelpSearchResult
345     objects within the range. The results consist of the document title and URL,
346     as well as a snippet from the document that contains the best match for the
347     search input.
348 
349     To display the given search results use the QHelpSearchResultWidget or build up your own one if you need
350     more advanced functionality. Note that the QHelpSearchResultWidget can not be instantiated
351     directly, you must retrieve the widget from the search engine in use as all connections will be
352     established for you by the widget itself.
353 */
354 
355 /*!
356     \fn void QHelpSearchEngine::indexingStarted()
357 
358     This signal is emitted when indexing process is started.
359 */
360 
361 /*!
362     \fn void QHelpSearchEngine::indexingFinished()
363 
364     This signal is emitted when the indexing process is complete.
365 */
366 
367 /*!
368     \fn void QHelpSearchEngine::searchingStarted()
369 
370     This signal is emitted when the search process is started.
371 */
372 
373 /*!
374     \fn void QHelpSearchEngine::searchingFinished(int searchResultCount)
375 
376     This signal is emitted when the search process is complete.
377     The search result count is stored in \a searchResultCount.
378 */
379 
380 /*!
381     Constructs a new search engine with the given \a parent. The search engine
382     uses the given \a helpEngine to access the documentation that needs to be indexed.
383     The QHelpEngine's setupFinished() signal is automatically connected to the
384     QHelpSearchEngine's indexing function, so that new documentation will be indexed
385     after the signal is emitted.
386 */
QHelpSearchEngine(QHelpEngineCore * helpEngine,QObject * parent)387 QHelpSearchEngine::QHelpSearchEngine(QHelpEngineCore *helpEngine, QObject *parent)
388     : QObject(parent)
389 {
390     d = new QHelpSearchEnginePrivate(helpEngine);
391 
392     connect(helpEngine, &QHelpEngineCore::setupFinished,
393             this, &QHelpSearchEngine::scheduleIndexDocumentation);
394 
395     connect(d, &QHelpSearchEnginePrivate::indexingStarted,
396             this, &QHelpSearchEngine::indexingStarted);
397     connect(d, &QHelpSearchEnginePrivate::indexingFinished,
398             this, &QHelpSearchEngine::indexingFinished);
399     connect(d, &QHelpSearchEnginePrivate::searchingStarted,
400             this, &QHelpSearchEngine::searchingStarted);
401     connect(d, &QHelpSearchEnginePrivate::searchingFinished,
402             this, &QHelpSearchEngine::searchingFinished);
403 }
404 
405 /*!
406     Destructs the search engine.
407 */
~QHelpSearchEngine()408 QHelpSearchEngine::~QHelpSearchEngine()
409 {
410     delete d;
411 }
412 
413 /*!
414     Returns a widget to use as input widget. Depending on your search engine
415     configuration you will get a different widget with more or less subwidgets.
416 */
queryWidget()417 QHelpSearchQueryWidget* QHelpSearchEngine::queryWidget()
418 {
419     if (!d->queryWidget)
420         d->queryWidget = new QHelpSearchQueryWidget();
421 
422     return d->queryWidget;
423 }
424 
425 /*!
426     Returns a widget that can hold and display the search results.
427 */
resultWidget()428 QHelpSearchResultWidget* QHelpSearchEngine::resultWidget()
429 {
430     if (!d->resultWidget)
431         d->resultWidget = new QHelpSearchResultWidget(this);
432 
433     return d->resultWidget;
434 }
435 
436 /*!
437     \obsolete
438     Use searchResultCount() instead.
439 */
hitsCount() const440 int QHelpSearchEngine::hitsCount() const
441 {
442     return d->searchResultCount();
443 }
444 
445 /*!
446     \since 4.6
447     \obsolete
448     Use searchResultCount() instead.
449 */
hitCount() const450 int QHelpSearchEngine::hitCount() const
451 {
452     return d->searchResultCount();
453 }
454 
455 /*!
456     \since 5.9
457     Returns the number of results the search engine found.
458 */
searchResultCount() const459 int QHelpSearchEngine::searchResultCount() const
460 {
461     return d->searchResultCount();
462 }
463 
464 /*!
465     \typedef QHelpSearchEngine::SearchHit
466     \obsolete
467 
468     Use QHelpSearchResult instead.
469 
470     Typedef for QPair<QString, QString>.
471     The values of that pair are the documentation file path and the page title.
472 
473     \sa hits()
474 */
475 
476 /*!
477     \obsolete
478     Use searchResults() instead.
479 */
hits(int start,int end) const480 QList<QHelpSearchEngine::SearchHit> QHelpSearchEngine::hits(int start, int end) const
481 {
482     QList<QHelpSearchEngine::SearchHit> hits;
483     for (const QHelpSearchResult &result : searchResults(start, end))
484         hits.append(qMakePair(result.url().toString(), result.title()));
485     return hits;
486 }
487 
488 /*!
489     \since 5.9
490     Returns a list of search results within the range from the index
491     specified by \a start to the index specified by \a end.
492 */
searchResults(int start,int end) const493 QVector<QHelpSearchResult> QHelpSearchEngine::searchResults(int start, int end) const
494 {
495     return d->searchResults(start, end);
496 }
497 
498 /*!
499     \since 5.9
500     Returns the phrase that was last searched for.
501 */
searchInput() const502 QString QHelpSearchEngine::searchInput() const
503 {
504     return d->m_searchInput;
505 }
506 
507 /*!
508     \obsolete
509     \since 4.5
510     Use searchInput() instead.
511 */
query() const512 QList<QHelpSearchQuery> QHelpSearchEngine::query() const
513 {
514     return QList<QHelpSearchQuery>() << QHelpSearchQuery(QHelpSearchQuery::DEFAULT,
515            d->m_searchInput.split(QChar::Space));
516 }
517 
518 /*!
519     Forces the search engine to reindex all documentation files.
520 */
reindexDocumentation()521 void QHelpSearchEngine::reindexDocumentation()
522 {
523     d->updateIndex(true);
524 }
525 
526 /*!
527     Stops the indexing process.
528 */
cancelIndexing()529 void QHelpSearchEngine::cancelIndexing()
530 {
531     d->cancelIndexing();
532 }
533 
534 /*!
535     Stops the search process.
536 */
cancelSearching()537 void QHelpSearchEngine::cancelSearching()
538 {
539     d->cancelSearching();
540 }
541 
542 /*!
543     \since 5.9
544     Starts the search process using the given search phrase \a searchInput.
545 
546     The phrase may consist of several words. By default, the search engine returns
547     the list of documents that contain all the specified words.
548     The phrase may contain any combination of the logical operators AND, OR, and
549     NOT. The operator must be written in all capital letters, otherwise it will
550     be considered a part of the search phrase.
551 
552     If double quotation marks are used to group the words,
553     the search engine will search for an exact match of the quoted phrase.
554 
555     For more information about the text query syntax,
556     see \l {https://sqlite.org/fts5.html#full_text_query_syntax}
557     {SQLite FTS5 Extension}.
558 */
search(const QString & searchInput)559 void QHelpSearchEngine::search(const QString &searchInput)
560 {
561     d->search(searchInput);
562 }
563 
564 /*!
565     \obsolete
566     Use search(const QString &searchInput) instead.
567 */
search(const QList<QHelpSearchQuery> & queryList)568 void QHelpSearchEngine::search(const QList<QHelpSearchQuery> &queryList)
569 {
570     if (queryList.isEmpty())
571         return;
572 
573     d->search(queryList.first().wordList.join(QChar::Space));
574 }
575 
576 /*!
577     \internal
578 */
scheduleIndexDocumentation()579 void QHelpSearchEngine::scheduleIndexDocumentation()
580 {
581     if (d->m_isIndexingScheduled)
582         return;
583 
584     d->m_isIndexingScheduled = true;
585     QTimer::singleShot(0, this, &QHelpSearchEngine::indexDocumentation);
586 }
587 
indexDocumentation()588 void QHelpSearchEngine::indexDocumentation()
589 {
590     d->m_isIndexingScheduled = false;
591     d->updateIndex();
592 }
593 
594 QT_END_NAMESPACE
595