blob: faf8be4655ae59b3f45a4886a51df0a9162ff6ca [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Assistant of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qhelpdbreader_p.h"
#include "qhelp_global.h"
#include <QtCore/QVariant>
#include <QtCore/QVector>
#include <QtCore/QFile>
#include <QtSql/QSqlError>
#include <QtSql/QSqlQuery>
QT_BEGIN_NAMESPACE
QHelpDBReader::QHelpDBReader(const QString &dbName)
: QObject(nullptr),
m_dbName(dbName),
m_uniqueId(QHelpGlobal::uniquifyConnectionName(QLatin1String("QHelpDBReader"),
this))
{
}
QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId,
QObject *parent)
: QObject(parent),
m_dbName(dbName),
m_uniqueId(uniqueId)
{
}
QHelpDBReader::~QHelpDBReader()
{
if (m_initDone) {
delete m_query;
QSqlDatabase::removeDatabase(m_uniqueId);
}
}
bool QHelpDBReader::init()
{
if (m_initDone)
return true;
if (!QFile::exists(m_dbName))
return false;
if (!initDB()) {
QSqlDatabase::removeDatabase(m_uniqueId);
return false;
}
m_initDone = true;
m_query = new QSqlQuery(QSqlDatabase::database(m_uniqueId));
return true;
}
bool QHelpDBReader::initDB()
{
QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_uniqueId);
db.setConnectOptions(QLatin1String("QSQLITE_OPEN_READONLY"));
db.setDatabaseName(m_dbName);
if (!db.open()) {
/*: The placeholders are: %1 - The name of the database which cannot be opened
%2 - The unique id for the connection
%3 - The actual error string */
m_error = tr("Cannot open database \"%1\" \"%2\": %3").arg(m_dbName, m_uniqueId, db.lastError().text());
return false;
}
return true;
}
QString QHelpDBReader::namespaceName() const
{
if (!m_namespace.isEmpty())
return m_namespace;
if (m_query) {
m_query->exec(QLatin1String("SELECT Name FROM NamespaceTable"));
if (m_query->next())
m_namespace = m_query->value(0).toString();
}
return m_namespace;
}
QString QHelpDBReader::virtualFolder() const
{
if (m_query) {
m_query->exec(QLatin1String("SELECT Name FROM FolderTable WHERE Id=1"));
if (m_query->next())
return m_query->value(0).toString();
}
return QString();
}
QString QHelpDBReader::version() const
{
const QString versionString = metaData(QLatin1String("version")).toString();
if (versionString.isEmpty())
return qtVersionHeuristic();
return versionString;
}
QString QHelpDBReader::qtVersionHeuristic() const
{
const QString nameSpace = namespaceName();
if (!nameSpace.startsWith(QLatin1String("org.qt-project.")))
return QString();
// We take the namespace tail, starting from the last letter in namespace name.
// We drop any non digit characters.
const QChar dot(QLatin1Char('.'));
QString tail;
for (int i = nameSpace.count(); i > 0; --i) {
const QChar c = nameSpace.at(i - 1);
if (c.isDigit() || c == dot)
tail.prepend(c);
if (c.isLetter())
break;
}
if (!tail.startsWith(dot) && tail.count(dot) == 1) {
// The org.qt-project.qtquickcontrols2.5120 case,
// tail = 2.5120 here. We need to cut "2." here.
const int dotIndex = tail.indexOf(dot);
if (dotIndex > 0)
tail = tail.mid(dotIndex);
}
// Drop beginning dots
while (tail.startsWith(dot))
tail = tail.mid(1);
// Drop ending dots
while (tail.endsWith(dot))
tail.chop(1);
if (tail.count(dot) == 0) {
if (tail.count() > 5)
return tail;
// When we have 3 digits, we split it like: ABC -> A.B.C
// When we have 4 digits, we split it like: ABCD -> A.BC.D
// When we have 5 digits, we split it like: ABCDE -> A.BC.DE
const int major = tail.left(1).toInt();
const int minor = tail.count() == 3
? tail.mid(1, 1).toInt() : tail.mid(1, 2).toInt();
const int patch = tail.count() == 5
? tail.right(2).toInt() : tail.right(1).toInt();
return QString::fromUtf8("%1.%2.%3").arg(major).arg(minor).arg(patch);
}
return tail;
}
static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
{
query->prepare(QString::fromLatin1("SELECT FilterAttributeId "
"FROM %1 "
"WHERE FilterAttributeId = ? "
"LIMIT 1").arg(tableName));
query->bindValue(0, attributeId);
query->exec();
return query->next(); // if we got a result it means it was used
}
static int filterDataCount(QSqlQuery *query, const QString &tableName)
{
query->exec(QString::fromLatin1("SELECT COUNT(*) FROM"
"(SELECT DISTINCT * FROM %1)").arg(tableName));
query->next();
return query->value(0).toInt();
}
QHelpDBReader::IndexTable QHelpDBReader::indexTable() const
{
IndexTable table;
if (!m_query)
return table;
QMap<int, QString> attributeIds;
m_query->exec(QLatin1String("SELECT DISTINCT Id, Name FROM FilterAttributeTable ORDER BY Id"));
while (m_query->next())
attributeIds.insert(m_query->value(0).toInt(), m_query->value(1).toString());
// Maybe some are unused and specified erroneously in the named filter only,
// like it was in case of qtlocation.qch <= qt 5.9
QVector<int> usedAttributeIds;
for (auto it = attributeIds.cbegin(), end = attributeIds.cend(); it != end; ++it) {
const int attributeId = it.key();
if (isAttributeUsed(m_query, QLatin1String("IndexFilterTable"), attributeId)
|| isAttributeUsed(m_query, QLatin1String("ContentsFilterTable"), attributeId)
|| isAttributeUsed(m_query, QLatin1String("FileFilterTable"), attributeId)) {
usedAttributeIds.append(attributeId);
}
}
bool legacy = false;
m_query->exec(QLatin1String("SELECT * FROM pragma_table_info('IndexTable') "
"WHERE name='ContextName'"));
if (m_query->next())
legacy = true;
const QString identifierColumnName = legacy
? QLatin1String("ContextName")
: QLatin1String("Identifier");
const int usedAttributeCount = usedAttributeIds.count();
QMap<int, IndexItem> idToIndexItem;
m_query->exec(QString::fromLatin1("SELECT Name, %1, FileId, Anchor, Id "
"FROM IndexTable "
"ORDER BY Id").arg(identifierColumnName));
while (m_query->next()) {
IndexItem indexItem;
indexItem.name = m_query->value(0).toString();
indexItem.identifier = m_query->value(1).toString();
indexItem.fileId = m_query->value(2).toInt();
indexItem.anchor = m_query->value(3).toString();
const int indexId = m_query->value(4).toInt();
idToIndexItem.insert(indexId, indexItem);
}
QMap<int, FileItem> idToFileItem;
QMap<int, int> originalFileIdToNewFileId;
int filesCount = 0;
m_query->exec(QLatin1String("SELECT "
"FileNameTable.FileId, "
"FileNameTable.Name, "
"FileNameTable.Title "
"FROM FileNameTable, FolderTable "
"WHERE FileNameTable.FolderId = FolderTable.Id "
"ORDER BY FileId"));
while (m_query->next()) {
const int fileId = m_query->value(0).toInt();
FileItem fileItem;
fileItem.name = m_query->value(1).toString();
fileItem.title = m_query->value(2).toString();
idToFileItem.insert(fileId, fileItem);
originalFileIdToNewFileId.insert(fileId, filesCount);
++filesCount;
}
QMap<int, ContentsItem> idToContentsItem;
m_query->exec(QLatin1String("SELECT Data, Id "
"FROM ContentsTable "
"ORDER BY Id"));
while (m_query->next()) {
ContentsItem contentsItem;
contentsItem.data = m_query->value(0).toByteArray();
const int contentsId = m_query->value(1).toInt();
idToContentsItem.insert(contentsId, contentsItem);
}
bool optimized = true;
if (usedAttributeCount) {
// May optimize only when all usedAttributes are attached to every
// index and file. It means the number of rows in the
// IndexTable multiplied by number of used attributes
// must equal the number of rows inside IndexFilterTable
// (yes, we have a combinatorial explosion of data in IndexFilterTable,
// which we want to optimize). The same with FileNameTable and
// FileFilterTable.
const bool mayOptimizeIndexTable
= filterDataCount(m_query, QLatin1String("IndexFilterTable"))
== idToIndexItem.count() * usedAttributeCount;
const bool mayOptimizeFileTable
= filterDataCount(m_query, QLatin1String("FileFilterTable"))
== idToFileItem.count() * usedAttributeCount;
const bool mayOptimizeContentsTable
= filterDataCount(m_query, QLatin1String("ContentsFilterTable"))
== idToContentsItem.count() * usedAttributeCount;
optimized = mayOptimizeIndexTable && mayOptimizeFileTable && mayOptimizeContentsTable;
if (!optimized) {
m_query->exec(QLatin1String(
"SELECT "
"IndexFilterTable.IndexId, "
"FilterAttributeTable.Name "
"FROM "
"IndexFilterTable, "
"FilterAttributeTable "
"WHERE "
"IndexFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
while (m_query->next()) {
const int indexId = m_query->value(0).toInt();
auto it = idToIndexItem.find(indexId);
if (it != idToIndexItem.end())
it.value().filterAttributes.append(m_query->value(1).toString());
}
m_query->exec(QLatin1String(
"SELECT "
"FileFilterTable.FileId, "
"FilterAttributeTable.Name "
"FROM "
"FileFilterTable, "
"FilterAttributeTable "
"WHERE "
"FileFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
while (m_query->next()) {
const int fileId = m_query->value(0).toInt();
auto it = idToFileItem.find(fileId);
if (it != idToFileItem.end())
it.value().filterAttributes.append(m_query->value(1).toString());
}
m_query->exec(QLatin1String(
"SELECT "
"ContentsFilterTable.ContentsId, "
"FilterAttributeTable.Name "
"FROM "
"ContentsFilterTable, "
"FilterAttributeTable "
"WHERE "
"ContentsFilterTable.FilterAttributeId = FilterAttributeTable.Id"));
while (m_query->next()) {
const int contentsId = m_query->value(0).toInt();
auto it = idToContentsItem.find(contentsId);
if (it != idToContentsItem.end())
it.value().filterAttributes.append(m_query->value(1).toString());
}
}
}
// reindex fileId references
for (auto it = idToIndexItem.cbegin(), end = idToIndexItem.cend(); it != end; ++it) {
IndexItem item = it.value();
item.fileId = originalFileIdToNewFileId.value(item.fileId);
table.indexItems.append(item);
}
table.fileItems = idToFileItem.values();
table.contentsItems = idToContentsItem.values();
if (optimized) {
for (int attributeId : usedAttributeIds)
table.usedFilterAttributes.append(attributeIds.value(attributeId));
}
return table;
}
QList<QStringList> QHelpDBReader::filterAttributeSets() const
{
QList<QStringList> result;
if (m_query) {
m_query->exec(QLatin1String(
"SELECT "
"FileAttributeSetTable.Id, "
"FilterAttributeTable.Name "
"FROM "
"FileAttributeSetTable, "
"FilterAttributeTable "
"WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
"ORDER BY FileAttributeSetTable.Id"));
int oldId = -1;
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());
}
}
return result;
}
QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
const QString &filePath) const
{
QByteArray ba;
if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
return ba;
namespaceName();
m_query->prepare(QLatin1String(
"SELECT "
"FileDataTable.Data "
"FROM "
"FileDataTable, "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE FileDataTable.Id = FileNameTable.FileId "
"AND (FileNameTable.Name = ? OR FileNameTable.Name = ?) "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.Name = ? "
"AND FolderTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Name = ?"));
m_query->bindValue(0, filePath);
m_query->bindValue(1, QString(QLatin1String("./") + filePath));
m_query->bindValue(2, virtualFolder);
m_query->bindValue(3, m_namespace);
m_query->exec();
if (m_query->next() && m_query->isValid())
ba = qUncompress(m_query->value(0).toByteArray());
return ba;
}
QStringList QHelpDBReader::customFilters() const
{
QStringList lst;
if (m_query) {
m_query->exec(QLatin1String("SELECT Name FROM FilterNameTable"));
while (m_query->next())
lst.append(m_query->value(0).toString());
}
return lst;
}
QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
{
QStringList lst;
if (m_query) {
if (filterName.isEmpty()) {
m_query->prepare(QLatin1String("SELECT Name FROM FilterAttributeTable"));
} else {
m_query->prepare(QLatin1String(
"SELECT "
"FilterAttributeTable.Name "
"FROM "
"FilterAttributeTable, "
"FilterTable, "
"FilterNameTable "
"WHERE FilterNameTable.Name = ? "
"AND FilterNameTable.Id = FilterTable.NameId "
"AND FilterTable.FilterAttributeId = FilterAttributeTable.Id"));
m_query->bindValue(0, filterName);
}
m_query->exec();
while (m_query->next())
lst.append(m_query->value(0).toString());
}
return lst;
}
QMap<QString, QByteArray> QHelpDBReader::filesData(
const QStringList &filterAttributes,
const QString &extensionFilter) const
{
QMap<QString, QByteArray> result;
if (!m_query)
return result;
QString query;
QString extension;
if (!extensionFilter.isEmpty())
extension = QString(QLatin1String("AND FileNameTable.Name "
"LIKE \'%.%1\'")).arg(extensionFilter);
if (filterAttributes.isEmpty()) {
query = QString(QLatin1String("SELECT "
"FileNameTable.Name, "
"FileDataTable.Data "
"FROM "
"FolderTable, "
"FileNameTable, "
"FileDataTable "
"WHERE FileDataTable.Id = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id %1"))
.arg(extension);
} else {
for (int i = 0; i < filterAttributes.count(); ++i) {
if (i > 0)
query.append(QLatin1String(" INTERSECT "));
query.append(QString(QLatin1String(
"SELECT "
"FileNameTable.Name, "
"FileDataTable.Data "
"FROM "
"FolderTable, "
"FileNameTable, "
"FileDataTable, "
"FileFilterTable, "
"FilterAttributeTable "
"WHERE FileDataTable.Id = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FileNameTable.FileId = FileFilterTable.FileId "
"AND FileFilterTable.FilterAttributeId = FilterAttributeTable.Id "
"AND FilterAttributeTable.Name = \'%1\' %2"))
.arg(quote(filterAttributes.at(i)))
.arg(extension));
}
}
m_query->exec(query);
while (m_query->next())
result.insert(m_query->value(0).toString(), qUncompress(m_query->value(1).toByteArray()));
return result;
}
QVariant QHelpDBReader::metaData(const QString &name) const
{
QVariant v;
if (!m_query)
return v;
m_query->prepare(QLatin1String("SELECT COUNT(Value), Value FROM MetaDataTable "
"WHERE Name=?"));
m_query->bindValue(0, name);
if (m_query->exec() && m_query->next()
&& m_query->value(0).toInt() == 1)
v = m_query->value(1);
return v;
}
QString QHelpDBReader::quote(const QString &string) const
{
QString s = string;
s.replace(QLatin1Char('\''), QLatin1String("\'\'"));
return s;
}
QT_END_NAMESPACE