/*
* Copyright (C) 2007-2013 Geometer Plus <contact@geometerplus.com>
*
* 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 WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
package org.geometerplus.fbreader.book;
import java.lang.ref.WeakReference;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.geometerplus.fbreader.Paths;
import org.geometerplus.fbreader.bookmodel.BookReadingException;
import org.geometerplus.fbreader.formats.FormatPlugin;
import org.geometerplus.fbreader.formats.PluginCollection;
import org.geometerplus.fbreader.sort.TitledEntity;
import org.geometerplus.zlibrary.core.filesystem.ZLFile;
import org.geometerplus.zlibrary.core.image.ZLImage;
import org.geometerplus.zlibrary.core.resources.ZLResource;
import org.geometerplus.zlibrary.core.util.MiscUtil;
public class Book extends TitledEntity {
public static final String FAVORITE_LABEL = "favorite";
public static final String READ_LABEL = "read";
public final ZLFile File;
private volatile long myId;
private volatile String myEncoding;
private volatile String myLanguage;
private volatile List<Author> myAuthors;
private volatile List<Tag> myTags;
private volatile List<String> myLabels;
private volatile SeriesInfo mySeriesInfo;
private volatile List<UID> myUids;
public volatile boolean HasBookmark;
private volatile boolean myIsSaved;
private static final WeakReference<ZLImage> NULL_IMAGE = new WeakReference<ZLImage>(null);
private WeakReference<ZLImage> myCover;
Book(long id, ZLFile file, String title, String encoding, String language) {
super(title);
myId = id;
File = file;
myEncoding = encoding;
myLanguage = language;
myIsSaved = true;
}
public Book(ZLFile file) throws BookReadingException {
super(null);
myId = -1;
final FormatPlugin plugin = getPlugin(file);
File = plugin.realBookFile(file);
readMetaInfo(plugin);
myIsSaved = false;
}
public void updateFrom(Book book) {
if (myId != book.myId) {
return;
}
setTitle(book.getTitle());
myEncoding = book.myEncoding;
myLanguage = book.myLanguage;
myAuthors = book.myAuthors != null ? new ArrayList<Author>(book.myAuthors) : null;
myTags = book.myTags != null ? new ArrayList<Tag>(book.myTags) : null;
myLabels = book.myLabels != null ? new ArrayList<String>(book.myLabels) : null;
mySeriesInfo = book.mySeriesInfo;
HasBookmark = book.HasBookmark;
}
public void reloadInfoFromFile() {
try {
readMetaInfo();
} catch (BookReadingException e) {
// ignore
}
}
private static FormatPlugin getPlugin(ZLFile file) throws BookReadingException {
final FormatPlugin plugin = PluginCollection.Instance().getPlugin(file);
if (plugin == null) {
throw new BookReadingException("pluginNotFound", file);
}
return plugin;
}
public FormatPlugin getPlugin() throws BookReadingException {
return getPlugin(File);
}
void readMetaInfo() throws BookReadingException {
readMetaInfo(getPlugin());
}
private void readMetaInfo(FormatPlugin plugin) throws BookReadingException {
myEncoding = null;
myLanguage = null;
setTitle(null);
myAuthors = null;
myTags = null;
mySeriesInfo = null;
myUids = null;
myIsSaved = false;
plugin.readMetaInfo(this);
if (myUids == null || myUids.isEmpty()) {
plugin.readUids(this);
}
if (isTitleEmpty()) {
final String fileName = File.getShortName();
final int index = fileName.lastIndexOf('.');
setTitle(index > 0 ? fileName.substring(0, index) : fileName);
}
final String demoPathPrefix = Paths.mainBookDirectory() + "/Demos/";
if (File.getPath().startsWith(demoPathPrefix)) {
final String demoTag = ZLResource.resource("library").getResource("demo").getValue();
setTitle(getTitle() + " (" + demoTag + ")");
addTag(demoTag);
}
}
void loadLists(BooksDatabase database) {
myAuthors = database.listAuthors(myId);
myTags = database.listTags(myId);
myLabels = database.listLabels(myId);
mySeriesInfo = database.getSeriesInfo(myId);
myUids = database.listUids(myId);
HasBookmark = database.hasVisibleBookmark(myId);
myIsSaved = true;
if (myUids == null || myUids.isEmpty()) {
try {
final FormatPlugin plugin = getPlugin();
if (plugin != null) {
plugin.readUids(this);
save(database, false);
}
} catch (BookReadingException e) {
}
}
}
public List<Author> authors() {
return (myAuthors != null) ? Collections.unmodifiableList(myAuthors) : Collections.<Author>emptyList();
}
void addAuthorWithNoCheck(Author author) {
if (myAuthors == null) {
myAuthors = new ArrayList<Author>();
}
myAuthors.add(author);
}
public void removeAllAuthors() {
if (myAuthors != null) {
myAuthors = null;
myIsSaved = false;
}
}
public void addAuthor(Author author) {
if (author == null) {
return;
}
if (myAuthors == null) {
myAuthors = new ArrayList<Author>();
myAuthors.add(author);
myIsSaved = false;
} else if (!myAuthors.contains(author)) {
myAuthors.add(author);
myIsSaved = false;
}
}
public void addAuthor(String name) {
addAuthor(name, "");
}
public void addAuthor(String name, String sortKey) {
String strippedName = name;
strippedName.trim();
if (strippedName.length() == 0) {
return;
}
String strippedKey = sortKey;
strippedKey.trim();
if (strippedKey.length() == 0) {
int index = strippedName.lastIndexOf(' ');
if (index == -1) {
strippedKey = strippedName;
} else {
strippedKey = strippedName.substring(index + 1);
while ((index >= 0) && (strippedName.charAt(index) == ' ')) {
--index;
}
strippedName = strippedName.substring(0, index + 1) + ' ' + strippedKey;
}
}
addAuthor(new Author(strippedName, strippedKey));
}
public long getId() {
return myId;
}
@Override
public void setTitle(String title) {
if (!getTitle().equals(title)) {
super.setTitle(title);
myIsSaved = false;
}
}
public SeriesInfo getSeriesInfo() {
return mySeriesInfo;
}
void setSeriesInfoWithNoCheck(String name, String index) {
mySeriesInfo = SeriesInfo.createSeriesInfo(name, index);
}
public void setSeriesInfo(String name, String index) {
setSeriesInfo(name, SeriesInfo.createIndex(index));
}
public void setSeriesInfo(String name, BigDecimal index) {
if (mySeriesInfo == null) {
if (name != null) {
mySeriesInfo = new SeriesInfo(name, index);
myIsSaved = false;
}
} else if (name == null) {
mySeriesInfo = null;
myIsSaved = false;
} else if (!name.equals(mySeriesInfo.Series.getTitle()) || mySeriesInfo.Index != index) {
mySeriesInfo = new SeriesInfo(name, index);
myIsSaved = false;
}
}
@Override
public String getLanguage() {
return myLanguage;
}
public void setLanguage(String language) {
if (!MiscUtil.equals(myLanguage, language)) {
myLanguage = language;
resetSortKey();
myIsSaved = false;
}
}
public String getEncoding() {
if (myEncoding == null) {
try {
getPlugin().detectLanguageAndEncoding(this);
} catch (BookReadingException e) {
}
if (myEncoding == null) {
setEncoding("utf-8");
}
}
return myEncoding;
}
public String getEncodingNoDetection() {
return myEncoding;
}
public void setEncoding(String encoding) {
if (!MiscUtil.equals(myEncoding, encoding)) {
myEncoding = encoding;
myIsSaved = false;
}
}
public List<Tag> tags() {
return myTags != null ? Collections.unmodifiableList(myTags) : Collections.<Tag>emptyList();
}
void addTagWithNoCheck(Tag tag) {
if (myTags == null) {
myTags = new ArrayList<Tag>();
}
myTags.add(tag);
}
public void removeAllTags() {
if (myTags != null) {
myTags = null;
myIsSaved = false;
}
}
public void addTag(Tag tag) {
if (tag != null) {
if (myTags == null) {
myTags = new ArrayList<Tag>();
}
if (!myTags.contains(tag)) {
myTags.add(tag);
myIsSaved = false;
}
}
}
public void addTag(String tagName) {
addTag(Tag.getTag(null, tagName));
}
public List<String> labels() {
return myLabels != null ? Collections.unmodifiableList(myLabels) : Collections.<String>emptyList();
}
void addLabelWithNoCheck(String label) {
if (myLabels == null) {
myLabels = new ArrayList<String>();
}
myLabels.add(label);
}
public void addLabel(String label) {
if (myLabels == null) {
myLabels = new ArrayList<String>();
}
if (!myLabels.contains(label)) {
myLabels.add(label);
myIsSaved = false;
}
}
public void removeLabel(String label) {
if (myLabels != null && myLabels.remove(label)) {
myIsSaved = false;
}
}
public List<UID> uids() {
return myUids != null ? Collections.unmodifiableList(myUids) : Collections.<UID>emptyList();
}
public void addUid(String type, String id) {
addUid(new UID(type, id));
}
void addUidWithNoCheck(UID uid) {
if (uid == null) {
return;
}
if (myUids == null) {
myUids = new ArrayList<UID>();
}
myUids.add(uid);
}
public void addUid(UID uid) {
if (uid == null) {
return;
}
if (myUids == null) {
myUids = new ArrayList<UID>();
}
if (!myUids.contains(uid)) {
myUids.add(uid);
myIsSaved = false;
}
}
public boolean matchesUid(UID uid) {
return myUids.contains(uid);
}
public boolean matches(String pattern) {
if (MiscUtil.matchesIgnoreCase(getTitle(), pattern)) {
return true;
}
if (mySeriesInfo != null && MiscUtil.matchesIgnoreCase(mySeriesInfo.Series.getTitle(), pattern)) {
return true;
}
if (myAuthors != null) {
for (Author author : myAuthors) {
if (MiscUtil.matchesIgnoreCase(author.DisplayName, pattern)) {
return true;
}
}
}
if (myTags != null) {
for (Tag tag : myTags) {
if (MiscUtil.matchesIgnoreCase(tag.Name, pattern)) {
return true;
}
}
}
if (MiscUtil.matchesIgnoreCase(File.getLongName(), pattern)) {
return true;
}
return false;
}
boolean save(final BooksDatabase database, boolean force) {
if (!force && myId != -1 && myIsSaved) {
return false;
}
database.executeAsTransaction(new Runnable() {
public void run() {
if (myId >= 0) {
final FileInfoSet fileInfos = new FileInfoSet(database, File);
database.updateBookInfo(myId, fileInfos.getId(File), myEncoding, myLanguage, getTitle());
} else {
myId = database.insertBookInfo(File, myEncoding, myLanguage, getTitle());
if (myId != -1 && myVisitedHyperlinks != null) {
for (String linkId : myVisitedHyperlinks) {
database.addVisitedHyperlink(myId, linkId);
}
}
}
long index = 0;
database.deleteAllBookAuthors(myId);
for (Author author : authors()) {
database.saveBookAuthorInfo(myId, index++, author);
}
database.deleteAllBookTags(myId);
for (Tag tag : tags()) {
database.saveBookTagInfo(myId, tag);
}
final List<String> labelsInDb = database.listLabels(myId);
for (String label : labelsInDb) {
if (myLabels == null || !myLabels.contains(label)) {
database.removeLabel(myId, label);
}
}
if (myLabels != null) {
for (String label : myLabels) {
database.setLabel(myId, label);
}
}
database.saveBookSeriesInfo(myId, mySeriesInfo);
database.deleteAllBookUids(myId);
for (UID uid : uids()) {
database.saveBookUid(myId, uid);
}
}
});
myIsSaved = true;
return true;
}
private Set<String> myVisitedHyperlinks;
private void initHyperlinkSet(BooksDatabase database) {
if (myVisitedHyperlinks == null) {
myVisitedHyperlinks = new TreeSet<String>();
if (myId != -1) {
myVisitedHyperlinks.addAll(database.loadVisitedHyperlinks(myId));
}
}
}
boolean isHyperlinkVisited(BooksDatabase database, String linkId) {
initHyperlinkSet(database);
return myVisitedHyperlinks.contains(linkId);
}
void markHyperlinkAsVisited(BooksDatabase database, String linkId) {
initHyperlinkSet(database);
if (!myVisitedHyperlinks.contains(linkId)) {
myVisitedHyperlinks.add(linkId);
if (myId != -1) {
database.addVisitedHyperlink(myId, linkId);
}
}
}
synchronized ZLImage getCover() {
if (myCover == NULL_IMAGE) {
return null;
} else if (myCover != null) {
final ZLImage image = myCover.get();
if (image != null) {
return image;
}
}
ZLImage image = null;
try {
image = getPlugin().readCover(File);
} catch (BookReadingException e) {
// ignore
}
myCover = image != null ? new WeakReference<ZLImage>(image) : NULL_IMAGE;
return image;
}
@Override
public int hashCode() {
return (int)myId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Book)) {
return false;
}
return File.equals(((Book)o).File);
}
@Override
public String toString() {
return new StringBuilder("Book[")
.append(File.getPath())
.append(", ")
.append(myId)
.append("]")
.toString();
}
}