mirror of https://github.com/kiwix/libkiwix.git
Move LibraryBase out of public API.
We use composition instead of inheritance to implement Library.
This commit is contained in:
parent
ff2c7b1fb2
commit
090c2fd31a
|
@ -140,51 +140,16 @@ private: // functions
|
||||||
bool accept(const Book& book) const;
|
bool accept(const Book& book) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is not part of the libkiwix API. Its only purpose is
|
|
||||||
* to simplify the implementation of the Library's move operations
|
|
||||||
* and avoid bugs should new data members be added to Library.
|
|
||||||
*/
|
|
||||||
class LibraryBase
|
|
||||||
{
|
|
||||||
protected: // types
|
|
||||||
typedef uint64_t LibraryRevision;
|
|
||||||
|
|
||||||
struct Entry : Book
|
|
||||||
{
|
|
||||||
LibraryRevision lastUpdatedRevision = 0;
|
|
||||||
|
|
||||||
// May also keep the Archive and Reader pointers here and get
|
|
||||||
// rid of the m_readers and m_archives data members in Library
|
|
||||||
};
|
|
||||||
|
|
||||||
protected: // data
|
|
||||||
LibraryRevision m_revision;
|
|
||||||
std::map<std::string, Entry> m_books;
|
|
||||||
std::map<std::string, std::shared_ptr<Reader>> m_readers;
|
|
||||||
std::map<std::string, std::shared_ptr<zim::Archive>> m_archives;
|
|
||||||
std::vector<kiwix::Bookmark> m_bookmarks;
|
|
||||||
class BookDB;
|
|
||||||
std::unique_ptr<BookDB> m_bookDB;
|
|
||||||
|
|
||||||
protected: // functions
|
|
||||||
LibraryBase();
|
|
||||||
~LibraryBase();
|
|
||||||
|
|
||||||
LibraryBase(LibraryBase&& );
|
|
||||||
LibraryBase& operator=(LibraryBase&& );
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Library store several books.
|
* A Library store several books.
|
||||||
*/
|
*/
|
||||||
class Library : private LibraryBase
|
class Library
|
||||||
{
|
{
|
||||||
// all data fields must be added in LibraryBase
|
// all data fields must be added in LibraryBase
|
||||||
mutable std::mutex m_mutex;
|
mutable std::mutex m_mutex;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
typedef LibraryRevision Revision;
|
typedef uint64_t Revision;
|
||||||
typedef std::vector<std::string> BookIdCollection;
|
typedef std::vector<std::string> BookIdCollection;
|
||||||
typedef std::map<std::string, int> AttributeCounts;
|
typedef std::map<std::string, int> AttributeCounts;
|
||||||
|
|
||||||
|
@ -351,7 +316,7 @@ class Library : private LibraryBase
|
||||||
*
|
*
|
||||||
* @return Current revision of the library.
|
* @return Current revision of the library.
|
||||||
*/
|
*/
|
||||||
LibraryRevision getRevision() const;
|
Revision getRevision() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove books that have not been updated since the specified revision.
|
* Remove books that have not been updated since the specified revision.
|
||||||
|
@ -359,13 +324,14 @@ class Library : private LibraryBase
|
||||||
* @param rev the library revision to use
|
* @param rev the library revision to use
|
||||||
* @return Count of books that were removed by this operation.
|
* @return Count of books that were removed by this operation.
|
||||||
*/
|
*/
|
||||||
uint32_t removeBooksNotUpdatedSince(LibraryRevision rev);
|
uint32_t removeBooksNotUpdatedSince(Revision rev);
|
||||||
|
|
||||||
friend class OPDSDumper;
|
friend class OPDSDumper;
|
||||||
friend class libXMLDumper;
|
friend class libXMLDumper;
|
||||||
|
|
||||||
private: // types
|
private: // types
|
||||||
typedef const std::string& (Book::*BookStrPropMemFn)() const;
|
typedef const std::string& (Book::*BookStrPropMemFn)() const;
|
||||||
|
struct Impl;
|
||||||
|
|
||||||
private: // functions
|
private: // functions
|
||||||
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
|
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
|
||||||
|
@ -373,6 +339,9 @@ private: // functions
|
||||||
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
||||||
void updateBookDB(const Book& book);
|
void updateBookDB(const Book& book);
|
||||||
void dropReader(const std::string& bookId);
|
void dropReader(const std::string& bookId);
|
||||||
|
|
||||||
|
private: //data
|
||||||
|
std::unique_ptr<Impl> mp_impl;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
110
src/library.cpp
110
src/library.cpp
|
@ -58,66 +58,92 @@ bool booksReferToTheSameArchive(const Book& book1, const Book& book2)
|
||||||
|
|
||||||
} // unnamed namespace
|
} // unnamed namespace
|
||||||
|
|
||||||
class LibraryBase::BookDB : public Xapian::WritableDatabase
|
struct Library::Impl
|
||||||
|
{
|
||||||
|
struct Entry : Book
|
||||||
|
{
|
||||||
|
Library::Revision lastUpdatedRevision = 0;
|
||||||
|
|
||||||
|
// May also keep the Archive and Reader pointers here and get
|
||||||
|
// rid of the m_readers and m_archives data members in Library
|
||||||
|
};
|
||||||
|
|
||||||
|
Library::Revision m_revision;
|
||||||
|
std::map<std::string, Entry> m_books;
|
||||||
|
std::map<std::string, std::shared_ptr<Reader>> m_readers;
|
||||||
|
std::map<std::string, std::shared_ptr<zim::Archive>> m_archives;
|
||||||
|
std::vector<kiwix::Bookmark> m_bookmarks;
|
||||||
|
class BookDB;
|
||||||
|
std::unique_ptr<BookDB> m_bookDB;
|
||||||
|
|
||||||
|
Impl();
|
||||||
|
~Impl();
|
||||||
|
|
||||||
|
Impl(Impl&& );
|
||||||
|
Impl& operator=(Impl&& );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class Library::Impl::BookDB : public Xapian::WritableDatabase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BookDB() : Xapian::WritableDatabase("", Xapian::DB_BACKEND_INMEMORY) {}
|
BookDB() : Xapian::WritableDatabase("", Xapian::DB_BACKEND_INMEMORY) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
LibraryBase::LibraryBase()
|
Library::Impl::Impl()
|
||||||
: m_bookDB(new BookDB)
|
: m_bookDB(new BookDB)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryBase::~LibraryBase()
|
Library::Impl::~Impl()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryBase::LibraryBase(LibraryBase&& ) = default;
|
Library::Impl::Impl(Library::Impl&& ) = default;
|
||||||
LibraryBase& LibraryBase::operator=(LibraryBase&& ) = default;
|
Library::Impl& Library::Impl::operator=(Library::Impl&& ) = default;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Constructor */
|
/* Constructor */
|
||||||
Library::Library()
|
Library::Library()
|
||||||
|
: mp_impl(new Library::Impl)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Library::Library(Library&& other)
|
Library::Library(Library&& other)
|
||||||
: LibraryBase(std::move(other))
|
: mp_impl(std::move(other.mp_impl))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Library& Library::operator=(Library&& other)
|
Library& Library::operator=(Library&& other)
|
||||||
{
|
{
|
||||||
LibraryBase::operator=(std::move(other));
|
mp_impl = std::move(other.mp_impl);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Destructor */
|
/* Destructor */
|
||||||
Library::~Library()
|
Library::~Library() = default;
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool Library::addBook(const Book& book)
|
bool Library::addBook(const Book& book)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
++m_revision;
|
++mp_impl->m_revision;
|
||||||
/* Try to find it */
|
/* Try to find it */
|
||||||
updateBookDB(book);
|
updateBookDB(book);
|
||||||
try {
|
try {
|
||||||
auto& oldbook = m_books.at(book.getId());
|
auto& oldbook = mp_impl->m_books.at(book.getId());
|
||||||
if ( ! booksReferToTheSameArchive(oldbook, book) ) {
|
if ( ! booksReferToTheSameArchive(oldbook, book) ) {
|
||||||
dropReader(book.getId());
|
dropReader(book.getId());
|
||||||
}
|
}
|
||||||
oldbook.update(book); // XXX: This may have no effect if oldbook is readonly
|
oldbook.update(book); // XXX: This may have no effect if oldbook is readonly
|
||||||
// XXX: Then m_bookDB will become out-of-sync with
|
// XXX: Then m_bookDB will become out-of-sync with
|
||||||
// XXX: the real contents of the library.
|
// XXX: the real contents of the library.
|
||||||
oldbook.lastUpdatedRevision = m_revision;
|
oldbook.lastUpdatedRevision = mp_impl->m_revision;
|
||||||
return false;
|
return false;
|
||||||
} catch (std::out_of_range&) {
|
} catch (std::out_of_range&) {
|
||||||
Entry& newEntry = m_books[book.getId()];
|
auto& newEntry = mp_impl->m_books[book.getId()];
|
||||||
static_cast<Book&>(newEntry) = book;
|
static_cast<Book&>(newEntry) = book;
|
||||||
newEntry.lastUpdatedRevision = m_revision;
|
newEntry.lastUpdatedRevision = mp_impl->m_revision;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,15 +151,15 @@ bool Library::addBook(const Book& book)
|
||||||
void Library::addBookmark(const Bookmark& bookmark)
|
void Library::addBookmark(const Bookmark& bookmark)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_bookmarks.push_back(bookmark);
|
mp_impl->m_bookmarks.push_back(bookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
|
for(auto it=mp_impl->m_bookmarks.begin(); it!=mp_impl->m_bookmarks.end(); it++) {
|
||||||
if (it->getBookId() == zimId && it->getUrl() == url) {
|
if (it->getBookId() == zimId && it->getUrl() == url) {
|
||||||
m_bookmarks.erase(it);
|
mp_impl->m_bookmarks.erase(it);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,30 +169,30 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||||
|
|
||||||
void Library::dropReader(const std::string& id)
|
void Library::dropReader(const std::string& id)
|
||||||
{
|
{
|
||||||
m_readers.erase(id);
|
mp_impl->m_readers.erase(id);
|
||||||
m_archives.erase(id);
|
mp_impl->m_archives.erase(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Library::removeBookById(const std::string& id)
|
bool Library::removeBookById(const std::string& id)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_bookDB->delete_document("Q" + id);
|
mp_impl->m_bookDB->delete_document("Q" + id);
|
||||||
dropReader(id);
|
dropReader(id);
|
||||||
return m_books.erase(id) == 1;
|
return mp_impl->m_books.erase(id) == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Library::Revision Library::getRevision() const
|
Library::Revision Library::getRevision() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
return m_revision;
|
return mp_impl->m_revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Library::removeBooksNotUpdatedSince(LibraryRevision libraryRevision)
|
uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
|
||||||
{
|
{
|
||||||
BookIdCollection booksToRemove;
|
BookIdCollection booksToRemove;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
for ( const auto& entry : m_books) {
|
for ( const auto& entry : mp_impl->m_books) {
|
||||||
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
|
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
|
||||||
booksToRemove.push_back(entry.first);
|
booksToRemove.push_back(entry.first);
|
||||||
}
|
}
|
||||||
|
@ -185,7 +211,7 @@ const Book& Library::getBookById(const std::string& id) const
|
||||||
{
|
{
|
||||||
// XXX: Doesn't make sense to lock this operation since it cannot
|
// XXX: Doesn't make sense to lock this operation since it cannot
|
||||||
// XXX: guarantee thread-safety because of its return type
|
// XXX: guarantee thread-safety because of its return type
|
||||||
return m_books.at(id);
|
return mp_impl->m_books.at(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Book Library::getBookByIdThreadSafe(const std::string& id) const
|
Book Library::getBookByIdThreadSafe(const std::string& id) const
|
||||||
|
@ -198,7 +224,7 @@ const Book& Library::getBookByPath(const std::string& path) const
|
||||||
{
|
{
|
||||||
// XXX: Doesn't make sense to lock this operation since it cannot
|
// XXX: Doesn't make sense to lock this operation since it cannot
|
||||||
// XXX: guarantee thread-safety because of its return type
|
// XXX: guarantee thread-safety because of its return type
|
||||||
for(auto& it: m_books) {
|
for(auto& it: mp_impl->m_books) {
|
||||||
auto& book = it.second;
|
auto& book = it.second;
|
||||||
if (book.getPath() == path)
|
if (book.getPath() == path)
|
||||||
return book;
|
return book;
|
||||||
|
@ -212,7 +238,7 @@ std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
return m_readers.at(id);
|
return mp_impl->m_readers.at(id);
|
||||||
} catch (std::out_of_range& e) {}
|
} catch (std::out_of_range& e) {}
|
||||||
|
|
||||||
const auto archive = getArchiveById(id);
|
const auto archive = getArchiveById(id);
|
||||||
|
@ -221,7 +247,7 @@ std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
|
||||||
|
|
||||||
const shared_ptr<Reader> reader(new Reader(archive, true));
|
const shared_ptr<Reader> reader(new Reader(archive, true));
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_readers[id] = reader;
|
mp_impl->m_readers[id] = reader;
|
||||||
return reader;
|
return reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +255,7 @@ std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
try {
|
try {
|
||||||
return m_archives.at(id);
|
return mp_impl->m_archives.at(id);
|
||||||
} catch (std::out_of_range& e) {}
|
} catch (std::out_of_range& e) {}
|
||||||
|
|
||||||
auto book = getBookById(id);
|
auto book = getBookById(id);
|
||||||
|
@ -237,7 +263,7 @@ std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
auto sptr = make_shared<zim::Archive>(book.getPath());
|
auto sptr = make_shared<zim::Archive>(book.getPath());
|
||||||
m_archives[id] = sptr;
|
mp_impl->m_archives[id] = sptr;
|
||||||
return sptr;
|
return sptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,7 +272,7 @@ unsigned int Library::getBookCount(const bool localBooks,
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
unsigned int result = 0;
|
unsigned int result = 0;
|
||||||
for (auto& pair: m_books) {
|
for (auto& pair: mp_impl->m_books) {
|
||||||
auto& book = pair.second;
|
auto& book = pair.second;
|
||||||
if ((!book.getPath().empty() && localBooks)
|
if ((!book.getPath().empty() && localBooks)
|
||||||
|| (book.getPath().empty() && remoteBooks)) {
|
|| (book.getPath().empty() && remoteBooks)) {
|
||||||
|
@ -284,7 +310,7 @@ Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) con
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
AttributeCounts propValueCounts;
|
AttributeCounts propValueCounts;
|
||||||
|
|
||||||
for (const auto& pair: m_books) {
|
for (const auto& pair: mp_impl->m_books) {
|
||||||
const auto& book = pair.second;
|
const auto& book = pair.second;
|
||||||
if (book.getOrigId().empty()) {
|
if (book.getOrigId().empty()) {
|
||||||
propValueCounts[(book.*p)()] += 1;
|
propValueCounts[(book.*p)()] += 1;
|
||||||
|
@ -317,7 +343,7 @@ std::vector<std::string> Library::getBooksCategories() const
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
std::set<std::string> categories;
|
std::set<std::string> categories;
|
||||||
|
|
||||||
for (const auto& pair: m_books) {
|
for (const auto& pair: mp_impl->m_books) {
|
||||||
const auto& book = pair.second;
|
const auto& book = pair.second;
|
||||||
const auto& c = book.getCategory();
|
const auto& c = book.getCategory();
|
||||||
if ( !c.empty() ) {
|
if ( !c.empty() ) {
|
||||||
|
@ -341,12 +367,12 @@ std::vector<std::string> Library::getBooksPublishers() const
|
||||||
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
|
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
|
||||||
{
|
{
|
||||||
if (!onlyValidBookmarks) {
|
if (!onlyValidBookmarks) {
|
||||||
return m_bookmarks;
|
return mp_impl->m_bookmarks;
|
||||||
}
|
}
|
||||||
std::vector<kiwix::Bookmark> validBookmarks;
|
std::vector<kiwix::Bookmark> validBookmarks;
|
||||||
auto booksId = getBooksIds();
|
auto booksId = getBooksIds();
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
for(auto& bookmark:m_bookmarks) {
|
for(auto& bookmark:mp_impl->m_bookmarks) {
|
||||||
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
|
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
|
||||||
validBookmarks.push_back(bookmark);
|
validBookmarks.push_back(bookmark);
|
||||||
}
|
}
|
||||||
|
@ -359,7 +385,7 @@ Library::BookIdCollection Library::getBooksIds() const
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
BookIdCollection bookIds;
|
BookIdCollection bookIds;
|
||||||
|
|
||||||
for (auto& pair: m_books) {
|
for (auto& pair: mp_impl->m_books) {
|
||||||
bookIds.push_back(pair.first);
|
bookIds.push_back(pair.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,7 +431,7 @@ void Library::updateBookDB(const Book& book)
|
||||||
|
|
||||||
doc.set_data(book.getId());
|
doc.set_data(book.getId());
|
||||||
|
|
||||||
m_bookDB->replace_document(idterm, doc);
|
mp_impl->m_bookDB->replace_document(idterm, doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
|
@ -538,9 +564,9 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
|
||||||
BookIdCollection bookIds;
|
BookIdCollection bookIds;
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
Xapian::Enquire enquire(*m_bookDB);
|
Xapian::Enquire enquire(*mp_impl->m_bookDB);
|
||||||
enquire.set_query(query);
|
enquire.set_query(query);
|
||||||
const auto results = enquire.get_mset(0, m_books.size());
|
const auto results = enquire.get_mset(0, mp_impl->m_books.size());
|
||||||
for ( auto it = results.begin(); it != results.end(); ++it ) {
|
for ( auto it = results.begin(); it != results.end(); ++it ) {
|
||||||
bookIds.push_back(it.get_document().get_data());
|
bookIds.push_back(it.get_document().get_data());
|
||||||
}
|
}
|
||||||
|
@ -554,7 +580,7 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
|
||||||
const auto preliminaryResult = filterViaBookDB(filter);
|
const auto preliminaryResult = filterViaBookDB(filter);
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
for(auto id : preliminaryResult) {
|
for(auto id : preliminaryResult) {
|
||||||
if(filter.accept(m_books.at(id))) {
|
if(filter.accept(mp_impl->m_books.at(id))) {
|
||||||
result.push_back(id);
|
result.push_back(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue