From 7167ce41b61d2ba2cdb526777a4233eb84a3b66a Mon Sep 17 00:00:00 2001 From: Unit 193 Date: Sat, 6 Dec 2014 17:33:25 -0500 Subject: Imported Upstream version 2.99.6 --- .../services/impl/collationmanagerimpl.cpp | 125 ++++ .../services/impl/collationmanagerimpl.h | 36 + .../coreSQLiteStudio/services/impl/configimpl.cpp | 770 +++++++++++++++++++ .../coreSQLiteStudio/services/impl/configimpl.h | 127 ++++ .../services/impl/dbmanagerimpl.cpp | 524 +++++++++++++ .../coreSQLiteStudio/services/impl/dbmanagerimpl.h | 183 +++++ .../services/impl/functionmanagerimpl.cpp | 694 +++++++++++++++++ .../services/impl/functionmanagerimpl.h | 96 +++ .../services/impl/pluginmanagerimpl.cpp | 820 +++++++++++++++++++++ .../services/impl/pluginmanagerimpl.h | 321 ++++++++ 10 files changed, 3696 insertions(+) create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp create mode 100644 SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h (limited to 'SQLiteStudio3/coreSQLiteStudio/services/impl') diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp new file mode 100644 index 0000000..5876021 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp @@ -0,0 +1,125 @@ +#include "collationmanagerimpl.h" +#include "services/pluginmanager.h" +#include "plugins/scriptingplugin.h" +#include "services/notifymanager.h" +#include "services/dbmanager.h" +#include "common/utils.h" +#include + +CollationManagerImpl::CollationManagerImpl() +{ + init(); +} + +void CollationManagerImpl::setCollations(const QList& newCollations) +{ + collations = newCollations; + refreshCollationsByKey(); + storeInConfig(); + emit collationListChanged(); +} + +QList CollationManagerImpl::getAllCollations() const +{ + return collations; +} + +QList CollationManagerImpl::getCollationsForDatabase(const QString& dbName) const +{ + QList results; + foreach (const CollationPtr& coll, collations) + { + if (coll->allDatabases || coll->databases.contains(dbName, Qt::CaseInsensitive)) + results << coll; + } + return results; +} + +int CollationManagerImpl::evaluate(const QString& name, const QString& value1, const QString& value2) +{ + if (!collationsByKey.contains(name)) + { + qWarning() << "Could not find requested collation" << name << ", so using default collation."; + return evaluateDefault(value1, value2); + } + + ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(collationsByKey[name]->lang); + if (!plugin) + { + qWarning() << "Plugin for collation" << name << ", not loaded, so using default collation."; + return evaluateDefault(value1, value2); + } + + QString err; + QVariant result = plugin->evaluate(collationsByKey[name]->code, {value1, value2}, &err); + + if (!err.isNull()) + { + qWarning() << "Error while evaluating collation:" << err; + return evaluateDefault(value1, value2); + } + + bool ok; + int intResult = result.toInt(&ok); + if (!ok) + { + qWarning() << "Not integer result from collation:" << result.toString(); + return evaluateDefault(value1, value2); + } + + return intResult; +} + +int CollationManagerImpl::evaluateDefault(const QString& value1, const QString& value2) +{ + return value1.compare(value2, Qt::CaseInsensitive); +} + +void CollationManagerImpl::init() +{ + loadFromConfig(); + refreshCollationsByKey(); +} + +void CollationManagerImpl::storeInConfig() +{ + QVariantList list; + QHash collHash; + for (CollationPtr coll : collations) + { + collHash["name"] = coll->name; + collHash["lang"] = coll->lang; + collHash["code"] = coll->code; + collHash["allDatabases"] = coll->allDatabases; + collHash["databases"] =common(DBLIST->getDbNames(), coll->databases); + list << collHash; + } + CFG_CORE.Internal.Collations.set(list); +} + +void CollationManagerImpl::loadFromConfig() +{ + collations.clear(); + + QVariantList list = CFG_CORE.Internal.Collations.get(); + QHash collHash; + CollationPtr coll; + for (const QVariant& var : list) + { + collHash = var.toHash(); + coll = CollationPtr::create(); + coll->name = collHash["name"].toString(); + coll->lang = collHash["lang"].toString(); + coll->code = collHash["code"].toString(); + coll->databases = collHash["databases"].toStringList(); + coll->allDatabases = collHash["allDatabases"].toBool(); + collations << coll; + } +} + +void CollationManagerImpl::refreshCollationsByKey() +{ + collationsByKey.clear(); + foreach (CollationPtr collation, collations) + collationsByKey[collation->name] = collation; +} diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h new file mode 100644 index 0000000..f5a5937 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h @@ -0,0 +1,36 @@ +#ifndef COLLATIONMANAGERIMPL_H +#define COLLATIONMANAGERIMPL_H + +#include "services/collationmanager.h" + +class ScriptingPlugin; +class Plugin; +class PluginType; + +class API_EXPORT CollationManagerImpl : public CollationManager +{ + public: + CollationManagerImpl(); + + void setCollations(const QList& newCollations); + QList getAllCollations() const; + QList getCollationsForDatabase(const QString& dbName) const; + int evaluate(const QString& name, const QString& value1, const QString& value2); + int evaluateDefault(const QString& value1, const QString& value2); + + private: + void init(); + void storeInConfig(); + void loadFromConfig(); + void refreshCollationsByKey(); + + QList collations; + QHash collationsByKey; + QHash scriptingPlugins; + + private slots: + void pluginLoaded(Plugin* plugin, PluginType* type); + void pluginUnloaded(Plugin* plugin, PluginType* type); +}; + +#endif // COLLATIONMANAGERIMPL_H diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp new file mode 100644 index 0000000..e210f01 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp @@ -0,0 +1,770 @@ +#include "configimpl.h" +#include "sqlhistorymodel.h" +#include "ddlhistorymodel.h" +#include "services/notifymanager.h" +#include "sqlitestudio.h" +#include "db/dbsqlite3.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static_qstring(DB_FILE_NAME, "settings3"); +qint64 ConfigImpl::sqlHistoryId = -1; + +ConfigImpl::~ConfigImpl() +{ + cleanUp(); +} + +void ConfigImpl::init() +{ + initDbFile(); + initTables(); + + connect(this, SIGNAL(sqlHistoryRefreshNeeded()), this, SLOT(refreshSqlHistory())); + connect(this, SIGNAL(ddlHistoryRefreshNeeded()), this, SLOT(refreshDdlHistory())); +} + +void ConfigImpl::cleanUp() +{ + if (db->isOpen()) + db->close(); + + safe_delete(db); +} + +const QString &ConfigImpl::getConfigDir() const +{ + return configDir; +} + +QString ConfigImpl::getConfigFilePath() const +{ + if (!db) + return QString(); + + return db->getPath(); +} + +void ConfigImpl::beginMassSave() +{ + if (isMassSaving()) + return; + + emit massSaveBegins(); + db->exec("BEGIN;"); + massSaving = true; +} + +void ConfigImpl::commitMassSave() +{ + if (!isMassSaving()) + return; + + db->exec("COMMIT;"); + emit massSaveCommited(); + massSaving = false; +} + +void ConfigImpl::rollbackMassSave() +{ + if (!isMassSaving()) + return; + + db->exec("ROLLBACK;"); + massSaving = false; +} + +bool ConfigImpl::isMassSaving() const +{ + return massSaving; +} + +void ConfigImpl::set(const QString &group, const QString &key, const QVariant &value) +{ + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + stream << value; + + db->exec("INSERT OR REPLACE INTO settings VALUES (?, ?, ?)", {group, key, bytes}); +} + +QVariant ConfigImpl::get(const QString &group, const QString &key) +{ + SqlQueryPtr results = db->exec("SELECT value FROM settings WHERE [group] = ? AND [key] = ?", {group, key}); + return deserializeValue(results->getSingleCell()); +} + +QHash ConfigImpl::getAll() +{ + SqlQueryPtr results = db->exec("SELECT [group], [key], value FROM settings"); + + QHash cfg; + QString key; + SqlResultsRowPtr row; + while (results->hasNext()) + { + row = results->next(); + key = row->value("group").toString() + "." + row->value("key").toString(); + cfg[key] = deserializeValue(row->value("value")); + } + return cfg; +} + +bool ConfigImpl::storeErrorAndReturn(SqlQueryPtr results) +{ + if (results->isError()) + { + lastQueryError = results->getErrorText(); + return true; + } + else + return false; +} + +void ConfigImpl::printErrorIfSet(SqlQueryPtr results) +{ + if (results && results->isError()) + { + qCritical() << "Config error while executing query:" << results->getErrorText(); + storeErrorAndReturn(results); + } +} + +bool ConfigImpl::addDb(const QString& name, const QString& path, const QHash& options) +{ + QByteArray optBytes = hashToBytes(options); + SqlQueryPtr results = db->exec("INSERT INTO dblist VALUES (?, ?, ?)", {name, path, optBytes}); + return !storeErrorAndReturn(results); +} + +bool ConfigImpl::updateDb(const QString &name, const QString &newName, const QString &path, const QHash &options) +{ + QByteArray optBytes = hashToBytes(options); + SqlQueryPtr results = db->exec("UPDATE dblist SET name = ?, path = ?, options = ? WHERE name = ?", + {newName, path, optBytes, name}); + + return (!storeErrorAndReturn(results) && results->rowsAffected() > 0); +} + +bool ConfigImpl::removeDb(const QString &name) +{ + SqlQueryPtr results = db->exec("DELETE FROM dblist WHERE name = ?", {name}); + return (!storeErrorAndReturn(results) && results->rowsAffected() > 0); +} + +bool ConfigImpl::isDbInConfig(const QString &name) +{ + SqlQueryPtr results = db->exec("SELECT * FROM dblist WHERE name = ?", {name}); + return (!storeErrorAndReturn(results) && results->hasNext()); +} + +QString ConfigImpl::getLastErrorString() const +{ + QString msg = db->getErrorText(); + if (msg.trimmed().isEmpty()) + return lastQueryError; + + return msg; +} + +QList ConfigImpl::dbList() +{ + QList entries; + SqlQueryPtr results = db->exec("SELECT name, path, options FROM dblist"); + CfgDbPtr cfgDb; + SqlResultsRowPtr row; + while (results->hasNext()) + { + row = results->next(); + cfgDb = CfgDbPtr::create(); + cfgDb->name = row->value("name").toString(); + cfgDb->path = row->value("path").toString(); + cfgDb->options = deserializeValue(row->value("options")).toHash(); + entries += cfgDb; + } + + return entries; +} + +ConfigImpl::CfgDbPtr ConfigImpl::getDb(const QString& dbName) +{ + SqlQueryPtr results = db->exec("SELECT path, options FROM dblist WHERE name = ?", {dbName}); + + if (!results->hasNext()) + return CfgDbPtr(); + + SqlResultsRowPtr row = results->next(); + + CfgDbPtr cfgDb = CfgDbPtr::create(); + cfgDb->name = dbName; + cfgDb->path = row->value("path").toString(); + cfgDb->options = deserializeValue(row->value("options")).toHash(); + return cfgDb; +} + +void ConfigImpl::storeGroups(const QList& groups) +{ + db->begin(); + db->exec("DELETE FROM groups"); + + foreach (const DbGroupPtr& group, groups) + storeGroup(group); + + db->commit(); +} + +void ConfigImpl::storeGroup(const ConfigImpl::DbGroupPtr &group, qint64 parentId) +{ + QVariant parent = QVariant(QVariant::LongLong); + if (parentId > -1) + parent = parentId; + + SqlQueryPtr results = db->exec("INSERT INTO groups (name, [order], parent, open, dbname) VALUES (?, ?, ?, ?, ?)", + {group->name, group->order, parent, group->open, group->referencedDbName}); + + qint64 newParentId = results->getRegularInsertRowId(); + foreach (const DbGroupPtr& childGroup, group->childs) + storeGroup(childGroup, newParentId); +} + +QList ConfigImpl::getGroups() +{ + DbGroupPtr topGroup = DbGroupPtr::create(); + topGroup->id = -1; + readGroupRecursively(topGroup); + return topGroup->childs; +} + +ConfigImpl::DbGroupPtr ConfigImpl::getDbGroup(const QString& dbName) +{ + SqlQueryPtr results = db->exec("SELECT id, name, [order], open, dbname FROM groups WHERE dbname = ? LIMIT 1", {dbName}); + + DbGroupPtr group = DbGroupPtr::create(); + group->referencedDbName = dbName; + + if (!results->hasNext()) + return group; + + SqlResultsRowPtr row = results->next(); + group->id = row->value("id").toULongLong(); + group->name = row->value("name").toString(); + group->order = row->value("order").toInt(); + group->open = row->value("open").toBool(); + return group; +} + +qint64 ConfigImpl::addSqlHistory(const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) +{ + if (sqlHistoryId < 0) + { + SqlQueryPtr results = db->exec("SELECT max(id) FROM sqleditor_history"); + if (results->isError()) + { + qCritical() << "Cannot add SQL history, because cannot determinate sqleditor_history max(id):" << results->getErrorText(); + return -1; + } + + if (results->hasNext()) + sqlHistoryId = results->getSingleCell().toLongLong() + 1; + else + sqlHistoryId = 0; + } + + QtConcurrent::run(this, &ConfigImpl::asyncAddSqlHistory, sqlHistoryId, sql, dbName, timeSpentMillis, rowsAffected); + sqlHistoryId++; + return sqlHistoryId; +} + +void ConfigImpl::updateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) +{ + QtConcurrent::run(this, &ConfigImpl::asyncUpdateSqlHistory, id, sql, dbName, timeSpentMillis, rowsAffected); +} + +void ConfigImpl::clearSqlHistory() +{ + QtConcurrent::run(this, &ConfigImpl::asyncClearSqlHistory); +} + +QAbstractItemModel* ConfigImpl::getSqlHistoryModel() +{ + if (!sqlHistoryModel) + sqlHistoryModel = new SqlHistoryModel(db, this); + + return sqlHistoryModel; +} + +void ConfigImpl::addCliHistory(const QString& text) +{ + QtConcurrent::run(this, &ConfigImpl::asyncAddCliHistory, text); +} + +void ConfigImpl::applyCliHistoryLimit() +{ + QtConcurrent::run(this, &ConfigImpl::asyncApplyCliHistoryLimit); +} + +void ConfigImpl::clearCliHistory() +{ + QtConcurrent::run(this, &ConfigImpl::asyncClearCliHistory); +} + +QStringList ConfigImpl::getCliHistory() const +{ + static_qstring(selectQuery, "SELECT text FROM cli_history ORDER BY id"); + + SqlQueryPtr results = db->exec(selectQuery); + if (results->isError()) + qWarning() << "Error while getting CLI history:" << db->getErrorText(); + + return results->columnAsList("text"); +} + +void ConfigImpl::addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile) +{ + QtConcurrent::run(this, &ConfigImpl::asyncAddDdlHistory, queries, dbName, dbFile); +} + +QList ConfigImpl::getDdlHistoryFor(const QString& dbName, const QString& dbFile, const QDate& date) +{ + static_qstring(sql, + "SELECT timestamp," + " queries" + " FROM ddl_history" + " WHERE dbname = ?" + " AND file = ?" + " AND date(timestamp, 'unixepoch') = ?"); + + SqlQueryPtr results = db->exec(sql, {dbName, dbFile, date.toString("yyyy-MM-dd")}); + + QList entries; + DdlHistoryEntryPtr entry; + SqlResultsRowPtr row; + while (results->hasNext()) + { + row = results->next(); + entry = DdlHistoryEntryPtr::create(); + entry->dbName = dbName; + entry->dbFile = dbFile; + entry->timestamp = QDateTime::fromTime_t(row->value("timestamp").toUInt()); + entry->queries = row->value("queries").toString(); + entries << entry; + } + return entries; +} + +DdlHistoryModel* ConfigImpl::getDdlHistoryModel() +{ + if (!ddlHistoryModel) + ddlHistoryModel = new DdlHistoryModel(db, this); + + return ddlHistoryModel; +} + +void ConfigImpl::clearDdlHistory() +{ + QtConcurrent::run(this, &ConfigImpl::asyncClearDdlHistory); +} + +void ConfigImpl::addReportHistory(bool isFeatureRequest, const QString& title, const QString& url) +{ + QtConcurrent::run(this, &ConfigImpl::asyncAddReportHistory, isFeatureRequest, title, url); +} + +QList ConfigImpl::getReportHistory() +{ + static_qstring(sql, "SELECT id, timestamp, title, url, feature_request FROM reports_history"); + + SqlQueryPtr results = db->exec(sql); + + QList entries; + SqlResultsRowPtr row; + ReportHistoryEntryPtr entry; + while (results->hasNext()) + { + row = results->next(); + entry = ReportHistoryEntryPtr::create(); + entry->id = row->value("id").toInt(); + entry->timestamp = row->value("timestamp").toInt(); + entry->title = row->value("title").toString(); + entry->url = row->value("url").toString(); + entry->isFeatureRequest = row->value("feature_request").toBool(); + entries << entry; + } + return entries; +} + +void ConfigImpl::deleteReport(int id) +{ + QtConcurrent::run(this, &ConfigImpl::asyncDeleteReport, id); +} + +void ConfigImpl::clearReportHistory() +{ + QtConcurrent::run(this, &ConfigImpl::asyncClearReportHistory); +} + +void ConfigImpl::readGroupRecursively(ConfigImpl::DbGroupPtr group) +{ + SqlQueryPtr results; + if (group->id < 0) + results = db->exec("SELECT id, name, [order], open, dbname FROM groups WHERE parent IS NULL ORDER BY [order]"); + else + results = db->exec("SELECT id, name, [order], open, dbname FROM groups WHERE parent = ? ORDER BY [order]", {group->id}); + + DbGroupPtr childGroup; + SqlResultsRowPtr row; + while (results->hasNext()) + { + row = results->next(); + childGroup = DbGroupPtr::create(); + childGroup->id = row->value("id").toULongLong(); + childGroup->name = row->value("name").toString(); + childGroup->order = row->value("order").toInt(); + childGroup->open = row->value("open").toBool(); + childGroup->referencedDbName = row->value("dbname").toString(); + group->childs += childGroup; + } + + for (int i = 0; i < group->childs.size(); i++) + readGroupRecursively(group->childs[i]); +} + +void ConfigImpl::begin() +{ + db->begin(); +} + +void ConfigImpl::commit() +{ + db->commit(); +} + +void ConfigImpl::rollback() +{ + db->rollback(); +} + +QString ConfigImpl::getConfigPath() +{ +#ifdef Q_OS_WIN + if (QSysInfo::windowsVersion() & QSysInfo::WV_NT_based) + return SQLITESTUDIO->getEnv("APPDATA")+"/sqlitestudio"; + else + return SQLITESTUDIO->getEnv("HOME")+"/sqlitestudio"; +#else + return SQLITESTUDIO->getEnv("HOME")+"/.config/sqlitestudio"; +#endif +} + +QString ConfigImpl::getPortableConfigPath() +{ + QFileInfo file; + QDir dir("./sqlitestudio-cfg"); + + file = QFileInfo(dir.absolutePath()); + if (!file.exists()) + return dir.absolutePath(); + + if (!file.isDir() || !file.isReadable() || !file.isWritable()) + return QString::null; + + foreach (file, dir.entryInfoList()) + { + if (!file.isReadable() || !file.isWritable()) + return QString::null; + } + + return dir.absolutePath(); +} + +void ConfigImpl::initTables() +{ + SqlQueryPtr results = db->exec("SELECT lower(name) AS name FROM sqlite_master WHERE type = 'table'"); + QList tables = results->columnAsList(0); + + if (!tables.contains("version")) + { + QString table; + foreach (table, tables) + db->exec("DROP TABLE "+table); + + tables.clear(); + db->exec("CREATE TABLE version (version NUMERIC)"); + db->exec("INSERT INTO version VALUES ("+QString::number(SQLITESTUDIO_CONFIG_VERSION)+")"); + } + + if (!tables.contains("settings")) + db->exec("CREATE TABLE settings ([group] TEXT, [key] TEXT, value, PRIMARY KEY([group], [key]))"); + + if (!tables.contains("sqleditor_history")) + db->exec("CREATE TABLE sqleditor_history (id INTEGER PRIMARY KEY, dbname TEXT, date INTEGER, time_spent INTEGER, rows INTEGER, sql TEXT)"); + + if (!tables.contains("dblist")) + db->exec("CREATE TABLE dblist (name TEXT PRIMARY KEY, path TEXT UNIQUE, options TEXT)"); + + if (!tables.contains("groups")) + db->exec("CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, parent INTEGER REFERENCES groups(id), " + "[order] INTEGER, open INTEGER DEFAULT 0, dbname TEXT UNIQUE REFERENCES dblist(name) ON UPDATE CASCADE ON DELETE CASCADE, " + "UNIQUE(name, parent))"); + + if (!tables.contains("ddl_history")) + db->exec("CREATE TABLE ddl_history (id INTEGER PRIMARY KEY AUTOINCREMENT, dbname TEXT, file TEXT, timestamp INTEGER, " + "queries TEXT)"); + + if (!tables.contains("cli_history")) + db->exec("CREATE TABLE cli_history (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)"); + + if (!tables.contains("reports_history")) + db->exec("CREATE TABLE reports_history (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, feature_request BOOLEAN, title TEXT, url TEXT)"); +} + +void ConfigImpl::initDbFile() +{ + // Determinate global config location and portable one + QString globalPath = getConfigPath(); + QString portablePath = getPortableConfigPath(); + + QStringList paths; + if (!globalPath.isNull() && !portablePath.isNull()) + { + if (QFileInfo(portablePath).exists()) + { + paths << portablePath+"/"+DB_FILE_NAME; + paths << globalPath+"/"+DB_FILE_NAME; + } + else + { + paths << globalPath+"/"+DB_FILE_NAME; + paths << portablePath+"/"+DB_FILE_NAME; + } + } + else if (!globalPath.isNull()) + { + paths << globalPath+"/"+DB_FILE_NAME; + } + else if (!portablePath.isNull()) + { + paths << portablePath+"/"+DB_FILE_NAME; + } + + // Create global config directory if not existing + QDir dir; + if (!globalPath.isNull()) + { + dir = QDir(globalPath); + if (!dir.exists()) + QDir::root().mkpath(globalPath); + } + + // A fallback to in-memory db + paths << ":memory:"; + + // Go through all candidates and pick one + QString path; + foreach (path, paths) + { + dir = QDir(path); + if (path != ":memory:") + dir.cdUp(); + + if (tryInitDbFile(path)) + { + configDir = dir.absolutePath(); + break; + } + } + + // We ended up with in-memory one? That's not good. + if (configDir == ":memory:") + { + paths.removeLast(); + notifyError(QObject::tr("Could not initialize configuration file. Any configuration changes and queries history will be lost after application restart." + " Tried to initialize the file at following localizations: %1.").arg(paths.join(", "))); + } + + qDebug() << "Using configuration directory:" << configDir; + db->exec("PRAGMA foreign_keys = 1;"); +} + +bool ConfigImpl::tryInitDbFile(const QString &dbPath) +{ + db = new DbSqlite3("SQLiteStudio settings", dbPath, {{DB_PURE_INIT, true}}); + if (!db->open()) + { + safe_delete(db); + return false; + } + + SqlQueryPtr results = db->exec("SELECT * FROM sqlite_master"); + if (results->isError()) + { + safe_delete(db); + return false; + } + + return true; +} + +QVariant ConfigImpl::deserializeValue(const QVariant &value) +{ + if (!value.isValid()) + return QVariant(); + + QByteArray bytes = value.toByteArray(); + if (bytes.isNull()) + return QVariant(); + + QVariant deserializedValue; + QDataStream stream(bytes); + stream >> deserializedValue; + return deserializedValue; +} + +void ConfigImpl::asyncAddSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) +{ + db->begin(); + SqlQueryPtr results = db->exec("INSERT INTO sqleditor_history (id, dbname, date, time_spent, rows, sql) VALUES (?, ?, ?, ?, ?, ?)", + {id, dbName, (QDateTime::currentMSecsSinceEpoch() / 1000), timeSpentMillis, rowsAffected, sql}); + + if (results->isError()) + { + qDebug() << "Error adding SQL history:" << results->getErrorText(); + db->rollback(); + return; + } + + int maxHistorySize = CFG_CORE.General.SqlHistorySize.get(); + + results = db->exec("SELECT count(*) FROM sqleditor_history"); + if (results->hasNext() && results->getSingleCell().toInt() > maxHistorySize) + { + results = db->exec(QString("SELECT id FROM sqleditor_history ORDER BY id DESC LIMIT 1 OFFSET %1").arg(maxHistorySize)); + if (results->hasNext()) + { + int id = results->getSingleCell().toInt(); + if (id > 0) // it will be 0 on fail conversion, but we won't delete id <= 0 ever. + db->exec("DELETE FROM sqleditor_history WHERE id <= ?", {id}); + } + } + db->commit(); + + emit sqlHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncUpdateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected) +{ + db->exec("UPDATE sqleditor_history SET dbname = ?, time_spent = ?, rows = ?, sql = ? WHERE id = ?", + {dbName, timeSpentMillis, rowsAffected, sql, id}); + + emit sqlHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncClearSqlHistory() +{ + db->exec("DELETE FROM sqleditor_history"); + emit sqlHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncAddCliHistory(const QString& text) +{ + static_qstring(insertQuery, "INSERT INTO cli_history (text) VALUES (?)"); + + SqlQueryPtr results = db->exec(insertQuery, {text}); + if (results->isError()) + qWarning() << "Error while adding CLI history:" << results->getErrorText(); + + applyCliHistoryLimit(); +} + +void ConfigImpl::asyncApplyCliHistoryLimit() +{ + static_qstring(limitQuery, "DELETE FROM cli_history WHERE id >= (SELECT id FROM cli_history ORDER BY id LIMIT 1 OFFSET %1)"); + + SqlQueryPtr results = db->exec(limitQuery.arg(CFG_CORE.Console.HistorySize.get())); + if (results->isError()) + qWarning() << "Error while limiting CLI history:" << db->getErrorText(); +} + +void ConfigImpl::asyncClearCliHistory() +{ + static_qstring(clearQuery, "DELETE FROM cli_history"); + + SqlQueryPtr results = db->exec(clearQuery); + if (results->isError()) + qWarning() << "Error while clearing CLI history:" << db->getErrorText(); +} + +void ConfigImpl::asyncAddDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile) +{ + static_qstring(insert, "INSERT INTO ddl_history (dbname, file, timestamp, queries) VALUES (?, ?, ?, ?)"); + static_qstring(countSql, "SELECT count(*) FROM ddl_history"); + static_qstring(idSql, "SELECT id FROM ddl_history ORDER BY id DESC LIMIT 1 OFFSET %1"); + static_qstring(deleteSql, "DELETE FROM ddl_history WHERE id <= ?"); + + db->begin(); + db->exec(insert, {dbName, dbFile, QDateTime::currentDateTime().toTime_t(), queries}); + + int maxHistorySize = CFG_CORE.General.DdlHistorySize.get(); + + SqlQueryPtr results = db->exec(countSql); + if (results->hasNext() && results->getSingleCell().toInt() > maxHistorySize) + { + results = db->exec(QString(idSql).arg(maxHistorySize), Db::Flag::NO_LOCK); + if (results->hasNext()) + { + int id = results->getSingleCell().toInt(); + if (id > 0) // it will be 0 on fail conversion, but we won't delete id <= 0 ever. + db->exec(deleteSql, {id}); + } + } + db->commit(); + + emit ddlHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncClearDdlHistory() +{ + db->exec("DELETE FROM ddl_history"); + emit ddlHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncAddReportHistory(bool isFeatureRequest, const QString& title, const QString& url) +{ + static_qstring(sql, "INSERT INTO reports_history (feature_request, timestamp, title, url) VALUES (?, ?, ?, ?)"); + db->exec(sql, {(isFeatureRequest ? 1 : 0), QDateTime::currentDateTime().toTime_t(), title, url}); + emit reportsHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncDeleteReport(int id) +{ + static_qstring(sql, "DELETE FROM reports_history WHERE id = ?"); + db->exec(sql, {id}); + emit reportsHistoryRefreshNeeded(); +} + +void ConfigImpl::asyncClearReportHistory() +{ + static_qstring(sql, "DELETE FROM reports_history"); + db->exec(sql); + emit reportsHistoryRefreshNeeded(); +} + +void ConfigImpl::refreshSqlHistory() +{ + if (sqlHistoryModel) + sqlHistoryModel->refresh(); +} + +void ConfigImpl::refreshDdlHistory() +{ + if (ddlHistoryModel) + ddlHistoryModel->refresh(); +} diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h new file mode 100644 index 0000000..ec32e8d --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h @@ -0,0 +1,127 @@ +#ifndef CONFIGIMPL_H +#define CONFIGIMPL_H + +#include "coreSQLiteStudio_global.h" +#include "services/config.h" +#include "db/sqlquery.h" + +class AsyncConfigHandler; +class SqlHistoryModel; + +class API_EXPORT ConfigImpl : public Config +{ + Q_OBJECT + + friend class AsyncConfigHandler; + + public: + virtual ~ConfigImpl(); + + void init(); + void cleanUp(); + const QString& getConfigDir() const; + QString getConfigFilePath() const; + + void beginMassSave(); + void commitMassSave(); + void rollbackMassSave(); + bool isMassSaving() const; + void set(const QString& group, const QString& key, const QVariant& value); + QVariant get(const QString& group, const QString& key); + QHash getAll(); + + bool addDb(const QString& name, const QString& path, const QHash &options); + bool updateDb(const QString& name, const QString &newName, const QString& path, const QHash &options); + bool removeDb(const QString& name); + bool isDbInConfig(const QString& name); + QString getLastErrorString() const; + + /** + * @brief Provides list of all registered databases. + * @return List of database entries. + * + * Registered databases are those that user added to the application. They are not necessary valid or supported. + * They can be inexisting or unsupported, but they are kept in registry in case user fixes file path, + * or loads plugin to support it. + */ + QList dbList(); + CfgDbPtr getDb(const QString& dbName); + + void storeGroups(const QList& groups); + QList getGroups(); + DbGroupPtr getDbGroup(const QString& dbName); + + qint64 addSqlHistory(const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected); + void updateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected); + void clearSqlHistory(); + QAbstractItemModel* getSqlHistoryModel(); + + void addCliHistory(const QString& text); + void applyCliHistoryLimit(); + void clearCliHistory(); + QStringList getCliHistory() const; + + void addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile); + QList getDdlHistoryFor(const QString& dbName, const QString& dbFile, const QDate& date); + DdlHistoryModel* getDdlHistoryModel(); + void clearDdlHistory(); + + void addReportHistory(bool isFeatureRequest, const QString& title, const QString& url); + QList getReportHistory(); + void deleteReport(int id); + void clearReportHistory(); + + void begin(); + void commit(); + void rollback(); + + private: + /** + * @brief Stores error from query in class member. + * @param query Query to get error from. + * @return true if the query had any error set, or false if not. + * + * If the error was defined in the query, its message is stored in lastQueryError. + */ + bool storeErrorAndReturn(SqlQueryPtr results); + void printErrorIfSet(SqlQueryPtr results); + void storeGroup(const DbGroupPtr& group, qint64 parentId = -1); + void readGroupRecursively(DbGroupPtr group); + QString getConfigPath(); + QString getPortableConfigPath(); + void initTables(); + void initDbFile(); + bool tryInitDbFile(const QString& dbPath); + QVariant deserializeValue(const QVariant& value); + + void asyncAddSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected); + void asyncUpdateSqlHistory(qint64 id, const QString& sql, const QString& dbName, int timeSpentMillis, int rowsAffected); + void asyncClearSqlHistory(); + + void asyncAddCliHistory(const QString& text); + void asyncApplyCliHistoryLimit(); + void asyncClearCliHistory(); + + void asyncAddDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile); + void asyncClearDdlHistory(); + + void asyncAddReportHistory(bool isFeatureRequest, const QString& title, const QString& url); + void asyncDeleteReport(int id); + void asyncClearReportHistory(); + + static Config* instance; + static qint64 sqlHistoryId; + + Db* db = nullptr; + QString configDir; + QString lastQueryError; + bool massSaving = false; + SqlHistoryModel* sqlHistoryModel = nullptr; + DdlHistoryModel* ddlHistoryModel = nullptr; + + public slots: + void refreshDdlHistory(); + void refreshSqlHistory(); +}; + +#endif // CONFIGIMPL_H diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp new file mode 100644 index 0000000..43fc953 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp @@ -0,0 +1,524 @@ +#include "dbmanagerimpl.h" +#include "db/db.h" +#include "services/config.h" +#include "plugins//dbplugin.h" +#include "services/pluginmanager.h" +#include "services/notifymanager.h" +#include "common/utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +DbManagerImpl::DbManagerImpl(QObject *parent) : + DbManager(parent) +{ + init(); +} + +DbManagerImpl::~DbManagerImpl() +{ + foreach (Db* db, dbList) + { + disconnect(db, SIGNAL(disconnected()), this, SLOT(dbDisconnectedSlot())); + disconnect(db, SIGNAL(aboutToDisconnect(bool&)), this, SLOT(dbAboutToDisconnect(bool&))); + if (db->isOpen()) + db->close(); + + delete db; + } + dbList.clear(); + nameToDb.clear(); + pathToDb.clear(); +} + +bool DbManagerImpl::addDb(const QString &name, const QString &path, bool permanent) +{ + return addDb(name, path, QHash(), permanent); +} + +bool DbManagerImpl::addDb(const QString &name, const QString &path, const QHash& options, bool permanent) +{ + if (getByName(name)) + { + qWarning() << "Tried to add database with name that was already on the list:" << name; + return false; // db with this name exists + } + + QString errorMessage; + Db* db = createDb(name, path, options, &errorMessage); + if (!db) + { + notifyError(tr("Could not add database %1: %2").arg(path).arg(errorMessage)); + return false; + } + + listLock.lockForWrite(); + addDbInternal(db, permanent); + listLock.unlock(); + + emit dbAdded(db); + + return true; +} + +bool DbManagerImpl::updateDb(Db* db, const QString &name, const QString &path, const QHash &options, bool permanent) +{ + if (db->isOpen()) + { + if (!db->close()) + return false; + } + + listLock.lockForWrite(); + nameToDb.remove(db->getName(), Qt::CaseInsensitive); + pathToDb.remove(db->getPath()); + + bool pathDifferent = db->getPath() != path; + + QString oldName = db->getName(); + db->setName(name); + db->setPath(path); + db->setConnectionOptions(options); + + bool result = false; + if (permanent) + { + if (CFG->isDbInConfig(oldName)) + result = CFG->updateDb(oldName, name, path, options); + else + result = CFG->addDb(name, path, options); + } + else if (CFG->isDbInConfig(name)) // switched "permanent" off? + result = CFG->removeDb(name); + + InvalidDb* invalidDb = dynamic_cast(db); + Db* reloadedDb = db; + if (pathDifferent && invalidDb) + reloadedDb = tryToLoadDb(invalidDb); + + if (reloadedDb) // reloading was not necessary (was not invalid) or it was successful + db = reloadedDb; + + nameToDb[name] = db; + pathToDb[path] = db; + + listLock.unlock(); + + if (result && reloadedDb) + emit dbUpdated(oldName, db); + else if (reloadedDb) // database reloaded correctly, but update failed + notifyError(tr("Database %1 could not be updated, because of an error: %2").arg(oldName).arg(CFG->getLastErrorString())); + + return result; +} + +void DbManagerImpl::removeDbByName(const QString &name, Qt::CaseSensitivity cs) +{ + listLock.lockForRead(); + bool contains = nameToDb.contains(name, cs); + listLock.unlock(); + + if (!contains) + return; + + listLock.lockForWrite(); + Db* db = nameToDb[name]; + removeDbInternal(db); + listLock.unlock(); + + emit dbRemoved(db); + + delete db; +} + +void DbManagerImpl::removeDbByPath(const QString &path) +{ + listLock.lockForRead(); + bool contains = pathToDb.contains(path); + listLock.unlock(); + if (!contains) + return; + + listLock.lockForWrite(); + Db* db = pathToDb[path]; + removeDbInternal(db); + listLock.unlock(); + + emit dbRemoved(db); + + delete db; +} + +void DbManagerImpl::removeDb(Db* db) +{ + db->close(); + + listLock.lockForWrite(); + removeDbInternal(db); + listLock.unlock(); + + emit dbRemoved(db); + delete db; +} + +void DbManagerImpl::removeDbInternal(Db* db, bool alsoFromConfig) +{ + QString name = db->getName(); + if (alsoFromConfig) + CFG->removeDb(name); + + nameToDb.remove(name); + pathToDb.remove(db->getPath()); + dbList.removeOne(db); + disconnect(db, SIGNAL(connected()), this, SLOT(dbConnectedSlot())); + disconnect(db, SIGNAL(disconnected()), this, SLOT(dbDisconnectedSlot())); + disconnect(db, SIGNAL(aboutToDisconnect(bool&)), this, SLOT(dbAboutToDisconnect(bool&))); +} + +QList DbManagerImpl::getDbList() +{ + listLock.lockForRead(); + QList list = dbList; + listLock.unlock(); + return list; +} + +QList DbManagerImpl::getValidDbList() +{ + QList list = getDbList(); + QMutableListIterator it(list); + while (it.hasNext()) + { + it.next(); + if (!it.value()->isValid()) + it.remove(); + } + + return list; +} + +QList DbManagerImpl::getConnectedDbList() +{ + QList list = getDbList(); + QMutableListIterator it(list); + while (it.hasNext()) + { + it.next(); + if (!it.value()->isOpen()) + it.remove(); + } + + return list; +} + +QStringList DbManagerImpl::getDbNames() +{ + QReadLocker lock(&listLock); + return nameToDb.keys(); +} + +Db* DbManagerImpl::getByName(const QString &name, Qt::CaseSensitivity cs) +{ + QReadLocker lock(&listLock); + return nameToDb.value(name, cs); +} + +Db* DbManagerImpl::getByPath(const QString &path) +{ + return pathToDb.value(path); +} + +Db* DbManagerImpl::createInMemDb() +{ + if (!inMemDbCreatorPlugin) + return nullptr; + + return inMemDbCreatorPlugin->getInstance("", ":memory:", {}); +} + +bool DbManagerImpl::isTemporary(Db* db) +{ + return CFG->getDb(db->getName()).isNull(); +} + +QString DbManagerImpl::quickAddDb(const QString& path, const QHash& options) +{ + QString newName = DbManager::generateDbName(path); + newName = generateUniqueName(newName, DBLIST->getDbNames()); + if (!DBLIST->addDb(newName, path, options, false)) + return QString::null; + + return newName; +} + +void DbManagerImpl::setInMemDbCreatorPlugin(DbPlugin* plugin) +{ + inMemDbCreatorPlugin = plugin; +} + +void DbManagerImpl::init() +{ + Q_ASSERT(PLUGINS); + + loadInitialDbList(); + + connect(PLUGINS, SIGNAL(aboutToUnload(Plugin*,PluginType*)), this, SLOT(aboutToUnload(Plugin*,PluginType*))); + connect(PLUGINS, SIGNAL(loaded(Plugin*,PluginType*)), this, SLOT(loaded(Plugin*,PluginType*))); +} + +void DbManagerImpl::loadInitialDbList() +{ + QUrl url; + InvalidDb* db = nullptr; + foreach (const Config::CfgDbPtr& cfgDb, CFG->dbList()) + { + db = new InvalidDb(cfgDb->name, cfgDb->path, cfgDb->options); + + url = QUrl::fromUserInput(cfgDb->path); + if (url.isLocalFile() && !QFile::exists(cfgDb->path)) + db->setError(tr("Database file doesn't exist.")); + else + db->setError(tr("No supporting plugin loaded.")); + + addDbInternal(db, false); + } +} + +void DbManagerImpl::notifyDatabasesAreLoaded() +{ + // Any databases were already loaded by loaded() slot, which is called when DbPlugin was loaded. + emit dbListLoaded(); +} + +void DbManagerImpl::scanForNewDatabasesInConfig() +{ + QList cfgDbList = CFG->dbList(); + + QUrl url; + InvalidDb* db = nullptr; + for (const Config::CfgDbPtr& cfgDb : cfgDbList) + { + if (getByName(cfgDb->name) || getByPath(cfgDb->path)) + continue; + + db = new InvalidDb(cfgDb->name, cfgDb->path, cfgDb->options); + + url = QUrl::fromUserInput(cfgDb->path); + if (url.isLocalFile() && !QFile::exists(cfgDb->path)) + db->setError(tr("Database file doesn't exist.")); + else + db->setError(tr("No supporting plugin loaded.")); + + addDbInternal(db); + tryToLoadDb(db); + } +} + +void DbManagerImpl::addDbInternal(Db* db, bool alsoToConfig) +{ + if (alsoToConfig) + CFG->addDb(db->getName(), db->getPath(), db->getConnectionOptions()); + + dbList << db; + nameToDb[db->getName()] = db; + pathToDb[db->getPath()] = db; + connect(db, SIGNAL(connected()), this, SLOT(dbConnectedSlot())); + connect(db, SIGNAL(disconnected()), this, SLOT(dbDisconnectedSlot())); + connect(db, SIGNAL(aboutToDisconnect(bool&)), this, SLOT(dbAboutToDisconnect(bool&))); +} + +QList DbManagerImpl::getInvalidDatabases() const +{ + return filter(dbList, [](Db* db) -> bool + { + return !db->isValid(); + }); +} + +Db* DbManagerImpl::tryToLoadDb(InvalidDb* invalidDb) +{ + QUrl url = QUrl::fromUserInput(invalidDb->getPath()); + if (url.isLocalFile() && !QFile::exists(invalidDb->getPath())) + return nullptr; + + Db* db = createDb(invalidDb->getName(), invalidDb->getPath(), invalidDb->getConnectionOptions()); + if (!db) + return nullptr; + + removeDbInternal(invalidDb, false); + delete invalidDb; + + addDbInternal(db, false); + + if (CFG->getDbGroup(db->getName())->open) + db->open(); + + emit dbLoaded(db); + return db; +} + +Db* DbManagerImpl::createDb(const QString &name, const QString &path, const QHash &options, QString* errorMessages) +{ + QList dbPlugins = PLUGINS->getLoadedPlugins(); + DbPlugin* dbPlugin = nullptr; + Db* db = nullptr; + QStringList messages; + QString message; + foreach (dbPlugin, dbPlugins) + { + if (options.contains("plugin") && options["plugin"] != dbPlugin->getName()) + continue; + + db = dbPlugin->getInstance(name, path, options, &message); + if (!db) + { + messages << message; + continue; + } + + if (!db->initAfterCreated()) + { + messages << tr("Database could not be initialized."); + continue; + } + + return db; + } + + if (errorMessages) + { + if (messages.size() == 0) + messages << tr("No suitable database driver plugin found."); + + *errorMessages = messages.join("; "); + } + + return nullptr; +} + + +void DbManagerImpl::dbConnectedSlot() +{ + QObject* sdr = sender(); + Db* db = dynamic_cast(sdr); + if (!db) + { + qWarning() << "Received connected() signal but could not cast it to Db!"; + return; + } + emit dbConnected(db); +} + +void DbManagerImpl::dbDisconnectedSlot() +{ + QObject* sdr = sender(); + Db* db = dynamic_cast(sdr); + if (!db) + { + qWarning() << "Received disconnected() signal but could not cast it to Db!"; + return; + } + emit dbDisconnected(db); +} + +void DbManagerImpl::dbAboutToDisconnect(bool& deny) +{ + QObject* sdr = sender(); + Db* db = dynamic_cast(sdr); + if (!db) + { + qWarning() << "Received dbAboutToDisconnect() signal but could not cast it to Db!"; + return; + } + emit dbAboutToBeDisconnected(db, deny); +} + +void DbManagerImpl::aboutToUnload(Plugin* plugin, PluginType* type) +{ + if (!type->isForPluginType()) + return; + + InvalidDb* invalidDb = nullptr; + DbPlugin* dbPlugin = dynamic_cast(plugin); + QList toRemove; + for (Db* db : dbList) + { + if (!dbPlugin->checkIfDbServedByPlugin(db)) + continue; + + toRemove << db; + } + + for (Db* db : toRemove) + { + emit dbAboutToBeUnloaded(db, dbPlugin); + + if (db->isOpen()) + db->close(); + + removeDbInternal(db, false); + + invalidDb = new InvalidDb(db->getName(), db->getPath(), db->getConnectionOptions()); + invalidDb->setError(tr("No supporting plugin loaded.")); + addDbInternal(invalidDb, false); + + delete db; + + emit dbUnloaded(invalidDb); + } +} + +void DbManagerImpl::loaded(Plugin* plugin, PluginType* type) +{ + if (!type->isForPluginType()) + return; + + DbPlugin* dbPlugin = dynamic_cast(plugin); + Db* db = nullptr; + + QUrl url; + for (Db* invalidDb : getInvalidDatabases()) + { + if (invalidDb->getConnectionOptions().contains(DB_PLUGIN) && invalidDb->getConnectionOptions()[DB_PLUGIN].toString() != dbPlugin->getName()) + continue; + + url = QUrl::fromUserInput(invalidDb->getPath()); + if (url.isLocalFile() && !QFile::exists(invalidDb->getPath())) + continue; + + db = createDb(invalidDb->getName(), invalidDb->getPath(), invalidDb->getConnectionOptions()); + if (!db) + continue; // For this db driver was not loaded yet. + + if (!dbPlugin->checkIfDbServedByPlugin(db)) + { + qDebug() << "Managed to load database" << db->getPath() << " (" << db->getName() << ")" + << "but it doesn't use DbPlugin that was just loaded, so it will not be loaded to the db manager"; + + delete db; + continue; + } + + removeDbInternal(invalidDb, false); + delete invalidDb; + + addDbInternal(db, false); + + if (!db->getConnectionOptions().contains(DB_PLUGIN)) + { + db->getConnectionOptions()[DB_PLUGIN] = dbPlugin->getName(); + if (!CFG->updateDb(db->getName(), db->getName(), db->getPath(), db->getConnectionOptions())) + qWarning() << "Could not store handling plugin in options for database" << db->getName(); + } + + if (CFG->getDbGroup(db->getName())->open) + db->open(); + + emit dbLoaded(db); + } +} diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h new file mode 100644 index 0000000..8e28080 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h @@ -0,0 +1,183 @@ +#ifndef DBMANAGERIMPL_H +#define DBMANAGERIMPL_H + +#include "db/db.h" +#include "coreSQLiteStudio_global.h" +#include "common/strhash.h" +#include "common/global.h" +#include "services/dbmanager.h" +#include +#include +#include +#include +#include + +class InvalidDb; + +class API_EXPORT DbManagerImpl : public DbManager +{ + Q_OBJECT + + public: + /** + * @brief Creates database manager. + * @param parent Parent object passed to QObject constructor. + */ + explicit DbManagerImpl(QObject *parent = 0); + + /** + * @brief Default destructor. + */ + ~DbManagerImpl(); + + bool addDb(const QString &name, const QString &path, const QHash &options, bool permanent = true); + bool addDb(const QString &name, const QString &path, bool permanent = true); + bool updateDb(Db* db, const QString &name, const QString &path, const QHash &options, bool permanent); + void removeDbByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive); + void removeDbByPath(const QString& path); + void removeDb(Db* db); + QList getDbList(); + QList getValidDbList(); + QList getConnectedDbList(); + QStringList getDbNames(); + Db* getByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive); + Db* getByPath(const QString& path); + Db* createInMemDb(); + bool isTemporary(Db* db); + QString quickAddDb(const QString &path, const QHash &options); + + /** + * @brief Defines database plugin used for creating in-memory databases. + * @param plugin Plugin to use. + */ + void setInMemDbCreatorPlugin(DbPlugin* plugin); + + private: + /** + * @brief Internal manager initialization. + * + * Called from any constructor. + */ + void init(); + + /** + * @brief Loads initial list of databases. + * + * Loaded databases are initially the invalid databases. + * They are turned into valid databases once their plugins are loaded. + */ + void loadInitialDbList(); + + /** + * @brief Removes database from application. + * @param db Database to be removed. + * @param alsoFromConfig If true, database will also be removed from configuration file, otherwise it's just from the manager. + * + * This method is internally called by public methods, as they all do pretty much the same thing, + * except they accept different input parameter. Then this method does the actual job. + */ + void removeDbInternal(Db* db, bool alsoFromConfig = true); + + /** + * @brief Adds database to the application. + * @param db Database to be added. + * @param alsoToConfig If true, the database will also be added to configuration file, otherwise it will be onle to the manager. + * + * When addDb() is called, it calls DbPlugin#getInstance() and if it returns object, then this method + * is called to register the database object in dbList variable. + */ + void addDbInternal(Db* db, bool alsoToConfig = true); + + /** + * @brief Filters invalid databases from all managed databases. + * @return Only invalid databases from this manager. + */ + QList getInvalidDatabases() const; + + Db* tryToLoadDb(InvalidDb* invalidDb); + + /** + * @brief Creates database object. + * @param name Symbolic name of the database. + * @param path Database file path. + * @param options Database options, such as password, etc. + * @param errorMessages If not null, then the error messages from DbPlugins are stored in that string (in case this method returns null). + * @return Database object, or null pointer. + * + * This method is used internally by addDb() methods. It goes through all DbPlugin instances + * and checks if any of them supports given file path and options and returns a database object. + * First plugin that provides database object is accepted and its result is returned from the method. + */ + static Db* createDb(const QString &name, const QString &path, const QHash &options, QString* errorMessages = nullptr); + + /** + * @brief Registered databases list. Both permanent and transient databases. + */ + QList dbList; + + /** + * @brief Database ame to database instance mapping, with keys being case insensitive. + */ + StrHash nameToDb; + + /** + * @brief Mapping from file path to the database. + * + * Mapping from database file path (as passed to addDb() or updateDb()) to the actual database object. + */ + QHash pathToDb; + + /** + * @brief Lock for dbList. + * Lock for dbList, so the list can be accessed from multiple threads. + */ + QReadWriteLock listLock; + + /** + * @brief Database plugin used to create in-memory databases. + */ + DbPlugin* inMemDbCreatorPlugin = nullptr; + + private slots: + /** + * @brief Slot called when connected to db. + * + * The slot is connected to the database object, therefore the database object has to be extracted from signal sender + * and converted to database type, then passed to the dbConnected(Db* db) signal. + */ + void dbConnectedSlot(); + /** + * @brief Slot called when connected to db. + * + * The slot is connected to the database object, therefore the database object has to be extracted from signal sender + * and converted to database type, then passed to the dbConnected(Db* db) signal. + */ + void dbDisconnectedSlot(); + + /** + * @brief Passes Db::aboutToDisconnect() signal to dbAboutToBeDisconnected() signal. + */ + void dbAboutToDisconnect(bool& deny); + + /** + * @brief Removes databases handled by the plugin from the list. + * @param plugin DbPlugin (any other will be ignored). + * @param type DbPlugin type. + * It removes all databases handled by the plugin being unloaded from the list of managed databases. + */ + void aboutToUnload(Plugin* plugin, PluginType* type); + + /** + * @brief Adds all configured databases handled by the plugin to managed list. + * @param plugin DbPlugin (any other will be ignored). + * @param type DbPlugin type. + * Checks configuration for any databases managed by the plugin and if there is any, it's loaded into the managed list. + */ + void loaded(Plugin* plugin, PluginType* type); + + public slots: + void notifyDatabasesAreLoaded(); + void scanForNewDatabasesInConfig(); +}; + +#endif // DBMANAGERIMPL_H diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp new file mode 100644 index 0000000..c94e4c2 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp @@ -0,0 +1,694 @@ +#include "functionmanagerimpl.h" +#include "services/config.h" +#include "services/pluginmanager.h" +#include "services/notifymanager.h" +#include "plugins/scriptingplugin.h" +#include "common/unused.h" +#include "common/utils.h" +#include "common/utils_sql.h" +#include "services/dbmanager.h" +#include "db/queryexecutor.h" +#include "db/sqlquery.h" +#include +#include +#include +#include +#include +#include + +FunctionManagerImpl::FunctionManagerImpl() +{ + init(); +} + +void FunctionManagerImpl::setScriptFunctions(const QList& newFunctions) +{ + clearFunctions(); + functions = newFunctions; + refreshFunctionsByKey(); + storeInConfig(); + emit functionListChanged(); +} + +QList FunctionManagerImpl::getAllScriptFunctions() const +{ + return functions; +} + +QList FunctionManagerImpl::getScriptFunctionsForDatabase(const QString& dbName) const +{ + QList results; + foreach (ScriptFunction* func, functions) + { + if (func->allDatabases || func->databases.contains(dbName, Qt::CaseInsensitive)) + results << func; + } + return results; +} + +QVariant FunctionManagerImpl::evaluateScalar(const QString& name, int argCount, const QList& args, Db* db, bool& ok) +{ + Key key; + key.name = name; + key.argCount = argCount; + key.type = ScriptFunction::SCALAR; + if (functionsByKey.contains(key)) + { + ScriptFunction* function = functionsByKey[key]; + return evaluateScriptScalar(function, name, argCount, args, db, ok); + } + else if (nativeFunctionsByKey.contains(key)) + { + NativeFunction* function = nativeFunctionsByKey[key]; + return evaluateNativeScalar(function, args, db, ok); + } + + ok = false; + return cannotFindFunctionError(name, argCount); +} + +void FunctionManagerImpl::evaluateAggregateInitial(const QString& name, int argCount, Db* db, QHash& aggregateStorage) +{ + Key key; + key.name = name; + key.argCount = argCount; + key.type = ScriptFunction::AGGREGATE; + if (functionsByKey.contains(key)) + { + ScriptFunction* function = functionsByKey[key]; + evaluateScriptAggregateInitial(function, db, aggregateStorage); + } +} + +void FunctionManagerImpl::evaluateAggregateStep(const QString& name, int argCount, const QList& args, Db* db, QHash& aggregateStorage) +{ + Key key; + key.name = name; + key.argCount = argCount; + key.type = ScriptFunction::AGGREGATE; + if (functionsByKey.contains(key)) + { + ScriptFunction* function = functionsByKey[key]; + evaluateScriptAggregateStep(function, args, db, aggregateStorage); + } +} + +QVariant FunctionManagerImpl::evaluateAggregateFinal(const QString& name, int argCount, Db* db, bool& ok, QHash& aggregateStorage) +{ + Key key; + key.name = name; + key.argCount = argCount; + key.type = ScriptFunction::AGGREGATE; + if (functionsByKey.contains(key)) + { + ScriptFunction* function = functionsByKey[key]; + return evaluateScriptAggregateFinal(function, name, argCount, db, ok, aggregateStorage); + } + + ok = false; + return cannotFindFunctionError(name, argCount); +} + +QVariant FunctionManagerImpl::evaluateScriptScalar(ScriptFunction* func, const QString& name, int argCount, const QList& args, Db* db, bool& ok) +{ + ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang); + if (!plugin) + { + ok = false; + return langUnsupportedError(name, argCount, func->lang); + } + DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast(plugin); + + QString error; + QVariant result; + + if (dbAwarePlugin) + result = dbAwarePlugin->evaluate(func->code, args, db, false, &error); + else + result = plugin->evaluate(func->code, args, &error); + + if (!error.isEmpty()) + { + ok = false; + return error; + } + return result; +} + +void FunctionManagerImpl::evaluateScriptAggregateInitial(ScriptFunction* func, Db* db, QHash& aggregateStorage) +{ + ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang); + if (!plugin) + return; + + DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast(plugin); + + ScriptingPlugin::Context* ctx = plugin->createContext(); + aggregateStorage["context"] = QVariant::fromValue(ctx); + + if (dbAwarePlugin) + dbAwarePlugin->evaluate(ctx, func->code, {}, db, false); + else + plugin->evaluate(ctx, func->code, {}); + + if (plugin->hasError(ctx)) + { + aggregateStorage["error"] = true; + aggregateStorage["errorMessage"] = plugin->getErrorMessage(ctx); + } +} + +void FunctionManagerImpl::evaluateScriptAggregateStep(ScriptFunction* func, const QList& args, Db* db, QHash& aggregateStorage) +{ + ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang); + if (!plugin) + return; + + if (aggregateStorage.contains("error")) + return; + + DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast(plugin); + + ScriptingPlugin::Context* ctx = aggregateStorage["context"].value(); + if (dbAwarePlugin) + dbAwarePlugin->evaluate(ctx, func->code, args, db, false); + else + plugin->evaluate(ctx, func->code, args); + + if (plugin->hasError(ctx)) + { + aggregateStorage["error"] = true; + aggregateStorage["errorMessage"] = plugin->getErrorMessage(ctx); + } +} + +QVariant FunctionManagerImpl::evaluateScriptAggregateFinal(ScriptFunction* func, const QString& name, int argCount, Db* db, bool& ok, QHash& aggregateStorage) +{ + ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang); + if (!plugin) + { + ok = false; + return langUnsupportedError(name, argCount, func->lang); + } + + ScriptingPlugin::Context* ctx = aggregateStorage["context"].value(); + if (aggregateStorage.contains("error")) + { + ok = false; + plugin->releaseContext(ctx); + return aggregateStorage["errorMessage"]; + } + + DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast(plugin); + + QVariant result; + if (dbAwarePlugin) + result = dbAwarePlugin->evaluate(ctx, func->code, {}, db, false); + else + result = plugin->evaluate(ctx, func->code, {}); + + if (plugin->hasError(ctx)) + { + ok = false; + QString msg = plugin->getErrorMessage(ctx); + plugin->releaseContext(ctx); + return msg; + } + + plugin->releaseContext(ctx); + return result; +} + +QList FunctionManagerImpl::getAllNativeFunctions() const +{ + return nativeFunctions; +} + +QVariant FunctionManagerImpl::evaluateNativeScalar(NativeFunction* func, const QList& args, Db* db, bool& ok) +{ + if (!func->undefinedArgs && args.size() != func->arguments.size()) + { + ok = false; + return tr("Invalid number of arguments to function '%1'. Expected %2, but got %3.").arg(func->name, QString::number(func->arguments.size()), + QString::number(args.size())); + } + + return func->functionPtr(args, db, ok); +} + +void FunctionManagerImpl::init() +{ + loadFromConfig(); + initNativeFunctions(); + refreshFunctionsByKey(); +} + +void FunctionManagerImpl::initNativeFunctions() +{ + registerNativeFunction("regexp", {"pattern", "arg"}, FunctionManagerImpl::nativeRegExp); + registerNativeFunction("sqlfile", {"file"}, FunctionManagerImpl::nativeSqlFile); + registerNativeFunction("readfile", {"file"}, FunctionManagerImpl::nativeReadFile); + registerNativeFunction("writefile", {"file", "data"}, FunctionManagerImpl::nativeWriteFile); + registerNativeFunction("langs", {}, FunctionManagerImpl::nativeLangs); + registerNativeFunction("script", {"language", "code"}, FunctionManagerImpl::nativeScript); + registerNativeFunction("html_escape", {"string"}, FunctionManagerImpl::nativeHtmlEscape); + registerNativeFunction("url_encode", {"string"}, FunctionManagerImpl::nativeUrlEncode); + registerNativeFunction("url_decode", {"string"}, FunctionManagerImpl::nativeUrlDecode); + registerNativeFunction("base64_encode", {"data"}, FunctionManagerImpl::nativeBase64Encode); + registerNativeFunction("base64_decode", {"data"}, FunctionManagerImpl::nativeBase64Decode); + registerNativeFunction("md4_bin", {"data"}, FunctionManagerImpl::nativeMd4); + registerNativeFunction("md4", {"data"}, FunctionManagerImpl::nativeMd4Hex); + registerNativeFunction("md5_bin", {"data"}, FunctionManagerImpl::nativeMd5); + registerNativeFunction("md5", {"data"}, FunctionManagerImpl::nativeMd5Hex); + registerNativeFunction("sha1", {"data"}, FunctionManagerImpl::nativeSha1); + registerNativeFunction("sha224", {"data"}, FunctionManagerImpl::nativeSha224); + registerNativeFunction("sha256", {"data"}, FunctionManagerImpl::nativeSha256); + registerNativeFunction("sha384", {"data"}, FunctionManagerImpl::nativeSha384); + registerNativeFunction("sha512", {"data"}, FunctionManagerImpl::nativeSha512); + registerNativeFunction("sha3_224", {"data"}, FunctionManagerImpl::nativeSha3_224); + registerNativeFunction("sha3_256", {"data"}, FunctionManagerImpl::nativeSha3_256); + registerNativeFunction("sha3_384", {"data"}, FunctionManagerImpl::nativeSha3_384); + registerNativeFunction("sha3_512", {"data"}, FunctionManagerImpl::nativeSha3_512); +} + +void FunctionManagerImpl::refreshFunctionsByKey() +{ + functionsByKey.clear(); + foreach (ScriptFunction* func, functions) + functionsByKey[Key(func)] = func; + + foreach (NativeFunction* func, nativeFunctions) + nativeFunctionsByKey[Key(func)] = func; +} + +void FunctionManagerImpl::storeInConfig() +{ + QVariantList list; + QHash fnHash; + foreach (ScriptFunction* func, functions) + { + fnHash["name"] = func->name; + fnHash["lang"] = func->lang; + fnHash["code"] = func->code; + fnHash["initCode"] = func->initCode; + fnHash["finalCode"] = func->finalCode; + fnHash["databases"] = common(DBLIST->getDbNames(), func->databases); + fnHash["arguments"] = func->arguments; + fnHash["type"] = static_cast(func->type); + fnHash["undefinedArgs"] = func->undefinedArgs; + fnHash["allDatabases"] = func->allDatabases; + list << fnHash; + } + CFG_CORE.Internal.Functions.set(list); +} + +void FunctionManagerImpl::loadFromConfig() +{ + clearFunctions(); + + QVariantList list = CFG_CORE.Internal.Functions.get(); + QHash fnHash; + ScriptFunction* func = nullptr; + for (const QVariant& var : list) + { + fnHash = var.toHash(); + func = new ScriptFunction(); + func->name = fnHash["name"].toString(); + func->lang = fnHash["lang"].toString(); + func->code = fnHash["code"].toString(); + func->initCode = fnHash["initCode"].toString(); + func->finalCode = fnHash["finalCode"].toString(); + func->databases = fnHash["databases"].toStringList(); + func->arguments = fnHash["arguments"].toStringList(); + func->type = static_cast(fnHash["type"].toInt()); + func->undefinedArgs = fnHash["undefinedArgs"].toBool(); + func->allDatabases = fnHash["allDatabases"].toBool(); + functions << func; + } +} + +void FunctionManagerImpl::clearFunctions() +{ + for (ScriptFunction* fn : functions) + delete fn; + + functions.clear(); +} + +QString FunctionManagerImpl::cannotFindFunctionError(const QString& name, int argCount) +{ + QStringList argMarkers = getArgMarkers(argCount); + return tr("No such function registered in SQLiteStudio: %1(%2)").arg(name).arg(argMarkers.join(",")); +} + +QString FunctionManagerImpl::langUnsupportedError(const QString& name, int argCount, const QString& lang) +{ + QStringList argMarkers = getArgMarkers(argCount); + return tr("Function %1(%2) was registered with language %3, but the plugin supporting that language is not currently loaded.") + .arg(name).arg(argMarkers.join(",")).arg(lang); +} + +QVariant FunctionManagerImpl::nativeRegExp(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 2) + { + ok = false; + return QVariant(); + } + + QRegularExpression re(args[0].toString()); + if (!re.isValid()) + { + ok = false; + return tr("Invalid regular expression pattern: %1").arg(args[0].toString()); + } + + QRegularExpressionMatch match = re.match(args[1].toString()); + return match.hasMatch(); +} + +QVariant FunctionManagerImpl::nativeSqlFile(const QList& args, Db* db, bool& ok) +{ + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + QFile file(args[0].toString()); + if (!file.open(QIODevice::ReadOnly)) + { + ok = false; + return tr("Could not open file %1 for reading: %2").arg(args[0].toString(), file.errorString()); + } + + QTextStream stream(&file); + QString sql = stream.readAll(); + file.close(); + + QueryExecutor executor(db); + executor.setAsyncMode(false); + executor.exec(sql); + SqlQueryPtr results = executor.getResults(); + if (results->isError()) + { + ok = false; + return results->getErrorText(); + } + return results->getSingleCell(); +} + +QVariant FunctionManagerImpl::nativeReadFile(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + QFile file(args[0].toString()); + if (!file.open(QIODevice::ReadOnly)) + { + ok = false; + return tr("Could not open file %1 for reading: %2").arg(args[0].toString(), file.errorString()); + } + + QByteArray data = file.readAll(); + file.close(); + return data; +} + +QVariant FunctionManagerImpl::nativeWriteFile(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 2) + { + ok = false; + return QVariant(); + } + + QFile file(args[0].toString()); + if (!file.open(QIODevice::WriteOnly|QIODevice::Truncate)) + { + ok = false; + return tr("Could not open file %1 for writting: %2").arg(args[0].toString(), file.errorString()); + } + + QByteArray data; + switch (args[1].type()) + { + case QVariant::String: + data = args[1].toString().toLocal8Bit(); + break; + default: + data = args[1].toByteArray(); + break; + } + + int res = file.write(data); + file.close(); + + if (res < 0) + { + ok = false; + return tr("Error while writting to file %1: %2").arg(args[0].toString(), file.errorString()); + } + + return res; +} + +QVariant FunctionManagerImpl::nativeScript(const QList& args, Db* db, bool& ok) +{ + if (args.size() != 2) + { + ok = false; + return QVariant(); + } + + ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(args[0].toString()); + if (!plugin) + { + ok = false; + return tr("Unsupported scripting language: %1").arg(args[0].toString()); + } + DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast(plugin); + + QString error; + QVariant result; + + if (dbAwarePlugin) + result = dbAwarePlugin->evaluate(args[1].toString(), QList(), db, false, &error); + else + result = plugin->evaluate(args[1].toString(), QList(), &error); + + if (!error.isEmpty()) + { + ok = false; + return error; + } + return result; +} + +QVariant FunctionManagerImpl::nativeLangs(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 0) + { + ok = false; + return QVariant(); + } + + QStringList names; + for (ScriptingPlugin* plugin : PLUGINS->getLoadedPlugins()) + names << plugin->getLanguage(); + + return names.join(", "); +} + +QVariant FunctionManagerImpl::nativeHtmlEscape(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + return args[0].toString().toHtmlEscaped(); +} + +QVariant FunctionManagerImpl::nativeUrlEncode(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + return QUrl::toPercentEncoding(args[0].toString()); +} + +QVariant FunctionManagerImpl::nativeUrlDecode(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + return QUrl::fromPercentEncoding(args[0].toString().toLocal8Bit()); +} + +QVariant FunctionManagerImpl::nativeBase64Encode(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + return args[0].toByteArray().toBase64(); +} + +QVariant FunctionManagerImpl::nativeBase64Decode(const QList& args, Db* db, bool& ok) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + return QByteArray::fromBase64(args[0].toByteArray()); +} + +QVariant FunctionManagerImpl::nativeCryptographicFunction(const QList& args, Db* db, bool& ok, QCryptographicHash::Algorithm algo) +{ + UNUSED(db); + + if (args.size() != 1) + { + ok = false; + return QVariant(); + } + + return QCryptographicHash::hash(args[0].toByteArray(), algo); +} + +QVariant FunctionManagerImpl::nativeMd4(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md4); +} + +QVariant FunctionManagerImpl::nativeMd4Hex(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md4).toByteArray().toHex(); +} + +QVariant FunctionManagerImpl::nativeMd5(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md5); +} + +QVariant FunctionManagerImpl::nativeMd5Hex(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md5).toByteArray().toHex(); +} + +QVariant FunctionManagerImpl::nativeSha1(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha1); +} + +QVariant FunctionManagerImpl::nativeSha224(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha224); +} + +QVariant FunctionManagerImpl::nativeSha256(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha256); +} + +QVariant FunctionManagerImpl::nativeSha384(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha384); +} + +QVariant FunctionManagerImpl::nativeSha512(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha512); +} + +QVariant FunctionManagerImpl::nativeSha3_224(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_224); +} + +QVariant FunctionManagerImpl::nativeSha3_256(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_256); +} + +QVariant FunctionManagerImpl::nativeSha3_384(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_384); +} + +QVariant FunctionManagerImpl::nativeSha3_512(const QList& args, Db* db, bool& ok) +{ + return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_512); +} + +QStringList FunctionManagerImpl::getArgMarkers(int argCount) +{ + QStringList argMarkers; + for (int i = 0; i < argCount; i++) + argMarkers << "?"; + + return argMarkers; +} + +void FunctionManagerImpl::registerNativeFunction(const QString& name, const QStringList& args, FunctionManager::NativeFunction::ImplementationFunction funcPtr) +{ + NativeFunction* nf = new NativeFunction(); + nf->name = name; + nf->arguments = args; + nf->type = FunctionBase::SCALAR; + nf->undefinedArgs = false; + nf->functionPtr = funcPtr; + nativeFunctions << nf; +} + +int qHash(const FunctionManagerImpl::Key& key) +{ + return qHash(key.name) ^ key.argCount ^ static_cast(key.type); +} + +bool operator==(const FunctionManagerImpl::Key& key1, const FunctionManagerImpl::Key& key2) +{ + return key1.name == key2.name && key1.type == key2.type && key1.argCount == key2.argCount; +} + +FunctionManagerImpl::Key::Key() +{ +} + +FunctionManagerImpl::Key::Key(FunctionBase* function) : + name(function->name), argCount(function->undefinedArgs ? -1 : function->arguments.size()), type(function->type) +{ +} diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h new file mode 100644 index 0000000..d8734e6 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h @@ -0,0 +1,96 @@ +#ifndef FUNCTIONMANAGERIMPL_H +#define FUNCTIONMANAGERIMPL_H + +#include "services/functionmanager.h" +#include + +class SqlFunctionPlugin; +class Plugin; +class PluginType; + +class API_EXPORT FunctionManagerImpl : public FunctionManager +{ + Q_OBJECT + + public: + FunctionManagerImpl(); + + void setScriptFunctions(const QList& newFunctions); + QList getAllScriptFunctions() const; + QList getScriptFunctionsForDatabase(const QString& dbName) const; + QList getAllNativeFunctions() const; + QVariant evaluateScalar(const QString& name, int argCount, const QList& args, Db* db, bool& ok); + void evaluateAggregateInitial(const QString& name, int argCount, Db* db, QHash& aggregateStorage); + void evaluateAggregateStep(const QString& name, int argCount, const QList& args, Db* db, QHash& aggregateStorage); + QVariant evaluateAggregateFinal(const QString& name, int argCount, Db* db, bool& ok, QHash& aggregateStorage); + QVariant evaluateScriptScalar(ScriptFunction* func, const QString& name, int argCount, const QList& args, Db* db, bool& ok); + void evaluateScriptAggregateInitial(ScriptFunction* func, Db* db, + QHash& aggregateStorage); + void evaluateScriptAggregateStep(ScriptFunction* func, const QList& args, Db* db, + QHash& aggregateStorage); + QVariant evaluateScriptAggregateFinal(ScriptFunction* func, const QString& name, int argCount, Db* db, bool& ok, + QHash& aggregateStorage); + QVariant evaluateNativeScalar(NativeFunction* func, const QList& args, Db* db, bool& ok); + + private: + struct Key + { + Key(); + Key(FunctionBase* function); + + QString name; + int argCount; + FunctionBase::Type type; + }; + + friend int qHash(const FunctionManagerImpl::Key& key); + friend bool operator==(const FunctionManagerImpl::Key& key1, const FunctionManagerImpl::Key& key2); + + void init(); + void initNativeFunctions(); + void refreshFunctionsByKey(); + void refreshNativeFunctionsByKey(); + void storeInConfig(); + void loadFromConfig(); + void clearFunctions(); + QString cannotFindFunctionError(const QString& name, int argCount); + QString langUnsupportedError(const QString& name, int argCount, const QString& lang); + void registerNativeFunction(const QString& name, const QStringList& args, NativeFunction::ImplementationFunction funcPtr); + + static QStringList getArgMarkers(int argCount); + static QVariant nativeRegExp(const QList& args, Db* db, bool& ok); + static QVariant nativeSqlFile(const QList& args, Db* db, bool& ok); + static QVariant nativeReadFile(const QList& args, Db* db, bool& ok); + static QVariant nativeWriteFile(const QList& args, Db* db, bool& ok); + static QVariant nativeScript(const QList& args, Db* db, bool& ok); + static QVariant nativeLangs(const QList& args, Db* db, bool& ok); + static QVariant nativeHtmlEscape(const QList& args, Db* db, bool& ok); + static QVariant nativeUrlEncode(const QList& args, Db* db, bool& ok); + static QVariant nativeUrlDecode(const QList& args, Db* db, bool& ok); + static QVariant nativeBase64Encode(const QList& args, Db* db, bool& ok); + static QVariant nativeBase64Decode(const QList& args, Db* db, bool& ok); + static QVariant nativeCryptographicFunction(const QList& args, Db* db, bool& ok, QCryptographicHash::Algorithm algo); + static QVariant nativeMd4(const QList& args, Db* db, bool& ok); + static QVariant nativeMd4Hex(const QList& args, Db* db, bool& ok); + static QVariant nativeMd5(const QList& args, Db* db, bool& ok); + static QVariant nativeMd5Hex(const QList& args, Db* db, bool& ok); + static QVariant nativeSha1(const QList& args, Db* db, bool& ok); + static QVariant nativeSha224(const QList& args, Db* db, bool& ok); + static QVariant nativeSha256(const QList& args, Db* db, bool& ok); + static QVariant nativeSha384(const QList& args, Db* db, bool& ok); + static QVariant nativeSha512(const QList& args, Db* db, bool& ok); + static QVariant nativeSha3_224(const QList& args, Db* db, bool& ok); + static QVariant nativeSha3_256(const QList& args, Db* db, bool& ok); + static QVariant nativeSha3_384(const QList& args, Db* db, bool& ok); + static QVariant nativeSha3_512(const QList& args, Db* db, bool& ok); + + QList functions; + QHash functionsByKey; + QList nativeFunctions; + QHash nativeFunctionsByKey; +}; + +int qHash(const FunctionManagerImpl::Key& key); +bool operator==(const FunctionManagerImpl::Key& key1, const FunctionManagerImpl::Key& key2); + +#endif // FUNCTIONMANAGERIMPL_H diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp new file mode 100644 index 0000000..5d7a517 --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp @@ -0,0 +1,820 @@ +#include "pluginmanagerimpl.h" +#include "plugins/scriptingplugin.h" +#include "plugins/genericplugin.h" +#include "services/notifymanager.h" +#include "common/unused.h" +#include +#include +#include +#include +#include + +PluginManagerImpl::PluginManagerImpl() +{ +} + +PluginManagerImpl::~PluginManagerImpl() +{ +} + +void PluginManagerImpl::init() +{ + pluginDirs += qApp->applicationDirPath() + "/plugins"; + pluginDirs += QDir(CFG->getConfigDir()).absoluteFilePath("plugins"); + + QString envDirs = SQLITESTUDIO->getEnv("SQLITESTUDIO_PLUGINS"); + if (!envDirs.isNull()) + pluginDirs += envDirs.split(PATH_LIST_SEPARATOR); + +#ifdef PLUGINS_DIR + pluginDirs += STRINGIFY(PLUGINS_DIR); +#endif + +#ifdef SYS_PLUGINS_DIR + pluginDirs += STRINGIFY(SYS_PLUGINS_DIR); +#endif + +#ifdef Q_OS_MACX + pluginDirs += QCoreApplication::applicationDirPath()+"/../PlugIns"; +#endif + + scanPlugins(); + loadPlugins(); +} + +void PluginManagerImpl::deinit() +{ + emit aboutToQuit(); + + // Plugin containers and their plugins + foreach (PluginContainer* container, pluginContainer.values()) + { + if (container->builtIn) + { + container->plugin->deinit(); + delete container->plugin; + } + else + unload(container->name); + } + + foreach (PluginContainer* container, pluginContainer.values()) + delete container; + + pluginContainer.clear(); + + // Types + foreach (PluginType* type, registeredPluginTypes) + delete type; + + registeredPluginTypes.clear(); + pluginCategories.clear(); +} + +QList PluginManagerImpl::getPluginTypes() const +{ + return registeredPluginTypes; +} + +QStringList PluginManagerImpl::getPluginDirs() const +{ + return pluginDirs; +} + +QString PluginManagerImpl::getFilePath(Plugin* plugin) const +{ + if (!pluginContainer.contains(plugin->getName())) + return QString::null; + + return pluginContainer[plugin->getName()]->filePath; +} + +bool PluginManagerImpl::loadBuiltInPlugin(Plugin* plugin) +{ + bool res = initPlugin(plugin); + res &= plugin->init(); + return res; +} + +PluginType* PluginManagerImpl::getPluginType(Plugin* plugin) const +{ + if (!pluginContainer.contains(plugin->getName())) + return nullptr; + + return pluginContainer[plugin->getName()]->type; +} + +void PluginManagerImpl::scanPlugins() +{ + QStringList nameFilters; + nameFilters << "*.so" << "*.dll" << "*.dylib"; + + QPluginLoader* loader = nullptr; + foreach (QString pluginDirPath, pluginDirs) + { + QDir pluginDir(pluginDirPath); + foreach (QString fileName, pluginDir.entryList(nameFilters, QDir::Files)) + { + fileName = pluginDir.absoluteFilePath(fileName); + loader = new QPluginLoader(fileName); + loader->setLoadHints(QLibrary::ExportExternalSymbolsHint|QLibrary::ResolveAllSymbolsHint); + + if (!initPlugin(loader, fileName)) + { + qDebug() << "File" << fileName << "was loaded as plugin, but SQLiteStudio couldn't initialize plugin."; + delete loader; + } + } + } + + QStringList names; + for (PluginContainer* container : pluginContainer.values()) + { + if (!container->builtIn) + names << container->name; + } + + qDebug() << "Following plugins found:" << names; +} + +void PluginManagerImpl::loadPlugins() +{ + QStringList alreadyAttempted; + for (const QString& pluginName : pluginContainer.keys()) + { + if (shouldAutoLoad(pluginName)) + load(pluginName, alreadyAttempted); + } + + pluginsAreInitiallyLoaded = true; + emit pluginsInitiallyLoaded(); +} + +bool PluginManagerImpl::initPlugin(QPluginLoader* loader, const QString& fileName) +{ + QJsonObject pluginMetaData = loader->metaData(); + QString pluginTypeName = pluginMetaData.value("MetaData").toObject().value("type").toString(); + PluginType* pluginType = nullptr; + foreach (PluginType* type, registeredPluginTypes) + { + if (type->getName() == pluginTypeName) + { + pluginType = type; + break; + } + } + + if (!pluginType) + { + qWarning() << "Could not load plugin" + fileName + "because its type was not recognized:" << pluginTypeName; + return false; + } + + QString pluginName = pluginMetaData.value("className").toString(); + QJsonObject metaObject = pluginMetaData.value("MetaData").toObject(); + + if (!checkPluginRequirements(pluginName, metaObject)) + return false; + + PluginContainer* container = new PluginContainer; + container->type = pluginType; + container->filePath = fileName; + container->loaded = false; + container->loader = loader; + pluginCategories[pluginType] << container; + pluginContainer[pluginName] = container; + + if (!readDependencies(pluginName, container, metaObject.value("dependencies"))) + return false; + + if (!readConflicts(pluginName, container, metaObject.value("conflicts"))) + return false; + + if (!readMetaData(container)) + { + delete container; + return false; + } + + return true; +} + +bool PluginManagerImpl::checkPluginRequirements(const QString& pluginName, const QJsonObject& metaObject) +{ + if (metaObject.value("gui").toBool(false) && !SQLITESTUDIO->isGuiAvailable()) + { + qDebug() << "Plugin" << pluginName << "skipped, because it requires GUI and this is not GUI client running."; + return false; + } + + int minVer = metaObject.value("minQtVersion").toInt(0); + if (QT_VERSION_CHECK(minVer / 10000, minVer / 100 % 100, minVer % 10000) > QT_VERSION) + { + qDebug() << "Plugin" << pluginName << "skipped, because it requires at least Qt version" << toPrintableVersion(minVer) << ", but got" << QT_VERSION_STR; + return false; + } + + int maxVer = metaObject.value("maxQtVersion").toInt(999999); + if (QT_VERSION_CHECK(maxVer / 10000, maxVer / 100 % 100, maxVer % 10000) < QT_VERSION) + { + qDebug() << "Plugin" << pluginName << "skipped, because it requires at most Qt version" << toPrintableVersion(maxVer) << ", but got" << QT_VERSION_STR; + return false; + } + + minVer = metaObject.value("minAppVersion").toInt(0); + if (SQLITESTUDIO->getVersion() < minVer) + { + qDebug() << "Plugin" << pluginName << "skipped, because it requires at least SQLiteStudio version" << toPrintableVersion(minVer) << ", but got" + << SQLITESTUDIO->getVersionString(); + return false; + } + + maxVer = metaObject.value("maxAppVersion").toInt(999999); + if (SQLITESTUDIO->getVersion() > maxVer) + { + qDebug() << "Plugin" << pluginName << "skipped, because it requires at most SQLiteStudio version" << toPrintableVersion(maxVer) << ", but got" + << SQLITESTUDIO->getVersionString(); + return false; + } + + return true; +} + +bool PluginManagerImpl::readDependencies(const QString& pluginName, PluginManagerImpl::PluginContainer* container, const QJsonValue& depsValue) +{ + if (depsValue.isUndefined()) + return true; + + QJsonArray depsArray; + if (depsValue.type() == QJsonValue::Array) + depsArray = depsValue.toArray(); + else + depsArray.append(depsValue); + + PluginDependency dep; + QJsonObject depObject; + for (const QJsonValue& value : depsArray) + { + if (value.type() == QJsonValue::Object) + { + depObject = value.toObject(); + if (!depObject.contains("name")) + { + qWarning() << "Invalid dependency entry in plugin" << pluginName << " - doesn't contain 'name' of the dependency."; + return false; + } + + dep.name = depObject.value("name").toString(); + dep.minVersion = depObject.value("minVersion").toInt(0); + dep.maxVersion = depObject.value("maxVersion").toInt(0); + } + else + { + dep.maxVersion = 0; + dep.minVersion = 0; + dep.name = value.toString(); + } + container->dependencies << dep; + } + return true; +} + +bool PluginManagerImpl::readConflicts(const QString& pluginName, PluginManagerImpl::PluginContainer* container, const QJsonValue& confValue) +{ + UNUSED(pluginName); + + if (confValue.isUndefined()) + return true; + + QJsonArray confArray; + if (confValue.type() == QJsonValue::Array) + confArray = confValue.toArray(); + else + confArray.append(confValue); + + for (const QJsonValue& value : confArray) + container->conflicts << value.toString(); + + return true; +} + +bool PluginManagerImpl::initPlugin(Plugin* plugin) +{ + QString pluginName = plugin->getName(); + PluginType* pluginType = nullptr; + foreach (PluginType* type, registeredPluginTypes) + { + if (type->test(plugin)) + { + pluginType = type; + break; + } + } + + if (!pluginType) + { + qWarning() << "Could not load built-in plugin" + pluginName + "because its type was not recognized."; + return false; + } + + PluginContainer* container = new PluginContainer; + container->type = pluginType; + container->loaded = true; + container->builtIn = true; + container->plugin = plugin; + pluginCategories[pluginType] << container; + pluginContainer[pluginName] = container; + if (!readMetaData(container)) + { + delete container; + return false; + } + + pluginLoaded(container); + return true; +} + +bool PluginManagerImpl::shouldAutoLoad(const QString& pluginName) +{ + QStringList loadedPlugins = CFG_CORE.General.LoadedPlugins.get().split(",", QString::SkipEmptyParts); + QStringList pair; + foreach (const QString& loadedPlugin, loadedPlugins) + { + pair = loadedPlugin.split("="); + if (pair.size() != 2) + { + qWarning() << "Invalid entry in config General.LoadedPlugins:" << loadedPlugin; + continue; + } + + if (pair[0] == pluginName) + return (bool)pair[1].toInt(); + } + + return true; +} + +QStringList PluginManagerImpl::getAllPluginNames(PluginType* type) const +{ + QStringList names; + if (!pluginCategories.contains(type)) + return names; + + foreach (PluginContainer* container, pluginCategories[type]) + names << container->name; + + return names; +} + +QStringList PluginManagerImpl::getAllPluginNames() const +{ + return pluginContainer.keys(); +} + +PluginType* PluginManagerImpl::getPluginType(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return nullptr; + + return pluginContainer[pluginName]->type; +} + +QString PluginManagerImpl::getAuthor(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return QString::null; + + return pluginContainer[pluginName]->author; +} + +QString PluginManagerImpl::getTitle(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return QString::null; + + return pluginContainer[pluginName]->title; +} + +QString PluginManagerImpl::getPrintableVersion(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return QString::null; + + return pluginContainer[pluginName]->printableVersion; +} + +int PluginManagerImpl::getVersion(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return 0; + + return pluginContainer[pluginName]->version; +} + +QString PluginManagerImpl::getDescription(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return QString::null; + + return pluginContainer[pluginName]->description; +} + +void PluginManagerImpl::unload(Plugin* plugin) +{ + if (!plugin) + return; + + unload(plugin->getName()); +} + +void PluginManagerImpl::unload(const QString& pluginName) +{ + if (!pluginContainer.contains(pluginName)) + { + qWarning() << "No such plugin in containers:" << pluginName << "while trying to unload plugin."; + return; + } + + // Checking preconditions + PluginContainer* container = pluginContainer[pluginName]; + if (container->builtIn) + return; + + if (!container->loaded) + return; + + // Unloading depdendent plugins + for (PluginContainer* otherContainer : pluginContainer.values()) + { + if (otherContainer == container) + continue; + + for (const PluginDependency& dep : otherContainer->dependencies) + { + if (dep.name == pluginName) + { + unload(otherContainer->name); + break; + } + } + } + + // Removing from fast-lookup collections + removePluginFromCollections(container->plugin); + + // Deinitializing and unloading plugin + emit aboutToUnload(container->plugin, container->type); + container->plugin->deinit(); + + QPluginLoader* loader = container->loader; + if (!loader->isLoaded()) + { + qWarning() << "QPluginLoader says the plugin is not loaded. Weird."; + emit unloaded(container->name, container->type); + return; + } + + loader->unload(); + + container->plugin = nullptr; + container->loaded = false; + + emit unloaded(container->name, container->type); + + qDebug() << pluginName << "unloaded:" << container->filePath; +} + +bool PluginManagerImpl::load(const QString& pluginName) +{ + QStringList alreadyAttempted; + bool res = load(pluginName, alreadyAttempted); + if (!res) + emit failedToLoad(pluginName); + + return res; +} + +bool PluginManagerImpl::load(const QString& pluginName, QStringList& alreadyAttempted, int minVersion, int maxVersion) +{ + if (alreadyAttempted.contains(pluginName)) + return false; + + // Checking initial conditions + if (!pluginContainer.contains(pluginName)) + { + qWarning() << "No such plugin in containers:" << pluginName << "while trying to load plugin."; + alreadyAttempted.append(pluginName); + return false; + } + + PluginContainer* container = pluginContainer[pluginName]; + + if (minVersion > 0 && container->version < minVersion) + { + qWarning() << "Requested plugin" << pluginName << "in version at least" << minVersion << "but have:" << container->version; + return false; + } + + if (maxVersion > 0 && container->version > maxVersion) + { + qWarning() << "Requested plugin" << pluginName << "in version at most" << maxVersion << "but have:" << container->version; + return false; + } + + if (container->builtIn) + return true; + + QPluginLoader* loader = container->loader; + if (loader->isLoaded()) + return true; + + // Checking for conflicting plugins + for (PluginContainer* otherContainer : pluginContainer.values()) + { + if (!otherContainer->loaded || otherContainer->name == pluginName) + continue; + + if (container->conflicts.contains(otherContainer->name) || otherContainer->conflicts.contains(pluginName)) + { + notifyWarn(tr("Cannot load plugin %1, because it's in conflict with plugin %2.").arg(pluginName, otherContainer->name)); + alreadyAttempted.append(pluginName); + return false; + } + } + + // Loading depended plugins + for (const PluginDependency& dep : container->dependencies) + { + if (!load(dep.name, alreadyAttempted, dep.minVersion, dep.maxVersion)) + { + notifyWarn(tr("Cannot load plugin %1, because its dependency was not loaded: %2.").arg(pluginName, dep.name)); + alreadyAttempted.append(pluginName); + return false; + } + } + + // Loading pluginName + if (!loader->load()) + { + notifyWarn(tr("Cannot load plugin %1. Error details: %2").arg(pluginName, loader->errorString())); + alreadyAttempted.append(pluginName); + return false; + } + + // Initializing loaded plugin + Plugin* plugin = dynamic_cast(container->loader->instance()); + GenericPlugin* genericPlugin = dynamic_cast(plugin); + if (genericPlugin) + { + genericPlugin->loadMetaData(container->loader->metaData()); + } + + if (!plugin->init()) + { + loader->unload(); + notifyWarn(tr("Cannot load plugin %1 (error while initializing plugin).").arg(pluginName)); + alreadyAttempted.append(pluginName); + return false; + } + + pluginLoaded(container); + + return true; +} + +void PluginManagerImpl::pluginLoaded(PluginManagerImpl::PluginContainer* container) +{ + if (!container->builtIn) + { + container->plugin = dynamic_cast(container->loader->instance()); + container->loaded = true; + } + addPluginToCollections(container->plugin); + + emit loaded(container->plugin, container->type); + if (!container->builtIn) + qDebug() << container->name << "loaded:" << container->filePath; +} + +void PluginManagerImpl::addPluginToCollections(Plugin* plugin) +{ + ScriptingPlugin* scriptingPlugin = dynamic_cast(plugin); + if (scriptingPlugin) + scriptingPlugins[scriptingPlugin->getLanguage()] = scriptingPlugin; +} + +void PluginManagerImpl::removePluginFromCollections(Plugin* plugin) +{ + ScriptingPlugin* scriptingPlugin = dynamic_cast(plugin); + if (scriptingPlugin && scriptingPlugins.contains(scriptingPlugin->getLanguage())) + scriptingPlugins.remove(plugin->getName()); +} + +bool PluginManagerImpl::readMetaData(PluginManagerImpl::PluginContainer* container) +{ + if (container->loader) + { + QHash metaData = readMetaData(container->loader->metaData()); + container->name = metaData["name"].toString(); + container->version = metaData["version"].toInt(); + container->printableVersion = toPrintableVersion(metaData["version"].toInt()); + container->author = metaData["author"].toString(); + container->description = metaData["description"].toString(); + container->title = metaData["title"].toString(); + } + else if (container->plugin) + { + container->name = container->plugin->getName(); + container->version = container->plugin->getVersion(); + container->printableVersion = container->plugin->getPrintableVersion(); + container->author = container->plugin->getAuthor(); + container->description = container->plugin->getDescription(); + container->title = container->plugin->getTitle(); + } + else + { + qCritical() << "Could not read metadata for some plugin. It has no loader or plugin object defined."; + return false; + } + return true; +} + +bool PluginManagerImpl::isLoaded(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + { + qWarning() << "No such plugin in containers:" << pluginName << "while trying to get plugin 'loaded' status."; + return false; + } + + return pluginContainer[pluginName]->loaded; +} + +bool PluginManagerImpl::isBuiltIn(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + { + qWarning() << "No such plugin in containers:" << pluginName << "while trying to get plugin 'builtIn' status."; + return false; + } + + return pluginContainer[pluginName]->builtIn; +} + +Plugin* PluginManagerImpl::getLoadedPlugin(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return nullptr; + + if (!pluginContainer[pluginName]->loaded) + return nullptr; + + return pluginContainer[pluginName]->plugin; +} + +QList PluginManagerImpl::getLoadedPlugins(PluginType* type) const +{ + QList list; + if (!pluginCategories.contains(type)) + return list; + + foreach (PluginContainer* container, pluginCategories[type]) + { + if (container->loaded) + list << container->plugin; + } + + return list; +} + +ScriptingPlugin* PluginManagerImpl::getScriptingPlugin(const QString& languageName) const +{ + if (scriptingPlugins.contains(languageName)) + return scriptingPlugins[languageName]; + + return nullptr; +} + +QHash PluginManagerImpl::readMetaData(const QJsonObject& metaData) +{ + QHash results; + results["name"] = metaData.value("className").toString(); + + QJsonObject root = metaData.value("MetaData").toObject(); + results["type"] = root.value("type").toString(); + results["title"] = root.value("title").toString(); + results["description"] = root.value("description").toString(); + results["author"] = root.value("author").toString(); + results["version"] = root.value("version").toInt(); + results["ui"] = root.value("ui").toString(); + return results; +} + +QString PluginManagerImpl::toPrintableVersion(int version) const +{ + static const QString versionStr = QStringLiteral("%1.%2.%3"); + return versionStr.arg(version / 10000) + .arg(version / 100 % 100) + .arg(version % 100); +} + +QStringList PluginManagerImpl::getDependencies(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return QStringList(); + + static const QString verTpl = QStringLiteral(" (%1)"); + QString minVerTpl = tr("min: %1", "plugin dependency version"); + QString maxVerTpl = tr("max: %1", "plugin dependency version"); + QStringList outputList; + QString depStr; + QStringList depVerList; + for (const PluginDependency& dep : pluginContainer[pluginName]->dependencies) + { + depStr = dep.name; + if (dep.minVersion > 0 || dep.maxVersion > 0) + { + depVerList.clear(); + if (dep.minVersion > 0) + depVerList << minVerTpl.arg(toPrintableVersion(dep.minVersion)); + + if (dep.maxVersion > 0) + depVerList << minVerTpl.arg(toPrintableVersion(dep.maxVersion)); + + depStr += verTpl.arg(depVerList.join(", ")); + } + outputList << depStr; + } + + return outputList; +} + +QStringList PluginManagerImpl::getConflicts(const QString& pluginName) const +{ + if (!pluginContainer.contains(pluginName)) + return QStringList(); + + return pluginContainer[pluginName]->conflicts; +} + +bool PluginManagerImpl::arePluginsInitiallyLoaded() const +{ + return pluginsAreInitiallyLoaded; +} + +QList PluginManagerImpl::getLoadedPlugins() const +{ + QList plugins; + foreach (PluginContainer* container, pluginContainer.values()) + { + if (container->loaded) + plugins << container->plugin; + } + return plugins; +} + +QStringList PluginManagerImpl::getLoadedPluginNames() const +{ + QStringList names; + foreach (PluginContainer* container, pluginContainer.values()) + { + if (container->loaded) + names << container->name; + } + return names; +} + +QList PluginManagerImpl::getAllPluginDetails() const +{ + QList results; + PluginManager::PluginDetails details; + foreach (PluginContainer* container, pluginContainer.values()) + { + details.name = container->name; + details.title = container->title; + details.description = container->description; + details.builtIn = container->builtIn; + details.version = container->version; + details.filePath = container->filePath; + details.versionString = formatVersion(container->version); + results << details; + } + return results; +} + +QList PluginManagerImpl::getLoadedPluginDetails() const +{ + QList results = getAllPluginDetails(); + QMutableListIterator it(results); + while (it.hasNext()) + { + if (!isLoaded(it.next().name)) + it.remove(); + } + return results; +} + +void PluginManagerImpl::registerPluginType(PluginType* type) +{ + registeredPluginTypes << type; +} diff --git a/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h new file mode 100644 index 0000000..6968cab --- /dev/null +++ b/SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h @@ -0,0 +1,321 @@ +#ifndef PLUGINMANAGERIMPL_H +#define PLUGINMANAGERIMPL_H + +#include "services/pluginmanager.h" +#include +#include + +class API_EXPORT PluginManagerImpl : public PluginManager +{ + Q_OBJECT + + public: + /** + * @brief Creates plugin manager. + */ + PluginManagerImpl(); + + /** + * @brief Deletes plugin manager. + */ + ~PluginManagerImpl(); + + void init(); + void deinit(); + QList getPluginTypes() const; + QStringList getPluginDirs() const; + QString getFilePath(Plugin* plugin) const; + bool loadBuiltInPlugin(Plugin* plugin); + bool load(const QString& pluginName); + void unload(const QString& pluginName); + void unload(Plugin* plugin); + bool isLoaded(const QString& pluginName) const; + bool isBuiltIn(const QString& pluginName) const; + Plugin* getLoadedPlugin(const QString& pluginName) const; + QStringList getAllPluginNames(PluginType* type) const; + QStringList getAllPluginNames() const; + PluginType* getPluginType(const QString& pluginName) const; + QString getAuthor(const QString& pluginName) const; + QString getTitle(const QString& pluginName) const; + QString getPrintableVersion(const QString& pluginName) const; + int getVersion(const QString& pluginName) const; + QString getDescription(const QString& pluginName) const; + PluginType* getPluginType(Plugin* plugin) const; + QList getLoadedPlugins(PluginType* type) const; + ScriptingPlugin* getScriptingPlugin(const QString& languageName) const; + QHash readMetaData(const QJsonObject& metaData); + QString toPrintableVersion(int version) const; + QStringList getDependencies(const QString& pluginName) const; + QStringList getConflicts(const QString& pluginName) const; + bool arePluginsInitiallyLoaded() const; + QList getLoadedPlugins() const; + QStringList getLoadedPluginNames() const; + QList getAllPluginDetails() const; + QList getLoadedPluginDetails() const; + + protected: + void registerPluginType(PluginType* type); + + private: + struct PluginDependency + { + QString name; + int minVersion = 0; + int maxVersion = 0; + }; + + /** + * @brief Container for plugin related data. + * + * The container is used to represent plugin available to the application, + * no matter if it's loaded or not. It keeps all plugin related data, + * so it's available even the plugin is not loaded. + */ + struct PluginContainer + { + /** + * @brief Name of the plugin. + */ + QString name; + + /** + * @brief Title of the plugin, used on UI. + */ + QString title; + + /** + * @brief Plugin's detailed description. + */ + QString description; + + /** + * @brief Plugin's author. + */ + QString author; + + /** + * @brief Numeric verion of the plugin. + */ + int version; + + /** + * @brief Human-readable version. + */ + QString printableVersion; + + /** + * @brief Type of the plugin. + */ + PluginType* type = nullptr; + + /** + * @brief Full path to the plugin's file. + */ + QString filePath; + + /** + * @brief Plugin's loaded state flag. + */ + bool loaded; + + /** + * @brief Qt's plugin framework loaded for this plugin. + */ + QPluginLoader* loader = nullptr; + + /** + * @brief Plugin object. + * + * It's null when plugin is not loaded. + */ + Plugin* plugin = nullptr; + + /** + * @brief Flag indicating that the plugin is built in. + * + * Plugins built-in are classes implementing plugin's interface, + * but they are compiled and statically linked to the main application binary. + * They cannot be loaded or unloaded - they are loaded by default. + */ + bool builtIn = false; + + /** + * @brief Names of plugnis that this plugin depends on. + */ + QList dependencies; + + /** + * @brief Names of plugins that this plugin conflicts with. + */ + QStringList conflicts; + }; + + /** + * @brief List of plugins, both loaded and unloaded. + */ + typedef QList PluginContainerList; + + /** + * @brief Scans plugin directories to find out available plugins. + * + * It looks in the following locations: + *
    + *
  • application_directory/plugins/ + *
  • application_config_directory/plugins/ + *
  • directory pointed by the SQLITESTUDIO_PLUGINS environment variable + *
  • directory compiled in as PLUGINS_DIR parameter of the compilation + *
+ * + * The application_directory is a directory where the application executable is. + * The application_config_directory can be different, see ConfigImpl::initDbFile() for details. + * The SQLITESTUDIO_PLUGINS variable can contain several paths, separated by : (for Unix/Mac) or ; (for Windows). + */ + void scanPlugins(); + + /** + * @brief Loads plugins defined in configuration. + * + * It loads all plugins that are available to the application + * and are not marked to not load in the configuration. + * + * In other words, every plugin will load by default, unless it was + * explicitly unloaded previously and that was saved in the configuration + * (when application was closing). + */ + void loadPlugins(); + + /** + * @brief Loads given plugin. + * @param pluginName Name of the plugin to load. + * @param alreadyAttempted List of plugin names that were already attempted to be loaded. + * @param minVersion Minimum required version of the plugin to load. + * @param maxVersion Maximum required version of the plugin to load. + * @return true on success, false on failure. + * + * This is pretty much what the public load() method does, except this one tracks what plugins were already + * attempted to be loaded (and failed), so it doesn't warn twice about the same plugin if it failed + * to load while it was a dependency for some other plugins. + * + * It also allows to define minimum and maximum plugin version, so if SQLiteStudio has the plugin available, + * but the version is out of required range, it will also fail to load. + */ + bool load(const QString& pluginName, QStringList& alreadyAttempted, int minVersion = 0, int maxVersion = 0); + + /** + * @brief Executes standard routines after plugin was loaded. + * @param container Container for the loaded plugin. + * + * It fills all members of the plugin container and emits loaded() signal. + */ + void pluginLoaded(PluginContainer* container); + + /** + * @brief Stores some specific plugin types in internal collections for faster access. + * @param plugin Plugin that was just loaded. + * + * This is called after we are sure we have a Plugin instance. + * + * The method stores certain plugin types in internal collections, so they can be accessed + * faster, instead of calling getLoadedPlugin(), which is not as fast. + * + * The internal collections are used for plugins that are likely to be accessed frequently, + * like ScriptingPlugin. + */ + void addPluginToCollections(Plugin* plugin); + + /** + * @brief Removes plugin from internal collections. + * @param plugin Plugin that is about to be unloaded. + * + * This is the reverse operation to what addPluginToCollections(Plugin*) does. + */ + void removePluginFromCollections(Plugin* plugin); + + /** + * @brief Reads title, description, author, etc. from the plugin. + * @param plugin Plugin to read data from. + * @param container Container to put the data to. + * @return true on success, false on problems (with details in logs) + * + * It does the reading by calling all related methods from Plugin interface, + * then stores those information in given \p container. + * + * The built-in plugins define those methods using their class metadata. + * + * External plugins provide this information in their file metadata + * and this method uses QPluginLoader to read this metadata. + */ + bool readMetaData(PluginContainer* container); + + /** + * @brief Creates plugin container and initializes it. + * @param loader Qt's plugin framework loader used to load this plugin. + * For built-in plugins (statically linked) this must be null. + * @param fileName Plugin's file path. For built-in plugins it's ignored. + * @param plugin Plugin object from loaded plugin. + * @return true if the initialization succeeded, or false otherwise. + * + * It assigns plugin type to the plugin, creates plugin container and fills + * all necessary data for the plugin. If the plugin was configured to not load, + * then this method unloads the file, before plugin was initialized (with Plugin::init()). + * + * All plugins are loaded at the start, but before they are fully initialized + * and enabled, they are simply queried for metadata, then either unloaded + * (when configured to not load at startup), or the initialization proceeds. + */ + bool initPlugin(QPluginLoader* loader, const QString& fileName); + + bool checkPluginRequirements(const QString& pluginName, const QJsonObject& metaObject); + bool readDependencies(const QString& pluginName, PluginContainer* container, const QJsonValue& depsValue); + bool readConflicts(const QString& pluginName, PluginContainer* container, const QJsonValue& confValue); + + /** + * @brief Creates plugin container and initializes it. + * @param plugin Built-in plugin object. + * @return true if the initialization succeeded, or false otherwise. + * + * This is pretty much the same as the other initPlugin() method, but this one is for built-in plugins. + */ + bool initPlugin(Plugin* plugin); + + /** + * @brief Tests if given plugin is configured to be loaded at startup. + * @param plugin Tested plugin object. + * @return true if plugin should be loaded at startup, or false otherwise. + * + * This method checks General.LoadedPlugins configuration entry to see if plugin + * was explicitly disabled for loading at startup. + */ + bool shouldAutoLoad(const QString& pluginName); + + /** + * @brief List of plugin directories (not necessarily absolute paths). + */ + QStringList pluginDirs; + + /** + * @brief List of registered plugin types. + */ + QList registeredPluginTypes; + + /** + * @brief Table with plugin types as keys and list of plugins assigned for each type. + */ + QHash pluginCategories; + + /** + * @brief Table with plugin names and containers assigned for each plugin. + */ + QHash pluginContainer; + + /** + * @brief Internal list of scripting plugins, updated on load/unload of plugins. + * + * Keys are scripting language name. It's a separate table to optimize querying scripting plugins. + */ + QHash scriptingPlugins; + + bool pluginsAreInitiallyLoaded = false; +}; + +#endif // PLUGINMANAGERIMPL_H -- cgit v1.2.3