1 /*
2  * Copyright (C) 2009-2010 Geometer Plus <contact@geometerplus.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  * 02110-1301, USA.
18  */
19 
20 #include <ZLibrary.h>
21 #include <ZLFile.h>
22 #include <ZLDir.h>
23 #include <ZLLanguageUtil.h>
24 
25 #include "BooksDB.h"
26 #include "BooksDBQuery.h"
27 
28 #include "../../library/Book.h"
29 #include "../../library/Author.h"
30 #include "../../library/Tag.h"
31 
32 #include "../sqldb/implsqlite/SQLiteFactory.h"
33 
34 shared_ptr<BooksDB> BooksDB::ourInstance = 0;
35 
36 const std::string BooksDB::DATABASE_NAME = "books.db";
37 const std::string BooksDB::STATE_DATABASE_NAME = "state.db";
38 const std::string BooksDB::NET_DATABASE_NAME = "network.db";
39 
Instance()40 BooksDB &BooksDB::Instance() {
41 	if (ourInstance.isNull()) {
42 		ZLFile dir(databaseDirName());
43 		dir.directory(true);
44 		ZLFile file(databaseDirName() + ZLibrary::FileNameDelimiter + DATABASE_NAME);
45 		ourInstance = new BooksDB(file.physicalFilePath());
46 		ourInstance->initDatabase();
47 	}
48 	return *ourInstance;
49 }
50 
BooksDB(const std::string & path)51 BooksDB::BooksDB(const std::string &path) : SQLiteDataBase(path), myInitialized(false) {
52 	initCommands();
53 }
54 
~BooksDB()55 BooksDB::~BooksDB() {
56 }
57 
initDatabase()58 bool BooksDB::initDatabase() {
59 	if (isInitialized()) {
60 		return true;
61 	}
62 
63 	if (!open()) {
64 		return false;
65 	}
66 
67 	myInitialized = true;
68 
69 	ZLFile stateFile(databaseDirName() + ZLibrary::FileNameDelimiter + STATE_DATABASE_NAME);
70 	ZLFile netFile(databaseDirName() + ZLibrary::FileNameDelimiter + NET_DATABASE_NAME);
71 	shared_ptr<DBCommand> cmd = SQLiteFactory::createCommand(BooksDBQuery::PREINIT_DATABASE, connection(), "@stateFile", DBValue::DBTEXT, "@netFile", DBValue::DBTEXT);
72 	((DBTextValue&)*cmd->parameter("@stateFile").value()) = stateFile.physicalFilePath();
73 	((DBTextValue&)*cmd->parameter("@netFile").value()) = netFile.physicalFilePath();
74 	if (!cmd->execute()) {
75 		myInitialized = false;
76 		close();
77 		return false;
78 	}
79 
80 	shared_ptr<DBRunnable> runnable = new InitBooksDBRunnable(connection());
81 	if (!executeAsTransaction(*runnable)) {
82 		myInitialized = false;
83 		close();
84 		return false;
85 	}
86 
87 	return true;
88 }
89 
initCommands()90 void BooksDB::initCommands() {
91 	myLoadBook     = SQLiteFactory::createCommand(BooksDBQuery::LOAD_BOOK, connection(), "@file_id", DBValue::DBINT);
92 	myGetFileSize  = SQLiteFactory::createCommand(BooksDBQuery::GET_FILE_SIZE, connection(), "@file_id", DBValue::DBINT);
93 	mySetFileSize  = SQLiteFactory::createCommand(BooksDBQuery::SET_FILE_SIZE, connection(), "@file_id", DBValue::DBINT, "@size", DBValue::DBINT);
94 	myFindFileName = SQLiteFactory::createCommand(BooksDBQuery::FIND_FILE_NAME, connection(), "@file_id", DBValue::DBINT);
95 	myFindAuthorId    = SQLiteFactory::createCommand(BooksDBQuery::FIND_AUTHOR_ID, connection(), "@name", DBValue::DBTEXT, "@sort_key", DBValue::DBTEXT);
96 
97 	myLoadBooks = SQLiteFactory::createCommand(BooksDBQuery::LOAD_BOOKS, connection());
98 
99 	myLoadBookStateStack = SQLiteFactory::createCommand(BooksDBQuery::LOAD_BOOK_STATE_STACK, connection(), "@book_id", DBValue::DBINT);
100 
101 	myGetPalmType = SQLiteFactory::createCommand(BooksDBQuery::GET_PALM_TYPE, connection(), "@file_id", DBValue::DBINT);
102 	mySetPalmType = SQLiteFactory::createCommand(BooksDBQuery::SET_PALM_TYPE, connection(), "@file_id", DBValue::DBINT, "@type", DBValue::DBTEXT);
103 
104 	myLoadStackPos = SQLiteFactory::createCommand(BooksDBQuery::LOAD_STACK_POS, connection(), "@book_id", DBValue::DBINT);
105 	mySetStackPos = SQLiteFactory::createCommand(BooksDBQuery::SET_STACK_POS, connection(), "@book_id", DBValue::DBINT, "@stack_pos", DBValue::DBINT);
106 
107 	myLoadBookState = SQLiteFactory::createCommand(BooksDBQuery::LOAD_BOOK_STATE, connection(), "@book_id", DBValue::DBINT);
108 	mySetBookState = SQLiteFactory::createCommand(BooksDBQuery::SET_BOOK_STATE, connection(), "@book_id", DBValue::DBINT, "@paragraph", DBValue::DBINT, "@word", DBValue::DBINT, "@char", DBValue::DBINT);
109 
110 	myInsertBookList = SQLiteFactory::createCommand(BooksDBQuery::INSERT_BOOK_LIST, connection(), "@book_id", DBValue::DBINT);
111 	myDeleteBookList = SQLiteFactory::createCommand(BooksDBQuery::DELETE_BOOK_LIST, connection(), "@book_id", DBValue::DBINT);
112 	myCheckBookList = SQLiteFactory::createCommand(BooksDBQuery::CHECK_BOOK_LIST, connection(), "@book_id", DBValue::DBINT);
113 
114 	mySaveTableBook = new SaveTableBookRunnable(connection());
115 	mySaveAuthors = new SaveAuthorsRunnable(connection());
116 	mySaveSeries = new SaveSeriesRunnable(connection());
117 	mySaveTags = new SaveTagsRunnable(connection());
118 	mySaveBook = new SaveBookRunnable(*mySaveTableBook, *mySaveAuthors, *mySaveSeries, *mySaveTags);
119 	mySaveFileEntries = new SaveFileEntriesRunnable(connection());
120 
121 	myFindFileId = new FindFileIdRunnable(connection());
122 
123 	myLoadFileEntries = new LoadFileEntriesRunnable(connection());
124 
125 	myLoadRecentBooks = new LoadRecentBooksRunnable(connection());
126 	mySaveRecentBooks = new SaveRecentBooksRunnable(connection());
127 
128 	mySaveBookStateStack = new SaveBookStateStackRunnable(connection());
129 
130 	myDeleteBook = new DeleteBookRunnable(connection());
131 }
132 
clearDatabase()133 bool BooksDB::clearDatabase() {
134 	if (!isInitialized()) {
135 		return false;
136 	}
137 	shared_ptr<DBRunnable> runnable = new ClearBooksDBRunnable(connection());
138 	return executeAsTransaction(*runnable);
139 }
140 
loadBook(const std::string & fileName)141 shared_ptr<Book> BooksDB::loadBook(const std::string &fileName) {
142 	if (!isInitialized()) {
143 		return 0;
144 	}
145 
146 	myFindFileId->setFileName(fileName);
147 	if (!myFindFileId->run()) {
148 		return 0;
149 	}
150 	((DBIntValue&)*myLoadBook->parameter("@file_id").value()) = myFindFileId->fileId();
151 	shared_ptr<DBDataReader> reader = myLoadBook->executeReader();
152 
153 	if (reader.isNull() || !reader->next() ||
154 			reader->type(0) != DBValue::DBINT /* book_id */) {
155 		return 0;
156 	}
157 	const int bookId = reader->intValue(0);
158 
159 	shared_ptr<Book> book = Book::createBook(
160 		ZLFile(fileName), bookId,
161 		reader->textValue(1, Book::AutoEncoding),
162 		reader->textValue(2, ZLLanguageUtil::OtherLanguageCode),
163 		reader->textValue(3, std::string())
164 	);
165 
166 	loadSeries(*book);
167 	loadAuthors(*book);
168 	loadTags(*book);
169 
170 	return book;
171 }
172 
173 
saveBook(const shared_ptr<Book> book)174 bool BooksDB::saveBook(const shared_ptr<Book> book) {
175 	if (!isInitialized()) {
176 		return false;
177 	}
178 	mySaveBook->setBook(book);
179 	return executeAsTransaction(*mySaveBook);
180 }
181 
saveAuthors(const shared_ptr<Book> book)182 bool BooksDB::saveAuthors(const shared_ptr<Book> book) {
183 	if (!isInitialized()) {
184 		return false;
185 	}
186 	mySaveAuthors->setBook(book);
187 	return executeAsTransaction(*mySaveAuthors);
188 }
189 
saveSeries(const shared_ptr<Book> book)190 bool BooksDB::saveSeries(const shared_ptr<Book> book) {
191 	if (!isInitialized()) {
192 		return false;
193 	}
194 	mySaveSeries->setBook(book);
195 	return executeAsTransaction(*mySaveSeries);
196 }
197 
saveTags(const shared_ptr<Book> book)198 bool BooksDB::saveTags(const shared_ptr<Book> book) {
199 	if (!isInitialized()) {
200 		return false;
201 	}
202 	mySaveTags->setBook(book);
203 	return executeAsTransaction(*mySaveTags);
204 }
205 
getFileSize(const std::string & fileName)206 int BooksDB::getFileSize(const std::string &fileName) {
207 	if (!isInitialized()) {
208 		return -1;
209 	}
210 	myFindFileId->setFileName(fileName);
211 	if (!myFindFileId->run()) {
212 		return 0;
213 	}
214 	((DBIntValue&)*myGetFileSize->parameter("@file_id").value()) = myFindFileId->fileId();
215 
216 	shared_ptr<DBValue> fileSize = myGetFileSize->executeScalar();
217 
218 	if (fileSize.isNull()) {
219 		return -1;
220 	}
221 	if (fileSize->type() == DBValue::DBNULL) {
222 		return 0;
223 	}
224 	if (fileSize->type() != DBValue::DBINT) {
225 		return -1;
226 	}
227 	return ((DBIntValue&)*fileSize).value();
228 }
229 
setFileSize(const std::string & fileName,int size)230 bool BooksDB::setFileSize(const std::string &fileName, int size) {
231 	if (!isInitialized()) {
232 		return false;
233 	}
234 	myFindFileId->setFileName(fileName, true);
235 	if (!executeAsTransaction(*myFindFileId)) {
236 		return false;
237 	}
238 	((DBIntValue&)*mySetFileSize->parameter("@file_id").value()) = myFindFileId->fileId();
239 	((DBIntValue&)*mySetFileSize->parameter("@size").value()) = size;
240 	return mySetFileSize->execute();
241 }
242 
setEncoding(const Book & book,const std::string & encoding)243 bool BooksDB::setEncoding(const Book &book, const std::string &encoding) {
244 	if (!isInitialized()) {
245 		return false;
246 	}
247 
248 	shared_ptr<DBCommand> command = SQLiteFactory::createCommand(BooksDBQuery::SET_ENCODING, connection());
249 
250 	command->parameters().push_back(DBCommandParameter("@book_id", new DBIntValue(book.bookId())));
251 	command->parameters().push_back(DBCommandParameter("@encoding", new DBTextValue(encoding)));
252 
253 	return command->execute();
254 }
255 
loadFileEntries(const std::string & fileName,std::vector<std::string> & entries)256 bool BooksDB::loadFileEntries(const std::string &fileName, std::vector<std::string> &entries) {
257 	myLoadFileEntries->setFileName(fileName);
258 	if (!myLoadFileEntries->run()) {
259 		return false;
260 	}
261 	myLoadFileEntries->collectEntries(entries);
262 	return true;
263 }
264 
saveFileEntries(const std::string & fileName,const std::vector<std::string> & entries)265 bool BooksDB::saveFileEntries(const std::string &fileName, const std::vector<std::string> &entries) {
266 	if (!isInitialized()) {
267 		return false;
268 	}
269 	mySaveFileEntries->setEntries(fileName, entries);
270 	return executeAsTransaction(*mySaveFileEntries);
271 }
272 
loadRecentBooks(std::vector<std::string> & fileNames)273 bool BooksDB::loadRecentBooks(std::vector<std::string> &fileNames) {
274 	std::vector<int> fileIds;
275 	if (!myLoadRecentBooks->run()) {
276 		return false;
277 	}
278 	myLoadRecentBooks->collectFileIds(fileIds);
279 	for (std::vector<int>::const_iterator it = fileIds.begin(); it != fileIds.end(); ++it) {
280 		const int fileId = *it;
281 		const std::string fileName = getFileName(fileId);
282 		fileNames.push_back(fileName);
283 	}
284 	return true;
285 }
286 
saveRecentBooks(const BookList & books)287 bool BooksDB::saveRecentBooks(const BookList &books) {
288 	if (!isInitialized()) {
289 		return false;
290 	}
291 	mySaveRecentBooks->setBooks(books);
292 	return executeAsTransaction(*mySaveRecentBooks);
293 }
294 
getFileName(int fileId)295 std::string BooksDB::getFileName(int fileId) {
296 	std::string fileName;
297 	DBIntValue &findFileId = (DBIntValue&)*myFindFileName->parameter("@file_id").value();
298 	findFileId = fileId;
299 	while (true) {
300 		shared_ptr<DBDataReader> reader = myFindFileName->executeReader();
301 		if (reader.isNull() || !reader->next()) {
302 			return std::string();
303 		}
304 		const std::string namePart = reader->textValue(0, std::string());
305 		switch (reader->type(1)) { /* parent_id */
306 			default:
307 				return std::string();
308 			case DBValue::DBNULL:
309 				return namePart + ZLibrary::FileNameDelimiter + fileName;
310 			case DBValue::DBINT:
311 				if (fileName.empty()) {
312 					fileName = namePart;
313 				} else {
314 					fileName = namePart + BooksDBQuery::ArchiveEntryDelimiter + fileName;
315 				}
316 				findFileId = reader->intValue(1);
317 				break;
318 		}
319 	}
320 }
321 
loadBooks(BookList & books)322 bool BooksDB::loadBooks(BookList &books) {
323 	shared_ptr<DBDataReader> reader = myLoadBooks->executeReader();
324 
325 	books.clear();
326 	std::map<int,shared_ptr<Book> > bookMap;
327 
328 	while (reader->next()) {
329 		if (reader->type(0) != DBValue::DBINT || /* book_id */
330 				reader->type(4) != DBValue::DBINT) { /* file_id */
331 			return false;
332 		}
333 		const int bookId = reader->intValue(0);
334 		const int fileId = reader->intValue(4);
335 		const std::string fileName = getFileName(fileId);
336 
337 		shared_ptr<Book> book = Book::createBook(
338 			ZLFile(fileName),
339 			bookId,
340 			reader->textValue(1, Book::AutoEncoding),
341 			reader->textValue(2, ZLLanguageUtil::OtherLanguageCode),
342 			reader->textValue(3, std::string())
343 		);
344 		books.push_back(book);
345 		bookMap[bookId] = book;
346 	}
347 
348 	loadSeries(bookMap);
349 	loadAuthors(bookMap);
350 	loadTags(bookMap);
351 
352 	return true;
353 }
354 
loadBookStateStack(const Book & book,std::deque<ReadingState> & stack)355 bool BooksDB::loadBookStateStack(const Book &book, std::deque<ReadingState> &stack) {
356 	if (book.bookId() == 0) {
357 		return false;
358 	}
359 	((DBIntValue&)*myLoadBookStateStack->parameter("@book_id").value()) = book.bookId();
360 	shared_ptr<DBDataReader> reader = myLoadBookStateStack->executeReader();
361 	if (reader.isNull()) {
362 		return false;
363 	}
364 	while (reader->next()) {
365 		if (reader->type(0) != DBValue::DBINT    /* paragraph */
366 			|| reader->type(1) != DBValue::DBINT /* word      */
367 			|| reader->type(2) != DBValue::DBINT /* char      */) {
368 			return false;
369 		}
370 		const int paragraph = reader->intValue(0);
371 		const int word = reader->intValue(1);
372 		const int character = reader->intValue(2);
373 		stack.push_back(ReadingState(paragraph, word, character));
374 	}
375 	return true;
376 }
377 
saveBookStateStack(const Book & book,const std::deque<ReadingState> & stack)378 bool BooksDB::saveBookStateStack(const Book &book, const std::deque<ReadingState> &stack) {
379 	if (!isInitialized() || book.bookId() == 0) {
380 		return false;
381 	}
382 	mySaveBookStateStack->setState(book.bookId(), stack);
383 	return executeAsTransaction(*mySaveBookStateStack);
384 }
385 
386 
removeBook(const Book & book)387 bool BooksDB::removeBook(const Book &book) {
388 	if (!isInitialized() || book.bookId() == 0) {
389 		return false;
390 	}
391 	myDeleteBook->setFileName(book.file().path());
392 	return executeAsTransaction(*myDeleteBook);
393 }
394 
getPalmType(const std::string & fileName)395 std::string BooksDB::getPalmType(const std::string &fileName) {
396 	if (!isInitialized()) {
397 		return "";
398 	}
399 	myFindFileId->setFileName(fileName);
400 	if (!myFindFileId->run()) {
401 		return "";
402 	}
403 	((DBIntValue&)*myGetPalmType->parameter("@file_id").value()) = myFindFileId->fileId();
404 	shared_ptr<DBValue> value = myGetPalmType->executeScalar();
405 	if (value.isNull() || value->type() != DBValue::DBTEXT) {
406 		return "";
407 	}
408 	return ((DBTextValue&)*value).value();
409 }
410 
setPalmType(const std::string & fileName,const std::string & type)411 bool BooksDB::setPalmType(const std::string &fileName, const std::string &type) {
412 	if (!isInitialized()) {
413 		return false;
414 	}
415 	myFindFileId->setFileName(fileName, true);
416 	if (!myFindFileId->run()) {
417 		return "";
418 	}
419 	((DBIntValue&)*mySetPalmType->parameter("@file_id").value()) = myFindFileId->fileId();
420 	((DBTextValue&)*mySetPalmType->parameter("@type").value()) = type;
421 	return mySetPalmType->execute();
422 }
423 
getNetFile(const std::string & url)424 std::string BooksDB::getNetFile(const std::string &url) {
425 	static shared_ptr<DBCommand> command = SQLiteFactory::createCommand(
426 		"SELECT file_id FROM NetFiles WHERE url = @url;",
427 		connection(), "@url", DBValue::DBTEXT
428 	);
429 
430 	((DBTextValue&)*command->parameter("@url").value()) = url;
431 	shared_ptr<DBValue> value = command->executeScalar();
432 	if (value.isNull() || value->type() != DBValue::DBINT) {
433 		return std::string();
434 	}
435 	return getFileName(((DBIntValue&)*value).value());
436 }
437 
setNetFile(const std::string & url,const std::string & fileName)438 bool BooksDB::setNetFile(const std::string &url, const std::string &fileName) {
439 	static shared_ptr<DBCommand> command = SQLiteFactory::createCommand(
440 		"INSERT OR REPLACE INTO NetFiles (url, file_id) VALUES (@url, @file_id);",
441 		connection(), "@file_id", DBValue::DBINT, "@url", DBValue::DBTEXT
442 	);
443 
444 	myFindFileId->setFileName(fileName, true);
445 	if (!myFindFileId->run()) {
446 		return false;
447 	}
448 	((DBIntValue&)*command->parameter("@file_id").value()) = myFindFileId->fileId();
449 	((DBTextValue&)*command->parameter("@url").value()) = url;
450 	return command->execute();
451 }
452 
unsetNetFile(const std::string & url)453 bool BooksDB::unsetNetFile(const std::string &url) {
454 	static shared_ptr<DBCommand> command = SQLiteFactory::createCommand(
455 		"SELECT file_id FROM NetFiles WHERE url = @url;",
456 		connection(), "@url", DBValue::DBTEXT
457 	);
458 
459 	((DBTextValue&)*command->parameter("@url").value()) = url;
460 	return command->execute();
461 }
462 
loadBookState(const Book & book,ReadingState & state)463 bool BooksDB::loadBookState(const Book &book, ReadingState &state) {
464 	state.Paragraph = state.Word = state.Character = 0;
465 	if (book.bookId() == 0) {
466 		return false;
467 	}
468 	((DBIntValue&)*myLoadBookState->parameter("@book_id").value()) = book.bookId();
469 	shared_ptr<DBDataReader> reader = myLoadBookState->executeReader();
470 	if (reader.isNull()) {
471 		return false;
472 	}
473 	if (!reader->next()
474 		|| reader->type(0) != DBValue::DBINT /* paragraph */
475 		|| reader->type(1) != DBValue::DBINT /* word      */
476 		|| reader->type(2) != DBValue::DBINT /* char      */) {
477 		return false;
478 	}
479 	state.Paragraph = reader->intValue(0);
480 	state.Word = reader->intValue(1);
481 	state.Character = reader->intValue(2);
482 	return true;
483 }
484 
setBookState(const Book & book,const ReadingState & state)485 bool BooksDB::setBookState(const Book &book, const ReadingState &state) {
486 	if (book.bookId() == 0) {
487 		return false;
488 	}
489 	((DBIntValue&)*mySetBookState->parameter("@book_id").value()) = book.bookId();
490 	((DBIntValue&)*mySetBookState->parameter("@paragraph").value()) = state.Paragraph;
491 	((DBIntValue&)*mySetBookState->parameter("@word").value()) = state.Word;
492 	((DBIntValue&)*mySetBookState->parameter("@char").value()) = state.Character;
493 	return mySetBookState->execute();
494 }
495 
loadStackPos(const Book & book)496 int BooksDB::loadStackPos(const Book &book) {
497 	if (book.bookId() == 0) {
498 		return 0;
499 	}
500 	((DBIntValue&)*myLoadStackPos->parameter("@book_id").value()) = book.bookId();
501 	shared_ptr<DBValue> stackPosValue = myLoadStackPos->executeScalar();
502 	if (stackPosValue.isNull()
503 		|| stackPosValue->type() != DBValue::DBINT) {
504 		return 0;
505 	}
506 	return ((DBIntValue&)*stackPosValue).value();
507 }
508 
setStackPos(const Book & book,int stackPos)509 bool BooksDB::setStackPos(const Book &book, int stackPos) {
510 	if (book.bookId() == 0) {
511 		return false;
512 	}
513 	((DBIntValue&)*mySetStackPos->parameter("@book_id").value()) = book.bookId();
514 	((DBIntValue&)*mySetStackPos->parameter("@stack_pos").value()) = stackPos;
515 	return mySetStackPos->execute();
516 }
517 
insertIntoBookList(const Book & book)518 bool BooksDB::insertIntoBookList(const Book &book) {
519 	if (book.bookId() == 0) {
520 		return false;
521 	}
522 	((DBIntValue&)*myInsertBookList->parameter("@book_id").value()) = book.bookId();
523 	return myInsertBookList->execute();
524 }
525 
deleteFromBookList(const Book & book)526 bool BooksDB::deleteFromBookList(const Book &book) {
527 	if (book.bookId() == 0) {
528 		return false;
529 	}
530 	((DBIntValue&)*myDeleteBookList->parameter("@book_id").value()) = book.bookId();
531 	return myDeleteBookList->execute();
532 }
533 
checkBookList(const Book & book)534 bool BooksDB::checkBookList(const Book &book) {
535 	if (book.bookId() == 0) {
536 		return false;
537 	}
538 	((DBIntValue&)*myCheckBookList->parameter("@book_id").value()) = book.bookId();
539 	shared_ptr<DBValue> res = myCheckBookList->executeScalar();
540 	if (res.isNull() || res->type() != DBValue::DBINT) {
541 		return false;
542 	}
543 	const int checkRes = ((DBIntValue&)*res).value();
544 	return checkRes > 0;
545 }
546 
547