package com.dteviot.epubviewer.epub; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import com.dteviot.epubviewer.Globals; import com.dteviot.epubviewer.HrefResolver; import com.dteviot.epubviewer.IResourceSource; import com.dteviot.epubviewer.ResourceResponse; import com.dteviot.epubviewer.Utility; import com.dteviot.epubviewer.XmlUtil; import android.net.Uri; import android.sax.Element; import android.sax.RootElement; import android.sax.StartElementListener; import android.util.Log; /* * Represents a book that's been packed into an epub file */ public class Book implements IResourceSource { private final static String HTTP_SCHEME = "http"; // the container XML private static final String XML_NAMESPACE_CONTAINER = "urn:oasis:names:tc:opendocument:xmlns:container"; private static final String XML_ELEMENT_CONTAINER = "container"; private static final String XML_ELEMENT_ROOTFILES = "rootfiles"; private static final String XML_ELEMENT_ROOTFILE = "rootfile"; private static final String XML_ATTRIBUTE_FULLPATH = "full-path"; private static final String XML_ATTRIBUTE_MEDIATYPE = "media-type"; // the .opf XML private static final String XML_NAMESPACE_PACKAGE = "http://www.idpf.org/2007/opf"; private static final String XML_ELEMENT_PACKAGE = "package"; private static final String XML_ELEMENT_MANIFEST = "manifest"; private static final String XML_ELEMENT_MANIFESTITEM = "item"; private static final String XML_ELEMENT_SPINE = "spine"; private static final String XML_ATTRIBUTE_TOC = "toc"; private static final String XML_ELEMENT_ITEMREF = "itemref"; private static final String XML_ATTRIBUTE_IDREF = "idref"; /* * The zip archive */ private ZipFile mZip; /* * Name of the ".opf" file in the zip archive */ private String mOpfFileName; /* * Id of the "table of contents" entry in manifest */ private String mTocID; // Allow access to state for unit tests. public String getOpfFileName() { return mOpfFileName; } public String getTocID() { return mTocID; } public ArrayList<ManifestItem> getSpine() { return mSpine; } public Manifest getManifest() { return mManifest; } public TableOfContents getTableOfContents() { return mTableOfContents; } /* * The resources that are in the spine element of the metadata. */ private ArrayList<ManifestItem> mSpine; /* * The manifest entry in the metadata. */ private Manifest mManifest; /* * The Table of Contents in the metadata. */ private TableOfContents mTableOfContents; /* * Intended for unit testing */ public Book() { mSpine = new ArrayList<ManifestItem>(); mManifest = new Manifest(); mTableOfContents = new TableOfContents(); } /* * Constructor * @param fileName the filename of the Zip archive file */ public Book(String fileName) { mSpine = new ArrayList<ManifestItem>(); mManifest = new Manifest(); mTableOfContents = new TableOfContents(); try { mZip = new ZipFile(fileName); parseEpub(); } catch (IOException e) { Log.e(Globals.TAG, "Error opening file", e); } } /* * Name of zip file */ public String getFileName() { return (mZip == null) ? null : mZip.getName(); } /* * Fetch file from zip */ private InputStream fetchFromZip(String fileName) { InputStream in = null; ZipEntry containerEntry = mZip.getEntry(fileName); if (containerEntry != null) { try { in = mZip.getInputStream(containerEntry); } catch (IOException e) { Log.e(Globals.TAG, "Error reading zip file " + fileName, e); } } if (in == null) { Log.e(Globals.TAG, "Unable to find file in zip: " + fileName); } return in; } /* * Fetch resource from ebook */ public ResourceResponse fetch(Uri resourceUri) { String resourceName = url2ResourceName(resourceUri); ManifestItem item = mManifest.findByResourceName(resourceName); if (item != null) { ResourceResponse response = new ResourceResponse(item.getMediaType(), fetchFromZip(resourceName)); response.setSize(mZip.getEntry(resourceName).getSize()); return response; } // if get here, something went wrong Log.e(Globals.TAG, "Unable to find resource in ebook " + resourceName); return null; } public Uri firstChapter() { return 0 < mSpine.size() ? resourceName2Url(mSpine.get(0).getHref()) : null; } /* * @return URI of next resource in sequence, or null if not one */ public Uri nextResource(Uri resourceUri) { String resourceName = url2ResourceName(resourceUri); for (int i = 0; i < mSpine.size() - 1; ++i) { if (mSpine.get(i).getHref().equals(resourceName)) { return resourceName2Url(mSpine.get(i + 1).getHref()); } } // if get here, not found return null; } /* * @return URI of previous resource in sequence, or null if not one */ public Uri previousResource(Uri resourceUri) { String resourceName = url2ResourceName(resourceUri); for (int i = 1; i < mSpine.size(); ++i) { if (mSpine.get(i).getHref().equals(resourceName)) { return resourceName2Url(mSpine.get(i - 1).getHref()); } } // if get here not found return null; } /* * Build up structure of epub */ private void parseEpub() { // clear everything mOpfFileName = null; mTocID = null; mSpine.clear(); mManifest.clear(); mTableOfContents.clear(); // get the "container" file, this tells us where the ".opf" file is parseXmlResource("META-INF/container.xml", constructContainerFileParser()); if (mOpfFileName != null) { parseXmlResource(mOpfFileName, constructOpfFileParser()); } if (mTocID != null) { ManifestItem tocManifestItem = mManifest.findById(mTocID); if (tocManifestItem != null) { String tocFileName = tocManifestItem.getHref(); HrefResolver resolver = new HrefResolver(tocFileName); parseXmlResource(tocFileName, mTableOfContents.constructTocFileParser(resolver)); } } } private void parseXmlResource(String fileName, ContentHandler handler) { InputStream in = fetchFromZip(fileName); if (in != null) { XmlUtil.parseXmlResource(in, handler, null); } } /* * build parser to parse the container file, * i.e. get the name of the ".opf" file in the zip. * @return parser */ public ContentHandler constructContainerFileParser() { // describe the relationship of the elements RootElement root = new RootElement(XML_NAMESPACE_CONTAINER, XML_ELEMENT_CONTAINER); Element rootfiles = root.getChild(XML_NAMESPACE_CONTAINER, XML_ELEMENT_ROOTFILES); Element rootfile = rootfiles.getChild(XML_NAMESPACE_CONTAINER, XML_ELEMENT_ROOTFILE); rootfile.setStartElementListener(new StartElementListener(){ public void start(Attributes attributes) { String mediaType = attributes.getValue(XML_ATTRIBUTE_MEDIATYPE); if ((mediaType != null) && mediaType.equals("application/oebps-package+xml")) { mOpfFileName = attributes.getValue(XML_ATTRIBUTE_FULLPATH); } } }); return root.getContentHandler(); } /* * build parser to parse the ".opf" file, * @return parser */ public ContentHandler constructOpfFileParser() { // describe the relationship of the elements RootElement root = new RootElement(XML_NAMESPACE_PACKAGE, XML_ELEMENT_PACKAGE); Element manifest = root.getChild(XML_NAMESPACE_PACKAGE, XML_ELEMENT_MANIFEST); Element manifestItem = manifest.getChild(XML_NAMESPACE_PACKAGE, XML_ELEMENT_MANIFESTITEM); Element spine = root.getChild(XML_NAMESPACE_PACKAGE, XML_ELEMENT_SPINE); Element itemref = spine.getChild(XML_NAMESPACE_PACKAGE, XML_ELEMENT_ITEMREF); final HrefResolver resolver = new HrefResolver(mOpfFileName); manifestItem.setStartElementListener(new StartElementListener(){ public void start(Attributes attributes) { mManifest.add(new ManifestItem(attributes, resolver)); } }); // get name of Table of Contents file from the Spine spine.setStartElementListener(new StartElementListener(){ public void start(Attributes attributes) { mTocID = attributes.getValue(XML_ATTRIBUTE_TOC); } }); itemref.setStartElementListener(new StartElementListener(){ public void start(Attributes attributes) { String temp = attributes.getValue(XML_ATTRIBUTE_IDREF); if (temp != null) { ManifestItem item = mManifest.findById(temp); if (item != null) { mSpine.add(item); } } } }); return root.getContentHandler(); } /* * @param url used by WebView * @return resourceName used by zip file */ private static String url2ResourceName(Uri url) { // we only care about the path part of the URL String resourceName = url.getPath(); // if path has a '/' prepended, strip it if (resourceName.charAt(0) == '/') { resourceName = resourceName.substring(1); } return resourceName; } /* * @param resourceName used by zip file * @return URL used by WebView */ public static Uri resourceName2Url(String resourceName) { // build path assuming local file. // pack resourceName into path section of a file URI // need to leave '/' chars in path, so WebView is aware // of path to current resource, so it can can correctly resolve // path of any relative URLs in the current resource. return new Uri.Builder().scheme(HTTP_SCHEME) .encodedAuthority("localhost:" + Globals.WEB_SERVER_PORT) .appendEncodedPath(Uri.encode(resourceName, "/")) .build(); } }