summaryrefslogtreecommitdiffstats
path: root/SQLiteStudio3/coreSQLiteStudio/services/impl
diff options
context:
space:
mode:
Diffstat (limited to 'SQLiteStudio3/coreSQLiteStudio/services/impl')
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.cpp125
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/collationmanagerimpl.h36
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.cpp770
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/configimpl.h127
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.cpp524
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/dbmanagerimpl.h183
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.cpp694
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/functionmanagerimpl.h96
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.cpp820
-rw-r--r--SQLiteStudio3/coreSQLiteStudio/services/impl/pluginmanagerimpl.h321
10 files changed, 3696 insertions, 0 deletions
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 <QDebug>
+
+CollationManagerImpl::CollationManagerImpl()
+{
+ init();
+}
+
+void CollationManagerImpl::setCollations(const QList<CollationManager::CollationPtr>& newCollations)
+{
+ collations = newCollations;
+ refreshCollationsByKey();
+ storeInConfig();
+ emit collationListChanged();
+}
+
+QList<CollationManager::CollationPtr> CollationManagerImpl::getAllCollations() const
+{
+ return collations;
+}
+
+QList<CollationManager::CollationPtr> CollationManagerImpl::getCollationsForDatabase(const QString& dbName) const
+{
+ QList<CollationPtr> 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<QString,QVariant> 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<QString,QVariant> 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<CollationPtr>& newCollations);
+ QList<CollationPtr> getAllCollations() const;
+ QList<CollationPtr> 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<CollationPtr> collations;
+ QHash<QString,CollationPtr> collationsByKey;
+ QHash<QString,ScriptingPlugin*> 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 <QtGlobal>
+#include <QDebug>
+#include <QList>
+#include <QDir>
+#include <QFileInfo>
+#include <QDataStream>
+#include <QRegExp>
+#include <QDateTime>
+#include <QSysInfo>
+#include <QtConcurrent/QtConcurrentRun>
+
+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<QString,QVariant> ConfigImpl::getAll()
+{
+ SqlQueryPtr results = db->exec("SELECT [group], [key], value FROM settings");
+
+ QHash<QString,QVariant> 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<QString,QVariant>& 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<QString,QVariant> &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::CfgDbPtr> ConfigImpl::dbList()
+{
+ QList<CfgDbPtr> 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<DbGroupPtr>& 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::DbGroupPtr> 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<QString>("text");
+}
+
+void ConfigImpl::addDdlHistory(const QString& queries, const QString& dbName, const QString& dbFile)
+{
+ QtConcurrent::run(this, &ConfigImpl::asyncAddDdlHistory, queries, dbName, dbFile);
+}
+
+QList<ConfigImpl::DdlHistoryEntryPtr> 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<DdlHistoryEntryPtr> 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<Config::ReportHistoryEntryPtr> ConfigImpl::getReportHistory()
+{
+ static_qstring(sql, "SELECT id, timestamp, title, url, feature_request FROM reports_history");
+
+ SqlQueryPtr results = db->exec(sql);
+
+ QList<ReportHistoryEntryPtr> 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<QString> tables = results->columnAsList<QString>(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<QString,QVariant> getAll();
+
+ bool addDb(const QString& name, const QString& path, const QHash<QString, QVariant> &options);
+ bool updateDb(const QString& name, const QString &newName, const QString& path, const QHash<QString, QVariant> &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<CfgDbPtr> dbList();
+ CfgDbPtr getDb(const QString& dbName);
+
+ void storeGroups(const QList<DbGroupPtr>& groups);
+ QList<DbGroupPtr> 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<DdlHistoryEntryPtr> 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<ReportHistoryEntryPtr> 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 <QCoreApplication>
+#include <QFileInfo>
+#include <QHash>
+#include <QHashIterator>
+#include <QPluginLoader>
+#include <QDebug>
+#include <QUrl>
+#include <db/invaliddb.h>
+
+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<QString,QVariant>(), permanent);
+}
+
+bool DbManagerImpl::addDb(const QString &name, const QString &path, const QHash<QString,QVariant>& 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<QString, QVariant> &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<InvalidDb*>(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<Db*> DbManagerImpl::getDbList()
+{
+ listLock.lockForRead();
+ QList<Db*> list = dbList;
+ listLock.unlock();
+ return list;
+}
+
+QList<Db*> DbManagerImpl::getValidDbList()
+{
+ QList<Db*> list = getDbList();
+ QMutableListIterator<Db*> it(list);
+ while (it.hasNext())
+ {
+ it.next();
+ if (!it.value()->isValid())
+ it.remove();
+ }
+
+ return list;
+}
+
+QList<Db*> DbManagerImpl::getConnectedDbList()
+{
+ QList<Db*> list = getDbList();
+ QMutableListIterator<Db*> 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<QString, QVariant>& 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<Config::CfgDbPtr> 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<Db*> DbManagerImpl::getInvalidDatabases() const
+{
+ return filter<Db*>(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<QString,QVariant> &options, QString* errorMessages)
+{
+ QList<DbPlugin*> dbPlugins = PLUGINS->getLoadedPlugins<DbPlugin>();
+ 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<Db*>(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<Db*>(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<Db*>(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<DbPlugin>())
+ return;
+
+ InvalidDb* invalidDb = nullptr;
+ DbPlugin* dbPlugin = dynamic_cast<DbPlugin*>(plugin);
+ QList<Db*> 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<DbPlugin>())
+ return;
+
+ DbPlugin* dbPlugin = dynamic_cast<DbPlugin*>(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 <QObject>
+#include <QList>
+#include <QHash>
+#include <QReadWriteLock>
+#include <QSharedPointer>
+
+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<QString, QVariant> &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<QString, QVariant> &options, bool permanent);
+ void removeDbByName(const QString& name, Qt::CaseSensitivity cs = Qt::CaseSensitive);
+ void removeDbByPath(const QString& path);
+ void removeDb(Db* db);
+ QList<Db*> getDbList();
+ QList<Db*> getValidDbList();
+ QList<Db*> 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<QString, QVariant> &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<Db*> 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<QString, QVariant> &options, QString* errorMessages = nullptr);
+
+ /**
+ * @brief Registered databases list. Both permanent and transient databases.
+ */
+ QList<Db*> dbList;
+
+ /**
+ * @brief Database ame to database instance mapping, with keys being case insensitive.
+ */
+ StrHash<Db*> 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<QString,Db*> 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 <QVariantList>
+#include <QHash>
+#include <QDebug>
+#include <QRegularExpression>
+#include <QFile>
+#include <QUrl>
+
+FunctionManagerImpl::FunctionManagerImpl()
+{
+ init();
+}
+
+void FunctionManagerImpl::setScriptFunctions(const QList<ScriptFunction*>& newFunctions)
+{
+ clearFunctions();
+ functions = newFunctions;
+ refreshFunctionsByKey();
+ storeInConfig();
+ emit functionListChanged();
+}
+
+QList<FunctionManager::ScriptFunction*> FunctionManagerImpl::getAllScriptFunctions() const
+{
+ return functions;
+}
+
+QList<FunctionManager::ScriptFunction*> FunctionManagerImpl::getScriptFunctionsForDatabase(const QString& dbName) const
+{
+ QList<ScriptFunction*> 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<QVariant>& 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<QString,QVariant>& 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<QVariant>& args, Db* db, QHash<QString,QVariant>& 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<QString,QVariant>& 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<QVariant>& 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<DbAwareScriptingPlugin*>(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<QString, QVariant>& aggregateStorage)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ return;
+
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(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<QVariant>& args, Db* db, QHash<QString, QVariant>& aggregateStorage)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ return;
+
+ if (aggregateStorage.contains("error"))
+ return;
+
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(plugin);
+
+ ScriptingPlugin::Context* ctx = aggregateStorage["context"].value<ScriptingPlugin::Context*>();
+ 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<QString, QVariant>& aggregateStorage)
+{
+ ScriptingPlugin* plugin = PLUGINS->getScriptingPlugin(func->lang);
+ if (!plugin)
+ {
+ ok = false;
+ return langUnsupportedError(name, argCount, func->lang);
+ }
+
+ ScriptingPlugin::Context* ctx = aggregateStorage["context"].value<ScriptingPlugin::Context*>();
+ if (aggregateStorage.contains("error"))
+ {
+ ok = false;
+ plugin->releaseContext(ctx);
+ return aggregateStorage["errorMessage"];
+ }
+
+ DbAwareScriptingPlugin* dbAwarePlugin = dynamic_cast<DbAwareScriptingPlugin*>(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<FunctionManager::NativeFunction*> FunctionManagerImpl::getAllNativeFunctions() const
+{
+ return nativeFunctions;
+}
+
+QVariant FunctionManagerImpl::evaluateNativeScalar(NativeFunction* func, const QList<QVariant>& 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<QString,QVariant> 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<int>(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<QString,QVariant> 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<ScriptFunction::Type>(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<QVariant>& 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<QVariant>& 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<QVariant>& 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<QVariant>& 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<QVariant>& 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<DbAwareScriptingPlugin*>(plugin);
+
+ QString error;
+ QVariant result;
+
+ if (dbAwarePlugin)
+ result = dbAwarePlugin->evaluate(args[1].toString(), QList<QVariant>(), db, false, &error);
+ else
+ result = plugin->evaluate(args[1].toString(), QList<QVariant>(), &error);
+
+ if (!error.isEmpty())
+ {
+ ok = false;
+ return error;
+ }
+ return result;
+}
+
+QVariant FunctionManagerImpl::nativeLangs(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ UNUSED(db);
+
+ if (args.size() != 0)
+ {
+ ok = false;
+ return QVariant();
+ }
+
+ QStringList names;
+ for (ScriptingPlugin* plugin : PLUGINS->getLoadedPlugins<ScriptingPlugin>())
+ names << plugin->getLanguage();
+
+ return names.join(", ");
+}
+
+QVariant FunctionManagerImpl::nativeHtmlEscape(const QList<QVariant>& 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<QVariant>& 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<QVariant>& 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<QVariant>& 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<QVariant>& 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<QVariant>& 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<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md4);
+}
+
+QVariant FunctionManagerImpl::nativeMd4Hex(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md4).toByteArray().toHex();
+}
+
+QVariant FunctionManagerImpl::nativeMd5(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md5);
+}
+
+QVariant FunctionManagerImpl::nativeMd5Hex(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Md5).toByteArray().toHex();
+}
+
+QVariant FunctionManagerImpl::nativeSha1(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha1);
+}
+
+QVariant FunctionManagerImpl::nativeSha224(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha224);
+}
+
+QVariant FunctionManagerImpl::nativeSha256(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha256);
+}
+
+QVariant FunctionManagerImpl::nativeSha384(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha384);
+}
+
+QVariant FunctionManagerImpl::nativeSha512(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha512);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_224(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_224);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_256(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_256);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_384(const QList<QVariant>& args, Db* db, bool& ok)
+{
+ return nativeCryptographicFunction(args, db, ok, QCryptographicHash::Sha3_384);
+}
+
+QVariant FunctionManagerImpl::nativeSha3_512(const QList<QVariant>& 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<int>(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 <QCryptographicHash>
+
+class SqlFunctionPlugin;
+class Plugin;
+class PluginType;
+
+class API_EXPORT FunctionManagerImpl : public FunctionManager
+{
+ Q_OBJECT
+
+ public:
+ FunctionManagerImpl();
+
+ void setScriptFunctions(const QList<ScriptFunction*>& newFunctions);
+ QList<ScriptFunction*> getAllScriptFunctions() const;
+ QList<ScriptFunction*> getScriptFunctionsForDatabase(const QString& dbName) const;
+ QList<NativeFunction*> getAllNativeFunctions() const;
+ QVariant evaluateScalar(const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok);
+ void evaluateAggregateInitial(const QString& name, int argCount, Db* db, QHash<QString, QVariant>& aggregateStorage);
+ void evaluateAggregateStep(const QString& name, int argCount, const QList<QVariant>& args, Db* db, QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateAggregateFinal(const QString& name, int argCount, Db* db, bool& ok, QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateScriptScalar(ScriptFunction* func, const QString& name, int argCount, const QList<QVariant>& args, Db* db, bool& ok);
+ void evaluateScriptAggregateInitial(ScriptFunction* func, Db* db,
+ QHash<QString, QVariant>& aggregateStorage);
+ void evaluateScriptAggregateStep(ScriptFunction* func, const QList<QVariant>& args, Db* db,
+ QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateScriptAggregateFinal(ScriptFunction* func, const QString& name, int argCount, Db* db, bool& ok,
+ QHash<QString, QVariant>& aggregateStorage);
+ QVariant evaluateNativeScalar(NativeFunction* func, const QList<QVariant>& 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<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSqlFile(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeReadFile(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeWriteFile(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeScript(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeLangs(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeHtmlEscape(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeUrlEncode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeUrlDecode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeBase64Encode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeBase64Decode(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeCryptographicFunction(const QList<QVariant>& args, Db* db, bool& ok, QCryptographicHash::Algorithm algo);
+ static QVariant nativeMd4(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeMd4Hex(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeMd5(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeMd5Hex(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha1(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha224(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha256(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha384(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha512(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_224(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_256(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_384(const QList<QVariant>& args, Db* db, bool& ok);
+ static QVariant nativeSha3_512(const QList<QVariant>& args, Db* db, bool& ok);
+
+ QList<ScriptFunction*> functions;
+ QHash<Key,ScriptFunction*> functionsByKey;
+ QList<NativeFunction*> nativeFunctions;
+ QHash<Key,NativeFunction*> 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 <QCoreApplication>
+#include <QDir>
+#include <QDebug>
+#include <QJsonArray>
+#include <QJsonValue>
+
+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<PluginType*> 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<Plugin*>(container->loader->instance());
+ GenericPlugin* genericPlugin = dynamic_cast<GenericPlugin*>(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<Plugin*>(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<ScriptingPlugin*>(plugin);
+ if (scriptingPlugin)
+ scriptingPlugins[scriptingPlugin->getLanguage()] = scriptingPlugin;
+}
+
+void PluginManagerImpl::removePluginFromCollections(Plugin* plugin)
+{
+ ScriptingPlugin* scriptingPlugin = dynamic_cast<ScriptingPlugin*>(plugin);
+ if (scriptingPlugin && scriptingPlugins.contains(scriptingPlugin->getLanguage()))
+ scriptingPlugins.remove(plugin->getName());
+}
+
+bool PluginManagerImpl::readMetaData(PluginManagerImpl::PluginContainer* container)
+{
+ if (container->loader)
+ {
+ QHash<QString, QVariant> 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<Plugin*> PluginManagerImpl::getLoadedPlugins(PluginType* type) const
+{
+ QList<Plugin*> 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<QString, QVariant> PluginManagerImpl::readMetaData(const QJsonObject& metaData)
+{
+ QHash<QString, QVariant> 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<Plugin*> PluginManagerImpl::getLoadedPlugins() const
+{
+ QList<Plugin*> 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<PluginManager::PluginDetails> PluginManagerImpl::getAllPluginDetails() const
+{
+ QList<PluginManager::PluginDetails> 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<PluginManager::PluginDetails> PluginManagerImpl::getLoadedPluginDetails() const
+{
+ QList<PluginManager::PluginDetails> results = getAllPluginDetails();
+ QMutableListIterator<PluginManager::PluginDetails> 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 <QPluginLoader>
+#include <QHash>
+
+class API_EXPORT PluginManagerImpl : public PluginManager
+{
+ Q_OBJECT
+
+ public:
+ /**
+ * @brief Creates plugin manager.
+ */
+ PluginManagerImpl();
+
+ /**
+ * @brief Deletes plugin manager.
+ */
+ ~PluginManagerImpl();
+
+ void init();
+ void deinit();
+ QList<PluginType*> 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<Plugin*> getLoadedPlugins(PluginType* type) const;
+ ScriptingPlugin* getScriptingPlugin(const QString& languageName) const;
+ QHash<QString,QVariant> 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<Plugin*> getLoadedPlugins() const;
+ QStringList getLoadedPluginNames() const;
+ QList<PluginDetails> getAllPluginDetails() const;
+ QList<PluginDetails> 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<PluginDependency> dependencies;
+
+ /**
+ * @brief Names of plugins that this plugin conflicts with.
+ */
+ QStringList conflicts;
+ };
+
+ /**
+ * @brief List of plugins, both loaded and unloaded.
+ */
+ typedef QList<PluginContainer*> PluginContainerList;
+
+ /**
+ * @brief Scans plugin directories to find out available plugins.
+ *
+ * It looks in the following locations:
+ * <ul>
+ * <li> application_directory/plugins/
+ * <li> application_config_directory/plugins/
+ * <li> directory pointed by the SQLITESTUDIO_PLUGINS environment variable
+ * <li> directory compiled in as PLUGINS_DIR parameter of the compilation
+ * </ul>
+ *
+ * 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<T>(), 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<PluginType*> registeredPluginTypes;
+
+ /**
+ * @brief Table with plugin types as keys and list of plugins assigned for each type.
+ */
+ QHash<PluginType*,PluginContainerList> pluginCategories;
+
+ /**
+ * @brief Table with plugin names and containers assigned for each plugin.
+ */
+ QHash<QString,PluginContainer*> 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<QString,ScriptingPlugin*> scriptingPlugins;
+
+ bool pluginsAreInitiallyLoaded = false;
+};
+
+#endif // PLUGINMANAGERIMPL_H