diff --git a/CHANGELOG.md b/CHANGELOG.md index e76a4978..c8c6f393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ spanish only. Sorry for the mess. Version counting is based on semantic versioning (Major.Feature.Patch) ## WIP +### YACReaderLibrary +* New search engine. ## 9.7.1 ### YACReader diff --git a/YACReaderLibrary/YACReaderLibrary.pro b/YACReaderLibrary/YACReaderLibrary.pro index 2e6d6888..4af13332 100644 --- a/YACReaderLibrary/YACReaderLibrary.pro +++ b/YACReaderLibrary/YACReaderLibrary.pro @@ -14,6 +14,8 @@ INCLUDEPATH += . \ DEFINES += SERVER_RELEASE NOMINMAX YACREADER_LIBRARY +CONFIG += c++11 + # load default build flags include (../config.pri) include (../dependencies/pdf_backend.pri) @@ -67,10 +69,6 @@ macx { QT += macextras gui-private } -unix:!macx { - CONFIG += c++11 -} - #CONFIG += release CONFIG -= flat QT += sql network widgets script @@ -80,7 +78,11 @@ QT += sql network widgets script # Input HEADERS += comic_flow.h \ + ../common/concurrent_queue.h \ create_library_dialog.h \ + db/comic_query_result_procesor.h \ + db/folder_query_result_processor.h \ + db/query_lexer.h \ library_creator.h \ library_window.h \ add_library_dialog.h \ @@ -147,6 +149,7 @@ HEADERS += comic_flow.h \ yacreader_comics_selection_helper.h \ yacreader_comic_info_helper.h \ db/reading_list.h \ + db/query_parser.h \ current_comic_view_helper.h !CONFIG(no_opengl) { @@ -155,6 +158,9 @@ HEADERS += comic_flow.h \ SOURCES += comic_flow.cpp \ create_library_dialog.cpp \ + db/comic_query_result_procesor.cpp \ + db/folder_query_result_processor.cpp \ + db/query_lexer.cpp \ library_creator.cpp \ library_window.cpp \ main.cpp \ @@ -219,7 +225,8 @@ SOURCES += comic_flow.cpp \ yacreader_comics_selection_helper.cpp \ yacreader_comic_info_helper.cpp\ db/reading_list.cpp \ - current_comic_view_helper.cpp + current_comic_view_helper.cpp \ + db/query_parser.cpp !CONFIG(no_opengl) { SOURCES += ../common/gl/yacreader_flow_gl.cpp diff --git a/YACReaderLibrary/db/comic_model.cpp b/YACReaderLibrary/db/comic_model.cpp index 7fee8000..ea03a617 100644 --- a/YACReaderLibrary/db/comic_model.cpp +++ b/YACReaderLibrary/db/comic_model.cpp @@ -9,6 +9,7 @@ #include "qnaturalsorting.h" #include "comic_db.h" #include "db_helper.h" +#include "query_parser.h" //ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read #include "QsLog.h" @@ -595,59 +596,25 @@ void ComicModel::setupReadingModelData(const QString &databasePath) endResetModel(); } -void ComicModel::setupModelData(const SearchModifiers modifier, const QString &filter, const QString &databasePath) +void ComicModel::setModelData(QList *data, const QString &databasePath) { - beginResetModel(); - qDeleteAll(_data); - _data.clear(); _databasePath = databasePath; - QString connectionName = ""; - { - QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); - QSqlQuery selectQuery(db); + beginResetModel(); - switch (modifier) { - case YACReader::NoModifiers: - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened " - "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "WHERE UPPER(ci.title) LIKE UPPER(:filter) OR UPPER(c.fileName) LIKE UPPER(:filter) LIMIT :limit"); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - selectQuery.bindValue(":limit", 500); //TODO, load this value from settings - break; + qDeleteAll(_data); - case YACReader::OnlyRead: - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened " - "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "WHERE (UPPER(ci.title) LIKE UPPER(:filter) OR UPPER(c.fileName) LIKE UPPER(:filter)) AND ci.read = 1 LIMIT :limit"); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - selectQuery.bindValue(":limit", 500); //TODO, load this value from settings - break; + _data.clear(); - case YACReader::OnlyUnread: - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened " - "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " - "WHERE (UPPER(ci.title) LIKE UPPER(:filter) OR UPPER(c.fileName) LIKE UPPER(:filter)) AND ci.read = 0 LIMIT :limit"); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - selectQuery.bindValue(":limit", 500); //TODO, load this value from settings - break; + QLOG_ERROR() << "-d2>" << data->size(); - default: - QLOG_ERROR() << "not implemented"; - break; - } + _data.append(*data); - selectQuery.exec(); - - QLOG_DEBUG() << selectQuery.lastError() << "--"; - - setupModelData(selectQuery); - connectionName = db.connectionName(); - } - QSqlDatabase::removeDatabase(connectionName); endResetModel(); emit searchNumResults(_data.length()); + + delete data; } QString ComicModel::getComicPath(QModelIndex mi) diff --git a/YACReaderLibrary/db/comic_model.h b/YACReaderLibrary/db/comic_model.h index 263eaa43..40f14517 100644 --- a/YACReaderLibrary/db/comic_model.h +++ b/YACReaderLibrary/db/comic_model.h @@ -16,7 +16,6 @@ class ComicItem; using namespace YACReader; -//! [0] class ComicModel : public QAbstractItemModel { Q_OBJECT @@ -89,8 +88,6 @@ public: void setupReadingListModelData(unsigned long long int parentReadingList, const QString &databasePath); void setupFavoritesModelData(const QString &databasePath); void setupReadingModelData(const QString &databasePath); - //configures the model for showing the comics matching the filter criteria. - void setupModelData(const SearchModifiers modifier, const QString &filter, const QString &databasePath); //Métodos de conveniencia QStringList getPaths(const QString &_source); @@ -142,6 +139,8 @@ public slots: void addComicsToLabel(const QList &comicIds, qulonglong labelId); void addComicsToReadingList(const QList &comicIds, qulonglong readingListId); + void setModelData(QList *data, const QString &databasePath); + protected: private: void setupModelData(QSqlQuery &sqlquery); @@ -164,6 +163,5 @@ signals: void resortedIndexes(QList); void newSelectedIndex(const QModelIndex &); }; -//! [0] #endif diff --git a/YACReaderLibrary/db/comic_query_result_procesor.cpp b/YACReaderLibrary/db/comic_query_result_procesor.cpp new file mode 100644 index 00000000..270075aa --- /dev/null +++ b/YACReaderLibrary/db/comic_query_result_procesor.cpp @@ -0,0 +1,112 @@ +#include "comic_query_result_procesor.h" + +#include "comic_item.h" +#include "comic_model.h" +#include "data_base_management.h" +#include "qnaturalsorting.h" +#include "db_helper.h" +#include "query_parser.h" + +#include "QsLog.h" + +QString getLastExecutedQuery(const QSqlQuery &query) +{ + QString str = query.lastQuery(); + QMapIterator it(query.boundValues()); + while (it.hasNext()) { + it.next(); + str.replace(it.key(), it.value().toString()); + } + return str; +} + +YACReader::ComicQueryResultProcesor::ComicQueryResultProcesor() + : querySearchQueue(1) +{ +} + +void YACReader::ComicQueryResultProcesor::createModelData(const YACReader::SearchModifiers modifier, const QString &filter, const QString &databasePath) +{ + querySearchQueue.cancellPending(); + + querySearchQueue.enqueue([=] { + QString connectionName = ""; + { + QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); + QSqlQuery selectQuery(db); + + std::string queryString("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened " + "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) LEFT JOIN folder f ON (f.id == c.parentId) WHERE "); + + try { + QueryParser parser; + auto result = parser.parse(filter.toStdString()); + result.buildSqlString(queryString); + + switch (modifier) { + case YACReader::NoModifiers: + queryString += " LIMIT :limit"; + break; + + case YACReader::OnlyRead: + queryString += " AND ci.read = 1 LIMIT :limit"; + break; + + case YACReader::OnlyUnread: + queryString += " AND ci.read = 0 LIMIT :limit"; + break; + + default: + queryString += " LIMIT :limit"; + QLOG_ERROR() << "not implemented"; + break; + } + selectQuery.prepare(queryString.c_str()); + selectQuery.bindValue(":limit", 500); //TODO, load this value from settings + result.bindValues(selectQuery); + + selectQuery.exec(); + + auto data = modelData(selectQuery); + + emit newData(data, databasePath); + } catch (const std::exception &e) { + //Do nothing, uncomplete search string will end here and it is part of how the QueryParser works + //I don't like the idea of using exceptions for this though + } + + connectionName = db.connectionName(); + } + QSqlDatabase::removeDatabase(connectionName); + }); +} + +QList *YACReader::ComicQueryResultProcesor::modelData(QSqlQuery &sqlquery) +{ + auto list = new QList(); + + int numColumns = sqlquery.record().count(); + + while (sqlquery.next()) { + QList data; + + for (int i = 0; i < numColumns; i++) + data << sqlquery.value(i); + + list->append(new ComicItem(data)); + } + + std::sort(list->begin(), list->end(), [](const ComicItem *c1, const ComicItem *c2) { + if (c1->data(ComicModel::Number).isNull() && c2->data(ComicModel::Number).isNull()) { + return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); + } else { + if (c1->data(ComicModel::Number).isNull() == false && c2->data(ComicModel::Number).isNull() == false) { + return c1->data(ComicModel::Number).toInt() < c2->data(ComicModel::Number).toInt(); + } else { + return c2->data(ComicModel::Number).isNull(); + } + } + }); + + return list; +} diff --git a/YACReaderLibrary/db/comic_query_result_procesor.h b/YACReaderLibrary/db/comic_query_result_procesor.h new file mode 100644 index 00000000..a948611c --- /dev/null +++ b/YACReaderLibrary/db/comic_query_result_procesor.h @@ -0,0 +1,32 @@ +#ifndef COMIC_QUERY_RESULT_PROCESOR_H +#define COMIC_QUERY_RESULT_PROCESOR_H + +#include +#include + +#include "yacreader_global.h" +#include "concurrent_queue.h" + +class ComicItem; + +namespace YACReader { + +class ComicQueryResultProcesor : public QObject +{ + Q_OBJECT +public: + ComicQueryResultProcesor(); + +public slots: + void createModelData(const SearchModifiers modifier, const QString &filter, const QString &databasePath); +signals: + void newData(QList *, const QString &); + +private: + ConcurrentQueue querySearchQueue; + + QList *modelData(QSqlQuery &sqlquery); +}; +}; + +#endif // COMIC_QUERY_RESULT_PROCESOR_H diff --git a/YACReaderLibrary/db/folder_model.cpp b/YACReaderLibrary/db/folder_model.cpp index 640462d7..288a7dde 100644 --- a/YACReaderLibrary/db/folder_model.cpp +++ b/YACReaderLibrary/db/folder_model.cpp @@ -1,60 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** This file is part of the examples of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor -** the names of its contributors may be used to endorse or promote -** products derived from this software without specific prior written -** permission. -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** $QT_END_LICENSE$ -** -****************************************************************************/ - -/* - treemodel.cpp - - Provides a simple tree model to show how to create and use hierarchical - models. -*/ - -#include +#include "folder_model.h" #include "folder_item.h" -#include "folder_model.h" #include "data_base_management.h" #include "folder.h" #include "db_helper.h" #include "qnaturalsorting.h" #include "yacreader_global_gui.h" #include "QsLog.h" +#include "query_parser.h" + +#include #ifdef Q_OS_MAC #include @@ -103,7 +58,6 @@ FolderModel::FolderModel(QObject *parent) connect(this, SIGNAL(reset()), this, SIGNAL(modelReset())); } -//! [0] FolderModel::FolderModel(QSqlQuery &sqlquery, QObject *parent) : QAbstractItemModel(parent), rootItem(0) { @@ -116,17 +70,13 @@ FolderModel::FolderModel(QSqlQuery &sqlquery, QObject *parent) setupModelData(sqlquery, rootItem); //sqlquery.finish(); } -//! [0] -//! [1] FolderModel::~FolderModel() { if (rootItem != 0) delete rootItem; } -//! [1] -//! [2] int FolderModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) @@ -134,9 +84,7 @@ int FolderModel::columnCount(const QModelIndex &parent) const else return rootItem->columnCount(); } -//! [2] -//! [3] QVariant FolderModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) @@ -187,9 +135,7 @@ QVariant FolderModel::data(const QModelIndex &index, int role) const return item->data(index.column()); } -//! [3] -//! [4] Qt::ItemFlags FolderModel::flags(const QModelIndex &index) const { if (!index.isValid()) @@ -197,9 +143,7 @@ Qt::ItemFlags FolderModel::flags(const QModelIndex &index) const return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled; } -//! [4] -//! [5] QVariant FolderModel::headerData(int section, Qt::Orientation orientation, int role) const { @@ -208,9 +152,7 @@ QVariant FolderModel::headerData(int section, Qt::Orientation orientation, return QVariant(); } -//! [5] -//! [6] QModelIndex FolderModel::index(int row, int column, const QModelIndex &parent) const { @@ -230,9 +172,7 @@ QModelIndex FolderModel::index(int row, int column, const QModelIndex &parent) else return QModelIndex(); } -//! [6] -//! [7] QModelIndex FolderModel::parent(const QModelIndex &index) const { if (!index.isValid()) @@ -246,19 +186,7 @@ QModelIndex FolderModel::parent(const QModelIndex &index) const return createIndex(parentItem->row(), 0, parentItem); } -//! [7] -/* -QModelIndex FolderModel::indexFromItem(FolderItem * item,int column) -{ - //if(item->parent() != 0) - // return index(item->row(),column,parent(indexFromItem(item->parent(),column-1))); - //else - // return index(item->row(),0,QModelIndex()); - return createIndex(item->row(), column, item); -}*/ - -//! [8] int FolderModel::rowCount(const QModelIndex &parent) const { FolderItem *parentItem; @@ -272,7 +200,6 @@ int FolderModel::rowCount(const QModelIndex &parent) const return parentItem->childCount(); } -//! [8] void FolderModel::setupModelData(QString path) { @@ -385,27 +312,6 @@ QString FolderModel::getFolderPath(const QModelIndex &folder) return static_cast(folder.internalPointer())->data(FolderModel::Path).toString(); } -/* -void FolderModel::resetFilter() -{ - beginResetModel(); - filter = ""; - includeComics = false; - //TODO hay que liberar la memoria reservada para el filtrado - //items.clear(); - filteredItems.clear(); - FolderItem * root = rootItem; - rootItem = rootBeforeFilter; //TODO si no se aplica el filtro previamente, esto invalidar�a en modelo - if(root !=0) - delete root; - - rootBeforeFilter = 0; - filterEnabled = false; - endResetModel(); - - -}*/ - void FolderModel::updateFolderCompletedStatus(const QModelIndexList &list, bool status) { QString connectionName = ""; @@ -625,7 +531,7 @@ void FolderModel::updateFolderChildrenInfo(qulonglong folderId) //PROXY FolderModelProxy::FolderModelProxy(QObject *parent) - : QSortFilterProxyModel(parent), rootItem(0), includeComics(true), filter(""), filterEnabled(false) + : QSortFilterProxyModel(parent), rootItem(0), filterEnabled(false) { } @@ -648,85 +554,29 @@ bool FolderModelProxy::filterAcceptsRow(int source_row, const QModelIndex &sourc return filteredItems.contains(item->id); } -void FolderModelProxy::setFilter(const YACReader::SearchModifiers modifier, QString filter, bool includeComics) +void FolderModelProxy::setFilterData(QMap *filteredItems, FolderItem *root) { clear(); - this->filter = filter; - this->includeComics = includeComics; - this->modifier = modifier; filterEnabled = true; - setupFilteredModelData(); -} -void FolderModelProxy::setupFilteredModelData() -{ beginResetModel(); - //TODO hay que liberar memoria de anteriores filtrados - - //inicializar el nodo ra�z - if (rootItem != 0) delete rootItem; //TODO comprobar que se libera bien la memoria - rootItem = 0; + rootItem = root; - //inicializar el nodo ra�z - QList rootData; - rootData << "root"; - rootItem = new FolderItem(rootData); - rootItem->id = ROOT; - rootItem->parentItem = 0; - - auto model = static_cast(sourceModel()); - - QString connectionName = ""; - { - QSqlDatabase db = DataBaseManagement::loadDatabase(model->_databasePath); - - QSqlQuery selectQuery(db); //TODO check - if (!includeComics) { - selectQuery.prepare("select * from folder where id <> 1 and upper(name) like upper(:filter) order by parentId,name "); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - } else { - switch (modifier) { - case YACReader::NoModifiers: - selectQuery.prepare("SELECT DISTINCT f.id, f.parentId, f.name, f.path, f.finished, f.completed " - "FROM folder f LEFT JOIN comic c ON (f.id = c.parentId) " - "WHERE f.id <> 1 AND ((UPPER(c.fileName) like UPPER(:filter)) OR (UPPER(f.name) like UPPER(:filter2))) ORDER BY f.parentId,f.name"); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - selectQuery.bindValue(":filter2", "%%" + filter + "%%"); - break; - - case YACReader::OnlyRead: - selectQuery.prepare("SELECT DISTINCT f.id, f.parentId, f.name, f.path, f.finished, f.completed " - "FROM folder f LEFT JOIN (comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id)) ON (f.id = c.parentId) " - "WHERE f.id <> 1 AND ((UPPER(c.fileName) like UPPER(:filter)) OR (UPPER(f.name) like UPPER(:filter2))) AND ci.read = 1 ORDER BY f.parentId,f.name;"); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - selectQuery.bindValue(":filter2", "%%" + filter + "%%"); - break; - - case YACReader::OnlyUnread: - selectQuery.prepare("SELECT DISTINCT f.id, f.parentId, f.name, f.path, f.finished, f.completed " - "FROM folder f LEFT JOIN (comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id)) ON (f.id = c.parentId) " - "WHERE f.id <> 1 AND ((UPPER(c.fileName) like UPPER(:filter)) OR (UPPER(f.name) like UPPER(:filter2))) AND ci.read = 0 ORDER BY f.parentId,f.name;"); - selectQuery.bindValue(":filter", "%%" + filter + "%%"); - selectQuery.bindValue(":filter2", "%%" + filter + "%%"); - break; - - default: - QLOG_ERROR() << "not implemented"; - break; - } - } - selectQuery.exec(); - - setupFilteredModelData(selectQuery, rootItem); - connectionName = db.connectionName(); - } - QSqlDatabase::removeDatabase(connectionName); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + QMap::iterator i; + for (i = filteredItems->begin(); i != filteredItems->end(); ++i) + this->filteredItems.insert(i.key(), i.value()); +#else + this->filteredItems.insert(*filteredItems); +#endif endResetModel(); + + delete filteredItems; } void FolderModelProxy::clear() @@ -737,88 +587,3 @@ void FolderModelProxy::clear() QSortFilterProxyModel::clear(); } - -void FolderModelProxy::setupFilteredModelData(QSqlQuery &sqlquery, FolderItem *parent) -{ - auto model = static_cast(sourceModel()); - - //64 bits para la primary key, es decir la misma precisi�n que soporta sqlit 2^64 - filteredItems.clear(); - - //se a�ade el nodo 0 al modelo que representa el arbol de elementos que cumplen con el filtro - filteredItems.insert(parent->id, parent); - - QSqlRecord record = sqlquery.record(); - - int name = record.indexOf("name"); - int path = record.indexOf("path"); - int finished = record.indexOf("finished"); - int completed = record.indexOf("completed"); - int parentIdIndex = record.indexOf("parentId"); - - while (sqlquery.next()) { //se procesan todos los folders que cumplen con el filtro - //datos de la base de datos - QList data; - - data << sqlquery.value(name).toString(); - data << sqlquery.value(path).toString(); - data << sqlquery.value(finished).toBool(); - data << sqlquery.value(completed).toBool(); - - auto item = new FolderItem(data); - item->id = sqlquery.value(0).toULongLong(); - - //id del padre - quint64 parentId = sqlquery.value(parentIdIndex).toULongLong(); - - //se a�ade el item al map, de forma que se pueda encontrar como padre en siguientes iteraciones - if (!filteredItems.contains(item->id)) - filteredItems.insert(item->id, item); - - //es necesario conocer las coordenadas de origen para poder realizar scroll autom�tico en la vista - item->originalItem = model->items.value(item->id); - - //si el padre ya existe en el modelo, el item se a�ade como hijo - if (filteredItems.contains(parentId)) - filteredItems.value(parentId)->appendChild(item); - else //si el padre a�n no se ha a�adido, hay que a�adirlo a �l y todos los padres hasta el nodo ra�z - { - //comprobamos con esta variable si el �ltimo de los padres (antes del nodo ra�z) ya exist�a en el modelo - bool parentPreviousInserted = false; - - //mientras no se alcance el nodo ra�z se procesan todos los padres (de abajo a arriba) - while (parentId != ROOT) { - //el padre no estaba en el modelo filtrado, as� que se rescata del modelo original - FolderItem *parentItem = model->items.value(parentId); - //se debe crear un nuevo nodo (para no compartir los hijos con el nodo original) - FolderItem *newparentItem = new FolderItem(parentItem->getData()); //padre que se a�adir� a la estructura de directorios filtrados - newparentItem->id = parentId; - - newparentItem->originalItem = parentItem; - - //si el modelo contiene al padre, se a�ade el item actual como hijo - if (filteredItems.contains(parentId)) { - filteredItems.value(parentId)->appendChild(item); - parentPreviousInserted = true; - } - //sino se registra el nodo para poder encontrarlo con posterioridad y se a�ade el item actual como hijo - else { - newparentItem->appendChild(item); - filteredItems.insert(newparentItem->id, newparentItem); - parentPreviousInserted = false; - } - - //variables de control del bucle, se avanza hacia el nodo padre - item = newparentItem; - parentId = parentItem->parentItem->id; - } - - //si el nodo es hijo de 1 y no hab�a sido previamente insertado como hijo, se a�ade como tal - if (!parentPreviousInserted) { - filteredItems.value(ROOT)->appendChild(item); - } else { - delete item; - } - } - } -} diff --git a/YACReaderLibrary/db/folder_model.h b/YACReaderLibrary/db/folder_model.h index ecac40ee..9597d07c 100644 --- a/YACReaderLibrary/db/folder_model.h +++ b/YACReaderLibrary/db/folder_model.h @@ -1,43 +1,3 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** This file is part of the examples of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor -** the names of its contributors may be used to endorse or promote -** products derived from this software without specific prior written -** permission. -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** $QT_END_LICENSE$ -** -****************************************************************************/ - #ifndef TREEMODEL_H #define TREEMODEL_H @@ -49,6 +9,7 @@ #include #include "yacreader_global.h" +#include "folder_query_result_processor.h" class FolderItem; @@ -59,9 +20,7 @@ public: explicit FolderModelProxy(QObject *parent = 0); ~FolderModelProxy() override; - void setFilter(const YACReader::SearchModifiers modifier, QString filter, bool includeComics); - void setupFilteredModelData(QSqlQuery &sqlquery, FolderItem *parent); - void setupFilteredModelData(); + void setFilterData(QMap *filteredItems, FolderItem *root); void clear(); bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; @@ -70,8 +29,6 @@ protected: FolderItem *rootItem; QMap filteredItems; //relación entre folders - bool includeComics; - QString filter; bool filterEnabled; YACReader::SearchModifiers modifier; @@ -79,10 +36,10 @@ protected: class FolderModel : public QAbstractItemModel { - Q_OBJECT friend class FolderModelProxy; + friend class YACReader::FolderQueryResultProcessor; public: FolderModel(QObject *parent = 0); @@ -147,6 +104,5 @@ signals: void beforeReset(); void reset(); }; -//! [0] #endif diff --git a/YACReaderLibrary/db/folder_query_result_processor.cpp b/YACReaderLibrary/db/folder_query_result_processor.cpp new file mode 100644 index 00000000..e0b6050d --- /dev/null +++ b/YACReaderLibrary/db/folder_query_result_processor.cpp @@ -0,0 +1,176 @@ +#include "folder_query_result_processor.h" + +#include "folder_item.h" +#include "qnaturalsorting.h" +#include "yacreader_global_gui.h" +#include "query_parser.h" +#include "folder_model.h" +#include "data_base_management.h" + +#include "QsLog.h" + +#include +#include + +//Copy/pasted from "folder_model.cpp" +#define ROOT 1 + +YACReader::FolderQueryResultProcessor::FolderQueryResultProcessor(FolderModel *model) + : querySearchQueue(1), model(model) +{ +} + +void YACReader::FolderQueryResultProcessor::createModelData(const YACReader::SearchModifiers modifier, const QString &filter, bool includeComics) +{ + querySearchQueue.cancellPending(); + + QString connectionName = ""; + { + QSqlDatabase db = DataBaseManagement::loadDatabase(model->getDatabase()); + + QSqlQuery selectQuery(db); //TODO check + if (!includeComics) { + selectQuery.prepare("select * from folder where id <> 1 and upper(name) like upper(:filter) order by parentId,name "); + selectQuery.bindValue(":filter", "%%" + filter + "%%"); + } else { + std::string queryString("SELECT DISTINCT f.id, f.parentId, f.name, f.path, f.finished, f.completed " + "FROM folder f LEFT JOIN comic c ON (f.id = c.parentId) " + "INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) WHERE "); + + try { + QueryParser parser; + auto result = parser.parse(filter.toStdString()); + result.buildSqlString(queryString); + + switch (modifier) { + case YACReader::NoModifiers: + queryString += " AND f.id <> 1 ORDER BY f.parentId,f.name"; + break; + + case YACReader::OnlyRead: + queryString += " AND f.id <> 1 AND ci.read = 1 ORDER BY f.parentId,f.name"; + break; + + case YACReader::OnlyUnread: + queryString += " AND f.id <> 1 AND ci.read = 0 ORDER BY f.parentId,f.name"; + break; + + default: + queryString += " AND f.id <> 1 ORDER BY f.parentId,f.name"; + QLOG_ERROR() << "not implemented"; + break; + } + + selectQuery.prepare(queryString.c_str()); + result.bindValues(selectQuery); + + selectQuery.exec(); + QLOG_DEBUG() << selectQuery.lastError() << "--"; + + setupFilteredModelData(selectQuery); + } catch (const std::exception &e) { + //Do nothing, uncomplete search string will end here and it is part of how the QueryParser works + //I don't like the idea of using exceptions for this though + } + } + + connectionName = db.connectionName(); + } + + QSqlDatabase::removeDatabase(connectionName); +} + +void YACReader::FolderQueryResultProcessor::setupFilteredModelData(QSqlQuery &sqlquery) +{ + FolderItem *rootItem = 0; + + //inicializar el nodo ra�z + QList rootData; + rootData << "root"; + rootItem = new FolderItem(rootData); + rootItem->id = ROOT; + rootItem->parentItem = 0; + + FolderItem *parent = rootItem; + + QMap *filteredItems = new QMap(); + + //add tree root node + filteredItems->insert(parent->id, parent); + + QSqlRecord record = sqlquery.record(); + + int name = record.indexOf("name"); + int path = record.indexOf("path"); + int finished = record.indexOf("finished"); + int completed = record.indexOf("completed"); + int parentIdIndex = record.indexOf("parentId"); + + while (sqlquery.next()) { //se procesan todos los folders que cumplen con el filtro + //datos de la base de datos + QList data; + + data << sqlquery.value(name).toString(); + data << sqlquery.value(path).toString(); + data << sqlquery.value(finished).toBool(); + data << sqlquery.value(completed).toBool(); + + auto item = new FolderItem(data); + item->id = sqlquery.value(0).toULongLong(); + + //id del padre + quint64 parentId = sqlquery.value(parentIdIndex).toULongLong(); + + //se a�ade el item al map, de forma que se pueda encontrar como padre en siguientes iteraciones + if (!filteredItems->contains(item->id)) + filteredItems->insert(item->id, item); + + //es necesario conocer las coordenadas de origen para poder realizar scroll autom�tico en la vista + item->originalItem = model->items.value(item->id); + + //si el padre ya existe en el modelo, el item se a�ade como hijo + if (filteredItems->contains(parentId)) + filteredItems->value(parentId)->appendChild(item); + else //si el padre a�n no se ha a�adido, hay que a�adirlo a �l y todos los padres hasta el nodo ra�z + { + //comprobamos con esta variable si el �ltimo de los padres (antes del nodo ra�z) ya exist�a en el modelo + bool parentPreviousInserted = false; + + //mientras no se alcance el nodo ra�z se procesan todos los padres (de abajo a arriba) + while (parentId != ROOT) { + //el padre no estaba en el modelo filtrado, as� que se rescata del modelo original + FolderItem *parentItem = model->items.value(parentId); + //se debe crear un nuevo nodo (para no compartir los hijos con el nodo original) + FolderItem *newparentItem = new FolderItem(parentItem->getData()); //padre que se a�adir� a la estructura de directorios filtrados + newparentItem->id = parentId; + + newparentItem->originalItem = parentItem; + + //si el modelo contiene al padre, se a�ade el item actual como hijo + if (filteredItems->contains(parentId)) { + filteredItems->value(parentId)->appendChild(item); + parentPreviousInserted = true; + } + //sino se registra el nodo para poder encontrarlo con posterioridad y se a�ade el item actual como hijo + else { + newparentItem->appendChild(item); + filteredItems->insert(newparentItem->id, newparentItem); + parentPreviousInserted = false; + } + + //variables de control del bucle, se avanza hacia el nodo padre + item = newparentItem; + parentId = parentItem->parentItem->id; + } + + //si el nodo es hijo de 1 y no hab�a sido previamente insertado como hijo, se a�ade como tal + if (!parentPreviousInserted) { + filteredItems->value(ROOT)->appendChild(item); + } else { + delete item; + } + } + } + + emit newData(filteredItems, rootItem); +} diff --git a/YACReaderLibrary/db/folder_query_result_processor.h b/YACReaderLibrary/db/folder_query_result_processor.h new file mode 100644 index 00000000..faa59046 --- /dev/null +++ b/YACReaderLibrary/db/folder_query_result_processor.h @@ -0,0 +1,36 @@ +#ifndef FOLDER_QUERY_RESULT_PROCESSOR_H +#define FOLDER_QUERY_RESULT_PROCESSOR_H + +#include + +#include "yacreader_global.h" +#include "concurrent_queue.h" + +class FolderItem; +class FolderModel; +class QSqlQuery; + +namespace YACReader { + +class FolderQueryResultProcessor : public QObject +{ + Q_OBJECT +public: + FolderQueryResultProcessor(FolderModel *model); + +public slots: + void createModelData(const SearchModifiers modifier, const QString &filter, bool includeComics); + +signals: + void newData(QMap *filteredItems, FolderItem *root); + +private: + ConcurrentQueue querySearchQueue; + + FolderModel *model; + + void setupFilteredModelData(QSqlQuery &sqlquery); +}; +}; + +#endif // FOLDER_QUERY_RESULT_PROCESSOR_H diff --git a/YACReaderLibrary/db/query_lexer.cpp b/YACReaderLibrary/db/query_lexer.cpp new file mode 100644 index 00000000..832b4978 --- /dev/null +++ b/YACReaderLibrary/db/query_lexer.cpp @@ -0,0 +1,84 @@ +#include "query_lexer.h" + +QueryLexer::QueryLexer(const std::string &input) + : input(input) +{ +} + +Token QueryLexer::next() +{ + while (isSpace(peek())) { + get(); + } + + switch (peek()) { + case '\0': + return Token(Token::Type::eof); + case '(': + case ')': + return single(Token::Type::opcode); + case '"': + return quotedWord(); + default: + return word(); + } +} + +char QueryLexer::peek() +{ + return input[index]; +} + +char QueryLexer::get() +{ + return input[index++]; +} + +Token QueryLexer::single(Token::Type type) +{ + return Token(type, input.substr(index++, 1)); +} + +Token QueryLexer::word() +{ + auto start = index; + get(); + auto current = peek(); + while (current != '\0' && !isSpace(current) && current != '"' && current != '(' && current != ')') { + get(); + current = peek(); + } + return Token(Token::Type::word, input.substr(start, index - start)); +} + +Token QueryLexer::quotedWord() +{ + auto start = index; + get(); + auto current = peek(); + while (current != '\0' && current != '"') { + get(); + current = peek(); + } + + if (current == '"') { + get(); + return Token(Token::Type::quotedWord, input.substr(start, index - start)); + } + + //This should be a lexical error, but the grammar doesn't support it + return Token(Token::Type::eof); +} + +bool QueryLexer::isSpace(char c) +{ + switch (c) { + case ' ': + case '\t': + case '\r': + case '\n': + return true; + default: + return false; + } +} diff --git a/YACReaderLibrary/db/query_lexer.h b/YACReaderLibrary/db/query_lexer.h new file mode 100644 index 00000000..4cc2b61f --- /dev/null +++ b/YACReaderLibrary/db/query_lexer.h @@ -0,0 +1,57 @@ +#ifndef QUERY_LEXER_H +#define QUERY_LEXER_H + +#include + +class Token +{ +public: + enum class Type { + eof, + opcode, + word, + quotedWord, + undefined + }; + + Token(Type type, std::string lexeme = "") + : _type(type), _lexeme(std::move(lexeme)) + { + } + + Type type() const + { + return _type; + } + + std::string lexeme() const + { + return _lexeme; + } + +private: + Type _type {}; + std::string _lexeme {}; +}; + +class QueryLexer +{ +public: + QueryLexer(const std::string &input); + Token next(); + +private: + std::string input; + int index = 0; + + char peek(); + char get(); + + Token single(Token::Type type); + Token word(); + Token quotedWord(); + + bool isSpace(char c); +}; + +#endif // QUERY_LEXER_H diff --git a/YACReaderLibrary/db/query_parser.cpp b/YACReaderLibrary/db/query_parser.cpp new file mode 100644 index 00000000..5a45160b --- /dev/null +++ b/YACReaderLibrary/db/query_parser.cpp @@ -0,0 +1,246 @@ +#include "query_parser.h" + +#include +#include +#include + +const std::map> QueryParser::fieldNames { + { FieldType::numeric, { "numpages", "number", "count", "arcnumber", "arccount" } }, + { FieldType::text, { "title", "volume", "storyarc", "genere", "writer", "penciller", "inker", "colorist", "letterer", "coverartist", "publisher", "format", "agerating", "synopsis", "characters", "notes" } }, + { FieldType::boolean, { "isbis", "color", "read" } }, + { FieldType::date, { "date" } }, + { FieldType::filename, { "filename" } }, + { FieldType::folder, { "folder" } }, + { FieldType::booleanFolder, { "completed", "finished" } }, +}; + +int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPosition) const +{ + if (t == "token") { + ++bindPosition; + if (toLower(children[0].t) == "all") { + sqlString += "("; + for (const auto &field : fieldNames.at(FieldType::text)) { + sqlString += "UPPER(ci." + field + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ") OR "; + } + sqlString += "UPPER(c.filename) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ") OR "; + sqlString += "UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + } else if (isIn(fieldType(children[0].t), { FieldType::numeric, FieldType::boolean })) { + sqlString += "ci." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; + } else if (fieldType(children[0].t) == FieldType::filename) { + sqlString += "(UPPER(c." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + } else if (fieldType(children[0].t) == FieldType::folder) { + sqlString += "(UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + } else if (fieldType(children[0].t) == FieldType::booleanFolder) { + sqlString += "f." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; + } else { + sqlString += "(UPPER(ci." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + } + } else if (t == "not") { + sqlString += "(NOT "; + bindPosition = children[0].buildSqlString(sqlString, bindPosition); + sqlString += ")"; + } else { + sqlString += "("; + bindPosition = children[0].buildSqlString(sqlString, bindPosition); + sqlString += " " + t + " "; + bindPosition = children[1].buildSqlString(sqlString, bindPosition); + sqlString += ")"; + } + + return bindPosition; +} + +int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) const +{ + if (t == "token") { + std::string bind_string(":bindPosition" + std::to_string(++bindPosition)); + if (isIn(fieldType(children[0].t), { FieldType::numeric })) { + selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t)); + } else if (isIn(fieldType(children[0].t), { FieldType::boolean, FieldType::booleanFolder })) { + auto value = toLower(children[1].t); + if (value == "true") { + selectQuery.bindValue(QString::fromStdString(bind_string), 1); + } else if (value == "false") { + selectQuery.bindValue(QString::fromStdString(bind_string), 0); + } else { + selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(value)); + } + } else { + selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString("%%" + children[1].t + "%%")); + } + } else if (t == "not") { + bindPosition = children[0].bindValues(selectQuery, bindPosition); + } else { + bindPosition = children[0].bindValues(selectQuery, bindPosition); + bindPosition = children[1].bindValues(selectQuery, bindPosition); + } + + return bindPosition; +} + +QueryParser::QueryParser() +{ +} + +QueryParser::TreeNode QueryParser::parse(const std::string &expr) +{ + lexer = QueryLexer(expr); + advance(); + + auto prog = orExpression(); + + if (!isEof()) { + throw std::invalid_argument("Extra characters at end of search"); + } + + return prog; +} + +std::string QueryParser::toLower(const std::string &string) +{ + std::string res(string); + std::transform(res.begin(), res.end(), res.begin(), ::tolower); + return res; +} + +std::string QueryParser::token(bool advance) +{ + if (isEof()) { + return ""; + } + + auto lexeme = currentToken.lexeme(); + + auto res = (tokenType() == Token::Type::quotedWord) ? currentToken.lexeme().substr(1, lexeme.size() - 2) : lexeme; //TODO process quotedWordDiferently? + if (advance) { + this->advance(); + } + return res; +} + +std::string QueryParser::lcaseToken(bool advance) +{ + if (isEof()) { + return ""; + } + + auto lexeme = currentToken.lexeme(); + + auto res = (tokenType() == Token::Type::quotedWord) ? currentToken.lexeme().substr(1, lexeme.size() - 2) : lexeme; + + if (advance) { + this->advance(); + } + return toLower(res); +} + +Token::Type QueryParser::tokenType() +{ + return currentToken.type(); +} + +bool QueryParser::isEof() const +{ + return currentToken.type() == Token::Type::eof; +} + +void QueryParser::advance() +{ + currentToken = lexer.next(); +} + +QueryParser::FieldType QueryParser::fieldType(const std::string &str) +{ + for (const auto &names : fieldNames) { + if (std::find(names.second.begin(), names.second.end(), toLower(str)) != names.second.end()) { + return names.first; + } + } + + return FieldType::unknown; +} + +std::string QueryParser::join(const QStringList &strings, const std::string &delim) +{ + return std::accumulate(strings.begin(), strings.end(), std::string(), + [&delim](const std::string &a, const QString &b) -> std::string { + return a + (a.length() > 0 && b.length() > 0 ? delim : "") + b.toStdString(); + }); +} + +QStringList QueryParser::split(const std::string &string, char delim) +{ + auto words = QString::fromStdString(string).split(delim); + return words; +} + +QueryParser::TreeNode QueryParser::orExpression() +{ + auto lhs = andExpression(); + if (lcaseToken() == "or") { + advance(); + return TreeNode("or", { lhs, orExpression() }); + } + return lhs; +} + +QueryParser::TreeNode QueryParser::andExpression() +{ + auto lhs = notExpression(); + if (lcaseToken() == "and") { + advance(); + return TreeNode("and", { lhs, andExpression() }); + } + + if ((isIn(tokenType(), { Token::Type::word, Token::Type::quotedWord }) || token() == "(") && lcaseToken() != "or") { + return TreeNode("and", { lhs, andExpression() }); + } + + return lhs; +} + +QueryParser::TreeNode QueryParser::notExpression() +{ + if (lcaseToken() == "not") { + advance(); + return TreeNode("not", { notExpression() }); + } + return locationExpression(); +} + +QueryParser::TreeNode QueryParser::locationExpression() +{ + if (tokenType() == Token::Type::opcode && token() == "(") { + advance(); + auto res = orExpression(); + if (tokenType() != Token::Type::opcode || token(true) != ")") { + throw std::invalid_argument("missing )'"); + } + return res; + } + if (!isIn(tokenType(), { Token::Type::word, Token::Type::quotedWord })) { + throw std::invalid_argument("Invalid syntax. Expected a lookup name or a word"); + } + return baseToken(); +} + +QueryParser::TreeNode QueryParser::baseToken() +{ + if (tokenType() == Token::Type::quotedWord) { + return TreeNode("token", { TreeNode("all", {}), TreeNode(token(true), {}) }); + } + + auto words(split(token(true), ':')); + + if (words.size() > 1 && fieldType(words[0].toStdString()) != FieldType::unknown) { + auto loc(toLower(words[0].toStdString())); + words.erase(words.begin()); + if (words.size() == 1 && tokenType() == Token::Type::quotedWord) { + return TreeNode("token", { TreeNode(loc, {}), TreeNode(token(true), {}) }); + } + return TreeNode("token", { TreeNode(loc, {}), TreeNode(join(words, ":"), {}) }); + } + + return TreeNode("token", { TreeNode("all", {}), TreeNode(join(words, ":"), {}) }); +} diff --git a/YACReaderLibrary/db/query_parser.h b/YACReaderLibrary/db/query_parser.h new file mode 100644 index 00000000..4b8f0c4e --- /dev/null +++ b/YACReaderLibrary/db/query_parser.h @@ -0,0 +1,100 @@ +#ifndef QUERY_PARSER_H +#define QUERY_PARSER_H + +#include "query_lexer.h" + +#include +#include +#include +#include +#include + +/** + * This class is used to generate an SQL query string from a search expression, + * with a syntax very similar to that used by the Google search engine. + * + * The code herein is based upon the SearchQueryParser python class written by + * Kovid Goyal as part of the Calibre eBook manager (https://calibre-ebook.com) + * + * Grammar: + * prog ::= or_expression + * or_expression ::= and_expression [ 'or' or_expression ] + * and_expression ::= not_expression [ [ 'and' ] and_expression ] + * not_expression ::= [ 'not' ] location_expression + * location_expression ::= base_token | ( '(' or_expression ')' ) + * base_token ::= a sequence of letters and colons, perhaps quoted + * + * Usage Example: + * QSqlQuery selectQuery(db); + * std::string queryString("SELECT ... FROM ... WHERE "); + * + * QueryParser parser; // Create the parser object + * TreeNode result = parser.parse(expr); // Parse the query expression + * + * result.buildSqlString(queryString); // Append the SQL query to a string + * + * selectQuery.prepare(queryString.c_str()); // Convert the string to a query + * result.bindValues(selectQuery); // Populate the SQL query variables + * + * selectQuery.exec(); + */ +class QueryParser +{ +public: + struct TreeNode { + std::string t; + std::vector children; + + explicit TreeNode(std::string t, std::vector children) + : t(t), children(children) + { + } + + int buildSqlString(std::string &sqlString, int bindPosition = 0) const; + int bindValues(QSqlQuery &selectQuery, int bindPosition = 0) const; + }; + + explicit QueryParser(); + TreeNode parse(const std::string &expr); + +private: + static std::string toLower(const std::string &string); + + std::string token(bool advance = false); + std::string lcaseToken(bool advance = false); + Token::Type tokenType(); + bool isEof() const; + void advance(); + + QueryLexer lexer = QueryLexer(""); + Token currentToken = Token(Token::Type::undefined); + + template + static bool isIn(const T &e, const std::list &v) + { + return std::find(v.begin(), v.end(), e) != v.end(); + } + + enum class FieldType { unknown, + numeric, + text, + boolean, + date, + folder, + booleanFolder, + filename }; + static FieldType fieldType(const std::string &str); + + static std::string join(const QStringList &strings, const std::string &delim); + static QStringList split(const std::string &string, char delim); + + TreeNode orExpression(); + TreeNode andExpression(); + TreeNode notExpression(); + TreeNode locationExpression(); + TreeNode baseToken(); + + static const std::map> fieldNames; +}; + +#endif // QUERY_PARSER_H diff --git a/YACReaderLibrary/library_window.cpp b/YACReaderLibrary/library_window.cpp index 8da72b0c..65f66b61 100644 --- a/YACReaderLibrary/library_window.cpp +++ b/YACReaderLibrary/library_window.cpp @@ -92,7 +92,7 @@ using namespace YACReader; LibraryWindow::LibraryWindow() - : QMainWindow(), fullscreen(false), previousFilter(""), fetching(false), status(LibraryWindow::Normal), removeError(false) + : QMainWindow(), fullscreen(false), previousFilter(""), fetching(false), status(LibraryWindow::Normal), removeError(false), comicQueryResultProcesor() { setupUI(); @@ -400,6 +400,7 @@ void LibraryWindow::doModels() //folders foldersModel = new FolderModel(); foldersModelProxy = new FolderModelProxy(); + folderQueryResultProcessor = new FolderQueryResultProcessor(foldersModel); //foldersModelProxy->setSourceModel(foldersModel); //comics comicsModel = new ComicModel(this); @@ -1053,10 +1054,10 @@ void LibraryWindow::createConnections() connect(optionsDialog, SIGNAL(optionsChanged()), this, SLOT(reloadOptions())); connect(optionsDialog, SIGNAL(editShortcuts()), editShortcutsDialog, SLOT(show())); - //Folders filter - //connect(clearFoldersFilter,SIGNAL(clicked()),foldersFilter,SLOT(clear())); + //Search filter connect(searchEdit, SIGNAL(filterChanged(YACReader::SearchModifiers, QString)), this, SLOT(setSearchFilter(YACReader::SearchModifiers, QString))); - //connect(includeComicsCheckBox,SIGNAL(stateChanged(int)),this,SLOT(searchInFiles(int))); + connect(&comicQueryResultProcesor, &ComicQueryResultProcesor::newData, this, &LibraryWindow::setComicSearchFilterData); + connect(folderQueryResultProcessor, &FolderQueryResultProcessor::newData, this, &LibraryWindow::setFolderSearchFilterData); //ContextMenus connect(openContainingFolderComicAction, SIGNAL(triggered()), this, SLOT(openContainingFolderComic())); @@ -2070,23 +2071,34 @@ void LibraryWindow::toNormal() void LibraryWindow::setSearchFilter(const YACReader::SearchModifiers modifier, QString filter) { if (!filter.isEmpty()) { - status = LibraryWindow::Searching; - foldersModelProxy->setFilter(modifier, filter, true); //includeComicsCheckBox->isChecked()); - comicsModel->setupModelData(modifier, filter, foldersModel->getDatabase()); - comicsViewsManager->comicsView->enableFilterMode(true); - comicsViewsManager->comicsView->setModel(comicsModel); //TODO, columns are messed up after ResetModel some times, this shouldn't be necesary - foldersView->expandAll(); - - if (comicsModel->rowCount() == 0) - comicsViewsManager->showNoSearchResultsView(); - else - comicsViewsManager->showComicsView(); + folderQueryResultProcessor->createModelData(modifier, filter, true); + comicQueryResultProcesor.createModelData(modifier, filter, foldersModel->getDatabase()); } else if (status == LibraryWindow::Searching) { //if no searching, then ignore this clearSearchFilter(); navigationController->loadPreviousStatus(); } } +void LibraryWindow::setComicSearchFilterData(QList *data, const QString &databasePath) +{ + status = LibraryWindow::Searching; + + comicsModel->setModelData(data, databasePath); + comicsViewsManager->comicsView->enableFilterMode(true); + comicsViewsManager->comicsView->setModel(comicsModel); //TODO, columns are messed up after ResetModel some times, this shouldn't be necesary + + if (comicsModel->rowCount() == 0) + comicsViewsManager->showNoSearchResultsView(); + else + comicsViewsManager->showComicsView(); +} + +void LibraryWindow::setFolderSearchFilterData(QMap *filteredItems, FolderItem *root) +{ + foldersModelProxy->setFilterData(filteredItems, root); + foldersView->expandAll(); +} + void LibraryWindow::clearSearchFilter() { foldersModelProxy->clear(); diff --git a/YACReaderLibrary/library_window.h b/YACReaderLibrary/library_window.h index 993b58b9..4e626cf3 100644 --- a/YACReaderLibrary/library_window.h +++ b/YACReaderLibrary/library_window.h @@ -10,6 +10,8 @@ #include "yacreader_libraries.h" #include "yacreader_navigation_controller.h" +#include "comic_query_result_procesor.h" +#include "folder_query_result_processor.h" #include @@ -327,6 +329,8 @@ public slots: void toNormal(); void toFullScreen(); void setSearchFilter(const YACReader::SearchModifiers modifier, QString filter); + void setComicSearchFilterData(QList *, const QString &); + void setFolderSearchFilterData(QMap *filteredItems, FolderItem *root); void clearSearchFilter(); void showProperties(); void exportLibrary(QString destPath); @@ -399,6 +403,8 @@ private: std::future upgradeLibraryFuture; TrayIconController *trayIconController; + ComicQueryResultProcesor comicQueryResultProcesor; + FolderQueryResultProcessor *folderQueryResultProcessor; }; #endif diff --git a/common/concurrent_queue.h b/common/concurrent_queue.h new file mode 100644 index 00000000..5b92f6f1 --- /dev/null +++ b/common/concurrent_queue.h @@ -0,0 +1,132 @@ +#ifndef CONCURRENT_QUEUE_H +#define CONCURRENT_QUEUE_H + +#include +#include +#include +#include +#include + +namespace YACReader { +class ConcurrentQueue +{ +public: + explicit ConcurrentQueue(int threadCount) + : jobsLeft(0), + bailout(false) + { + threads = std::vector(threadCount); + for (int index = 0; index < threadCount; ++index) { + threads[index] = std::thread([this] { + this->nextJob(); + }); + } + } + + ~ConcurrentQueue() + { + joinAll(); + } + + void enqueue(std::function job) + { + { + std::lock_guard lock(queueMutex); + _queue.emplace(job); + } + + { + std::lock_guard lock(jobsLeftMutex); + ++jobsLeft; + } + + jobAvailableVar.notify_one(); + } + + void cancellPending() + { + std::unique_lock lock(jobsLeftMutex); + _queue = std::queue>(); + jobsLeft = 0; + } + + void waitAll() + { + std::unique_lock lock(jobsLeftMutex); + if (jobsLeft > 0) { + _waitVar.wait(lock, [this] { + return jobsLeft == 0; + }); + } + } + +private: + std::vector threads; + std::queue> _queue; + int jobsLeft; + bool bailout; + std::condition_variable jobAvailableVar; + std::condition_variable _waitVar; + std::mutex jobsLeftMutex; + std::mutex queueMutex; + + void nextJob() + { + while (true) { + std::function job; + + { + std::unique_lock lock(queueMutex); + + if (bailout) { + return; + } + + jobAvailableVar.wait(lock, [this] { + return _queue.size() > 0 || bailout; + }); + + if (bailout) { + return; + } + + job = _queue.front(); + _queue.pop(); + } + + job(); + + { + std::lock_guard lock(jobsLeftMutex); + --jobsLeft; + } + + _waitVar.notify_one(); + } + } + + void joinAll() + { + { + std::lock_guard lock(queueMutex); + + if (bailout) { + return; + } + + bailout = true; + } + + jobAvailableVar.notify_all(); + + for (auto &x : threads) { + if (x.joinable()) { + x.join(); + } + } + } +}; + +} + +#endif // CONCURRENT_QUEUE_H diff --git a/common/yacreader_global.h b/common/yacreader_global.h index e52d73a7..770e35e9 100644 --- a/common/yacreader_global.h +++ b/common/yacreader_global.h @@ -36,8 +36,7 @@ enum YACReaderErrors { enum SearchModifiers { NoModifiers = 0, OnlyRead, - OnlyUnread, - ByAuthor + OnlyUnread }; enum LabelColors { diff --git a/custom_widgets/yacreader_search_line_edit.cpp b/custom_widgets/yacreader_search_line_edit.cpp index 3a209a27..0232184e 100644 --- a/custom_widgets/yacreader_search_line_edit.cpp +++ b/custom_widgets/yacreader_search_line_edit.cpp @@ -40,7 +40,7 @@ YACReaderSearchLineEdit::YACReaderSearchLineEdit(QWidget *parent) #ifdef Q_OS_MAC setMaximumWidth(212); #else - setMaximumWidth(173); + setMaximumWidth(255); setFixedHeight(26); #endif