package org.albite.book.model.book;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Vector;
import javax.microedition.io.Connection;
import javax.microedition.io.Connector;
import javax.microedition.io.InputConnection;
import javax.microedition.io.file.FileConnection;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.StringItem;
import org.albite.albite.AlbiteMIDlet;
import org.albite.book.model.parser.HTMLTextParser;
import org.albite.book.model.parser.PlainTextParser;
import org.albite.book.model.parser.TextParser;
import org.albite.io.PartitionedConnection;
import org.albite.io.RandomReadingFile;
import org.albite.util.archive.Archive;
import org.albite.util.archive.File;
import org.albite.util.archive.folder.ArchiveFolder;
//#if !(TinyMode || TinyModeExport || LightMode || LightModeExport)
import org.geometerplus.zlibrary.text.hyphenation.Languages;
//#endif
import org.kxml2.io.KXmlParser;
import org.kxml2.kdom.Document;
import org.kxml2.kdom.Element;
import org.kxml2.kdom.Node;
import org.xmlpull.v1.XmlPullParserException;
public abstract class Book
implements Connection {
public static final String EPUB_EXTENSION = ".epub";
public static final String PLAIN_TEXT_EXTENSION = ".txt";
public static final String HTM_EXTENSION = ".htm";
public static final String HTML_EXTENSION = ".html";
public static final String XHTML_EXTENSION = ".xhtml";
public static final String[] SUPPORTED_BOOK_EXTENSIONS = new String[] {
EPUB_EXTENSION,
PLAIN_TEXT_EXTENSION,
HTM_EXTENSION, HTML_EXTENSION, XHTML_EXTENSION
};
protected static final String USERDATA_BOOKMARK_TAG = "b";
protected static final byte[] USERDATA_BOOKMARK_TAG_BYTES =
USERDATA_BOOKMARK_TAG.getBytes();
protected static final String USERDATA_CHAPTER_ATTRIB = "c";
protected static final byte[] USERDATA_CHAPTER_ATTRIB_BYTES =
USERDATA_CHAPTER_ATTRIB.getBytes();
protected static final String USERDATA_ENCODING_ATTRIB = "e";
protected static final byte[] USERDATA_ENCODING_ATTRIB_BYTES =
USERDATA_ENCODING_ATTRIB.getBytes();
protected static final String USERDATA_POSITION_ATTRIB = "p";
protected static final byte[] USERDATA_POSITION_ATTRIB_BYTES =
USERDATA_POSITION_ATTRIB.getBytes();
private static final int ALBX_MAGIC_NUMBER = 0x616C6278;
/*
* Main info
*/
protected String title = "Untitled";
protected String author = "Unknown Author";
//#if !(TinyMode || TinyModeExport || LightMode || LightModeExport)
protected String language = Languages.NO_LANGUAGE;
protected String currentLanguage = Languages.NO_LANGUAGE;
//#else
//# protected String language = "";
//# protected String currentLanguage = "";
//#endif
/*
* Contains various book attribs,
* e.g. 'fiction', 'for_children', 'prose', etc.
*/
protected BookmarkManager bookmarks = new BookmarkManager();
/*
* .alx book user settings
*/
protected FileConnection bookSettingsFile = null;
protected FileConnection bookmarksFile = null;
protected String bookURL = null;
/*
* Chapters
*/
protected Chapter[] chapters;
protected Chapter currentChapter;
protected TextParser parser;
public abstract void close() throws IOException;
protected void closeUserFiles() throws IOException {
if (bookSettingsFile != null) {
bookSettingsFile.close();
}
if (bookmarksFile != null) {
bookmarksFile.close();
}
}
//#if !(TinyMode || TinyModeExport || LightMode || LightModeExport)
public final String getLanguage() {
return currentLanguage;
}
//#endif
/**
* Returns book's original language using the its full name, rather than
* just the language code. If such is not found, the code will be
* returned anyway.
*
* @return full language name or its code if there is no full name alias
* for the current language code
*/
public final String getLanguageAlias() {
//#if !(TinyMode || TinyModeExport || LightMode || LightModeExport)
final String[][] langs = Languages.LANGUAGES;
for (int i = 0; i < langs.length; i++) {
/*
* Using reference comparison, as the language strings
* are expected to have been already interned
*/
if (langs[i][0].equalsIgnoreCase(language)) {
return langs[i][1];
}
}
//#endif
return language;
}
/**
*
* @param language
* @return true, if the language was changed
*/
public final boolean setLanguage(final String language) {
if (language != null && !language.equalsIgnoreCase(currentLanguage)) {
currentLanguage = language;
return true;
}
return false;
}
public final String getDefaultLanguage() {
return language;
}
/**
* unload all chapters from memory
*/
public final void unloadChaptersBuffers() {
Chapter chap = chapters[0];
while (chap != null) {
chap.unload();
chap = chap.getNextChapter();
}
}
public boolean setEncoding(final String encoding) {
boolean reflowNeeded = currentChapter.setEncoding(encoding);
//Set it for all chapters. Epubs would override this behaviour
for (int i = 0; i < chapters.length; i++) {
chapters[i].setEncoding(encoding);
}
return reflowNeeded;
}
protected void loadUserFiles(final String filename)
throws BookException, IOException {
/*
* Set default chapter
*/
currentChapter = chapters[0];
bookSettingsFile = loadUserFile(
RandomReadingFile.changeExtension(filename, ".alx"));
bookmarksFile = loadUserFile(
RandomReadingFile.changeExtension(filename, ".alb"));
loadUserData();
}
protected FileConnection loadUserFile(final String filename)
throws IOException {
try {
//#debug
AlbiteMIDlet.LOGGER.log("Opening [" + filename + "]...");
final FileConnection file = (FileConnection) Connector.open(
filename, Connector.READ_WRITE);
//#debug
AlbiteMIDlet.LOGGER.log(file != null);
return file;
} catch (SecurityException e) {
} catch (IOException e) {}
return null;
}
private void loadUserData() {
try {
loadBookSettings();
} catch (IOException e) {
} catch (SecurityException e) {
} catch (BookException e) {}
try {
loadBookmarks();
} catch (IOException e) {
} catch (SecurityException e) {
} catch (BookException e) {}
}
private void loadBookSettings() throws IOException, BookException {
if (bookSettingsFile != null) {
final DataInputStream in = bookSettingsFile.openDataInputStream();
try {
if (in.readInt() != ALBX_MAGIC_NUMBER) {
throw new BookException("Wrong magic number");
}
currentLanguage = in.readUTF();
currentChapter = getChapter(in.readShort());
Chapter chapter;
final int chaptersNumber = in.readShort();
for (int i = 0; i < chaptersNumber; i++) {
chapter = getChapter(i);
chapter.setCurrentPosition(in.readInt());
chapter.setEncoding(in.readUTF());
}
} finally {
in.close();
}
}
}
private void loadBookmarks() throws IOException, BookException {
if (bookmarksFile == null) {
return;
}
/*
* Loading bookmarks
*/
InputStream in = bookmarksFile.openInputStream();
KXmlParser parser = null;
Document doc = null;
Element root;
Element kid;
try {
parser = new KXmlParser();
parser.setInput(new InputStreamReader(in, "UTF-8"));
doc = new Document();
doc.parse(parser);
parser = null;
} catch (XmlPullParserException e) {
parser = null;
doc = null;
throw new BookException("Wrong XML data.");
}
try {
/*
* root element
*/
root = doc.getRootElement();
int childCount = root.getChildCount();
for (int i = 0; i < childCount ; i++ ) {
if (root.getType(i) != Node.ELEMENT) {
continue;
}
kid = root.getElement(i);
final int chapter =
readIntFromXML(kid, USERDATA_CHAPTER_ATTRIB);
int position =
readIntFromXML(kid, USERDATA_POSITION_ATTRIB);
if (position < 0) {
position = 0;
}
if (kid.getName().equals(USERDATA_BOOKMARK_TAG)) {
String text = kid.getText(0);
if (text == null) {
text = "Untitled";
}
bookmarks.addBookmark(
new Bookmark(getChapter(chapter),
position, text));
}
}
} catch (NullPointerException e) {
bookmarks.deleteAll();
throw new BookException("Missing info (NP Exception)");
} catch (IllegalArgumentException e) {
bookmarks.deleteAll();
throw new BookException("Malformed int data");
} catch (RuntimeException e) {
/*
* document has not got a root element
*/
bookmarks.deleteAll();
throw new BookException("Wrong data");
} finally {
in.close();
}
}
private int readIntFromXML(final Element kid, final String elementName) {
int number = 0;
try {
number = Integer.parseInt(
kid.getAttributeValue(
KXmlParser.NO_NAMESPACE, elementName));
} catch (NumberFormatException nfe) {}
return number;
}
public final void saveBookSettings() {
if (chapters != null && bookSettingsFile != null) {
//#debug
AlbiteMIDlet.LOGGER.log("saving book settings");
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
DataOutputStream out = new DataOutputStream(baos);
try {
out.writeInt(ALBX_MAGIC_NUMBER);
out.writeUTF(currentLanguage);
out.writeShort((short) currentChapter.getNumber());
final int chaptersLength = chapters.length;
Chapter chapter;
out.writeShort((short) chaptersLength);
for (int i = 0; i < chaptersLength; i++) {
chapter = chapters[i];
out.writeInt(chapter.getCurrentPosition());
out.writeUTF(chapter.getEncoding());
}
writeData(baos.toByteArray(), bookSettingsFile);
} finally {
out.close();
}
} catch (IOException e) {
//#debug
AlbiteMIDlet.LOGGER.log(e);
} catch (SecurityException e) {
//#debug
AlbiteMIDlet.LOGGER.log(e);
}
}
}
public final void saveBookmarks() {
if (chapters != null && //i.e. if any chapters have been read
bookmarksFile != null //i.e. the file is OK for writing
) {
final byte lt = (byte) ('<' & 0xFF);
final byte gt = (byte) ('>' & 0xFF);
final byte sl = (byte) ('/' & 0xFF);
final byte sp = (byte) (' ' & 0xFF);
final byte nl = (byte) ('\n' & 0xFF);
final byte eq = (byte) ('=' & 0xFF);
final byte qt = (byte) ('"' & 0xFF);
final String encoding = "UTF-8";
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
DataOutputStream out = new DataOutputStream(baos);
try {
/*
* Root element
*/
out.write(lt);
out.write(USERDATA_BOOKMARK_TAG_BYTES);
out.write(gt);
out.write(nl);
/*
* bookmarks
* <b c="3" p="1234">Text</bookmark>
*/
Bookmark bookmark = bookmarks.getFirst();
while (bookmark != null) {
out.write(lt);
out.write(USERDATA_BOOKMARK_TAG.getBytes(encoding));
out.write(sp);
out.write(USERDATA_CHAPTER_ATTRIB.getBytes(encoding));
out.write(eq);
out.write(qt);
out.write(Integer.toString(
bookmark.getChapter().getNumber()
).getBytes(encoding));
out.write(qt);
out.write(sp);
out.write(USERDATA_POSITION_ATTRIB
.getBytes(encoding));
out.write(eq);
out.write(qt);
out.write(Integer.toString(bookmark.getPosition())
.getBytes(encoding));
out.write(qt);
out.write(gt);
out.write(bookmark.getTextForHTML()
.getBytes(encoding));
out.write(lt);
out.write(sl);
out.write(USERDATA_BOOKMARK_TAG
.getBytes(encoding));
out.write(gt);
out.write(nl);
bookmark = bookmark.next;
}
/*
* Close book tag
*/
out.write(lt);
out.write(sl);
out.write(USERDATA_BOOKMARK_TAG_BYTES);
out.write(gt);
out.write(nl);
writeData(baos.toByteArray(), bookmarksFile);
} catch (IOException ioe) {
//#debug
AlbiteMIDlet.LOGGER.log(ioe);
} finally {
out.close();
}
} catch (IOException ioe) {
//#debug
AlbiteMIDlet.LOGGER.log(ioe);
}
}
}
private void writeData(byte[] data, FileConnection file) {
/*
* if there is a dir by that name,
* the functionality will be disabled
*
*/
if (file != null && !file.isDirectory()) {
try {
if (!file.exists()) {
/*
* create the file if it doesn't exist
*/
file.create();
}
file.truncate(0);
DataOutputStream out = file.openDataOutputStream();
try {
out.write(data);
} catch (IOException e) {
} finally {
out.close();
}
} catch (IOException e) {
} catch (SecurityException e) {}
}
}
public final int getChaptersCount() {
return chapters.length;
}
public final Chapter getChapter(final int number) {
if (number < 0) {
return chapters[0];
}
if (number > chapters.length - 1) {
return chapters[chapters.length - 1];
}
return chapters[number];
}
public final void fillBookInfo(Form f) {
StringItem s;
s = new StringItem("Title:", title);
s.setLayout(StringItem.LAYOUT_LEFT);
f.append(s);
s = new StringItem("Author:", author);
f.append(s);
s = new StringItem("Language:", getLanguageAlias());
f.append(s);
s = new StringItem("Language:", language);
}
public final BookmarkManager getBookmarkManager() {
return bookmarks;
}
public final Chapter getCurrentChapter() {
return currentChapter;
}
public final void setCurrentChapter(final Chapter bc) {
currentChapter = bc;
}
public final int getCurrentChapterPosition() {
return currentChapter.getCurrentPosition();
}
public final void setCurrentChapterPos(final int pos) {
if (pos < 0 || pos >= currentChapter.getTextBuffer().length) {
throw new IllegalArgumentException("Position is wrong");
}
currentChapter.setCurrentPosition(pos);
}
public final TextParser getParser() {
return parser;
}
public static Book open(String filename)
throws IOException, BookException {
String filenameLowerCase = filename.toLowerCase();
if (filenameLowerCase.endsWith(EPUB_EXTENSION)) {
return new EPubBook(filename);
}
if (filenameLowerCase.endsWith(PLAIN_TEXT_EXTENSION)) {
return new FileBook(filename, null, new PlainTextParser(), false);
}
if (filenameLowerCase.endsWith(HTM_EXTENSION)
|| filenameLowerCase.endsWith(HTML_EXTENSION)
|| filenameLowerCase.endsWith(XHTML_EXTENSION)) {
return new FileBook(
filename,
new ArchiveFolder(
RandomReadingFile.getPathFromURL(filename)),
new HTMLTextParser(),
true);
}
throw new BookException("Unsupported file format.");
}
protected final void linkChapters() {
Chapter prev;
Chapter cur;
for (int i = 1; i < chapters.length; i++) {
prev = chapters[i - 1];
cur = chapters[i];
prev.setNextChapter(cur);
cur.setPrevChapter(prev);
}
}
protected final void splitChapterIntoPieces(
final InputConnection chapterFile,
final int chapterFilesize,
final File pathReference,
final int maxChapterSize,
final int chapterNumber,
final boolean processHtmlEntities,
final Vector chapters
) throws IOException, BookException {
if (chapterFilesize <= maxChapterSize) {
chapters.addElement(new Chapter(
chapterFile, chapterFilesize, pathReference,
"Chapter #" + (chapterNumber + 1),
processHtmlEntities, chapterNumber)
);
return;
} else {
int kMax = chapterFilesize / maxChapterSize;
if (chapterFilesize % maxChapterSize > 0) {
kMax++;
}
int left = chapterFilesize;
int chapSize;
for (int k = 0; k < kMax; k++) {
chapSize = (left > maxChapterSize ? maxChapterSize : left);
chapters.addElement(new Chapter(
new PartitionedConnection(
chapterFile, k * maxChapterSize, chapSize),
chapSize,
pathReference,
"Chapter #" + (chapterNumber + k + 1),
processHtmlEntities,
chapterNumber + k
));
left -= maxChapterSize;
}
}
}
/*
* The maximum file size after which the Filebook is split
* forcefully into chapters. The split is a dumb one, for it splits
* on bytes, not characters or tags, i.e. it may split a utf-8 character
* in two halves, making it unreadable (so that it would be visible as a
* question mark) or it may split an HTML tag (so that it would become
* useless and be shown in the text of the chapter)
*/
protected final int getMaximumTxtFilesize(final boolean lightMode) {
return (lightMode ? 16 * 1024 : 64 * 1024);
}
//#if (TinyMode || TinyModeExport)
//# public static final int MAXIMUM_TXT_FILESIZE = 16 * 1024;
//#elif (LightMode || LightModeExport)
//# public static final int MAXIMUM_TXT_FILESIZE = 24 * 1024;
//#elif (HDMode || HDModeExport)
//# public static final int MAXIMUM_TXT_FILESIZE = 128 * 1024;
//#else
public static final int MAXIMUM_TXT_FILESIZE = 64 * 1024;
//#endif
//#if (TinyMode || TinyModeExport)
//# public static final int MAXIMUM_HTML_FILESIZE = 16 * 1024;
//#elif (LightMode || LightModeExport)
//# public static final int MAXIMUM_HTML_FILESIZE = 48 * 1024;
//#elif (HDMode || HDModeExport)
//# public static final int MAXIMUM_HTML_FILESIZE = 512 * 1024;
//#else
public static final int MAXIMUM_HTML_FILESIZE = 192 * 1024;
//#endif
protected final int getMaximumHtmlFilesize(final boolean lightMode) {
return (lightMode ? 16 * 1024 : 192 * 1024);
}
public final String getURL() {
return bookURL;
}
public abstract Archive getArchive();
}