/* * Copyright (C) 2007-2015 FBReader.ORG Limited <contact@fbreader.org> * * 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.util.*; import java.text.DateFormat; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import android.util.Xml; import org.geometerplus.zlibrary.core.constants.XMLNamespaces; import org.geometerplus.zlibrary.core.util.RationalNumber; import org.geometerplus.zlibrary.core.util.ZLColor; import org.geometerplus.zlibrary.text.view.ZLTextPosition; class XMLSerializer extends AbstractSerializer { private StringBuilder builder() { return new StringBuilder("<?xml version='1.1' encoding='UTF-8'?>"); } @Override public String serialize(BookQuery query) { final StringBuilder buffer = builder(); appendTag(buffer, "query", false, "limit", String.valueOf(query.Limit), "page", String.valueOf(query.Page) ); serialize(buffer, query.Filter); closeTag(buffer, "query"); return buffer.toString(); } private void serialize(StringBuilder buffer, Filter filter) { if (filter instanceof Filter.Empty) { appendTag(buffer, "filter", true, "type", "empty" ); } else if (filter instanceof Filter.Not) { appendTag(buffer, "not", false); serialize(buffer, ((Filter.Not)filter).Base); closeTag(buffer, "not"); } else if (filter instanceof Filter.And) { appendTag(buffer, "and", false); serialize(buffer, ((Filter.And)filter).First); serialize(buffer, ((Filter.And)filter).Second); closeTag(buffer, "and"); } else if (filter instanceof Filter.Or) { appendTag(buffer, "or", false); serialize(buffer, ((Filter.Or)filter).First); serialize(buffer, ((Filter.Or)filter).Second); closeTag(buffer, "or"); } else if (filter instanceof Filter.ByAuthor) { final Author author = ((Filter.ByAuthor)filter).Author; appendTag(buffer, "filter", true, "type", "author", "displayName", author.DisplayName, "sorkKey", author.SortKey ); } else if (filter instanceof Filter.ByTag) { final LinkedList<String> lst = new LinkedList<String>(); for (Tag t = ((Filter.ByTag)filter).Tag; t != null; t = t.Parent) { lst.add(0, t.Name); } final String[] params = new String[lst.size() * 2 + 2]; int index = 0; params[index++] = "type"; params[index++] = "tag"; int num = 0; for (String name : lst) { params[index++] = "name" + num++; params[index++] = name; } appendTag(buffer, "filter", true, params); } else if (filter instanceof Filter.ByLabel) { appendTag(buffer, "filter", true, "type", "label", "name", ((Filter.ByLabel)filter).Label ); } else if (filter instanceof Filter.BySeries) { appendTag(buffer, "filter", true, "type", "series", "title", ((Filter.BySeries)filter).Series.getTitle() ); } else if (filter instanceof Filter.ByPattern) { appendTag(buffer, "filter", true, "type", "pattern", "pattern", ((Filter.ByPattern)filter).Pattern ); } else if (filter instanceof Filter.ByTitlePrefix) { appendTag(buffer, "filter", true, "type", "title-prefix", "prefix", ((Filter.ByTitlePrefix)filter).Prefix ); } else if (filter instanceof Filter.HasBookmark) { appendTag(buffer, "filter", true, "type", "has-bookmark" ); } else if (filter instanceof Filter.HasPhysicalFile) { appendTag(buffer, "filter", true, "type", "has-physical-file" ); } else { throw new RuntimeException("Unsupported filter type: " + filter.getClass()); } } @Override public BookQuery deserializeBookQuery(String xml) { try { final BookQueryDeserializer deserializer = new BookQueryDeserializer(); Xml.parse(xml, deserializer); return deserializer.getQuery(); } catch (SAXException e) { System.err.println(xml); e.printStackTrace(); return null; } } @Override public String serialize(BookmarkQuery query) { final StringBuilder buffer = builder(); appendTag(buffer, "query", false, "visible", String.valueOf(query.Visible), "limit", String.valueOf(query.Limit), "page", String.valueOf(query.Page) ); if (query.Book != null) { serialize(buffer, query.Book); } closeTag(buffer, "query"); return buffer.toString(); } @Override public BookmarkQuery deserializeBookmarkQuery(String xml, BookCreator<? extends AbstractBook> creator) { try { final BookmarkQueryDeserializer deserializer = new BookmarkQueryDeserializer(creator); Xml.parse(xml, deserializer); return deserializer.getQuery(); } catch (SAXException e) { System.err.println(xml); e.printStackTrace(); return null; } } @Override public String serialize(AbstractBook book) { final StringBuilder buffer = builder(); serialize(buffer, book); return buffer.toString(); } private void serialize(StringBuilder buffer, AbstractBook book) { appendTag( buffer, "entry", false, "xmlns:dc", XMLNamespaces.DublinCore, "xmlns:calibre", XMLNamespaces.CalibreMetadata ); appendTagWithContent(buffer, "id", book.getId()); appendTagWithContent(buffer, "title", book.getTitle()); appendTagWithContent(buffer, "dc:language", book.getLanguage()); appendTagWithContent(buffer, "dc:encoding", book.getEncodingNoDetection()); for (UID uid : book.uids()) { appendTag( buffer, "dc:identifier", false, "scheme", uid.Type ); buffer.append(escapeForXml(uid.Id)); closeTag(buffer, "dc:identifier"); } for (Author author : book.authors()) { appendTag(buffer, "author", false); appendTagWithContent(buffer, "uri", author.SortKey); appendTagWithContent(buffer, "name", author.DisplayName); closeTag(buffer, "author"); } for (Tag tag : book.tags()) { appendTag( buffer, "category", true, "term", tag.toString("/"), "label", tag.Name ); } for (Label label : book.labels()) { appendTag( buffer, "label", true, "uid", label.Uid, "name", label.Name ); } final SeriesInfo seriesInfo = book.getSeriesInfo(); if (seriesInfo != null) { appendTagWithContent(buffer, "calibre:series", seriesInfo.Series.getTitle()); if (seriesInfo.Index != null) { appendTagWithContent(buffer, "calibre:series_index", seriesInfo.Index.toPlainString()); } } if (book.HasBookmark) { appendTag(buffer, "has-bookmark", true); } // TODO: serialize description (?) // TODO: serialize cover (?) appendTag( buffer, "link", true, "href", "file://" + book.getPath(), // TODO: real book mimetype "type", "application/epub+zip", "rel", "http://opds-spec.org/acquisition" ); final RationalNumber progress = book.getProgress(); if (progress != null) { appendTag( buffer, "progress", true, "numerator", Long.toString(progress.Numerator), "denominator", Long.toString(progress.Denominator) ); } closeTag(buffer, "entry"); } @Override public <B extends AbstractBook> B deserializeBook(String xml, BookCreator<B> creator) { try { final BookDeserializer<B> deserializer = new BookDeserializer<B>(creator); Xml.parse(xml, deserializer); return deserializer.getBook(); } catch (SAXException e) { System.err.println(xml); e.printStackTrace(); return null; } } @Override public String serialize(Bookmark bookmark) { final StringBuilder buffer = builder(); appendTag( buffer, "bookmark", false, "id", String.valueOf(bookmark.getId()), "uid", bookmark.Uid, "versionUid", bookmark.getVersionUid(), "visible", String.valueOf(bookmark.IsVisible) ); appendTag( buffer, "book", true, "id", String.valueOf(bookmark.BookId), "title", bookmark.BookTitle ); appendTagWithContent(buffer, "text", bookmark.getText()); appendTagWithContent(buffer, "original-text", bookmark.getOriginalText()); appendTag( buffer, "history", true, "ts-creation", timestampByDate(bookmark.getTimestamp(Bookmark.DateType.Creation)), "ts-modification", timestampByDate(bookmark.getTimestamp(Bookmark.DateType.Modification)), "ts-access", timestampByDate(bookmark.getTimestamp(Bookmark.DateType.Access)), // obsolete, old format plugins compatibility "date-creation", formatDate(bookmark.getTimestamp(Bookmark.DateType.Creation)), "date-modification", formatDate(bookmark.getTimestamp(Bookmark.DateType.Modification)), "date-access", formatDate(bookmark.getTimestamp(Bookmark.DateType.Access)) ); appendTag( buffer, "start", true, "model", bookmark.ModelId, "paragraph", String.valueOf(bookmark.getParagraphIndex()), "element", String.valueOf(bookmark.getElementIndex()), "char", String.valueOf(bookmark.getCharIndex()) ); final ZLTextPosition end = bookmark.getEnd(); if (end != null) { appendTag( buffer, "end", true, "paragraph", String.valueOf(end.getParagraphIndex()), "element", String.valueOf(end.getElementIndex()), "char", String.valueOf(end.getCharIndex()) ); } else { appendTag( buffer, "end", true, "length", String.valueOf(bookmark.getLength()) ); } appendTag( buffer, "style", true, "id", String.valueOf(bookmark.getStyleId()) ); closeTag(buffer, "bookmark"); return buffer.toString(); } @Override public Bookmark deserializeBookmark(String xml) { try { final BookmarkDeserializer deserializer = new BookmarkDeserializer(); Xml.parse(xml, deserializer); return deserializer.getBookmark(); } catch (SAXException e) { System.err.println(xml); e.printStackTrace(); return null; } } @Override public String serialize(HighlightingStyle style) { final StringBuilder buffer = builder(); final ZLColor bgColor = style.getBackgroundColor(); final ZLColor fgColor = style.getForegroundColor(); appendTag(buffer, "style", true, "id", String.valueOf(style.Id), "timestamp", String.valueOf(style.LastUpdateTimestamp), "name", style.getNameOrNull(), "bg-color", bgColor != null ? String.valueOf(bgColor.intValue()) : "-1", "fg-color", fgColor != null ? String.valueOf(fgColor.intValue()) : "-1" ); return buffer.toString(); } @Override public HighlightingStyle deserializeStyle(String xml) { try { final StyleDeserializer deserializer = new StyleDeserializer(); Xml.parse(xml, deserializer); return deserializer.getStyle(); } catch (SAXException e) { System.err.println(xml); e.printStackTrace(); return null; } } private static String timestampByDate(Long date) { return date != null ? String.valueOf(date) : null; } private static Date dateByTimestamp(String str) throws SAXException { try { return str != null ? new Date(Long.valueOf(str)) : null; } catch (Exception e) { throw new SAXException("XML parsing error", e); } } private static DateFormat ourDateFormatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.FULL, Locale.ENGLISH); private static String formatDate(Long timestamp) { return timestamp != null ? ourDateFormatter.format(new Date(timestamp)) : null; } private static long parseDate(String str) throws SAXException { try { return str != null ? ourDateFormatter.parse(str).getTime() : null; } catch (Exception e) { throw new SAXException("XML parsing error", e); } } private static Long parseDateSafe(String str) throws SAXException { try { return str != null ? ourDateFormatter.parse(str).getTime() : null; } catch (Exception e) { return null; } } private static int parseInt(String str) throws SAXException { try { return Integer.parseInt(str); } catch (Exception e) { throw new SAXException("XML parsing error", e); } } private static int parseIntSafe(String str, int defaultValue) { try { return Integer.parseInt(str); } catch (Exception e) { return defaultValue; } } private static long parseLong(String str) throws SAXException { try { return Long.parseLong(str); } catch (Exception e) { throw new SAXException("XML parsing error", e); } } private static long parseLongSafe(String str, long defaultValue) { try { return Long.parseLong(str); } catch (Exception e) { return defaultValue; } } private static Long parseLongObjectSafe(String str) { try { return Long.parseLong(str); } catch (Exception e) { return null; } } private static boolean parseBoolean(String str) throws SAXException { try { return Boolean.parseBoolean(str); } catch (Exception e) { throw new SAXException("XML parsing error", e); } } private static void appendTag(StringBuilder buffer, String tag, boolean close, String ... attrs) { buffer.append('<').append(tag); for (int i = 0; i < attrs.length - 1; i += 2) { if (attrs[i + 1] != null) { buffer.append(' ') .append(escapeForXml(attrs[i])).append("=\"") .append(escapeForXml(attrs[i + 1])).append('"'); } } if (close) { buffer.append('/'); } buffer.append(">\n"); } private static void closeTag(StringBuilder buffer, String tag) { buffer.append("</").append(tag).append(">"); } private static void appendTagWithContent(StringBuilder buffer, String tag, String content) { if (content != null) { buffer .append('<').append(tag).append('>') .append(escapeForXml(content)) .append("</").append(tag).append(">\n"); } } private static void appendTagWithContent(StringBuilder buffer, String tag, Object content) { if (content != null) { appendTagWithContent(buffer, tag, String.valueOf(content)); } } private static CharSequence escapeForXml(String data) { final StringBuilder buffer = new StringBuilder(); final int len = data.length(); for (int i = 0; i < len; ++i) { final char ch = data.charAt(i); switch (ch) { case '\u0009': case '\n': buffer.append(ch); break; default: if ((ch >= '\u0020' && ch <= '\uD7FF') || (ch >= '\u0E00' && ch <= '\uFFFD')) { buffer.append(ch); } break; case '&': buffer.append("&"); break; case '<': buffer.append("<"); break; case '>': buffer.append(">"); break; case '"': buffer.append("""); break; case '\'': buffer.append("'"); break; } } return buffer; } private static void clear(StringBuilder buffer) { buffer.delete(0, buffer.length()); } private static String string(StringBuilder buffer) { return buffer.length() != 0 ? buffer.toString() : null; } private static final class BookDeserializer<B extends AbstractBook> extends DefaultHandler { private static enum State { READ_NOTHING, READ_ENTRY, READ_ID, READ_UID, READ_TITLE, READ_LANGUAGE, READ_ENCODING, READ_AUTHOR, READ_AUTHOR_URI, READ_AUTHOR_NAME, READ_SERIES_TITLE, READ_SERIES_INDEX, } private final BookCreator<B> myBookCreator; private State myState = State.READ_NOTHING; private long myId = -1; private String myUrl; private final StringBuilder myTitle = new StringBuilder(); private final StringBuilder myLanguage = new StringBuilder(); private final StringBuilder myEncoding = new StringBuilder(); private String myScheme; private final StringBuilder myUid = new StringBuilder(); private final ArrayList<UID> myUidList = new ArrayList<UID>(); private final ArrayList<Author> myAuthors = new ArrayList<Author>(); private final ArrayList<Tag> myTags = new ArrayList<Tag>(); private final ArrayList<Label> myLabels = new ArrayList<Label>(); private final StringBuilder myAuthorSortKey = new StringBuilder(); private final StringBuilder myAuthorName = new StringBuilder(); private final StringBuilder mySeriesTitle = new StringBuilder(); private final StringBuilder mySeriesIndex = new StringBuilder(); private boolean myHasBookmark; private RationalNumber myProgress; private B myBook; private BookDeserializer(BookCreator<B> creator) { myBookCreator = creator; } public B getBook() { return myState == State.READ_NOTHING ? myBook : null; } @Override public void startDocument() { myBook = null; myId = -1; myUrl = null; clear(myTitle); clear(myLanguage); clear(myEncoding); clear(mySeriesTitle); clear(mySeriesIndex); clear(myUid); myUidList.clear(); myAuthors.clear(); myTags.clear(); myLabels.clear(); myHasBookmark = false; myProgress = null; myState = State.READ_NOTHING; } @Override public void endDocument() { if (myId == -1) { return; } myBook = myBookCreator.createBook( myId, myUrl, string(myTitle), string(myEncoding), string(myLanguage) ); for (Author author : myAuthors) { myBook.addAuthorWithNoCheck(author); } for (Tag tag : myTags) { myBook.addTagWithNoCheck(tag); } for (Label label : myLabels) { myBook.addLabelWithNoCheck(label); } for (UID uid : myUidList) { myBook.addUidWithNoCheck(uid); } myBook.setSeriesInfoWithNoCheck(string(mySeriesTitle), string(mySeriesIndex)); myBook.setProgressWithNoCheck(myProgress); myBook.HasBookmark = myHasBookmark; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { switch (myState) { case READ_NOTHING: if (!"entry".equals(localName)) { throw new SAXException("Unexpected tag " + localName); } myState = State.READ_ENTRY; break; case READ_ENTRY: if ("id".equals(localName)) { myState = State.READ_ID; } else if ("title".equals(localName)) { myState = State.READ_TITLE; } else if ("identifier".equals(localName) && XMLNamespaces.DublinCore.equals(uri)) { myState = State.READ_UID; myScheme = attributes.getValue("scheme"); } else if ("language".equals(localName) && XMLNamespaces.DublinCore.equals(uri)) { myState = State.READ_LANGUAGE; } else if ("encoding".equals(localName) && XMLNamespaces.DublinCore.equals(uri)) { myState = State.READ_ENCODING; } else if ("author".equals(localName)) { myState = State.READ_AUTHOR; clear(myAuthorName); clear(myAuthorSortKey); } else if ("category".equals(localName)) { final String term = attributes.getValue("term"); if (term != null) { myTags.add(Tag.getTag(term.split("/"))); } } else if ("label".equals(localName)) { final String name = attributes.getValue("name"); if (name != null) { final String uid = attributes.getValue("uid"); if (uid != null) { myLabels.add(new Label(uid, name)); } else { myLabels.add(new Label(name)); } } } else if ("series".equals(localName) && XMLNamespaces.CalibreMetadata.equals(uri)) { myState = State.READ_SERIES_TITLE; } else if ("series_index".equals(localName) && XMLNamespaces.CalibreMetadata.equals(uri)) { myState = State.READ_SERIES_INDEX; } else if ("has-bookmark".equals(localName)) { myHasBookmark = true; } else if ("link".equals(localName)) { // TODO: use "rel" attribute myUrl = attributes.getValue("href"); } else if ("progress".equals(localName)) { myProgress = RationalNumber.create( parseLong(attributes.getValue("numerator")), parseLong(attributes.getValue("denominator")) ); } else { throw new SAXException("Unexpected tag " + localName); } break; case READ_AUTHOR: if ("uri".equals(localName)) { myState = State.READ_AUTHOR_URI; } else if ("name".equals(localName)) { myState = State.READ_AUTHOR_NAME; } else { throw new SAXException("Unexpected tag " + localName); } break; } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { switch (myState) { case READ_NOTHING: throw new SAXException("Unexpected closing tag " + localName); case READ_ENTRY: if ("entry".equals(localName)) { myState = State.READ_NOTHING; } break; case READ_AUTHOR_URI: case READ_AUTHOR_NAME: myState = State.READ_AUTHOR; break; case READ_AUTHOR: if (myAuthorSortKey.length() > 0 && myAuthorName.length() > 0) { myAuthors.add( new Author(myAuthorName.toString(), myAuthorSortKey.toString()) ); } myState = State.READ_ENTRY; break; case READ_UID: myUidList.add(new UID(myScheme, myUid.toString())); clear(myUid); myState = State.READ_ENTRY; break; default: myState = State.READ_ENTRY; break; } } @Override public void characters(char[] ch, int start, int length) { switch (myState) { case READ_ID: myId = parseLongSafe(new String(ch, start, length), -1); break; case READ_TITLE: myTitle.append(ch, start, length); break; case READ_UID: myUid.append(ch, start, length); break; case READ_LANGUAGE: myLanguage.append(ch, start, length); break; case READ_ENCODING: myEncoding.append(ch, start, length); break; case READ_AUTHOR_URI: myAuthorSortKey.append(ch, start, length); break; case READ_AUTHOR_NAME: myAuthorName.append(ch, start, length); break; case READ_SERIES_TITLE: mySeriesTitle.append(ch, start, length); break; case READ_SERIES_INDEX: mySeriesIndex.append(ch, start, length); break; } } } private static final class BookQueryDeserializer extends DefaultHandler { private static enum State { READ_QUERY, READ_FILTER_NOT, READ_FILTER_AND, READ_FILTER_OR, READ_FILTER_SIMPLE } private LinkedList<State> myStateStack = new LinkedList<State>(); private LinkedList<Filter> myFilterStack = new LinkedList<Filter>(); private Filter myFilter; private int myLimit = -1; private int myPage = -1; private BookQuery myQuery; public BookQuery getQuery() { return myQuery; } @Override public void startDocument() { myStateStack.clear(); } @Override public void endDocument() { if (myFilter != null && myLimit > 0 && myPage >= 0) { myQuery = new BookQuery(myFilter, myLimit, myPage); } } private void setFilterToStack() { if (!myFilterStack.isEmpty() && myFilterStack.getLast() == null) { myFilterStack.set(myFilterStack.size() - 1, myFilter); } } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (myStateStack.isEmpty()) { if ("query".equals(localName)) { myLimit = parseInt(attributes.getValue("limit")); myPage = parseInt(attributes.getValue("page")); myStateStack.add(State.READ_QUERY); } else { throw new SAXException("Unexpected tag " + localName); } } else { if ("filter".equals(localName)) { final String type = attributes.getValue("type"); if ("empty".equals(type)) { myFilter = new Filter.Empty(); } else if ("author".equals(type)) { myFilter = new Filter.ByAuthor(new Author( attributes.getValue("displayName"), attributes.getValue("sorkKey") )); } else if ("tag".equals(type)) { final LinkedList<String> names = new LinkedList<String>(); int num = 0; String n; while ((n = attributes.getValue("name" + num++)) != null) { names.add(n); } myFilter = new Filter.ByTag(Tag.getTag(names.toArray(new String[names.size()]))); } else if ("label".equals(type)) { myFilter = new Filter.ByLabel(attributes.getValue("name")); } else if ("series".equals(type)) { myFilter = new Filter.BySeries(new Series( attributes.getValue("title") )); } else if ("pattern".equals(type)) { myFilter = new Filter.ByPattern(attributes.getValue("pattern")); } else if ("title-prefix".equals(type)) { myFilter = new Filter.ByTitlePrefix(attributes.getValue("prefix")); } else if ("has-bookmark".equals(type)) { myFilter = new Filter.HasBookmark(); } else if ("has-physical-file".equals(type)) { myFilter = new Filter.HasPhysicalFile(); } else { // we create empty filter for all other types // to keep a door to add new filters in a future myFilter = new Filter.Empty(); } myStateStack.add(State.READ_FILTER_SIMPLE); } else if ("not".equals(localName)) { myFilterStack.add(null); myStateStack.add(State.READ_FILTER_NOT); } else if ("and".equals(localName)) { myFilterStack.add(null); myStateStack.add(State.READ_FILTER_AND); } else if ("or".equals(localName)) { myFilterStack.add(null); myStateStack.add(State.READ_FILTER_OR); } else { throw new SAXException("Unexpected tag " + localName); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (myStateStack.isEmpty()) { // should be never thrown throw new SAXException("Unexpected end of tag " + localName); } switch (myStateStack.removeLast()) { case READ_QUERY: break; case READ_FILTER_NOT: myFilter = new Filter.Not(myFilterStack.removeLast()); break; case READ_FILTER_AND: myFilter = new Filter.And(myFilterStack.removeLast(), myFilter); break; case READ_FILTER_OR: myFilter = new Filter.Or(myFilterStack.removeLast(), myFilter); break; case READ_FILTER_SIMPLE: break; } setFilterToStack(); } } private static final class BookmarkQueryDeserializer extends DefaultHandler { private boolean myVisible; private int myLimit; private int myPage; private final BookDeserializer<? extends AbstractBook> myBookDeserializer; private BookmarkQuery myQuery; BookmarkQueryDeserializer(BookCreator<? extends AbstractBook> creator) { myBookDeserializer = new BookDeserializer(creator); } BookmarkQuery getQuery() { return myQuery; } @Override public void startDocument() { myQuery = null; myBookDeserializer.startDocument(); } @Override public void endDocument() { myBookDeserializer.endDocument(); myQuery = new BookmarkQuery(myBookDeserializer.getBook(), myVisible, myLimit, myPage); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if ("query".equals(localName)) { myVisible = parseBoolean(attributes.getValue("visible")); myLimit = parseInt(attributes.getValue("limit")); myPage = parseInt(attributes.getValue("page")); } else { myBookDeserializer.startElement(uri, localName, qName, attributes); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (!"query".equals(localName)) { myBookDeserializer.endElement(uri, localName, qName); } } @Override public void characters(char[] ch, int start, int length) { myBookDeserializer.characters(ch, start, length); } } private static final class BookmarkDeserializer extends DefaultHandler { private static enum State { READ_NOTHING, READ_BOOKMARK, READ_TEXT, READ_ORIGINAL_TEXT } private State myState = State.READ_NOTHING; private Bookmark myBookmark; private long myId = -1; private String myUid; private String myVersionUid; private long myBookId; private String myBookTitle; private final StringBuilder myText = new StringBuilder(); private StringBuilder myOriginalText; private Long myCreationTimestamp; private Long myModificationTimestamp; private Long myAccessTimestamp; private String myModelId; private int myStartParagraphIndex; private int myStartElementIndex; private int myStartCharIndex; private int myEndParagraphIndex; private int myEndElementIndex; private int myEndCharIndex; private boolean myIsVisible; private int myStyle; public Bookmark getBookmark() { return myState == State.READ_NOTHING ? myBookmark : null; } @Override public void startDocument() { myBookmark = null; myId = -1; myUid = null; myVersionUid = null; myBookId = -1; myBookTitle = null; clear(myText); myOriginalText = null; myCreationTimestamp = null; myModificationTimestamp = null; myAccessTimestamp = null; myModelId = null; myStartParagraphIndex = 0; myStartElementIndex = 0; myStartCharIndex = 0; myEndParagraphIndex = -1; myEndElementIndex = -1; myEndCharIndex = -1; myIsVisible = false; myStyle = 1; myState = State.READ_NOTHING; } @Override public void endDocument() { if (myBookId == -1) { return; } myBookmark = new Bookmark( myId, myUid, myVersionUid, myBookId, myBookTitle, myText.toString(), myOriginalText != null ? myOriginalText.toString() : null, myCreationTimestamp, myModificationTimestamp, myAccessTimestamp, myModelId, myStartParagraphIndex, myStartElementIndex, myStartCharIndex, myEndParagraphIndex, myEndElementIndex, myEndCharIndex, myIsVisible, myStyle ); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { switch (myState) { case READ_NOTHING: if (!"bookmark".equals(localName)) { throw new SAXException("Unexpected tag " + localName); } myId = parseLong(attributes.getValue("id")); myUid = attributes.getValue("uid"); myVersionUid = attributes.getValue("versionUid"); myIsVisible = parseBoolean(attributes.getValue("visible")); myState = State.READ_BOOKMARK; break; case READ_BOOKMARK: if ("book".equals(localName)) { myBookId = parseLong(attributes.getValue("id")); myBookTitle = attributes.getValue("title"); } else if ("text".equals(localName)) { myState = State.READ_TEXT; } else if ("original-text".equals(localName)) { myState = State.READ_ORIGINAL_TEXT; myOriginalText = new StringBuilder(); } else if ("history".equals(localName)) { if (attributes.getValue("ts-creation") != null) { myCreationTimestamp = parseLong(attributes.getValue("ts-creation")); myModificationTimestamp = parseLongObjectSafe(attributes.getValue("ts-modification")); myAccessTimestamp = parseLongObjectSafe(attributes.getValue("ts-access")); } else { // obsolete, old format plugins compatibility myCreationTimestamp = parseDate(attributes.getValue("date-creation")); myModificationTimestamp = parseDateSafe(attributes.getValue("date-modification")); myAccessTimestamp = parseDateSafe(attributes.getValue("date-access")); } } else if ("start".equals(localName)) { myModelId = attributes.getValue("model"); myStartParagraphIndex = parseInt(attributes.getValue("paragraph")); myStartElementIndex = parseInt(attributes.getValue("element")); myStartCharIndex = parseInt(attributes.getValue("char")); } else if ("end".equals(localName)) { final String para = attributes.getValue("paragraph"); if (para != null) { myEndParagraphIndex = parseInt(para); myEndElementIndex = parseInt(attributes.getValue("element")); myEndCharIndex = parseInt(attributes.getValue("char")); } else { myEndParagraphIndex = parseInt(attributes.getValue("length")); myEndElementIndex = -1; myEndCharIndex = -1; } } else if ("style".equals(localName)) { myStyle = parseInt(attributes.getValue("id")); } else { throw new SAXException("Unexpected tag " + localName); } break; case READ_TEXT: case READ_ORIGINAL_TEXT: throw new SAXException("Unexpected tag " + localName); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { switch (myState) { case READ_NOTHING: throw new SAXException("Unexpected closing tag " + localName); case READ_BOOKMARK: if ("bookmark".equals(localName)) { myState = State.READ_NOTHING; } break; case READ_TEXT: case READ_ORIGINAL_TEXT: myState = State.READ_BOOKMARK; } } @Override public void characters(char[] ch, int start, int length) { switch (myState) { case READ_TEXT: myText.append(ch, start, length); break; case READ_ORIGINAL_TEXT: myOriginalText.append(ch, start, length); break; } } } private static final class StyleDeserializer extends DefaultHandler { private HighlightingStyle myStyle; public HighlightingStyle getStyle() { return myStyle; } @Override public void startDocument() { myStyle = null; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if ("style".equals(localName)) { final int id = parseIntSafe(attributes.getValue("id"), -1); if (id != -1) { final long timestamp = parseLongSafe(attributes.getValue("timestamp"), 0L); final int bg = parseIntSafe(attributes.getValue("bg-color"), -1); final int fg = parseIntSafe(attributes.getValue("fg-color"), -1); myStyle = new HighlightingStyle( id, timestamp, attributes.getValue("name"), bg != -1 ? new ZLColor(bg) : null, fg != -1 ? new ZLColor(fg) : null ); } } } } }