Next: 08 - Building Storage Module, Previous: 06 - 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.