lollipop版本分开,有些新特性android5.0和6.0不支持

This commit is contained in:
kunfei committed 2023-03-30 08:40:58 +08:00
1 parent 0959e50f53
commit 3cf3231da5
258 files changed
+12817 -13529

No files matched your search

@@ -0,0 +1,88 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import me.ag2s.epublib.util.StringUtil;
/**
* Represents one of the authors of the book
*
* @author paul
*/
public class Author implements Serializable {
private static final long serialVersionUID = 6663408501416574200L;
private String firstname;
private String lastname;
private Relator relator = Relator.AUTHOR;
public Author(String singleName) {
this("", singleName);
}
public Author(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
@Override
@SuppressWarnings("NullableProblems")
public String toString() {
return this.lastname + ", " + this.firstname;
}
public int hashCode() {
return StringUtil.hashCode(firstname, lastname);
}
public boolean equals(Object authorObject) {
if (!(authorObject instanceof Author)) {
return false;
}
Author other = (Author) authorObject;
return StringUtil.equals(firstname, other.firstname)
&& StringUtil.equals(lastname, other.lastname);
}
/**
* 设置贡献者的角色
*
* @param code 角色编号
*/
public void setRole(String code) {
Relator result = Relator.byCode(code);
if (result == null) {
result = Relator.AUTHOR;
}
this.relator = result;
}
public Relator getRelator() {
return relator;
}
public void setRelator(Relator relator) {
this.relator = relator;
}
}
@@ -0,0 +1,112 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Locale;
import me.ag2s.epublib.epub.PackageDocumentBase;
/**
* A Date used by the book's metadata.
* <p>
* Examples: creation-date, modification-date, etc
*
* @author paul
*/
public class Date implements Serializable {
private static final long serialVersionUID = 7533866830395120136L;
public enum Event {
PUBLICATION("publication"),
MODIFICATION("modification"),
CREATION("creation");
private final String value;
Event(String v) {
value = v;
}
public static Event fromValue(String v) {
for (Event c : Event.values()) {
if (c.value.equals(v)) {
return c;
}
}
return null;
}
@Override
@SuppressWarnings("NullableProblems")
public String toString() {
return value;
}
}
private Event event;
private String dateString;
public Date() {
this(new java.util.Date(), Event.CREATION);
}
public Date(java.util.Date date) {
this(date, (Event) null);
}
public Date(String dateString) {
this(dateString, (Event) null);
}
public Date(java.util.Date date, Event event) {
this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date),
event);
}
public Date(String dateString, Event event) {
this.dateString = dateString;
this.event = event;
}
public Date(java.util.Date date, String event) {
this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date),
event);
}
public Date(String dateString, String event) {
this(checkDate(dateString), Event.fromValue(event));
this.dateString = dateString;
}
private static String checkDate(String dateString) {
if (dateString == null) {
throw new IllegalArgumentException(
"Cannot create a date from a blank string");
}
return dateString;
}
public String getValue() {
return dateString;
}
public Event getEvent() {
return event;
}
public void setEvent(Event event) {
this.event = event;
}
@Override
@SuppressWarnings("NullableProblems")
public String toString() {
if (event == null) {
return dateString;
}
return "" + event + ":" + dateString;
}
}
@@ -0,0 +1,323 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Representation of a Book.
* <p>
* All resources of a Book (html, css, xml, fonts, images) are represented
* as Resources. See getResources() for access to these.<br/>
* A Book as 3 indexes into these Resources, as per the epub specification.<br/>
* <dl>
* <dt>Spine</dt>
* <dd>these are the Resources to be shown when a user reads the book from
* start to finish.</dd>
* <dt>Table of Contents<dt>
* <dd>The table of contents. Table of Contents references may be in a
* different order and contain different Resources than the spine, and often do.
* <dt>Guide</dt>
* <dd>The Guide has references to a set of special Resources like the
* cover page, the Glossary, the copyright page, etc.
* </dl>
* <p/>
* The complication is that these 3 indexes may and usually do point to
* different pages.
* A chapter may be split up in 2 pieces to fit it in to memory. Then the
* spine will contain both pieces, but the Table of Contents only the first.
* <p>
* The Content page may be in the Table of Contents, the Guide, but not
* in the Spine.
* Etc.
* <p/>
* <p>
* Please see the illustration at: doc/schema.svg
*
* @author paul
* @author jake
*/
public class EpubBook implements Serializable {
private static final long serialVersionUID = 2068355170895770100L;
private Resources resources = new Resources();
private Metadata metadata = new Metadata();
private Spine spine = new Spine();
private TableOfContents tableOfContents = new TableOfContents();
private final Guide guide = new Guide();
private Resource opfResource;
private Resource ncxResource;
private Resource coverImage;
private String version = "2.0";
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public boolean isEpub3() {
return this.version.startsWith("3.");
}
@SuppressWarnings("UnusedReturnValue")
public TOCReference addSection(
TOCReference parentSection, String sectionTitle, Resource resource) {
return addSection(parentSection, sectionTitle, resource, null);
}
/**
* Adds the resource to the table of contents of the book as a child
* section of the given parentSection
*
* @param parentSection parentSection
* @param sectionTitle sectionTitle
* @param resource resource
* @param fragmentId fragmentId
* @return The table of contents
*/
public TOCReference addSection(
TOCReference parentSection, String sectionTitle, Resource resource,
String fragmentId) {
getResources().add(resource);
if (spine.findFirstResourceById(resource.getId()) < 0) {
spine.addSpineReference(new SpineReference(resource));
}
return parentSection.addChildSection(
new TOCReference(sectionTitle, resource, fragmentId));
}
public TOCReference addSection(String title, Resource resource) {
return addSection(title, resource, null);
}
/**
* Adds a resource to the book's set of resources, table of contents and
* if there is no resource with the id in the spine also adds it to the spine.
*
* @param title title
* @param resource resource
* @param fragmentId fragmentId
* @return The table of contents
*/
public TOCReference addSection(
String title, Resource resource, String fragmentId) {
getResources().add(resource);
TOCReference tocReference = tableOfContents
.addTOCReference(new TOCReference(title, resource, fragmentId));
if (spine.findFirstResourceById(resource.getId()) < 0) {
spine.addSpineReference(new SpineReference(resource));
}
return tocReference;
}
@SuppressWarnings("unused")
public void generateSpineFromTableOfContents() {
Spine spine = new Spine(tableOfContents);
// in case the tocResource was already found and assigned
spine.setTocResource(this.spine.getTocResource());
this.spine = spine;
}
/**
* The Book's metadata (titles, authors, etc)
*
* @return The Book's metadata (titles, authors, etc)
*/
public Metadata getMetadata() {
return metadata;
}
public void setMetadata(Metadata metadata) {
this.metadata = metadata;
}
public void setResources(Resources resources) {
this.resources = resources;
}
@SuppressWarnings("unused")
public Resource addResource(Resource resource) {
return resources.add(resource);
}
/**
* The collection of all images, chapters, sections, xhtml files,
* stylesheets, etc that make up the book.
*
* @return The collection of all images, chapters, sections, xhtml files,
* stylesheets, etc that make up the book.
*/
public Resources getResources() {
return resources;
}
/**
* The sections of the book that should be shown if a user reads the book
* from start to finish.
*
* @return The Spine
*/
public Spine getSpine() {
return spine;
}
public void setSpine(Spine spine) {
this.spine = spine;
}
/**
* The Table of Contents of the book.
*
* @return The Table of Contents of the book.
*/
public TableOfContents getTableOfContents() {
return tableOfContents;
}
public void setTableOfContents(TableOfContents tableOfContents) {
this.tableOfContents = tableOfContents;
}
/**
* The book's cover page as a Resource.
* An XHTML document containing a link to the cover image.
*
* @return The book's cover page as a Resource
*/
public Resource getCoverPage() {
Resource coverPage = guide.getCoverPage();
if (coverPage == null) {
coverPage = spine.getResource(0);
}
return coverPage;
}
public void setCoverPage(Resource coverPage) {
if (coverPage == null) {
return;
}
if (resources.notContainsByHref(coverPage.getHref())) {
resources.add(coverPage);
}
guide.setCoverPage(coverPage);
}
/**
* Gets the first non-blank title from the book's metadata.
*
* @return the first non-blank title from the book's metadata.
*/
public String getTitle() {
return getMetadata().getFirstTitle();
}
/**
* The book's cover image.
*
* @return The book's cover image.
*/
public Resource getCoverImage() {
return coverImage;
}
public void setCoverImage(Resource coverImage) {
if (coverImage == null) {
return;
}
if (resources.notContainsByHref(coverImage.getHref())) {
resources.add(coverImage);
}
this.coverImage = coverImage;
}
/**
* The guide; contains references to special sections of the book like
* colophon, glossary, etc.
*
* @return The guide; contains references to special sections of the book
* like colophon, glossary, etc.
*/
public Guide getGuide() {
return guide;
}
/**
* All Resources of the Book that can be reached via the Spine, the
* TableOfContents or the Guide.
* <p/>
* Consists of a list of "reachable" resources:
* <ul>
* <li>The coverpage</li>
* <li>The resources of the Spine that are not already in the result</li>
* <li>The resources of the Table of Contents that are not already in the
* result</li>
* <li>The resources of the Guide that are not already in the result</li>
* </ul>
* To get all html files that make up the epub file use
* {@link #getResources()}
*
* @return All Resources of the Book that can be reached via the Spine,
* the TableOfContents or the Guide.
*/
public List<Resource> getContents() {
Map<String, Resource> result = new LinkedHashMap<>();
addToContentsResult(getCoverPage(), result);
for (SpineReference spineReference : getSpine().getSpineReferences()) {
addToContentsResult(spineReference.getResource(), result);
}
for (Resource resource : getTableOfContents().getAllUniqueResources()) {
addToContentsResult(resource, result);
}
for (GuideReference guideReference : getGuide().getReferences()) {
addToContentsResult(guideReference.getResource(), result);
}
return new ArrayList<>(result.values());
}
private static void addToContentsResult(Resource resource,
Map<String, Resource> allReachableResources) {
if (resource != null && (!allReachableResources
.containsKey(resource.getHref()))) {
allReachableResources.put(resource.getHref(), resource);
}
}
public Resource getOpfResource() {
return opfResource;
}
public void setOpfResource(Resource opfResource) {
this.opfResource = opfResource;
}
public void setNcxResource(Resource ncxResource) {
this.ncxResource = ncxResource;
}
public Resource getNcxResource() {
return ncxResource;
}
}
@@ -0,0 +1,37 @@
package me.ag2s.epublib.domain;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
import me.ag2s.epublib.util.zip.ZipEntryWrapper;
import me.ag2s.epublib.util.zip.ZipFileWrapper;
/**
* @author jake
*/
public class EpubResourceProvider implements LazyResourceProvider {
private final ZipFileWrapper zipFileWrapper;
public EpubResourceProvider(ZipFileWrapper zipFileWrapper) {
this.zipFileWrapper = zipFileWrapper;
}
@Override
public InputStream getResourceStream(@NonNull String href) throws IOException {
//ZipFile zipFile = new ZipFile(epubFilename);
ZipEntryWrapper zipEntry = zipFileWrapper.getEntry(href);
if (zipEntry == null) {
//zipFile.close();
throw new IllegalStateException(
"Cannot find entry " + href + " in epub file " + zipFileWrapper);
}
return new ResourceInputStream(zipFileWrapper.getInputStream(zipEntry));
}
}
@@ -0,0 +1,46 @@
package me.ag2s.epublib.domain;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 用于创建epub,添加大文件(如大量图片)时容易OOM,使用LazyResource,避免OOM.
*/
public class FileResourceProvider implements LazyResourceProvider {
//需要导入资源的父目录
String dir;
/**
* 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。
*
* @param parentDir 文件的目录
*/
public FileResourceProvider(String parentDir) {
this.dir = parentDir;
}
/**
* 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。
*
* @param parentFile 文件夹
*/
@SuppressWarnings("unused")
public FileResourceProvider(File parentFile) {
this.dir = parentFile.getPath();
}
/**
* 根据子文件名href,再父目录下读取文件获取FileInputStream
*
* @param href 子文件名href
* @return 对应href的FileInputStream
* @throws IOException 抛出IOException
*/
@Override
public InputStream getResourceStream(String href) throws IOException {
return new FileInputStream(new File(dir, href));
}
}
@@ -0,0 +1,128 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* The guide is a selection of special pages of the book.
* Examples of these are the cover, list of illustrations, etc.
* <p>
* It is an optional part of an epub, and support for the various types
* of references varies by reader.
* <p>
* The only part of this that is heavily used is the cover page.
*
* @author paul
*/
public class Guide implements Serializable {
/**
*
*/
private static final long serialVersionUID = -6256645339915751189L;
public static final String DEFAULT_COVER_TITLE = GuideReference.COVER;
private List<GuideReference> references = new ArrayList<>();
private static final int COVERPAGE_NOT_FOUND = -1;
private static final int COVERPAGE_UNITIALIZED = -2;
private int coverPageIndex = -1;
public List<GuideReference> getReferences() {
return references;
}
public void setReferences(List<GuideReference> references) {
this.references = references;
uncheckCoverPage();
}
private void uncheckCoverPage() {
coverPageIndex = COVERPAGE_UNITIALIZED;
}
public GuideReference getCoverReference() {
checkCoverPage();
if (coverPageIndex >= 0) {
return references.get(coverPageIndex);
}
return null;
}
@SuppressWarnings("UnusedReturnValue")
public int setCoverReference(GuideReference guideReference) {
if (coverPageIndex >= 0) {
references.set(coverPageIndex, guideReference);
} else {
references.add(0, guideReference);
coverPageIndex = 0;
}
return coverPageIndex;
}
private void checkCoverPage() {
if (coverPageIndex == COVERPAGE_UNITIALIZED) {
initCoverPage();
}
}
private void initCoverPage() {
int result = COVERPAGE_NOT_FOUND;
for (int i = 0; i < references.size(); i++) {
GuideReference guideReference = references.get(i);
if (guideReference.getType().equals(GuideReference.COVER)) {
result = i;
break;
}
}
coverPageIndex = result;
}
/**
* The coverpage of the book.
*
* @return The coverpage of the book.
*/
public Resource getCoverPage() {
GuideReference guideReference = getCoverReference();
if (guideReference == null) {
return null;
}
return guideReference.getResource();
}
public void setCoverPage(Resource coverPage) {
GuideReference coverpageGuideReference = new GuideReference(coverPage,
GuideReference.COVER, DEFAULT_COVER_TITLE);
setCoverReference(coverpageGuideReference);
}
@SuppressWarnings("UnusedReturnValue")
public ResourceReference addReference(GuideReference reference) {
this.references.add(reference);
uncheckCoverPage();
return reference;
}
/**
* A list of all GuideReferences that have the given
* referenceTypeName (ignoring case).
*
* @param referenceTypeName referenceTypeName
* @return A list of all GuideReferences that have the given
* referenceTypeName (ignoring case).
*/
public List<GuideReference> getGuideReferencesByType(
String referenceTypeName) {
List<GuideReference> result = new ArrayList<>();
for (GuideReference guideReference : references) {
if (referenceTypeName.equalsIgnoreCase(guideReference.getType())) {
result.add(guideReference);
}
}
return result;
}
}
@@ -0,0 +1,100 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import me.ag2s.epublib.util.StringUtil;
/**
* These are references to elements of the book's guide.
*
* @author paul
* @see Guide
*/
public class GuideReference extends TitledResourceReference
implements Serializable {
private static final long serialVersionUID = -316179702440631834L;
/**
* the book cover(s), jacket information, etc.
*/
public static final String COVER = "cover";
/**
* human-readable page with title, author, publisher, and other metadata
*/
public static String TITLE_PAGE = "title-page";
/**
* Human-readable table of contents.
* Not to be confused the epub file table of contents
*/
public static String TOC = "toc";
/**
* back-of-book style index
*/
public static String INDEX = "index";
public static String GLOSSARY = "glossary";
public static String ACKNOWLEDGEMENTS = "acknowledgements";
public static String BIBLIOGRAPHY = "bibliography";
public static String COLOPHON = "colophon";
public static String COPYRIGHT_PAGE = "copyright-page";
public static String DEDICATION = "dedication";
/**
* an epigraph is a phrase, quotation, or poem that is set at the
* beginning of a document or component.
* <p>
* source: http://en.wikipedia.org/wiki/Epigraph_%28literature%29
*/
public static String EPIGRAPH = "epigraph";
public static String FOREWORD = "foreword";
/**
* list of illustrations
*/
public static String LOI = "loi";
/**
* list of tables
*/
public static String LOT = "lot";
public static String NOTES = "notes";
public static String PREFACE = "preface";
/**
* A page of content (e.g. "Chapter 1")
*/
public static String TEXT = "text";
private String type;
public GuideReference(Resource resource) {
this(resource, null);
}
public GuideReference(Resource resource, String title) {
super(resource, title);
}
public GuideReference(Resource resource, String type, String title) {
this(resource, type, title, null);
}
public GuideReference(Resource resource, String type, String title,
String fragmentId) {
super(resource, title, fragmentId);
this.type = StringUtil.isNotBlank(type) ? type.toLowerCase() : null;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
@@ -0,0 +1,133 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.List;
import java.util.UUID;
import me.ag2s.epublib.util.StringUtil;
/**
* A Book's identifier.
* <p>
* Defaults to a random UUID and scheme "UUID"
*
* @author paul
*/
public class Identifier implements Serializable {
private static final long serialVersionUID = 955949951416391810L;
@SuppressWarnings("unused")
public interface Scheme {
String UUID = "UUID";
String ISBN = "ISBN";
String URL = "URL";
String URI = "URI";
}
private boolean bookId = false;
private String scheme;
private String value;
/**
* Creates an Identifier with as value a random UUID and scheme "UUID"
*/
public Identifier() {
this(Scheme.UUID, UUID.randomUUID().toString());
}
public Identifier(String scheme, String value) {
this.scheme = scheme;
this.value = value;
}
/**
* The first identifier for which the bookId is true is made the
* bookId identifier.
* <p>
* If no identifier has bookId == true then the first bookId identifier
* is written as the primary.
*
* @param identifiers i
* @return The first identifier for which the bookId is true is made
* the bookId identifier.
*/
public static Identifier getBookIdIdentifier(List<Identifier> identifiers) {
if (identifiers == null || identifiers.isEmpty()) {
return null;
}
Identifier result = null;
for (Identifier identifier : identifiers) {
if (identifier.isBookId()) {
result = identifier;
break;
}
}
if (result == null) {
result = identifiers.get(0);
}
return result;
}
public String getScheme() {
return scheme;
}
public void setScheme(String scheme) {
this.scheme = scheme;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public void setBookId(boolean bookId) {
this.bookId = bookId;
}
/**
* This bookId property allows the book creator to add multiple ids and
* tell the epubwriter which one to write out as the bookId.
* <p>
* The Dublin Core metadata spec allows multiple identifiers for a Book.
* The epub spec requires exactly one identifier to be marked as the book id.
*
* @return whether this is the unique book id.
*/
public boolean isBookId() {
return bookId;
}
public int hashCode() {
return StringUtil.defaultIfNull(scheme).hashCode() ^ StringUtil
.defaultIfNull(value).hashCode();
}
public boolean equals(Object otherIdentifier) {
if (!(otherIdentifier instanceof Identifier)) {
return false;
}
return StringUtil.equals(scheme, ((Identifier) otherIdentifier).scheme)
&& StringUtil.equals(value, ((Identifier) otherIdentifier).value);
}
@SuppressWarnings("NullableProblems")
@Override
public String toString() {
if (StringUtil.isBlank(scheme)) {
return "" + value;
}
return "" + scheme + ":" + value;
}
}
@@ -0,0 +1,145 @@
package me.ag2s.epublib.domain;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import me.ag2s.epublib.util.IOUtil;
/**
* A Resource that loads its data only on-demand from a EPUB book file.
* This way larger books can fit into memory and can be opened faster.
*/
public class LazyResource extends Resource {
private static final long serialVersionUID = 5089400472352002866L;
private final String TAG = getClass().getName();
private final LazyResourceProvider resourceProvider;
private final long cachedSize;
/**
* Creates a lazy resource, when the size is unknown.
*
* @param resourceProvider The resource provider loads data on demand.
* @param href The resource's href within the epub.
*/
public LazyResource(LazyResourceProvider resourceProvider, String href) {
this(resourceProvider, -1, href);
}
public LazyResource(LazyResourceProvider resourceProvider, String href, String originalHref) {
this(resourceProvider, -1, href, originalHref);
}
/**
* Creates a Lazy resource, by not actually loading the data for this entry.
* <p>
* The data will be loaded on the first call to getData()
*
* @param resourceProvider The resource provider loads data on demand.
* @param size The size of this resource.
* @param href The resource's href within the epub.
*/
public LazyResource(
LazyResourceProvider resourceProvider, long size, String href) {
super(null, null, href, MediaTypes.determineMediaType(href));
this.resourceProvider = resourceProvider;
this.cachedSize = size;
}
public LazyResource(
LazyResourceProvider resourceProvider, long size, String href, String originalHref) {
super(null, null, href, originalHref, MediaTypes.determineMediaType(href));
this.resourceProvider = resourceProvider;
this.cachedSize = size;
}
/**
* Gets the contents of the Resource as an InputStream.
*
* @return The contents of the Resource.
* @throws IOException IOException
*/
public InputStream getInputStream() throws IOException {
if (isInitialized()) {
return new ByteArrayInputStream(getData());
} else {
return resourceProvider.getResourceStream(this.originalHref);
}
}
/**
* Initializes the resource by loading its data into memory.
*
* @throws IOException IOException
*/
public void initialize() throws IOException {
getData();
}
/**
* The contents of the resource as a byte[]
* <p>
* If this resource was lazy-loaded and the data was not yet loaded,
* it will be loaded into memory at this point.
* This included opening the zip file, so expect a first load to be slow.
*
* @return The contents of the resource
*/
public byte[] getData() throws IOException {
if (data == null) {
Log.d(TAG, "Initializing lazy resource: " + this.getHref());
InputStream in = resourceProvider.getResourceStream(this.originalHref);
byte[] readData = IOUtil.toByteArray(in, (int) this.cachedSize);
if (readData == null) {
throw new IOException(
"Could not load the contents of resource: " + this.getHref());
} else {
this.data = readData;
}
in.close();
}
return data;
}
/**
* Tells this resource to release its cached data.
* <p>
* If this resource was not lazy-loaded, this is a no-op.
*/
public void close() {
if (this.resourceProvider != null) {
this.data = null;
}
}
/**
* Returns if the data for this resource has been loaded into memory.
*
* @return true if data was loaded.
*/
public boolean isInitialized() {
return data != null;
}
/**
* Returns the size of this resource in bytes.
*
* @return the size.
*/
public long getSize() {
if (data != null) {
return data.length;
}
return cachedSize;
}
}
@@ -0,0 +1,12 @@
package me.ag2s.epublib.domain;
import java.io.IOException;
import java.io.InputStream;
/**
* @author jake
*/
public interface LazyResourceProvider {
InputStream getResourceStream(String href) throws IOException;
}
@@ -0,0 +1,22 @@
package me.ag2s.epublib.domain;
@SuppressWarnings("unused")
public enum ManifestItemProperties implements ManifestProperties {
COVER_IMAGE("cover-image"),
MATHML("mathml"),
NAV("nav"),
REMOTE_RESOURCES("remote-resources"),
SCRIPTED("scripted"),
SVG("svg"),
SWITCH("switch");
private final String name;
ManifestItemProperties(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,17 @@
package me.ag2s.epublib.domain;
@SuppressWarnings("unused")
public enum ManifestItemRefProperties implements ManifestProperties {
PAGE_SPREAD_LEFT("page-spread-left"),
PAGE_SPREAD_RIGHT("page-spread-right");
private final String name;
ManifestItemRefProperties(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,6 @@
package me.ag2s.epublib.domain;
public interface ManifestProperties {
String getName();
}
@@ -0,0 +1,73 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
/**
* MediaType is used to tell the type of content a resource is.
* <p>
* Examples of mediatypes are image/gif, text/css and application/xhtml+xml
* <p>
* All allowed mediaTypes are maintained bye the MediaTypeService.
*
* @author paul
* @see MediaTypes
*/
public class MediaType implements Serializable {
private static final long serialVersionUID = -7256091153727506788L;
private final String name;
private final String defaultExtension;
private final Collection<String> extensions;
public MediaType(String name, String defaultExtension) {
this(name, defaultExtension, new String[]{defaultExtension});
}
public MediaType(String name, String defaultExtension,
String[] extensions) {
this(name, defaultExtension, Arrays.asList(extensions));
}
public int hashCode() {
if (name == null) {
return 0;
}
return name.hashCode();
}
public MediaType(String name, String defaultExtension,
Collection<String> mextensions) {
super();
this.name = name;
this.defaultExtension = defaultExtension;
this.extensions = mextensions;
}
public String getName() {
return name;
}
public String getDefaultExtension() {
return defaultExtension;
}
public Collection<String> getExtensions() {
return extensions;
}
public boolean equals(Object otherMediaType) {
if (!(otherMediaType instanceof MediaType)) {
return false;
}
return name.equals(((MediaType) otherMediaType).getName());
}
@SuppressWarnings("NullableProblems")
public String toString() {
return name;
}
}
@@ -0,0 +1,95 @@
package me.ag2s.epublib.domain;
import java.util.HashMap;
import java.util.Map;
import me.ag2s.epublib.util.StringUtil;
/**
* Manages mediatypes that are used by epubs
*
* @author paul
*/
public class MediaTypes {
public static final MediaType XHTML = new MediaType("application/xhtml+xml",
".xhtml", new String[]{".htm", ".html", ".xhtml"});
public static final MediaType EPUB = new MediaType("application/epub+zip",
".epub");
public static final MediaType NCX = new MediaType("application/x-dtbncx+xml",
".ncx");
public static final MediaType JAVASCRIPT = new MediaType("text/javascript",
".js");
public static final MediaType CSS = new MediaType("text/css", ".css");
// images
public static final MediaType JPG = new MediaType("image/jpeg", ".jpg",
new String[]{".jpg", ".jpeg"});
public static final MediaType PNG = new MediaType("image/png", ".png");
public static final MediaType GIF = new MediaType("image/gif", ".gif");
public static final MediaType SVG = new MediaType("image/svg+xml", ".svg");
// fonts
public static final MediaType TTF = new MediaType(
"application/x-truetype-font", ".ttf");
public static final MediaType OPENTYPE = new MediaType(
"application/vnd.ms-opentype", ".otf");
public static final MediaType WOFF = new MediaType("application/font-woff",
".woff");
// audio
public static final MediaType MP3 = new MediaType("audio/mpeg", ".mp3");
public static final MediaType OGG = new MediaType("audio/ogg", ".ogg");
// video
public static final MediaType MP4 = new MediaType("video/mp4", ".mp4");
public static final MediaType SMIL = new MediaType("application/smil+xml",
".smil");
public static final MediaType XPGT = new MediaType(
"application/adobe-page-template+xml", ".xpgt");
public static final MediaType PLS = new MediaType("application/pls+xml",
".pls");
public static final MediaType[] mediaTypes = new MediaType[]{
XHTML, EPUB, JPG, PNG, GIF, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF,
SMIL, PLS, JAVASCRIPT, MP3, MP4, OGG
};
public static final Map<String, MediaType> mediaTypesByName = new HashMap<>();
static {
for (MediaType mediaType : mediaTypes) {
mediaTypesByName.put(mediaType.getName(), mediaType);
}
}
public static boolean isBitmapImage(MediaType mediaType) {
return mediaType == JPG || mediaType == PNG || mediaType == GIF;
}
/**
* Gets the MediaType based on the file extension.
* Null of no matching extension found.
*
* @param filename filename
* @return the MediaType based on the file extension.
*/
public static MediaType determineMediaType(String filename) {
for (MediaType mediaType : mediaTypesByName.values()) {
for (String extension : mediaType.getExtensions()) {
if (StringUtil.endsWithIgnoreCase(filename, extension)) {
return mediaType;
}
}
}
return null;
}
public static MediaType getMediaTypeByName(String mediaTypeName) {
return mediaTypesByName.get(mediaTypeName);
}
}
@@ -0,0 +1,241 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import me.ag2s.epublib.util.StringUtil;
/**
* A Book's collection of Metadata.
* In the future it should contain all Dublin Core attributes, for now
* it contains a set of often-used ones.
*
* @author paul
*/
public class Metadata implements Serializable {
private static final long serialVersionUID = -2437262888962149444L;
public static final String DEFAULT_LANGUAGE = "en";
private boolean autoGeneratedId;//true;
private List<Author> authors = new ArrayList<>();
private List<Author> contributors = new ArrayList<>();
private List<Date> dates = new ArrayList<>();
private String language = DEFAULT_LANGUAGE;
private Map<QName, String> otherProperties = new HashMap<>();
private List<String> rights = new ArrayList<>();
private List<String> titles = new ArrayList<>();
private List<Identifier> identifiers = new ArrayList<>();
private List<String> subjects = new ArrayList<>();
private String format = MediaTypes.EPUB.getName();
private List<String> types = new ArrayList<>();
private List<String> descriptions = new ArrayList<>();
private List<String> publishers = new ArrayList<>();
private Map<String, String> metaAttributes = new HashMap<>();
public Metadata() {
identifiers.add(new Identifier());
autoGeneratedId = true;
}
@SuppressWarnings("unused")
public boolean isAutoGeneratedId() {
return autoGeneratedId;
}
/**
* Metadata properties not hard-coded like the author, title, etc.
*
* @return Metadata properties not hard-coded like the author, title, etc.
*/
public Map<QName, String> getOtherProperties() {
return otherProperties;
}
public void setOtherProperties(Map<QName, String> otherProperties) {
this.otherProperties = otherProperties;
}
@SuppressWarnings("unused")
public Date addDate(Date date) {
this.dates.add(date);
return date;
}
public List<Date> getDates() {
return dates;
}
public void setDates(List<Date> dates) {
this.dates = dates;
}
@SuppressWarnings("UnusedReturnValue")
public Author addAuthor(Author author) {
authors.add(author);
return author;
}
public List<Author> getAuthors() {
return authors;
}
public void setAuthors(List<Author> authors) {
this.authors = authors;
}
@SuppressWarnings("UnusedReturnValue")
public Author addContributor(Author contributor) {
contributors.add(contributor);
return contributor;
}
public List<Author> getContributors() {
return contributors;
}
public void setContributors(List<Author> contributors) {
this.contributors = contributors;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public List<String> getSubjects() {
return subjects;
}
public void setSubjects(List<String> subjects) {
this.subjects = subjects;
}
public void setRights(List<String> rights) {
this.rights = rights;
}
public List<String> getRights() {
return rights;
}
/**
* Gets the first non-blank title of the book.
* Will return "" if no title found.
*
* @return the first non-blank title of the book.
*/
public String getFirstTitle() {
if (titles == null || titles.isEmpty()) {
return "";
}
for (String title : titles) {
if (StringUtil.isNotBlank(title)) {
return title;
}
}
return "";
}
public String addTitle(String title) {
this.titles.add(title);
return title;
}
public void setTitles(List<String> titles) {
this.titles = titles;
}
public List<String> getTitles() {
return titles;
}
@SuppressWarnings("UnusedReturnValue")
public String addPublisher(String publisher) {
this.publishers.add(publisher);
return publisher;
}
public void setPublishers(List<String> publishers) {
this.publishers = publishers;
}
public List<String> getPublishers() {
return publishers;
}
@SuppressWarnings("UnusedReturnValue")
public String addDescription(String description) {
this.descriptions.add(description);
return description;
}
public void setDescriptions(List<String> descriptions) {
this.descriptions = descriptions;
}
public List<String> getDescriptions() {
return descriptions;
}
@SuppressWarnings("unused")
public Identifier addIdentifier(Identifier identifier) {
if (autoGeneratedId && (!(identifiers.isEmpty()))) {
identifiers.set(0, identifier);
} else {
identifiers.add(identifier);
}
autoGeneratedId = false;
return identifier;
}
public void setIdentifiers(List<Identifier> identifiers) {
this.identifiers = identifiers;
autoGeneratedId = false;
}
public List<Identifier> getIdentifiers() {
return identifiers;
}
public void setFormat(String format) {
this.format = format;
}
public String getFormat() {
return format;
}
@SuppressWarnings("UnusedReturnValue")
public String addType(String type) {
this.types.add(type);
return type;
}
public List<String> getTypes() {
return types;
}
public void setTypes(List<String> types) {
this.types = types;
}
@SuppressWarnings("unused")
public String getMetaAttribute(String name) {
return metaAttributes.get(name);
}
public void setMetaAttributes(Map<String, String> metaAttributes) {
this.metaAttributes = metaAttributes;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,336 @@
package me.ag2s.epublib.domain;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.util.IOUtil;
import me.ag2s.epublib.util.StringUtil;
import me.ag2s.epublib.util.commons.io.XmlStreamReader;
/**
* Represents a resource that is part of the epub.
* A resource can be a html file, image, xml, etc.
*
* @author paul
*/
public class Resource implements Serializable {
private static final long serialVersionUID = 1043946707835004037L;
private String id;
private String title;
private String href;
private String properties;
protected final String originalHref;
private MediaType mediaType;
private String inputEncoding;
protected byte[] data;
/**
* Creates an empty Resource with the given href.
* <p>
* Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8
*
* @param href The location of the resource within the epub. Example: "chapter1.html".
*/
public Resource(String href) {
this(null, new byte[0], href, MediaTypes.determineMediaType(href));
}
/**
* Creates a Resource with the given data and MediaType.
* The href will be automatically generated.
* <p>
* Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8
*
* @param data The Resource's contents
* @param mediaType The MediaType of the Resource
*/
public Resource(byte[] data, MediaType mediaType) {
this(null, data, null, mediaType);
}
/**
* Creates a resource with the given data at the specified href.
* The MediaType will be determined based on the href extension.
* <p>
* Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8
*
* @param data The Resource's contents
* @param href The location of the resource within the epub. Example: "chapter1.html".
* @see MediaTypes#determineMediaType(String)
*/
public Resource(byte[] data, String href) {
this(null, data, href, MediaTypes.determineMediaType(href),
Constants.CHARACTER_ENCODING);
}
/**
* Creates a resource with the data from the given Reader at the specified href.
* The MediaType will be determined based on the href extension.
*
* @param in The Resource's contents
* @param href The location of the resource within the epub. Example: "cover.jpg".
* @see MediaTypes#determineMediaType(String)
*/
public Resource(Reader in, String href) throws IOException {
this(null, IOUtil.toByteArray(in, Constants.CHARACTER_ENCODING), href,
MediaTypes.determineMediaType(href),
Constants.CHARACTER_ENCODING);
}
/**
* Creates a resource with the data from the given InputStream at the specified href.
* The MediaType will be determined based on the href extension.
*
* @param in The Resource's contents
* @param href The location of the resource within the epub. Example: "cover.jpg".
* @see MediaTypes#determineMediaType(String)
* <p>
* Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8
* <p>
* It is recommended to us the {@link #Resource(Reader, String)} method for creating textual
* (html/css/etc) resources to prevent encoding problems.
* Use this method only for binary Resources like images, fonts, etc.
*/
public Resource(InputStream in, String href) throws IOException {
this(null, IOUtil.toByteArray(in), href,
MediaTypes.determineMediaType(href));
}
/**
* Creates a resource with the given id, data, mediatype at the specified href.
* Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8
*
* @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value.
* @param data The Resource's contents
* @param href The location of the resource within the epub. Example: "chapter1.html".
* @param mediaType The resources MediaType
*/
public Resource(String id, byte[] data, String href, MediaType mediaType) {
this(id, data, href, mediaType, Constants.CHARACTER_ENCODING);
}
public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType) {
this(id, data, href, originalHref, mediaType, Constants.CHARACTER_ENCODING);
}
/**
* Creates a resource with the given id, data, mediatype at the specified href.
* If the data is of a text type (html/css/etc) then it will use the given inputEncoding.
*
* @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value.
* @param data The Resource's contents
* @param href The location of the resource within the epub. Example: "chapter1.html".
* @param mediaType The resources MediaType
* @param inputEncoding If the data is of a text type (html/css/etc) then it will use the given inputEncoding.
*/
public Resource(String id, byte[] data, String href, MediaType mediaType,
String inputEncoding) {
this.id = id;
this.href = href;
this.originalHref = href;
this.mediaType = mediaType;
this.inputEncoding = inputEncoding;
this.data = data;
}
public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType,
String inputEncoding) {
this.id = id;
this.href = href;
this.originalHref = originalHref;
this.mediaType = mediaType;
this.inputEncoding = inputEncoding;
this.data = data;
}
/**
* Gets the contents of the Resource as an InputStream.
*
* @return The contents of the Resource.
* @throws IOException IOException
*/
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(getData());
}
/**
* The contents of the resource as a byte[]
*
* @return The contents of the resource
*/
public byte[] getData() throws IOException {
return data;
}
/**
* Tells this resource to release its cached data.
* <p>
* If this resource was not lazy-loaded, this is a no-op.
*/
public void close() {
}
/**
* Sets the data of the Resource.
* If the data is a of a different type then the original data then make sure to change the MediaType.
*
* @param data the data of the Resource
*/
public void setData(byte[] data) {
this.data = data;
}
/**
* Returns the size of this resource in bytes.
*
* @return the size.
*/
public long getSize() {
return data.length;
}
/**
* If the title is found by scanning the underlying html document then it is cached here.
*
* @return the title
*/
public String getTitle() {
return title;
}
/**
* Sets the Resource's id: Make sure it is unique and a valid identifier.
*
* @param id Resource's id
*/
public void setId(String id) {
this.id = id;
}
/**
* The resources Id.
* <p>
* Must be both unique within all the resources of this book and a valid identifier.
*
* @return The resources Id.
*/
public String getId() {
return id;
}
/**
* The location of the resource within the contents folder of the epub file.
* <p>
* Example:<br/>
* images/cover.jpg<br/>
* content/chapter1.xhtml<br/>
*
* @return The location of the resource within the contents folder of the epub file.
*/
public String getHref() {
return href;
}
/**
* Sets the Resource's href.
*
* @param href Resource's href.
*/
public void setHref(String href) {
this.href = href;
}
/**
* The character encoding of the resource.
* Is allowed to be null for non-text resources like images.
*
* @return The character encoding of the resource.
*/
public String getInputEncoding() {
return inputEncoding;
}
/**
* Sets the Resource's input character encoding.
*
* @param encoding Resource's input character encoding.
*/
public void setInputEncoding(String encoding) {
this.inputEncoding = encoding;
}
/**
* Gets the contents of the Resource as Reader.
* <p>
* Does all sorts of smart things (courtesy of apache commons io XMLStreamREader) to handle encodings, byte order markers, etc.
*
* @return the contents of the Resource as Reader.
* @throws IOException IOException
*/
public Reader getReader() throws IOException {
return new XmlStreamReader(new ByteArrayInputStream(getData()),
getInputEncoding());
}
/**
* Gets the hashCode of the Resource's href.
*/
public int hashCode() {
return href.hashCode();
}
/**
* Checks to see of the given resourceObject is a resource and whether its href is equal to this one.
*
* @return whether the given resourceObject is a resource and whether its href is equal to this one.
*/
public boolean equals(Object resourceObject) {
if (!(resourceObject instanceof Resource)) {
return false;
}
return href.equals(((Resource) resourceObject).getHref());
}
/**
* This resource's mediaType.
*
* @return This resource's mediaType.
*/
public MediaType getMediaType() {
return mediaType;
}
public void setMediaType(MediaType mediaType) {
this.mediaType = mediaType;
}
public void setTitle(String title) {
this.title = title;
}
public String getProperties() {
return properties;
}
public void setProperties(String properties) {
this.properties = properties;
}
@SuppressWarnings("NullableProblems")
public String toString() {
return StringUtil.toString("id", id,
"title", title,
"encoding", inputEncoding,
"mediaType", mediaType,
"href", href,
"size", (data == null ? 0 : data.length));
}
}
@@ -0,0 +1,33 @@
package me.ag2s.epublib.domain;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* A wrapper class for closing a AndroidZipFile object when the InputStream derived
* from it is closed.
*
* @author ttopalov
*/
public class ResourceInputStream extends FilterInputStream {
//private final ZipFile zipFile;
/**
* Constructor.
*
* @param in The InputStream object.
*/
public ResourceInputStream(InputStream in) {
super(in);
//this.zipFile = zipFile;
}
@Override
public void close() throws IOException {
super.close();
//zipFile.close();
}
}
@@ -0,0 +1,43 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
public class ResourceReference implements Serializable {
private static final long serialVersionUID = 2596967243557743048L;
protected Resource resource;
public ResourceReference(Resource resource) {
this.resource = resource;
}
public Resource getResource() {
return resource;
}
/**
* Besides setting the resource it also sets the fragmentId to null.
*
* @param resource resource
*/
public void setResource(Resource resource) {
this.resource = resource;
}
/**
* The id of the reference referred to.
* <p>
* null of the reference is null or has a null id itself.
*
* @return The id of the reference referred to.
*/
public String getResourceId() {
if (resource != null) {
return resource.getId();
}
return null;
}
}
@@ -0,0 +1,417 @@
package me.ag2s.epublib.domain;
import android.util.Base64;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.util.StringUtil;
/**
* All the resources that make up the book.
* XHTML files, images and epub xml documents must be here.
*
* @author paul
*/
public class Resources implements Serializable {
private static final long serialVersionUID = 2450876953383871451L;
private static final String IMAGE_PREFIX = "image_";
private static final String ITEM_PREFIX = "item_";
private int lastId = 1;
private Map<String, Resource> resources = new HashMap<>();
/**
* Adds a resource to the resources.
* <p>
* Fixes the resources id and href if necessary.
*
* @param resource resource
* @return the newly added resource
*/
public Resource add(Resource resource) {
fixResourceHref(resource);
fixResourceId(resource);
this.resources.put(resource.getHref(), resource);
return resource;
}
/**
* Checks the id of the given resource and changes to a unique identifier if it isn't one already.
*
* @param resource resource
*/
public void fixResourceId(Resource resource) {
String resourceId = resource.getId();
// first try and create a unique id based on the resource's href
if (StringUtil.isBlank(resource.getId())) {
resourceId = StringUtil.substringBeforeLast(resource.getHref(), '.');
resourceId = StringUtil.substringAfterLast(resourceId, '/');
}
resourceId = makeValidId(resourceId, resource);
// check if the id is unique. if not: create one from scratch
if (StringUtil.isBlank(resourceId) || containsId(resourceId)) {
resourceId = createUniqueResourceId(resource);
}
resource.setId(resourceId);
}
/**
* Check if the id is a valid identifier. if not: prepend with valid identifier
*
* @param resource resource
* @return a valid id
*/
private String makeValidId(String resourceId, Resource resource) {
if (StringUtil.isNotBlank(resourceId) && !Character
.isJavaIdentifierStart(resourceId.charAt(0))) {
resourceId = getResourceItemPrefix(resource) + resourceId;
}
return resourceId;
}
private String getResourceItemPrefix(Resource resource) {
String result;
if (MediaTypes.isBitmapImage(resource.getMediaType())) {
result = IMAGE_PREFIX;
} else {
result = ITEM_PREFIX;
}
return result;
}
/**
* Creates a new resource id that is guaranteed to be unique for this set of Resources
*
* @param resource resource
* @return a new resource id that is guaranteed to be unique for this set of Resources
*/
private String createUniqueResourceId(Resource resource) {
int counter = lastId;
if (counter == Integer.MAX_VALUE) {
if (resources.size() == Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Resources contains " + Integer.MAX_VALUE
+ " elements: no new elements can be added");
} else {
counter = 1;
}
}
String prefix = getResourceItemPrefix(resource);
String result = prefix + counter;
while (containsId(result)) {
result = prefix + (++counter);
}
lastId = counter;
return result;
}
/**
* Whether the map of resources already contains a resource with the given id.
*
* @param id id
* @return Whether the map of resources already contains a resource with the given id.
*/
public boolean containsId(String id) {
if (StringUtil.isBlank(id)) {
return false;
}
for (Resource resource : resources.values()) {
if (id.equals(resource.getId())) {
return true;
}
}
return false;
}
/**
* Gets the resource with the given id.
*
* @param id id
* @return null if not found
*/
public Resource getById(String id) {
if (StringUtil.isBlank(id)) {
return null;
}
for (Resource resource : resources.values()) {
if (id.equals(resource.getId())) {
return resource;
}
}
return null;
}
public Resource getByProperties(String properties) {
if (StringUtil.isBlank(properties)) {
return null;
}
for (Resource resource : resources.values()) {
if (properties.equals(resource.getProperties())) {
return resource;
}
}
return null;
}
/**
* Remove the resource with the given href.
*
* @param href href
* @return the removed resource, null if not found
*/
public Resource remove(String href) {
return resources.remove(href);
}
private void fixResourceHref(Resource resource) {
if (StringUtil.isNotBlank(resource.getHref())
&& !resources.containsKey(resource.getHref())) {
return;
}
if (StringUtil.isBlank(resource.getHref())) {
if (resource.getMediaType() == null) {
throw new IllegalArgumentException(
"Resource must have either a MediaType or a href");
}
int i = 1;
String href = createHref(resource.getMediaType(), i);
while (resources.containsKey(href)) {
href = createHref(resource.getMediaType(), (++i));
}
resource.setHref(href);
}
}
private String createHref(MediaType mediaType, int counter) {
if (MediaTypes.isBitmapImage(mediaType)) {
return IMAGE_PREFIX + counter + mediaType.getDefaultExtension();
} else {
return ITEM_PREFIX + counter + mediaType.getDefaultExtension();
}
}
public boolean isEmpty() {
return resources.isEmpty();
}
/**
* The number of resources
*
* @return The number of resources
*/
public int size() {
return resources.size();
}
/**
* The resources that make up this book.
* Resources can be xhtml pages, images, xml documents, etc.
*
* @return The resources that make up this book.
*/
@SuppressWarnings("unused")
public Map<String, Resource> getResourceMap() {
return resources;
}
public Collection<Resource> getAll() {
return resources.values();
}
/**
* Whether there exists a resource with the given href
*
* @param href href
* @return Whether there exists a resource with the given href
*/
public boolean notContainsByHref(String href) {
if (StringUtil.isBlank(href)) {
return true;
} else {
return !resources.containsKey(
StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR));
}
}
/**
* Whether there exists a resource with the given href
*
* @param href href
* @return Whether there exists a resource with the given href
*/
@SuppressWarnings("unused")
public boolean containsByHref(String href) {
return !notContainsByHref(href);
}
/**
* Sets the collection of Resources to the given collection of resources
*
* @param resources resources
*/
public void set(Collection<Resource> resources) {
this.resources.clear();
addAll(resources);
}
/**
* Adds all resources from the given Collection of resources to the existing collection.
*
* @param resources resources
*/
public void addAll(Collection<Resource> resources) {
for (Resource resource : resources) {
fixResourceHref(resource);
this.resources.put(resource.getHref(), resource);
}
}
/**
* Sets the collection of Resources to the given collection of resources
*
* @param resources A map with as keys the resources href and as values the Resources
*/
public void set(Map<String, Resource> resources) {
this.resources = new HashMap<>(resources);
}
/**
* First tries to find a resource with as id the given idOrHref, if that
* fails it tries to find one with the idOrHref as href.
*
* @param idOrHref idOrHref
* @return the found Resource
*/
public Resource getByIdOrHref(String idOrHref) {
Resource resource = getById(idOrHref);
if (resource == null) {
resource = getByHref(idOrHref);
}
return resource;
}
/**
* Gets the resource with the given href.
* If the given href contains a fragmentId then that fragment id will be ignored.
*
* @param href href
* @return null if not found.
*/
public Resource getByHref(String href) {
if (StringUtil.isBlank(href)) {
return null;
}
href = StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR);
Pattern dataUriRegex = Pattern.compile("data:([\\w/\\-\\.]+);base64,(.*)");
Matcher dataUriMatcher = dataUriRegex.matcher(href);
if (dataUriMatcher.find()) {
String dataUriMediaTypeString = dataUriMatcher.group(1);
MediaType dataUriMediaType = new MediaType(dataUriMediaTypeString, "." + StringUtil.substringAfterLast(dataUriMediaTypeString, '/'));
byte[] dataUriData = Base64.decode(dataUriMatcher.group(2), Base64.DEFAULT);
return new Resource(dataUriData, dataUriMediaType);
} else {
return resources.get(href);
}
}
/**
* Gets the first resource (random order) with the give mediatype.
* <p>
* Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype.
*
* @param mediaType mediaType
* @return the first resource (random order) with the give mediatype.
*/
public Resource findFirstResourceByMediaType(MediaType mediaType) {
return findFirstResourceByMediaType(resources.values(), mediaType);
}
/**
* Gets the first resource (random order) with the give mediatype.
* <p>
* Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype.
*
* @param mediaType mediaType
* @return the first resource (random order) with the give mediatype.
*/
public static Resource findFirstResourceByMediaType(
Collection<Resource> resources, MediaType mediaType) {
for (Resource resource : resources) {
if (resource.getMediaType() == mediaType) {
return resource;
}
}
return null;
}
/**
* All resources that have the given MediaType.
*
* @param mediaType mediaType
* @return All resources that have the given MediaType.
*/
public List<Resource> getResourcesByMediaType(MediaType mediaType) {
List<Resource> result = new ArrayList<>();
if (mediaType == null) {
return result;
}
for (Resource resource : getAll()) {
if (resource.getMediaType() == mediaType) {
result.add(resource);
}
}
return result;
}
/**
* All Resources that match any of the given list of MediaTypes
*
* @param mediaTypes mediaType
* @return All Resources that match any of the given list of MediaTypes
*/
@SuppressWarnings("unused")
public List<Resource> getResourcesByMediaTypes(MediaType[] mediaTypes) {
List<Resource> result = new ArrayList<>();
if (mediaTypes == null) {
return result;
}
// this is the fastest way of doing this according to
// http://stackoverflow.com/questions/1128723/in-java-how-can-i-test-if-an-array-contains-a-certain-value
List<MediaType> mediaTypesList = Arrays.asList(mediaTypes);
for (Resource resource : getAll()) {
if (mediaTypesList.contains(resource.getMediaType())) {
result.add(resource);
}
}
return result;
}
/**
* All resource hrefs
*
* @return all resource hrefs
*/
public Collection<String> getAllHrefs() {
return resources.keySet();
}
}
@@ -0,0 +1,190 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import me.ag2s.epublib.util.StringUtil;
/**
* The spine sections are the sections of the book in the order in which the book should be read.
* <p>
* This contrasts with the Table of Contents sections which is an index into the Book's sections.
*
* @author paul
* @see TableOfContents
*/
public class Spine implements Serializable {
private static final long serialVersionUID = 3878483958947357246L;
private Resource tocResource;
private List<SpineReference> spineReferences;
public Spine() {
this(new ArrayList<>());
}
/**
* Creates a spine out of all the resources in the table of contents.
*
* @param tableOfContents tableOfContents
*/
public Spine(TableOfContents tableOfContents) {
this.spineReferences = createSpineReferences(
tableOfContents.getAllUniqueResources());
}
public Spine(List<SpineReference> spineReferences) {
this.spineReferences = spineReferences;
}
public static List<SpineReference> createSpineReferences(
Collection<Resource> resources) {
List<SpineReference> result = new ArrayList<>(
resources.size());
for (Resource resource : resources) {
result.add(new SpineReference(resource));
}
return result;
}
public List<SpineReference> getSpineReferences() {
return spineReferences;
}
public void setSpineReferences(List<SpineReference> spineReferences) {
this.spineReferences = spineReferences;
}
/**
* Gets the resource at the given index.
* Null if not found.
*
* @param index index
* @return the resource at the given index.
*/
public Resource getResource(int index) {
if (index < 0 || index >= spineReferences.size()) {
return null;
}
return spineReferences.get(index).getResource();
}
/**
* Finds the first resource that has the given resourceId.
* <p>
* Null if not found.
*
* @param resourceId resourceId
* @return the first resource that has the given resourceId.
*/
public int findFirstResourceById(String resourceId) {
if (StringUtil.isBlank(resourceId)) {
return -1;
}
for (int i = 0; i < spineReferences.size(); i++) {
SpineReference spineReference = spineReferences.get(i);
if (resourceId.equals(spineReference.getResourceId())) {
return i;
}
}
return -1;
}
/**
* Adds the given spineReference to the spine references and returns it.
*
* @param spineReference spineReference
* @return the given spineReference
*/
public SpineReference addSpineReference(SpineReference spineReference) {
if (spineReferences == null) {
this.spineReferences = new ArrayList<>();
}
spineReferences.add(spineReference);
return spineReference;
}
/**
* Adds the given resource to the spine references and returns it.
*
* @return the given spineReference
*/
@SuppressWarnings("unused")
public SpineReference addResource(Resource resource) {
return addSpineReference(new SpineReference(resource));
}
/**
* The number of elements in the spine.
*
* @return The number of elements in the spine.
*/
public int size() {
return spineReferences.size();
}
/**
* As per the epub file format the spine officially maintains a reference to the Table of Contents.
* The epubwriter will look for it here first, followed by some clever tricks to find it elsewhere if not found.
* Put it here to be sure of the expected behaviours.
*
* @param tocResource tocResource
*/
public void setTocResource(Resource tocResource) {
this.tocResource = tocResource;
}
/**
* The resource containing the XML for the tableOfContents.
* When saving an epub file this resource needs to be in this place.
*
* @return The resource containing the XML for the tableOfContents.
*/
public Resource getTocResource() {
return tocResource;
}
/**
* The position within the spine of the given resource.
*
* @param currentResource currentResource
* @return something &lt; 0 if not found.
*/
public int getResourceIndex(Resource currentResource) {
if (currentResource == null) {
return -1;
}
return getResourceIndex(currentResource.getHref());
}
/**
* The first position within the spine of a resource with the given href.
*
* @return something &lt; 0 if not found.
*/
public int getResourceIndex(String resourceHref) {
int result = -1;
if (StringUtil.isBlank(resourceHref)) {
return result;
}
for (int i = 0; i < spineReferences.size(); i++) {
if (resourceHref.equals(spineReferences.get(i).getResource().getHref())) {
result = i;
break;
}
}
return result;
}
/**
* Whether the spine has any references
*
* @return Whether the spine has any references
*/
public boolean isEmpty() {
return spineReferences.isEmpty();
}
}
@@ -0,0 +1,52 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
/**
* A Section of a book.
* Represents both an item in the package document and a item in the index.
*
* @author paul
*/
public class SpineReference extends ResourceReference implements Serializable {
private static final long serialVersionUID = -7921609197351510248L;
private boolean linear;//default = true;
public SpineReference(Resource resource) {
this(resource, true);
}
public SpineReference(Resource resource, boolean linear) {
super(resource);
this.linear = linear;
}
/**
* Linear denotes whether the section is Primary or Auxiliary.
* Usually the cover page has linear set to false and all the other sections
* have it set to true.
* <p>
* It's an optional property that readers may also ignore.
*
* <blockquote>primary or auxiliary is useful for Reading Systems which
* opt to present auxiliary content differently than primary content.
* For example, a Reading System might opt to render auxiliary content in
* a popup window apart from the main window which presents the primary
* content. (For an example of the types of content that may be considered
* auxiliary, refer to the example below and the subsequent discussion.)</blockquote>
*
* @return whether the section is Primary or Auxiliary.
* @see <a href="http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4">OPF Spine specification</a>
*/
public boolean isLinear() {
return linear;
}
public void setLinear(boolean linear) {
this.linear = linear;
}
}
@@ -0,0 +1,57 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* An item in the Table of Contents.
*
* @author paul
* @see TableOfContents
*/
public class TOCReference extends TitledResourceReference
implements Serializable {
private static final long serialVersionUID = 5787958246077042456L;
private List<TOCReference> children;
private static final Comparator<TOCReference> COMPARATOR_BY_TITLE_IGNORE_CASE = (tocReference1, tocReference2) -> String.CASE_INSENSITIVE_ORDER.compare(tocReference1.getTitle(), tocReference2.getTitle());
@Deprecated
public TOCReference() {
this(null, null, null);
}
public TOCReference(String name, Resource resource) {
this(name, resource, null);
}
public TOCReference(String name, Resource resource, String fragmentId) {
this(name, resource, fragmentId, new ArrayList<>());
}
public TOCReference(String title, Resource resource, String fragmentId,
List<TOCReference> children) {
super(resource, title, fragmentId);
this.children = children;
}
@SuppressWarnings("unused")
public static Comparator<TOCReference> getComparatorByTitleIgnoreCase() {
return COMPARATOR_BY_TITLE_IGNORE_CASE;
}
public List<TOCReference> getChildren() {
return children;
}
public TOCReference addChildSection(TOCReference childSection) {
this.children.add(childSection);
return childSection;
}
public void setChildren(List<TOCReference> children) {
this.children = children;
}
}
@@ -0,0 +1,264 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* The table of contents of the book.
* The TableOfContents is a tree structure at the root it is a list of TOCReferences, each if which may have as children another list of TOCReferences.
* <p>
* The table of contents is used by epub as a quick index to chapters and sections within chapters.
* It may contain duplicate entries, may decide to point not to certain chapters, etc.
* <p>
* See the spine for the complete list of sections in the order in which they should be read.
*
* @author paul
* @see Spine
*/
public class TableOfContents implements Serializable {
private static final long serialVersionUID = -3147391239966275152L;
public static final String DEFAULT_PATH_SEPARATOR = "/";
private List<TOCReference> tocReferences;
public TableOfContents() {
this(new ArrayList<>());
}
public TableOfContents(List<TOCReference> tocReferences) {
this.tocReferences = tocReferences;
}
public List<TOCReference> getTocReferences() {
return tocReferences;
}
public void setTocReferences(List<TOCReference> tocReferences) {
this.tocReferences = tocReferences;
}
/**
* Calls addTOCReferenceAtLocation after splitting the path using the DEFAULT_PATH_SEPARATOR.
*
* @return the new TOCReference
*/
@SuppressWarnings("unused")
public TOCReference addSection(Resource resource, String path) {
return addSection(resource, path, DEFAULT_PATH_SEPARATOR);
}
/**
* Calls addTOCReferenceAtLocation after splitting the path using the given pathSeparator.
*
* @param resource resource
* @param path path
* @param pathSeparator pathSeparator
* @return the new TOCReference
*/
public TOCReference addSection(Resource resource, String path,
String pathSeparator) {
String[] pathElements = path.split(pathSeparator);
return addSection(resource, pathElements);
}
/**
* Finds the first TOCReference in the given list that has the same title as the given Title.
*
* @param title title
* @param tocReferences tocReferences
* @return null if not found.
*/
private static TOCReference findTocReferenceByTitle(String title,
List<TOCReference> tocReferences) {
for (TOCReference tocReference : tocReferences) {
if (title.equals(tocReference.getTitle())) {
return tocReference;
}
}
return null;
}
/**
* Adds the given Resources to the TableOfContents at the location specified by the pathElements.
* <p>
* Example:
* Calling this method with a Resource and new String[] {"chapter1", "paragraph1"} will result in the following:
* <ul>
* <li>a TOCReference with the title "chapter1" at the root level.<br/>
* If this TOCReference did not yet exist it will have been created and does not point to any resource</li>
* <li>A TOCReference that has the title "paragraph1". This TOCReference will be the child of TOCReference "chapter1" and
* will point to the given Resource</li>
* </ul>
*
* @param resource resource
* @param pathElements pathElements
* @return the new TOCReference
*/
public TOCReference addSection(Resource resource, String[] pathElements) {
if (pathElements == null || pathElements.length == 0) {
return null;
}
TOCReference result = null;
List<TOCReference> currentTocReferences = this.tocReferences;
for (String currentTitle : pathElements) {
result = findTocReferenceByTitle(currentTitle, currentTocReferences);
if (result == null) {
result = new TOCReference(currentTitle, null);
currentTocReferences.add(result);
}
currentTocReferences = result.getChildren();
}
result.setResource(resource);
return result;
}
/**
* Adds the given Resources to the TableOfContents at the location specified by the pathElements.
* <p>
* Example:
* Calling this method with a Resource and new int[] {0, 0} will result in the following:
* <ul>
* <li>a TOCReference at the root level.<br/>
* If this TOCReference did not yet exist it will have been created with a title of "" and does not point to any resource</li>
* <li>A TOCReference that points to the given resource and is a child of the previously created TOCReference.<br/>
* If this TOCReference didn't exist yet it will be created and have a title of ""</li>
* </ul>
*
* @param resource resource
* @param pathElements pathElements
* @return the new TOCReference
*/
@SuppressWarnings("unused")
public TOCReference addSection(Resource resource, int[] pathElements,
String sectionTitlePrefix, String sectionNumberSeparator) {
if (pathElements == null || pathElements.length == 0) {
return null;
}
TOCReference result = null;
List<TOCReference> currentTocReferences = this.tocReferences;
for (int i = 0; i < pathElements.length; i++) {
int currentIndex = pathElements[i];
if (currentIndex > 0 && currentIndex < (currentTocReferences.size()
- 1)) {
result = currentTocReferences.get(currentIndex);
} else {
result = null;
}
if (result == null) {
paddTOCReferences(currentTocReferences, pathElements, i,
sectionTitlePrefix, sectionNumberSeparator);
result = currentTocReferences.get(currentIndex);
}
currentTocReferences = result.getChildren();
}
result.setResource(resource);
return result;
}
private void paddTOCReferences(List<TOCReference> currentTocReferences,
int[] pathElements, int pathPos, String sectionPrefix,
String sectionNumberSeparator) {
for (int i = currentTocReferences.size(); i <= pathElements[pathPos]; i++) {
String sectionTitle = createSectionTitle(pathElements, pathPos, i,
sectionPrefix,
sectionNumberSeparator);
currentTocReferences.add(new TOCReference(sectionTitle, null));
}
}
private String createSectionTitle(int[] pathElements, int pathPos,
int lastPos,
String sectionPrefix, String sectionNumberSeparator) {
StringBuilder title = new StringBuilder(sectionPrefix);
for (int i = 0; i < pathPos; i++) {
if (i > 0) {
title.append(sectionNumberSeparator);
}
title.append(pathElements[i] + 1);
}
if (pathPos > 0) {
title.append(sectionNumberSeparator);
}
title.append(lastPos + 1);
return title.toString();
}
public TOCReference addTOCReference(TOCReference tocReference) {
if (tocReferences == null) {
tocReferences = new ArrayList<>();
}
tocReferences.add(tocReference);
return tocReference;
}
/**
* All unique references (unique by href) in the order in which they are referenced to in the table of contents.
*
* @return All unique references (unique by href) in the order in which they are referenced to in the table of contents.
*/
public List<Resource> getAllUniqueResources() {
Set<String> uniqueHrefs = new HashSet<>();
List<Resource> result = new ArrayList<>();
getAllUniqueResources(uniqueHrefs, result, tocReferences);
return result;
}
private static void getAllUniqueResources(
Set<String> uniqueHrefs,
List<Resource> result,
List<TOCReference> tocReferences
) {
for (TOCReference tocReference : tocReferences) {
Resource resource = tocReference.getResource();
if (resource != null && !uniqueHrefs.contains(resource.getHref())) {
uniqueHrefs.add(resource.getHref());
result.add(resource);
}
getAllUniqueResources(uniqueHrefs, result, tocReference.getChildren());
}
}
/**
* The total number of references in this table of contents.
*
* @return The total number of references in this table of contents.
*/
public int size() {
return getTotalSize(tocReferences);
}
private static int getTotalSize(Collection<TOCReference> tocReferences) {
int result = tocReferences.size();
for (TOCReference tocReference : tocReferences) {
result += getTotalSize(tocReference.getChildren());
}
return result;
}
/**
* The maximum depth of the reference tree
*
* @return The maximum depth of the reference tree
*/
public int calculateDepth() {
return calculateDepth(tocReferences, 0);
}
private int calculateDepth(List<TOCReference> tocReferences,
int currentDepth) {
int maxChildDepth = 0;
for (TOCReference tocReference : tocReferences) {
int childDepth = calculateDepth(tocReference.getChildren(), 1);
if (childDepth > maxChildDepth) {
maxChildDepth = childDepth;
}
}
return currentDepth + maxChildDepth;
}
}
@@ -0,0 +1,91 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.util.StringUtil;
public class TitledResourceReference extends ResourceReference
implements Serializable {
private static final long serialVersionUID = 3918155020095190080L;
private String fragmentId;
private String title;
/**
* 这会使title为null
*
* @param resource resource
*/
@Deprecated
@SuppressWarnings("unused")
public TitledResourceReference(Resource resource) {
this(resource, null);
}
public TitledResourceReference(Resource resource, String title) {
this(resource, title, null);
}
public TitledResourceReference(Resource resource, String title,
String fragmentId) {
super(resource);
this.title = title;
this.fragmentId = fragmentId;
}
public String getFragmentId() {
return fragmentId;
}
public void setFragmentId(String fragmentId) {
this.fragmentId = fragmentId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
/**
* If the fragmentId is blank it returns the resource href, otherwise
* it returns the resource href + '#' + the fragmentId.
*
* @return If the fragmentId is blank it returns the resource href,
* otherwise it returns the resource href + '#' + the fragmentId.
*/
public String getCompleteHref() {
if (StringUtil.isBlank(fragmentId)) {
return resource.getHref();
} else {
return resource.getHref() + Constants.FRAGMENT_SEPARATOR_CHAR
+ fragmentId;
}
}
@Override
public Resource getResource() {
//resource为null时不设置标题
if (this.resource != null && this.title != null) {
resource.setTitle(title);
}
return resource;
}
public void setResource(Resource resource, String fragmentId) {
super.setResource(resource);
this.fragmentId = fragmentId;
}
/**
* Sets the resource to the given resource and sets the fragmentId to null.
*/
public void setResource(Resource resource) {
setResource(resource, null);
}
}