diff --git a/include/library.h b/include/library.h index 890d140d5..7703ff1f6 100644 --- a/include/library.h +++ b/include/library.h @@ -26,6 +26,7 @@ #include "book.h" #include "bookmark.h" +#include "common.h" #define KIWIX_LIBRARY_VERSION "20110515" @@ -44,6 +45,65 @@ enum supportedListMode { VALID = 1 << 4, NOVALID = 1 << 5 }; + +class Filter { + private: + uint64_t activeFilters; + std::vector _acceptTags; + std::vector _rejectTags; + std::string _lang; + std::string _publisher; + std::string _creator; + size_t _maxSize; + std::string _query; + + public: + Filter(); + ~Filter() = default; + + /** + * Set the filter to check local. + * + * A local book is a book with a path. + * If accept is true, only local book are accepted. + * If accept is false, only non local book are accepted. + */ + Filter& local(bool accept); + + /** + * Set the filter to check remote. + * + * A remote book is a book with a url. + * If accept is true, only remote book are accepted. + * If accept is false, only non remote book are accepted. + */ + Filter& remote(bool accept); + + /** + * Set the filter to check validity. + * + * A valid book is a book with a path pointing to a existing zim file. + * If accept is true, only valid book are accepted. + * If accept is false, only non valid book are accepted. + */ + Filter& valid(bool accept); + + /** + * Set the filter to only accept book with corresponding tag. + */ + Filter& acceptTags(std::vector tags); + Filter& rejectTags(std::vector tags); + + Filter& lang(std::string lang); + Filter& publisher(std::string publisher); + Filter& creator(std::string creator); + Filter& maxSize(size_t size); + Filter& query(std::string query); + + bool accept(const Book& book) const; +}; + + /** * A Library store several books. */ @@ -162,9 +222,27 @@ class Library * @param search List only books with search in the title or description. * @return The list of bookIds corresponding to the query. */ - std::vector filter(const std::string& search); + DEPRECATED std::vector filter(const std::string& search); + /** + * Filter the library and return the id of the keep elements. + * + * @param filter The filter to use. + * @return The list of bookIds corresponding to the filter. + */ + std::vector filter(const Filter& filter); + + + /** + * Sort (in place) bookIds using the given comparator. + * + * @param bookIds the list of book Ids to sort + * @param comparator how to sort the books + * @return The sorted list of books + */ + void sort(std::vector& bookIds, supportedListSortBy sortBy, bool ascending); + /** * List books in the library. * @@ -187,7 +265,7 @@ class Library * Set to 0 to cancel this filter. * @return The list of bookIds corresponding to the query. */ - std::vector listBooksIds( + DEPRECATED std::vector listBooksIds( int supportedListMode = ALL, supportedListSortBy sortBy = UNSORTED, const std::string& search = "", diff --git a/src/library.cpp b/src/library.cpp index 58c6d8b48..c024ea869 100644 --- a/src/library.cpp +++ b/src/library.cpp @@ -96,14 +96,16 @@ unsigned int Library::getBookCount(const bool localBooks, return result; } -bool Library::writeToFile(const std::string& path) { +bool Library::writeToFile(const std::string& path) +{ auto baseDir = removeLastPathElement(path, true, false); LibXMLDumper dumper(this); dumper.setBaseDir(baseDir); return writeTextFile(path, dumper.dumpLibXMLContent(getBooksIds())); } -bool Library::writeBookmarksToFile(const std::string& path) { +bool Library::writeBookmarksToFile(const std::string& path) +{ LibXMLDumper dumper(this); return writeTextFile(path, dumper.dumpLibXMLBookmark()); } @@ -182,67 +184,104 @@ std::vector Library::filter(const std::string& search) return getBooksIds(); } + return filter(Filter().query(search)); +} + + +std::vector Library::filter(const Filter& filter) +{ std::vector bookIds; for(auto& pair:m_books) { - auto& book = pair.second; - if (matchRegex(book.getTitle(), "\\Q" + search + "\\E") - || matchRegex(book.getDescription(), "\\Q" + search + "\\E")) { - bookIds.push_back(pair.first); - } + auto book = pair.second; + if(filter.accept(book)) { + bookIds.push_back(pair.first); + } } - return bookIds; } -template -struct Comparator { - Library* lib; - Comparator(Library* lib) : lib(lib) {} - - bool operator() (const std::string& id1, const std::string& id2) { - return get_keys(id1) < get_keys(id2); - } - - std::string get_keys(const std::string& id); - unsigned int get_keyi(const std::string& id); +template +struct KEY_TYPE { + typedef std::string TYPE; }; template<> -std::string Comparator::get_keys(const std::string& id) +struct KEY_TYPE<SIZE> { + typedef size_t TYPE; +}; + +template<supportedListSortBy sort> +class Comparator { + private: + Library* lib; + bool ascending; + + inline typename KEY_TYPE<sort>::TYPE get_key(const std::string& id); + + public: + Comparator(Library* lib, bool ascending) : lib(lib), ascending(ascending) {} + inline bool operator() (const std::string& id1, const std::string& id2) { + if (ascending) { + return get_key(id1) < get_key(id2); + } else { + return get_key(id2) < get_key(id1); + } + } +}; + +template<> +std::string Comparator<TITLE>::get_key(const std::string& id) { return lib->getBookById(id).getTitle(); } template<> -unsigned int Comparator<SIZE>::get_keyi(const std::string& id) +size_t Comparator<SIZE>::get_key(const std::string& id) { return lib->getBookById(id).getSize(); } template<> -bool Comparator<SIZE>::operator() (const std::string& id1, const std::string& id2) -{ - return get_keyi(id1) < get_keyi(id2); -} - -template<> -std::string Comparator<DATE>::get_keys(const std::string& id) +std::string Comparator<DATE>::get_key(const std::string& id) { return lib->getBookById(id).getDate(); } template<> -std::string Comparator<CREATOR>::get_keys(const std::string& id) +std::string Comparator<CREATOR>::get_key(const std::string& id) { return lib->getBookById(id).getCreator(); } template<> -std::string Comparator<PUBLISHER>::get_keys(const std::string& id) +std::string Comparator<PUBLISHER>::get_key(const std::string& id) { return lib->getBookById(id).getPublisher(); } +void Library::sort(std::vector<std::string>& bookIds, supportedListSortBy sort, bool ascending) +{ + switch(sort) { + case TITLE: + std::sort(bookIds.begin(), bookIds.end(), Comparator<TITLE>(this, ascending)); + break; + case SIZE: + std::sort(bookIds.begin(), bookIds.end(), Comparator<SIZE>(this, ascending)); + break; + case DATE: + std::sort(bookIds.begin(), bookIds.end(), Comparator<DATE>(this, ascending)); + break; + case CREATOR: + std::sort(bookIds.begin(), bookIds.end(), Comparator<CREATOR>(this, ascending)); + break; + case PUBLISHER: + std::sort(bookIds.begin(), bookIds.end(), Comparator<PUBLISHER>(this, ascending)); + break; + default: + break; + } +} + std::vector<std::string> Library::listBooksIds( int mode, @@ -254,74 +293,205 @@ std::vector<std::string> Library::listBooksIds( const std::vector<std::string>& tags, size_t maxSize) { - std::vector<std::string> bookIds; - for(auto& pair:m_books) { - auto& book = pair.second; - auto local = !book.getPath().empty(); - if (mode & LOCAL && !local) - continue; - if (mode & NOLOCAL && local) - continue; - auto valid = book.isPathValid(); - if (mode & VALID && !valid) - continue; - if (mode & NOVALID && valid) - continue; - auto remote = !book.getUrl().empty(); - if (mode & REMOTE && !remote) - continue; - if (mode & NOREMOTE && remote) - continue; - if (!tags.empty()) { - auto vBookTags = split(book.getTags(), ";"); - std::set<std::string> sBookTags(vBookTags.begin(), vBookTags.end()); - bool ok = true; - for (auto& t: tags) { - if (sBookTags.find(t) == sBookTags.end()) { - // A "filter" tag is not in the book tag. - // No need to loop for all "filter" tags. - ok = false; - break; - } - } - if (! ok ) { - // Skip the book - continue; - } - } - if (maxSize != 0 && book.getSize() > maxSize) - continue; - if (!language.empty() && book.getLanguage() != language) - continue; - if (!publisher.empty() && book.getPublisher() != publisher) - continue; - if (!creator.empty() && book.getCreator() != creator) - continue; - if (!search.empty() && !(matchRegex(book.getTitle(), "\\Q" + search + "\\E") - || matchRegex(book.getDescription(), "\\Q" + search + "\\E"))) - continue; - bookIds.push_back(pair.first); - } + Filter _filter; + if (mode & LOCAL) + _filter.local(true); + if (mode & NOLOCAL) + _filter.local(false); + if (mode & VALID) + _filter.valid(true); + if (mode & NOVALID) + _filter.valid(false); + if (mode & REMOTE) + _filter.remote(true); + if (mode & NOREMOTE) + _filter.remote(false); + if (!tags.empty()) + _filter.acceptTags(tags); + if (maxSize != 0) + _filter.maxSize(maxSize); + if (!language.empty()) + _filter.lang(language); + if (!publisher.empty()) + _filter.publisher(publisher); + if (!creator.empty()) + _filter.creator(creator); + if (!search.empty()) + _filter.query(search); - switch(sortBy) { - case TITLE: - std::sort(bookIds.begin(), bookIds.end(), Comparator<TITLE>(this)); - break; - case SIZE: - std::sort(bookIds.begin(), bookIds.end(), Comparator<SIZE>(this)); - break; - case DATE: - std::sort(bookIds.begin(), bookIds.end(), Comparator<DATE>(this)); - break; - case CREATOR: - std::sort(bookIds.begin(), bookIds.end(), Comparator<CREATOR>(this)); - break; - case PUBLISHER: - std::sort(bookIds.begin(), bookIds.end(), Comparator<PUBLISHER>(this)); - break; - default: - break; - } + auto bookIds = filter(_filter); + + sort(bookIds, sortBy, true); return bookIds; } + +Filter::Filter() + : activeFilters(0), + _maxSize(0) +{}; + +#define FLAG(x) (1 << x) +enum filterTypes { + NONE = 0, + _LOCAL = FLAG(0), + _REMOTE = FLAG(1), + _NOLOCAL = FLAG(2), + _NOREMOTE = FLAG(3), + _VALID = FLAG(4), + _NOVALID = FLAG(5), + ACCEPTTAGS = FLAG(6), + REJECTTAGS = FLAG(7), + LANG = FLAG(8), + _PUBLISHER = FLAG(9), + _CREATOR = FLAG(10), + MAXSIZE = FLAG(11), + QUERY = FLAG(12), +}; + +Filter& Filter::local(bool accept) +{ + if (accept) { + activeFilters |= _LOCAL; + activeFilters &= ~_NOLOCAL; + } else { + activeFilters |= _NOLOCAL; + activeFilters &= ~_LOCAL; + } + return *this; +} + +Filter& Filter::remote(bool accept) +{ + if (accept) { + activeFilters |= _REMOTE; + activeFilters &= ~_NOREMOTE; + } else { + activeFilters |= _NOREMOTE; + activeFilters &= ~_REMOTE; + } + return *this; +} + +Filter& Filter::valid(bool accept) +{ + if (accept) { + activeFilters |= _VALID; + activeFilters &= ~_NOVALID; + } else { + activeFilters |= _NOVALID; + activeFilters &= ~_VALID; + } + return *this; +} + +Filter& Filter::acceptTags(std::vector<std::string> tags) +{ + _acceptTags = tags; + activeFilters |= ACCEPTTAGS; + return *this; +} + +Filter& Filter::rejectTags(std::vector<std::string> tags) +{ + _rejectTags = tags; + activeFilters |= REJECTTAGS; + return *this; +} + +Filter& Filter::lang(std::string lang) +{ + _lang = lang; + activeFilters |= LANG; + return *this; +} + +Filter& Filter::publisher(std::string publisher) +{ + _publisher = publisher; + activeFilters |= _PUBLISHER; + return *this; +} + +Filter& Filter::creator(std::string creator) +{ + _creator = creator; + activeFilters |= _CREATOR; + return *this; +} + +Filter& Filter::maxSize(size_t maxSize) +{ + _maxSize = maxSize; + activeFilters |= MAXSIZE; + return *this; +} + +Filter& Filter::query(std::string query) +{ + _query = query; + activeFilters |= QUERY; + return *this; +} + +#define ACTIVE(X) (activeFilters & (X)) +bool Filter::accept(const Book& book) const +{ + auto local = !book.getPath().empty(); + if (ACTIVE(_LOCAL) && !local) + return false; + if (ACTIVE(_NOLOCAL) && local) + return false; + auto valid = book.isPathValid(); + if (ACTIVE(_VALID) && !valid) + return false; + if (ACTIVE(_NOVALID) && valid) + return false; + auto remote = !book.getUrl().empty(); + if (ACTIVE(_REMOTE) && !remote) + return false; + if (ACTIVE(_NOREMOTE) && remote) + return false; + if (ACTIVE(ACCEPTTAGS)) { + if (!_acceptTags.empty()) { + auto vBookTags = split(book.getTags(), ";"); + std::set<std::string> sBookTags(vBookTags.begin(), vBookTags.end()); + for (auto& t: _acceptTags) { + if (sBookTags.find(t) == sBookTags.end()) { + return false; + } + } + } + } + if (ACTIVE(REJECTTAGS)) { + if (!_rejectTags.empty()) { + auto vBookTags = split(book.getTags(), ";"); + std::set<std::string> sBookTags(vBookTags.begin(), vBookTags.end()); + for (auto& t: _rejectTags) { + if (sBookTags.find(t) != sBookTags.end()) { + return false; + } + } + } + } + if (ACTIVE(MAXSIZE) && book.getSize() > _maxSize) + return false; + + if (ACTIVE(LANG) && book.getLanguage() != _lang) + return false; + + if (ACTIVE(_PUBLISHER) && book.getPublisher() != _publisher) + return false; + + if (ACTIVE(_CREATOR) && book.getCreator() != _creator) + return false; + + if ( ACTIVE(QUERY) + && !(matchRegex(book.getTitle(), "\\Q" + _query + "\\E") + || matchRegex(book.getDescription(), "\\Q" + _query + "\\E"))) + return false; + + return true; + +} + } diff --git a/test/library.cpp b/test/library.cpp new file mode 100644 index 000000000..4ff2afb72 --- /dev/null +++ b/test/library.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2013 Tommi Maekitalo + * + * 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) any later version. + * + * This program is distributed in the hope that it will be useful, but + * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and + * NON-INFRINGEMENT. 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "gtest/gtest.h" +#include <string> + + +const char * sampleOpdsStream = R"( +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog"> + <id>00000000-0000-0000-0000-000000000000</id> + <entry> + <title>Encyclopédie de la Tunisie + urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd + /meta?name=favicon&content=wikipedia_fr_tunisie_novid_2018-10 + 2018-10-08T00:00::00:Z + fra + Le meilleur de Wikipédia sur la Tunisie + wikipedia;novid;_ftindex + + + Wikipedia + + + + + + Tania Louis + urn:uuid:0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2 + /meta?name=favicon&content=biologie-tout-compris_fr_all_2018-06 + 2018-06-23T00:00::00:Z + fra + Tania Louis videos + youtube + + + Tania Louis + + + + + + Wikiquote + urn:uuid:0ea1cde6-441d-6c58-f2c7-21c2838e659f + /meta?name=favicon&content=wikiquote_fr_all_nopic_2019-06 + 2019-06-05T00:00::00:Z + fra + Une page de Wikiquote, le recueil des citations libres. + wikiquote;nopic + + + Wikiquote + + + + + + Géographie par Wikipédia + urn:uuid:1123e574-6eef-6d54-28fc-13e4caeae474 + /meta?name=favicon&content=wikipedia_fr_geography_nopic_2019-06 + 2019-06-02T00:00::00:Z + Une sélection d'articles de Wikipédia sur la géographie + fra + wikipedia;nopic + + + Wikipedia + + + + + + Mathématiques + urn:uuid:14829621-c490-c376-0792-9de558b57efa + /meta?name=favicon&content=wikipedia_fr_mathematics_nopic_2019-05 + 2019-05-13T00:00::00:Z + fra + Une + wikipedia;nopic + + + Wikipedia + + + + + + Granblue Fantasy Wiki + urn:uuid:006cbd1b-16d8-b00d-a584-c1ae110a94ed + /meta?name=favicon&content=granbluefantasy_en_all_all_nopic_2018-10 + 2018-10-14T00:00::00:Z + eng + Granblue Fantasy Wiki + gbf;nopic;_ftindex + + + Wiki + + + + + + Movies & TV Stack Exchange + urn:uuid:00f37b00-f4da-0675-995a-770f9c72903e + /meta?name=favicon&content=movies.stackexchange.com_en_all_2019-02 + 2019-02-03T00:00::00:Z + eng + Q&A for movie and tv enthusiasts + stackexchange;_ftindex + + + Movies & TV Stack Exchange + + + + + + TED talks - Business + urn:uuid:0189d9be-2fd0-b4b6-7300-20fab0b5cdc8 + /meta?name=favicon&content=ted_en_business_2018-07 + 2018-07-23T00:00::00:Z + eng + Ideas worth spreading + + + + TED + + + + + + Mythology & Folklore Stack Exchange + urn:uuid:028055ac-4acc-1d54-65e0-a96de45e1b22 + /meta?name=favicon&content=mythology.stackexchange.com_en_all_2019-02 + 2019-02-03T00:00::00:Z + eng + Q&A for enthusiasts and scholars of mythology and folklore + stackexchange;_ftindex + + + Mythology & Folklore Stack Exchange + + + + + + Islam Stack Exchange + urn:uuid:02e9c7ff-36fc-9c6e-6ac7-cd7085989029 + /meta?name=favicon&content=islam.stackexchange.com_en_all_2019-01 + 2019-01-31T00:00::00:Z + eng + Q&A for Muslims, experts in Islam, and those interested in learning more about Islam + stackexchange;_ftindex + + + Islam Stack Exchange + + + + + + +)"; + +#include "../include/library.h" +#include "../include/manager.h" + +namespace +{ + +class LibraryTest : public ::testing::Test { + protected: + void SetUp() override { + kiwix::Manager manager(&lib); + manager.readOpds(sampleOpdsStream, "foo.urlHost"); + } + + kiwix::Library lib; +}; + +TEST_F(LibraryTest, sanityCheck) +{ + EXPECT_EQ(lib.getBookCount(true, true), 10U); + EXPECT_EQ(lib.getBooksLanguages().size(), 2U); + EXPECT_EQ(lib.getBooksCreators().size(), 8U); + EXPECT_EQ(lib.getBooksPublishers().size(), 1U); +} + +TEST_F(LibraryTest, filterCheck) +{ + auto bookIds = lib.filter(kiwix::Filter()); + EXPECT_EQ(bookIds, lib.getBooksIds()); + + bookIds = lib.filter(kiwix::Filter().lang("eng")); + EXPECT_EQ(bookIds.size(), 5U); + + bookIds = lib.filter(kiwix::Filter().acceptTags({"stackexchange"})); + EXPECT_EQ(bookIds.size(), 3U); + + bookIds = lib.filter(kiwix::Filter().acceptTags({"wikipedia"})); + EXPECT_EQ(bookIds.size(), 3U); + + bookIds = lib.filter(kiwix::Filter().acceptTags({"wikipedia", "nopic"})); + EXPECT_EQ(bookIds.size(), 2U); + + bookIds = lib.filter(kiwix::Filter().acceptTags({"wikipedia"}).rejectTags({"nopic"})); + EXPECT_EQ(bookIds.size(), 1U); + + bookIds = lib.filter(kiwix::Filter().query("folklore")); + EXPECT_EQ(bookIds.size(), 1U); + + bookIds = lib.filter(kiwix::Filter().query("Wiki")); + EXPECT_EQ(bookIds.size(), 3U); + + bookIds = lib.filter(kiwix::Filter().query("Wiki").creator("Wiki")); + EXPECT_EQ(bookIds.size(), 1U); + +} +}; + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/meson.build b/test/meson.build index 9d29177fe..b08cabd47 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,7 +1,8 @@ tests = [ - 'parseUrl' + 'parseUrl', + 'library' ]