X-Git-Url: https://vaikene.ee/gitweb/gitweb.cgi?p=evaf;a=blobdiff_plain;f=www%2Fpswgen07.html;fp=www%2Fpswgen07.html;h=d35d48405ace88d2fa9aeca056c7a8d51dce1732;hp=0000000000000000000000000000000000000000;hb=0a383af776fe4c1c0ac1059dd3732f32cb6e1c7a;hpb=adefe2aae67c52dc2aa41c45aedb8ad822478ee1 diff --git a/www/pswgen07.html b/www/pswgen07.html new file mode 100644 index 0000000..d35d484 --- /dev/null +++ b/www/pswgen07.html @@ -0,0 +1,563 @@ + + + + + + eVaf Tutorial - 07 - Storage Module + + + + + + + + + +

eVaf Tutorial

+ +

07 - Storage Module

+ +

Create the module.cpp file in the src/apps/PswGen/Storage directory: + +

/**
+ * @file PswGen/Storage/module.cpp
+ */
+
+#include "module.h"
+#include <QtCore>
+ +

Copy version information files from the Generator module:

+ +
evaf/src/apps/PswGen/Storage $ cp ../Generator/version.{h,rc} .
+ +

Modify the version.h file:

+ +
/**
+ * @file PswGen/Storage/version.h
+ */
+
+#ifndef __PSWGEN_STORAGE_VERSION_H
+#define   __PSWGEN_STORAGE_VERSION_H
+
+#include <version_rc.h>
+
+/**
+ * Module/library version number in the form major,minor,release,build
+ */
+#define VER_FILE_VERSION                0,1,1,1
+
+/**
+ * Module/library version number in the string format (shall end with \0)
+ */
+#define VER_FILE_VERSION_STR            "0.1.1.1\0"
+
+/**
+ * Module/library name (shall end with \0)
+ */
+#define VER_MODULE_NAME_STR             "PswStorage\0"
+
+/**
+ * Module type (see version_rc.h for all the types)
+ */
+#define VER_MODULE_TYPE                 MT_GENERIC
+
+/**
+ * Module type in the string format (see version_rc for all the types)
+ */
+#define VER_MODULE_TYPE_STR             MT_GENERIC
+
+/**
+ * Original file name for windows (shall end with \0)
+ */
+#define VER_ORIGINAL_FILE_NAME_STR      "PswStorage.dll\0"
+
+/**
+ * Description of the module/library (shall end with \0)
+ */
+#define VER_FILE_DESCRIPTION_STR         "Module that stores data for generating strong passwords.\0"
+
+#endif // version.h
+ +

Include the version.h header file in module.cpp and export version information:

+ +
+#include "version.h"
+
+VER_EXPORT_VERSION_INFO()
+ +

Make it a proper Qt plugin by using the Q_EXPORT_PLUGIN2() macro:

+ +
Q_EXPORT_PLUGIN2(VER_MODULE_NAME_STR, eVaf::PswGen::Storage::Module)
+ +

The Module class creates the internal StorageImpl object in the constructor, but initializes it in the + init() method. This way we can return errors from the module if initialization fails. Similarly, we finalize the + internal StorageImpl object in the done() method and delete in the destructor.

+ +

The module is ready when the internal StorageImpl object is initalized and we set the mReady flag to + true when the initialization is done and back to false in the done() method.

+ +

The rest of the code sets the name of the object and outputs info messages.

+ +
Module::Module()
+    : Plugins::iPlugin()
+    , mReady(false)
+{
+    setObjectName(QString("%1.%2").arg(VER_MODULE_NAME_STR).arg(__FUNCTION__));
+
+    mStorage = new Internal::StorageImpl;
+
+    EVAF_INFO("%s created", qPrintable(objectName()));
+}
+
+Module::~Module()
+{
+    delete mStorage;
+
+    EVAF_INFO("%s destroyed", qPrintable(objectName()));
+}
+
+bool Module::init(QString const & args)
+{
+    Q_UNUSED(args);
+
+    if (!mStorage->init())
+        return false;
+
+    mReady = true;
+
+    EVAF_INFO("%s initialized", qPrintable(objectName()));
+
+    return true;
+}
+
+void Module::done()
+{
+    mReady = false;
+
+    mStorage->done();
+
+    EVAF_INFO("%s finalized", qPrintable(objectName()));
+}
+ +

The StorageImpl class does very little in the constructor and in the destructor:

+ +
StorageImpl::StorageImpl()
+    : QAbstractListModel()
+{
+    setObjectName(QString("%1.iGenerator").arg(VER_MODULE_NAME_STR));
+
+    EVAF_INFO("%s created", qPrintable(objectName()));
+}
+
+StorageImpl::~StorageImpl()
+{
+    EVAF_INFO("%s destroyed", qPrintable(objectName()));
+}
+ +

Initialization of the StorageImpl class happens in the init() method, where we open the database connection, + create tables if necessary and load data from the database. We also register the iStorage interface in the global + registry of interfaces.

+ +

We use the Common::iApp interface to find the data root directory where the SQLITE database file is going to be + located.

+ +
bool StorageImpl::init()
+{
+    // Open the database
+    if (!QSqlDatabase::contains(DbConnectionName)) {
+        // No database connection yet
+        mDb = QSqlDatabase::addDatabase("QSQLITE", DbConnectionName);
+        mDb.setDatabaseName(Common::iApp::instance()->dataRootDir() + DbName);
+        if (!mDb.open()) {
+            QSqlError err = mDb.lastError();
+            EVAF_ERROR("Failed to open database : %s", qPrintable(err.text()));
+            return false;
+        }
+    }
+    else {
+        // Database connection already exists
+        mDb = QSqlDatabase::database(DbConnectionName);
+    }
+
+    // Create tables if necessary
+    if (!createTables())
+        return false;
+
+    // Load data
+    if (!loadData())
+        return false;
+
+    /// Register our interface
+    Common::iRegistry::instance()->registerInterface("iStorage", this);
+
+    EVAF_INFO("%s initialized", qPrintable(objectName()));
+
+    return true;
+}
+ +

The finalization of the StorageImpl class happens in the done() method, where we only need to clear the list + of shared data objects:

+ +
void StorageImpl::done()
+{
+    mData.clear();
+    EVAF_INFO("%s finalized", qPrintable(objectName()));
+}
+ +

The StorageImpl::save() method verifies that the shared data object is valid and the name not empty. Then it either + adds a new data record to the database or updates an existing one.

+ +
bool StorageImpl::save(QString const & name, QExplicitlySharedDataPointer<Storage::Data> data)
+{
+    EVAF_TEST_X(data, "Data cannot be null");
+    EVAF_TEST_X(!name.isEmpty(), "Name cannot be empty");
+
+    // Is it an update or a new data record?
+    if (mData.constFind(name) != mData.constEnd()) {
+        // This is an update
+        if (data->modified()) {
+            QSqlQuery q(mDb);
+            if (!q.exec(QString("UPDATE data SET length = \'%1\', flags = \'%2\' WHERE name = \'%3\';")
+                            .arg(data->length()).arg(data->flags()).arg(name))) {
+                QSqlError err = mDb.lastError();
+                EVAF_ERROR("Failed to update \'%s\' : %s", qPrintable(name), qPrintable(err.text()));
+                return false;
+            }
+        }
+    }
+    else {
+        // Store to the database
+        QSqlQuery q(mDb);
+        if (!q.exec(QString("INSERT INTO data (name, length, flags) VALUES (\'%1\', %2, %3);")
+                            .arg(name).arg(data->length())
+                            .arg(int(data->flags())))) {
+            QSqlError err = mDb.lastError();
+            EVAF_ERROR("Failed to insert \'%s\' : %s", qPrintable(name), qPrintable(err.text()));
+            return false;
+        }
+
+        // Store also into the local hash
+        mData.insert(name, data);
+
+        // Reset the model
+        reset();
+    }
+
+    data->reset();
+
+    return true;
+}
+ +

Calling the reset() method resets the QAbstractItemModel data model and assures that the associated widget + updates the QCompleter with a fresh list of auto completion words. Resetting the data model in this case is fine as + long as the data model is simple and the list short. The proper way would be finding out the actual insertion point and using + beginInsertRows() and endInsertRows() methods.

+ +

Querying data is much simpler thanks to the internal QMap container:

+ +
QExplicitlySharedDataPointer<Storage::Data> StorageImpl::query(QString const & name) const
+{
+    QMap<QString, QExplicitlySharedDataPointer<Storage::Data> >::const_iterator it = mData.constFind(name);
+    if (it != mData.constEnd())
+        return it.value();
+    else
+        return QExplicitlySharedDataPointer<Storage::Data>();
+}
+ +

We return an empty QExplicitlySharedDataPointer object if no data objects with the given name exists.

+ +

The data() method returns names of data objects for the QCompleter auto completion list of words:

+ +
QVariant StorageImpl::data(QModelIndex const & index, int role) const
+{
+    if (!index.isValid() || index.row() < 0 || index.row() >= mData.size() || index.column() != 0)
+        return QVariant();
+
+    if (role == Qt::EditRole || role == Qt::DisplayRole)
+        return mData.keys().at(index.row());
+
+    return QVariant();
+}
+ +

The createTabled() method creates the data table if it does not exist. This function could be improved to + alter the data table if it exists, but is from an older version. Right now we assume that if the table exists, it + is good for our application:

+ +
bool StorageImpl::createTables()
+{
+    QSqlQuery q(mDb);
+    if (!q.exec("SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'data\';")) {
+        QSqlError err = mDb.lastError();
+        EVAF_ERROR("Failed to query database : %s", qPrintable(err.text()));
+        return false;
+    }
+
+    if (q.isActive() && q.isSelect() && q.first())
+        return true; // We already have a table called 'data'
+
+    // Create the 'data' table
+    if (!q.exec("CREATE TABLE data (name text primary key not null, length integer, flags integer);")) {
+        QSqlError err = mDb.lastError();
+        EVAF_ERROR("Failed to create table \'data\' : %s", qPrintable(err.text()));
+        return false;
+    }
+
+    return true;
+}
+ +

Finally, the loadData() method loads all the data records from the database to the memory:

+ +
bool StorageImpl::loadData()
+{
+    QSqlQuery q(mDb);
+    if (!q.exec("SELECT name, length, flags FROM data;")) {
+        QSqlError err = mDb.lastError();
+        EVAF_ERROR("Failed to query database : %s", qPrintable(err.text()));
+        return false;
+    }
+
+    while (q.next()) {
+        QString name = q.value(0).toString();
+        QExplicitlySharedDataPointer<Storage::Data> data(new Storage::Data(name, q.value(1).toInt(), uint(q.value(2).toInt())));
+        mData.insert(name, data);
+    }
+
+    reset();
+
+    return true;
+}
+ +

Resetting the QAbstractItemModel data model here is ok, becase before loading any data the model is supposed to be + empty.

+ +

Here is the complete module.cpp file: + +

/**
+ * @file PswGen/Storage/module.cpp
+ */
+
+#include "module.h"
+#include "version.h"
+
+#include <Common/Globals>
+#include <Common/iLogger>
+#include <Common/iRegistry>
+#include <Common/iApp>
+
+#include <QtCore>
+#include <QtSql/QtSql>
+
+VER_EXPORT_VERSION_INFO()
+Q_EXPORT_PLUGIN2(VER_MODULE_NAME_STR, eVaf::PswGen::Storage::Module)
+
+using namespace eVaf;
+using namespace eVaf::PswGen;
+using namespace eVaf::PswGen::Storage;
+
+Module::Module()
+    : Plugins::iPlugin()
+    , mReady(false)
+{
+    setObjectName(QString("%1.%2").arg(VER_MODULE_NAME_STR).arg(__FUNCTION__));
+
+    mStorage = new Internal::StorageImpl;
+
+    EVAF_INFO("%s created", qPrintable(objectName()));
+}
+
+Module::~Module()
+{
+    delete mStorage;
+
+    EVAF_INFO("%s destroyed", qPrintable(objectName()));
+}
+
+bool Module::init(QString const & args)
+{
+    Q_UNUSED(args);
+
+    if (!mStorage->init())
+        return false;
+
+    mReady = true;
+
+    EVAF_INFO("%s initialized", qPrintable(objectName()));
+
+    return true;
+}
+
+void Module::done()
+{
+    mReady = false;
+
+    mStorage->done();
+
+    EVAF_INFO("%s finalized", qPrintable(objectName()));
+}
+
+using namespace eVaf::PswGen::Storage::Internal;
+
+char const * const StorageImpl::DbConnectionName = "PswGenDB";
+
+char const * const StorageImpl::DbName = "PswGen.sqlite";
+
+StorageImpl::StorageImpl()
+    : QAbstractListModel()
+{
+    setObjectName(QString("%1.iGenerator").arg(VER_MODULE_NAME_STR));
+
+    EVAF_INFO("%s created", qPrintable(objectName()));
+}
+
+StorageImpl::~StorageImpl()
+{
+    EVAF_INFO("%s destroyed", qPrintable(objectName()));
+}
+
+bool StorageImpl::init()
+{
+    // Open the database
+    if (!QSqlDatabase::contains(DbConnectionName)) {
+        // No database connection yet
+        mDb = QSqlDatabase::addDatabase("QSQLITE", DbConnectionName);
+        mDb.setDatabaseName(Common::iApp::instance()->dataRootDir() + DbName);
+        if (!mDb.open()) {
+            QSqlError err = mDb.lastError();
+            EVAF_ERROR("Failed to open database : %s", qPrintable(err.text()));
+            return false;
+        }
+    }
+    else {
+        // Database connection already exists
+        mDb = QSqlDatabase::database(DbConnectionName);
+    }
+
+    // Create tables if necessary
+    if (!createTables())
+        return false;
+
+    // Load data
+    if (!loadData())
+        return false;
+
+    /// Register our interface
+    Common::iRegistry::instance()->registerInterface("iStorage", this);
+
+    EVAF_INFO("%s initialized", qPrintable(objectName()));
+
+    return true;
+}
+
+void StorageImpl::done()
+{
+    mData.clear();
+    EVAF_INFO("%s finalized", qPrintable(objectName()));
+}
+
+bool StorageImpl::save(QString const & name, QExplicitlySharedDataPointer<Storage::Data> data)
+{
+    EVAF_TEST_X(data, "Data cannot be null");
+    EVAF_TEST_X(!name.isEmpty(), "Name cannot be empty");
+
+    // Is it an update or a new data record?
+    if (mData.constFind(name) != mData.constEnd()) {
+        // This is an update
+        if (data->modified()) {
+            QSqlQuery q(mDb);
+            if (!q.exec(QString("UPDATE data SET length = \'%1\', flags = \'%2\' WHERE name = \'%3\';")
+                            .arg(data->length()).arg(data->flags()).arg(name))) {
+                QSqlError err = mDb.lastError();
+                EVAF_ERROR("Failed to update \'%s\' : %s", qPrintable(name), qPrintable(err.text()));
+                return false;
+            }
+        }
+    }
+    else {
+        // Store to the database
+        QSqlQuery q(mDb);
+        if (!q.exec(QString("INSERT INTO data (name, length, flags) VALUES (\'%1\', %2, %3);")
+                            .arg(name).arg(data->length())
+                            .arg(int(data->flags())))) {
+            QSqlError err = mDb.lastError();
+            EVAF_ERROR("Failed to insert \'%s\' : %s", qPrintable(name), qPrintable(err.text()));
+            return false;
+        }
+
+        // Store also into the local hash
+        mData.insert(name, data);
+
+        // Reset the model
+        reset();
+    }
+
+    data->reset();
+
+    return true;
+}
+
+QExplicitlySharedDataPointer<Storage::Data> StorageImpl::query(QString const & name) const
+{
+    QMap<QString, QExplicitlySharedDataPointer<Storage::Data> >::const_iterator it = mData.constFind(name);
+    if (it != mData.constEnd())
+        return it.value();
+    else
+        return QExplicitlySharedDataPointer<Storage::Data>();
+}
+
+QVariant StorageImpl::data(QModelIndex const & index, int role) const
+{
+    if (!index.isValid() || index.row() < 0 || index.row() >= mData.size() || index.column() != 0)
+        return QVariant();
+
+    if (role == Qt::EditRole || role == Qt::DisplayRole)
+        return mData.keys().at(index.row());
+
+    return QVariant();
+}
+
+bool StorageImpl::createTables()
+{
+    QSqlQuery q(mDb);
+    if (!q.exec("SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'data\';")) {
+        QSqlError err = mDb.lastError();
+        EVAF_ERROR("Failed to query database : %s", qPrintable(err.text()));
+        return false;
+    }
+
+    if (q.isActive() && q.isSelect() && q.first())
+        return true; // We already have a table called 'data'
+
+    // Create the 'data' table
+    if (!q.exec("CREATE TABLE data (name text primary key not null, length integer, flags integer);")) {
+        QSqlError err = mDb.lastError();
+        EVAF_ERROR("Failed to create table \'data\' : %s", qPrintable(err.text()));
+        return false;
+    }
+
+    return true;
+}
+
+bool StorageImpl::loadData()
+{
+    QSqlQuery q(mDb);
+    if (!q.exec("SELECT name, length, flags FROM data;")) {
+        QSqlError err = mDb.lastError();
+        EVAF_ERROR("Failed to query database : %s", qPrintable(err.text()));
+        return false;
+    }
+
+    while (q.next()) {
+        QString name = q.value(0).toString();
+        QExplicitlySharedDataPointer<Storage::Data> data(new Storage::Data(name, q.value(1).toInt(), uint(q.value(2).toInt())));
+        mData.insert(name, data);
+    }
+
+    reset();
+
+    return true;
+}
+ +

Next -- Building Storage Module.

+ + +