com.zyz.mobile.book.UserBookData.java Source code

Java tutorial

Introduction

Here is the source code for com.zyz.mobile.book.UserBookData.java

Source

/*
Copyright (C) 2013 Ray Zhou
    
JadeRead 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 3 of the License, or
(at your option) any later version.
    
JadeRead 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 JadeRead.  If not, see <http://www.gnu.org/licenses/>
    
Author: Ray Zhou
Date: 2013 04 26
    
*/
package com.zyz.mobile.book;

import android.text.Spannable;
import android.text.Spanned;
import android.util.Log;
import org.apache.commons.lang3.StringEscapeUtils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.util.List;

/**
 * Manage the xml info file of a book
 */
public class UserBookData extends DefaultHandler {

    private static final String TAG = "UserBookData";

    public static final String BOOKMARK_LEFT = "[";
    public static final String BOOKMARK_RIGHT = "]";
    /**
     * path of the info xml
     */
    private File mInfoFile;

    private int mVersion = 0;

    private UserSpan mCurrentSpanObj; // current span object
    private StringBuilder mCurrentSpanObjNote = null;

    /**
     * current position of the book
     */
    private int mOffset;

    private Spannable mSpannable;

    private static final int MAX_DESCRIPTION_LENGTH = 30;

    /**
     * tracks highlight/bookmarks/notes
     */
    private UserSpanTracker mBookmarkTracker;
    private UserSpanTracker mNoteTracker;
    private UserSpanTracker mHighlighTracker; // for highlight and underline

    private LocationHistory mLocationHistory;

    private Status mStatus = new Status();

    public UserBookData(File infoFile, Spannable spannable) {
        mInfoFile = infoFile;
        mSpannable = spannable;

        mBookmarkTracker = new UserSpanTracker();
        mHighlighTracker = new UserSpanTracker();
        mNoteTracker = new UserSpanTracker();
        mLocationHistory = new LocationHistory(this);
    }

    /**
     * open the info xml file. create one if it cannot be found
     */
    public boolean parse() {
        try {
            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser sp = spf.newSAXParser();
            XMLReader reader = sp.getXMLReader();
            reader.setContentHandler(this);
            reader.parse(new InputSource(new FileReader(mInfoFile)));
        } catch (Exception e) {
            // constructor doesn't check for validity of the file
            // catch all exceptions here
            Log.e(TAG, "Failed to parse xml file?");
            return false;
        }
        return true;
    }

    /**
     * saves the interests to a xml file
     */
    public void save() {
        boolean success = false;
        File tmpFile = null;
        try {
            tmpFile = File.createTempFile("xml", "tmp", mInfoFile.getParentFile());
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmpFile)));
            bw.write(toXml());
            bw.close();
            success = true;
        } catch (Exception e) {
            Log.e(TAG, "failed to save file");
        } finally {
            if (success) {
                success = tmpFile.renameTo(mInfoFile); // don't care if it fails
                if (!success) {
                    Log.e(TAG, "failed to overwrite saved file");
                }
            }
        }
    }

    /**
     * Sets the current offset (scroll position) of the book
     *
     * @param offset the offset
     */
    public void setOffset(int offset) {
        mOffset = offset;
    }

    /**
     * get the current offset (scroll position) of the book
     *
     * @return the current offset of the book
     */
    public int getOffset() {
        return mOffset;
    }

    /**
     * remove the given span from this book if it exists
     *
     * @param span the span to remove
     * @return true if remove is successful, false otherwise
     */
    public boolean removeSpan(UserSpan span) {
        boolean success = false;

        switch (span.getType()) {
        case BOOKMARK:
            success = mBookmarkTracker.removeSpan(span);
            break;
        case NOTE:
            success = mNoteTracker.removeSpan(span);
            break;
        case HIGHLGHT:
        case UNDERLINE:
            success = mHighlighTracker.removeSpan(span);
            break;
        }

        if (success) {
            mSpannable.removeSpan(span.getObject());
        }

        return success;
    }

    public void insertSpan(UserSpan span) {

        if (span == null || span.getStart() < 0 || span.getEnd() >= mSpannable.length()) {
            // illegal arguments, do nothing
            return;
        }

        switch (span.getType()) {
        case BOOKMARK:
            mBookmarkTracker.insertSpan(span);
            break;
        case NOTE:
            mNoteTracker.insertSpan(span);
            break;
        case HIGHLGHT:
        case UNDERLINE:
            mHighlighTracker.insertSpan(span);
            break;
        }

        updateSpannable(span);
    }

    public void replaceSpan(UserSpan newSpan, UserSpan oldSpan) {
        boolean success = false;

        switch (newSpan.getType()) {
        case BOOKMARK:
            success = mBookmarkTracker.replaceSpan(newSpan, oldSpan);
            break;
        case NOTE:
            success = mNoteTracker.replaceSpan(newSpan, oldSpan);
            break;
        case HIGHLGHT:
        case UNDERLINE:
            success = mHighlighTracker.replaceSpan(newSpan, oldSpan);
            break;
        }

        if (success) {
            mSpannable.removeSpan(oldSpan.getObject());
            updateSpannable(newSpan);
        }
    }

    /**
     * get the short text span by the specified {@code UserSpan}
     *
     * @param userSpan the user span
     * @return the short text beginning at the specified {@code UserSpan}
     */
    public String getDefaultDescription(UserSpan userSpan) {
        String description = "";
        int start = userSpan.getStart();
        int end = start + MAX_DESCRIPTION_LENGTH;

        if (start < mSpannable.length()) {
            if (userSpan.getType() == UserSpanType.HIGHLGHT || userSpan.getType() == UserSpanType.UNDERLINE) {
                end = Math.min(userSpan.getEnd(), end);

            }

            if (userSpan.getType() == UserSpanType.BOOKMARK) {
                float percentage = ((float) start / (float) mSpannable.length() * 100);
                description = String.format("%s%.2f%%%s ", BOOKMARK_LEFT, percentage, BOOKMARK_RIGHT);
            }

            end = Math.min(end, mSpannable.length());
            description += mSpannable.subSequence(start, end).toString();

            if ((userSpan.getType() == UserSpanType.HIGHLGHT || userSpan.getType() == UserSpanType.UNDERLINE)
                    && userSpan.getEnd() > end) {
                description += "...";
            }
        }
        return description;
    }

    /**
     * Find the first span that covers the specified offset
     *
     * @param offset an offset of the text
     * @return the span that covers the specified offset, null if not found.
     */
    public UserSpan findHighlightSpan(int offset) {
        return mHighlighTracker.findSpan(offset);
    }

    /**
     * draw the specified {@code UserSpan} on the spannable
     *
     * @param userSpan the span to be drawn
     */
    private void updateSpannable(UserSpan userSpan) {
        if (userSpan.isStyleSpan()) {
            mSpannable.setSpan(userSpan.getObject(), userSpan.getStart(), userSpan.getEnd(),
                    Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        }
    }

    public String toXml() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("<%s %s='%d' %s='%d'>", Element.BOOK, Attribute.OFFSET, mOffset, Attribute.VERSION,
                mVersion));

        userSpanTrackerToXml(sb, mBookmarkTracker, Element.BOOKMARKS);
        userSpanTrackerToXml(sb, mHighlighTracker, Element.SPANS);
        sb.append(mLocationHistory.toXml());

        sb.append(String.format("</%s>", Element.BOOK)); // </book>
        return sb.toString();
    }

    /**
     * translate the specified {@code UserSpanTracker} to XML
     *
     * @param sb      the {@code StringBuilder} to append the resulting XML to
     * @param tracker the {@code UserSpanTracker}  to translate
     * @param tag     the xml tag of this {@code UserSpanTracker}
     */
    private void userSpanTrackerToXml(StringBuilder sb, UserSpanTracker tracker, String tag) {
        if (tracker.size() > 0) {
            sb.append(String.format("<%s>", tag));
            for (UserSpan span : tracker) {
                sb.append(span.toXML());
            }
            sb.append(String.format("</%s>", tag));
        }
    }

    public List<UserSpan> getBookmarksListReadOnly() {
        return mBookmarkTracker.getList();
    }

    public List<UserSpan> getHighlightListReadOnly() {
        return mHighlighTracker.getList();
    }

    public LocationHistory getLocationHistory() {
        return mLocationHistory;
    }

    /**********************************/
    /**** ContentHandler methods ******/
    /**
     * ******************************
     */

    @Override
    public void startDocument() throws SAXException {
    }

    @Override
    public void endDocument() throws SAXException {
    }

    @SuppressWarnings("UnusedAssignment")
    @Override
    public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
            throws SAXException {
        if (localName.equals(Element.BOOK)) {
            mStatus.in_book = true;
            try {
                if (atts.getLength() >= 1) {
                    setOffset(Integer.parseInt(atts.getValue(0)));
                }
                if (atts.getLength() >= 2) {
                    mVersion = Integer.parseInt(atts.getValue(1));
                }
            } catch (NumberFormatException e) {

            }
        } else if (localName.equals(Element.SPANS)) {
            mStatus.in_spans = true;
        } else if (localName.equals(Element.SPAN)) {
            mStatus.in_span = true;

            try {
                int i = 0;
                if (atts.getLength() >= 4) {
                    int type = Integer.parseInt(atts.getValue(i++));
                    int color = Integer.parseInt(atts.getValue(i++));
                    int start = Integer.parseInt(atts.getValue(i++));
                    int end = Integer.parseInt(atts.getValue(i++));

                    mCurrentSpanObj = new UserSpan(UserSpanType.toEnum(type), color, start, end);
                    insertSpan(mCurrentSpanObj);
                }
                if (atts.getLength() >= 5) {
                    mCurrentSpanObj.setDescription(StringEscapeUtils.unescapeXml(atts.getValue(i)));
                }
            } catch (NumberFormatException e) {
                // should not happen unless the file is corrupted or modified incorrectly
            }
        } else if (localName.equals(Element.NOTE)) {
            mStatus.in_note = true;
        } else if (localName.equals(Element.BOOKMARKS)) {
            mStatus.in_bookmarks = true;
        } else if (localName.equals(Element.BOOKMARK)) {
            mStatus.in_bookmark = true;
            int i = 0;

            try {
                if (mStatus.in_bookmarks) {
                    if (atts.getLength() >= 1) {
                        int start = Integer.parseInt(atts.getValue(i++));
                        mCurrentSpanObj = (new UserSpan.Builder()).setType(UserSpanType.BOOKMARK).setStart(start)
                                .create();
                        insertSpan(mCurrentSpanObj);
                    }
                    if (atts.getLength() >= 2) {
                        mCurrentSpanObj.setDescription(StringEscapeUtils.unescapeXml(atts.getValue(i)));
                    }
                } else if (mStatus.in_history) {
                    if (atts.getLength() >= 1) {
                        int offset = Integer.parseInt(atts.getValue(i++));
                        mLocationHistory.add(offset);
                    }
                }
            } catch (NumberFormatException e) {

            }

        } else if (localName.equals(Element.HISTORY)) {
            mStatus.in_history = true;
        }
    }

    @Override
    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        if (localName.equals(Element.BOOK)) {
            mStatus.in_book = false;
        } else if (localName.equals(Element.NOTE)) {
            mStatus.in_note = false;
        } else if (localName.equals(Element.BOOKMARKS)) {
            mStatus.in_bookmarks = false;
        } else if (localName.equals(Element.BOOKMARK)) {
            mStatus.in_bookmark = false;
        } else if (localName.equals(Element.SPANS)) {
            mStatus.in_spans = false;
        } else if (localName.equals(Element.SPAN)) {
            mStatus.in_span = false;
        } else if (localName.equals(Element.HISTORY)) {
            mStatus.in_history = false;
        }
    }

    @Override
    public void characters(char[] ch, int start, int len) throws SAXException {
        if (mStatus.in_note) {
            mCurrentSpanObj.appendNote(new String(ch, start, len));
            if (mCurrentSpanObjNote == null) {
                mCurrentSpanObjNote = new StringBuilder(new String(ch, start, len));
            } else {
                mCurrentSpanObjNote.append(new String(ch, start, len));
            }
        }
    }

    private static class Status {
        /**
         * parser tag status
         */
        public boolean in_book = false;
        public boolean in_note = false;
        public boolean in_bookmarks = false;
        public boolean in_bookmark = false;
        public boolean in_spans = false;
        public boolean in_span = false;
        public boolean in_history = false;
    }

    public static class Element {
        public static final String BOOK = "book";
        public static final String SPAN = "span";
        public static final String SPANS = "spans";
        public static final String NOTE = "note";
        public static final String BOOKMARK = "mark";
        public static final String BOOKMARKS = "bookmarks";
        public static final String HISTORY = "history";
    }

    public static class Attribute {
        public static final String START = "a";
        public static final String END = "b";
        public static final String POSITION = "p";
        public static final String TYPE = "t";
        public static final String COLOR = "c";
        public static final String DESCRIPTION = "d";
        public static final String VERSION = "version";
        public static final String OFFSET = "offset";
    }
}