/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the Qt Assistant of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qhelpgenerator_p.h" #include "qhelpdatainterface_p.h" #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE class QHelpGeneratorPrivate { public: QHelpGeneratorPrivate(); ~QHelpGeneratorPrivate(); QString error; QSqlQuery *query; int namespaceId; int virtualFolderId; QMap fileMap; QMap > fileFilterMap; double progress; double oldProgress; double contentStep; double fileStep; double indexStep; }; QHelpGeneratorPrivate::QHelpGeneratorPrivate() { query = 0; namespaceId = -1; virtualFolderId = -1; } QHelpGeneratorPrivate::~QHelpGeneratorPrivate() { } /*! \internal \class QHelpGenerator \since 4.4 \brief The QHelpGenerator class generates a new Qt compressed help file (.qch). The help generator takes a help data structure as input for generating a new Qt compressed help files. Since the generation may takes some time, the generator emits various signals to inform about its current state. */ /*! \fn void QHelpGenerator::statusChanged(const QString &msg) This signal is emitted when the generation status changes. The status is basically a specific task like inserting files or building up the keyword index. The parameter \a msg contains the detailed status description. */ /*! \fn void QHelpGenerator::progressChanged(double progress) This signal is emitted when the progress changes. The \a progress ranges from 0 to 100. */ /*! \fn void QHelpGenerator::warning(const QString &msg) This signal is emitted when a non critical error occurs, e.g. when a referenced file cannot be found. \a msg contains the exact warning message. */ /*! Constructs a new help generator with the give \a parent. */ QHelpGenerator::QHelpGenerator(QObject *parent) : QObject(parent) { d = new QHelpGeneratorPrivate; } /*! Destructs the help generator. */ QHelpGenerator::~QHelpGenerator() { delete d; } /*! Takes the \a helpData and generates a new documentation set from it. The Qt compressed help file is written to \a outputFileName. Returns true on success, otherwise false. */ bool QHelpGenerator::generate(QHelpDataInterface *helpData, const QString &outputFileName) { emit progressChanged(0); d->error.clear(); if (!helpData || helpData->namespaceName().isEmpty()) { d->error = tr("Invalid help data!"); return false; } QString outFileName = outputFileName; if (outFileName.isEmpty()) { d->error = tr("No output file name specified!"); return false; } QFileInfo fi(outFileName); if (fi.exists()) { if (!fi.dir().remove(fi.fileName())) { d->error = tr("The file %1 cannot be overwritten!").arg(outFileName); return false; } } setupProgress(helpData); emit statusChanged(tr("Building up file structure...")); bool openingOk = true; { QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("builder")); db.setDatabaseName(outFileName); openingOk = db.open(); if (openingOk) d->query = new QSqlQuery(db); } if (!openingOk) { d->error = tr("Cannot open data base file %1!").arg(outFileName); cleanupDB(); return false; } addProgress(1.0); createTables(); insertFileNotFoundFile(); insertMetaData(helpData->metaData()); if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) { d->error = tr("Cannot register namespace %1!").arg(helpData->namespaceName()); cleanupDB(); return false; } addProgress(1.0); emit statusChanged(tr("Insert custom filters...")); foreach (QHelpDataCustomFilter f, helpData->customFilters()) { if (!registerCustomFilter(f.name, f.filterAttributes, true)) { cleanupDB(); return false; } } addProgress(1.0); int i = 1; QList::const_iterator it = helpData->filterSections().constBegin(); while (it != helpData->filterSections().constEnd()) { emit statusChanged(tr("Insert help data for filter section (%1 of %2)...") .arg(i++).arg(helpData->filterSections().count())); insertFilterAttributes((*it).filterAttributes()); QByteArray ba; QDataStream s(&ba, QIODevice::WriteOnly); foreach (QHelpDataContentItem *itm, (*it).contents()) writeTree(s, itm, 0); if (!insertFiles((*it).files(), helpData->rootPath(), (*it).filterAttributes()) || !insertContents(ba, (*it).filterAttributes()) || !insertKeywords((*it).indices(), (*it).filterAttributes())) { cleanupDB(); return false; } ++it; } cleanupDB(); emit progressChanged(100); emit statusChanged(tr("Documentation successfully generated.")); return true; } void QHelpGenerator::setupProgress(QHelpDataInterface *helpData) { d->progress = 0; d->oldProgress = 0; int numberOfFiles = 0; int numberOfIndices = 0; QList::const_iterator it = helpData->filterSections().constBegin(); while (it != helpData->filterSections().constEnd()) { numberOfFiles += (*it).files().count(); numberOfIndices += (*it).indices().count(); ++it; } // init 2% // filters 1% // contents 10% // files 60% // indices 27% d->contentStep = 10.0/(double)helpData->customFilters().count(); d->fileStep = 60.0/(double)numberOfFiles; d->indexStep = 27.0/(double)numberOfIndices; } void QHelpGenerator::addProgress(double step) { d->progress += step; if ((d->progress-d->oldProgress) >= 1.0 && d->progress <= 100.0) { d->oldProgress = d->progress; emit progressChanged(ceil(d->progress)); } } void QHelpGenerator::cleanupDB() { if (d->query) { d->query->clear(); delete d->query; d->query = 0; } QSqlDatabase::removeDatabase(QLatin1String("builder")); } void QHelpGenerator::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth) { QString fReference = QDir::cleanPath(item->reference()); if (fReference.startsWith(QLatin1String("./"))) fReference = fReference.mid(2); s << depth; s << fReference; s << item->title(); foreach (QHelpDataContentItem *i, item->children()) writeTree(s, i, depth+1); } /*! Returns the last error message. */ QString QHelpGenerator::error() const { return d->error; } bool QHelpGenerator::createTables() { if (!d->query) return false; d->query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'" "AND Name=\'NamespaceTable\'")); d->query->next(); if (d->query->value(0).toInt() > 0) { d->error = tr("Some tables already exist!"); return false; } QStringList tables; tables << QLatin1String("CREATE TABLE NamespaceTable (" "Id INTEGER PRIMARY KEY," "Name TEXT )") << QLatin1String("CREATE TABLE FilterAttributeTable (" "Id INTEGER PRIMARY KEY, " "Name TEXT )") << QLatin1String("CREATE TABLE FilterNameTable (" "Id INTEGER PRIMARY KEY, " "Name TEXT )") << QLatin1String("CREATE TABLE FilterTable (" "NameId INTEGER, " "FilterAttributeId INTEGER )") << QLatin1String("CREATE TABLE IndexTable (" "Id INTEGER PRIMARY KEY, " "Name TEXT, " "Identifier TEXT, " "NamespaceId INTEGER, " "FileId INTEGER, " "Anchor TEXT )") << QLatin1String("CREATE TABLE IndexItemTable (" "Id INTEGER, " "IndexId INTEGER )") << QLatin1String("CREATE TABLE IndexFilterTable (" "FilterAttributeId INTEGER, " "IndexId INTEGER )") << QLatin1String("CREATE TABLE ContentsTable (" "Id INTEGER PRIMARY KEY, " "NamespaceId INTEGER, " "Data BLOB )") << QLatin1String("CREATE TABLE ContentsFilterTable (" "FilterAttributeId INTEGER, " "ContentsId INTEGER )") << QLatin1String("CREATE TABLE FileAttributeSetTable (" "Id INTEGER, " "FilterAttributeId INTEGER )") << QLatin1String("CREATE TABLE FileDataTable (" "Id INTEGER PRIMARY KEY, " "Data BLOB )") << QLatin1String("CREATE TABLE FileFilterTable (" "FilterAttributeId INTEGER, " "FileId INTEGER )") << QLatin1String("CREATE TABLE FileNameTable (" "FolderId INTEGER, " "Name TEXT, " "FileId INTEGER, " "Title TEXT )") << QLatin1String("CREATE TABLE FolderTable(" "Id INTEGER PRIMARY KEY, " "Name Text, " "NamespaceID INTEGER )") << QLatin1String("CREATE TABLE MetaDataTable(" "Name Text, " "Value BLOB )"); foreach (QString q, tables) { if (!d->query->exec(q)) { d->error = tr("Cannot create tables!"); return false; } } d->query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')")); d->query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES('CreationDate', ?)")); d->query->bindValue(0, QDateTime::currentDateTime().toString(Qt::ISODate)); d->query->exec(); return true; } bool QHelpGenerator::insertFileNotFoundFile() { if (!d->query) return false; d->query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'")); if (d->query->next() && d->query->isValid()) return true; d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)")); d->query->bindValue(0, QByteArray()); if (!d->query->exec()) return false; int fileId = d->query->lastInsertId().toInt(); d->query->prepare(QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) " " VALUES (0, '', ?, '')")); d->query->bindValue(0, fileId); if (fileId > -1 && d->query->exec()) { d->fileMap.insert(QString(), fileId); return true; } return false; } bool QHelpGenerator::registerVirtualFolder(const QString &folderName, const QString &ns) { if (!d->query || folderName.isEmpty() || ns.isEmpty()) return false; d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); d->query->bindValue(0, folderName); d->query->exec(); d->query->next(); if (d->query->isValid() && d->query->value(0).toInt() > 0) return true; d->namespaceId = -1; d->query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?")); d->query->bindValue(0, ns); d->query->exec(); while (d->query->next()) { d->namespaceId = d->query->value(0).toInt(); break; } if (d->namespaceId < 0) { d->query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)")); d->query->bindValue(0, ns); if (d->query->exec()) d->namespaceId = d->query->lastInsertId().toInt(); } if (d->namespaceId > 0) { d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); d->query->bindValue(0, folderName); d->query->exec(); while (d->query->next()) d->virtualFolderId = d->query->value(0).toInt(); if (d->virtualFolderId > 0) return true; d->query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) " "VALUES (?, ?)")); d->query->bindValue(0, d->namespaceId); d->query->bindValue(1, folderName); if (d->query->exec()) { d->virtualFolderId = d->query->lastInsertId().toInt(); return d->virtualFolderId > 0; } } d->error = tr("Cannot register virtual folder!"); return false; } bool QHelpGenerator::insertFiles(const QStringList &files, const QString &rootPath, const QStringList &filterAttributes) { if (!d->query) return false; emit statusChanged(tr("Insert files...")); QList filterAtts; foreach (const QString &filterAtt, filterAttributes) { d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable " "WHERE Name=?")); d->query->bindValue(0, filterAtt); d->query->exec(); if (d->query->next()) filterAtts.append(d->query->value(0).toInt()); } int filterSetId = -1; d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable")); if (d->query->next()) filterSetId = d->query->value(0).toInt(); if (filterSetId < 0) return false; ++filterSetId; foreach (const int &attId, filterAtts) { d->query->prepare(QLatin1String("INSERT INTO FileAttributeSetTable " "VALUES(?, ?)")); d->query->bindValue(0, filterSetId); d->query->bindValue(1, attId); d->query->exec(); } int tableFileId = 1; d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); if (d->query->next()) tableFileId = d->query->value(0).toInt() + 1; QString title; QString charSet; FileNameTableData fileNameData; QList fileDataList; QMap > tmpFileFilterMap; QList fileNameDataList; int i = 0; foreach (const QString &file, files) { const QString fileName = QDir::cleanPath(file); if (fileName.startsWith(QLatin1String("../"))) { emit warning(tr("The referenced file %1 must be inside or within a " "subdirectory of (%2). Skipping it.").arg(fileName).arg(rootPath)); continue; } QFile fi(rootPath + QDir::separator() + fileName); if (!fi.exists()) { emit warning(tr("The file %1 does not exist! Skipping it.") .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName))); continue; } if (!fi.open(QIODevice::ReadOnly)) { emit warning(tr("Cannot open file %1! Skipping it.") .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName))); continue; } QByteArray data = fi.readAll(); if (fileName.endsWith(QLatin1String(".html")) || fileName.endsWith(QLatin1String(".htm"))) { charSet = QHelpGlobal::codecFromData(data); QTextStream stream(&data); stream.setCodec(QTextCodec::codecForName(charSet.toLatin1().constData())); title = QHelpGlobal::documentTitle(stream.readAll()); } else { title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1); } int fileId = -1; if (!d->fileMap.contains(fileName)) { fileDataList.append(qCompress(data)); fileNameData.name = fileName; fileNameData.fileId = tableFileId; fileNameData.title = title; fileNameDataList.append(fileNameData); d->fileMap.insert(fileName, tableFileId); d->fileFilterMap.insert(tableFileId, filterAtts.toSet()); tmpFileFilterMap.insert(tableFileId, filterAtts.toSet()); ++tableFileId; } else { fileId = d->fileMap.value(fileName); foreach (const int &filter, filterAtts) { if (!d->fileFilterMap.value(fileId).contains(filter) && !tmpFileFilterMap.value(fileId).contains(filter)) { d->fileFilterMap[fileId].insert(filter); tmpFileFilterMap[fileId].insert(filter); } } } } if (tmpFileFilterMap.count()) { d->query->exec(QLatin1String("BEGIN")); QMap >::const_iterator it = tmpFileFilterMap.constBegin(); while (it != tmpFileFilterMap.constEnd()) { QSet::const_iterator si = it.value().constBegin(); while (si != it.value().constEnd()) { d->query->prepare(QLatin1String("INSERT INTO FileFilterTable " "VALUES(?, ?)")); d->query->bindValue(0, *si); d->query->bindValue(1, it.key()); d->query->exec(); ++si; } ++it; } QList::const_iterator fileIt = fileDataList.constBegin(); while (fileIt != fileDataList.constEnd()) { d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES " "(Null, ?)")); d->query->bindValue(0, *fileIt); d->query->exec(); ++fileIt; if (++i%20 == 0) addProgress(d->fileStep*20.0); } QList::const_iterator fileNameIt = fileNameDataList.constBegin(); while (fileNameIt != fileNameDataList.constEnd()) { d->query->prepare(QLatin1String("INSERT INTO FileNameTable " "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)")); d->query->bindValue(0, 1); d->query->bindValue(1, (*fileNameIt).name); d->query->bindValue(2, (*fileNameIt).fileId); d->query->bindValue(3, (*fileNameIt).title); d->query->exec(); ++fileNameIt; } d->query->exec(QLatin1String("COMMIT")); } d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); if (d->query->next() && d->query->value(0).toInt() == tableFileId-1) { addProgress(d->fileStep*(i%20)); return true; } return false; } bool QHelpGenerator::registerCustomFilter(const QString &filterName, const QStringList &filterAttribs, bool forceUpdate) { if (!d->query) return false; d->query->exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable")); QStringList idsToInsert = filterAttribs; QMap attributeMap; while (d->query->next()) { attributeMap.insert(d->query->value(1).toString(), d->query->value(0).toInt()); if (idsToInsert.contains(d->query->value(1).toString())) idsToInsert.removeAll(d->query->value(1).toString()); } foreach (QString id, idsToInsert) { d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); d->query->bindValue(0, id); d->query->exec(); attributeMap.insert(id, d->query->lastInsertId().toInt()); } int nameId = -1; d->query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?")); d->query->bindValue(0, filterName); d->query->exec(); while (d->query->next()) { nameId = d->query->value(0).toInt(); break; } if (nameId < 0) { d->query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)")); d->query->bindValue(0, filterName); if (d->query->exec()) nameId = d->query->lastInsertId().toInt(); } else if (!forceUpdate) { d->error = tr("The filter %1 is already registered!").arg(filterName); return false; } if (nameId < 0) { d->error = tr("Cannot register filter %1!").arg(filterName); return false; } d->query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?")); d->query->bindValue(0, nameId); d->query->exec(); foreach (QString att, filterAttribs) { d->query->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)")); d->query->bindValue(0, nameId); d->query->bindValue(1, attributeMap[att]); if (!d->query->exec()) return false; } return true; } bool QHelpGenerator::insertKeywords(const QList keywords, const QStringList &filterAttributes) { if (!d->query) return false; emit statusChanged(tr("Insert indices...")); int indexId = 1; d->query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable")); if (d->query->next()) indexId = d->query->value(0).toInt() + 1; QList filterAtts; foreach (QString filterAtt, filterAttributes) { d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?")); d->query->bindValue(0, filterAtt); d->query->exec(); if (d->query->next()) filterAtts.append(d->query->value(0).toInt()); } int pos = -1; QString fileName; QString anchor; QString fName; int fileId = 1; QList indexFilterTable; int i = 0; d->query->exec(QLatin1String("BEGIN")); foreach (QHelpDataIndexItem itm, keywords) { pos = itm.reference.indexOf(QLatin1Char('#')); fileName = itm.reference.left(pos); if (pos > -1) anchor = itm.reference.mid(pos+1); else anchor.clear(); fName = QDir::cleanPath(fileName); if (fName.startsWith(QLatin1String("./"))) fName = fName.mid(2); if (d->fileMap.contains(fName)) fileId = d->fileMap.value(fName); else fileId = 1; d->query->prepare(QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) " "VALUES(?, ?, ?, ?, ?)")); d->query->bindValue(0, itm.name); d->query->bindValue(1, itm.identifier); d->query->bindValue(2, d->namespaceId); d->query->bindValue(3, fileId); d->query->bindValue(4, anchor); d->query->exec(); indexFilterTable.append(indexId++); if (++i%100 == 0) addProgress(d->indexStep*100.0); } d->query->exec(QLatin1String("COMMIT")); d->query->exec(QLatin1String("BEGIN")); foreach (int idx, indexFilterTable) { foreach (int a, filterAtts) { d->query->prepare(QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) " "VALUES(?, ?)")); d->query->bindValue(0, a); d->query->bindValue(1, idx); d->query->exec(); } } d->query->exec(QLatin1String("COMMIT")); d->query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable")); if (d->query->next() && d->query->value(0).toInt() >= keywords.count()) return true; return false; } bool QHelpGenerator::insertContents(const QByteArray &ba, const QStringList &filterAttributes) { if (!d->query) return false; emit statusChanged(tr("Insert contents...")); d->query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) " "VALUES(?, ?)")); d->query->bindValue(0, d->namespaceId); d->query->bindValue(1, ba); d->query->exec(); int contentId = d->query->lastInsertId().toInt(); if (contentId < 1) { d->error = tr("Cannot insert contents!"); return false; } // associate the filter attributes foreach (QString filterAtt, filterAttributes) { d->query->prepare(QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) " "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?")); d->query->bindValue(0, contentId); d->query->bindValue(1, filterAtt); d->query->exec(); if (!d->query->isActive()) { d->error = tr("Cannot register contents!"); return false; } } addProgress(d->contentStep); return true; } bool QHelpGenerator::insertFilterAttributes(const QStringList &attributes) { if (!d->query) return false; d->query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); QSet atts; while (d->query->next()) atts.insert(d->query->value(0).toString()); foreach (QString s, attributes) { if (!atts.contains(s)) { d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); d->query->bindValue(0, s); d->query->exec(); } } return true; } bool QHelpGenerator::insertMetaData(const QMap &metaData) { if (!d->query) return false; QMap::const_iterator it = metaData.constBegin(); while (it != metaData.constEnd()) { d->query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)")); d->query->bindValue(0, it.key()); d->query->bindValue(1, it.value()); d->query->exec(); ++it; } return true; } QT_END_NAMESPACE