1 /****************************************************************************
2 **
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://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 http://www.qt.io/terms-conditions. For further
15 ** information use the contact form at http://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 2.1 or version 3 as published by the Free
20 ** Software Foundation and appearing in the file LICENSE.LGPLv21 and
21 ** LICENSE.LGPLv3 included in the packaging of this file. Please review the
22 ** following information to ensure the GNU Lesser General Public License
23 ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
24 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 **
26 ** As a special exception, The Qt Company gives you certain additional
27 ** rights. These rights are described in The Qt Company LGPL Exception
28 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 **
30 ** GNU General Public License Usage
31 ** Alternatively, this file may be used under the terms of the GNU
32 ** General Public License version 3.0 as published by the Free Software
33 ** Foundation and appearing in the file LICENSE.GPL included in the
34 ** packaging of this file. Please review the following information to
35 ** ensure the GNU General Public License version 3.0 requirements will be
36 ** met: http://www.gnu.org/copyleft/gpl.html.
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 #include "tracer.h"
42
43 #include <QtCore/QDir>
44 #include <QtCore/QFileInfo>
45 #include <QtCore/QLibraryInfo>
46 #include <QtCore/QLocale>
47 #include <QtCore/QScopedPointer>
48 #include <QtCore/QStringList>
49 #include <QtCore/QTranslator>
50 #include <QtCore/QUrl>
51
52 #include <QtGui/QApplication>
53 #include <QtGui/QDesktopServices>
54
55 #include <QtHelp/QHelpEngine>
56 #include <QtHelp/QHelpSearchEngine>
57
58 #include <QtNetwork/QLocalSocket>
59
60 #include <QtSql/QSqlDatabase>
61
62 #include "../shared/collectionconfiguration.h"
63 #include "helpenginewrapper.h"
64 #include "mainwindow.h"
65 #include "cmdlineparser.h"
66
67 // #define TRACING_REQUESTED
68
69 QT_USE_NAMESPACE
70
71 #if defined(USE_STATIC_SQLITE_PLUGIN)
72 #include <QtPlugin>
73 Q_IMPORT_PLUGIN(qsqlite)
74 #endif
75
76 namespace {
77
78 void
updateLastPagesOnUnregister(QHelpEngineCore & helpEngine,const QString & nsName)79 updateLastPagesOnUnregister(QHelpEngineCore& helpEngine, const QString& nsName)
80 {
81 TRACE_OBJ
82 int lastPage = CollectionConfiguration::lastTabPage(helpEngine);
83 QStringList currentPages = CollectionConfiguration::lastShownPages(helpEngine);
84 if (!currentPages.isEmpty()) {
85 QStringList zoomList = CollectionConfiguration::lastZoomFactors(helpEngine);
86 while (zoomList.count() < currentPages.count())
87 zoomList.append(CollectionConfiguration::DefaultZoomFactor);
88
89 for (int i = currentPages.count(); --i >= 0;) {
90 if (QUrl(currentPages.at(i)).host() == nsName) {
91 zoomList.removeAt(i);
92 currentPages.removeAt(i);
93 lastPage = (lastPage == (i + 1)) ? 1 : lastPage;
94 }
95 }
96
97 CollectionConfiguration::setLastShownPages(helpEngine, currentPages);
98 CollectionConfiguration::setLastTabPage(helpEngine, lastPage);
99 CollectionConfiguration::setLastZoomFactors(helpEngine, zoomList);
100 }
101 }
102
103 bool
updateUserCollection(QHelpEngineCore & user,const QHelpEngineCore & caller)104 updateUserCollection(QHelpEngineCore& user, const QHelpEngineCore& caller)
105 {
106 TRACE_OBJ
107 if (!CollectionConfiguration::isNewer(caller, user))
108 return false;
109 CollectionConfiguration::copyConfiguration(caller, user);
110 return true;
111 }
112
stripNonexistingDocs(QHelpEngineCore & collection)113 void stripNonexistingDocs(QHelpEngineCore& collection)
114 {
115 TRACE_OBJ
116 const QStringList &namespaces = collection.registeredDocumentations();
117 foreach (const QString &ns, namespaces) {
118 QFileInfo fi(collection.documentationFileName(ns));
119 if (!fi.exists() || !fi.isFile())
120 collection.unregisterDocumentation(ns);
121 }
122 }
123
indexFilesFolder(const QString & collectionFile)124 QString indexFilesFolder(const QString &collectionFile)
125 {
126 TRACE_OBJ
127 QString indexFilesFolder = QLatin1String(".fulltextsearch");
128 if (!collectionFile.isEmpty()) {
129 QFileInfo fi(collectionFile);
130 indexFilesFolder = QLatin1Char('.') +
131 fi.fileName().left(fi.fileName().lastIndexOf(QLatin1String(".qhc")));
132 }
133 return indexFilesFolder;
134 }
135
136 /*
137 * Returns the expected absolute file path of the cached collection file
138 * correspondinging to the given collection's file.
139 * It may or may not exist yet.
140 */
constructCachedCollectionFilePath(const QHelpEngineCore & collection)141 QString constructCachedCollectionFilePath(const QHelpEngineCore &collection)
142 {
143 TRACE_OBJ
144 const QString &filePath = collection.collectionFile();
145 const QString &fileName = QFileInfo(filePath).fileName();
146 const QString &cacheDir = CollectionConfiguration::cacheDir(collection);
147 const QString &dir = !cacheDir.isEmpty()
148 && CollectionConfiguration::cacheDirIsRelativeToCollection(collection)
149 ? QFileInfo(filePath).dir().absolutePath()
150 + QDir::separator() + cacheDir
151 : MainWindow::collectionFileDirectory(false, cacheDir);
152 return dir + QDir::separator() + fileName;
153 }
154
synchronizeDocs(QHelpEngineCore & collection,QHelpEngineCore & cachedCollection,CmdLineParser & cmd)155 bool synchronizeDocs(QHelpEngineCore &collection,
156 QHelpEngineCore &cachedCollection,
157 CmdLineParser &cmd)
158 {
159 TRACE_OBJ
160 const QDateTime &lastCollectionRegisterTime =
161 CollectionConfiguration::lastRegisterTime(collection);
162 if (!lastCollectionRegisterTime.isValid() || lastCollectionRegisterTime
163 < CollectionConfiguration::lastRegisterTime(cachedCollection))
164 return true;
165
166 const QStringList &docs = collection.registeredDocumentations();
167 const QStringList &cachedDocs = cachedCollection.registeredDocumentations();
168
169 /*
170 * Ensure that the cached collection contains all docs that
171 * the collection contains.
172 */
173 foreach (const QString &doc, docs) {
174 if (!cachedDocs.contains(doc)) {
175 const QString &docFile = collection.documentationFileName(doc);
176 if (!cachedCollection.registerDocumentation(docFile)) {
177 cmd.showMessage(QCoreApplication::translate("Assistant",
178 "Error registering documentation file '%1': %2").
179 arg(docFile).arg(cachedCollection.error()), true);
180 return false;
181 }
182 }
183 }
184
185 CollectionConfiguration::updateLastRegisterTime(cachedCollection);
186
187 return true;
188 }
189
removeSearchIndex(const QString & collectionFile)190 bool removeSearchIndex(const QString &collectionFile)
191 {
192 TRACE_OBJ
193 QString path = QFileInfo(collectionFile).path();
194 path += QLatin1Char('/') + indexFilesFolder(collectionFile);
195
196 QLocalSocket localSocket;
197 localSocket.connectToServer(QString(QLatin1String("QtAssistant%1"))
198 .arg(QLatin1String(QT_VERSION_STR)));
199
200 QDir dir(path); // check if there is no other instance ruinning
201 if (!dir.exists() || localSocket.waitForConnected())
202 return false;
203
204 QStringList lst = dir.entryList(QDir::Files | QDir::Hidden);
205 foreach (const QString &item, lst)
206 dir.remove(item);
207 return true;
208 }
209
rebuildSearchIndex(QCoreApplication & app,const QString & collectionFile,CmdLineParser & cmd)210 bool rebuildSearchIndex(QCoreApplication &app, const QString &collectionFile,
211 CmdLineParser &cmd)
212 {
213 TRACE_OBJ
214 QHelpEngine engine(collectionFile);
215 if (!engine.setupData()) {
216 cmd.showMessage(QCoreApplication::translate("Assistant", "Error: %1")
217 .arg(engine.error()), true);
218 return false;
219 }
220
221 QHelpSearchEngine * const searchEngine = engine.searchEngine();
222 QObject::connect(searchEngine, SIGNAL(indexingFinished()), &app,
223 SLOT(quit()));
224 searchEngine->reindexDocumentation();
225 return app.exec() == 0;
226 }
227
useGui(int argc,char * argv[])228 bool useGui(int argc, char *argv[])
229 {
230 TRACE_OBJ
231 bool gui = true;
232 #ifndef Q_OS_WIN
233 // Look for arguments that imply command-line mode.
234 const char * cmdModeArgs[] = {
235 "-help", "-register", "-unregister", "-remove-search-index",
236 "-rebuild-search-index"
237 };
238 for (int i = 1; i < argc; ++i) {
239 for (size_t j = 0; j < sizeof cmdModeArgs/sizeof *cmdModeArgs; ++j) {
240 if(strcmp(argv[i], cmdModeArgs[j]) == 0) {
241 gui = false;
242 break;
243 }
244 }
245 }
246 #else
247 Q_UNUSED(argc)
248 Q_UNUSED(argv)
249 #endif
250 return gui;
251 }
252
registerDocumentation(QHelpEngineCore & collection,CmdLineParser & cmd,bool printSuccess)253 bool registerDocumentation(QHelpEngineCore &collection, CmdLineParser &cmd,
254 bool printSuccess)
255 {
256 TRACE_OBJ
257 if (!collection.registerDocumentation(cmd.helpFile())) {
258 cmd.showMessage(QCoreApplication::translate("Assistant",
259 "Could not register documentation file\n%1\n\nReason:\n%2")
260 .arg(cmd.helpFile()).arg(collection.error()), true);
261 return false;
262 }
263 if (printSuccess)
264 cmd.showMessage(QCoreApplication::translate("Assistant",
265 "Documentation successfully registered."),
266 false);
267 CollectionConfiguration::updateLastRegisterTime(collection);
268 return true;
269 }
270
unregisterDocumentation(QHelpEngineCore & collection,const QString & namespaceName,CmdLineParser & cmd,bool printSuccess)271 bool unregisterDocumentation(QHelpEngineCore &collection,
272 const QString &namespaceName, CmdLineParser &cmd, bool printSuccess)
273 {
274 TRACE_OBJ
275 if (!collection.unregisterDocumentation(namespaceName)) {
276 cmd.showMessage(QCoreApplication::translate("Assistant",
277 "Could not unregister documentation"
278 " file\n%1\n\nReason:\n%2").
279 arg(cmd.helpFile()).arg(collection.error()), true);
280 return false;
281 }
282 updateLastPagesOnUnregister(collection, namespaceName);
283 if (printSuccess)
284 cmd.showMessage(QCoreApplication::translate("Assistant",
285 "Documentation successfully unregistered."),
286 false);
287 return true;
288 }
289
setupTranslation(const QString & fileName,const QString & dir)290 void setupTranslation(const QString &fileName, const QString &dir)
291 {
292 QTranslator *translator = new QTranslator(QCoreApplication::instance());
293 if (translator->load(fileName, dir)) {
294 QCoreApplication::installTranslator(translator);
295 } else if (!fileName.endsWith(QLatin1String("en_US"))
296 && !fileName.endsWith(QLatin1String("_C"))) {
297 qWarning("Could not load translation file %s in directory %s.",
298 qPrintable(fileName), qPrintable(dir));
299 }
300 }
301
setupTranslations()302 void setupTranslations()
303 {
304 TRACE_OBJ
305 const QString& locale = QLocale::system().name();
306 const QString &resourceDir
307 = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
308 setupTranslation(QLatin1String("assistant_") + locale, resourceDir);
309 setupTranslation(QLatin1String("qt_") + locale, resourceDir);
310 setupTranslation(QLatin1String("qt_help_") + locale, resourceDir);
311 }
312
313 } // Anonymous namespace.
314
main(int argc,char * argv[])315 int main(int argc, char *argv[])
316 {
317 TRACE_OBJ
318 QApplication a(argc, argv, useGui(argc, argv));
319 a.addLibraryPath(a.applicationDirPath() + QLatin1String("/plugins"));
320 setupTranslations();
321
322 // Parse arguments.
323 CmdLineParser cmd(a.arguments());
324 CmdLineParser::Result res = cmd.parse();
325 if (res == CmdLineParser::Help)
326 return 0;
327 else if (res == CmdLineParser::Error)
328 return -1;
329
330 /*
331 * Create the collection objects that we need. We always have the
332 * cached collection file. Depending on whether the user specified
333 * one, we also may have an input collection file.
334 */
335 const QString collectionFile = cmd.collectionFile();
336 const bool collectionFileGiven = !collectionFile.isEmpty();
337 QScopedPointer<QHelpEngineCore> collection;
338 if (collectionFileGiven) {
339 collection.reset(new QHelpEngineCore(collectionFile));
340 if (!collection->setupData()) {
341 cmd.showMessage(QCoreApplication::translate("Assistant",
342 "Error reading collection file '%1': %2.").
343 arg(collectionFile).arg(collection->error()), true);
344 return EXIT_FAILURE;
345 }
346 }
347 const QString &cachedCollectionFile = collectionFileGiven
348 ? constructCachedCollectionFilePath(*collection)
349 : MainWindow::defaultHelpCollectionFileName();
350 if (collectionFileGiven && !QFileInfo(cachedCollectionFile).exists()
351 && !collection->copyCollectionFile(cachedCollectionFile)) {
352 cmd.showMessage(QCoreApplication::translate("Assistant",
353 "Error creating collection file '%1': %2.").
354 arg(cachedCollectionFile).arg(collection->error()), true);
355 return EXIT_FAILURE;
356 }
357 QHelpEngineCore cachedCollection(cachedCollectionFile);
358 if (!cachedCollection.setupData()) {
359 cmd.showMessage(QCoreApplication::translate("Assistant",
360 "Error reading collection file '%1': %2.").
361 arg(cachedCollectionFile).
362 arg(cachedCollection.error()), true);
363 return EXIT_FAILURE;
364 }
365
366 stripNonexistingDocs(cachedCollection);
367 if (collectionFileGiven) {
368 if (CollectionConfiguration::isNewer(*collection, cachedCollection))
369 CollectionConfiguration::copyConfiguration(*collection,
370 cachedCollection);
371 if (!synchronizeDocs(*collection, cachedCollection, cmd))
372 return EXIT_FAILURE;
373 }
374
375 if (cmd.registerRequest() != CmdLineParser::None) {
376 const QStringList &cachedDocs =
377 cachedCollection.registeredDocumentations();
378 const QString &namespaceName =
379 QHelpEngineCore::namespaceName(cmd.helpFile());
380 if (cmd.registerRequest() == CmdLineParser::Register) {
381 if (collectionFileGiven
382 && !registerDocumentation(*collection, cmd, true))
383 return EXIT_FAILURE;
384 if (!cachedDocs.contains(namespaceName)
385 && !registerDocumentation(cachedCollection, cmd, !collectionFileGiven))
386 return EXIT_FAILURE;
387 return EXIT_SUCCESS;
388 }
389 if (cmd.registerRequest() == CmdLineParser::Unregister) {
390 if (collectionFileGiven
391 && !unregisterDocumentation(*collection, namespaceName, cmd, true))
392 return EXIT_FAILURE;
393 if (cachedDocs.contains(namespaceName)
394 && !unregisterDocumentation(cachedCollection, namespaceName,
395 cmd, !collectionFileGiven))
396 return EXIT_FAILURE;
397 return EXIT_SUCCESS;
398 }
399 }
400
401 if (cmd.removeSearchIndex()) {
402 return removeSearchIndex(cachedCollectionFile)
403 ? EXIT_SUCCESS : EXIT_FAILURE;
404 }
405
406 if (cmd.rebuildSearchIndex()) {
407 return rebuildSearchIndex(a, cachedCollectionFile, cmd)
408 ? EXIT_SUCCESS : EXIT_FAILURE;
409 }
410
411 if (!QSqlDatabase::isDriverAvailable(QLatin1String("QSQLITE"))) {
412 cmd.showMessage(QCoreApplication::translate("Assistant",
413 "Cannot load sqlite database driver!"),
414 true);
415 return EXIT_FAILURE;
416 }
417
418 if (!cmd.currentFilter().isEmpty()) {
419 if (collectionFileGiven)
420 collection->setCurrentFilter(cmd.currentFilter());
421 cachedCollection.setCurrentFilter(cmd.currentFilter());
422 }
423
424 if (collectionFileGiven)
425 cmd.setCollectionFile(cachedCollectionFile);
426
427 MainWindow *w = new MainWindow(&cmd);
428 w->show();
429 a.connect(&a, SIGNAL(lastWindowClosed()), &a, SLOT(quit()));
430
431 /*
432 * We need to be careful here: The main window has to be deleted before
433 * the help engine wrapper, which has to be deleted before the
434 * QApplication.
435 */
436 const int retval = a.exec();
437 delete w;
438 HelpEngineWrapper::removeInstance();
439 return retval;
440 }
441