diff --git a/src/org/geometerplus/fbreader/book/Book.java b/src/org/geometerplus/fbreader/book/Book.java index 8e72222a1..8e503ea67 100644 --- a/src/org/geometerplus/fbreader/book/Book.java +++ b/src/org/geometerplus/fbreader/book/Book.java @@ -41,11 +41,13 @@ import org.geometerplus.fbreader.library.Library; public class Book { public static Book getById(long bookId) { - final Book book = BooksDatabase.Instance().loadBook(bookId); + final BooksDatabase database = BooksDatabase.Instance(); + + final Book book = database.loadBook(bookId); if (book == null) { return null; } - book.loadLists(); + book.loadLists(database); final ZLFile bookFile = book.File; final ZLPhysicalFile physicalFile = bookFile.getPhysicalFile(); @@ -56,7 +58,7 @@ public class Book { return null; } - FileInfoSet fileInfos = new FileInfoSet(BooksDatabase.Instance(), physicalFile); + FileInfoSet fileInfos = new FileInfoSet(database, physicalFile); if (fileInfos.check(physicalFile, physicalFile != bookFile)) { return book; } @@ -80,11 +82,12 @@ public class Book { return null; } - final FileInfoSet fileInfos = new FileInfoSet(BooksDatabase.Instance(), bookFile); + final BooksDatabase database = BooksDatabase.Instance(); + final FileInfoSet fileInfos = new FileInfoSet(database, bookFile); - Book book = BooksDatabase.Instance().loadBookByFile(fileInfos.getId(bookFile), bookFile); + Book book = database.loadBookByFile(fileInfos.getId(bookFile), bookFile); if (book != null) { - book.loadLists(); + book.loadLists(database); } if (book != null && fileInfos.check(physicalFile, physicalFile != bookFile)) { @@ -197,8 +200,7 @@ public class Book { } } - private void loadLists() { - final BooksDatabase database = BooksDatabase.Instance(); + void loadLists(BooksDatabase database) { myAuthors = database.loadAuthors(myId); myTags = database.loadTags(myId); mySeriesInfo = database.loadSeriesInfo(myId); @@ -405,10 +407,14 @@ public class Book { } public boolean save() { - if (myIsSaved) { + return save(BooksDatabase.Instance(), false); + } + + boolean save(final BooksDatabase database, boolean force) { + if (!force && myIsSaved) { return false; } - final BooksDatabase database = BooksDatabase.Instance(); + database.executeAsATransaction(new Runnable() { public void run() { if (myId >= 0) { @@ -416,7 +422,11 @@ public class Book { database.updateBookInfo(myId, fileInfos.getId(File), myEncoding, myLanguage, myTitle); } else { myId = database.insertBookInfo(File, myEncoding, myLanguage, myTitle); - storeAllVisitedHyperinks(); + if (myId != -1 && myVisitedHyperlinks != null) { + for (String linkId : myVisitedHyperlinks) { + database.addVisitedHyperlink(myId, linkId); + } + } } long index = 0; @@ -447,34 +457,34 @@ public class Book { } private Set myVisitedHyperlinks; - private void initHyperlinkSet() { + private void initHyperlinkSet(BooksDatabase database) { if (myVisitedHyperlinks == null) { myVisitedHyperlinks = new TreeSet(); if (myId != -1) { - myVisitedHyperlinks.addAll(BooksDatabase.Instance().loadVisitedHyperlinks(myId)); + myVisitedHyperlinks.addAll(database.loadVisitedHyperlinks(myId)); } } } public boolean isHyperlinkVisited(String linkId) { - initHyperlinkSet(); + return isHyperlinkVisited(BooksDatabase.Instance(), linkId); + } + + boolean isHyperlinkVisited(BooksDatabase database, String linkId) { + initHyperlinkSet(database); return myVisitedHyperlinks.contains(linkId); } public void markHyperlinkAsVisited(String linkId) { - initHyperlinkSet(); + markHyperlinkAsVisited(BooksDatabase.Instance(), linkId); + } + + void markHyperlinkAsVisited(BooksDatabase database, String linkId) { + initHyperlinkSet(database); if (!myVisitedHyperlinks.contains(linkId)) { myVisitedHyperlinks.add(linkId); if (myId != -1) { - BooksDatabase.Instance().addVisitedHyperlink(myId, linkId); - } - } - } - - private void storeAllVisitedHyperinks() { - if (myId != -1 && myVisitedHyperlinks != null) { - for (String linkId : myVisitedHyperlinks) { - BooksDatabase.Instance().addVisitedHyperlink(myId, linkId); + database.addVisitedHyperlink(myId, linkId); } } } diff --git a/src/org/geometerplus/fbreader/book/BookCollection.java b/src/org/geometerplus/fbreader/book/BookCollection.java index 1732a4c8f..5c1675a06 100644 --- a/src/org/geometerplus/fbreader/book/BookCollection.java +++ b/src/org/geometerplus/fbreader/book/BookCollection.java @@ -29,6 +29,479 @@ import org.geometerplus.zlibrary.text.view.ZLTextPosition; import org.geometerplus.fbreader.Paths; import org.geometerplus.fbreader.bookmodel.BookReadingException; -public abstract class BookCollection extends AbstractBookCollection { +public class BookCollection extends AbstractBookCollection { + private final BooksDatabase myDatabase; + private final Map myBooksByFile = + Collections.synchronizedMap(new LinkedHashMap()); + private final Map myBooksById = + Collections.synchronizedMap(new HashMap()); + private static enum BuildStatus { + NotStarted, + Started, + Finished + }; + private volatile BuildStatus myBuildStatus = BuildStatus.NotStarted; + + public BookCollection(BooksDatabase db) { + myDatabase = db; + } + + public int size() { + return myBooksByFile.size(); + } + + public Book getBookByFile(ZLFile bookFile) { + if (bookFile == null) { + return null; + } + + Book book = myBooksByFile.get(bookFile); + if (book != null) { + return book; + } + + final ZLPhysicalFile physicalFile = bookFile.getPhysicalFile(); + if (physicalFile != null && !physicalFile.exists()) { + return null; + } + + final FileInfoSet fileInfos = new FileInfoSet(myDatabase, bookFile); + + book = myDatabase.loadBookByFile(fileInfos.getId(bookFile), bookFile); + if (book != null) { + book.loadLists(myDatabase); + } + + if (book != null && fileInfos.check(physicalFile, physicalFile != bookFile)) { + addBook(book, false); + return book; + } + fileInfos.save(); + + try { + if (book == null) { + book = new Book(bookFile); + } else { + book.readMetaInfo(); + } + } catch (BookReadingException e) { + return null; + } + + saveBook(book, false); + addBook(book, false); + return book; + } + + public Book getBookById(long id) { + Book book = myBooksById.get(id); + if (book != null) { + return book; + } + + book = myDatabase.loadBook(id); + if (book == null) { + return null; + } + book.loadLists(myDatabase); + + final ZLFile bookFile = book.File; + final ZLPhysicalFile physicalFile = bookFile.getPhysicalFile(); + if (physicalFile == null) { + addBook(book, false); + return book; + } + if (!physicalFile.exists()) { + return null; + } + + FileInfoSet fileInfos = new FileInfoSet(myDatabase, physicalFile); + if (fileInfos.check(physicalFile, physicalFile != bookFile)) { + addBook(book, false); + return book; + } + fileInfos.save(); + + try { + book.readMetaInfo(); + addBook(book, false); + return book; + } catch (BookReadingException e) { + return null; + } + } + + private void addBook(Book book, boolean force) { + if (book == null) { + return; + } + + synchronized (myBooksByFile) { + Listener.BookEvent event = Listener.BookEvent.Added; + if (myBooksByFile.containsKey(book.File)) { + if (!force) { + return; + } + event = Listener.BookEvent.Updated; + } + myBooksByFile.put(book.File, book); + myBooksById.put(book.getId(), book); + fireBookEvent(event, book); + } + } + + public boolean saveBook(Book book, boolean force) { + if (book == null) { + return false; + } + + addBook(book, true); + return book.save(myDatabase, force); + } + + public void removeBook(Book book, boolean deleteFromDisk) { + synchronized (myBooksByFile) { + myBooksByFile.remove(book.File); + myBooksById.remove(book.getId()); + + final List ids = myDatabase.loadRecentBookIds(); + if (ids.remove(book.getId())) { + myDatabase.saveRecentBookIds(ids); + } + if (deleteFromDisk) { + book.File.getPhysicalFile().delete(); + } + } + fireBookEvent(Listener.BookEvent.Removed, book); + } + + public List books() { + synchronized (myBooksByFile) { + return new ArrayList(myBooksByFile.values()); + } + } + + public List books(String pattern) { + if (pattern == null || pattern.length() == 0) { + return Collections.emptyList(); + } + + final LinkedList filtered = new LinkedList(); + for (Book b : books()) { + if (b.matches(pattern)) { + filtered.add(b); + } + } + return filtered; + } + + public List recentBooks() { + return books(myDatabase.loadRecentBookIds()); + } + + public List favorites() { + return books(myDatabase.loadFavoriteIds()); + } + + private List books(List ids) { + final List bookList = new ArrayList(ids.size()); + for (long id : ids) { + final Book book = getBookById(id); + if (book != null) { + bookList.add(book); + } + } + return bookList; + } + + public Book getRecentBook(int index) { + List recentIds = myDatabase.loadRecentBookIds(); + return recentIds.size() > index ? getBookById(recentIds.get(index)) : null; + } + + public void addBookToRecentList(Book book) { + final List ids = myDatabase.loadRecentBookIds(); + final Long bookId = book.getId(); + ids.remove(bookId); + ids.add(0, bookId); + if (ids.size() > 12) { + ids.remove(12); + } + myDatabase.saveRecentBookIds(ids); + } + + public void setBookFavorite(Book book, boolean favorite) { + if (favorite) { + myDatabase.addToFavorites(book.getId()); + } else { + myDatabase.removeFromFavorites(book.getId()); + } + fireBookEvent(Listener.BookEvent.Updated, book); + } + + public synchronized void startBuild() { + if (myBuildStatus != BuildStatus.NotStarted) { + fireBuildEvent(Listener.BuildEvent.NotStarted); + return; + } + myBuildStatus = BuildStatus.Started; + + final Thread builder = new Thread("Library.build") { + public void run() { + try { + fireBuildEvent(Listener.BuildEvent.Started); + build(); + fireBuildEvent(Listener.BuildEvent.Succeeded); + } catch (Throwable t) { + fireBuildEvent(Listener.BuildEvent.Failed); + } finally { + fireBuildEvent(Listener.BuildEvent.Completed); + myBuildStatus = BuildStatus.Finished; + } + } + }; + builder.setPriority(Thread.MIN_PRIORITY); + builder.start(); + } + + private void build() { + // Step 0: get database books marked as "existing" + final FileInfoSet fileInfos = new FileInfoSet(myDatabase); + final Map savedBooksByFileId = myDatabase.loadBooks(fileInfos, true); + final Map savedBooksByBookId = new HashMap(); + for (Book b : savedBooksByFileId.values()) { + savedBooksByBookId.put(b.getId(), b); + } + + // Step 1: set myDoGroupTitlesByFirstLetter value + //if (savedBooksByFileId.size() > 10) { + // final HashSet letterSet = new HashSet(); + // for (Book book : savedBooksByFileId.values()) { + // final String letter = TitleTree.firstTitleLetter(book); + // if (letter != null) { + // letterSet.add(letter); + // } + // } + // myDoGroupTitlesByFirstLetter = savedBooksByFileId.values().size() > letterSet.size() * 5 / 4; + //} + + // Step 2: check if files corresponding to "existing" books really exists; + // add books to library if yes (and reload book info if needed); + // remove from recent/favorites list if no; + // collect newly "orphaned" books + final Set orphanedBooks = new HashSet(); + final Set physicalFiles = new HashSet(); + int count = 0; + for (Book book : savedBooksByFileId.values()) { + synchronized (this) { + final ZLPhysicalFile file = book.File.getPhysicalFile(); + if (file != null) { + physicalFiles.add(file); + } + if (file != book.File && file != null && file.getPath().endsWith(".epub")) { + continue; + } + if (book.File.exists()) { + boolean doAdd = true; + if (file == null) { + continue; + } + if (!fileInfos.check(file, true)) { + try { + book.readMetaInfo(); + saveBook(book, false); + } catch (BookReadingException e) { + doAdd = false; + } + file.setCached(false); + } + if (doAdd) { + addBook(book, false); + } + } else { + orphanedBooks.add(book); + } + } + } + myDatabase.setExistingFlag(orphanedBooks, false); + + // Step 3: collect books from physical files; add new, update already added, + // unmark orphaned as existing again, collect newly added + final Map orphanedBooksByFileId = myDatabase.loadBooks(fileInfos, false); + final Set newBooks = new HashSet(); + + final List physicalFilesList = collectPhysicalFiles(); + for (ZLPhysicalFile file : physicalFilesList) { + if (physicalFiles.contains(file)) { + continue; + } + collectBooks( + file, fileInfos, + savedBooksByFileId, orphanedBooksByFileId, + newBooks, + !fileInfos.check(file, true) + ); + file.setCached(false); + } + + // Step 4: add help file + try { + final ZLFile helpFile = getHelpFile(); + Book helpBook = savedBooksByFileId.get(fileInfos.getId(helpFile)); + if (helpBook == null) { + helpBook = new Book(helpFile); + } + addBook(helpBook, false); + } catch (BookReadingException e) { + // that's impossible + e.printStackTrace(); + } + + // Step 5: save changes into database + fileInfos.save(); + + myDatabase.executeAsATransaction(new Runnable() { + public void run() { + for (Book book : newBooks) { + saveBook(book, false); + addBook(book, false); + } + } + }); + myDatabase.setExistingFlag(newBooks, true); + } + + public List bookDirectories() { + return Collections.singletonList(Paths.BooksDirectoryOption().getValue()); + } + + private List collectPhysicalFiles() { + final Queue dirQueue = new LinkedList(); + final HashSet dirSet = new HashSet(); + final LinkedList fileList = new LinkedList(); + + for (String path : bookDirectories()) { + dirQueue.offer(new ZLPhysicalFile(new File(path))); + } + + while (!dirQueue.isEmpty()) { + for (ZLFile file : dirQueue.poll().children()) { + if (file.isDirectory()) { + if (!dirSet.contains(file)) { + dirQueue.add(file); + dirSet.add(file); + } + } else { + file.setCached(true); + fileList.add((ZLPhysicalFile)file); + } + } + } + return fileList; + } + + private void collectBooks( + ZLFile file, FileInfoSet fileInfos, + Map savedBooksByFileId, Map orphanedBooksByFileId, + Set newBooks, + boolean doReadMetaInfo + ) { + final long fileId = fileInfos.getId(file); + if (savedBooksByFileId.get(fileId) != null) { + return; + } + + try { + final Book book = orphanedBooksByFileId.get(fileId); + if (book != null) { + if (doReadMetaInfo) { + book.readMetaInfo(); + } + newBooks.add(book); + return; + } + } catch (BookReadingException e) { + // ignore + } + + try { + final Book book = new Book(file); + newBooks.add(book); + return; + } catch (BookReadingException e) { + // ignore + } + + if (file.isArchive()) { + for (ZLFile entry : fileInfos.archiveEntries(file)) { + collectBooks( + entry, fileInfos, + savedBooksByFileId, orphanedBooksByFileId, + newBooks, + doReadMetaInfo + ); + } + } + } + + public List allBookmarks() { + return myDatabase.loadAllVisibleBookmarks(); + } + + public List invisibleBookmarks(Book book) { + final List list = myDatabase.loadBookmarks(book.getId(), false); + Collections.sort(list, new Bookmark.ByTimeComparator()); + return list; + } + + public void saveBookmark(Bookmark bookmark) { + if (bookmark != null) { + bookmark.setId(myDatabase.saveBookmark(bookmark)); + } + } + + public void deleteBookmark(Bookmark bookmark) { + if (bookmark != null && bookmark.getId() != -1) { + myDatabase.deleteBookmark(bookmark); + } + } + + public static ZLResourceFile getHelpFile() { + final Locale locale = Locale.getDefault(); + + ZLResourceFile file = ZLResourceFile.createResourceFile( + "data/help/MiniHelp." + locale.getLanguage() + "_" + locale.getCountry() + ".fb2" + ); + if (file.exists()) { + return file; + } + + file = ZLResourceFile.createResourceFile( + "data/help/MiniHelp." + locale.getLanguage() + ".fb2" + ); + if (file.exists()) { + return file; + } + + return ZLResourceFile.createResourceFile("data/help/MiniHelp.en.fb2"); + } + + public ZLTextPosition getStoredPosition(long bookId) { + return myDatabase.getStoredPosition(bookId); + } + + public void storePosition(long bookId, ZLTextPosition position) { + if (bookId != -1) { + myDatabase.storePosition(bookId, position); + } + } + + public boolean isHyperlinkVisited(Book book, String linkId) { + return book.isHyperlinkVisited(myDatabase, linkId); + } + + public void markHyperlinkAsVisited(Book book, String linkId) { + book.markHyperlinkAsVisited(myDatabase, linkId); + } } diff --git a/src/org/geometerplus/fbreader/book/Bookmark.java b/src/org/geometerplus/fbreader/book/Bookmark.java index 5aff6b99e..063c6b1bb 100644 --- a/src/org/geometerplus/fbreader/book/Bookmark.java +++ b/src/org/geometerplus/fbreader/book/Bookmark.java @@ -239,4 +239,8 @@ mainLoop: } return builder.toString(); } + + void setId(long id) { + myId = id; + } }