diff --git a/liblearnerprofile/src/storage.cpp b/liblearnerprofile/src/storage.cpp index f8921a0..9b880be 100644 --- a/liblearnerprofile/src/storage.cpp +++ b/liblearnerprofile/src/storage.cpp @@ -1,679 +1,679 @@ /* * Copyright 2013-2016 Andreas Cord-Landwehr * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "storage.h" #include "learner.h" #include "liblearner_debug.h" #include #include #include #include #include #include #include using namespace LearnerProfile; Storage::Storage(QObject* parent) : QObject(parent) , m_databasePath(QStandardPaths::writableLocation( QStandardPaths::DataLocation) + QLatin1Char('/') + "learnerdata.db") , m_errorMessage(QString()) { } -Storage::Storage(const QString databasePath, QObject* parent) +Storage::Storage(const QString &databasePath, QObject* parent) : QObject(parent) , m_databasePath(databasePath) , m_errorMessage(QString()) { qCDebug(LIBLEARNER_LOG) << "Initialize with custom DB path:" << m_databasePath; } QString Storage::errorMessage() const { return m_errorMessage; } void Storage::raiseError(const QSqlError &error) { m_errorMessage = QStringLiteral("%1 : %2").arg(error.driverText()).arg(error.databaseText()); emit errorMessageChanged(); } bool Storage::storeProfile(Learner *learner) { QSqlDatabase db = database(); // test whether ID is present QSqlQuery idExistsQuery(db); idExistsQuery.prepare(QStringLiteral("SELECT COUNT(*) FROM profiles WHERE id = :id")); idExistsQuery.bindValue(QStringLiteral(":id"), learner->identifier()); idExistsQuery.exec(); if (db.lastError().isValid()) { qCritical() << "ExistsQuery: " << db.lastError().text(); raiseError(db.lastError()); return false; } // go to first result row that contains the count idExistsQuery.next(); if (idExistsQuery.value(0).toInt() < 1) { // in case learner ID is not found in database QSqlQuery insertProfileQuery(db); insertProfileQuery.prepare(QStringLiteral("INSERT INTO profiles (id, name) VALUES (?, ?)")); insertProfileQuery.bindValue(0, learner->identifier()); insertProfileQuery.bindValue(1, learner->name()); insertProfileQuery.exec(); if (insertProfileQuery.lastError().isValid()) { raiseError(insertProfileQuery.lastError()); db.rollback(); return false; } } else { // update name otherwise QSqlQuery updateProfileQuery(db); updateProfileQuery.prepare(QStringLiteral("UPDATE profiles SET name = :name WHERE id = :id")); updateProfileQuery.bindValue(QStringLiteral(":id"), learner->identifier()); updateProfileQuery.bindValue(QStringLiteral(":name"), learner->name()); updateProfileQuery.exec(); if (updateProfileQuery.lastError().isValid()) { qCritical() << updateProfileQuery.lastError().text(); raiseError(updateProfileQuery.lastError()); db.rollback(); return false; } } // store existing learning goal relations foreach (LearningGoal *goal, learner->goals()) { QSqlQuery relationExistsQuery(db); relationExistsQuery.prepare("SELECT COUNT(*) FROM learner_goals " "WHERE goal_category = :goalCategory " "AND goal_identifier = :goalIdentifier " "AND profile_id = :profileId " ); relationExistsQuery.bindValue(QStringLiteral(":goalCategory"), goal->category()); relationExistsQuery.bindValue(QStringLiteral(":goalIdentifier"), goal->identifier()); relationExistsQuery.bindValue(QStringLiteral(":profileId"), learner->identifier()); relationExistsQuery.exec(); if (db.lastError().isValid()) { qCritical() << "ExistsQuery: " << db.lastError().text(); raiseError(db.lastError()); return false; } // go to first result row that contains the count relationExistsQuery.next(); if (relationExistsQuery.value(0).toInt() < 1) { QSqlQuery insertProfileQuery(db); insertProfileQuery.prepare(QStringLiteral("INSERT INTO learner_goals (goal_category, goal_identifier, profile_id) VALUES (?, ?, ?)")); insertProfileQuery.bindValue(0, goal->category()); insertProfileQuery.bindValue(1, goal->identifier()); insertProfileQuery.bindValue(2, learner->identifier()); insertProfileQuery.exec(); } } // remove deleted relations QSqlQuery cleanupRelations(db); cleanupRelations.prepare(QStringLiteral("DELETE FROM learner_goals WHERE ")); //TODO change creation of relations to same way as remove-relations: explicit connections return true; } bool Storage::removeProfile(Learner *learner) { QSqlDatabase db = database(); QSqlQuery removeProfileQuery(db); // delete learner removeProfileQuery.prepare(QStringLiteral("DELETE FROM profiles WHERE id = ?")); removeProfileQuery.bindValue(0, learner->identifier()); removeProfileQuery.exec(); if (removeProfileQuery.lastError().isValid()) { qCritical() << removeProfileQuery.lastError().text(); raiseError(removeProfileQuery.lastError()); db.rollback(); return false; } // delete learning goal relations QSqlQuery removeGoalRelationQuery(db); removeGoalRelationQuery.prepare(QStringLiteral("DELETE FROM learner_goals WHERE profile_id = ?")); removeGoalRelationQuery.bindValue(0, learner->identifier()); removeGoalRelationQuery.exec(); if (removeGoalRelationQuery.lastError().isValid()) { qCritical() << removeGoalRelationQuery.lastError().text(); raiseError(removeGoalRelationQuery.lastError()); db.rollback(); return false; } return true; } bool Storage::removeRelation(Learner *learner, LearningGoal *goal) { QSqlDatabase db = database(); QSqlQuery removeGoalRelationQuery(db); removeGoalRelationQuery.prepare( "DELETE FROM learner_goals " "WHERE goal_category = :goalCategory " "AND goal_identifier = :goalIdentifier " "AND profile_id = :profileId " ); removeGoalRelationQuery.bindValue(QStringLiteral(":goalCategory"), goal->category()); removeGoalRelationQuery.bindValue(QStringLiteral(":goalIdentifier"), goal->identifier()); removeGoalRelationQuery.bindValue(QStringLiteral(":profileId"), learner->identifier()); removeGoalRelationQuery.exec(); if (db.lastError().isValid()) { qCritical() << "ExistsQuery: " << db.lastError().text(); raiseError(db.lastError()); return false; } return true; } QList< Learner* > Storage::loadProfiles(QList goals) { QSqlDatabase db = database(); QSqlQuery profileQuery(db); profileQuery.prepare(QStringLiteral("SELECT id, name FROM profiles")); profileQuery.exec(); if (profileQuery.lastError().isValid()) { qCritical() << profileQuery.lastError().text(); raiseError(profileQuery.lastError()); return QList(); } QList profiles; while (profileQuery.next()) { Learner* profile = new Learner(); profile->setIdentifier(profileQuery.value(0).toInt()); profile->setName(profileQuery.value(1).toString()); profiles.append(profile); } // associate to goals QSqlQuery goalRelationQuery(db); goalRelationQuery.prepare(QStringLiteral("SELECT goal_category, goal_identifier, profile_id FROM learner_goals")); goalRelationQuery.exec(); if (goalRelationQuery.lastError().isValid()) { qCritical() << goalRelationQuery.lastError().text(); raiseError(goalRelationQuery.lastError()); return QList(); } while (goalRelationQuery.next()) { Learner *learner = nullptr; LearningGoal *goal = nullptr; foreach (Learner *cmpProfile, profiles) { if (cmpProfile->identifier() == goalRelationQuery.value(2).toInt()) { learner = cmpProfile; break; } } if (!learner) { qCCritical(LIBLEARNER_LOG) << "Could not retrieve learner from database."; return QList(); } foreach (LearningGoal *cmpGoal, goals) { if (cmpGoal->category() == goalRelationQuery.value(0).toInt() && cmpGoal->identifier() == goalRelationQuery.value(1).toString()) { goal = cmpGoal; break; } } if (learner->goals().contains(goal)) { continue; } if (goal) { learner->addGoal(goal); } } return profiles; } bool Storage::storeGoal(LearningGoal *goal) { QSqlDatabase db = database(); // test whether ID is present QSqlQuery goalExistsQuery(db); goalExistsQuery.prepare(QStringLiteral("SELECT COUNT(*) FROM goals WHERE category = :category AND identifier = :identifier")); goalExistsQuery.bindValue(QStringLiteral(":identifier"), goal->identifier()); goalExistsQuery.bindValue(QStringLiteral(":category"), static_cast(goal->category())); goalExistsQuery.exec(); if (db.lastError().isValid()) { qCritical() << "ExistsQuery: " << db.lastError().text(); raiseError(db.lastError()); return false; } // go to first result row that contains the count goalExistsQuery.next(); if (goalExistsQuery.value(0).toInt() < 1) { // in case learner ID is not found in database QSqlQuery insertGoalQuery(db); insertGoalQuery.prepare(QStringLiteral("INSERT INTO goals (category, identifier, name) VALUES (?, ?, ?)")); insertGoalQuery.bindValue(0, static_cast(goal->category())); insertGoalQuery.bindValue(1, goal->identifier()); insertGoalQuery.bindValue(2, goal->name()); insertGoalQuery.exec(); if (insertGoalQuery.lastError().isValid()) { raiseError(insertGoalQuery.lastError()); db.rollback(); return false; } return true; } else { // update name otherwise QSqlQuery updateGoalQuery(db); updateGoalQuery.prepare(QStringLiteral("UPDATE goals SET name = :name WHERE category = :category AND identifier = :identifier")); updateGoalQuery.bindValue(QStringLiteral(":category"), static_cast(goal->category())); updateGoalQuery.bindValue(QStringLiteral(":identifier"), goal->identifier()); updateGoalQuery.bindValue(QStringLiteral(":name"), goal->name()); updateGoalQuery.exec(); if (updateGoalQuery.lastError().isValid()) { qCritical() << updateGoalQuery.lastError().text(); raiseError(updateGoalQuery.lastError()); db.rollback(); return false; } return true; } } QList< LearningGoal* > Storage::loadGoals() { QSqlDatabase db = database(); QSqlQuery goalQuery(db); goalQuery.prepare(QStringLiteral("SELECT category, identifier, name FROM goals")); goalQuery.exec(); if (goalQuery.lastError().isValid()) { qCritical() << goalQuery.lastError().text(); raiseError(goalQuery.lastError()); return QList(); } QList goals; while (goalQuery.next()) { LearningGoal::Category category = static_cast(goalQuery.value(0).toInt()); QString identifier = goalQuery.value(1).toString(); QString name = goalQuery.value(2).toString(); LearningGoal* goal = new LearningGoal(category, identifier); goal->setName(name); goals.append(goal); } return goals; } bool Storage::storeProgressLog(Learner *learner, LearningGoal *goal, const QString &container, const QString &item, int payload, const QDateTime &time) { QSqlDatabase db = database(); QSqlQuery insertQuery(db); insertQuery.prepare("INSERT INTO learner_progress_log " "(goal_category, goal_identifier, profile_id, item_container, item, payload, date) " "VALUES (:gcategory, :gidentifier, :pid, :container, :item, :payload, :date)"); insertQuery.bindValue(QStringLiteral(":gcategory"), static_cast(goal->category())); insertQuery.bindValue(QStringLiteral(":gidentifier"), goal->identifier()); insertQuery.bindValue(QStringLiteral(":pid"), learner->identifier()); insertQuery.bindValue(QStringLiteral(":container"), container); insertQuery.bindValue(QStringLiteral(":item"), item); insertQuery.bindValue(QStringLiteral(":payload"), payload); insertQuery.bindValue(QStringLiteral(":date"), time.toString(Qt::ISODate)); insertQuery.exec(); if (insertQuery.lastError().isValid()) { raiseError(insertQuery.lastError()); qCCritical(LIBLEARNER_LOG) << "DB Error:" << m_errorMessage; db.rollback(); return false; } return true; } QList> Storage::readProgressLog(Learner *learner, LearningGoal *goal, const QString &container, const QString &item) { QSqlDatabase db = database(); QSqlQuery logQuery(db); logQuery.prepare("SELECT date, payload FROM learner_progress_log " "WHERE goal_category = :goalcategory " "AND goal_identifier = :goalid " "AND profile_id = :profileid " "AND item_container = :container " "AND item = :item"); logQuery.bindValue(QStringLiteral(":goalcategory"), static_cast(goal->category())); logQuery.bindValue(QStringLiteral(":goalid"), goal->identifier()); logQuery.bindValue(QStringLiteral(":profileid"), learner->identifier()); logQuery.bindValue(QStringLiteral(":container"), container); logQuery.bindValue(QStringLiteral(":item"), item); logQuery.exec(); if (logQuery.lastError().isValid()) { qCritical() << logQuery.lastError().text(); raiseError(logQuery.lastError()); return QList>(); } QList> log; while (logQuery.next()) { const QDateTime date{logQuery.value(0).toDateTime()}; int payload{logQuery.value(1).toInt()}; log.append(qMakePair(date, payload)); } return log; } bool Storage::storeProgressValue(Learner *learner, LearningGoal *goal, const QString &container, const QString &item, int payload) { QSqlDatabase db = database(); QSqlQuery query(db); // test if already payload stored query.prepare("SELECT payload FROM learner_progress_value " "WHERE goal_category = :gcategory " "AND goal_identifier = :gidentifier " "AND profile_id = :pid " "AND item_container = :container " "AND item = :item"); query.bindValue(QStringLiteral(":gcategory"), static_cast(goal->category())); query.bindValue(QStringLiteral(":gidentifier"), goal->identifier()); query.bindValue(QStringLiteral(":pid"), learner->identifier()); query.bindValue(QStringLiteral(":container"), container); query.bindValue(QStringLiteral(":item"), item); query.exec(); if (query.lastError().isValid()) { qCritical() << query.lastError().text(); raiseError(query.lastError()); return false; } // if query contains values, perform update query if (query.next()) { query.finish(); // release resources from previous query query.prepare("UPDATE learner_progress_value " "SET payload = :payload " "WHERE goal_category = :gcategory " "AND goal_identifier = :gidentifier " "AND profile_id = :pid " "AND item_container = :container " "AND item = :item"); query.bindValue(QStringLiteral(":payload"), static_cast(payload)); query.bindValue(QStringLiteral(":gcategory"), static_cast(goal->category())); query.bindValue(QStringLiteral(":gidentifier"), goal->identifier()); query.bindValue(QStringLiteral(":pid"), learner->identifier()); query.bindValue(QStringLiteral(":container"), container); query.bindValue(QStringLiteral(":item"), item); query.exec(); if (query.lastError().isValid()) { qCritical() << query.lastError().text(); raiseError(query.lastError()); db.rollback(); return false; } return true; } // else insert new row else { query.finish(); // release resources from previous query query.prepare("INSERT INTO learner_progress_value " "(goal_category, goal_identifier, profile_id, item_container, item, payload) " "VALUES (:gcategory, :gidentifier, :pid, :container, :item, :payload)"); query.bindValue(QStringLiteral(":gcategory"), static_cast(goal->category())); query.bindValue(QStringLiteral(":gidentifier"), goal->identifier()); query.bindValue(QStringLiteral(":pid"), learner->identifier()); query.bindValue(QStringLiteral(":container"), container); query.bindValue(QStringLiteral(":item"), item); query.bindValue(QStringLiteral(":payload"), static_cast(payload)); query.exec(); if (query.lastError().isValid()) { qCritical() << query.lastError().text(); raiseError(query.lastError()); db.rollback(); return false; } return true; } Q_UNREACHABLE(); return false; } QHash Storage::readProgressValues(Learner *learner, LearningGoal *goal, const QString &container) { QSqlDatabase db = database(); QSqlQuery query(db); query.prepare("SELECT item, payload FROM learner_progress_value " "WHERE goal_category = :goalcategory " "AND goal_identifier = :goalid " "AND profile_id = :profileid " "AND item_container = :container"); query.bindValue(QStringLiteral(":goalcategory"), static_cast(goal->category())); query.bindValue(QStringLiteral(":goalid"), goal->identifier()); query.bindValue(QStringLiteral(":profileid"), learner->identifier()); query.bindValue(QStringLiteral(":container"), container); query.exec(); if (query.lastError().isValid()) { qCritical() << query.lastError().text(); raiseError(query.lastError()); return QHash(); } QHash values; while (query.next()) { const QString item{query.value(0).toString()}; const int payload{query.value(1).toInt()}; values.insert(item, payload); } return values; } int Storage::readProgressValue(Learner *learner, LearningGoal *goal, const QString &container, const QString &item) { QSqlDatabase db = database(); QSqlQuery query(db); query.prepare("SELECT payload FROM learner_progress_value " "WHERE goal_category = :goalcategory " "AND goal_identifier = :goalid " "AND profile_id = :profileid " "AND item_container = :container " "AND item = :item"); query.bindValue(QStringLiteral(":goalcategory"), static_cast(goal->category())); query.bindValue(QStringLiteral(":goalid"), goal->identifier()); query.bindValue(QStringLiteral(":profileid"), learner->identifier()); query.bindValue(QStringLiteral(":container"), container); query.bindValue(QStringLiteral(":item"), item); query.exec(); if (query.lastError().isValid()) { qCritical() << query.lastError().text(); raiseError(query.lastError()); return -1; } if (query.next()) { return query.value(0).toInt(); } return -1; } QSqlDatabase Storage::database() { if (QSqlDatabase::contains(QSqlDatabase::defaultConnection)) { return QSqlDatabase::database(QSqlDatabase::defaultConnection); } // create data directory if it does not exist QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); if (!dir.exists()) { dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); } qCDebug(LIBLEARNER_LOG) << "Database path: " << m_databasePath; QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); db.setDatabaseName(m_databasePath); if (!db.open()) { qCritical() << "Could not open database: " << db.lastError().text(); raiseError(db.lastError()); return db; } if (!updateSchema()) { qCritical() << "Database scheme not correct."; return db; } // return correctly set up database return db; } bool Storage::updateSchema() { QSqlDatabase db = database(); // check database version format db.exec("CREATE TABLE IF NOT EXISTS metadata (" "key TEXT PRIMARY KEY, " "value TEXT" ")"); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } QSqlQuery versionQuery = db.exec(QStringLiteral("SELECT value FROM metadata WHERE key = 'version'")); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } if (versionQuery.next()) { QString version = versionQuery.value(0).toString(); if (version != QLatin1String("1")) { m_errorMessage = i18n("Invalid database version '%1'.", version); emit errorMessageChanged(); return false; } } else { if (!db.transaction()) { qCWarning(LIBLEARNER_LOG) << db.lastError().text(); raiseError(db.lastError()); return false; } db.exec(QStringLiteral("INSERT INTO metadata (key, value) VALUES ('version', '1')")); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } if (!db.commit()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } } // table for learner profiles db.exec("CREATE TABLE IF NOT EXISTS profiles (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "name TEXT" ")"); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } // table for registered learning goals db.exec("CREATE TABLE IF NOT EXISTS goals (" "category INTEGER, " // LearningGoal::Category "identifier TEXT, " // identifier, unique per Category "name TEXT, " // name "PRIMARY KEY ( category, identifier )" ")"); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } // table for learner - learningGoal relations db.exec("CREATE TABLE IF NOT EXISTS learner_goals (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "goal_category INTEGER, " // LearningGoal::Category "goal_identifier TEXT, " // LearningGoal::Identifier "profile_id INTEGER " // Learner::Identifier ")"); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } // table for full progress data log db.exec("CREATE TABLE IF NOT EXISTS learner_progress_log (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "goal_category INTEGER, " // LearningGoal::Category "goal_identifier TEXT, " // LearningGoal::Identifier "profile_id INTEGER, " // Learner::Identifier "item_container TEXT, " "item TEXT, " "payload INTEGER, " "date TEXT" ")"); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } // table for progress data quick access db.exec("CREATE TABLE IF NOT EXISTS learner_progress_value (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "goal_category INTEGER, " // LearningGoal::Category "goal_identifier TEXT, " // LearningGoal::Identifier "profile_id INTEGER, " // Learner::Identifier "item_container TEXT, " "item TEXT, " "payload INTEGER" ")"); if (db.lastError().isValid()) { qCritical() << db.lastError().text(); raiseError(db.lastError()); return false; } return true; } diff --git a/liblearnerprofile/src/storage.h b/liblearnerprofile/src/storage.h index 80cf255..67af00a 100644 --- a/liblearnerprofile/src/storage.h +++ b/liblearnerprofile/src/storage.h @@ -1,103 +1,103 @@ /* * Copyright 2013-2016 Andreas Cord-Landwehr * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #ifndef STORAGE_H #define STORAGE_H #include class QSqlError; class QSqlDatabase; namespace LearnerProfile { class Learner; class LearningGoal; /** * \class Storage * Database storage for learner information database. */ class Storage : public QObject { Q_OBJECT Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) public: /** * Default constructor, which sets a default database path at * DataLocation + learnerdata.db */ explicit Storage(QObject* parent = nullptr); /** * \note this constructor is tailored for unit tests */ - explicit Storage(const QString databasePath, QObject* parent = nullptr); + explicit Storage(const QString &databasePath, QObject* parent = nullptr); QString errorMessage() const; /** * Store profile in database. This can either be a new or an existing profile. * If it is an existing profile, the corresponding values are updated. */ bool storeProfile(Learner *learner); bool removeProfile(Learner *learner); bool removeRelation(Learner *learner, LearningGoal *goal); QList loadProfiles(QList< LearnerProfile::LearningGoal* > goals); bool storeGoal(LearningGoal *goal); QList loadGoals(); bool storeProgressLog(Learner *learner, LearningGoal *goal, const QString &container, const QString &item, int payload, const QDateTime &time); /** * Load list of progress values for specified item * \return list of date/payload values for this item */ QList> readProgressLog(Learner *learner, LearningGoal *goal, const QString &container, const QString &item); bool storeProgressValue(Learner *learner, LearningGoal *goal, const QString &container, const QString &item, int payload); /** * Load list of progress values for specified container * \return list of item/payload values for all items in container */ QHash readProgressValues(Learner *learner, LearningGoal *goal, const QString &container); /** * Load payload value of specified item. If no value is found, \return -1 */ int readProgressValue(Learner *learner, LearningGoal *goal, const QString &container, const QString &item); Q_SIGNALS: void errorMessageChanged(); protected: QSqlDatabase database(); void raiseError(const QSqlError &error); private: bool updateSchema(); const QString m_databasePath; QString m_errorMessage; }; } #endif // STORAGE_H diff --git a/src/core/resources/courseresource.cpp b/src/core/resources/courseresource.cpp index ad26f4e..5e7e289 100644 --- a/src/core/resources/courseresource.cpp +++ b/src/core/resources/courseresource.cpp @@ -1,488 +1,488 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "courseresource.h" #include "core/resourcemanager.h" #include "core/language.h" #include "core/course.h" #include "core/unit.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/resources/languageresource.h" #include #include #include #include #include #include #include #include #include "artikulate_debug.h" class CourseResourcePrivate { public: CourseResourcePrivate(ResourceManager *resourceManager) : m_resourceManager(resourceManager) , m_type(ResourceInterface::CourseResourceType) , m_courseResource(nullptr) { } ~CourseResourcePrivate() { } ResourceManager *m_resourceManager; QUrl m_path; ResourceInterface::Type m_type; QString m_identifier; QString m_title; QString m_language; QString m_i18nTitle; Course *m_courseResource; }; CourseResource::CourseResource(ResourceManager *resourceManager, const QUrl &path) : ResourceInterface(resourceManager) , d(new CourseResourcePrivate(resourceManager)) { d->m_path = path; // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { d->m_identifier = xml.readElementText(); continue; } if (xml.name() == "title") { d->m_title = xml.readElementText(); d->m_i18nTitle = d->m_title; continue; } //TODO i18nTitle must be implemented, currently missing and hence not parsed if (xml.name() == "language") { d->m_language = xml.readElementText(); continue; } // quit reading when basic elements are read if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() && !d->m_language.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } xml.clear(); file.close(); } CourseResource::~CourseResource() { } QString CourseResource::identifier() { if (d->m_courseResource) { return d->m_courseResource->id(); } return d->m_identifier; } QString CourseResource::title() { if (d->m_courseResource) { return d->m_courseResource->title(); } return d->m_title; } QString CourseResource::i18nTitle() { if (d->m_courseResource) { return d->m_courseResource->title(); //TODO } return d->m_i18nTitle; } QString CourseResource::language() const { if (d->m_courseResource) { return d->m_courseResource->language()->id(); } return d->m_language; } ResourceInterface::Type CourseResource::type() const { return d->m_type; } void CourseResource::sync() { Q_ASSERT(path().isValid()); Q_ASSERT(path().isLocalFile()); Q_ASSERT(!path().isEmpty()); // if resource was never loaded, it cannot be changed if (d->m_courseResource == nullptr) { qCDebug(ARTIKULATE_LOG) << "Aborting sync, course was not parsed."; return; } //TODO // // not writing back if not modified // if (!d->m_courseResource->modified()) { // qCDebug(ARTIKULATE_LOG) << "Aborting sync, course was not modified."; // return; // } // write back to file // create directories if necessary QFileInfo info(path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG) << "create xml output file directory, not existing"; QDir dir; dir.mkpath(path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to KSaveFile QFile file(path().toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG) << "Unable to open file " << file.fileName() << " in write mode, aborting."; return; } file.write(serializedDocument().toByteArray()); return; } void CourseResource::exportGhns(const QString &path) { // ensure that course is loaded before exporting it Course *course = CourseResource::course(); // filename const QString fileName = identifier() + ".tar.bz2"; KTar tar = KTar(path + '/' + fileName, QStringLiteral("application/x-bzip")); if (!tar.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG) << "Unable to open tar file" << path + '/' + fileName << "in write mode, aborting."; return; } foreach (auto *unit, course->unitList()) { foreach (auto *phrase, unit->phraseList()) { if (QFile::exists(phrase->soundFileUrl())) { tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg"); } } } tar.writeFile(identifier() + ".xml", serializedDocument(true).toByteArray()); tar.close(); } void CourseResource::close() { d->m_courseResource->deleteLater(); d->m_courseResource = nullptr; } bool CourseResource::isOpen() const { return (d->m_courseResource != nullptr); } QUrl CourseResource::path() const { if (d->m_courseResource) { return d->m_courseResource->file(); } return d->m_path; } QObject * CourseResource::resource() { if (d->m_courseResource != nullptr) { return d->m_courseResource; } // if file does not exist, create new course QFileInfo info(d->m_path.toLocalFile()); if (!info.exists()) { d->m_courseResource = new Course(this); d->m_courseResource->setId(d->m_identifier); d->m_courseResource->setTitle(d->m_title); return d->m_courseResource; } // load existing file QXmlSchema schema = loadXmlSchema(QStringLiteral("course")); if (!schema.isValid()) { return nullptr; } QDomDocument document = loadDomDocument(path(), schema); if (document.isNull()) { qCWarning(ARTIKULATE_LOG) << "Could not parse document " << path().toLocalFile() << ", aborting."; return nullptr; } // create course QDomElement root(document.documentElement()); d->m_courseResource = new Course(this); d->m_courseResource->setFile(d->m_path); d->m_courseResource->setId(root.firstChildElement(QStringLiteral("id")).text()); d->m_courseResource->setTitle(root.firstChildElement(QStringLiteral("title")).text()); d->m_courseResource->setDescription(root.firstChildElement(QStringLiteral("description")).text()); if (!root.firstChildElement(QStringLiteral("foreignId")).isNull()) { d->m_courseResource->setForeignId(root.firstChildElement(QStringLiteral("foreignId")).text()); } // set language //TODO not efficient to load completely every language for this comparison QString language = root.firstChildElement(QStringLiteral("language")).text(); foreach(LanguageResource * resource, d->m_resourceManager->languageResources()) { if (resource->language()->id() == language) { d->m_courseResource->setLanguage(resource->language()); break; } } if (d->m_courseResource->language() == nullptr) { qCWarning(ARTIKULATE_LOG) << "Language ID" << language << "unknown, could not register any language, aborting"; return nullptr; } // create units for (QDomElement unitNode = root.firstChildElement(QStringLiteral("units")).firstChildElement(); !unitNode.isNull(); unitNode = unitNode.nextSiblingElement()) { Unit *unit = new Unit(d->m_courseResource); unit->setId(unitNode.firstChildElement(QStringLiteral("id")).text()); unit->setCourse(d->m_courseResource); unit->setTitle(unitNode.firstChildElement(QStringLiteral("title")).text()); if (!unitNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { unit->setForeignId(unitNode.firstChildElement(QStringLiteral("foreignId")).text()); } d->m_courseResource->addUnit(unit); // create phrases for (QDomElement phraseNode = unitNode.firstChildElement(QStringLiteral("phrases")).firstChildElement(); !phraseNode.isNull(); phraseNode = phraseNode.nextSiblingElement()) { unit->addPhrase(parsePhrase(phraseNode, unit)); // add to unit at last step to produce only one signal //FIXME phrase does not cause unit signals that phonemes list is changed } } d->m_courseResource->setModified(false); return d->m_courseResource; } Course * CourseResource::course() { return qobject_cast(resource()); } -Phrase* CourseResource::parsePhrase(QDomElement phraseNode, Unit* parentUnit) const +Phrase* CourseResource::parsePhrase(const QDomElement &phraseNode, Unit* parentUnit) const { Phrase *phrase = new Phrase(parentUnit); phrase->setId(phraseNode.firstChildElement(QStringLiteral("id")).text()); phrase->setText(phraseNode.firstChildElement(QStringLiteral("text")).text()); phrase->seti18nText(phraseNode.firstChildElement(QStringLiteral("i18nText")).text()); phrase->setUnit(parentUnit); if (!phraseNode.firstChildElement(QStringLiteral("soundFile")).text().isEmpty()) { phrase->setSound(QUrl::fromLocalFile( path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + '/' + phraseNode.firstChildElement(QStringLiteral("soundFile")).text()) ); } phrase->setType(phraseNode.firstChildElement(QStringLiteral("type")).text()); phrase->setEditState(phraseNode.firstChildElement(QStringLiteral("editState")).text()); if (!phraseNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { phrase->setForeignId(phraseNode.firstChildElement(QStringLiteral("foreignId")).text()); } // add phonemes QList phonemes = d->m_courseResource->language()->phonemes(); for (QDomElement phonemeID = phraseNode.firstChildElement(QStringLiteral("phonemes")).firstChildElement(); !phonemeID.isNull(); phonemeID = phonemeID.nextSiblingElement()) { QString id = phonemeID.text(); if (id.isEmpty()) { qCritical() << "Phoneme ID string is empty for phrase "<< phrase->id() <<", aborting."; continue; } foreach (Phoneme *phoneme, phonemes) { if (phoneme->id() == id) { phrase->addPhoneme(phoneme); break; } } } if (!phraseNode.firstChildElement(QStringLiteral("excluded")).isNull() && phraseNode.firstChildElement(QStringLiteral("excluded")).text() == QLatin1String("true")) { phrase->setExcluded(true); } return phrase; } QDomDocument CourseResource::serializedDocument(bool trainingExport) const { QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("course")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); QDomElement languageElement = document.createElement(QStringLiteral("language")); idElement.appendChild(document.createTextNode(d->m_courseResource->id())); titleElement.appendChild(document.createTextNode(d->m_courseResource->title())); descriptionElement.appendChild(document.createTextNode(d->m_courseResource->description())); languageElement.appendChild(document.createTextNode(d->m_courseResource->language()->id())); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units foreach (Unit *unit, d->m_courseResource->unitList()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases foreach (Phrase *phrase, unit->phraseList()) { if (trainingExport && phrase->soundFileUrl().isEmpty()) { continue; } unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); } if (trainingExport && unitPhraseListElement.childNodes().count() == 0) { continue; } // construct the unit element unitElement.appendChild(unitIdElement); if (!unit->foreignId().isEmpty()) { QDomElement unitForeignIdElement = document.createElement(QStringLiteral("foreignId")); unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId())); unitElement.appendChild(unitForeignIdElement); } unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); if (!d->m_courseResource->foreignId().isEmpty()) { QDomElement courseForeignIdElement = document.createElement(QStringLiteral("foreignId")); courseForeignIdElement.appendChild(document.createTextNode(d->m_courseResource->foreignId())); root.appendChild(courseForeignIdElement); } root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(languageElement); root.appendChild(unitListElement); return document; } QDomElement CourseResource::serializedPhrase(Phrase *phrase, QDomDocument &document) const { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phrasei18nTextElement = document.createElement(QStringLiteral("i18nText")); QDomElement phraseSoundFileElement = document.createElement(QStringLiteral("soundFile")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); QDomElement phraseEditStateElement = document.createElement(QStringLiteral("editState")); QDomElement phrasePhonemeListElement = document.createElement(QStringLiteral("phonemes")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText())); phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString())); // add phonemes foreach (Phoneme *phoneme, phrase->phonemes()) { QDomElement phonemeElement = document.createElement(QStringLiteral("phonemeID")); phonemeElement.appendChild(document.createTextNode(phoneme->id())); phrasePhonemeListElement.appendChild(phonemeElement); } phraseElement.appendChild(phraseIdElement); if (!phrase->foreignId().isEmpty()) { QDomElement phraseForeignIdElement = document.createElement(QStringLiteral("foreignId")); phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId())); phraseElement.appendChild(phraseForeignIdElement); } phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phrasei18nTextElement); phraseElement.appendChild(phraseSoundFileElement); phraseElement.appendChild(phraseTypeElement); phraseElement.appendChild(phraseEditStateElement); phraseElement.appendChild(phrasePhonemeListElement); if (phrase->isExcluded()) { QDomElement phraseIsExcludedElement = document.createElement(QStringLiteral("excluded")); phraseIsExcludedElement.appendChild(document.createTextNode(QStringLiteral("true"))); phraseElement.appendChild(phraseIsExcludedElement); } return phraseElement; } diff --git a/src/core/resources/courseresource.h b/src/core/resources/courseresource.h index f124839..ee134ac 100644 --- a/src/core/resources/courseresource.h +++ b/src/core/resources/courseresource.h @@ -1,120 +1,120 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef COURSERESOURCE_H #define COURSERESOURCE_H #include "artikulatecore_export.h" #include "resourceinterface.h" #include class QDomElement; class QString; class CourseResourcePrivate; class Course; class Unit; class Phrase; class ARTIKULATECORE_EXPORT CourseResource : public ResourceInterface { Q_OBJECT void course(QString text); public: /** * Create course resource from file. */ explicit CourseResource(ResourceManager *resourceManager, const QUrl &path); virtual ~CourseResource(); /** * \return unique identifier */ QString identifier() Q_DECL_OVERRIDE; /** * \return human readable localized title */ QString title() Q_DECL_OVERRIDE; /** * \return human readable title in English */ QString i18nTitle() Q_DECL_OVERRIDE; /** * \return language identifier of this course */ QString language() const; /** * \return type of resource */ Type type() const Q_DECL_OVERRIDE; /** * \return true if resource is loaded, otherwise false */ bool isOpen() const Q_DECL_OVERRIDE; void sync() Q_DECL_OVERRIDE; /** * export course as .tar.bz2 file in the specified folder. */ void exportGhns(const QString &path); /** * close resource without writing changes back to file */ void close() Q_DECL_OVERRIDE; /** * \return path to resource file */ QUrl path() const Q_DECL_OVERRIDE; /** * \return reference to the loaded resource * if resource is not open yet, it will be loaded */ QObject * resource() Q_DECL_OVERRIDE; /** * \return reference to the loaded course resource * Same behavior as \see resource() but casted to Course */ Course * course(); private: - Phrase * parsePhrase(QDomElement phraseNode, Unit *parentUnit) const; + Phrase * parsePhrase(const QDomElement &phraseNode, Unit *parentUnit) const; /** * \return serialized course as DOM document * \param trainingExport if true phrases without recording and empty units are excluded */ QDomDocument serializedDocument(bool trainingExport=false) const; QDomElement serializedPhrase(Phrase * phrase, QDomDocument &document) const; const QScopedPointer d; }; #endif