Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qhelpcollectionhandler_p.h"
#include "qhelp_global.h"
#include "qhelpdbreader_p.h"
#include "qhelpfilterdata.h"
#include "qhelplink.h"
#include <QtCore/qdatastream.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qset.h>
#include <QtCore/qtimer.h>
#include <QtCore/qversionnumber.h>
#include <QtSql/qsqldriver.h>
#include <QtSql/qsqlerror.h>
#include <QtSql/qsqlquery.h>
#include <algorithm>
#include <iterator>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
// Stored as PRAGMA user_version. Increase when the format of the collection
// file changes in an incompatible way. Collection files without a version
// (0) were created before the version was introduced.
static constexpr int CollectionSchemaVersion = 1;
static QString writeSchemaVersionStatement()
{
return "PRAGMA user_version = "_L1 + QString::number(CollectionSchemaVersion);
}
class Transaction
{
public:
Q_DISABLE_COPY_MOVE(Transaction);
Transaction(const QString &connectionName)
: m_db(QSqlDatabase::database(connectionName)),
m_inTransaction(m_db.driver()->hasFeature(QSqlDriver::Transactions))
{
if (m_inTransaction)
m_inTransaction = m_db.transaction();
}
~Transaction()
{
if (m_inTransaction)
m_db.rollback();
}
[[nodiscard]] bool commit()
{
if (!m_inTransaction)
return true;
// On failure, the transaction stays open and is rolled back in the destructor.
if (!m_db.commit())
return false;
m_inTransaction = false;
return true;
}
private:
QSqlDatabase m_db;
bool m_inTransaction;
};
QHelpCollectionHandler::QHelpCollectionHandler(const QString &collectionFile, QObject *parent)
: QObject(parent)
, m_collectionFile(collectionFile)
{
const QFileInfo fi(m_collectionFile);
if (!fi.isAbsolute())
m_collectionFile = fi.absoluteFilePath();
}
QHelpCollectionHandler::~QHelpCollectionHandler()
{
closeDB();
}
bool QHelpCollectionHandler::isDBOpened() const
{
if (m_query)
return true;
auto *that = const_cast<QHelpCollectionHandler *>(this);
emit that->error(tr("The collection file \"%1\" is not set up yet.").arg(m_collectionFile));
return false;
}
void QHelpCollectionHandler::closeDB()
{
if (!m_query)
return;
m_query.reset();
QSqlDatabase::removeDatabase(m_connectionName);
m_connectionName.clear();
}
bool QHelpCollectionHandler::openCollectionFile()
{
if (m_query)
return true;
if (m_readOnly) {
// Opening a database creates the file when it doesn't exist, and an empty
// file gets no tables, as these are only created in fully writable mode.
// Either way the collection is unusable, so report it here instead of
// silently leaving an empty file behind and failing on first access.
const QFileInfo fi(m_collectionFile);
if (!fi.exists()) {
emit error(tr("The collection file \"%1\" does not exist.").arg(m_collectionFile));
return false;
}
if (fi.size() == 0) {
emit error(tr("The collection file \"%1\" is empty.").arg(m_collectionFile));
return false;
}
}
m_connectionName = QHelpGlobal::uniquifyConnectionName("QHelpCollectionHandler"_L1, this);
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_connectionName);
if (db.driver()
&& db.driver()->lastError().type() == QSqlError::ConnectionError) {
emit error(tr("Cannot load sqlite database driver."));
return false;
}
db.setDatabaseName(collectionFile());
if (db.open())
m_query.reset(new QSqlQuery(db));
if (!m_query) {
QSqlDatabase::removeDatabase(m_connectionName);
emit error(tr("Cannot open collection file: %1").arg(collectionFile()));
return false;
}
}
if (!m_query->exec("PRAGMA user_version"_L1) || !m_query->next()) {
closeDB();
emit error(tr("Cannot read the version of collection file %1.").arg(collectionFile()));
return false;
}
const int schemaVersion = m_query->value(0).toInt();
if (schemaVersion > CollectionSchemaVersion) {
closeDB();
emit error(tr("The collection file \"%1\" was created by a newer version of Qt Help.")
.arg(m_collectionFile));
return false;
}
if (m_readOnly)
return true;
m_query->exec("PRAGMA synchronous=OFF"_L1);
m_query->exec("PRAGMA cache_size=3000"_L1);
m_query->exec("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\' "
"AND Name=\'NamespaceTable\'"_L1);
m_query->next();
const bool tablesExist = m_query->value(0).toInt() > 0;
if (!tablesExist) {
if (!createTables(m_query.get())) {
closeDB();
emit error(tr("Cannot create tables in file %1.").arg(collectionFile()));
return false;
}
}
bool indexAndNamespaceFilterTablesMissing = false;
const QStringList newTables = {
"IndexTable"_L1,
"FileNameTable"_L1,
"ContentsTable"_L1,
"FileFilterTable"_L1,
"IndexFilterTable"_L1,
"ContentsFilterTable"_L1,
"FileAttributeSetTable"_L1,
"OptimizedFilterTable"_L1,
"TimeStampTable"_L1,
"VersionTable"_L1,
"Filter"_L1,
"ComponentTable"_L1,
"ComponentMapping"_L1,
"ComponentFilter"_L1,
"VersionFilter"_L1
};
QString queryString = "SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"_L1;
queryString.append(" AND (Name=\'"_L1);
queryString.append(newTables.join("\' OR Name=\'"_L1));
queryString.append("\')"_L1);
m_query->exec(queryString);
m_query->next();
if (m_query->value(0).toInt() != newTables.size()) {
if (!recreateIndexAndNamespaceFilterTables(m_query.get())) {
emit error(tr("Cannot create index tables in file %1.").arg(collectionFile()));
return false;
}
// Old tables exist, index tables didn't, recreate index tables only in this case
indexAndNamespaceFilterTablesMissing = tablesExist;
}
if (schemaVersion < CollectionSchemaVersion
&& !m_query->exec(writeSchemaVersionStatement())) {
emit error(tr("Cannot write the version of collection file %1.").arg(collectionFile()));
return false;
}
const FileInfoList &docList = registeredDocumentations();
if (indexAndNamespaceFilterTablesMissing) {
for (const QHelpCollectionHandler::FileInfo &info : docList) {
if (!registerIndexAndNamespaceFilterTables(info.namespaceName, true)) {
emit error(tr("Cannot register index tables in file %1.").arg(collectionFile()));
return false;
}
}
return true;
}
QList<TimeStamp> timeStamps;
m_query->exec("SELECT NamespaceId, FolderId, FilePath, Size, TimeStamp FROM TimeStampTable"_L1);
while (m_query->next()) {
TimeStamp timeStamp;
timeStamp.namespaceId = m_query->value(0).toInt();
timeStamp.folderId = m_query->value(1).toInt();
timeStamp.fileName = m_query->value(2).toString();
timeStamp.size = m_query->value(3).toLongLong();
timeStamp.timeStamp = m_query->value(4).toDateTime();
timeStamps.append(timeStamp);
}
QList<TimeStamp> toRemove;
for (const TimeStamp &timeStamp : timeStamps) {
if (!isTimeStampCorrect(timeStamp))
toRemove.append(timeStamp);
}
// TODO: we may optimize when toRemove.size() == timeStamps.size().
// In this case we remove all records from tables.
Transaction transaction(m_connectionName);
for (const TimeStamp &timeStamp : toRemove) {
if (!unregisterIndexTable(timeStamp.namespaceId, timeStamp.folderId)) {
emit error(tr("Cannot unregister index tables in file %1.").arg(collectionFile()));
return false;
}
}
if (!transaction.commit()) {
emit error(tr("Cannot unregister index tables in file %1.").arg(collectionFile()));
return false;
}
for (const QHelpCollectionHandler::FileInfo &info : docList) {
if (!hasTimeStampInfo(info.namespaceName)
&& !registerIndexAndNamespaceFilterTables(info.namespaceName)) {
// we may have a doc registered without a timestamp
// and the doc may be missing currently
unregisterDocumentation(info.namespaceName);
}
}
return true;
}
QString QHelpCollectionHandler::absoluteDocPath(const QString &fileName) const
{
const QFileInfo fi(collectionFile());
return QDir::isAbsolutePath(fileName)
? fileName
: QFileInfo(fi.absolutePath() + u'/' + fileName).absoluteFilePath();
}
bool QHelpCollectionHandler::isTimeStampCorrect(const TimeStamp &timeStamp) const
{
const QFileInfo fi(absoluteDocPath(timeStamp.fileName));
if (!fi.exists())
return false;
if (fi.size() != timeStamp.size)
return false;
if (fi.lastModified(QTimeZone::UTC) != timeStamp.timeStamp)
return false;
m_query->prepare("SELECT FilePath FROM NamespaceTable WHERE Id = ?"_L1);
m_query->bindValue(0, timeStamp.namespaceId);
if (!m_query->exec() || !m_query->next())
return false;
const QString oldFileName = m_query->value(0).toString();
m_query->clear();
if (oldFileName != timeStamp.fileName)
return false;
return true;
}
bool QHelpCollectionHandler::hasTimeStampInfo(const QString &nameSpace) const
{
m_query->prepare(
"SELECT "
"TimeStampTable.NamespaceId "
"FROM "
"NamespaceTable, "
"TimeStampTable "
"WHERE NamespaceTable.Id = TimeStampTable.NamespaceId "
"AND NamespaceTable.Name = ? LIMIT 1"_L1);
m_query->bindValue(0, nameSpace);
if (!m_query->exec())
return false;
if (!m_query->next())
return false;
m_query->clear();
return true;
}
void QHelpCollectionHandler::scheduleVacuum()
{
if (m_vacuumScheduled)
return;
m_vacuumScheduled = true;
QTimer::singleShot(0, this, &QHelpCollectionHandler::execVacuum);
}
void QHelpCollectionHandler::execVacuum()
{
if (!m_query)
return;
m_query->exec("VACUUM"_L1);
m_vacuumScheduled = false;
}
bool QHelpCollectionHandler::copyCollectionFile(const QString &fileName)
{
if (!m_query)
return false;
const QFileInfo fi(fileName);
if (fi.exists()) {
emit error(tr("The collection file \"%1\" already exists.").arg(fileName));
return false;
}
if (!fi.absoluteDir().exists() && !QDir().mkpath(fi.absolutePath())) {
emit error(tr("Cannot create directory: %1").arg(fi.absolutePath()));
return false;
}
const QString &colFile = fi.absoluteFilePath();
const QString &connectionName =
QHelpGlobal::uniquifyConnectionName("QHelpCollectionHandlerCopy"_L1, this);
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, connectionName);
db.setDatabaseName(colFile);
if (!db.open()) {
emit error(tr("Cannot open collection file: %1").arg(colFile));
return false;
}
const auto copyTables = [&] {
QSqlQuery copyQuery(db);
copyQuery.exec("PRAGMA synchronous=OFF"_L1);
copyQuery.exec("PRAGMA cache_size=3000"_L1);
Transaction transaction(connectionName);
if (!createTables(©Query) || !recreateIndexAndNamespaceFilterTables(©Query)
|| !copyQuery.exec(writeSchemaVersionStatement())) {
return false;
}
const QString &oldBaseDir = QFileInfo(collectionFile()).absolutePath();
const QFileInfo newColFi(colFile);
m_query->exec("SELECT Name, FilePath FROM NamespaceTable"_L1);
while (m_query->next()) {
copyQuery.prepare("INSERT INTO NamespaceTable VALUES(NULL, ?, ?)"_L1);
copyQuery.bindValue(0, m_query->value(0).toString());
QString oldFilePath = m_query->value(1).toString();
if (!QDir::isAbsolutePath(oldFilePath))
oldFilePath = oldBaseDir + u'/' + oldFilePath;
copyQuery.bindValue(1, newColFi.absoluteDir().relativeFilePath(oldFilePath));
if (!copyQuery.exec())
return false;
}
m_query->exec("SELECT NamespaceId, Name FROM FolderTable"_L1);
while (m_query->next()) {
copyQuery.prepare("INSERT INTO FolderTable VALUES(NULL, ?, ?)"_L1);
copyQuery.bindValue(0, m_query->value(0).toString());
copyQuery.bindValue(1, m_query->value(1).toString());
if (!copyQuery.exec())
return false;
}
m_query->exec("SELECT Name FROM FilterAttributeTable"_L1);
while (m_query->next()) {
copyQuery.prepare("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"_L1);
copyQuery.bindValue(0, m_query->value(0).toString());
if (!copyQuery.exec())
return false;
}
m_query->exec("SELECT Name FROM FilterNameTable"_L1);
while (m_query->next()) {
copyQuery.prepare("INSERT INTO FilterNameTable VALUES(NULL, ?)"_L1);
copyQuery.bindValue(0, m_query->value(0).toString());
if (!copyQuery.exec())
return false;
}
m_query->exec("SELECT NameId, FilterAttributeId FROM FilterTable"_L1);
while (m_query->next()) {
copyQuery.prepare("INSERT INTO FilterTable VALUES(?, ?)"_L1);
copyQuery.bindValue(0, m_query->value(0).toInt());
copyQuery.bindValue(1, m_query->value(1).toInt());
if (!copyQuery.exec())
return false;
}
m_query->exec("SELECT Key, Value FROM SettingsTable"_L1);
while (m_query->next()) {
if (m_query->value(0).toString() == "FTS5IndexedNamespaces"_L1)
continue;
copyQuery.prepare("INSERT INTO SettingsTable VALUES(?, ?)"_L1);
copyQuery.bindValue(0, m_query->value(0).toString());
copyQuery.bindValue(1, m_query->value(1));
if (!copyQuery.exec())
return false;
}
return transaction.commit();
};
const bool copied = copyTables();
db.close();
db = QSqlDatabase();
QSqlDatabase::removeDatabase(connectionName);
if (!copied) {
QFile::remove(colFile);
emit error(tr("Cannot copy collection file: %1").arg(colFile));
return false;
}
return true;
}
bool QHelpCollectionHandler::createTables(QSqlQuery *query)
{
const QStringList tables = {
"CREATE TABLE NamespaceTable ("
"Id INTEGER PRIMARY KEY, "
"Name TEXT, "
"FilePath TEXT )"_L1,
"CREATE TABLE FolderTable ("
"Id INTEGER PRIMARY KEY, "
"NamespaceId INTEGER, "
"Name TEXT )"_L1,
"CREATE TABLE FilterAttributeTable ("
"Id INTEGER PRIMARY KEY, "
"Name TEXT )"_L1,
"CREATE TABLE FilterNameTable ("
"Id INTEGER PRIMARY KEY, "
"Name TEXT )"_L1,
"CREATE TABLE FilterTable ("
"NameId INTEGER, "
"FilterAttributeId INTEGER )"_L1,
"CREATE TABLE SettingsTable ("
"Key TEXT PRIMARY KEY, "
"Value BLOB )"_L1
};
for (const QString &q : tables) {
if (!query->exec(q))
return false;
}
return true;
}
bool QHelpCollectionHandler::recreateIndexAndNamespaceFilterTables(QSqlQuery *query)
{
const QStringList tables = {
"DROP TABLE IF EXISTS FileNameTable"_L1,
"DROP TABLE IF EXISTS IndexTable"_L1,
"DROP TABLE IF EXISTS ContentsTable"_L1,
"DROP TABLE IF EXISTS FileFilterTable"_L1, // legacy
"DROP TABLE IF EXISTS IndexFilterTable"_L1, // legacy
"DROP TABLE IF EXISTS ContentsFilterTable"_L1, // legacy
"DROP TABLE IF EXISTS FileAttributeSetTable"_L1, // legacy
"DROP TABLE IF EXISTS OptimizedFilterTable"_L1, // legacy
"DROP TABLE IF EXISTS TimeStampTable"_L1,
"DROP TABLE IF EXISTS VersionTable"_L1,
"DROP TABLE IF EXISTS Filter"_L1,
"DROP TABLE IF EXISTS ComponentTable"_L1,
"DROP TABLE IF EXISTS ComponentMapping"_L1,
"DROP TABLE IF EXISTS ComponentFilter"_L1,
"DROP TABLE IF EXISTS VersionFilter"_L1,
"CREATE TABLE FileNameTable ("
"FolderId INTEGER, "
"Name TEXT, "
"FileId INTEGER PRIMARY KEY, "
"Title TEXT)"_L1,
"CREATE TABLE IndexTable ("
"Id INTEGER PRIMARY KEY, "
"Name TEXT, "
"Identifier TEXT, "
"NamespaceId INTEGER, "
"FileId INTEGER, "
"Anchor TEXT)"_L1,
"CREATE TABLE ContentsTable ("
"Id INTEGER PRIMARY KEY, "
"NamespaceId INTEGER, "
"Data BLOB)"_L1,
"CREATE TABLE FileFilterTable ("
"FilterAttributeId INTEGER, "
"FileId INTEGER)"_L1,
"CREATE TABLE IndexFilterTable ("
"FilterAttributeId INTEGER, "
"IndexId INTEGER)"_L1,
"CREATE TABLE ContentsFilterTable ("
"FilterAttributeId INTEGER, "
"ContentsId INTEGER )"_L1,
"CREATE TABLE FileAttributeSetTable ("
"NamespaceId INTEGER, "
"FilterAttributeSetId INTEGER, "
"FilterAttributeId INTEGER)"_L1,
"CREATE TABLE OptimizedFilterTable ("
"NamespaceId INTEGER, "
"FilterAttributeId INTEGER)"_L1,
"CREATE TABLE TimeStampTable ("
"NamespaceId INTEGER, "
"FolderId INTEGER, "
"FilePath TEXT, "
"Size INTEGER, "
"TimeStamp TEXT)"_L1,
"CREATE TABLE VersionTable ("
"NamespaceId INTEGER, "
"Version TEXT)"_L1,
"CREATE TABLE Filter ("
"FilterId INTEGER PRIMARY KEY, "
"Name TEXT)"_L1,
"CREATE TABLE ComponentTable ("
"ComponentId INTEGER PRIMARY KEY, "
"Name TEXT)"_L1,
"CREATE TABLE ComponentMapping ("
"ComponentId INTEGER, "
"NamespaceId INTEGER)"_L1,
"CREATE TABLE ComponentFilter ("
"ComponentName TEXT, "
"FilterId INTEGER)"_L1,
"CREATE TABLE VersionFilter ("
"Version TEXT, "
"FilterId INTEGER)"_L1
};
for (const QString &q : tables) {
if (!query->exec(q))
return false;
}
return true;
}
QStringList QHelpCollectionHandler::customFilters() const
{
QStringList list;
if (m_query) {
m_query->exec("SELECT Name FROM FilterNameTable"_L1);
while (m_query->next())
list.append(m_query->value(0).toString());
}
return list;
}
QStringList QHelpCollectionHandler::filters() const
{
QStringList list;
if (m_query) {
m_query->exec("SELECT Name FROM Filter ORDER BY Name"_L1);
while (m_query->next())
list.append(m_query->value(0).toString());
}
return list;
}
QStringList QHelpCollectionHandler::availableComponents() const
{
QStringList list;
if (m_query) {
m_query->exec("SELECT DISTINCT Name FROM ComponentTable ORDER BY Name"_L1);
while (m_query->next())
list.append(m_query->value(0).toString());
}
return list;
}
QList<QVersionNumber> QHelpCollectionHandler::availableVersions() const
{
QList<QVersionNumber> list;
if (m_query) {
m_query->exec("SELECT DISTINCT Version FROM VersionTable ORDER BY Version"_L1);
while (m_query->next())
list.append(QVersionNumber::fromString(m_query->value(0).toString()));
}
return list;
}
QMap<QString, QString> QHelpCollectionHandler::namespaceToComponent() const
{
QMap<QString, QString> result;
if (m_query) {
m_query->exec(
"SELECT "
"NamespaceTable.Name, "
"ComponentTable.Name "
"FROM NamespaceTable, "
"ComponentTable, "
"ComponentMapping "
"WHERE NamespaceTable.Id = ComponentMapping.NamespaceId "
"AND ComponentMapping.ComponentId = ComponentTable.ComponentId"_L1);
while (m_query->next())
result.insert(m_query->value(0).toString(), m_query->value(1).toString());
}
return result;
}
QMap<QString, QVersionNumber> QHelpCollectionHandler::namespaceToVersion() const
{
QMap<QString, QVersionNumber> result;
if (m_query) {
m_query->exec(
"SELECT "
"NamespaceTable.Name, "
"VersionTable.Version "
"FROM NamespaceTable, "
"VersionTable "
"WHERE NamespaceTable.Id = VersionTable.NamespaceId"_L1);
while (m_query->next()) {
result.insert(m_query->value(0).toString(),
QVersionNumber::fromString(m_query->value(1).toString()));
}
}
return result;
}
QHelpFilterData QHelpCollectionHandler::filterData(const QString &filterName) const
{
QStringList components;
QList<QVersionNumber> versions;
if (m_query) {
m_query->prepare(
"SELECT ComponentFilter.ComponentName "
"FROM ComponentFilter, Filter "
"WHERE ComponentFilter.FilterId = Filter.FilterId "
"AND Filter.Name = ? "
"ORDER BY ComponentFilter.ComponentName"_L1);
m_query->bindValue(0, filterName);
m_query->exec();
while (m_query->next())
components.append(m_query->value(0).toString());
m_query->prepare(
"SELECT VersionFilter.Version "
"FROM VersionFilter, Filter "
"WHERE VersionFilter.FilterId = Filter.FilterId "
"AND Filter.Name = ? "
"ORDER BY VersionFilter.Version"_L1);
m_query->bindValue(0, filterName);
m_query->exec();
while (m_query->next())
versions.append(QVersionNumber::fromString(m_query->value(0).toString()));
}
QHelpFilterData data;
data.setComponents(components);
data.setVersions(versions);
return data;
}
bool QHelpCollectionHandler::setFilterData(const QString &filterName,
const QHelpFilterData &filterData)
{
if (!removeFilter(filterName))
return false;
m_query->prepare("INSERT INTO Filter VALUES (NULL, ?)"_L1);
m_query->bindValue(0, filterName);
if (!m_query->exec())
return false;
const int filterId = m_query->lastInsertId().toInt();
QVariantList componentList;
QVariantList versionList;
QVariantList filterIdList;
const auto &components = filterData.components();
for (const QString &component : components) {
componentList.append(component);
filterIdList.append(filterId);
}
m_query->prepare("INSERT INTO ComponentFilter VALUES (?, ?)"_L1);
m_query->addBindValue(componentList);
m_query->addBindValue(filterIdList);
if (!m_query->execBatch())
return false;
filterIdList.clear();
const auto &versions = filterData.versions();
for (const QVersionNumber &version : versions) {
versionList.append(version.isNull() ? QString() : version.toString());
filterIdList.append(filterId);
}
m_query->prepare("INSERT INTO VersionFilter VALUES (?, ?)"_L1);
m_query->addBindValue(versionList);
m_query->addBindValue(filterIdList);
if (!m_query->execBatch())
return false;
return true;
}
bool QHelpCollectionHandler::removeFilter(const QString &filterName)
{
m_query->prepare("SELECT FilterId FROM Filter WHERE Name = ?"_L1);
m_query->bindValue(0, filterName);
if (!m_query->exec())
return false;
if (!m_query->next())
return true; // no filter in DB
const int filterId = m_query->value(0).toInt();
m_query->prepare("DELETE FROM Filter WHERE Filter.Name = ?"_L1);
m_query->bindValue(0, filterName);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM ComponentFilter WHERE ComponentFilter.FilterId = ?"_L1);
m_query->bindValue(0, filterId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM VersionFilter WHERE VersionFilter.FilterId = ?"_L1);
m_query->bindValue(0, filterId);
if (!m_query->exec())
return false;
return true;
}
bool QHelpCollectionHandler::removeCustomFilter(const QString &filterName)
{
if (!isDBOpened() || filterName.isEmpty())
return false;
int filterNameId = -1;
m_query->prepare("SELECT Id FROM FilterNameTable WHERE Name=?"_L1);
m_query->bindValue(0, filterName);
m_query->exec();
if (m_query->next())
filterNameId = m_query->value(0).toInt();
if (filterNameId < 0) {
emit error(tr("Unknown filter \"%1\".").arg(filterName));
return false;
}
Transaction transaction(m_connectionName);
m_query->prepare("DELETE FROM FilterTable WHERE NameId=?"_L1);
m_query->bindValue(0, filterNameId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM FilterNameTable WHERE Id=?"_L1);
m_query->bindValue(0, filterNameId);
if (!m_query->exec())
return false;
return transaction.commit();
}
bool QHelpCollectionHandler::addCustomFilter(const QString &filterName,
const QStringList &attributes)
{
if (!isDBOpened() || filterName.isEmpty())
return false;
int nameId = -1;
m_query->prepare("SELECT Id FROM FilterNameTable WHERE Name=?"_L1);
m_query->bindValue(0, filterName);
m_query->exec();
if (m_query->next())
nameId = m_query->value(0).toInt();
m_query->exec("SELECT Id, Name FROM FilterAttributeTable"_L1);
QStringList idsToInsert = attributes;
QMap<QString, int> attributeMap;
while (m_query->next()) {
// all old attributes
const QString attributeName = m_query->value(1).toString();
attributeMap.insert(attributeName, m_query->value(0).toInt());
idsToInsert.removeAll(attributeName);
}
Transaction transaction(m_connectionName);
for (const QString &id : std::as_const(idsToInsert)) {
m_query->prepare("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"_L1);
m_query->bindValue(0, id);
if (!m_query->exec())
return false;
attributeMap.insert(id, m_query->lastInsertId().toInt());
}
if (nameId < 0) {
m_query->prepare("INSERT INTO FilterNameTable VALUES(NULL, ?)"_L1);
m_query->bindValue(0, filterName);
if (m_query->exec())
nameId = m_query->lastInsertId().toInt();
}
if (nameId < 0) {
emit error(tr("Cannot register filter %1.").arg(filterName));
return false;
}
m_query->prepare("DELETE FROM FilterTable WHERE NameId=?"_L1);
m_query->bindValue(0, nameId);
if (!m_query->exec())
return false;
for (const QString &att : attributes) {
m_query->prepare("INSERT INTO FilterTable VALUES(?, ?)"_L1);
m_query->bindValue(0, nameId);
m_query->bindValue(1, attributeMap[att]);
if (!m_query->exec())
return false;
}
return transaction.commit();
}
QHelpCollectionHandler::FileInfo QHelpCollectionHandler::registeredDocumentation(
const QString &namespaceName) const
{
FileInfo fileInfo;
if (!m_query)
return fileInfo;
m_query->prepare(
"SELECT "
"NamespaceTable.Name, "
"NamespaceTable.FilePath, "
"FolderTable.Name "
"FROM "
"NamespaceTable, "
"FolderTable "
"WHERE NamespaceTable.Id = FolderTable.NamespaceId "
"AND NamespaceTable.Name = ? LIMIT 1"_L1);
m_query->bindValue(0, namespaceName);
if (!m_query->exec() || !m_query->next())
return fileInfo;
fileInfo.namespaceName = m_query->value(0).toString();
fileInfo.fileName = m_query->value(1).toString();
fileInfo.folderName = m_query->value(2).toString();
m_query->clear();
return fileInfo;
}
QHelpCollectionHandler::FileInfoList QHelpCollectionHandler::registeredDocumentations() const
{
FileInfoList list;
if (!m_query)
return list;
m_query->exec(
"SELECT "
"NamespaceTable.Name, "
"NamespaceTable.FilePath, "
"FolderTable.Name "
"FROM "
"NamespaceTable, "
"FolderTable "
"WHERE NamespaceTable.Id = FolderTable.NamespaceId"_L1);
while (m_query->next()) {
FileInfo fileInfo;
fileInfo.namespaceName = m_query->value(0).toString();
fileInfo.fileName = m_query->value(1).toString();
fileInfo.folderName = m_query->value(2).toString();
list.append(fileInfo);
}
return list;
}
bool QHelpCollectionHandler::registerDocumentation(const QString &fileName)
{
if (!isDBOpened())
return false;
QHelpDBReader reader(fileName, QHelpGlobal::uniquifyConnectionName(
"QHelpCollectionHandler"_L1, this), nullptr);
if (!reader.init()) {
emit error(tr("Cannot open documentation file %1.").arg(fileName));
return false;
}
const QString &ns = reader.namespaceName();
if (ns.isEmpty()) {
emit error(tr("Invalid documentation file \"%1\".").arg(fileName));
return false;
}
// A file holds exactly one namespace. A registration of the same file under
// a different namespace is stale (e.g. the file was regenerated for another
// version), so drop it. Files at other paths are never affected.
const QString absFileName = QFileInfo(fileName).absoluteFilePath();
const auto ®istered = registeredDocumentations();
for (const FileInfo &info : registered) {
if (info.namespaceName != ns && absoluteDocPath(info.fileName) == absFileName)
unregisterDocumentation(info.namespaceName);
}
Transaction transaction(m_connectionName);
const int nsId = registerNamespace(ns, fileName);
if (nsId < 1)
return false;
const int vfId = registerVirtualFolder(reader.virtualFolder(), nsId);
if (vfId < 1)
return false;
registerVersion(reader.version(), nsId);
registerFilterAttributes(reader.filterAttributeSets(), nsId); // qset, what happens when removing documentation?
const auto &customFilters = reader.customFilters();
for (const QString &filterName : customFilters)
addCustomFilter(filterName, reader.filterAttributes(filterName));
if (!registerIndexTable(reader.indexTable(), nsId, vfId, registeredDocumentation(ns).fileName))
return false;
return transaction.commit();
}
bool QHelpCollectionHandler::unregisterDocumentation(const QString &namespaceName)
{
if (!isDBOpened())
return false;
m_query->prepare("SELECT Id FROM NamespaceTable WHERE Name = ?"_L1);
m_query->bindValue(0, namespaceName);
m_query->exec();
if (!m_query->next()) {
emit error(tr("The namespace %1 was not registered.").arg(namespaceName));
return false;
}
const int nsId = m_query->value(0).toInt();
m_query->prepare("SELECT Id FROM FolderTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
m_query->exec();
if (!m_query->next()) {
emit error(tr("The namespace %1 was not registered.").arg(namespaceName));
return false;
}
const int vfId = m_query->value(0).toInt();
Transaction transaction(m_connectionName);
m_query->prepare("DELETE FROM NamespaceTable WHERE Id = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM FolderTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
if (!unregisterIndexTable(nsId, vfId) || !transaction.commit())
return false;
scheduleVacuum();
return true;
}
static QHelpCollectionHandler::FileInfo extractFileInfo(const QUrl &url)
{
QHelpCollectionHandler::FileInfo fileInfo;
if (!url.isValid() || url.toString().count(u'/') < 4
|| url.scheme() != "qthelp"_L1) {
return fileInfo;
}
fileInfo.namespaceName = url.authority();
fileInfo.fileName = url.path();
if (fileInfo.fileName.startsWith(u'/'))
fileInfo.fileName = fileInfo.fileName.mid(1);
fileInfo.folderName = fileInfo.fileName.mid(0, fileInfo.fileName.indexOf(u'/', 1));
fileInfo.fileName.remove(0, fileInfo.folderName.size() + 1);
return fileInfo;
}
bool QHelpCollectionHandler::fileExists(const QUrl &url) const
{
if (!isDBOpened())
return false;
const FileInfo fileInfo = extractFileInfo(url);
if (fileInfo.namespaceName.isEmpty())
return false;
m_query->prepare(
"SELECT COUNT (DISTINCT NamespaceTable.Id) "
"FROM "
"FileNameTable, "
"NamespaceTable, "
"FolderTable "
"WHERE FolderTable.Name = ? "
"AND FileNameTable.Name = ? "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.NamespaceId = NamespaceTable.Id"_L1);
m_query->bindValue(0, fileInfo.folderName);
m_query->bindValue(1, fileInfo.fileName);
if (!m_query->exec() || !m_query->next())
return false;
const int count = m_query->value(0).toInt();
m_query->clear();
return count;
}
static QString prepareFilterQuery(const QString &filterName)
{
if (filterName.isEmpty())
return {};
return " AND EXISTS(SELECT * FROM Filter WHERE Filter.Name = ?) "
"AND ("
"(NOT EXISTS(" // 1. filter by component
"SELECT * FROM "
"ComponentFilter, "
"Filter "
"WHERE ComponentFilter.FilterId = Filter.FilterId "
"AND Filter.Name = ?) "
"OR NamespaceTable.Id IN ("
"SELECT "
"NamespaceTable.Id "
"FROM "
"NamespaceTable, "
"ComponentTable, "
"ComponentMapping, "
"ComponentFilter, "
"Filter "
"WHERE ComponentMapping.NamespaceId = NamespaceTable.Id "
"AND ComponentTable.ComponentId = ComponentMapping.ComponentId "
"AND ((ComponentTable.Name = ComponentFilter.ComponentName) "
"OR (ComponentTable.Name IS NULL AND ComponentFilter.ComponentName IS NULL)) "
"AND ComponentFilter.FilterId = Filter.FilterId "
"AND Filter.Name = ?))"
" AND "
"(NOT EXISTS(" // 2. filter by version
"SELECT * FROM "
"VersionFilter, "
"Filter "
"WHERE VersionFilter.FilterId = Filter.FilterId "
"AND Filter.Name = ?) "
"OR NamespaceTable.Id IN ("
"SELECT "
"NamespaceTable.Id "
"FROM "
"NamespaceTable, "
"VersionFilter, "
"VersionTable, "
"Filter "
"WHERE VersionFilter.FilterId = Filter.FilterId "
"AND ((VersionFilter.Version = VersionTable.Version) "
"OR (VersionFilter.Version IS NULL AND VersionTable.Version IS NULL)) "
"AND VersionTable.NamespaceId = NamespaceTable.Id "
"AND Filter.Name = ?))"
")"_L1;
}
static void bindFilterQuery(QSqlQuery *query, int bindStart, const QString &filterName)
{
if (filterName.isEmpty())
return;
query->bindValue(bindStart, filterName);
query->bindValue(bindStart + 1, filterName);
query->bindValue(bindStart + 2, filterName);
query->bindValue(bindStart + 3, filterName);
query->bindValue(bindStart + 4, filterName);
}
static QString prepareFilterQuery(int attributesCount,
const QString &idTableName,
const QString &idColumnName,
const QString &filterTableName,
const QString &filterColumnName)
{
if (!attributesCount)
return {};
QString filterQuery = " AND (%1.%2 IN ("_L1.arg(idTableName, idColumnName);
const QString filterQueryTemplate =
"SELECT %1.%2 "
"FROM %1, FilterAttributeTable "
"WHERE %1.FilterAttributeId = FilterAttributeTable.Id "
"AND FilterAttributeTable.Name = ?"_L1.arg(filterTableName, filterColumnName);
for (int i = 0; i < attributesCount; ++i) {
if (i > 0)
filterQuery.append(" INTERSECT "_L1);
filterQuery.append(filterQueryTemplate);
}
filterQuery.append(") OR NamespaceTable.Id IN ("_L1);
const QString optimizedFilterQueryTemplate =
"SELECT OptimizedFilterTable.NamespaceId "
"FROM OptimizedFilterTable, FilterAttributeTable "
"WHERE OptimizedFilterTable.FilterAttributeId = FilterAttributeTable.Id "
"AND FilterAttributeTable.Name = ?"_L1;
for (int i = 0; i < attributesCount; ++i) {
if (i > 0)
filterQuery.append(" INTERSECT "_L1);
filterQuery.append(optimizedFilterQueryTemplate);
}
filterQuery.append("))"_L1);
return filterQuery;
}
static void bindFilterQuery(QSqlQuery *query, int startingBindPos, const QStringList &filterAttributes)
{
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < filterAttributes.size(); j++) {
query->bindValue(i * filterAttributes.size() + j + startingBindPos,
filterAttributes.at(j));
}
}
}
QString QHelpCollectionHandler::namespaceForFile(const QUrl &url,
const QStringList &filterAttributes) const
{
if (!isDBOpened())
return {};
const FileInfo fileInfo = extractFileInfo(url);
if (fileInfo.namespaceName.isEmpty())
return {};
const QString filterlessQuery =
"SELECT DISTINCT "
"NamespaceTable.Name "
"FROM "
"FileNameTable, "
"NamespaceTable, "
"FolderTable "
"WHERE FolderTable.Name = ? "
"AND FileNameTable.Name = ? "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.NamespaceId = NamespaceTable.Id"_L1;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterAttributes.size(), "FileNameTable"_L1, "FileId"_L1,
"FileFilterTable"_L1, "FileId"_L1);
m_query->prepare(filterQuery);
m_query->bindValue(0, fileInfo.folderName);
m_query->bindValue(1, fileInfo.fileName);
bindFilterQuery(m_query.get(), 2, filterAttributes);
if (!m_query->exec())
return {};
QStringList namespaceList;
while (m_query->next())
namespaceList.append(m_query->value(0).toString());
if (namespaceList.isEmpty())
return {};
if (namespaceList.contains(fileInfo.namespaceName))
return fileInfo.namespaceName;
const QString originalVersion = namespaceVersion(fileInfo.namespaceName);
// Otherwise, fall back to the highest available version, with ties broken by name,
// so that the result doesn't depend on the order of rows in the collection file.
QString fallbackNamespace;
QVersionNumber fallbackVersion;
for (const QString &ns : std::as_const(namespaceList)) {
const QString nsVersion = namespaceVersion(ns);
if (originalVersion == nsVersion)
return ns;
const QVersionNumber version = QVersionNumber::fromString(nsVersion);
const int compare = QVersionNumber::compare(version, fallbackVersion);
if (fallbackNamespace.isEmpty() || compare > 0
|| (compare == 0 && ns < fallbackNamespace)) {
fallbackNamespace = ns;
fallbackVersion = version;
}
}
return fallbackNamespace;
}
QString QHelpCollectionHandler::namespaceForFile(const QUrl &url,
const QString &filterName) const
{
if (!isDBOpened())
return {};
const FileInfo fileInfo = extractFileInfo(url);
if (fileInfo.namespaceName.isEmpty())
return {};
const QString filterlessQuery =
"SELECT DISTINCT "
"NamespaceTable.Name "
"FROM "
"FileNameTable, "
"NamespaceTable, "
"FolderTable "
"WHERE FolderTable.Name = ? "
"AND FileNameTable.Name = ? "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.NamespaceId = NamespaceTable.Id"_L1;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterName);
m_query->prepare(filterQuery);
m_query->bindValue(0, fileInfo.folderName);
m_query->bindValue(1, fileInfo.fileName);
bindFilterQuery(m_query.get(), 2, filterName);
if (!m_query->exec())
return {};
QStringList namespaceList;
while (m_query->next())
namespaceList.append(m_query->value(0).toString());
if (namespaceList.isEmpty())
return {};
if (namespaceList.contains(fileInfo.namespaceName))
return fileInfo.namespaceName;
const QString originalVersion = namespaceVersion(fileInfo.namespaceName);
// Otherwise, fall back to the highest available version, with ties broken by name,
// so that the result doesn't depend on the order of rows in the collection file.
QString fallbackNamespace;
QVersionNumber fallbackVersion;
for (const QString &ns : std::as_const(namespaceList)) {
const QString nsVersion = namespaceVersion(ns);
if (originalVersion == nsVersion)
return ns;
const QVersionNumber version = QVersionNumber::fromString(nsVersion);
const int compare = QVersionNumber::compare(version, fallbackVersion);
if (fallbackNamespace.isEmpty() || compare > 0
|| (compare == 0 && ns < fallbackNamespace)) {
fallbackNamespace = ns;
fallbackVersion = version;
}
}
return fallbackNamespace;
}
QStringList QHelpCollectionHandler::files(const QString &namespaceName,
const QStringList &filterAttributes,
const QString &extensionFilter) const
{
if (!isDBOpened())
return {};
const QString extensionQuery = extensionFilter.isEmpty()
? QString() : " AND FileNameTable.Name LIKE ?"_L1;
const QString filterlessQuery =
"SELECT "
"FolderTable.Name, "
"FileNameTable.Name "
"FROM "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Name = ?"_L1 + extensionQuery;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterAttributes.size(), "FileNameTable"_L1, "FileId"_L1,
"FileFilterTable"_L1, "FileId"_L1);
m_query->prepare(filterQuery);
m_query->bindValue(0, namespaceName);
int bindCount = 1;
if (!extensionFilter.isEmpty()) {
m_query->bindValue(bindCount, "%.%1"_L1.arg(extensionFilter));
++bindCount;
}
bindFilterQuery(m_query.get(), bindCount, filterAttributes);
if (!m_query->exec())
return {};
QStringList fileNames;
while (m_query->next())
fileNames.append(m_query->value(0).toString() + u'/' + m_query->value(1).toString());
return fileNames;
}
QStringList QHelpCollectionHandler::files(const QString &namespaceName,
const QString &filterName,
const QString &extensionFilter) const
{
if (!isDBOpened())
return {};
const QString extensionQuery = extensionFilter.isEmpty()
? QString() : " AND FileNameTable.Name LIKE ?"_L1;
const QString filterlessQuery =
"SELECT "
"FolderTable.Name, "
"FileNameTable.Name "
"FROM "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Name = ?"_L1 + extensionQuery;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterName);
m_query->prepare(filterQuery);
m_query->bindValue(0, namespaceName);
int bindCount = 1;
if (!extensionFilter.isEmpty()) {
m_query->bindValue(bindCount, "%.%1"_L1.arg(extensionFilter));
++bindCount;
}
bindFilterQuery(m_query.get(), bindCount, filterName);
if (!m_query->exec())
return{};
QStringList fileNames;
while (m_query->next())
fileNames.append(m_query->value(0).toString() + u'/' + m_query->value(1).toString());
return fileNames;
}
QUrl QHelpCollectionHandler::findFile(const QUrl &url, const QStringList &filterAttributes) const
{
if (!isDBOpened())
return {};
const QString namespaceName = namespaceForFile(url, filterAttributes);
if (namespaceName.isEmpty())
return {};
QUrl result = url;
result.setAuthority(namespaceName);
return result;
}
QUrl QHelpCollectionHandler::findFile(const QUrl &url, const QString &filterName) const
{
if (!isDBOpened())
return {};
const QString namespaceName = namespaceForFile(url, filterName);
if (namespaceName.isEmpty())
return {};
QUrl result = url;
result.setAuthority(namespaceName);
return result;
}
QByteArray QHelpCollectionHandler::fileData(const QUrl &url) const
{
if (!isDBOpened())
return {};
const QString namespaceName = namespaceForFile(url, QString());
if (namespaceName.isEmpty())
return {};
const FileInfo fileInfo = extractFileInfo(url);
const FileInfo docInfo = registeredDocumentation(namespaceName);
const QString absFileName = absoluteDocPath(docInfo.fileName);
QHelpDBReader reader(absFileName, QHelpGlobal::uniquifyConnectionName(
docInfo.fileName, const_cast<QHelpCollectionHandler *>(this)), nullptr);
if (!reader.init())
return {};
return reader.fileData(fileInfo.folderName, fileInfo.fileName);
}
QStringList QHelpCollectionHandler::indicesForFilter(const QStringList &filterAttributes) const
{
QStringList indices;
if (!isDBOpened())
return indices;
const QString filterlessQuery =
"SELECT DISTINCT "
"IndexTable.Name "
"FROM "
"IndexTable, "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE IndexTable.FileId = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND IndexTable.NamespaceId = NamespaceTable.Id"_L1;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterAttributes.size(), "IndexTable"_L1, "Id"_L1,
"IndexFilterTable"_L1, "IndexId"_L1)
+ " ORDER BY LOWER(IndexTable.Name), IndexTable.Name"_L1;
// this doesn't work: ASC COLLATE NOCASE
m_query->prepare(filterQuery);
bindFilterQuery(m_query.get(), 0, filterAttributes);
m_query->exec();
while (m_query->next())
indices.append(m_query->value(0).toString());
return indices;
}
QStringList QHelpCollectionHandler::indicesForFilter(const QString &filterName) const
{
QStringList indices;
if (!isDBOpened())
return indices;
const QString filterlessQuery =
"SELECT DISTINCT "
"IndexTable.Name "
"FROM "
"IndexTable, "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE IndexTable.FileId = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND IndexTable.NamespaceId = NamespaceTable.Id"_L1;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterName)
+ " ORDER BY LOWER(IndexTable.Name), IndexTable.Name"_L1;
m_query->prepare(filterQuery);
bindFilterQuery(m_query.get(), 0, filterName);
m_query->exec();
while (m_query->next())
indices.append(m_query->value(0).toString());
return indices;
}
static QString getTitle(const QByteArray &contents)
{
if (!contents.size())
return {};
int depth = 0;
QString link;
QString title;
QDataStream s(contents);
s >> depth;
s >> link;
s >> title;
return title;
}
QList<QHelpCollectionHandler::ContentsData> QHelpCollectionHandler::contentsForFilter(
const QStringList &filterAttributes) const
{
if (!isDBOpened())
return {};
const QString filterlessQuery =
"SELECT DISTINCT "
"NamespaceTable.Name, "
"FolderTable.Name, "
"ContentsTable.Data, "
"VersionTable.Version "
"FROM "
"FolderTable, "
"NamespaceTable, "
"ContentsTable, "
"VersionTable "
"WHERE ContentsTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Id = FolderTable.NamespaceId "
"AND ContentsTable.NamespaceId = NamespaceTable.Id "
"AND VersionTable.NamespaceId = NamespaceTable.Id"_L1;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterAttributes.size(), "ContentsTable"_L1, "Id"_L1,
"ContentsFilterTable"_L1, "ContentsId"_L1);
m_query->prepare(filterQuery);
bindFilterQuery(m_query.get(), 0, filterAttributes);
m_query->exec();
QMap<QString, QMap<QVersionNumber, ContentsData>> contentsMap;
while (m_query->next()) {
const QString namespaceName = m_query->value(0).toString();
const QByteArray contents = m_query->value(2).toByteArray();
const QString versionString = m_query->value(3).toString();
const QString title = getTitle(contents);
const QVersionNumber version = QVersionNumber::fromString(versionString);
// get existing or insert a new one otherwise
ContentsData &contentsData = contentsMap[title][version];
contentsData.namespaceName = namespaceName;
contentsData.folderName = m_query->value(1).toString();
contentsData.contentsList.append(contents);
}
QList<QHelpCollectionHandler::ContentsData> result;
for (const auto &versionContents : std::as_const(contentsMap)) {
// insert items in the reverse order of version number
const auto itBegin = versionContents.constBegin();
auto it = versionContents.constEnd();
while (it != itBegin) {
--it;
result.append(it.value());
}
}
return result;
}
QList<QHelpCollectionHandler::ContentsData> QHelpCollectionHandler::contentsForFilter(const QString &filterName) const
{
if (!isDBOpened())
return {};
const QString filterlessQuery =
"SELECT DISTINCT "
"NamespaceTable.Name, "
"FolderTable.Name, "
"ContentsTable.Data, "
"VersionTable.Version "
"FROM "
"FolderTable, "
"NamespaceTable, "
"ContentsTable, "
"VersionTable "
"WHERE ContentsTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Id = FolderTable.NamespaceId "
"AND ContentsTable.NamespaceId = NamespaceTable.Id "
"AND VersionTable.NamespaceId = NamespaceTable.Id"_L1;
const QString filterQuery = filterlessQuery + prepareFilterQuery(filterName);
m_query->prepare(filterQuery);
bindFilterQuery(m_query.get(), 0, filterName);
m_query->exec();
QMap<QString, QMap<QVersionNumber, ContentsData>> contentsMap;
while (m_query->next()) {
const QString namespaceName = m_query->value(0).toString();
const QByteArray contents = m_query->value(2).toByteArray();
const QString versionString = m_query->value(3).toString();
const QString title = getTitle(contents);
const QVersionNumber version = QVersionNumber::fromString(versionString);
// get existing or insert a new one otherwise
ContentsData &contentsData = contentsMap[title][version];
contentsData.namespaceName = namespaceName;
contentsData.folderName = m_query->value(1).toString();
contentsData.contentsList.append(contents);
}
QList<QHelpCollectionHandler::ContentsData> result;
for (const auto &versionContents : std::as_const(contentsMap)) {
// insert items in the reverse order of version number
const auto itBegin = versionContents.constBegin();
auto it = versionContents.constEnd();
while (it != itBegin) {
--it;
result.append(it.value());
}
}
return result;
}
bool QHelpCollectionHandler::removeCustomValue(const QString &key)
{
if (!isDBOpened())
return false;
m_query->prepare("DELETE FROM SettingsTable WHERE Key=?"_L1);
m_query->bindValue(0, key);
return m_query->exec();
}
QVariant QHelpCollectionHandler::customValue(const QString &key,
const QVariant &defaultValue) const
{
if (!m_query)
return defaultValue;
m_query->prepare("SELECT COUNT(Key) FROM SettingsTable WHERE Key=?"_L1);
m_query->bindValue(0, key);
if (!m_query->exec() || !m_query->next() || !m_query->value(0).toInt()) {
m_query->clear();
return defaultValue;
}
m_query->clear();
m_query->prepare("SELECT Value FROM SettingsTable WHERE Key=?"_L1);
m_query->bindValue(0, key);
if (m_query->exec() && m_query->next()) {
const QVariant &value = m_query->value(0);
m_query->clear();
return value;
}
return defaultValue;
}
bool QHelpCollectionHandler::setCustomValue(const QString &key,
const QVariant &value)
{
if (!isDBOpened())
return false;
m_query->prepare("SELECT Value FROM SettingsTable WHERE Key=?"_L1);
m_query->bindValue(0, key);
m_query->exec();
if (m_query->next()) {
m_query->prepare("UPDATE SettingsTable SET Value=? where Key=?"_L1);
m_query->bindValue(0, value);
m_query->bindValue(1, key);
} else {
m_query->prepare("INSERT INTO SettingsTable VALUES(?, ?)"_L1);
m_query->bindValue(0, key);
m_query->bindValue(1, value);
}
return m_query->exec();
}
bool QHelpCollectionHandler::registerFilterAttributes(const QList<QStringList> &attributeSets,
int nsId)
{
if (!isDBOpened())
return false;
m_query->exec("SELECT Name FROM FilterAttributeTable"_L1);
QSet<QString> atts;
while (m_query->next())
atts.insert(m_query->value(0).toString());
for (const QStringList &attributeSet : attributeSets) {
for (const QString &attribute : attributeSet) {
if (!atts.contains(attribute)) {
m_query->prepare("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"_L1);
m_query->bindValue(0, attribute);
m_query->exec();
}
}
}
return registerFileAttributeSets(attributeSets, nsId);
}
bool QHelpCollectionHandler::registerFileAttributeSets(const QList<QStringList> &attributeSets,
int nsId)
{
if (!isDBOpened())
return false;
if (attributeSets.isEmpty())
return true;
QVariantList nsIds;
QVariantList attributeSetIds;
QVariantList filterAttributeIds;
if (!m_query->exec("SELECT MAX(FilterAttributeSetId) FROM FileAttributeSetTable"_L1)
|| !m_query->next()) {
return false;
}
int attributeSetId = m_query->value(0).toInt();
for (const QStringList &attributeSet : attributeSets) {
++attributeSetId;
for (const QString &attribute : attributeSet) {
m_query->prepare("SELECT Id FROM FilterAttributeTable WHERE Name=?"_L1);
m_query->bindValue(0, attribute);
if (!m_query->exec() || !m_query->next())
return false;
nsIds.append(nsId);
attributeSetIds.append(attributeSetId);
filterAttributeIds.append(m_query->value(0).toInt());
}
}
m_query->prepare("INSERT INTO FileAttributeSetTable "
"(NamespaceId, FilterAttributeSetId, FilterAttributeId) "
"VALUES(?, ?, ?)"_L1);
m_query->addBindValue(nsIds);
m_query->addBindValue(attributeSetIds);
m_query->addBindValue(filterAttributeIds);
return m_query->execBatch();
}
QStringList QHelpCollectionHandler::filterAttributes() const
{
QStringList list;
if (m_query) {
m_query->exec("SELECT Name FROM FilterAttributeTable"_L1);
while (m_query->next())
list.append(m_query->value(0).toString());
}
return list;
}
QStringList QHelpCollectionHandler::filterAttributes(const QString &filterName) const
{
QStringList list;
if (m_query) {
m_query->prepare(
"SELECT "
"FilterAttributeTable.Name "
"FROM "
"FilterAttributeTable, "
"FilterTable, "
"FilterNameTable "
"WHERE FilterAttributeTable.Id = FilterTable.FilterAttributeId "
"AND FilterTable.NameId = FilterNameTable.Id "
"AND FilterNameTable.Name=?"_L1);
m_query->bindValue(0, filterName);
m_query->exec();
while (m_query->next())
list.append(m_query->value(0).toString());
}
return list;
}
QList<QStringList> QHelpCollectionHandler::filterAttributeSets(const QString &namespaceName) const
{
if (!isDBOpened())
return {};
m_query->prepare(
"SELECT "
"FileAttributeSetTable.FilterAttributeSetId, "
"FilterAttributeTable.Name "
"FROM "
"FileAttributeSetTable, "
"FilterAttributeTable, "
"NamespaceTable "
"WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
"AND FileAttributeSetTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Name = ? "
"ORDER BY FileAttributeSetTable.FilterAttributeSetId"_L1);
m_query->bindValue(0, namespaceName);
m_query->exec();
int oldId = -1;
QList<QStringList> result;
while (m_query->next()) {
const int id = m_query->value(0).toInt();
if (id != oldId) {
result.append(QStringList());
oldId = id;
}
result.last().append(m_query->value(1).toString());
}
if (result.isEmpty())
result.append(QStringList());
return result;
}
QString QHelpCollectionHandler::namespaceVersion(const QString &namespaceName) const
{
if (!m_query)
return {};
m_query->prepare(
"SELECT "
"VersionTable.Version "
"FROM "
"NamespaceTable, "
"VersionTable "
"WHERE NamespaceTable.Name = ? "
"AND NamespaceTable.Id = VersionTable.NamespaceId"_L1);
m_query->bindValue(0, namespaceName);
if (!m_query->exec() || !m_query->next())
return {};
const QString ret = m_query->value(0).toString();
m_query->clear();
return ret;
}
int QHelpCollectionHandler::registerNamespace(const QString &nspace, const QString &fileName)
{
const int errorValue = -1;
if (!m_query)
return errorValue;
m_query->prepare("SELECT COUNT(Id) FROM NamespaceTable WHERE Name=?"_L1);
m_query->bindValue(0, nspace);
m_query->exec();
while (m_query->next()) {
if (m_query->value(0).toInt() > 0) {
emit error(tr("Namespace %1 already exists.").arg(nspace));
return errorValue;
}
}
QFileInfo fi(m_collectionFile);
m_query->prepare("INSERT INTO NamespaceTable VALUES(NULL, ?, ?)"_L1);
m_query->bindValue(0, nspace);
m_query->bindValue(1, fi.absoluteDir().relativeFilePath(fileName));
int namespaceId = errorValue;
if (m_query->exec()) {
namespaceId = m_query->lastInsertId().toInt();
m_query->clear();
}
if (namespaceId < 1) {
emit error(tr("Cannot register namespace \"%1\".").arg(nspace));
return errorValue;
}
return namespaceId;
}
int QHelpCollectionHandler::registerVirtualFolder(const QString &folderName, int namespaceId)
{
if (!m_query)
return false;
m_query->prepare("INSERT INTO FolderTable VALUES(NULL, ?, ?)"_L1);
m_query->bindValue(0, namespaceId);
m_query->bindValue(1, folderName);
int virtualId = -1;
if (m_query->exec()) {
virtualId = m_query->lastInsertId().toInt();
m_query->clear();
}
if (virtualId < 1) {
emit error(tr("Cannot register virtual folder '%1'.").arg(folderName));
return -1;
}
if (registerComponent(folderName, namespaceId) < 0)
return -1;
return virtualId;
}
int QHelpCollectionHandler::registerComponent(const QString &componentName, int namespaceId)
{
m_query->prepare("SELECT ComponentId FROM ComponentTable WHERE Name = ?"_L1);
m_query->bindValue(0, componentName);
if (!m_query->exec())
return -1;
if (!m_query->next()) {
m_query->prepare("INSERT INTO ComponentTable VALUES(NULL, ?)"_L1);
m_query->bindValue(0, componentName);
if (!m_query->exec())
return -1;
m_query->prepare("SELECT ComponentId FROM ComponentTable WHERE Name = ?"_L1);
m_query->bindValue(0, componentName);
if (!m_query->exec() || !m_query->next())
return -1;
}
const int componentId = m_query->value(0).toInt();
m_query->prepare("INSERT INTO ComponentMapping VALUES(?, ?)"_L1);
m_query->bindValue(0, componentId);
m_query->bindValue(1, namespaceId);
if (!m_query->exec())
return -1;
return componentId;
}
bool QHelpCollectionHandler::registerVersion(const QString &version, int namespaceId)
{
if (!m_query)
return false;
m_query->prepare("INSERT INTO VersionTable (NamespaceId, Version) VALUES(?, ?)"_L1);
m_query->addBindValue(namespaceId);
m_query->addBindValue(version);
return m_query->exec();
}
bool QHelpCollectionHandler::registerIndexAndNamespaceFilterTables(
const QString &nameSpace, bool createDefaultVersionFilter)
{
if (!isDBOpened())
return false;
m_query->prepare("SELECT Id, FilePath FROM NamespaceTable WHERE Name=?"_L1);
m_query->bindValue(0, nameSpace);
m_query->exec();
if (!m_query->next())
return false;
const int nsId = m_query->value(0).toInt();
const QString fileName = m_query->value(1).toString();
m_query->prepare("SELECT Id, Name FROM FolderTable WHERE NamespaceId=?"_L1);
m_query->bindValue(0, nsId);
m_query->exec();
if (!m_query->next())
return false;
const int vfId = m_query->value(0).toInt();
const QString vfName = m_query->value(1).toString();
const QString absFileName = absoluteDocPath(fileName);
QHelpDBReader reader(absFileName, QHelpGlobal::uniquifyConnectionName(
fileName, this), this);
if (!reader.init())
return false;
registerComponent(vfName, nsId);
registerVersion(reader.version(), nsId);
if (!registerFileAttributeSets(reader.filterAttributeSets(), nsId))
return false;
if (!registerIndexTable(reader.indexTable(), nsId, vfId, fileName))
return false;
if (createDefaultVersionFilter)
createVersionFilter(reader.version());
return true;
}
void QHelpCollectionHandler::createVersionFilter(const QString &version)
{
if (version.isEmpty())
return;
const QVersionNumber versionNumber = QVersionNumber::fromString(version);
if (versionNumber.isNull())
return;
const QString filterName = tr("Version %1").arg(version);
if (filters().contains(filterName))
return;
QHelpFilterData filterData;
filterData.setVersions({versionNumber});
setFilterData(filterName, filterData);
}
bool QHelpCollectionHandler::registerIndexTable(const QHelpDBReader::IndexTable &indexTable,
int nsId, int vfId, const QString &fileName)
{
Transaction transaction(m_connectionName);
QMap<QString, QVariantList> filterAttributeToNewFileId;
QVariantList fileFolderIds;
QVariantList fileNames;
QVariantList fileTitles;
const int fileSize = indexTable.fileItems.size();
fileFolderIds.reserve(fileSize);
fileNames.reserve(fileSize);
fileTitles.reserve(fileSize);
if (!m_query->exec("SELECT MAX(FileId) FROM FileNameTable"_L1) || !m_query->next())
return false;
const int maxFileId = m_query->value(0).toInt();
int newFileId = 0;
for (const QHelpDBReader::FileItem &item : indexTable.fileItems) {
fileFolderIds.append(vfId);
fileNames.append(item.name);
fileTitles.append(item.title);
for (const QString &filterAttribute : item.filterAttributes)
filterAttributeToNewFileId[filterAttribute].append(maxFileId + newFileId + 1);
++newFileId;
}
m_query->prepare("INSERT INTO FileNameTable VALUES(?, ?, NULL, ?)"_L1);
m_query->addBindValue(fileFolderIds);
m_query->addBindValue(fileNames);
m_query->addBindValue(fileTitles);
if (!m_query->execBatch())
return false;
for (auto it = filterAttributeToNewFileId.cbegin(),
end = filterAttributeToNewFileId.cend(); it != end; ++it) {
const QString filterAttribute = it.key();
m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
m_query->bindValue(0, filterAttribute);
if (!m_query->exec() || !m_query->next())
return false;
const int attributeId = m_query->value(0).toInt();
QVariantList attributeIds;
for (int i = 0; i < it.value().size(); i++)
attributeIds.append(attributeId);
m_query->prepare("INSERT INTO FileFilterTable VALUES(?, ?)"_L1);
m_query->addBindValue(attributeIds);
m_query->addBindValue(it.value());
if (!m_query->execBatch())
return false;
}
QMap<QString, QVariantList> filterAttributeToNewIndexId;
if (!m_query->exec("SELECT MAX(Id) FROM IndexTable"_L1) || !m_query->next())
return false;
const int maxIndexId = m_query->value(0).toInt();
int newIndexId = 0;
QVariantList indexNames;
QVariantList indexIdentifiers;
QVariantList indexNamespaceIds;
QVariantList indexFileIds;
QVariantList indexAnchors;
const int indexSize = indexTable.indexItems.size();
indexNames.reserve(indexSize);
indexIdentifiers.reserve(indexSize);
indexNamespaceIds.reserve(indexSize);
indexFileIds.reserve(indexSize);
indexAnchors.reserve(indexSize);
for (const QHelpDBReader::IndexItem &item : indexTable.indexItems) {
// The file id is an index into the fileItems of this documentation. Anything else
// would make the keyword point to a file of other documentation.
if (item.fileId < 0 || item.fileId >= fileSize) {
emit error(tr("Invalid file reference for keyword '%1' in '%2'.")
.arg(item.name, fileName));
return false;
}
indexNames.append(item.name);
indexIdentifiers.append(item.identifier);
indexNamespaceIds.append(nsId);
indexFileIds.append(maxFileId + item.fileId + 1);
indexAnchors.append(item.anchor);
for (const QString &filterAttribute : item.filterAttributes)
filterAttributeToNewIndexId[filterAttribute].append(maxIndexId + newIndexId + 1);
++newIndexId;
}
m_query->prepare("INSERT INTO IndexTable VALUES(NULL, ?, ?, ?, ?, ?)"_L1);
m_query->addBindValue(indexNames);
m_query->addBindValue(indexIdentifiers);
m_query->addBindValue(indexNamespaceIds);
m_query->addBindValue(indexFileIds);
m_query->addBindValue(indexAnchors);
if (!m_query->execBatch())
return false;
for (auto it = filterAttributeToNewIndexId.cbegin(),
end = filterAttributeToNewIndexId.cend(); it != end; ++it) {
const QString filterAttribute = it.key();
m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
m_query->bindValue(0, filterAttribute);
if (!m_query->exec() || !m_query->next())
return false;
const int attributeId = m_query->value(0).toInt();
QVariantList attributeIds;
for (int i = 0; i < it.value().size(); i++)
attributeIds.append(attributeId);
m_query->prepare("INSERT INTO IndexFilterTable VALUES(?, ?)"_L1);
m_query->addBindValue(attributeIds);
m_query->addBindValue(it.value());
if (!m_query->execBatch())
return false;
}
QMap<QString, QVariantList> filterAttributeToNewContentsId;
QVariantList contentsNsIds;
QVariantList contentsData;
const int contentsSize = indexTable.contentsItems.size();
contentsNsIds.reserve(contentsSize);
contentsData.reserve(contentsSize);
if (!m_query->exec("SELECT MAX(Id) FROM ContentsTable"_L1) || !m_query->next())
return false;
const int maxContentsId = m_query->value(0).toInt();
int newContentsId = 0;
for (const QHelpDBReader::ContentsItem &item : indexTable.contentsItems) {
contentsNsIds.append(nsId);
contentsData.append(item.data);
for (const QString &filterAttribute : item.filterAttributes) {
filterAttributeToNewContentsId[filterAttribute]
.append(maxContentsId + newContentsId + 1);
}
++newContentsId;
}
m_query->prepare("INSERT INTO ContentsTable VALUES(NULL, ?, ?)"_L1);
m_query->addBindValue(contentsNsIds);
m_query->addBindValue(contentsData);
if (!m_query->execBatch())
return false;
for (auto it = filterAttributeToNewContentsId.cbegin(),
end = filterAttributeToNewContentsId.cend(); it != end; ++it) {
const QString filterAttribute = it.key();
m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
m_query->bindValue(0, filterAttribute);
if (!m_query->exec() || !m_query->next())
return false;
const int attributeId = m_query->value(0).toInt();
QVariantList attributeIds;
for (int i = 0; i < it.value().size(); i++)
attributeIds.append(attributeId);
m_query->prepare("INSERT INTO ContentsFilterTable VALUES(?, ?)"_L1);
m_query->addBindValue(attributeIds);
m_query->addBindValue(it.value());
if (!m_query->execBatch())
return false;
}
QVariantList filterNsIds;
QVariantList filterAttributeIds;
for (const QString &filterAttribute : indexTable.usedFilterAttributes) {
filterNsIds.append(nsId);
m_query->prepare("SELECT Id From FilterAttributeTable WHERE Name = ?"_L1);
m_query->bindValue(0, filterAttribute);
if (!m_query->exec() || !m_query->next())
return false;
filterAttributeIds.append(m_query->value(0).toInt());
}
m_query->prepare("INSERT INTO OptimizedFilterTable "
"(NamespaceId, FilterAttributeId) VALUES(?, ?)"_L1);
m_query->addBindValue(filterNsIds);
m_query->addBindValue(filterAttributeIds);
if (!m_query->execBatch())
return false;
m_query->prepare("INSERT INTO TimeStampTable "
"(NamespaceId, FolderId, FilePath, Size, TimeStamp) "
"VALUES(?, ?, ?, ?, ?)"_L1);
m_query->addBindValue(nsId);
m_query->addBindValue(vfId);
m_query->addBindValue(fileName);
const QFileInfo fi(absoluteDocPath(fileName));
m_query->addBindValue(fi.size());
QDateTime lastModified = fi.lastModified(QTimeZone::UTC);
if (qEnvironmentVariableIsSet("SOURCE_DATE_EPOCH")) {
const QString sourceDateEpochStr = qEnvironmentVariable("SOURCE_DATE_EPOCH");
bool ok;
const qlonglong sourceDateEpoch = sourceDateEpochStr.toLongLong(&ok);
if (ok && sourceDateEpoch < lastModified.toSecsSinceEpoch())
lastModified.setSecsSinceEpoch(sourceDateEpoch);
}
m_query->addBindValue(lastModified);
if (!m_query->exec())
return false;
return transaction.commit();
}
bool QHelpCollectionHandler::unregisterIndexTable(int nsId, int vfId)
{
m_query->prepare("DELETE FROM IndexFilterTable WHERE IndexId IN "
"(SELECT Id FROM IndexTable WHERE NamespaceId = ?)"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM IndexTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM FileFilterTable WHERE FileId IN "
"(SELECT FileId FROM FileNameTable WHERE FolderId = ?)"_L1);
m_query->bindValue(0, vfId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM FileNameTable WHERE FolderId = ?"_L1);
m_query->bindValue(0, vfId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM ContentsFilterTable WHERE ContentsId IN "
"(SELECT Id FROM ContentsTable WHERE NamespaceId = ?)"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM ContentsTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM FileAttributeSetTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM OptimizedFilterTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM TimeStampTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("DELETE FROM VersionTable WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("SELECT ComponentId FROM ComponentMapping WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
if (!m_query->next())
return true; // the component mapping is already removed
const int componentId = m_query->value(0).toInt();
m_query->prepare("DELETE FROM ComponentMapping WHERE NamespaceId = ?"_L1);
m_query->bindValue(0, nsId);
if (!m_query->exec())
return false;
m_query->prepare("SELECT ComponentId FROM ComponentMapping WHERE ComponentId = ?"_L1);
m_query->bindValue(0, componentId);
if (!m_query->exec())
return false;
if (!m_query->next()) { // no more namespaces refer to the componentId
m_query->prepare("DELETE FROM ComponentTable WHERE ComponentId = ?"_L1);
m_query->bindValue(0, componentId);
if (!m_query->exec())
return false;
}
return true;
}
QUrl QHelpCollectionHandler::buildQUrl(const QString &ns, const QString &folder,
const QString &relFileName, const QString &anchor)
{
QUrl url;
url.setScheme("qthelp"_L1);
url.setAuthority(ns);
url.setPath(u'/' + folder + u'/' + relFileName);
url.setFragment(anchor);
return url;
}
QList<QHelpLink> QHelpCollectionHandler::documentsForIdentifier(
const QString &id, const QStringList &filterAttributes) const
{
return documentsForField("Identifier"_L1, id, filterAttributes);
}
QList<QHelpLink> QHelpCollectionHandler::documentsForKeyword(
const QString &keyword, const QStringList &filterAttributes) const
{
return documentsForField("Name"_L1, keyword, filterAttributes);
}
QList<QHelpLink> QHelpCollectionHandler::documentsForField(const QString &fieldName,
const QString &fieldValue, const QStringList &filterAttributes) const
{
if (!isDBOpened())
return {};
const QString filterlessQuery =
"SELECT "
"FileNameTable.Title, "
"NamespaceTable.Name, "
"FolderTable.Name, "
"FileNameTable.Name, "
"IndexTable.Anchor "
"FROM "
"IndexTable, "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE IndexTable.FileId = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND IndexTable.NamespaceId = NamespaceTable.Id "
"AND IndexTable.%1 = ?"_L1.arg(fieldName);
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterAttributes.size(), "IndexTable"_L1, "Id"_L1,
"IndexFilterTable"_L1, "IndexId"_L1);
m_query->prepare(filterQuery);
m_query->bindValue(0, fieldValue);
bindFilterQuery(m_query.get(), 1, filterAttributes);
m_query->exec();
QList<QHelpLink> docList;
while (m_query->next()) {
QString title = m_query->value(0).toString();
if (title.isEmpty()) // generate a title + corresponding path
title = fieldValue + " : "_L1 + m_query->value(3).toString();
const QUrl url = buildQUrl(m_query->value(1).toString(),
m_query->value(2).toString(),
m_query->value(3).toString(),
m_query->value(4).toString());
docList.append(QHelpLink {url, title});
}
return docList;
}
QList<QHelpLink> QHelpCollectionHandler::documentsForIdentifier(
const QString &id, const QString &filterName) const
{
return documentsForField("Identifier"_L1, id, filterName);
}
QList<QHelpLink> QHelpCollectionHandler::documentsForKeyword(
const QString &keyword, const QString &filterName) const
{
return documentsForField("Name"_L1, keyword, filterName);
}
QMultiMap<QString, QUrl> QHelpCollectionHandler::linksForField(const QString &fieldName,
const QString &fieldValue, const QString &filterName) const
{
QMultiMap<QString, QUrl> linkMap;
const auto documents = documentsForField(fieldName, fieldValue, filterName);
for (const auto &document : documents)
linkMap.insert(document.title, document.url);
return linkMap;
}
struct IndexEntry
{
QString title;
QUrl url;
QVersionNumber version;
private:
friend bool comparesEqual(const IndexEntry &lhs, const IndexEntry &rhs) noexcept
{
return lhs.title == rhs.title && lhs.url == rhs.url && lhs.version == rhs.version;
}
Q_DECLARE_EQUALITY_COMPARABLE(IndexEntry)
};
static inline bool hasVersion(const IndexEntry &e)
{
return !e.version.isNull();
}
static bool versionLessThan(const IndexEntry &lhs, const IndexEntry &rhs)
{
return lhs.version < rhs.version;
}
static QHelpLink toHelpLink(const IndexEntry &e)
{
return { e.url, e.title };
}
QList<QHelpLink> QHelpCollectionHandler::documentsForField(const QString &fieldName,
const QString &fieldValue, const QString &filterName) const
{
if (!isDBOpened())
return {};
const QString filterlessQuery =
"SELECT "
"FileNameTable.Title, "
"NamespaceTable.Name, "
"FolderTable.Name, "
"FileNameTable.Name, "
"IndexTable.Anchor "
"FROM "
"IndexTable, "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE IndexTable.FileId = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND IndexTable.NamespaceId = NamespaceTable.Id "
"AND IndexTable.%1 = ?"_L1.arg(fieldName);
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterName)
+ " ORDER BY LOWER(FileNameTable.Title), FileNameTable.Title"_L1;
m_query->prepare(filterQuery);
m_query->bindValue(0, fieldValue);
bindFilterQuery(m_query.get(), 1, filterName);
m_query->exec();
QList<IndexEntry> entries;
while (m_query->next()) {
QString title = m_query->value(0).toString();
if (title.isEmpty()) // generate a title + corresponding path
title = fieldValue + " : "_L1 + m_query->value(3).toString();
const QString nameSpace = m_query->value(1).toString();
const QUrl url = buildQUrl(nameSpace,
m_query->value(2).toString(),
m_query->value(3).toString(),
m_query->value(4).toString());
IndexEntry newEntry{ title, url, QHelpDBReader::versionHeuristic(nameSpace) };
if (!entries.contains(newEntry))
entries.emplaceBack(newEntry);
}
// Reverse sort by trailing version number on the namespace, newest first
if (std::all_of(entries.cbegin(), entries.cend(), hasVersion))
std::stable_sort(entries.rbegin(), entries.rend(), versionLessThan);
QList<QHelpLink> docList;
docList.reserve(entries.size());
std::transform(entries.cbegin(), entries.cend(), std::back_inserter(docList), toHelpLink);
return docList;
}
QStringList QHelpCollectionHandler::namespacesForFilter(const QString &filterName) const
{
QStringList namespaceList;
if (!isDBOpened())
return namespaceList;
const QString filterlessQuery =
"SELECT "
"NamespaceTable.Name "
"FROM "
"NamespaceTable "
"WHERE TRUE"_L1;
const QString filterQuery = filterlessQuery
+ prepareFilterQuery(filterName);
m_query->prepare(filterQuery);
bindFilterQuery(m_query.get(), 0, filterName);
m_query->exec();
while (m_query->next())
namespaceList.append(m_query->value(0).toString());
return namespaceList;
}
QT_END_NAMESPACE