/*************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2008 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Qt Software Information (qt-info@nokia.com) ** ** ** Non-Open Source Usage ** ** Licensees may use this file in accordance with the Qt Beta Version ** License Agreement, Agreement version 2.2 provided with the Software or, ** alternatively, in accordance with the terms contained in a written ** agreement between you and Nokia. ** ** GNU General Public License Usage ** ** Alternatively, this file may be used under the terms of the GNU General ** Public License versions 2.0 or 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the packaging ** of this file. Please review the following information to ensure GNU ** General Public Licensing requirements will be met: ** ** http://www.fsf.org/licensing/licenses/info/GPLv2.html and ** http://www.gnu.org/copyleft/gpl.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt GPL Exception ** version 1.3, included in the file GPL_EXCEPTION.txt in this package. ** ***************************************************************************/ #include "helpengine.h" #include "config.h" #include <QtCore/QDebug> #include <QtCore/QDir> #include <QtCore/QDateTime> #include <QtCore/QCoreApplication> using namespace Help::Internal; static bool verifyDirectory(const QString &str) { QFileInfo dirInfo(str); if (!dirInfo.exists()) return QDir().mkdir(str); if (!dirInfo.isDir()) { qWarning("'%s' exists but is not a directory", str.toLatin1().constData()); return false; } return true; } struct IndexKeyword { IndexKeyword(const QString &kw, const QString &l) : keyword(kw), link(l) {} IndexKeyword() : keyword(QString()), link(QString()) {} bool operator<(const IndexKeyword &ik) const { return keyword.toLower() < ik.keyword.toLower(); } bool operator<=(const IndexKeyword &ik) const { return keyword.toLower() <= ik.keyword.toLower(); } bool operator>(const IndexKeyword &ik) const { return keyword.toLower() > ik.keyword.toLower(); } Q_DUMMY_COMPARISON_OPERATOR(IndexKeyword) QString keyword; QString link; }; QDataStream &operator>>(QDataStream &s, IndexKeyword &ik) { s >> ik.keyword; s >> ik.link; return s; } QDataStream &operator<<(QDataStream &s, const IndexKeyword &ik) { s << ik.keyword; s << ik.link; return s; } /** * Compare in a human-preferred alphanumeric way, * e.g. 'Qt tutorial 2' will be less than 'Qt tutorial 11'. */ bool caseInsensitiveLessThan(const QString &as, const QString &bs) { const QChar *a = as.unicode(); const QChar *b = bs.unicode(); int result = 0; while (result == 0) { ushort aa = a->unicode(); ushort bb = b->unicode(); if (aa == 0 || bb == 0) { result = aa - bb; break; } else if (a->isDigit() && b->isDigit()) { const QChar *a_begin = a; const QChar *b_begin = b; bool loop = true; do { if (a->isDigit()) ++a; else if (b->isDigit()) ++b; else loop = false; } while (loop); // optimization: comparing the length of the two numbers is more efficient than constructing two qstrings. result = (a - a_begin) - (b - b_begin); if (result == 0) { QString astr(a_begin, a - a_begin); QString bstr(b_begin, b - b_begin); long la = astr.toLong(); long lb = bstr.toLong(); result = la - lb; } } else { aa = QChar(aa).toLower().unicode(); bb = QChar(bb).toLower().unicode(); result = aa - bb; ++a; ++b; } } return result < 0 ? true : false; } /** * \a real is kinda a hack for the smart search, need a way to match a regexp to an item * How would you say the best match for Q.*Wiget is QWidget? */ QModelIndex IndexListModel::filter(const QString &s, const QString &real) { QStringList list; int goodMatch = -1; int perfectMatch = -1; if (s.isEmpty()) perfectMatch = 0; const QRegExp regExp(s); QMultiMap<QString, QString>::iterator it = contents.begin(); QString lastKey; for (; it != contents.end(); ++it) { if (it.key() == lastKey) continue; lastKey = it.key(); const QString key = it.key(); if (key.contains(regExp) || key.contains(s, Qt::CaseInsensitive)) { list.append(key); //qDebug() << regExp << regExp.indexIn(s) << s << key << regExp.matchedLength(); if (perfectMatch == -1 && (key.startsWith(real, Qt::CaseInsensitive))) { if (goodMatch == -1) goodMatch = list.count() - 1; if (s.length() == key.length()) perfectMatch = list.count() - 1; } else if (perfectMatch > -1 && s == key) { perfectMatch = list.count() - 1; } } } int bestMatch = perfectMatch; if (bestMatch == -1) bestMatch = goodMatch; bestMatch = qMax(0, bestMatch); // sort the new list QString match; if (bestMatch >= 0 && list.count() > bestMatch) match = list[bestMatch]; qSort(list.begin(), list.end(), caseInsensitiveLessThan); setStringList(list); for (int i = 0; i < list.size(); ++i) { if (list.at(i) == match){ bestMatch = i; break; } } return index(bestMatch, 0, QModelIndex()); } HelpEngine::HelpEngine(QObject *parent, const QString &defaultQtVersionPath) : QObject(parent) { titleMapThread = new TitleMapThread(this); connect(titleMapThread, SIGNAL(errorOccured(const QString&)), this, SIGNAL(errorOccured(const QString&))); connect(titleMapThread, SIGNAL(finished()), this, SLOT(titleMapFinished())); indexThread = new IndexThread(this); connect(indexThread, SIGNAL(errorOccured(const QString&)), this, SIGNAL(errorOccured(const QString&))); connect(indexThread, SIGNAL(finished()), this, SLOT(indexFinished())); indexModel = new IndexListModel(this); Config::loadConfig(defaultQtVersionPath); cacheFilesPath = QDir::homePath() + QLatin1String("/.assistant"); } HelpEngine::~HelpEngine() { Config::configuration()->save(); } void HelpEngine::init() { } QString HelpEngine::cacheFilePath() const { return cacheFilesPath; } IndexListModel *HelpEngine::indices() { return indexModel; } void HelpEngine::buildContents() { contentsOnly = true; if (!titleMapThread->isRunning()) { titleMapThread->start(QThread::NormalPriority); } } void HelpEngine::buildIndex() { if (!titleMapThread->isRunning()) { contentsOnly = false; titleMapThread->start(QThread::NormalPriority); } if (!indexThread->isRunning()) indexThread->start(QThread::NormalPriority); } void HelpEngine::titleMapFinished() { contentList = titleMapThread->contents(); titleMap = titleMapThread->documentTitleMap(); if (contentsOnly) { contentsOnly = false; emit contentsInitialized(); } } void HelpEngine::indexFinished() { indexModel = indexThread->model(); emit indexInitialized(); } void HelpEngine::removeOldCacheFiles(bool onlyFulltextSearchIndex) { if (!verifyDirectory(cacheFilesPath)) { qWarning("Failed to created assistant directory"); return; } QString pname = QLatin1String(".") + Config::configuration()->profileName(); QStringList fileList; fileList << QLatin1String("indexdb40.dict") << QLatin1String("indexdb40.doc"); if (!onlyFulltextSearchIndex) fileList << QLatin1String("indexdb40") << QLatin1String("contentdb40"); QStringList::iterator it = fileList.begin(); for (; it != fileList.end(); ++it) { if (QFile::exists(cacheFilesPath + QDir::separator() + *it + pname)) { QFile f(cacheFilesPath + QDir::separator() + *it + pname); f.remove(); } } } quint32 HelpEngine::getFileAges() { QStringList addDocuFiles = Config::configuration()->docFiles(); QStringList::const_iterator i = addDocuFiles.begin(); quint32 fileAges = 0; for (; i != addDocuFiles.end(); ++i) { QFileInfo fi(*i); if (fi.exists()) fileAges += fi.lastModified().toTime_t(); } return fileAges; } QString HelpEngine::removeAnchorFromLink(const QString &link) { int i = link.length(); int j = link.lastIndexOf('/'); int l = link.lastIndexOf(QDir::separator()); if (l > j) j = l; if (j > -1) { QString fileName = link.mid(j+1); int k = fileName.lastIndexOf('#'); if (k > -1) i = j + k + 1; } return link.left(i); } QString HelpEngine::titleOfLink(const QString &link) { QString s = HelpEngine::removeAnchorFromLink(link); s = titleMap[s]; if (s.isEmpty()) return link; return s; } QString HelpEngine::home() const { QString link = Config::configuration()->homePage(); if (!link.startsWith(QLatin1String("file:"))) link.prepend("file:"); return link; } TitleMapThread::TitleMapThread(HelpEngine *he) : QThread(he) { engine = he; done = false; } TitleMapThread::~TitleMapThread() { } void TitleMapThread::run() { if (done) { engine->mutex.lock(); engine->titleMapDoneCondition.wakeAll(); engine->mutex.unlock(); return; } bool needRebuild = false; if (Config::configuration()->profileName() == QLatin1String("default")) { const QStringList docuFiles = Config::configuration()->docFiles(); for (QStringList::ConstIterator it = docuFiles.begin(); it != docuFiles.end(); it++) { if (!QFile::exists(*it)) { Config::configuration()->saveProfile(Profile::createDefaultProfile()); Config::configuration()->loadDefaultProfile(); needRebuild = true; break; } } } if (Config::configuration()->docRebuild() || needRebuild) { engine->removeOldCacheFiles(); Config::configuration()->setDocRebuild(false); Config::configuration()->save(); } if (contentList.isEmpty()) getAllContents(); titleMap.clear(); for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) { ContentList lst = (*it).second; foreach (ContentItem item, lst) { titleMap[item.reference] = item.title.trimmed(); } } done = true; engine->mutex.lock(); engine->titleMapDoneCondition.wakeAll(); engine->mutex.unlock(); } void TitleMapThread::getAllContents() { QFile contentFile(engine->cacheFilePath() + QDir::separator() + QLatin1String("contentdb40.") + Config::configuration()->profileName()); contentList.clear(); if (!contentFile.open(QFile::ReadOnly)) { buildContentDict(); return; } QDataStream ds(&contentFile); quint32 fileAges; ds >> fileAges; if (fileAges != engine->getFileAges()) { contentFile.close(); engine->removeOldCacheFiles(true); buildContentDict(); return; } QString key; QList<ContentItem> lst; while (!ds.atEnd()) { ds >> key; ds >> lst; contentList += qMakePair(key, QList<ContentItem>(lst)); } contentFile.close(); } void TitleMapThread::buildContentDict() { QStringList docuFiles = Config::configuration()->docFiles(); quint32 fileAges = 0; for (QStringList::iterator it = docuFiles.begin(); it != docuFiles.end(); it++) { QFile file(*it); if (!file.exists()) { #ifdef _SHOW_ERRORS_ emit errorOccured(tr("Documentation file %1 does not exist!\n" "Skipping file.").arg(QFileInfo(file).absoluteFilePath())); #endif continue; } fileAges += QFileInfo(file).lastModified().toTime_t(); DocuParser *handler = DocuParser::createParser(*it); if (!handler) { #ifdef _SHOW_ERRORS_ emit errorOccured(tr("Documentation file %1 is not compatible!\n" "Skipping file.").arg(QFileInfo(file).absoluteFilePath())); #endif continue; } bool ok = handler->parse(&file); file.close(); if (ok) { contentList += qMakePair(*it, QList<ContentItem>(handler->getContentItems())); delete handler; } else { #ifdef _SHOW_ERRORS_ QString msg = QString::fromLatin1("In file %1:\n%2") .arg(QFileInfo(file).absoluteFilePath()) .arg(handler->errorProtocol()); emit errorOccured(msg); #endif continue; } } QFile contentOut(engine->cacheFilePath() + QDir::separator() + QLatin1String("contentdb40.") + Config::configuration()->profileName()); if (contentOut.open(QFile::WriteOnly)) { QDataStream s(&contentOut); s << fileAges; for (QList<QPair<QString, ContentList> >::Iterator it = contentList.begin(); it != contentList.end(); ++it) { s << *it; } contentOut.close(); } } IndexThread::IndexThread(HelpEngine *he) : QThread(he) { engine = he; indexModel = new IndexListModel(this); indexDone = false; } void IndexThread::run() { if (indexDone) return; engine->mutex.lock(); if (engine->titleMapThread->isRunning()) engine->titleMapDoneCondition.wait(&(engine->mutex)); engine->mutex.unlock(); keywordDocuments.clear(); QList<IndexKeyword> lst; QFile indexFile(engine->cacheFilePath() + QDir::separator() + QLatin1String("indexdb40.") + Config::configuration()->profileName()); if (!indexFile.open(QFile::ReadOnly)) { buildKeywordDB(); if (!indexFile.open(QFile::ReadOnly)) { #ifdef _SHOW_ERRORS_ emit errorOccured(tr("Failed to load keyword index file!")); #endif return; } } QDataStream ds(&indexFile); quint32 fileAges; ds >> fileAges; if (fileAges != engine->getFileAges()) { indexFile.close(); buildKeywordDB(); if (!indexFile.open(QFile::ReadOnly)) { #ifdef _SHOW_ERRORS_ emit errorOccured(tr("Cannot open the index file %1") .arg(QFileInfo(indexFile).absoluteFilePath())); #endif return; } ds.setDevice(&indexFile); ds >> fileAges; } ds >> lst; indexFile.close(); for (int i=0; i<lst.count(); ++i) { const IndexKeyword &idx = lst.at(i); indexModel->addLink(idx.keyword, idx.link); keywordDocuments << HelpEngine::removeAnchorFromLink(idx.link); } indexModel->publish(); indexDone = true; } void IndexThread::buildKeywordDB() { QStringList addDocuFiles = Config::configuration()->docFiles(); QStringList::iterator i = addDocuFiles.begin(); int steps = 0; for (; i != addDocuFiles.end(); i++) steps += QFileInfo(*i).size(); QList<IndexKeyword> lst; quint32 fileAges = 0; for (i = addDocuFiles.begin(); i != addDocuFiles.end(); i++) { QFile file(*i); if (!file.exists()) { #ifdef _SHOW_ERRORS_ emit errorOccured(tr("Documentation file %1 does not exist!\n" "Skipping file.").arg(QFileInfo(file).absoluteFilePath())); #endif continue; } fileAges += QFileInfo(file).lastModified().toTime_t(); DocuParser *handler = DocuParser::createParser(*i); bool ok = handler->parse(&file); file.close(); if (!ok){ #ifdef _SHOW_ERRORS_ QString msg = QString::fromLatin1("In file %1:\n%2") .arg(QFileInfo(file).absoluteFilePath()) .arg(handler->errorProtocol()); emit errorOccured(msg); #endif delete handler; continue; } QList<IndexItem*> indLst = handler->getIndexItems(); foreach (IndexItem *indItem, indLst) { QFileInfo fi(indItem->reference); lst.append(IndexKeyword(indItem->keyword, indItem->reference)); } delete handler; } if (!lst.isEmpty()) qSort(lst); QFile indexout(engine->cacheFilePath() + QDir::separator() + QLatin1String("indexdb40.") + Config::configuration()->profileName()); if (verifyDirectory(engine->cacheFilePath()) && indexout.open(QFile::WriteOnly)) { QDataStream s(&indexout); s << fileAges; s << lst; indexout.close(); } }