feat(project): 初始化项目结构和配置

This commit is contained in:
2026-06-10 04:09:02 +08:00
parent 2d301cf1c3
commit 45f883b772
1815 changed files with 315166 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+34
View File
@@ -0,0 +1,34 @@
plugins {
alias libs.plugins.android.library
alias libs.plugins.kotlin.android
}
android {
compileSdk = compile_sdk_version
namespace = 'me.ag2s'
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
defaultConfig {
minSdk 21
targetSdk 36
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
checkDependencies = true
}
}
dependencies {
implementation(libs.androidx.annotation)
//implementation 'androidx.annotation:annotation:1.7.1'
}
View File
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
+102
View File
@@ -0,0 +1,102 @@
package me.ag2s.base;
import static me.ag2s.base.ThrowableUtils.rethrowAsIOException;
import android.os.ParcelFileDescriptor;
import android.system.ErrnoException;
import android.system.OsConstants;
import java.io.EOFException;
import java.io.IOException;
/**
* 读取ParcelFileDescriptor的工具类
*/
@SuppressWarnings("unused")
public final class PfdHelper {
/**
* 读取基本类型的buffer
*/
private static final byte[] readBuffer = new byte[8];
public static void seek(ParcelFileDescriptor pfd, long pos) throws IOException {
try {
android.system.Os.lseek(pfd.getFileDescriptor(), pos, OsConstants.SEEK_SET);
} catch (ErrnoException e) {
throw rethrowAsIOException(e);
}
}
public static long getFilePointer(ParcelFileDescriptor pfd) throws IOException {
try {
return android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_CUR);
} catch (ErrnoException e) {
throw rethrowAsIOException(e);
}
}
public static long length(ParcelFileDescriptor pfd) throws IOException {
try {
return android.system.Os.fstat(pfd.getFileDescriptor()).st_size; //android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_END);
} catch (ErrnoException e) {
throw rethrowAsIOException(e);
}
}
private static int readBytes(ParcelFileDescriptor pfd, byte[] b, int off, int len) throws IOException {
try {
return android.system.Os.read(pfd.getFileDescriptor(), b, off, len);
} catch (ErrnoException e) {
throw rethrowAsIOException(e);
}
}
public static int read(ParcelFileDescriptor pfd) throws IOException {
return (read(pfd, readBuffer, 0, 1) != -1) ? readBuffer[0] & 0xff : -1;
}
public static int read(ParcelFileDescriptor pfd, byte[] b, int off, int len) throws IOException {
return readBytes(pfd, b, off, len);
}
public static int read(ParcelFileDescriptor pfd, byte[] b) throws IOException {
return readBytes(pfd, b, 0, b.length);
}
public static void readFully(ParcelFileDescriptor pfd, byte[] b) throws IOException {
readFully(pfd, b, 0, b.length);
}
public static void readFully(ParcelFileDescriptor pfd, byte[] b, int off, int len) throws IOException {
int n = 0;
do {
int count = read(pfd, b, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;
} while (n < len);
}
public static int skipBytes(ParcelFileDescriptor pfd, int n) throws IOException {
long pos;
long len;
long newpos;
if (n <= 0) {
return 0;
}
pos = getFilePointer(pfd);
len = length(pfd);
newpos = pos + n;
if (newpos > len) {
newpos = len;
}
seek(pfd, newpos);
/* return the actual number of bytes skipped */
return (int) (newpos - pos);
}
}
@@ -0,0 +1,15 @@
package me.ag2s.base;
import androidx.annotation.NonNull;
import java.io.IOException;
public class ThrowableUtils {
public static @NonNull
IOException rethrowAsIOException(Throwable throwable) throws IOException {
IOException newException = new IOException(throwable.getMessage(), throwable);
throw newException;
}
}
+13
View File
@@ -0,0 +1,13 @@
package me.ag2s.epublib;
public interface Constants {
String CHARACTER_ENCODING = "UTF-8";
String DOCTYPE_XHTML = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">";
String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml";
String EPUB_GENERATOR_NAME = "Ag2S EpubLib";
String EPUB_DUOKAN_NAME = "DK-SONGTI";
char FRAGMENT_SEPARATOR_CHAR = '#';
String DEFAULT_TOC_ID = "toc";
}
@@ -0,0 +1,157 @@
package me.ag2s.epublib.browsersupport;
import java.util.EventObject;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.StringUtil;
/**
* Used to tell NavigationEventListener just what kind of navigation action
* the user just did.
*
* @author paul
*/
@SuppressWarnings("unused")
public class NavigationEvent extends EventObject {
private static final long serialVersionUID = -6346750144308952762L;
private Resource oldResource;
private int oldSpinePos;
private Navigator navigator;
private EpubBook oldBook;
private int oldSectionPos;
private String oldFragmentId;
public NavigationEvent(Object source) {
super(source);
}
public NavigationEvent(Object source, Navigator navigator) {
super(source);
this.navigator = navigator;
this.oldBook = navigator.getBook();
this.oldFragmentId = navigator.getCurrentFragmentId();
this.oldSectionPos = navigator.getCurrentSectionPos();
this.oldResource = navigator.getCurrentResource();
this.oldSpinePos = navigator.getCurrentSpinePos();
}
/**
* The previous position within the section.
*
* @return The previous position within the section.
*/
public int getOldSectionPos() {
return oldSectionPos;
}
public Navigator getNavigator() {
return navigator;
}
public String getOldFragmentId() {
return oldFragmentId;
}
// package
void setOldFragmentId(String oldFragmentId) {
this.oldFragmentId = oldFragmentId;
}
public EpubBook getOldBook() {
return oldBook;
}
// package
void setOldPagePos(int oldPagePos) {
this.oldSectionPos = oldPagePos;
}
public int getCurrentSectionPos() {
return navigator.getCurrentSectionPos();
}
public int getOldSpinePos() {
return oldSpinePos;
}
public int getCurrentSpinePos() {
return navigator.getCurrentSpinePos();
}
public String getCurrentFragmentId() {
return navigator.getCurrentFragmentId();
}
public boolean isBookChanged() {
if (oldBook == null) {
return true;
}
return oldBook != navigator.getBook();
}
public boolean isSpinePosChanged() {
return getOldSpinePos() != getCurrentSpinePos();
}
public boolean isFragmentChanged() {
return StringUtil.equals(getOldFragmentId(), getCurrentFragmentId());
}
public Resource getOldResource() {
return oldResource;
}
public Resource getCurrentResource() {
return navigator.getCurrentResource();
}
public void setOldResource(Resource oldResource) {
this.oldResource = oldResource;
}
public void setOldSpinePos(int oldSpinePos) {
this.oldSpinePos = oldSpinePos;
}
public void setNavigator(Navigator navigator) {
this.navigator = navigator;
}
public void setOldBook(EpubBook oldBook) {
this.oldBook = oldBook;
}
public EpubBook getCurrentBook() {
return getNavigator().getBook();
}
public boolean isResourceChanged() {
return oldResource != getCurrentResource();
}
@SuppressWarnings("NullableProblems")
public String toString() {
return StringUtil.toString(
"oldSectionPos", oldSectionPos,
"oldResource", oldResource,
"oldBook", oldBook,
"oldFragmentId", oldFragmentId,
"oldSpinePos", oldSpinePos,
"currentPagePos", getCurrentSectionPos(),
"currentResource", getCurrentResource(),
"currentBook", getCurrentBook(),
"currentFragmentId", getCurrentFragmentId(),
"currentSpinePos", getCurrentSpinePos()
);
}
public boolean isSectionPosChanged() {
return oldSectionPos != getCurrentSectionPos();
}
}
@@ -0,0 +1,17 @@
package me.ag2s.epublib.browsersupport;
/**
* Implemented by classes that want to be notified if the user moves to
* another location in the book.
*
* @author paul
*/
public interface NavigationEventListener {
/**
* Called whenever the user navigates to another position in the book.
*
* @param navigationEvent f
*/
void navigationPerformed(NavigationEvent navigationEvent);
}
@@ -0,0 +1,207 @@
package me.ag2s.epublib.browsersupport;
import java.util.ArrayList;
import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
/**
* A history of the user's locations with the epub.
*
* @author paul.siegmann
*/
public class NavigationHistory implements NavigationEventListener {
public static final int DEFAULT_MAX_HISTORY_SIZE = 1000;
private static final long DEFAULT_HISTORY_WAIT_TIME = 1000;
private static class Location {
private String href;
public Location(String href) {
super();
this.href = href;
}
@SuppressWarnings("unused")
public void setHref(String href) {
this.href = href;
}
public String getHref() {
return href;
}
}
private long lastUpdateTime = 0;
private List<Location> locations = new ArrayList<>();
private final Navigator navigator;
private int currentPos = -1;
private int currentSize = 0;
private int maxHistorySize = DEFAULT_MAX_HISTORY_SIZE;
private long historyWaitTime = DEFAULT_HISTORY_WAIT_TIME;
public NavigationHistory(Navigator navigator) {
this.navigator = navigator;
navigator.addNavigationEventListener(this);
initBook(navigator.getBook());
}
public int getCurrentPos() {
return currentPos;
}
public int getCurrentSize() {
return currentSize;
}
public void initBook(EpubBook book) {
if (book == null) {
return;
}
locations = new ArrayList<>();
currentPos = -1;
currentSize = 0;
if (navigator.getCurrentResource() != null) {
addLocation(navigator.getCurrentResource().getHref());
}
}
/**
* If the time between a navigation event is less than the historyWaitTime
* then the new location is not added to the history.
* <p>
* When a user is rapidly viewing many pages using the slider we do not
* want all of them to be added to the history.
*
* @return the time we wait before adding the page to the history
*/
public long getHistoryWaitTime() {
return historyWaitTime;
}
public void setHistoryWaitTime(long historyWaitTime) {
this.historyWaitTime = historyWaitTime;
}
public void addLocation(Resource resource) {
if (resource == null) {
return;
}
addLocation(resource.getHref());
}
/**
* Adds the location after the current position.
* If the currentposition is not the end of the list then the elements
* between the current element and the end of the list will be discarded.
* <p>
* Does nothing if the new location matches the current location.
* <br/>
* If this nr of locations becomes larger then the historySize then the
* first item(s) will be removed.
* v
*
* @param location d
*/
public void addLocation(Location location) {
// do nothing if the new location matches the current location
if (!(locations.isEmpty()) &&
location.getHref().equals(locations.get(currentPos).getHref())) {
return;
}
currentPos++;
if (currentPos != currentSize) {
locations.set(currentPos, location);
} else {
locations.add(location);
checkHistorySize();
}
currentSize = currentPos + 1;
}
/**
* Removes all elements that are too much for the maxHistorySize
* out of the history.
*/
private void checkHistorySize() {
while (locations.size() > maxHistorySize) {
locations.remove(0);
currentSize--;
currentPos--;
}
}
public void addLocation(String href) {
addLocation(new Location(href));
}
private String getLocationHref(int pos) {
if (pos < 0 || pos >= locations.size()) {
return null;
}
return locations.get(currentPos).getHref();
}
/**
* Moves the current positions delta positions.
* <p>
* move(-1) to go one position back in history.<br/>
* move(1) to go one position forward.<br/>发
*
* @param delta f
* @return Whether we actually moved. If the requested value is illegal
* it will return false, true otherwise.
*/
public boolean move(int delta) {
if (((currentPos + delta) < 0)
|| ((currentPos + delta) >= currentSize)) {
return false;
}
currentPos += delta;
navigator.gotoResource(getLocationHref(currentPos), this);
return true;
}
/**
* If this is not the source of the navigationEvent then the addLocation
* will be called with the href of the currentResource in the navigationEvent.
*/
@Override
public void navigationPerformed(NavigationEvent navigationEvent) {
if (this == navigationEvent.getSource()) {
return;
}
if (navigationEvent.getCurrentResource() == null) {
return;
}
if ((System.currentTimeMillis() - this.lastUpdateTime) > historyWaitTime) {
// if the user scrolled rapidly through the pages then the last page
// will not be added to the history. We fix that here:
addLocation(navigationEvent.getOldResource());
addLocation(navigationEvent.getCurrentResource().getHref());
}
lastUpdateTime = System.currentTimeMillis();
}
public String getCurrentHref() {
if (currentPos < 0 || currentPos >= locations.size()) {
return null;
}
return locations.get(currentPos).getHref();
}
public void setMaxHistorySize(int maxHistorySize) {
this.maxHistorySize = maxHistorySize;
}
public int getMaxHistorySize() {
return maxHistorySize;
}
}
@@ -0,0 +1,219 @@
package me.ag2s.epublib.browsersupport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
/**
* A helper class for epub browser applications.
* <p>
* It helps moving from one resource to the other, from one resource
* to the other and keeping other elements of the application up-to-date
* by calling the NavigationEventListeners.
*
* @author paul
*/
public class Navigator implements Serializable {
private static final long serialVersionUID = 1076126986424925474L;
private EpubBook book;
private int currentSpinePos;
private Resource currentResource;
private int currentPagePos;
private String currentFragmentId;
private final List<NavigationEventListener> eventListeners = new ArrayList<>();
public Navigator() {
this(null);
}
public Navigator(EpubBook book) {
this.book = book;
this.currentSpinePos = 0;
if (book != null) {
this.currentResource = book.getCoverPage();
}
this.currentPagePos = 0;
}
private synchronized void handleEventListeners(
NavigationEvent navigationEvent) {
for (int i = 0; i < eventListeners.size(); i++) {
NavigationEventListener navigationEventListener = eventListeners.get(i);
navigationEventListener.navigationPerformed(navigationEvent);
}
}
public boolean addNavigationEventListener(
NavigationEventListener navigationEventListener) {
return this.eventListeners.add(navigationEventListener);
}
public boolean removeNavigationEventListener(
NavigationEventListener navigationEventListener) {
return this.eventListeners.remove(navigationEventListener);
}
public int gotoFirstSpineSection(Object source) {
return gotoSpineSection(0, source);
}
public int gotoPreviousSpineSection(Object source) {
return gotoPreviousSpineSection(0, source);
}
public int gotoPreviousSpineSection(int pagePos, Object source) {
if (currentSpinePos < 0) {
return gotoSpineSection(0, pagePos, source);
} else {
return gotoSpineSection(currentSpinePos - 1, pagePos, source);
}
}
public boolean hasNextSpineSection() {
return (currentSpinePos < (book.getSpine().size() - 1));
}
public boolean hasPreviousSpineSection() {
return (currentSpinePos > 0);
}
public int gotoNextSpineSection(Object source) {
if (currentSpinePos < 0) {
return gotoSpineSection(0, source);
} else {
return gotoSpineSection(currentSpinePos + 1, source);
}
}
public int gotoResource(String resourceHref, Object source) {
Resource resource = book.getResources().getByHref(resourceHref);
return gotoResource(resource, source);
}
public int gotoResource(Resource resource, Object source) {
return gotoResource(resource, 0, null, source);
}
public int gotoResource(Resource resource, String fragmentId, Object source) {
return gotoResource(resource, 0, fragmentId, source);
}
public int gotoResource(Resource resource, int pagePos, Object source) {
return gotoResource(resource, pagePos, null, source);
}
public int gotoResource(Resource resource, int pagePos, String fragmentId,
Object source) {
if (resource == null) {
return -1;
}
NavigationEvent navigationEvent = new NavigationEvent(source, this);
this.currentResource = resource;
this.currentSpinePos = book.getSpine().getResourceIndex(currentResource);
this.currentPagePos = pagePos;
this.currentFragmentId = fragmentId;
handleEventListeners(navigationEvent);
return currentSpinePos;
}
public int gotoResourceId(String resourceId, Object source) {
return gotoSpineSection(book.getSpine().findFirstResourceById(resourceId),
source);
}
public int gotoSpineSection(int newSpinePos, Object source) {
return gotoSpineSection(newSpinePos, 0, source);
}
/**
* Go to a specific section.
* Illegal spine positions are silently ignored.
*
* @param newSpinePos f
* @param source f
* @return The current position within the spine
*/
public int gotoSpineSection(int newSpinePos, int newPagePos, Object source) {
if (newSpinePos == currentSpinePos) {
return currentSpinePos;
}
if (newSpinePos < 0 || newSpinePos >= book.getSpine().size()) {
return currentSpinePos;
}
NavigationEvent navigationEvent = new NavigationEvent(source, this);
currentSpinePos = newSpinePos;
currentPagePos = newPagePos;
currentResource = book.getSpine().getResource(currentSpinePos);
handleEventListeners(navigationEvent);
return currentSpinePos;
}
public int gotoLastSpineSection(Object source) {
return gotoSpineSection(book.getSpine().size() - 1, source);
}
public void gotoBook(EpubBook book, Object source) {
NavigationEvent navigationEvent = new NavigationEvent(source, this);
this.book = book;
this.currentFragmentId = null;
this.currentPagePos = 0;
this.currentResource = null;
this.currentSpinePos = book.getSpine().getResourceIndex(currentResource);
handleEventListeners(navigationEvent);
}
/**
* The current position within the spine.
*
* @return something &lt; 0 if the current position is not within the spine.
*/
public int getCurrentSpinePos() {
return currentSpinePos;
}
public Resource getCurrentResource() {
return currentResource;
}
/**
* Sets the current index and resource without calling the eventlisteners.
* <p>
* If you want the eventListeners called use gotoSection(index);
*
* @param currentIndex f
*/
public void setCurrentSpinePos(int currentIndex) {
this.currentSpinePos = currentIndex;
this.currentResource = book.getSpine().getResource(currentIndex);
}
public EpubBook getBook() {
return book;
}
/**
* Sets the current index and resource without calling the eventlisteners.
* <p>
* If you want the eventListeners called use gotoSection(index);
*/
public int setCurrentResource(Resource currentResource) {
this.currentSpinePos = book.getSpine().getResourceIndex(currentResource);
this.currentResource = currentResource;
return currentSpinePos;
}
public String getCurrentFragmentId() {
return currentFragmentId;
}
public int getCurrentSectionPos() {
return currentPagePos;
}
}
@@ -0,0 +1,7 @@
/**
* Provides classes that help make an epub reader application.
* <p>
* These classes have no dependencies on graphic toolkits, they're purely
* to help with the browsing/navigation logic.
*/
package me.ag2s.epublib.browsersupport;
@@ -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;
}
}
+112
View File
@@ -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,102 @@
package me.ag2s.epublib.domain;
import java.util.LinkedHashMap;
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");
public static final MediaType WEBP = new MediaType("image/webp", ".webp");
// 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 UNKNOWN = new MediaType("application/octet-stream", "");
public static final MediaType[] mediaTypes = new MediaType[]{
XHTML, EPUB, JPG, PNG, GIF, WEBP, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF,
SMIL, PLS, JAVASCRIPT, MP3, MP4, OGG, UNKNOWN
};
public static final Map<String, MediaType> mediaTypesByName = new LinkedHashMap<>();
static {
for (MediaType mediaType : mediaTypes) {
mediaTypesByName.put(mediaType.getName(), mediaType);
}
}
public static boolean isBitmapImage(MediaType mediaType) {
return mediaType == JPG || mediaType == PNG || mediaType == GIF || mediaType == WEBP;
}
public static boolean isImage(MediaType mediaType) {
return mediaType == JPG || mediaType == PNG || mediaType == GIF || mediaType == SVG || mediaType == WEBP;
}
/**
* 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,418 @@
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 static final Pattern dataUriRegex = Pattern.compile("data:([\\w/\\-\\.]+);base64,(.*)");
private int lastId = 1;
private Map<String, Resource> resources = new HashMap<>();
private final Map<String, Resource> resourcesById = 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);
resourcesById.put(resource.getId(), 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;
}
return resourcesById.containsKey(id);
}
/**
* 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;
}
return resourcesById.get(id);
}
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();
resourcesById.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) {
add(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);
resourcesById.clear();
for (Resource resource : resources.values()) {
resourcesById.put(resource.getId(), resource);
}
}
/**
* 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);
if (!StringUtil.startsWithIgnoreCase(href, "data")) {
return resources.get(href);
}
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);
}
}
@@ -0,0 +1,20 @@
package me.ag2s.epublib.epub;
import me.ag2s.epublib.domain.EpubBook;
/**
* Post-processes a book.
* <p>
* Can be used to clean up a book after reading or before writing.
*
* @author paul
*/
public interface BookProcessor {
/**
* A BookProcessor that returns the input book unchanged.
*/
BookProcessor IDENTITY_BOOKPROCESSOR = book -> book;
EpubBook processBook(EpubBook book);
}
@@ -0,0 +1,73 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
/**
* A book processor that combines several other bookprocessors
* <p>
* Fixes coverpage/coverimage.
* Cleans up the XHTML.
*
* @author paul.siegmann
*/
@SuppressWarnings("unused declaration")
public class BookProcessorPipeline implements BookProcessor {
private static final String TAG = BookProcessorPipeline.class.getName();
private List<BookProcessor> bookProcessors;
public BookProcessorPipeline() {
this(null);
}
public BookProcessorPipeline(List<BookProcessor> bookProcessingPipeline) {
this.bookProcessors = bookProcessingPipeline;
}
@Override
public EpubBook processBook(EpubBook book) {
if (bookProcessors == null) {
return book;
}
for (BookProcessor bookProcessor : bookProcessors) {
try {
book = bookProcessor.processBook(book);
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
return book;
}
public void addBookProcessor(BookProcessor bookProcessor) {
if (this.bookProcessors == null) {
bookProcessors = new ArrayList<>();
}
this.bookProcessors.add(bookProcessor);
}
public void addBookProcessors(Collection<BookProcessor> bookProcessors) {
if (this.bookProcessors == null) {
this.bookProcessors = new ArrayList<>();
}
this.bookProcessors.addAll(bookProcessors);
}
public List<BookProcessor> getBookProcessors() {
return bookProcessors;
}
public void setBookProcessingPipeline(
List<BookProcessor> bookProcessingPipeline) {
this.bookProcessors = bookProcessingPipeline;
}
}
@@ -0,0 +1,178 @@
package me.ag2s.epublib.epub;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import java.util.ArrayList;
import java.util.List;
import me.ag2s.epublib.util.StringUtil;
/**
* Utility methods for working with the DOM.
*
* @author paul
*/
// package
class DOMUtil {
/**
* First tries to get the attribute value by doing an getAttributeNS on the element, if that gets an empty element it does a getAttribute without namespace.
*
* @param element element
* @param namespace namespace
* @param attribute attribute
* @return String Attribute
*/
public static String getAttribute(Element element, String namespace,
String attribute) {
String result = element.getAttributeNS(namespace, attribute);
if (StringUtil.isEmpty(result)) {
result = element.getAttribute(attribute);
}
return result;
}
/**
* Gets all descendant elements of the given parentElement with the given namespace and tagname and returns their text child as a list of String.
*
* @param parentElement parentElement
* @param namespace namespace
* @param tagName tagName
* @return List<String>
*/
public static List<String> getElementsTextChild(Element parentElement,
String namespace, String tagName) {
NodeList elements = parentElement
.getElementsByTagNameNS(namespace, tagName);
//ArrayList 初始化时指定长度提高性能
List<String> result = new ArrayList<>(elements.getLength());
for (int i = 0; i < elements.getLength(); i++) {
result.add(getTextChildrenContent((Element) elements.item(i)));
}
return result;
}
/**
* Finds in the current document the first element with the given namespace and elementName and with the given findAttributeName and findAttributeValue.
* It then returns the value of the given resultAttributeName.
*
* @param document document
* @param namespace namespace
* @param elementName elementName
* @param findAttributeName findAttributeName
* @param findAttributeValue findAttributeValue
* @param resultAttributeName resultAttributeName
* @return String value
*/
public static String getFindAttributeValue(Document document,
String namespace, String elementName, String findAttributeName,
String findAttributeValue, String resultAttributeName) {
NodeList metaTags = document.getElementsByTagNameNS(namespace, elementName);
for (int i = 0; i < metaTags.getLength(); i++) {
Element metaElement = (Element) metaTags.item(i);
if (findAttributeValue
.equalsIgnoreCase(metaElement.getAttribute(findAttributeName))
&& StringUtil
.isNotBlank(metaElement.getAttribute(resultAttributeName))) {
return metaElement.getAttribute(resultAttributeName);
}
}
return null;
}
/**
* Gets the first element that is a child of the parentElement and has the given namespace and tagName
*
* @param parentElement parentElement
* @param namespace namespace
* @param tagName tagName
* @return Element
*/
public static NodeList getElementsByTagNameNS(Element parentElement,
String namespace, String tagName) {
NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);
if (nodes.getLength() != 0) {
return nodes;
}
nodes = parentElement.getElementsByTagName(tagName);
if (nodes.getLength() == 0) {
return null;
}
return nodes;
}
/**
* Gets the first element that is a child of the parentElement and has the given namespace and tagName
*
* @param parentElement parentElement
* @param namespace namespace
* @param tagName tagName
* @return Element
*/
public static NodeList getElementsByTagNameNS(Document parentElement,
String namespace, String tagName) {
NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);
if (nodes.getLength() != 0) {
return nodes;
}
nodes = parentElement.getElementsByTagName(tagName);
if (nodes.getLength() == 0) {
return null;
}
return nodes;
}
/**
* Gets the first element that is a child of the parentElement and has the given namespace and tagName
*
* @param parentElement parentElement
* @param namespace namespace
* @param tagName tagName
* @return Element
*/
public static Element getFirstElementByTagNameNS(Element parentElement,
String namespace, String tagName) {
NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);
if (nodes.getLength() != 0) {
return (Element) nodes.item(0);
}
nodes = parentElement.getElementsByTagName(tagName);
if (nodes.getLength() == 0) {
return null;
}
return (Element) nodes.item(0);
}
/**
* The contents of all Text nodes that are children of the given parentElement.
* The result is trim()-ed.
* <p>
* The reason for this more complicated procedure instead of just returning the data of the firstChild is that
* when the text is Chinese characters then on Android each Characater is represented in the DOM as
* an individual Text node.
*
* @param parentElement parentElement
* @return String value
*/
public static String getTextChildrenContent(Element parentElement) {
if (parentElement == null) {
return null;
}
StringBuilder result = new StringBuilder();
NodeList childNodes = parentElement.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if ((node == null) ||
(node.getNodeType() != Node.TEXT_NODE)) {
continue;
}
result.append(((Text) node).getData());
}
return result.toString().trim();
}
}
@@ -0,0 +1,139 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URL;
import java.util.Objects;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import me.ag2s.epublib.Constants;
/**
* Various low-level support methods for reading/writing epubs.
*
* @author paul.siegmann
*/
public class EpubProcessorSupport {
private static final String TAG = EpubProcessorSupport.class.getName();
protected static DocumentBuilderFactory documentBuilderFactory;
static {
init();
}
static class EntityResolverImpl implements EntityResolver {
private String previousLocation;
@Override
public InputSource resolveEntity(String publicId, String systemId)
throws IOException {
String resourcePath;
if (systemId.startsWith("http:")) {
URL url = new URL(systemId);
resourcePath = "dtd/" + url.getHost() + url.getPath();
previousLocation = resourcePath
.substring(0, resourcePath.lastIndexOf('/'));
} else {
resourcePath =
previousLocation + systemId.substring(systemId.lastIndexOf('/'));
}
if (Objects.requireNonNull(this.getClass().getClassLoader()).getResource(resourcePath) == null) {
throw new RuntimeException(
"remote resource is not cached : [" + systemId
+ "] cannot continue");
}
InputStream in = Objects.requireNonNull(EpubProcessorSupport.class.getClassLoader())
.getResourceAsStream(resourcePath);
return new InputSource(in);
}
}
private static void init() {
EpubProcessorSupport.documentBuilderFactory = DocumentBuilderFactory
.newInstance();
documentBuilderFactory.setNamespaceAware(true);
documentBuilderFactory.setValidating(false);
}
public static XmlSerializer createXmlSerializer(OutputStream out)
throws UnsupportedEncodingException {
return createXmlSerializer(
new OutputStreamWriter(out, Constants.CHARACTER_ENCODING));
}
public static XmlSerializer createXmlSerializer(Writer out) {
XmlSerializer result = null;
try {
/*
* Disable XmlPullParserFactory here before it doesn't work when
* building native image using GraalVM
*/
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setValidating(true);
result = factory.newSerializer();
//result = new KXmlSerializer();
result.setFeature(
"http://xmlpull.org/v1/doc/features.html#indent-output", true);
result.setOutput(out);
} catch (Exception e) {
Log.e(TAG,
"When creating XmlSerializer: " + e.getClass().getName() + ": " + e
.getMessage());
}
return result;
}
/**
* Gets an EntityResolver that loads dtd's and such from the epub4j classpath.
* In order to enable the loading of relative urls the given EntityResolver contains the previousLocation.
* Because of a new EntityResolver is created every time this method is called.
* Fortunately the EntityResolver created uses up very little memory per instance.
*
* @return an EntityResolver that loads dtd's and such from the epub4j classpath.
*/
public static EntityResolver getEntityResolver() {
return new EntityResolverImpl();
}
@SuppressWarnings("unused")
public DocumentBuilderFactory getDocumentBuilderFactory() {
return documentBuilderFactory;
}
/**
* Creates a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath.
*
* @return a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath.
*/
public static DocumentBuilder createDocumentBuilder() {
DocumentBuilder result = null;
try {
result = documentBuilderFactory.newDocumentBuilder();
result.setEntityResolver(getEntityResolver());
} catch (ParserConfigurationException e) {
Log.e(TAG, e.getMessage());
}
return result;
}
}
@@ -0,0 +1,186 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import androidx.annotation.NonNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.Resources;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil;
import me.ag2s.epublib.util.zip.AndroidZipFile;
import me.ag2s.epublib.util.zip.ZipFileWrapper;
/**
* Reads an epub file.
*
* @author paul
*/
@SuppressWarnings("ALL")
public class EpubReader {
private static final String TAG = EpubReader.class.getName();
private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR;
public EpubBook readEpub(InputStream in) throws IOException {
return readEpub(in, Constants.CHARACTER_ENCODING);
}
public EpubBook readEpub(ZipInputStream in) throws IOException {
return readEpub(in, Constants.CHARACTER_ENCODING);
}
public EpubBook readEpub(ZipFile zipfile) throws IOException {
return readEpub(zipfile, Constants.CHARACTER_ENCODING);
}
/**
* Read epub from inputstream
*
* @param in the inputstream from which to read the epub
* @param encoding the encoding to use for the html files within the epub
* @return the Book as read from the inputstream
* @throws IOException IOException
*/
public EpubBook readEpub(InputStream in, String encoding) throws IOException {
return readEpub(new ZipInputStream(in), encoding);
}
/**
* Reads this EPUB without loading any resources into memory.
*
* @param zipFile the file to load
* @param encoding the encoding for XHTML files
* @return this Book without loading all resources into memory.
* @throws IOException IOException
*/
public EpubBook readEpubLazy(@NonNull ZipFile zipFile, @NonNull String encoding)
throws IOException {
return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes));
}
public EpubBook readEpubLazy(@NonNull AndroidZipFile zipFile, @NonNull String encoding)
throws IOException {
return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes));
}
public EpubBook readEpub(@NonNull ZipInputStream in, @NonNull String encoding) throws IOException {
return readEpub(ResourcesLoader.loadResources(in, encoding));
}
public EpubBook readEpub(ZipFile in, String encoding) throws IOException {
return readEpub(ResourcesLoader.loadResources(new ZipFileWrapper(in), encoding));
}
/**
* Reads this EPUB without loading all resources into memory.
*
* @param zipFile the file to load
* @param encoding the encoding for XHTML files
* @param lazyLoadedTypes a list of the MediaType to load lazily
* @return this Book without loading all resources into memory.
* @throws IOException IOException
*/
public EpubBook readEpubLazy(@NonNull ZipFile zipFile, @NonNull String encoding,
@NonNull List<MediaType> lazyLoadedTypes) throws IOException {
Resources resources = ResourcesLoader
.loadResources(new ZipFileWrapper(zipFile), encoding, lazyLoadedTypes);
return readEpub(resources);
}
public EpubBook readEpubLazy(@NonNull AndroidZipFile zipFile, @NonNull String encoding,
@NonNull List<MediaType> lazyLoadedTypes) throws IOException {
Resources resources = ResourcesLoader
.loadResources(new ZipFileWrapper(zipFile), encoding, lazyLoadedTypes);
return readEpub(resources);
}
public EpubBook readEpub(Resources resources) {
return readEpub(resources, new EpubBook());
}
public EpubBook readEpub(Resources resources, EpubBook result) {
if (result == null) {
result = new EpubBook();
}
handleMimeType(result, resources);
String packageResourceHref = getPackageResourceHref(resources);
Resource packageResource = processPackageResource(packageResourceHref, result, resources);
result.setOpfResource(packageResource);
Resource ncxResource = processNcxResource(packageResource, result);
result.setNcxResource(ncxResource);
result = postProcessBook(result);
return result;
}
private EpubBook postProcessBook(EpubBook book) {
if (bookProcessor != null) {
book = bookProcessor.processBook(book);
}
return book;
}
private Resource processNcxResource(Resource packageResource, EpubBook book) {
Log.d(TAG, "OPF:getHref()" + packageResource.getHref());
if (book.isEpub3()) {
return NCXDocumentV3.read(book, this);
} else {
return NCXDocumentV2.read(book, this);
}
}
private Resource processPackageResource(String packageResourceHref, EpubBook book,
Resources resources) {
Resource packageResource = resources.remove(packageResourceHref);
try {
PackageDocumentReader.read(packageResource, this, book, resources);
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
return packageResource;
}
private String getPackageResourceHref(Resources resources) {
String defaultResult = "OEBPS/content.opf";
String result = defaultResult;
Resource containerResource = resources.remove("META-INF/container.xml");
if (containerResource == null) {
return result;
}
try {
Document document = ResourceUtil.getAsDocument(containerResource);
Element rootFileElement = (Element) ((Element) document
.getDocumentElement().getElementsByTagName("rootfiles").item(0))
.getElementsByTagName("rootfile").item(0);
result = rootFileElement.getAttribute("full-path");
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
if (StringUtil.isBlank(result)) {
result = defaultResult;
}
return result;
}
private void handleMimeType(EpubBook result, Resources resources) {
resources.remove("mimetype");
//result.setResources(resources);
}
}
@@ -0,0 +1,206 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Objects;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.IOUtil;
/**
* Generates an epub file. Not thread-safe, single use object.
*
* @author paul
*/
public class EpubWriter {
// package
static final String EMPTY_NAMESPACE_PREFIX = "";
private static final String TAG = EpubWriter.class.getName();
private BookProcessor bookProcessor;
private EpubWriterProcessor epubWriterProcessor;
public EpubWriter() {
this(BookProcessor.IDENTITY_BOOKPROCESSOR);
this.epubWriterProcessor = new EpubWriterProcessor();
// 写入MimeType、Container,初始化TOCResource整体为1
// 写入PackageDocument 为1
// 关闭流 为1
this.epubWriterProcessor.setTotalProgress(3);
}
public EpubWriter(BookProcessor bookProcessor) {
this.bookProcessor = bookProcessor;
}
public EpubWriter setCallback(EpubWriterProcessor.Callback callback) {
epubWriterProcessor.setCallback(callback);
return this;
}
public void write(EpubBook book, OutputStream out) throws IOException {
if (Objects.nonNull(this.epubWriterProcessor.getCallback())) {
epubWriterProcessor.getCallback().onStart(book);
}
epubWriterProcessor.setTotalProgress(epubWriterProcessor.getTotalProgress() + book.getResources().size());
book = processBook(book);
ZipOutputStream resultStream = new ZipOutputStream(out);
//resultStream.setLevel(ZipOutputStream.STORED);
writeMimeType(resultStream);
writeContainer(resultStream);
initTOCResource(book);
epubWriterProcessor.updateCurrentProgress(1);
writeResources(book, resultStream);
writePackageDocument(book, resultStream);
epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1);
resultStream.close();
epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1);
if (Objects.nonNull(epubWriterProcessor.getCallback())) {
epubWriterProcessor.getCallback().onEnd(book);
}
}
private EpubBook processBook(EpubBook book) {
if (bookProcessor != null) {
book = bookProcessor.processBook(book);
}
return book;
}
private void initTOCResource(EpubBook book) {
Resource tocResource;
try {
if (book.isEpub3()) {
tocResource = NCXDocumentV3.createNCXResource(book);
} else {
tocResource = NCXDocumentV2.createNCXResource(book);
}
Resource currentTocResource = book.getSpine().getTocResource();
if (currentTocResource != null) {
book.getResources().remove(currentTocResource.getHref());
}
book.getSpine().setTocResource(tocResource);
book.getResources().add(tocResource);
} catch (Exception ex) {
Log.e(TAG, "Error writing table of contents: " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
}
}
private void writeResources(EpubBook book, ZipOutputStream resultStream) {
for (Resource resource : book.getResources().getAll()) {
writeResource(resource, resultStream);
epubWriterProcessor.updateCurrentProgress(epubWriterProcessor.getCurrentProgress() + 1);
}
}
/**
* Writes the resource to the resultStream.
*
* @param resource resource
* @param resultStream resultStream
*/
private void writeResource(Resource resource, ZipOutputStream resultStream) {
if (resource == null) {
return;
}
try {
resultStream.putNextEntry(new ZipEntry("OEBPS/" + resource.getHref()));
InputStream inputStream = resource.getInputStream();
IOUtil.copy(inputStream, resultStream);
inputStream.close();
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) throws IOException {
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
XmlSerializer xmlSerializer = EpubProcessorSupport.createXmlSerializer(resultStream);
PackageDocumentWriter.write(this, xmlSerializer, book);
xmlSerializer.flush();
// String resultAsString = result.toString();
// resultStream.write(resultAsString.getBytes(Constants.ENCODING));
}
/**
* Writes the META-INF/container.xml file.
*
* @param resultStream resultStream
* @throws IOException IOException
*/
private void writeContainer(ZipOutputStream resultStream) throws IOException {
resultStream.putNextEntry(new ZipEntry("META-INF/container.xml"));
Writer out = new OutputStreamWriter(resultStream);
out.write("<?xml version=\"1.0\"?>\n");
out.write("<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
out.write("\t<rootfiles>\n");
out.write("\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
out.write("\t</rootfiles>\n");
out.write("</container>");
out.flush();
}
/**
* Stores the mimetype as an uncompressed file in the ZipOutputStream.
*
* @param resultStream resultStream
* @throws IOException IOException
*/
private void writeMimeType(ZipOutputStream resultStream) throws IOException {
ZipEntry mimetypeZipEntry = new ZipEntry("mimetype");
mimetypeZipEntry.setMethod(ZipEntry.STORED);
byte[] mimetypeBytes = MediaTypes.EPUB.getName().getBytes();
mimetypeZipEntry.setSize(mimetypeBytes.length);
mimetypeZipEntry.setCrc(calculateCrc(mimetypeBytes));
resultStream.putNextEntry(mimetypeZipEntry);
resultStream.write(mimetypeBytes);
}
private long calculateCrc(byte[] data) {
CRC32 crc = new CRC32();
crc.update(data);
return crc.getValue();
}
String getNcxId() {
return "ncx";
}
String getNcxHref() {
return "toc.ncx";
}
String getNcxMediaType() {
return MediaTypes.NCX.getName();
}
@SuppressWarnings("unused")
public BookProcessor getBookProcessor() {
return bookProcessor;
}
@SuppressWarnings("unused")
public void setBookProcessor(BookProcessor bookProcessor) {
this.bookProcessor = bookProcessor;
}
}
@@ -0,0 +1,57 @@
package me.ag2s.epublib.epub;
import java.util.Objects;
import me.ag2s.epublib.domain.EpubBook;
/**
* epub导出进度管理器
*
* @author Discut
*/
public class EpubWriterProcessor {
private int totalProgress = 0;
private int currentProgress = 0;
private Callback callback;
public int getCurrentProgress() {
return currentProgress;
}
public int getTotalProgress() {
return totalProgress;
}
public void setTotalProgress(int totalProgress) {
this.totalProgress = totalProgress;
}
public void setCallback(Callback callback) {
this.callback = callback;
}
protected void updateCurrentProgress(int current) {
this.currentProgress = Math.min(current, totalProgress);
if (Objects.isNull(callback)) {
return;
}
callback.onProgressing(totalProgress, this.currentProgress);
}
protected Callback getCallback() {
return callback;
}
@SuppressWarnings("unused")
public interface Callback {
default void onStart(EpubBook epubBook) {
}
default void onProgressing(int total, int progress) {
}
default void onEnd(EpubBook epubBook) {
}
}
}
@@ -0,0 +1,11 @@
package me.ag2s.epublib.epub;
import java.io.OutputStream;
import me.ag2s.epublib.domain.Resource;
@SuppressWarnings("unused")
public interface HtmlProcessor {
void processHtmlResource(Resource resource, OutputStream out);
}
@@ -0,0 +1,344 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.TOCReference;
import me.ag2s.epublib.domain.TableOfContents;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil;
/**
* Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/
*
* @author paul
*/
public class NCXDocumentV2 {
public static final String NAMESPACE_NCX = "http://www.daisy.org/z3986/2005/ncx/";
@SuppressWarnings("unused")
public static final String PREFIX_NCX = "ncx";
public static final String NCX_ITEM_ID = "ncx";
public static final String DEFAULT_NCX_HREF = "toc.ncx";
public static final String PREFIX_DTB = "dtb";
private static final String TAG = NCXDocumentV2.class.getName();
private interface NCXTags {
String ncx = "ncx";
String meta = "meta";
String navPoint = "navPoint";
String navMap = "navMap";
String navLabel = "navLabel";
String content = "content";
String text = "text";
String docTitle = "docTitle";
String docAuthor = "docAuthor";
String head = "head";
}
private interface NCXAttributes {
String src = "src";
String name = "name";
String content = "content";
String id = "id";
String playOrder = "playOrder";
String clazz = "class";
String version = "version";
}
private interface NCXAttributeValues {
String chapter = "chapter";
String version = "2005-1";
}
@SuppressWarnings("unused")
public static Resource read(EpubBook book, EpubReader epubReader) {
Resource ncxResource = null;
if (book.getSpine().getTocResource() == null) {
Log.e(TAG, "Book does not contain a table of contents file");
return null;
}
try {
ncxResource = book.getSpine().getTocResource();
if (ncxResource == null) {
return null;
}
Log.d(TAG, ncxResource.getHref());
Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);
Element navMapElement = DOMUtil
.getFirstElementByTagNameNS(ncxDocument.getDocumentElement(),
NAMESPACE_NCX, NCXTags.navMap);
if (navMapElement == null) {
return null;
}
TableOfContents tableOfContents = new TableOfContents(
readTOCReferences(navMapElement.getChildNodes(), book));
book.setTableOfContents(tableOfContents);
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
return ncxResource;
}
static List<TOCReference> readTOCReferences(NodeList navpoints,
EpubBook book) {
if (navpoints == null) {
return new ArrayList<>();
}
List<TOCReference> result = new ArrayList<>(
navpoints.getLength());
for (int i = 0; i < navpoints.getLength(); i++) {
Node node = navpoints.item(i);
if (node.getNodeType() != Document.ELEMENT_NODE) {
continue;
}
if (!(node.getLocalName().equals(NCXTags.navPoint))) {
continue;
}
TOCReference tocReference = readTOCReference((Element) node, book);
result.add(tocReference);
}
return result;
}
static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
String label = readNavLabel(navpointElement);
//Log.d(TAG,"label:"+label);
String tocResourceRoot = StringUtil
.substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');
if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()
.length()) {
tocResourceRoot = "";
} else {
tocResourceRoot = tocResourceRoot + "/";
}
String reference = StringUtil
.collapsePathDots(tocResourceRoot + readNavReference(navpointElement));
String href = StringUtil
.substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
String fragmentId = StringUtil
.substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
Resource resource = book.getResources().getByHref(href);
if (resource == null) {
Log.e(TAG, "Resource with href " + href + " in NCX document not found");
}
//Log.v(TAG, "label:" + label);
//Log.v(TAG, "href:" + href);
//Log.v(TAG, "fragmentId:" + fragmentId);
TOCReference result = new TOCReference(label, resource, fragmentId);
List<TOCReference> childTOCReferences = readTOCReferences(
navpointElement.getChildNodes(), book);
result.setChildren(childTOCReferences);
return result;
}
private static String readNavReference(Element navpointElement) {
Element contentElement = DOMUtil
.getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX,
NCXTags.content);
if (contentElement == null) {
return null;
}
String result = DOMUtil
.getAttribute(contentElement, NAMESPACE_NCX, NCXAttributes.src);
try {
result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, e.getMessage());
}
return result;
}
private static String readNavLabel(Element navpointElement) {
//Log.d(TAG,navpointElement.getTagName());
Element navLabel = DOMUtil
.getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX,
NCXTags.navLabel);
assert navLabel != null;
return DOMUtil.getTextChildrenContent(DOMUtil
.getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text));
}
@SuppressWarnings("unused")
public static void write(EpubWriter epubWriter, EpubBook book,
ZipOutputStream resultStream) throws IOException {
resultStream
.putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref()));
XmlSerializer out = EpubProcessorSupport.createXmlSerializer(resultStream);
write(out, book);
out.flush();
}
/**
* Generates a resource containing an xml document containing the table of contents of the book in ncx format.
*
* @param xmlSerializer the serializer used
* @param book the book to serialize
* @throws IOException IOException
* @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException
*/
public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents());
}
public static Resource createNCXResource(EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException {
return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(),
book.getTableOfContents());
}
public static Resource createNCXResource(List<Identifier> identifiers,
String title, List<Author> authors, TableOfContents tableOfContents)
throws IllegalArgumentException, IllegalStateException, IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);
write(out, identifiers, title, authors, tableOfContents);
return new Resource(NCX_ITEM_ID, data.toByteArray(),
DEFAULT_NCX_HREF, MediaTypes.NCX);
}
public static void write(XmlSerializer serializer,
List<Identifier> identifiers, String title, List<Author> authors,
TableOfContents tableOfContents)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_NCX);
serializer.startTag(NAMESPACE_NCX, NCXTags.ncx);
// serializer.writeNamespace("ncx", NAMESPACE_NCX);
// serializer.attribute("xmlns", NAMESPACE_NCX);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.version,
NCXAttributeValues.version);
serializer.startTag(NAMESPACE_NCX, NCXTags.head);
for (Identifier identifier : identifiers) {
writeMetaElement(identifier.getScheme(), identifier.getValue(),
serializer);
}
writeMetaElement("generator", Constants.EPUB_GENERATOR_NAME, serializer);
writeMetaElement("depth", String.valueOf(tableOfContents.calculateDepth()),
serializer);
writeMetaElement("totalPageCount", "0", serializer);
writeMetaElement("maxPageNumber", "0", serializer);
serializer.endTag(NAMESPACE_NCX, "head");
serializer.startTag(NAMESPACE_NCX, NCXTags.docTitle);
serializer.startTag(NAMESPACE_NCX, NCXTags.text);
// write the first title
serializer.text(StringUtil.defaultIfNull(title));
serializer.endTag(NAMESPACE_NCX, NCXTags.text);
serializer.endTag(NAMESPACE_NCX, NCXTags.docTitle);
for (Author author : authors) {
serializer.startTag(NAMESPACE_NCX, NCXTags.docAuthor);
serializer.startTag(NAMESPACE_NCX, NCXTags.text);
serializer.text(author.getLastname() + ", " + author.getFirstname());
serializer.endTag(NAMESPACE_NCX, NCXTags.text);
serializer.endTag(NAMESPACE_NCX, NCXTags.docAuthor);
}
serializer.startTag(NAMESPACE_NCX, NCXTags.navMap);
writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);
serializer.endTag(NAMESPACE_NCX, NCXTags.navMap);
serializer.endTag(NAMESPACE_NCX, "ncx");
serializer.endDocument();
}
private static void writeMetaElement(String dtbName, String content,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_NCX, NCXTags.meta);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.name,
PREFIX_DTB + ":" + dtbName);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.content,
content);
serializer.endTag(NAMESPACE_NCX, NCXTags.meta);
}
private static int writeNavPoints(List<TOCReference> tocReferences,
int playOrder,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
for (TOCReference tocReference : tocReferences) {
if (tocReference.getResource() == null) {
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
serializer);
continue;
}
writeNavPointStart(tocReference, playOrder, serializer);
playOrder++;
if (!tocReference.getChildren().isEmpty()) {
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
serializer);
}
writeNavPointEnd(tocReference, serializer);
}
return playOrder;
}
private static void writeNavPointStart(TOCReference tocReference,
int playOrder, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_NCX, NCXTags.navPoint);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.id,
"navPoint-" + playOrder);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.playOrder,
String.valueOf(playOrder));
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.clazz,
NCXAttributeValues.chapter);
serializer.startTag(NAMESPACE_NCX, NCXTags.navLabel);
serializer.startTag(NAMESPACE_NCX, NCXTags.text);
serializer.text(tocReference.getTitle());
serializer.endTag(NAMESPACE_NCX, NCXTags.text);
serializer.endTag(NAMESPACE_NCX, NCXTags.navLabel);
serializer.startTag(NAMESPACE_NCX, NCXTags.content);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.src,
tocReference.getCompleteHref());
serializer.endTag(NAMESPACE_NCX, NCXTags.content);
}
@SuppressWarnings("unused")
private static void writeNavPointEnd(TOCReference tocReference,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.endTag(NAMESPACE_NCX, NCXTags.navPoint);
}
}
@@ -0,0 +1,461 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.TOCReference;
import me.ag2s.epublib.domain.TableOfContents;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil;
/**
* Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/
*
* @author Ag2S20150909
*/
public class NCXDocumentV3 {
public static final String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml";
public static final String NAMESPACE_EPUB = "http://www.idpf.org/2007/ops";
public static final String LANGUAGE = "en";
@SuppressWarnings("unused")
public static final String PREFIX_XHTML = "html";
public static final String NCX_ITEM_ID = "htmltoc";
public static final String DEFAULT_NCX_HREF = "toc.xhtml";
public static final String V3_NCX_PROPERTIES = "nav";
public static final MediaType V3_NCX_MEDIATYPE = MediaTypes.XHTML;
private static final String TAG = NCXDocumentV3.class.getName();
private interface XHTMLTgs {
String html = "html";
String head = "head";
String title = "title";
String meta = "meta";
String link = "link";
String body = "body";
String h1 = "h1";
String h2 = "h2";
String nav = "nav";
String ol = "ol";
String li = "li";
String a = "a";
String span = "span";
}
private interface XHTMLAttributes {
String xmlns = "xmlns";
String xmlns_epub = "xmlns:epub";
String lang = "lang";
String xml_lang = "xml:lang";
String rel = "rel";
String type = "type";
String epub_type = "epub:type";//nav的必须属性
String id = "id";
String role = "role";
String href = "href";
String http_equiv = "http-equiv";
String content = "content";
}
private interface XHTMLAttributeValues {
String Content_Type = "Content-Type";
String HTML_UTF8 = "text/html; charset=utf-8";
String lang = "en";
String epub_type = "toc";
String role_toc = "doc-toc";
}
/**
* 解析epub的目录文件
*
* @param book Book
* @param epubReader epubreader
* @return Resource
*/
@SuppressWarnings("unused")
public static Resource read(EpubBook book, EpubReader epubReader) {
Resource ncxResource = null;
if (book.getSpine().getTocResource() == null) {
Log.e(TAG, "Book does not contain a table of contents file");
return null;
}
try {
ncxResource = book.getSpine().getTocResource();
if (ncxResource == null) {
return null;
}
//一些epub 3 文件没有按照epub3的标准使用删除掉ncx目录文件
if (ncxResource.getHref().endsWith(".ncx")) {
Log.v(TAG, "该epub文件不标准,使用了epub2的目录文件");
return NCXDocumentV2.read(book, epubReader);
}
Log.d(TAG, ncxResource.getHref());
Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);
Log.d(TAG, ncxDocument.getNodeName());
Element navMapElement = (Element) ncxDocument.getElementsByTagName(XHTMLTgs.nav).item(0);
if (navMapElement == null) {
Log.d(TAG, "epub3目录文件未发现nav节点,尝试使用epub2的规则解析");
return NCXDocumentV2.read(book, epubReader);
}
navMapElement = (Element) navMapElement.getElementsByTagName(XHTMLTgs.ol).item(0);
Log.d(TAG, navMapElement.getTagName());
TableOfContents tableOfContents = new TableOfContents(
readTOCReferences(navMapElement.getChildNodes(), book));
Log.d(TAG, tableOfContents.toString());
book.setTableOfContents(tableOfContents);
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
return ncxResource;
}
private static List<TOCReference> doToc(Node n, EpubBook book) {
if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {
return new ArrayList<>();
}
Element el = (Element) n;
Node node = el.getElementsByTagName(XHTMLTgs.ol).item(0);
if (node == null || node.getNodeType() != Document.ELEMENT_NODE) {
return new ArrayList<>();
}
return readTOCReferences(node.getChildNodes(), book);
}
static List<TOCReference> readTOCReferences(NodeList navpoints,
EpubBook book) {
if (navpoints == null) {
return new ArrayList<>();
}
//Log.d(TAG, "readTOCReferences:navpoints.getLength()" + navpoints.getLength());
List<TOCReference> result = new ArrayList<>(navpoints.getLength());
for (int i = 0; i < navpoints.getLength(); i++) {
Node node = navpoints.item(i);
//如果该node是null,或者不是Element,跳出本次循环
if (node == null || node.getNodeType() != Document.ELEMENT_NODE) {
continue;
}
Element el = (Element) node;
//如果该Element的name为”li“,将其添加到目录结果
if (el.getTagName().equals(XHTMLTgs.li)) {
result.add(readTOCReference(el, book));
}
}
return result;
}
static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
//章节的名称
String label = readNavLabel(navpointElement);
//Log.d(TAG, "label:" + label);
String tocResourceRoot = StringUtil
.substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');
if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()
.length()) {
tocResourceRoot = "";
} else {
tocResourceRoot = tocResourceRoot + "/";
}
String reference = StringUtil
.collapsePathDots(tocResourceRoot + readNavReference(navpointElement));
String href = StringUtil
.substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
String fragmentId = StringUtil
.substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
Resource resource = book.getResources().getByHref(href);
if (resource == null) {
Log.e(TAG, "Resource with href " + href + " in NCX document not found");
}
// Log.v(TAG, "label:" + label);
// Log.v(TAG, "href:" + href);
// Log.v(TAG, "fragmentId:" + fragmentId);
//父级目录
TOCReference result = new TOCReference(label, resource, fragmentId);
//解析子级目录
List<TOCReference> childTOCReferences = doToc(navpointElement, book);
//readTOCReferences(
//navpointElement.getChildNodes(), book);
result.setChildren(childTOCReferences);
return result;
}
/**
* 获取目录节点的href
*
* @param navpointElement navpointElement
* @return String
*/
private static String readNavReference(Element navpointElement) {
//https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav
//父级节点必须是 "li"
//Log.d(TAG, "readNavReference:" + navpointElement.getTagName());
Element contentElement = DOMUtil
.getFirstElementByTagNameNS(navpointElement, "", XHTMLTgs.a);
if (contentElement == null) {
return null;
}
String result = DOMUtil
.getAttribute(contentElement, "", XHTMLAttributes.href);
try {
result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, e.getMessage());
}
return result;
}
/**
* 获取目录节点里面的章节名
*
* @param navpointElement navpointElement
* @return String
*/
private static String readNavLabel(Element navpointElement) {
//https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav
//父级节点必须是 "li"
//Log.d(TAG, "readNavLabel:" + navpointElement.getTagName());
String label;
Element labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "a");
assert labelElement != null;
label = labelElement.getTextContent();
if (StringUtil.isNotBlank(label)) {
return label;
} else {
labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "span");
}
assert labelElement != null;
label = labelElement.getTextContent();
//如果通过 a 标签无法获取章节列表,则是无href章节名
return label;
}
public static Resource createNCXResource(EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException {
return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(),
book.getTableOfContents());
}
public static Resource createNCXResource(List<Identifier> identifiers,
String title, List<Author> authors, TableOfContents tableOfContents)
throws IllegalArgumentException, IllegalStateException, IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);
write(out, identifiers, title, authors, tableOfContents);
Resource resource = new Resource(NCX_ITEM_ID, data.toByteArray(),
DEFAULT_NCX_HREF, V3_NCX_MEDIATYPE);
resource.setProperties(V3_NCX_PROPERTIES);
return resource;
}
/**
* Generates a resource containing an xml document containing the table of contents of the book in ncx format.
*
* @param xmlSerializer the serializer used
* @param book the book to serialize
* @throws IOException IOException
* @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException
*/
public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents());
}
/**
* 写入
*
* @param serializer serializer
* @param identifiers identifiers
* @param title title
* @param authors authors
* @param tableOfContents tableOfContents
*/
@SuppressWarnings("unused")
public static void write(XmlSerializer serializer,
List<Identifier> identifiers, String title, List<Author> authors,
TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_XHTML);
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.html);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xmlns_epub, NAMESPACE_EPUB);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xml_lang, XHTMLAttributeValues.lang);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.lang, LANGUAGE);
//写入头部head标签
writeHead(title, serializer);
//body开始
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.body);
//h1开始
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h1);
serializer.text(title);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h1);
//h1关闭
//nav开始
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.nav);
serializer.attribute("", XHTMLAttributes.epub_type, XHTMLAttributeValues.epub_type);
serializer.attribute("", XHTMLAttributes.id, XHTMLAttributeValues.epub_type);
serializer.attribute("", XHTMLAttributes.role, XHTMLAttributeValues.role_toc);
//h2开始
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h2);
serializer.text("目录");
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h2);
writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.nav);
//body关闭
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.body);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.html);
serializer.endDocument();
}
private static int writeNavPoints(List<TOCReference> tocReferences,
int playOrder,
XmlSerializer serializer) throws IOException {
writeOlStart(serializer);
for (TOCReference tocReference : tocReferences) {
if (tocReference.getResource() == null) {
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
serializer);
continue;
}
writeNavPointStart(tocReference, serializer);
playOrder++;
if (!tocReference.getChildren().isEmpty()) {
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
serializer);
}
writeNavPointEnd(tocReference, serializer);
}
writeOlSEnd(serializer);
return playOrder;
}
private static void writeNavPointStart(TOCReference tocReference, XmlSerializer serializer) throws IOException {
writeLiStart(serializer);
String title = tocReference.getTitle();
String href = tocReference.getCompleteHref();
if (StringUtil.isNotBlank(href)) {
writeLabel(title, href, serializer);
} else {
writeLabel(title, serializer);
}
}
@SuppressWarnings("unused")
private static void writeNavPointEnd(TOCReference tocReference,
XmlSerializer serializer) throws IOException {
writeLiEnd(serializer);
}
protected static void writeLabel(String title, String href, XmlSerializer serializer) throws IOException {
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.a);
serializer.attribute("", XHTMLAttributes.href, href);
//attribute必须在Text之前设置。
serializer.text(title);
//serializer.attribute(NAMESPACE_XHTML, XHTMLAttributes.href, href);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.a);
}
protected static void writeLabel(String title, XmlSerializer serializer) throws IOException {
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.span);
serializer.text(title);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.span);
}
private static void writeLiStart(XmlSerializer serializer) throws IOException {
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.li);
Log.d(TAG, "writeLiStart");
}
private static void writeLiEnd(XmlSerializer serializer) throws IOException {
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.li);
Log.d(TAG, "writeLiEND");
}
private static void writeOlStart(XmlSerializer serializer) throws IOException {
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.ol);
Log.d(TAG, "writeOlStart");
}
private static void writeOlSEnd(XmlSerializer serializer) throws IOException {
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.ol);
Log.d(TAG, "writeOlEnd");
}
private static void writeHead(String title, XmlSerializer serializer) throws IOException {
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.head);
//title
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.title);
serializer.text(StringUtil.defaultIfNull(title));
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.title);
//link
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.link);
serializer.attribute("", XHTMLAttributes.rel, "stylesheet");
serializer.attribute("", XHTMLAttributes.type, "text/css");
serializer.attribute("", XHTMLAttributes.href, "css/style.css");
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.link);
//meta
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.meta);
serializer.attribute("", XHTMLAttributes.http_equiv, XHTMLAttributeValues.Content_Type);
serializer.attribute("", XHTMLAttributes.content, XHTMLAttributeValues.HTML_UTF8);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.meta);
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.head);
}
}
@@ -0,0 +1,96 @@
package me.ag2s.epublib.epub;
/**
* Functionality shared by the PackageDocumentReader and the PackageDocumentWriter
*
* @author paul
*/
public class PackageDocumentBase {
public static final String BOOK_ID_ID = "duokan-book-id";
public static final String NAMESPACE_OPF = "http://www.idpf.org/2007/opf";
public static final String NAMESPACE_DUBLIN_CORE = "http://purl.org/dc/elements/1.1/";
public static final String PREFIX_DUBLIN_CORE = "dc";
//public static final String PREFIX_OPF = "opf";
//在EPUB3标准中,packge前面没有opf头,一些epub阅读器也不支持opf头。
//Some Epub Reader not reconize op:packge,So just let it empty;
public static final String PREFIX_OPF = "";
//添加 version 变量来区分Epub文件的版本
//Add the version field to distinguish the version of EPUB file
public static final String version = "version";
public static final String dateFormat = "yyyy-MM-dd";
protected interface DCTags {
String title = "title";
String creator = "creator";
String subject = "subject";
String description = "description";
String publisher = "publisher";
String contributor = "contributor";
String date = "date";
String type = "type";
String format = "format";
String identifier = "identifier";
String source = "source";
String language = "language";
String relation = "relation";
String coverage = "coverage";
String rights = "rights";
}
protected interface DCAttributes {
String scheme = "scheme";
String id = "id";
}
protected interface OPFTags {
String metadata = "metadata";
String meta = "meta";
String manifest = "manifest";
String packageTag = "package";
String itemref = "itemref";
String spine = "spine";
String reference = "reference";
String guide = "guide";
String item = "item";
}
protected interface OPFAttributes {
String uniqueIdentifier = "unique-identifier";
String idref = "idref";
String name = "name";
String content = "content";
String type = "type";
String href = "href";
String linear = "linear";
String event = "event";
String role = "role";
String file_as = "file-as";
String id = "id";
String media_type = "media-type";
String title = "title";
String toc = "toc";
String version = "version";
String scheme = "scheme";
String property = "property";
//add for epub3
/**
* add for epub3
*/
String properties = "properties";
}
protected interface OPFValues {
String meta_cover = "cover";
String reference_cover = "cover";
String no = "no";
String generator = "generator";
String duokan = "duokan-body-font";
}
}
@@ -0,0 +1,225 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
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.domain.Author;
import me.ag2s.epublib.domain.Date;
import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.domain.Metadata;
import me.ag2s.epublib.util.StringUtil;
/**
* Reads the package document metadata.
* <p>
* In its own separate class because the PackageDocumentReader became a bit large and unwieldy.
*
* @author paul
*/
// package
class PackageDocumentMetadataReader extends PackageDocumentBase {
private static final String TAG = PackageDocumentMetadataReader.class.getName();
public static Metadata readMetadata(Document packageDocument) {
Metadata result = new Metadata();
Element metadataElement = DOMUtil
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
NAMESPACE_OPF, OPFTags.metadata);
if (metadataElement == null) {
Log.e(TAG, "Package does not contain element " + OPFTags.metadata);
return result;
}
result.setTitles(DOMUtil
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.title));
result.setPublishers(DOMUtil
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.publisher));
result.setDescriptions(DOMUtil
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.description));
result.setRights(DOMUtil
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.rights));
result.setTypes(DOMUtil
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.type));
result.setSubjects(DOMUtil
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.subject));
result.setIdentifiers(readIdentifiers(metadataElement));
result.setAuthors(readCreators(metadataElement));
result.setContributors(readContributors(metadataElement));
result.setDates(readDates(metadataElement));
result.setOtherProperties(readOtherProperties(metadataElement));
result.setMetaAttributes(readMetaProperties(metadataElement));
Element languageTag = DOMUtil
.getFirstElementByTagNameNS(metadataElement, NAMESPACE_DUBLIN_CORE,
DCTags.language);
if (languageTag != null) {
result.setLanguage(DOMUtil.getTextChildrenContent(languageTag));
}
return result;
}
/**
* consumes meta tags that have a property attribute as defined in the standard. For example:
* &lt;meta property="rendition:layout"&gt;pre-paginated&lt;/meta&gt;
*
* @param metadataElement metadataElement
* @return Map<QName, String>
*/
private static Map<QName, String> readOtherProperties(
Element metadataElement) {
Map<QName, String> result = new HashMap<>();
NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta);
for (int i = 0; i < metaTags.getLength(); i++) {
Node metaNode = metaTags.item(i);
Node property = metaNode.getAttributes()
.getNamedItem(OPFAttributes.property);
if (property != null) {
String name = property.getNodeValue();
String value = metaNode.getTextContent();
result.put(new QName(name), value);
}
}
return result;
}
/**
* consumes meta tags that have a property attribute as defined in the standard. For example:
* &lt;meta property="rendition:layout"&gt;pre-paginated&lt;/meta&gt;
*
* @param metadataElement metadataElement
* @return Map<String, String>
*/
private static Map<String, String> readMetaProperties(
Element metadataElement) {
Map<String, String> result = new HashMap<>();
NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta);
for (int i = 0; i < metaTags.getLength(); i++) {
Element metaElement = (Element) metaTags.item(i);
String name = metaElement.getAttribute(OPFAttributes.name);
String value = metaElement.getAttribute(OPFAttributes.content);
result.put(name, value);
}
return result;
}
private static String getBookIdId(Document document) {
Element packageElement = DOMUtil
.getFirstElementByTagNameNS(document.getDocumentElement(),
NAMESPACE_OPF, OPFTags.packageTag);
if (packageElement == null) {
return null;
}
return DOMUtil.getAttribute(packageElement, NAMESPACE_OPF, OPFAttributes.uniqueIdentifier);
}
private static List<Author> readCreators(Element metadataElement) {
return readAuthors(DCTags.creator, metadataElement);
}
private static List<Author> readContributors(Element metadataElement) {
return readAuthors(DCTags.contributor, metadataElement);
}
private static List<Author> readAuthors(String authorTag,
Element metadataElement) {
NodeList elements = metadataElement
.getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, authorTag);
List<Author> result = new ArrayList<>(elements.getLength());
for (int i = 0; i < elements.getLength(); i++) {
Element authorElement = (Element) elements.item(i);
Author author = createAuthor(authorElement);
if (author != null) {
result.add(author);
}
}
return result;
}
private static List<Date> readDates(Element metadataElement) {
NodeList elements = metadataElement
.getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.date);
List<Date> result = new ArrayList<>(elements.getLength());
for (int i = 0; i < elements.getLength(); i++) {
Element dateElement = (Element) elements.item(i);
Date date;
try {
date = new Date(DOMUtil.getTextChildrenContent(dateElement),
DOMUtil.getAttribute(dateElement, NAMESPACE_OPF, OPFAttributes.event));
result.add(date);
} catch (IllegalArgumentException e) {
Log.e(TAG, e.getMessage());
}
}
return result;
}
private static Author createAuthor(Element authorElement) {
String authorString = DOMUtil.getTextChildrenContent(authorElement);
if (StringUtil.isBlank(authorString)) {
return null;
}
int spacePos = authorString.lastIndexOf(' ');
Author result;
if (spacePos < 0) {
result = new Author(authorString);
} else {
result = new Author(authorString.substring(0, spacePos),
authorString.substring(spacePos + 1));
}
result.setRole(
DOMUtil.getAttribute(authorElement, NAMESPACE_OPF, OPFAttributes.role));
return result;
}
private static List<Identifier> readIdentifiers(Element metadataElement) {
NodeList identifierElements = metadataElement
.getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
if (identifierElements.getLength() == 0) {
Log.e(TAG, "Package does not contain element " + DCTags.identifier);
return new ArrayList<>();
}
String bookIdId = getBookIdId(metadataElement.getOwnerDocument());
List<Identifier> result = new ArrayList<>(
identifierElements.getLength());
for (int i = 0; i < identifierElements.getLength(); i++) {
Element identifierElement = (Element) identifierElements.item(i);
String schemeName = DOMUtil.getAttribute(identifierElement, NAMESPACE_OPF, DCAttributes.scheme);
String identifierValue = DOMUtil
.getTextChildrenContent(identifierElement);
if (StringUtil.isBlank(identifierValue)) {
continue;
}
Identifier identifier = new Identifier(schemeName, identifierValue);
if (identifierElement.getAttribute("id").equals(bookIdId)) {
identifier.setBookId(true);
}
result.add(identifier);
}
return result;
}
}
@@ -0,0 +1,187 @@
package me.ag2s.epublib.epub;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.Date;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.util.StringUtil;
public class PackageDocumentMetadataWriter extends PackageDocumentBase {
/**
* Writes the book's metadata.
*
* @param book book
* @param serializer serializer
* @throws IOException IOException
* @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException
*/
public static void writeMetaData(EpubBook book, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.metadata);
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
writeIdentifiers(book.getMetadata().getIdentifiers(), serializer);
writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(),
serializer);
writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(),
serializer);
writeSimpleMetdataElements(DCTags.description,
book.getMetadata().getDescriptions(), serializer);
writeSimpleMetdataElements(DCTags.publisher,
book.getMetadata().getPublishers(), serializer);
writeSimpleMetdataElements(DCTags.type, book.getMetadata().getTypes(),
serializer);
writeSimpleMetdataElements(DCTags.rights, book.getMetadata().getRights(),
serializer);
// write authors
for (Author author : book.getMetadata().getAuthors()) {
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.creator);
serializer.attribute(NAMESPACE_OPF, OPFAttributes.role,
author.getRelator().getCode());
serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as,
author.getLastname() + ", " + author.getFirstname());
serializer.text(author.getFirstname() + " " + author.getLastname());
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.creator);
}
// write contributors
for (Author author : book.getMetadata().getContributors()) {
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor);
serializer.attribute(NAMESPACE_OPF, OPFAttributes.role,
author.getRelator().getCode());
serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as,
author.getLastname() + ", " + author.getFirstname());
serializer.text(author.getFirstname() + " " + author.getLastname());
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor);
}
// write dates
for (Date date : book.getMetadata().getDates()) {
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.date);
if (date.getEvent() != null) {
serializer.attribute(NAMESPACE_OPF, OPFAttributes.event,
date.getEvent().toString());
}
serializer.text(date.getValue());
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.date);
}
// write language
if (StringUtil.isNotBlank(book.getMetadata().getLanguage())) {
serializer.startTag(NAMESPACE_DUBLIN_CORE, "language");
serializer.text(book.getMetadata().getLanguage());
serializer.endTag(NAMESPACE_DUBLIN_CORE, "language");
}
// write other properties
if (book.getMetadata().getOtherProperties() != null) {
for (Map.Entry<QName, String> mapEntry : book.getMetadata()
.getOtherProperties().entrySet()) {
serializer.startTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX,
OPFAttributes.property, mapEntry.getKey().getLocalPart());
serializer.text(mapEntry.getValue());
serializer.endTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta);
}
}
// write coverimage
if (book.getCoverImage() != null) { // write the cover image
serializer.startTag(NAMESPACE_OPF, OPFTags.meta);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,
OPFValues.meta_cover);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,
book.getCoverImage().getId());
serializer.endTag(NAMESPACE_OPF, OPFTags.meta);
}
// write generator
serializer.startTag(NAMESPACE_OPF, OPFTags.meta);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,
OPFValues.generator);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,
Constants.EPUB_GENERATOR_NAME);
serializer.endTag(NAMESPACE_OPF, OPFTags.meta);
// write duokan
serializer.startTag(NAMESPACE_OPF, OPFTags.meta);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,
OPFValues.duokan);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,
Constants.EPUB_DUOKAN_NAME);
serializer.endTag(NAMESPACE_OPF, OPFTags.meta);
serializer.endTag(NAMESPACE_OPF, OPFTags.metadata);
}
private static void writeSimpleMetdataElements(String tagName,
List<String> values, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
for (String value : values) {
if (StringUtil.isBlank(value)) {
continue;
}
serializer.startTag(NAMESPACE_DUBLIN_CORE, tagName);
serializer.text(value);
serializer.endTag(NAMESPACE_DUBLIN_CORE, tagName);
}
}
/**
* Writes out the complete list of Identifiers to the package document.
* The first identifier for which the bookId is true is made the bookId identifier.
* If no identifier has bookId == true then the first bookId identifier is written as the primary.
*
* @param identifiers identifiers
* @param serializer serializer
* @throws IllegalStateException e
* @throws IllegalArgumentException e
* @
*/
private static void writeIdentifiers(List<Identifier> identifiers,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
Identifier bookIdIdentifier = Identifier.getBookIdIdentifier(identifiers);
if (bookIdIdentifier == null) {
return;
}
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, DCAttributes.id,
BOOK_ID_ID);
serializer.attribute(NAMESPACE_OPF, OPFAttributes.scheme,
bookIdIdentifier.getScheme());
serializer.text(bookIdIdentifier.getValue());
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
for (Identifier identifier : identifiers.subList(1, identifiers.size())) {
if (identifier == bookIdIdentifier) {
continue;
}
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
serializer.attribute(NAMESPACE_OPF, "scheme", identifier.getScheme());
serializer.text(identifier.getValue());
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
}
}
}
@@ -0,0 +1,508 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Guide;
import me.ag2s.epublib.domain.GuideReference;
import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.Resources;
import me.ag2s.epublib.domain.Spine;
import me.ag2s.epublib.domain.SpineReference;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.StringUtil;
import me.ag2s.epublib.util.URLEncodeUtil;
/**
* Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf
*
* @author paul
*/
public class PackageDocumentReader extends PackageDocumentBase {
private static final String TAG = PackageDocumentReader.class.getName();
private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[]{"toc",
"ncx", "ncxtoc", "htmltoc"};
private static final Pattern namespaceRegex = Pattern.compile(" s?mlns=\"");
public static void read(
Resource packageResource, EpubReader epubReader, EpubBook book,
Resources resources)
throws SAXException, IOException {
/*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/
String string = namespaceRegex.matcher(new String(packageResource.getData()))
.replaceAll(" xmlns=\"");
packageResource.setData(string.getBytes());
Document packageDocument = ResourceUtil.getAsDocument(packageResource);
String packageHref = packageResource.getHref();
URI packagePath;
try {
packagePath = new URI(packageHref);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
//resources = fixHrefs(packageHref, resources);
readGuide(packageDocument, epubReader, book, resources);
// Books sometimes use non-identifier ids. We map these here to legal ones
Map<String, String> idMapping = new HashMap<>();
String version = DOMUtil.getAttribute(packageDocument.getDocumentElement(), PREFIX_OPF, PackageDocumentBase.version);
resources = readManifest(packageDocument, packageHref, packagePath, epubReader,
resources, idMapping);
book.setResources(resources);
book.setVersion(version);
readCover(packageDocument, packagePath, book);
book.setMetadata(
PackageDocumentMetadataReader.readMetadata(packageDocument));
book.setSpine(readSpine(packageDocument, book.getResources(), idMapping));
// if we did not find a cover page then we make the first page of the book the cover page
if (book.getCoverPage() == null && book.getSpine().size() > 0) {
book.setCoverPage(book.getSpine().getResource(0));
}
}
/**
* 修复一些非标准epub格式由于 opf 文件内容不全而读取不到图片的问题
*
* @return 修复图片路径后的一个Element列表
* @author qianfanguojin
*/
private static ArrayList<Element> ensureImageInfo(Resources resources,
Element manifestElement,
URI packagePath,
Document packageDocument) {
ArrayList<Element> fixedElements = new ArrayList<>();
HashSet<String> originItemHrefSet = new HashSet<>();
//加入当前所有的 item 标签 并将 href 保存到集合中
NodeList originItemElements = manifestElement
.getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.item);
for (int i = 0; i < originItemElements.getLength(); i++) {
Element itemElement = (Element) originItemElements.item(i).cloneNode(false);
String href = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href);
String resolvedHref = resolvePath(packagePath, href);
itemElement.setAttribute("href", resolvedHref);
fixedElements.add(itemElement);
try {
href = URLDecoder.decode(resolvedHref, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, e.getMessage());
}
originItemHrefSet.add(href);
}
//如果有图片资源未定义在 originItemElements ,则加入该图片信息得到 fixedElements 中
for (Resource resource : resources.getAll()) {
MediaType currentMediaType = resource.getMediaType();
if (!MediaTypes.isImage(currentMediaType)) {
continue;
}
String imageHref = resource.getHref();
//确保该图片信息 resource 在原 originItemHrefSet 集合中没有出现过
if (originItemHrefSet.contains(imageHref)) {
continue;
}
Element itemEl = packageDocument.createElement("item");
itemEl.setAttribute("id", resource.getId());
try {
imageHref = URLEncoder.encode(imageHref, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, e.getMessage());
continue;
}
itemEl.setAttribute("href", imageHref.replace("+", "%20"));
itemEl.setAttribute("media-type", currentMediaType.getName());
fixedElements.add(itemEl);
}
return fixedElements;
}
/**
* Reads the manifest containing the resource ids, hrefs and mediatypes.
*
* @param packageDocument e
* @param packageHref e
* @param epubReader e
* @param resources e
* @param idMapping e
* @return a Map with resources, with their id's as key.
*/
@SuppressWarnings("unused")
private static Resources readManifest(Document packageDocument,
String packageHref,
URI packagePath,
EpubReader epubReader, Resources resources,
Map<String, String> idMapping) {
Element manifestElement = DOMUtil
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
NAMESPACE_OPF, OPFTags.manifest);
Resources result = new Resources();
if (manifestElement == null) {
Log.e(TAG,
"Package document does not contain element " + OPFTags.manifest);
return result;
}
List<Element> ensuredElements = ensureImageInfo(resources, manifestElement, packagePath, packageDocument);
for (Element itemElement : ensuredElements) {
// Element itemElement = ;
String id = DOMUtil
.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.id);
String href = DOMUtil
.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href);
try {
href = URLDecoder.decode(href, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, e.getMessage());
}
String mediaTypeName = DOMUtil
.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.media_type);
Resource resource = resources.remove(href);
if (resource == null) {
Log.e(TAG, "resource with href '" + href + "' not found");
continue;
}
resource.setId(id);
//for epub3
String properties = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.properties);
resource.setProperties(properties);
MediaType mediaType = MediaTypes.getMediaTypeByName(mediaTypeName);
if (mediaType != null) {
resource.setMediaType(mediaType);
}
result.add(resource);
idMapping.put(id, resource.getId());
}
return result;
}
/**
* Reads the book's guide.
* Here some more attempts are made at finding the cover page.
*
* @param packageDocument r
* @param epubReader r
* @param book r
* @param resources g
*/
@SuppressWarnings("unused")
private static void readGuide(Document packageDocument,
EpubReader epubReader, EpubBook book, Resources resources) {
Element guideElement = DOMUtil
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
NAMESPACE_OPF, OPFTags.guide);
if (guideElement == null) {
return;
}
Guide guide = book.getGuide();
NodeList guideReferences = guideElement
.getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference);
for (int i = 0; i < guideReferences.getLength(); i++) {
Element referenceElement = (Element) guideReferences.item(i);
String resourceHref = DOMUtil
.getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href);
if (StringUtil.isBlank(resourceHref)) {
continue;
}
Resource resource = resources.getByHref(StringUtil
.substringBefore(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR));
if (resource == null) {
Log.e(TAG, "Guide is referencing resource with href " + resourceHref
+ " which could not be found");
continue;
}
String type = DOMUtil
.getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.type);
if (StringUtil.isBlank(type)) {
Log.e(TAG, "Guide is referencing resource with href " + resourceHref
+ " which is missing the 'type' attribute");
continue;
}
String title = DOMUtil
.getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.title);
if (GuideReference.COVER.equalsIgnoreCase(type)) {
continue; // cover is handled elsewhere
}
GuideReference reference = new GuideReference(resource, type, title,
StringUtil
.substringAfter(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR));
guide.addReference(reference);
}
}
/**
* Strips off the package prefixes up to the href of the packageHref.
* <p>
* Example:
* If the packageHref is "OEBPS/content.opf" then a resource href like "OEBPS/foo/bar.html" will be turned into "foo/bar.html"
*
* @param packageHref f
* @param resourcesByHref g
* @return The stripped package href
*/
static Resources fixHrefs(String packageHref,
Resources resourcesByHref) {
int lastSlashPos = packageHref.lastIndexOf('/');
if (lastSlashPos < 0) {
return resourcesByHref;
}
Resources result = new Resources();
for (Resource resource : resourcesByHref.getAll()) {
if (StringUtil.isNotBlank(resource.getHref())
&& resource.getHref().length() > lastSlashPos) {
resource.setHref(resource.getHref().substring(lastSlashPos + 1));
}
result.add(resource);
}
return result;
}
/**
* Reads the document's spine, containing all sections in reading order.
*
* @param packageDocument b
* @param resources b
* @param idMapping b
* @return the document's spine, containing all sections in reading order.
*/
private static Spine readSpine(Document packageDocument, Resources resources,
Map<String, String> idMapping) {
Element spineElement = DOMUtil
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
NAMESPACE_OPF, OPFTags.spine);
if (spineElement == null) {
Log.e(TAG, "Element " + OPFTags.spine
+ " not found in package document, generating one automatically");
return generateSpineFromResources(resources);
}
Spine result = new Spine();
String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc);
Log.v(TAG, tocResourceId);
result.setTocResource(findTableOfContentsResource(tocResourceId, resources));
NodeList spineNodes = DOMUtil.getElementsByTagNameNS(packageDocument, NAMESPACE_OPF, OPFTags.itemref);
if (spineNodes == null) {
Log.e(TAG, "spineNodes is null");
return result;
}
List<SpineReference> spineReferences = new ArrayList<>(spineNodes.getLength());
for (int i = 0; i < spineNodes.getLength(); i++) {
Element spineItem = (Element) spineNodes.item(i);
String itemref = DOMUtil.getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.idref);
if (StringUtil.isBlank(itemref)) {
Log.e(TAG, "itemref with missing or empty idref"); // XXX
continue;
}
String id = idMapping.get(itemref);
if (id == null) {
id = itemref;
}
Resource resource = resources.getByIdOrHref(id);
if (resource == null) {
Log.e(TAG, "resource with id '" + id + "' not found");
continue;
}
SpineReference spineReference = new SpineReference(resource);
if (OPFValues.no.equalsIgnoreCase(DOMUtil
.getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.linear))) {
spineReference.setLinear(false);
}
spineReferences.add(spineReference);
}
result.setSpineReferences(spineReferences);
return result;
}
/**
* Creates a spine out of all resources in the resources.
* The generated spine consists of all XHTML pages in order of their href.
*
* @param resources f
* @return a spine created out of all resources in the resources.
*/
private static Spine generateSpineFromResources(Resources resources) {
Spine result = new Spine();
List<String> resourceHrefs = new ArrayList<>(resources.getAllHrefs());
Collections.sort(resourceHrefs, String.CASE_INSENSITIVE_ORDER);
for (String resourceHref : resourceHrefs) {
Resource resource = resources.getByHref(resourceHref);
if (resource.getMediaType() == MediaTypes.NCX) {
result.setTocResource(resource);
} else if (resource.getMediaType() == MediaTypes.XHTML) {
result.addSpineReference(new SpineReference(resource));
}
}
return result;
}
/**
* The spine tag should contain a 'toc' attribute with as value the resource id of the table of contents resource.
* <p>
* Here we try several ways of finding this table of contents resource.
* We try the given attribute value, some often-used ones and finally look through all resources for the first resource with the table of contents mimetype.
*
* @param tocResourceId g
* @param resources g
* @return the Resource containing the table of contents
*/
static Resource findTableOfContentsResource(
String tocResourceId,
Resources resources
) {
Resource tocResource;
//一些epub3的文件为了兼容epub2,保留的epub2的目录文件,这里优先选择epub3的xml目录
tocResource = resources.getByProperties("nav");
if (tocResource != null) {
return tocResource;
}
if (StringUtil.isNotBlank(tocResourceId)) {
tocResource = resources.getByIdOrHref(tocResourceId);
}
if (tocResource != null) {
return tocResource;
}
// get the first resource with the NCX mediatype
tocResource = resources.findFirstResourceByMediaType(MediaTypes.NCX);
if (tocResource == null) {
for (String possibleNcxItemId : POSSIBLE_NCX_ITEM_IDS) {
tocResource = resources.getByIdOrHref(possibleNcxItemId);
if (tocResource != null) {
break;
}
tocResource = resources
.getByIdOrHref(possibleNcxItemId.toUpperCase());
if (tocResource != null) {
break;
}
}
}
if (tocResource == null) {
Log.e(TAG,
"Could not find table of contents resource. Tried resource with id '"
+ tocResourceId + "', " + Constants.DEFAULT_TOC_ID + ", "
+ Constants.DEFAULT_TOC_ID.toUpperCase()
+ " and any NCX resource.");
}
return tocResource;
}
/**
* Find all resources that have something to do with the coverpage and the cover image.
* Search the meta tags and the guide references
*
* @param packageDocument s
* @return all resources that have something to do with the coverpage and the cover image.
*/
// package
static Set<String> findCoverHrefs(Document packageDocument, URI packagePath) {
Set<String> result = new HashSet<>();
// try and find a meta tag with name = 'cover' and a non-blank id
String coverResourceId = DOMUtil
.getFindAttributeValue(packageDocument, NAMESPACE_OPF,
OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover,
OPFAttributes.content);
if (StringUtil.isNotBlank(coverResourceId)) {
String coverHref = DOMUtil
.getFindAttributeValue(packageDocument, NAMESPACE_OPF,
OPFTags.item, OPFAttributes.id, coverResourceId,
OPFAttributes.href);
if (StringUtil.isNotBlank(coverHref)) {
result.add(resolvePath(packagePath, coverHref));
} else {
String resolved = resolvePath(packagePath, coverResourceId);
result.add(
resolved); // maybe there was a cover href put in the cover id attribute
}
}
// try and find a reference tag with type is 'cover' and reference is not blank
String coverHref = DOMUtil
.getFindAttributeValue(packageDocument, NAMESPACE_OPF,
OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover,
OPFAttributes.href);
if (StringUtil.isNotBlank(coverHref)) {
result.add(resolvePath(packagePath, coverHref));
}
return result;
}
private static String resolvePath(URI parentPath, String href) {
href = URLEncodeUtil.encode(href);
String resolved = parentPath.resolve(href).toString();
try {
return URLDecoder.decode(resolved, Constants.CHARACTER_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Finds the cover resource in the packageDocument and adds it to the book if found.
* Keeps the cover resource in the resources map
*
* @param packageDocument s
* @param book x
*/
private static void readCover(Document packageDocument, URI packagePath, EpubBook book) {
Collection<String> coverHrefs = findCoverHrefs(packageDocument, packagePath);
for (String coverHref : coverHrefs) {
Resource resource = book.getResources().getByHref(coverHref);
if (resource == null) {
Log.e(TAG, "Cover resource " + coverHref + " not found");
continue;
}
if (resource.getMediaType() == MediaTypes.XHTML) {
book.setCoverPage(resource);
} else if (MediaTypes.isBitmapImage(resource.getMediaType())) {
book.setCoverImage(resource);
}
}
}
}
@@ -0,0 +1,247 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Guide;
import me.ag2s.epublib.domain.GuideReference;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.Spine;
import me.ag2s.epublib.domain.SpineReference;
import me.ag2s.epublib.util.StringUtil;
/**
* Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf
*
* @author paul
*/
public class PackageDocumentWriter extends PackageDocumentBase {
private static final String TAG = PackageDocumentWriter.class.getName();
public static void write(EpubWriter epubWriter, XmlSerializer serializer,
EpubBook book) {
try {
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);
serializer.startTag(NAMESPACE_OPF, OPFTags.packageTag);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version,
book.getVersion());
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX,
OPFAttributes.uniqueIdentifier, BOOK_ID_ID);
PackageDocumentMetadataWriter.writeMetaData(book, serializer);
writeManifest(book, epubWriter, serializer);
writeSpine(book, epubWriter, serializer);
writeGuide(book, epubWriter, serializer);
serializer.endTag(NAMESPACE_OPF, OPFTags.packageTag);
serializer.endDocument();
serializer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Writes the package's spine.
*
* @param book e
* @param epubWriter g
* @param serializer g
* @throws IOException g
* @throws IllegalStateException g
* @throws IllegalArgumentException 1@throws XMLStreamException
*/
@SuppressWarnings("unused")
private static void writeSpine(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.spine);
Resource tocResource = book.getSpine().getTocResource();
String tocResourceId = tocResource.getId();
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.toc,
tocResourceId);
if (book.getCoverPage() != null // there is a cover page
&& book.getSpine().findFirstResourceById(book.getCoverPage().getId())
< 0) { // cover page is not already in the spine
// write the cover html file
serializer.startTag(NAMESPACE_OPF, OPFTags.itemref);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref,
book.getCoverPage().getId());
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear,
"no");
serializer.endTag(NAMESPACE_OPF, OPFTags.itemref);
}
writeSpineItems(book.getSpine(), serializer);
serializer.endTag(NAMESPACE_OPF, OPFTags.spine);
}
private static void writeManifest(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.manifest);
serializer.startTag(NAMESPACE_OPF, OPFTags.item);
//For EPUB3
if (book.isEpub3()) {
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties, NCXDocumentV3.V3_NCX_PROPERTIES);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName());
} else {
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id,
epubWriter.getNcxId());
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, epubWriter.getNcxHref());
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, epubWriter.getNcxMediaType());
}
serializer.endTag(NAMESPACE_OPF, OPFTags.item);
// writeCoverResources(book, serializer);
for (Resource resource : getAllResourcesSortById(book)) {
writeItem(book, resource, serializer);
}
serializer.endTag(NAMESPACE_OPF, OPFTags.manifest);
}
private static List<Resource> getAllResourcesSortById(EpubBook book) {
List<Resource> allResources = new ArrayList<>(
book.getResources().getAll());
Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId()));
return allResources;
}
/**
* Writes a resources as an item element
*
* @param resource g
* @param serializer g
* @throws IOException g
* @throws IllegalStateException g
* @throws IllegalArgumentException 1@throws XMLStreamException
*/
private static void writeItem(EpubBook book, Resource resource,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
if (resource == null ||
(resource.getMediaType() == MediaTypes.NCX
&& book.getSpine().getTocResource() != null)) {
return;
}
if (StringUtil.isBlank(resource.getId())) {
Log.e(TAG, "resource id must not be empty (href: " + resource.getHref()
+ ", mediatype:" + resource.getMediaType() + ")");
return;
}
if (StringUtil.isBlank(resource.getHref())) {
Log.e(TAG, "resource href must not be empty (id: " + resource.getId()
+ ", mediatype:" + resource.getMediaType() + ")");
return;
}
if (resource.getMediaType() == null) {
Log.e(TAG, "resource mediatype must not be empty (id: " + resource.getId()
+ ", href:" + resource.getHref() + ")");
return;
}
serializer.startTag(NAMESPACE_OPF, OPFTags.item);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id,
resource.getId());
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href,
resource.getHref());
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type,
resource.getMediaType().getName());
serializer.endTag(NAMESPACE_OPF, OPFTags.item);
}
/**
* List all spine references
*
* @throws IOException f
* @throws IllegalStateException f
* @throws IllegalArgumentException f
*/
@SuppressWarnings("unused")
private static void writeSpineItems(Spine spine, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
for (SpineReference spineReference : spine.getSpineReferences()) {
serializer.startTag(NAMESPACE_OPF, OPFTags.itemref);
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref,
spineReference.getResourceId());
if (!spineReference.isLinear()) {
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear,
OPFValues.no);
}
serializer.endTag(NAMESPACE_OPF, OPFTags.itemref);
}
}
private static void writeGuide(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.guide);
ensureCoverPageGuideReferenceWritten(book.getGuide(), epubWriter,
serializer);
for (GuideReference reference : book.getGuide().getReferences()) {
writeGuideReference(reference, serializer);
}
serializer.endTag(NAMESPACE_OPF, OPFTags.guide);
}
@SuppressWarnings("unused")
private static void ensureCoverPageGuideReferenceWritten(Guide guide,
EpubWriter epubWriter, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
if (!(guide.getGuideReferencesByType(GuideReference.COVER).isEmpty())) {
return;
}
Resource coverPage = guide.getCoverPage();
if (coverPage != null) {
writeGuideReference(
new GuideReference(guide.getCoverPage(), GuideReference.COVER,
GuideReference.COVER), serializer);
}
}
private static void writeGuideReference(GuideReference reference,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
if (reference == null) {
return;
}
serializer.startTag(NAMESPACE_OPF, OPFTags.reference);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.type,
reference.getType());
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href,
reference.getCompleteHref());
if (StringUtil.isNotBlank(reference.getTitle())) {
serializer
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.title,
reference.getTitle());
}
serializer.endTag(NAMESPACE_OPF, OPFTags.reference);
}
}
@@ -0,0 +1,177 @@
package me.ag2s.epublib.epub;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import me.ag2s.epublib.domain.EpubResourceProvider;
import me.ag2s.epublib.domain.LazyResource;
import me.ag2s.epublib.domain.LazyResourceProvider;
import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.domain.Resources;
import me.ag2s.epublib.util.CollectionUtil;
import me.ag2s.epublib.util.ResourceUtil;
import me.ag2s.epublib.util.zip.ZipEntryWrapper;
import me.ag2s.epublib.util.zip.ZipFileWrapper;
/**
* Loads Resources from inputStreams, ZipFiles, etc
*
* @author paul
*/
public class ResourcesLoader {
private static final String TAG = ResourcesLoader.class.getName();
/**
* Loads the entries of the zipFileWrapper as resources.
* <p>
* The MediaTypes that are in the lazyLoadedTypes will not get their
* contents loaded, but are stored as references to entries into the
* AndroidZipFile and are loaded on demand by the Resource system.
*
* @param zipFileWrapper import epub zipfile
* @param defaultHtmlEncoding epub xhtml default encoding
* @param lazyLoadedTypes lazyLoadedTypes
* @return Resources
* @throws IOException IOException
*/
public static Resources loadResources(
ZipFileWrapper zipFileWrapper,
String defaultHtmlEncoding,
List<MediaType> lazyLoadedTypes
) throws IOException {
LazyResourceProvider resourceProvider =
new EpubResourceProvider(zipFileWrapper);
Resources result = new Resources();
Enumeration entries = zipFileWrapper.entries();
while (entries.hasMoreElements()) {
ZipEntryWrapper zipEntry = new ZipEntryWrapper(entries.nextElement());
if (zipEntry == null || zipEntry.isDirectory()) {
continue;
}
String href = zipEntry.getName();
Resource resource;
if (shouldLoadLazy(href, lazyLoadedTypes)) {
resource = new LazyResource(resourceProvider, zipEntry.getSize(), href);
} else {
resource = ResourceUtil
.createResource(zipEntry.getName(), zipFileWrapper.getInputStream(zipEntry));
}
if (resource.getMediaType() == MediaTypes.XHTML) {
resource.setInputEncoding(defaultHtmlEncoding);
}
result.add(resource);
}
return result;
}
/**
* Whether the given href will load a mediaType that is in the
* collection of lazilyLoadedMediaTypes.
*
* @param href href
* @param lazilyLoadedMediaTypes lazilyLoadedMediaTypes
* @return Whether the given href will load a mediaType that is
* in the collection of lazilyLoadedMediaTypes.
*/
private static boolean shouldLoadLazy(String href,
Collection<MediaType> lazilyLoadedMediaTypes) {
if (CollectionUtil.isEmpty(lazilyLoadedMediaTypes)) {
return false;
}
MediaType mediaType = MediaTypes.determineMediaType(href);
return lazilyLoadedMediaTypes.contains(mediaType);
}
/**
* Loads all entries from the ZipInputStream as Resources.
* <p>
* Loads the contents of all ZipEntries into memory.
* Is fast, but may lead to memory problems when reading large books
* on devices with small amounts of memory.
*
* @param zipInputStream zipInputStream
* @param defaultHtmlEncoding defaultHtmlEncoding
* @return Resources
* @throws IOException IOException
*/
public static Resources loadResources(ZipInputStream zipInputStream,
String defaultHtmlEncoding) throws IOException {
Resources result = new Resources();
ZipEntry zipEntry;
do {
// get next valid zipEntry
zipEntry = getNextZipEntry(zipInputStream);
if ((zipEntry == null) || zipEntry.isDirectory()) {
continue;
}
//String href = zipEntry.getName();
// store resource
Resource resource = ResourceUtil.createResource(zipEntry.getName(), zipInputStream);
if (resource.getMediaType() == MediaTypes.XHTML) {
resource.setInputEncoding(defaultHtmlEncoding);
}
result.add(resource);
} while (zipEntry != null);
return result;
}
private static ZipEntry getNextZipEntry(ZipInputStream zipInputStream)
throws IOException {
try {
return zipInputStream.getNextEntry();
} catch (ZipException e) {
//see <a href="https://github.com/psiegman/epublib/issues/122">Issue #122 Infinite loop</a>.
//when reading a file that is not a real zip archive or a zero length file, zipInputStream.getNextEntry()
//throws an exception and does not advance, so loadResources enters an infinite loop
//log.error("Invalid or damaged zip file.", e);
Log.e(TAG, e.getLocalizedMessage());
try {
zipInputStream.closeEntry();
} catch (Exception ignored) {
}
throw e;
}
}
/**
* Loads all entries from the ZipInputStream as Resources.
* <p>
* Loads the contents of all ZipEntries into memory.
* Is fast, but may lead to memory problems when reading large books
* on devices with small amounts of memory.
*
* @param zipFile zipFile
* @param defaultHtmlEncoding defaultHtmlEncoding
* @return Resources
* @throws IOException IOException
*/
public static Resources loadResources(ZipFileWrapper zipFile, String defaultHtmlEncoding) throws IOException {
List<MediaType> ls = new ArrayList<>();
return loadResources(zipFile, defaultHtmlEncoding, ls);
}
}
@@ -0,0 +1,72 @@
package me.ag2s.epublib.util;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
public class CollectionUtil {
/**
* Wraps an Enumeration around an Iterator
*
* @param <T>
* @author paul.siegmann
*/
private static class IteratorEnumerationAdapter<T> implements Enumeration<T> {
private final Iterator<T> iterator;
public IteratorEnumerationAdapter(Iterator<T> iter) {
this.iterator = iter;
}
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public T nextElement() {
return iterator.next();
}
}
/**
* Creates an Enumeration out of the given Iterator.
*
* @param <T> g
* @param it g
* @return an Enumeration created out of the given Iterator.
*/
@SuppressWarnings("unused")
public static <T> Enumeration<T> createEnumerationFromIterator(
Iterator<T> it) {
return new IteratorEnumerationAdapter<>(it);
}
/**
* Returns the first element of the list, null if the list is null or empty.
*
* @param <T> f
* @param list f
* @return the first element of the list, null if the list is null or empty.
*/
public static <T> T first(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
/**
* Whether the given collection is null or has no elements.
*
* @param collection g
* @return Whether the given collection is null or has no elements.
*/
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
}
+959
View File
@@ -0,0 +1,959 @@
package me.ag2s.epublib.util;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import me.ag2s.epublib.util.commons.io.IOConsumer;
/**
* Most of the functions herein are re-implementations of the ones in
* apache io IOUtils.
* <p>
* The reason for re-implementing this is that the functions are fairly simple
* and using my own implementation saves the inclusion of a 200Kb jar file.
*/
public class IOUtil {
private static final String TAG = IOUtil.class.getName();
/**
* Represents the end-of-file (or stream).
*
* @since 2.5 (made public)
*/
public static final int EOF = -1;
public static final int DEFAULT_BUFFER_SIZE = 1024 * 8;
private static final byte[] SKIP_BYTE_BUFFER = new byte[DEFAULT_BUFFER_SIZE];
// Allocated in the relevant skip method if necessary.
/*
* These buffers are static and are shared between threads.
* This is possible because the buffers are write-only - the contents are never read.
*
* N.B. there is no need to synchronize when creating these because:
* - we don't care if the buffer is created multiple times (the data is ignored)
* - we always use the same size buffer, so if it it is recreated it will still be OK
* (if the buffer size were variable, we would need to synch. to ensure some other thread
* did not create a smaller one)
*/
private static char[] SKIP_CHAR_BUFFER;
/**
* Gets the contents of the Reader as a byte[], with the given character encoding.
*
* @param in g
* @param encoding g
* @return the contents of the Reader as a byte[], with the given character encoding.
* @throws IOException g
*/
public static byte[] toByteArray(Reader in, String encoding)
throws IOException {
StringWriter out = new StringWriter();
copy(in, out);
out.flush();
return out.toString().getBytes(encoding);
}
/**
* Returns the contents of the InputStream as a byte[]
*
* @param in f
* @return the contents of the InputStream as a byte[]
* @throws IOException f
*/
public static byte[] toByteArray(InputStream in) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
copy(in, result);
result.flush();
return result.toByteArray();
}
/**
* Reads data from the InputStream, using the specified buffer size.
* <p>
* This is meant for situations where memory is tight, since
* it prevents buffer expansion.
*
* @param in the stream to read data from
* @param size the size of the array to create
* @return the array, or null
* @throws IOException f
*/
public static byte[] toByteArray(InputStream in, int size)
throws IOException {
try {
ByteArrayOutputStream result;
if (size > 0) {
result = new ByteArrayOutputStream(size);
} else {
result = new ByteArrayOutputStream();
}
copy(in, result);
result.flush();
return result.toByteArray();
} catch (OutOfMemoryError error) {
//Return null so it gets loaded lazily.
return null;
}
}
/**
* if totalNrRead &lt; 0 then totalNrRead is returned, if
* (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead
* is returned, -1 otherwise.
*
* @param nrRead f
* @param totalNrNread f
* @return if totalNrRead &lt; 0 then totalNrRead is returned, if
* (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead
* is returned, -1 otherwise.
*/
protected static int calcNewNrReadSize(int nrRead, int totalNrNread) {
if (totalNrNread < 0) {
return totalNrNread;
}
if (totalNrNread > (Integer.MAX_VALUE - nrRead)) {
return -1;
} else {
return (totalNrNread + nrRead);
}
}
//
public static void copy(InputStream in, OutputStream result) throws IOException {
copy(in, result, DEFAULT_BUFFER_SIZE);
}
/**
* Copies bytes from an <code>InputStream</code> to an <code>OutputStream</code> using an internal buffer of the
* given size.
* <p>
* This method buffers the input internally, so there is no need to use a <code>BufferedInputStream</code>.
* </p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param bufferSize the bufferSize used to copy from the input to the output
* @return the number of bytes copied. or {@code 0} if {@code input is null}.
* @throws NullPointerException if the output is null
* @throws IOException if an I/O error occurs
* @since 2.5
*/
public static long copy(final InputStream input, final OutputStream output, final int bufferSize)
throws IOException {
return copyLarge(input, output, new byte[bufferSize]);
}
/**
* Copies bytes from an <code>InputStream</code> to chars on a
* <code>Writer</code> using the default character encoding of the platform.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* This method uses {@link InputStreamReader}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>Writer</code> to write to
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.1
* @deprecated 2.5 use {@link #copy(InputStream, Writer, Charset)} instead
*/
@Deprecated
public static void copy(final InputStream input, final Writer output)
throws IOException {
copy(input, output, Charset.defaultCharset());
}
/**
* Copies bytes from an <code>InputStream</code> to chars on a
* <code>Writer</code> using the specified character encoding.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* This method uses {@link InputStreamReader}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>Writer</code> to write to
* @param inputCharset the charset to use for the input stream, null means platform default
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.3
*/
public static void copy(final InputStream input, final Writer output, final Charset inputCharset)
throws IOException {
final InputStreamReader in = new InputStreamReader(input, inputCharset.name());
copy(in, output);
}
/**
* Copies bytes from an <code>InputStream</code> to chars on a
* <code>Writer</code> using the specified character encoding.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* <p>
* Character encoding names can be found at
* <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
* <p>
* This method uses {@link InputStreamReader}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>Writer</code> to write to
* @param inputCharsetName the name of the requested charset for the InputStream, null means platform default
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
* .UnsupportedEncodingException} in version 2.2 if the
* encoding is not supported.
* @since 1.1
*/
public static void copy(final InputStream input, final Writer output, final String inputCharsetName)
throws IOException {
copy(input, output, Charset.forName(inputCharsetName));
}
/**
* Copies chars from a <code>Reader</code> to a <code>Appendable</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
* Large streams (over 2GB) will return a chars copied value of
* <code>-1</code> after the copy has completed since the correct
* number of chars cannot be returned as an int. For large streams
* use the <code>copyLarge(Reader, Writer)</code> method.
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Appendable</code> to write to
* @return the number of characters copied, or -1 if &gt; Integer.MAX_VALUE
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.7
*/
public static long copy(final Reader input, final Appendable output) throws IOException {
return copy(input, output, CharBuffer.allocate(DEFAULT_BUFFER_SIZE));
}
/**
* Copies chars from a <code>Reader</code> to an <code>Appendable</code>.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedReader</code>.
* </p>
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Appendable</code> to write to
* @param buffer the buffer to be used for the copy
* @return the number of characters copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.7
*/
public static long copy(final Reader input, final Appendable output, final CharBuffer buffer) throws IOException {
long count = 0;
int n;
while (EOF != (n = input.read(buffer))) {
buffer.flip();
output.append(buffer, 0, n);
count += n;
}
return count;
}
/**
* Copies chars from a <code>Reader</code> to bytes on an
* <code>OutputStream</code> using the default character encoding of the
* platform, and calling flush.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
* Due to the implementation of OutputStreamWriter, this method performs a
* flush.
* <p>
* This method uses {@link OutputStreamWriter}.
*
* @param input the <code>Reader</code> to read from
* @param output the <code>OutputStream</code> to write to
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.1
* @deprecated 2.5 use {@link #copy(Reader, OutputStream, Charset)} instead
*/
@Deprecated
public static void copy(final Reader input, final OutputStream output)
throws IOException {
copy(input, output, Charset.defaultCharset());
}
/**
* Copies chars from a <code>Reader</code> to bytes on an
* <code>OutputStream</code> using the specified character encoding, and
* calling flush.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* </p>
* <p>
* Due to the implementation of OutputStreamWriter, this method performs a
* flush.
* </p>
* <p>
* This method uses {@link OutputStreamWriter}.
* </p>
*
* @param input the <code>Reader</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param outputCharset the charset to use for the OutputStream, null means platform default
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.3
*/
public static void copy(final Reader input, final OutputStream output, final Charset outputCharset)
throws IOException {
final OutputStreamWriter out = new OutputStreamWriter(output, outputCharset.name());
copy(input, out);
// XXX Unless anyone is planning on rewriting OutputStreamWriter,
// we have to flush here.
out.flush();
}
/**
* Copies chars from a <code>Reader</code> to bytes on an
* <code>OutputStream</code> using the specified character encoding, and
* calling flush.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
* Character encoding names can be found at
* <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
* <p>
* Due to the implementation of OutputStreamWriter, this method performs a
* flush.
* <p>
* This method uses {@link OutputStreamWriter}.
*
* @param input the <code>Reader</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
* .UnsupportedEncodingException} in version 2.2 if the
* encoding is not supported.
* @since 1.1
*/
public static void copy(final Reader input, final OutputStream output, final String outputCharsetName)
throws IOException {
copy(input, output, Charset.forName(outputCharsetName));
}
/**
* Copies chars from a <code>Reader</code> to a <code>Writer</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
* Large streams (over 2GB) will return a chars copied value of
* <code>-1</code> after the copy has completed since the correct
* number of chars cannot be returned as an int. For large streams
* use the <code>copyLarge(Reader, Writer)</code> method.
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Writer</code> to write to
* @return the number of characters copied, or -1 if &gt; Integer.MAX_VALUE
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.1
*/
public static int copy(final Reader input, final Writer output) throws IOException {
final long count = copyLarge(input, output);
if (count > Integer.MAX_VALUE) {
return -1;
}
return (int) count;
}
/**
* Copies bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* </p>
* <p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
* </p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @return the number of bytes copied. or {@code 0} if {@code input is null}.
* @throws NullPointerException if the output is null
* @throws IOException if an I/O error occurs
* @since 1.3
*/
public static long copyLarge(final InputStream input, final OutputStream output)
throws IOException {
return copy(input, output, DEFAULT_BUFFER_SIZE);
}
/**
* Copies bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedInputStream</code>.
* </p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param buffer the buffer to use for the copy
* @return the number of bytes copied. or {@code 0} if {@code input is null}.
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer)
throws IOException {
long count = 0;
if (input != null) {
int n;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
//input.close();
}
return count;
}
/**
* Copies some or all bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>, optionally skipping input bytes.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedInputStream</code>.
* </p>
* <p>
* Note that the implementation uses {@link #skip(InputStream, long)}.
* This means that the method may be considerably less efficient than using the actual skip implementation,
* this is done to guarantee that the correct number of characters are skipped.
* </p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param inputOffset : number of bytes to skip from input before copying
* -ve values are ignored
* @param length : number of bytes to copy. -ve means all
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset,
final long length) throws IOException {
return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]);
}
/**
* Copies some or all bytes from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>, optionally skipping input bytes.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedInputStream</code>.
* </p>
* <p>
* Note that the implementation uses {@link #skip(InputStream, long)}.
* This means that the method may be considerably less efficient than using the actual skip implementation,
* this is done to guarantee that the correct number of characters are skipped.
* </p>
*
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @param inputOffset : number of bytes to skip from input before copying
* -ve values are ignored
* @param length : number of bytes to copy. -ve means all
* @param buffer the buffer to use for the copy
* @return the number of bytes copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(final InputStream input, final OutputStream output,
final long inputOffset, final long length, final byte[] buffer) throws IOException {
if (inputOffset > 0) {
skipFully(input, inputOffset);
}
if (length == 0) {
return 0;
}
final int bufferLength = buffer.length;
int bytesToRead = bufferLength;
if (length > 0 && length < bufferLength) {
bytesToRead = (int) length;
}
int read;
long totalRead = 0;
while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {
output.write(buffer, 0, read);
totalRead += read;
if (length > 0) { // only adjust length if not reading to the end
// Note the cast must work because buffer.length is an integer
bytesToRead = (int) Math.min(length - totalRead, bufferLength);
}
}
return totalRead;
}
/**
* Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Writer</code> to write to
* @return the number of characters copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 1.3
*/
public static long copyLarge(final Reader input, final Writer output) throws IOException {
return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]);
}
/**
* Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Writer</code> to write to
* @param buffer the buffer to be used for the copy
* @return the number of characters copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(final Reader input, final Writer output, final char[] buffer) throws IOException {
long count = 0;
int n;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
/**
* Copies some or all chars from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>, optionally skipping input chars.
* <p>
* This method buffers the input internally, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
* The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Writer</code> to write to
* @param inputOffset : number of chars to skip from input before copying
* -ve values are ignored
* @param length : number of chars to copy. -ve means all
* @return the number of chars copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length)
throws IOException {
return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]);
}
/**
* Copies some or all chars from a large (over 2GB) <code>InputStream</code> to an
* <code>OutputStream</code>, optionally skipping input chars.
* <p>
* This method uses the provided buffer, so there is no need to use a
* <code>BufferedReader</code>.
* <p>
*
* @param input the <code>Reader</code> to read from
* @param output the <code>Writer</code> to write to
* @param inputOffset : number of chars to skip from input before copying
* -ve values are ignored
* @param length : number of chars to copy. -ve means all
* @param buffer the buffer to be used for the copy
* @return the number of chars copied
* @throws NullPointerException if the input or output is null
* @throws IOException if an I/O error occurs
* @since 2.2
*/
public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length,
final char[] buffer)
throws IOException {
if (inputOffset > 0) {
skipFully(input, inputOffset);
}
if (length == 0) {
return 0;
}
int bytesToRead = buffer.length;
if (length > 0 && length < buffer.length) {
bytesToRead = (int) length;
}
int read;
long totalRead = 0;
while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {
output.write(buffer, 0, read);
totalRead += read;
if (length > 0) { // only adjust length if not reading to the end
// Note the cast must work because buffer.length is an integer
bytesToRead = (int) Math.min(length - totalRead, buffer.length);
}
}
return totalRead;
}
/**
* Skips bytes from an input byte stream.
* This implementation guarantees that it will read as many bytes
* as possible before giving up; this may not always be the case for
* skip() implementations in subclasses of {@link InputStream}.
* <p>
* Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather
* than delegating to {@link InputStream#skip(long)}.
* This means that the method may be considerably less efficient than using the actual skip implementation,
* this is done to guarantee that the correct number of bytes are skipped.
* </p>
*
* @param input byte stream to skip
* @param toSkip number of bytes to skip.
* @return number of bytes actually skipped.
* @throws IOException if there is a problem reading the file
* @throws IllegalArgumentException if toSkip is negative
* @see InputStream#skip(long)
* @see <a href="https://issues.apache.org/jira/browse/IO-203">IO-203 - Add skipFully() method for InputStreams</a>
* @since 2.0
*/
public static long skip(final InputStream input, final long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
}
/*
* N.B. no need to synchronize access to SKIP_BYTE_BUFFER: - we don't care if the buffer is created multiple
* times (the data is ignored) - we always use the same size buffer, so if it it is recreated it will still be
* OK (if the buffer size were variable, we would need to synch. to ensure some other thread did not create a
* smaller one)
*/
long remain = toSkip;
while (remain > 0) {
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length));
if (n < 0) { // EOF
break;
}
remain -= n;
}
return toSkip - remain;
}
/**
* Skips bytes from a ReadableByteChannel.
* This implementation guarantees that it will read as many bytes
* as possible before giving up.
*
* @param input ReadableByteChannel to skip
* @param toSkip number of bytes to skip.
* @return number of bytes actually skipped.
* @throws IOException if there is a problem reading the ReadableByteChannel
* @throws IllegalArgumentException if toSkip is negative
* @since 2.5
*/
public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
}
final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BYTE_BUFFER.length));
long remain = toSkip;
while (remain > 0) {
skipByteBuffer.position(0);
skipByteBuffer.limit((int) Math.min(remain, SKIP_BYTE_BUFFER.length));
final int n = input.read(skipByteBuffer);
if (n == EOF) {
break;
}
remain -= n;
}
return toSkip - remain;
}
/**
* Skips characters from an input character stream.
* This implementation guarantees that it will read as many characters
* as possible before giving up; this may not always be the case for
* skip() implementations in subclasses of {@link Reader}.
* <p>
* Note that the implementation uses {@link Reader#read(char[], int, int)} rather
* than delegating to {@link Reader#skip(long)}.
* This means that the method may be considerably less efficient than using the actual skip implementation,
* this is done to guarantee that the correct number of characters are skipped.
* </p>
*
* @param input character stream to skip
* @param toSkip number of characters to skip.
* @return number of characters actually skipped.
* @throws IOException if there is a problem reading the file
* @throws IllegalArgumentException if toSkip is negative
* @see Reader#skip(long)
* @see <a href="https://issues.apache.org/jira/browse/IO-203">IO-203 - Add skipFully() method for InputStreams</a>
* @since 2.0
*/
public static long skip(final Reader input, final long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
}
/*
* N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
* is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
* size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
*/
if (SKIP_CHAR_BUFFER == null) {
SKIP_CHAR_BUFFER = new char[SKIP_BYTE_BUFFER.length];
}
long remain = toSkip;
while (remain > 0) {
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
final long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length));
if (n < 0) { // EOF
break;
}
remain -= n;
}
return toSkip - remain;
}
/**
* Skips the requested number of bytes or fail if there are not enough left.
* <p>
* This allows for the possibility that {@link InputStream#skip(long)} may
* not skip as many bytes as requested (most likely because of reaching EOF).
* <p>
* Note that the implementation uses {@link #skip(InputStream, long)}.
* This means that the method may be considerably less efficient than using the actual skip implementation,
* this is done to guarantee that the correct number of characters are skipped.
* </p>
*
* @param input stream to skip
* @param toSkip the number of bytes to skip
* @throws IOException if there is a problem reading the file
* @throws IllegalArgumentException if toSkip is negative
* @throws EOFException if the number of bytes skipped was incorrect
* @see InputStream#skip(long)
* @since 2.0
*/
public static void skipFully(final InputStream input, final long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
}
final long skipped = skip(input, toSkip);
if (skipped != toSkip) {
throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
}
}
/**
* Skips the requested number of bytes or fail if there are not enough left.
*
* @param input ReadableByteChannel to skip
* @param toSkip the number of bytes to skip
* @throws IOException if there is a problem reading the ReadableByteChannel
* @throws IllegalArgumentException if toSkip is negative
* @throws EOFException if the number of bytes skipped was incorrect
* @since 2.5
*/
public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException {
if (toSkip < 0) {
throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
}
final long skipped = skip(input, toSkip);
if (skipped != toSkip) {
throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
}
}
/**
* Skips the requested number of characters or fail if there are not enough left.
* <p>
* This allows for the possibility that {@link Reader#skip(long)} may
* not skip as many characters as requested (most likely because of reaching EOF).
* <p>
* Note that the implementation uses {@link #skip(Reader, long)}.
* This means that the method may be considerably less efficient than using the actual skip implementation,
* this is done to guarantee that the correct number of characters are skipped.
* </p>
*
* @param input stream to skip
* @param toSkip the number of characters to skip
* @throws IOException if there is a problem reading the file
* @throws IllegalArgumentException if toSkip is negative
* @throws EOFException if the number of characters skipped was incorrect
* @see Reader#skip(long)
* @since 2.0
*/
public static void skipFully(final Reader input, final long toSkip) throws IOException {
final long skipped = skip(input, toSkip);
if (skipped != toSkip) {
throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped);
}
}
/**
* Returns the length of the given array in a null-safe manner.
*
* @param array an array or null
* @return the array length -- or 0 if the given array is null.
* @since 2.7
*/
public static int length(final byte[] array) {
return array == null ? 0 : array.length;
}
/**
* Returns the length of the given array in a null-safe manner.
*
* @param array an array or null
* @return the array length -- or 0 if the given array is null.
* @since 2.7
*/
public static int length(final char[] array) {
return array == null ? 0 : array.length;
}
/**
* Returns the length of the given CharSequence in a null-safe manner.
*
* @param csq a CharSequence or null
* @return the CharSequence length -- or 0 if the given CharSequence is null.
* @since 2.7
*/
public static int length(final CharSequence csq) {
return csq == null ? 0 : csq.length();
}
/**
* Returns the length of the given array in a null-safe manner.
*
* @param array an array or null
* @return the array length -- or 0 if the given array is null.
* @since 2.7
*/
public static int length(final Object[] array) {
return array == null ? 0 : array.length;
}
/**
* Closes the given {@link Closeable} as a null-safe operation.
*
* @param closeable The resource to close, may be null.
* @throws IOException if an I/O error occurs.
* @since 2.7
*/
public static void close(final Closeable closeable) throws IOException {
if (closeable != null) {
closeable.close();
}
}
/**
* Closes the given {@link Closeable} as a null-safe operation.
*
* @param closeables The resource(s) to close, may be null.
* @throws IOException if an I/O error occurs.
* @since 2.8.0
*/
public static void close(final Closeable... closeables) throws IOException {
if (closeables != null) {
for (final Closeable closeable : closeables) {
close(closeable);
}
}
}
/**
* Closes the given {@link Closeable} as a null-safe operation.
*
* @param closeable The resource to close, may be null.
* @param consumer Consume the IOException thrown by {@link Closeable#close()}.
* @throws IOException if an I/O error occurs.
* @since 2.7
*/
public static void close(final Closeable closeable, final IOConsumer<IOException> consumer) throws IOException {
if (closeable != null) {
try {
closeable.close();
} catch (final IOException e) {
if (consumer != null) {
consumer.accept(e);
}
}
}
}
/**
* Closes a URLConnection.
*
* @param conn the connection to close.
* @since 2.4
*/
public static void close(final URLConnection conn) {
if (conn instanceof HttpURLConnection) {
((HttpURLConnection) conn).disconnect();
}
}
@SuppressWarnings("unused")
public static String Stream2String(InputStream inputStream) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
return result.toString();
} catch (Exception e) {
return e.getLocalizedMessage();
}
}
}
@@ -0,0 +1,33 @@
package me.ag2s.epublib.util;
import java.io.IOException;
import java.io.OutputStream;
/**
* OutputStream with the close() disabled.
* We write multiple documents to a ZipOutputStream.
* Some of the formatters call a close() after writing their data.
* We don't want them to do that, so we wrap regular OutputStreams in this NoCloseOutputStream.
*
* @author paul
*/
@SuppressWarnings("unused")
public class NoCloseOutputStream extends OutputStream {
private final OutputStream outputStream;
public NoCloseOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
}
/**
* A close() that does not call it's parent's close()
*/
public void close() {
}
}
@@ -0,0 +1,36 @@
package me.ag2s.epublib.util;
import java.io.IOException;
import java.io.Writer;
/**
* Writer with the close() disabled.
* We write multiple documents to a ZipOutputStream.
* Some of the formatters call a close() after writing their data.
* We don't want them to do that, so we wrap regular Writers in this NoCloseWriter.
*
* @author paul
*/
@SuppressWarnings("unused")
public class NoCloseWriter extends Writer {
private final Writer writer;
public NoCloseWriter(Writer writer) {
this.writer = writer;
}
@Override
public void close() {
}
@Override
public void flush() throws IOException {
writer.flush();
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
writer.write(cbuf, off, len);
}
}
@@ -0,0 +1,181 @@
package me.ag2s.epublib.util;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.epub.EpubProcessorSupport;
/**
* Various resource utility methods
*
* @author paul
*/
public class ResourceUtil {
/**
* 快速创建HTML类型的Resource
*
* @param title 章节的标题
* @param txt 章节的正文
* @param model html模板
* @return 返回Resource
*/
public static Resource createChapterResource(String title, String txt, String model, String href) {
//String[] title_list = title.split("\\s+", 2);
//String title_part1 = title_list[0];
//String title_part2 = "";
//if (title_list.length == 2) {
// title_part2 = title_list[1];
//}
String ori_title = title;
title = title.replaceFirst("\\s+", "</span><br />");
if (title.contains("</span>")) {
title = "<span class=\"chapter-sequence-number\">" + title;
}
String html = model.replace("{title}", title)
.replace("{ori_title}", ori_title)
//.replace("{title_part1}", title_part1)
//.replace("{title_part2}", title_part2)
.replace("{content}", StringUtil.formatHtml(txt));
return new Resource(html.getBytes(), href);
}
public static Resource createPublicResource(String name, String author, String intro, String kind, String wordCount, String model, String href) {
String html = model.replace("{name}", name)
.replace("{author}", author)
.replace("{kind}", kind == null ? "" : kind)
.replace("{wordCount}", wordCount == null ? "" : wordCount)
.replace("{intro}", StringUtil.formatHtml(intro == null ? "" : intro));
return new Resource(html.getBytes(), href);
}
/**
* 快速从File创建Resource
*
* @param file File
* @return Resource
* @throws IOException IOException
*/
@SuppressWarnings("unused")
public static Resource createResource(File file) throws IOException {
if (file == null) {
return null;
}
MediaType mediaType = MediaTypes.determineMediaType(file.getName());
byte[] data = IOUtil.toByteArray(new FileInputStream(file));
return new Resource(data, mediaType);
}
/**
* 创建一个只带标题的HTMl类型的Resource,常用于封面页,大卷页
*
* @param title v
* @param href v
* @return a resource with as contents a html page with the given title.
*/
@SuppressWarnings("unused")
public static Resource createResource(String title, String href) {
String content =
"<html><head><title>" + title + "</title></head><body><h1>" + title
+ "</h1></body></html>";
return new Resource(null, content.getBytes(), href, MediaTypes.XHTML,
Constants.CHARACTER_ENCODING);
}
/**
* Creates a resource out of the given zipEntry and zipInputStream.
*
* @param name v
* @param zipInputStream v
* @return a resource created out of the given zipEntry and zipInputStream.
* @throws IOException v
*/
public static Resource createResource(String name,
ZipInputStream zipInputStream) throws IOException {
return new Resource(zipInputStream, name);
}
public static Resource createResource(String name,
InputStream zipInputStream) throws IOException {
return new Resource(zipInputStream, name);
}
/**
* Converts a given string from given input character encoding to the requested output character encoding.
*
* @param inputEncoding v
* @param outputEncoding v
* @param input v
* @return the string from given input character encoding converted to the requested output character encoding.
* @throws UnsupportedEncodingException v
*/
@SuppressWarnings("unused")
public static byte[] recode(String inputEncoding, String outputEncoding,
byte[] input) throws UnsupportedEncodingException {
return new String(input, inputEncoding).getBytes(outputEncoding);
}
/**
* Gets the contents of the Resource as an InputSource in a null-safe manner.
*/
@SuppressWarnings("unused")
public static InputSource getInputSource(Resource resource)
throws IOException {
if (resource == null) {
return null;
}
Reader reader = resource.getReader();
if (reader == null) {
return null;
}
return new InputSource(reader);
}
/**
* Reads parses the xml therein and returns the result as a Document
*/
public static Document getAsDocument(Resource resource)
throws SAXException, IOException {
return getAsDocument(resource,
EpubProcessorSupport.createDocumentBuilder());
}
/**
* Reads the given resources inputstream, parses the xml therein and returns the result as a Document
*
* @param resource v
* @param documentBuilder v
* @return the document created from the given resource
* @throws UnsupportedEncodingException v
* @throws SAXException v
* @throws IOException v
*/
public static Document getAsDocument(Resource resource,
DocumentBuilder documentBuilder)
throws UnsupportedEncodingException, SAXException, IOException {
InputSource inputSource = getInputSource(resource);
if (inputSource == null) {
return null;
}
return documentBuilder.parse(inputSource);
}
}
@@ -0,0 +1,305 @@
package me.ag2s.epublib.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Various String utility functions.
* <p>
* Most of the functions herein are re-implementations of the ones in apache
* commons StringUtils. The reason for re-implementing this is that the
* functions are fairly simple and using my own implementation saves the
* inclusion of a 200Kb jar file.
*
* @author paul.siegmann
*/
public class StringUtil {
/**
* Changes a path containing '..', '.' and empty dirs into a path that
* doesn't. X/foo/../Y is changed into 'X/Y', etc. Does not handle invalid
* paths like "../".
*
* @param path path
* @return the normalized path
*/
public static String collapsePathDots(String path) {
String[] stringParts = path.split("/");
List<String> parts = new ArrayList<>(Arrays.asList(stringParts));
for (int i = 0; i < parts.size() - 1; i++) {
String currentDir = parts.get(i);
if (currentDir.length() == 0 || currentDir.equals(".")) {
parts.remove(i);
i--;
} else if (currentDir.equals("..")) {
parts.remove(i - 1);
parts.remove(i - 1);
i -= 2;
}
}
StringBuilder result = new StringBuilder();
if (path.startsWith("/")) {
result.append('/');
}
for (int i = 0; i < parts.size(); i++) {
result.append(parts.get(i));
if (i < (parts.size() - 1)) {
result.append('/');
}
}
return result.toString();
}
/**
* Whether the String is not null, not zero-length and does not contain of
* only whitespace.
*
* @param text text
* @return Whether the String is not null, not zero-length and does not contain of
*/
public static boolean isNotBlank(String text) {
return !isBlank(text);
}
/**
* Whether the String is null, zero-length and does contain only whitespace.
*
* @return Whether the String is null, zero-length and does contain only whitespace.
*/
public static boolean isBlank(String text) {
if (isEmpty(text)) {
return true;
}
for (int i = 0; i < text.length(); i++) {
if (!Character.isWhitespace(text.charAt(i))) {
return false;
}
}
return true;
}
/**
* Whether the given string is null or zero-length.
*
* @param text the input for this method
* @return Whether the given string is null or zero-length.
*/
public static boolean isEmpty(String text) {
return (text == null) || (text.length() == 0);
}
/**
* Whether the given source string ends with the given suffix, ignoring
* case.
*
* @param source source
* @param suffix suffix
* @return Whether the given source string ends with the given suffix, ignoring case.
*/
public static boolean endsWithIgnoreCase(String source, String suffix) {
if (isEmpty(suffix)) {
return true;
}
if (isEmpty(source)) {
return false;
}
if (suffix.length() > source.length()) {
return false;
}
return source.substring(source.length() - suffix.length())
.toLowerCase().endsWith(suffix.toLowerCase());
}
public static boolean startsWithIgnoreCase(String source, String prefix) {
if (isEmpty(prefix)) {
return true;
}
if (isEmpty(source)) {
return false;
}
if (prefix.length() > source.length()) {
return false;
}
return source.substring(0, prefix.length())
.toLowerCase().startsWith(prefix.toLowerCase());
}
/**
* If the given text is null return "", the original text otherwise.
*
* @param text text
* @return If the given text is null "", the original text otherwise.
*/
public static String defaultIfNull(String text) {
return defaultIfNull(text, "");
}
/**
* If the given text is null return "", the given defaultValue otherwise.
*
* @param text d
* @param defaultValue d
* @return If the given text is null "", the given defaultValue otherwise.
*/
public static String defaultIfNull(String text, String defaultValue) {
if (text == null) {
return defaultValue;
}
return text;
}
/**
* Null-safe string comparator
*
* @param text1 d
* @param text2 d
* @return whether the two strings are equal
*/
public static boolean equals(String text1, String text2) {
if (text1 == null) {
return (text2 == null);
}
return text1.equals(text2);
}
/**
* Pretty toString printer.
*
* @param keyValues d
* @return a string representation of the input values
*/
public static String toString(Object... keyValues) {
StringBuilder result = new StringBuilder();
result.append('[');
for (int i = 0; i < keyValues.length; i += 2) {
if (i > 0) {
result.append(", ");
}
result.append(keyValues[i]);
result.append(": ");
Object value = null;
if ((i + 1) < keyValues.length) {
value = keyValues[i + 1];
}
if (value == null) {
result.append("<null>");
} else {
result.append('\'');
result.append(value);
result.append('\'');
}
}
result.append(']');
return result.toString();
}
public static int hashCode(String... values) {
int result = 31;
for (String value : values) {
result ^= String.valueOf(value).hashCode();
}
return result;
}
/**
* Gives the substring of the given text before the given separator.
* <p>
* If the text does not contain the given separator then the given text is
* returned.
*
* @param text d
* @param separator d
* @return the substring of the given text before the given separator.
*/
public static String substringBefore(String text, char separator) {
if (isEmpty(text)) {
return text;
}
int sepPos = text.indexOf(separator);
if (sepPos < 0) {
return text;
}
return text.substring(0, sepPos);
}
/**
* Gives the substring of the given text before the last occurrence of the
* given separator.
* <p>
* If the text does not contain the given separator then the given text is
* returned.
*
* @param text d
* @param separator d
* @return the substring of the given text before the last occurrence of the given separator.
*/
public static String substringBeforeLast(String text, char separator) {
if (isEmpty(text)) {
return text;
}
int cPos = text.lastIndexOf(separator);
if (cPos < 0) {
return text;
}
return text.substring(0, cPos);
}
/**
* Gives the substring of the given text after the last occurrence of the
* given separator.
* <p>
* If the text does not contain the given separator then "" is returned.
*
* @param text d
* @param separator d
* @return the substring of the given text after the last occurrence of the given separator.
*/
public static String substringAfterLast(String text, char separator) {
if (isEmpty(text)) {
return text;
}
int cPos = text.lastIndexOf(separator);
if (cPos < 0) {
return "";
}
return text.substring(cPos + 1);
}
/**
* Gives the substring of the given text after the given separator.
* <p>
* If the text does not contain the given separator then "" is returned.
*
* @param text the input text
* @param c the separator char
* @return the substring of the given text after the given separator.
*/
public static String substringAfter(String text, char c) {
if (isEmpty(text)) {
return text;
}
int cPos = text.indexOf(c);
if (cPos < 0) {
return "";
}
return text.substring(cPos + 1);
}
public static String formatHtml(String text) {
StringBuilder body = new StringBuilder();
for (String s : text.split("\\r?\\n")) {
s = s.replaceAll("^\\s+|\\s+$", "");
if (s.length() > 0) {
//段落为一张图片才认定为图片章节/漫画并启用多看单图优化,否则认定为普通文字夹杂着的图片文字。
if (s.matches("(?i)^<img\\s([^>]+)/?>$")) {
body.append(s.replaceAll("(?i)^<img\\s([^>]+)/?>$",
"<div class=\"duokan-image-single\"><img class=\"picture-80\" $1/></div>"));
} else {
body.append("<p>").append(s).append("</p>");
}
}
}
return body.toString();
}
}
@@ -0,0 +1,118 @@
package me.ag2s.epublib.util;
import java.io.CharArrayWriter;
import java.nio.charset.Charset;
import java.util.BitSet;
import java.util.Objects;
import kotlin.text.Charsets;
public class URLEncodeUtil {
static BitSet dontNeedEncoding;
static final int caseDiff = ('a' - 'A');
static {
dontNeedEncoding = new BitSet(256);
int i;
for (i = 'a'; i <= 'z'; i++) {
dontNeedEncoding.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
dontNeedEncoding.set(i);
}
for (i = '0'; i <= '9'; i++) {
dontNeedEncoding.set(i);
}
String mark = "-_.!~*'()";
for (i = 0; i < mark.length(); i++) {
dontNeedEncoding.set(mark.charAt(i));
}
String reversed = ";/?:@&=+$,#";
for (i = 0; i < reversed.length(); i++) {
dontNeedEncoding.set(reversed.charAt(i));
}
}
public static String encode(String s, Charset charset) {
Objects.requireNonNull(charset, "charset");
boolean needToChange = false;
StringBuilder out = new StringBuilder(s.length());
CharArrayWriter charArrayWriter = new CharArrayWriter();
for (int i = 0; i < s.length(); ) {
int c = s.charAt(i);
//System.out.println("Examining character: " + c);
if (dontNeedEncoding.get(c)) {
//System.out.println("Storing: " + c);
out.append((char) c);
i++;
} else {
// convert to external encoding before hex conversion
do {
charArrayWriter.write(c);
/*
* If this character represents the start of a Unicode
* surrogate pair, then pass in two characters. It's not
* clear what should be done if a byte reserved in the
* surrogate pairs range occurs outside of a legal
* surrogate pair. For now, just treat it as if it were
* any other character.
*/
if (c >= 0xD800 && c <= 0xDBFF) {
/*
System.out.println(Integer.toHexString(c)
+ " is high surrogate");
*/
if ((i + 1) < s.length()) {
int d = (int) s.charAt(i + 1);
/*
System.out.println("\tExamining "
+ Integer.toHexString(d));
*/
if (d >= 0xDC00 && d <= 0xDFFF) {
/*
System.out.println("\t"
+ Integer.toHexString(d)
+ " is low surrogate");
*/
charArrayWriter.write(d);
i++;
}
}
}
i++;
} while (i < s.length() && !dontNeedEncoding.get((c = (int) s.charAt(i))));
charArrayWriter.flush();
String str = new String(charArrayWriter.toCharArray());
byte[] ba = str.getBytes(charset);
for (int j = 0; j < ba.length; j++) {
out.append('%');
char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16);
// converting to use uppercase letter as part of
// the hex value if ch is a letter.
if (Character.isLetter(ch)) {
ch -= caseDiff;
}
out.append(ch);
ch = Character.forDigit(ba[j] & 0xF, 16);
if (Character.isLetter(ch)) {
ch -= caseDiff;
}
out.append(ch);
}
charArrayWriter.reset();
needToChange = true;
}
}
return (needToChange ? out.toString() : s);
}
public static String encode(String s) {
return encode(s, Charsets.UTF_8);
}
}
@@ -0,0 +1,383 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.ag2s.epublib.util.commons.io;
import static me.ag2s.epublib.util.IOUtil.EOF;
import android.os.Build;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import me.ag2s.epublib.util.IOUtil;
/**
* This class is used to wrap a stream that includes an encoded {@link ByteOrderMark} as its first bytes.
* <p>
* This class detects these bytes and, if required, can automatically skip them and return the subsequent byte as the
* first byte in the stream.
* <p>
* The {@link ByteOrderMark} implementation has the following pre-defined BOMs:
* <ul>
* <li>UTF-8 - {@link ByteOrderMark#UTF_8}</li>
* <li>UTF-16BE - {@link ByteOrderMark#UTF_16LE}</li>
* <li>UTF-16LE - {@link ByteOrderMark#UTF_16BE}</li>
* <li>UTF-32BE - {@link ByteOrderMark#UTF_32LE}</li>
* <li>UTF-32LE - {@link ByteOrderMark#UTF_32BE}</li>
* </ul>
*
*
* <h2>Example 1 - Detect and exclude a UTF-8 BOM</h2>
*
* <pre>
* BOMInputStream bomIn = new BOMInputStream(in);
* if (bomIn.hasBOM()) {
* // has a UTF-8 BOM
* }
* </pre>
*
* <h2>Example 2 - Detect a UTF-8 BOM (but don't exclude it)</h2>
*
* <pre>
* boolean include = true;
* BOMInputStream bomIn = new BOMInputStream(in, include);
* if (bomIn.hasBOM()) {
* // has a UTF-8 BOM
* }
* </pre>
*
* <h2>Example 3 - Detect Multiple BOMs</h2>
*
* <pre>
* BOMInputStream bomIn = new BOMInputStream(in,
* ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,
* ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE
* );
* if (bomIn.hasBOM() == false) {
* // No BOM found
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {
* // has a UTF-16LE BOM
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {
* // has a UTF-16BE BOM
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_32LE)) {
* // has a UTF-32LE BOM
* } else if (bomIn.hasBOM(ByteOrderMark.UTF_32BE)) {
* // has a UTF-32BE BOM
* }
* </pre>
*
* @see ByteOrderMark
* @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia - Byte Order Mark</a>
* @since 2.0
*/
public class BOMInputStream extends ProxyInputStream {
private final boolean include;
/**
* BOMs are sorted from longest to shortest.
*/
private final List<ByteOrderMark> boms;
private ByteOrderMark byteOrderMark;
private int[] firstBytes;
private int fbLength;
private int fbIndex;
private int markFbIndex;
private boolean markedAtStart;
/**
* Constructs a new BOM InputStream that excludes a {@link ByteOrderMark#UTF_8} BOM.
*
* @param delegate the InputStream to delegate to
*/
@SuppressWarnings("unused")
public BOMInputStream(final InputStream delegate) {
this(delegate, false, ByteOrderMark.UTF_8);
}
/**
* Constructs a new BOM InputStream that detects a a {@link ByteOrderMark#UTF_8} and optionally includes it.
*
* @param delegate the InputStream to delegate to
* @param include true to include the UTF-8 BOM or false to exclude it
*/
@SuppressWarnings("unused")
public BOMInputStream(final InputStream delegate, final boolean include) {
this(delegate, include, ByteOrderMark.UTF_8);
}
/**
* Constructs a new BOM InputStream that excludes the specified BOMs.
*
* @param delegate the InputStream to delegate to
* @param boms The BOMs to detect and exclude
*/
@SuppressWarnings("unused")
public BOMInputStream(final InputStream delegate, final ByteOrderMark... boms) {
this(delegate, false, boms);
}
/**
* Compares ByteOrderMark objects in descending length order.
*/
private static final Comparator<ByteOrderMark> ByteOrderMarkLengthComparator = (bom1, bom2) -> {
final int len1 = bom1.length();
final int len2 = bom2.length();
return Integer.compare(len2, len1);
};
/**
* Constructs a new BOM InputStream that detects the specified BOMs and optionally includes them.
*
* @param delegate the InputStream to delegate to
* @param include true to include the specified BOMs or false to exclude them
* @param boms The BOMs to detect and optionally exclude
*/
public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) {
super(delegate);
if (IOUtil.length(boms) == 0) {
throw new IllegalArgumentException("No BOMs specified");
}
this.include = include;
final List<ByteOrderMark> list = Arrays.asList(boms);
// Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
list.sort(ByteOrderMarkLengthComparator);
}
this.boms = list;
}
/**
* Indicates whether the stream contains one of the specified BOMs.
*
* @return true if the stream has one of the specified BOMs, otherwise false if it does not
* @throws IOException if an error reading the first bytes of the stream occurs
*/
@SuppressWarnings("unused")
public boolean hasBOM() throws IOException {
return getBOM() != null;
}
/**
* Indicates whether the stream contains the specified BOM.
*
* @param bom The BOM to check for
* @return true if the stream has the specified BOM, otherwise false if it does not
* @throws IllegalArgumentException if the BOM is not one the stream is configured to detect
* @throws IOException if an error reading the first bytes of the stream occurs
*/
@SuppressWarnings("unused")
public boolean hasBOM(final ByteOrderMark bom) throws IOException {
if (!boms.contains(bom)) {
throw new IllegalArgumentException("Stream not configure to detect " + bom);
}
getBOM();
return byteOrderMark != null && byteOrderMark.equals(bom);
}
/**
* Return the BOM (Byte Order Mark).
*
* @return The BOM or null if none
* @throws IOException if an error reading the first bytes of the stream occurs
*/
public ByteOrderMark getBOM() throws IOException {
if (firstBytes == null) {
fbLength = 0;
// BOMs are sorted from longest to shortest
final int maxBomSize = boms.get(0).length();
firstBytes = new int[maxBomSize];
// Read first maxBomSize bytes
for (int i = 0; i < firstBytes.length; i++) {
firstBytes[i] = in.read();
fbLength++;
if (firstBytes[i] < 0) {
break;
}
}
// match BOM in firstBytes
byteOrderMark = find();
if (byteOrderMark != null) {
if (!include) {
if (byteOrderMark.length() < firstBytes.length) {
fbIndex = byteOrderMark.length();
} else {
fbLength = 0;
}
}
}
}
return byteOrderMark;
}
/**
* Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}.
*
* @return The BOM charset Name or null if no BOM found
* @throws IOException if an error reading the first bytes of the stream occurs
*/
public String getBOMCharsetName() throws IOException {
getBOM();
return byteOrderMark == null ? null : byteOrderMark.getCharsetName();
}
/**
* This method reads and either preserves or skips the first bytes in the stream. It behaves like the single-byte
* <code>read()</code> method, either returning a valid byte or -1 to indicate that the initial bytes have been
* processed already.
*
* @return the byte read (excluding BOM) or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
private int readFirstBytes() throws IOException {
getBOM();
return fbIndex < fbLength ? firstBytes[fbIndex++] : EOF;
}
/**
* Find a BOM with the specified bytes.
*
* @return The matched BOM or null if none matched
*/
private ByteOrderMark find() {
for (final ByteOrderMark bom : boms) {
if (matches(bom)) {
return bom;
}
}
return null;
}
/**
* Check if the bytes match a BOM.
*
* @param bom The BOM
* @return true if the bytes match the bom, otherwise false
*/
private boolean matches(final ByteOrderMark bom) {
// if (bom.length() != fbLength) {
// return false;
// }
// firstBytes may be bigger than the BOM bytes
for (int i = 0; i < bom.length(); i++) {
if (bom.get(i) != firstBytes[i]) {
return false;
}
}
return true;
}
// ----------------------------------------------------------------------------
// Implementation of InputStream
// ----------------------------------------------------------------------------
/**
* Invokes the delegate's <code>read()</code> method, detecting and optionally skipping BOM.
*
* @return the byte read (excluding BOM) or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read() throws IOException {
final int b = readFirstBytes();
return b >= 0 ? b : in.read();
}
/**
* Invokes the delegate's <code>read(byte[], int, int)</code> method, detecting and optionally skipping BOM.
*
* @param buf the buffer to read the bytes into
* @param off The start offset
* @param len The number of bytes to read (excluding BOM)
* @return the number of bytes read or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read(final byte[] buf, int off, int len) throws IOException {
int firstCount = 0;
int b = 0;
while (len > 0 && b >= 0) {
b = readFirstBytes();
if (b >= 0) {
buf[off++] = (byte) (b & 0xFF);
len--;
firstCount++;
}
}
final int secondCount = in.read(buf, off, len);
return secondCount < 0 ? firstCount > 0 ? firstCount : EOF : firstCount + secondCount;
}
/**
* Invokes the delegate's <code>read(byte[])</code> method, detecting and optionally skipping BOM.
*
* @param buf the buffer to read the bytes into
* @return the number of bytes read (excluding BOM) or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read(final byte[] buf) throws IOException {
return read(buf, 0, buf.length);
}
/**
* Invokes the delegate's <code>mark(int)</code> method.
*
* @param readlimit read ahead limit
*/
@Override
public synchronized void mark(final int readlimit) {
markFbIndex = fbIndex;
markedAtStart = firstBytes == null;
in.mark(readlimit);
}
/**
* Invokes the delegate's <code>reset()</code> method.
*
* @throws IOException if an I/O error occurs
*/
@Override
public synchronized void reset() throws IOException {
fbIndex = markFbIndex;
if (markedAtStart) {
firstBytes = null;
}
in.reset();
}
/**
* Invokes the delegate's <code>skip(long)</code> method, detecting and optionally skipping BOM.
*
* @param n the number of bytes to skip
* @return the number of bytes to skipped or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public long skip(final long n) throws IOException {
int skipped = 0;
while ((n > skipped) && (readFirstBytes() >= 0)) {
skipped++;
}
return in.skip(n - skipped) + skipped;
}
}
@@ -0,0 +1,203 @@
package me.ag2s.epublib.util.commons.io;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.Serializable;
import java.util.Locale;
/**
* Byte Order Mark (BOM) representation - see {@link BOMInputStream}.
*
* @see BOMInputStream
* @see <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Wikipedia: Byte Order Mark</a>
* @see <a href="http://www.w3.org/TR/2006/REC-xml-20060816/#sec-guessing">W3C: Autodetection of Character Encodings
* (Non-Normative)</a>
* @since 2.0
*/
public class ByteOrderMark implements Serializable {
private static final long serialVersionUID = 1L;
/**
* UTF-8 BOM
*/
public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF);
/**
* UTF-16BE BOM (Big-Endian)
*/
public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF);
/**
* UTF-16LE BOM (Little-Endian)
*/
public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE);
/**
* UTF-32BE BOM (Big-Endian)
*
* @since 2.2
*/
public static final ByteOrderMark UTF_32BE = new ByteOrderMark("UTF-32BE", 0x00, 0x00, 0xFE, 0xFF);
/**
* UTF-32LE BOM (Little-Endian)
*
* @since 2.2
*/
public static final ByteOrderMark UTF_32LE = new ByteOrderMark("UTF-32LE", 0xFF, 0xFE, 0x00, 0x00);
/**
* Unicode BOM character; external form depends on the encoding.
*
* @see <a href="http://unicode.org/faq/utf_bom.html#BOM">Byte Order Mark (BOM) FAQ</a>
* @since 2.5
*/
@SuppressWarnings("unused")
public static final char UTF_BOM = '\uFEFF';
private final String charsetName;
private final int[] bytes;
/**
* Construct a new BOM.
*
* @param charsetName The name of the charset the BOM represents
* @param bytes The BOM's bytes
* @throws IllegalArgumentException if the charsetName is null or
* zero length
* @throws IllegalArgumentException if the bytes are null or zero
* length
*/
public ByteOrderMark(final String charsetName, final int... bytes) {
if (charsetName == null || charsetName.isEmpty()) {
throw new IllegalArgumentException("No charsetName specified");
}
if (bytes == null || bytes.length == 0) {
throw new IllegalArgumentException("No bytes specified");
}
this.charsetName = charsetName;
this.bytes = new int[bytes.length];
System.arraycopy(bytes, 0, this.bytes, 0, bytes.length);
}
/**
* Return the name of the {@link java.nio.charset.Charset} the BOM represents.
*
* @return the character set name
*/
public String getCharsetName() {
return charsetName;
}
/**
* Return the length of the BOM's bytes.
*
* @return the length of the BOM's bytes
*/
public int length() {
return bytes.length;
}
/**
* The byte at the specified position.
*
* @param pos The position
* @return The specified byte
*/
public int get(final int pos) {
return bytes[pos];
}
/**
* Return a copy of the BOM's bytes.
*
* @return a copy of the BOM's bytes
*/
public byte[] getBytes() {
final byte[] copy = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
copy[i] = (byte) bytes[i];
}
return copy;
}
/**
* Indicates if this BOM's bytes equals another.
*
* @param obj The object to compare to
* @return true if the bom's bytes are equal, otherwise
* false
*/
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof ByteOrderMark)) {
return false;
}
final ByteOrderMark bom = (ByteOrderMark) obj;
if (bytes.length != bom.length()) {
return false;
}
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] != bom.get(i)) {
return false;
}
}
return true;
}
/**
* Return the hashcode for this BOM.
*
* @return the hashcode for this BOM.
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int hashCode = getClass().hashCode();
for (final int b : bytes) {
hashCode += b;
}
return hashCode;
}
/**
* Provide a String representation of the BOM.
*
* @return the length of the BOM's bytes
*/
@Override
@SuppressWarnings("NullableProblems")
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append(getClass().getSimpleName());
builder.append('[');
builder.append(charsetName);
builder.append(": ");
for (int i = 0; i < bytes.length; i++) {
if (i > 0) {
builder.append(",");
}
builder.append("0x");
builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase(Locale.ROOT));
}
builder.append(']');
return builder.toString();
}
}
@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.ag2s.epublib.util.commons.io;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Like {@link Consumer} but throws {@link IOException}.
*
* @param <T> the type of the input to the operations.
* @since 2.7
*/
@FunctionalInterface
public interface IOConsumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
* @throws IOException if an I/O error occurs.
*/
void accept(T t) throws IOException;
/**
* Returns a composed {@code IoConsumer} that performs, in sequence, this operation followed by the {@code after}
* operation. If performing either operation throws an exception, it is relayed to the caller of the composed
* operation. If performing this operation throws an exception, the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after}
* operation
* @throws NullPointerException if {@code after} is null
*/
@SuppressWarnings("unused")
default IOConsumer<T> andThen(final IOConsumer<? super T> after) {
Objects.requireNonNull(after);
return (final T t) -> {
accept(t);
after.accept(t);
};
}
}
@@ -0,0 +1,252 @@
package me.ag2s.epublib.util.commons.io;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import static me.ag2s.epublib.util.IOUtil.EOF;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import me.ag2s.epublib.util.IOUtil;
/**
* A Proxy stream which acts as expected, that is it passes the method
* calls on to the proxied stream and doesn't change which methods are
* being called.
* <p>
* It is an alternative base class to FilterInputStream
* to increase reusability, because FilterInputStream changes the
* methods being called, such as read(byte[]) to read(byte[], int, int).
* </p>
* <p>
* See the protected methods for ways in which a subclass can easily decorate
* a stream with custom pre-, post- or error processing functionality.
* </p>
*/
public abstract class ProxyInputStream extends FilterInputStream {
/**
* Constructs a new ProxyInputStream.
*
* @param proxy the InputStream to delegate to
*/
public ProxyInputStream(final InputStream proxy) {
super(proxy);
// the proxy is stored in a protected superclass variable named 'in'
}
/**
* Invokes the delegate's <code>read()</code> method.
*
* @return the byte read or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read() throws IOException {
try {
beforeRead(1);
final int b = in.read();
afterRead(b != EOF ? 1 : EOF);
return b;
} catch (final IOException e) {
handleIOException(e);
return EOF;
}
}
/**
* Invokes the delegate's <code>read(byte[])</code> method.
*
* @param bts the buffer to read the bytes into
* @return the number of bytes read or EOF if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read(final byte[] bts) throws IOException {
try {
beforeRead(IOUtil.length(bts));
final int n = in.read(bts);
afterRead(n);
return n;
} catch (final IOException e) {
handleIOException(e);
return EOF;
}
}
/**
* Invokes the delegate's <code>read(byte[], int, int)</code> method.
*
* @param bts the buffer to read the bytes into
* @param off The start offset
* @param len The number of bytes to read
* @return the number of bytes read or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read(final byte[] bts, final int off, final int len) throws IOException {
try {
beforeRead(len);
final int n = in.read(bts, off, len);
afterRead(n);
return n;
} catch (final IOException e) {
handleIOException(e);
return EOF;
}
}
/**
* Invokes the delegate's <code>skip(long)</code> method.
*
* @param ln the number of bytes to skip
* @return the actual number of bytes skipped
* @throws IOException if an I/O error occurs
*/
@Override
public long skip(final long ln) throws IOException {
try {
return in.skip(ln);
} catch (final IOException e) {
handleIOException(e);
return 0;
}
}
/**
* Invokes the delegate's <code>available()</code> method.
*
* @return the number of available bytes
* @throws IOException if an I/O error occurs
*/
@Override
public int available() throws IOException {
try {
return super.available();
} catch (final IOException e) {
handleIOException(e);
return 0;
}
}
/**
* Invokes the delegate's <code>close()</code> method.
*
* @throws IOException if an I/O error occurs
*/
@Override
public void close() throws IOException {
IOUtil.close(in, this::handleIOException);
}
/**
* Invokes the delegate's <code>mark(int)</code> method.
*
* @param readlimit read ahead limit
*/
@Override
public synchronized void mark(final int readlimit) {
in.mark(readlimit);
}
/**
* Invokes the delegate's <code>reset()</code> method.
*
* @throws IOException if an I/O error occurs
*/
@Override
public synchronized void reset() throws IOException {
try {
in.reset();
} catch (final IOException e) {
handleIOException(e);
}
}
/**
* Invokes the delegate's <code>markSupported()</code> method.
*
* @return true if mark is supported, otherwise false
*/
@Override
public boolean markSupported() {
return in.markSupported();
}
/**
* Invoked by the read methods before the call is proxied. The number
* of bytes that the caller wanted to read (1 for the {@link #read()}
* method, buffer length for {@link #read(byte[])}, etc.) is given as
* an argument.
* <p>
* Subclasses can override this method to add common pre-processing
* functionality without having to override all the read methods.
* The default implementation does nothing.
* <p>
* Note this method is <em>not</em> called from {@link #skip(long)} or
* {@link #reset()}. You need to explicitly override those methods if
* you want to add pre-processing steps also to them.
*
* @param n number of bytes that the caller asked to be read
* @since 2.0
*/
@SuppressWarnings("unused")
protected void beforeRead(final int n) {
// no-op
}
/**
* Invoked by the read methods after the proxied call has returned
* successfully. The number of bytes returned to the caller (or -1 if
* the end of stream was reached) is given as an argument.
* <p>
* Subclasses can override this method to add common post-processing
* functionality without having to override all the read methods.
* The default implementation does nothing.
* <p>
* Note this method is <em>not</em> called from {@link #skip(long)} or
* {@link #reset()}. You need to explicitly override those methods if
* you want to add post-processing steps also to them.
*
* @param n number of bytes read, or -1 if the end of stream was reached
* @since 2.0
*/
@SuppressWarnings("unused")
protected void afterRead(final int n) {
// no-op
}
/**
* Handle any IOExceptions thrown.
* <p>
* This method provides a point to implement custom exception
* handling. The default behavior is to re-throw the exception.
*
* @param e The IOException thrown
* @throws IOException if an I/O error occurs
* @since 2.0
*/
protected void handleIOException(final IOException e) throws IOException {
throw e;
}
}
@@ -0,0 +1,819 @@
package me.ag2s.epublib.util.commons.io;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import me.ag2s.epublib.util.IOUtil;
/**
* Character stream that handles all the necessary Voodoo to figure out the
* charset encoding of the XML document within the stream.
* <p>
* IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader.
* This one IS a character stream.
* </p>
* <p>
* All this has to be done without consuming characters from the stream, if not
* the XML parser will not recognized the document as a valid XML. This is not
* 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers
* right now, XmlStreamReader handles it and things work in all parsers).
* </p>
* <p>
* The XmlStreamReader class handles the charset encoding of XML documents in
* Files, raw streams and HTTP streams by offering a wide set of constructors.
* </p>
* <p>
* By default the charset encoding detection is lenient, the constructor with
* the lenient flag can be used for a script (following HTTP MIME and XML
* specifications). All this is nicely explained by Mark Pilgrim in his blog, <a
* href="http://diveintomark.org/archives/2004/02/13/xml-media-types">
* Determining the character encoding of a feed</a>.
* </p>
* <p>
* Originally developed for <a href="http://rome.dev.java.net">ROME</a> under
* Apache License 2.0.
* </p>
* <p>
* //@seerr XmlStreamWriter
*
* @since 2.0
*/
public class XmlStreamReader extends Reader {
private static final int BUFFER_SIZE = IOUtil.DEFAULT_BUFFER_SIZE;
private static final String UTF_8 = "UTF-8";
private static final String US_ASCII = "US-ASCII";
private static final String UTF_16BE = "UTF-16BE";
private static final String UTF_16LE = "UTF-16LE";
private static final String UTF_32BE = "UTF-32BE";
private static final String UTF_32LE = "UTF-32LE";
private static final String UTF_16 = "UTF-16";
private static final String UTF_32 = "UTF-32";
private static final String EBCDIC = "CP1047";
private static final ByteOrderMark[] BOMS = new ByteOrderMark[]{
ByteOrderMark.UTF_8,
ByteOrderMark.UTF_16BE,
ByteOrderMark.UTF_16LE,
ByteOrderMark.UTF_32BE,
ByteOrderMark.UTF_32LE
};
// UTF_16LE and UTF_32LE have the same two starting BOM bytes.
private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[]{
new ByteOrderMark(UTF_8, 0x3C, 0x3F, 0x78, 0x6D),
new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F),
new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00),
new ByteOrderMark(UTF_32BE, 0x00, 0x00, 0x00, 0x3C,
0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D),
new ByteOrderMark(UTF_32LE, 0x3C, 0x00, 0x00, 0x00,
0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00),
new ByteOrderMark(EBCDIC, 0x4C, 0x6F, 0xA7, 0x94)
};
private final Reader reader;
private final String encoding;
private final String defaultEncoding;
/**
* Returns the default encoding to use if none is set in HTTP content-type,
* XML prolog and the rules based on content-type are not adequate.
* <p>
* If it is NULL the content-type based rules are used.
*
* @return the default encoding to use.
*/
public String getDefaultEncoding() {
return defaultEncoding;
}
/**
* Creates a Reader for a File.
* <p>
* It looks for the UTF-8 BOM first, if none sniffs the XML prolog charset,
* if this is also missing defaults to UTF-8.
* <p>
* It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details.
*
* @param file File to create a Reader from.
* @throws IOException thrown if there is a problem reading the file.
*/
@SuppressWarnings("unused")
public XmlStreamReader(final File file) throws IOException {
this(new FileInputStream(Objects.requireNonNull(file)));
}
/**
* Creates a Reader for a raw InputStream.
* <p>
* It follows the same logic used for files.
* <p>
* It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details.
*
* @param inputStream InputStream to create a Reader from.
* @throws IOException thrown if there is a problem reading the stream.
*/
public XmlStreamReader(final InputStream inputStream) throws IOException {
this(inputStream, true);
}
/**
* Creates a Reader for a raw InputStream.
* <p>
* It follows the same logic used for files.
* <p>
* If lenient detection is indicated and the detection above fails as per
* specifications it then attempts the following:
* <p>
* If the content type was 'text/html' it replaces it with 'text/xml' and
* tries the detection again.
* <p>
* Else if the XML prolog had a charset encoding that encoding is used.
* <p>
* Else if the content type had a charset encoding that encoding is used.
* <p>
* Else 'UTF-8' is used.
* <p>
* If lenient detection is indicated an XmlStreamReaderException is never
* thrown.
*
* @param inputStream InputStream to create a Reader from.
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @throws IOException thrown if there is a problem reading the stream.
* @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs.
*/
public XmlStreamReader(final InputStream inputStream, final boolean lenient) throws IOException {
this(inputStream, lenient, null);
}
/**
* Creates a Reader for a raw InputStream.
* <p>
* It follows the same logic used for files.
* <p>
* If lenient detection is indicated and the detection above fails as per
* specifications it then attempts the following:
* <p>
* If the content type was 'text/html' it replaces it with 'text/xml' and
* tries the detection again.
* <p>
* Else if the XML prolog had a charset encoding that encoding is used.
* <p>
* Else if the content type had a charset encoding that encoding is used.
* <p>
* Else 'UTF-8' is used.
* <p>
* If lenient detection is indicated an XmlStreamReaderException is never
* thrown.
*
* @param inputStream InputStream to create a Reader from.
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @param defaultEncoding The default encoding
* @throws IOException thrown if there is a problem reading the stream.
* @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs.
*/
public XmlStreamReader(final InputStream inputStream, final boolean lenient, final String defaultEncoding)
throws IOException {
Objects.requireNonNull(inputStream, "inputStream");
this.defaultEncoding = defaultEncoding;
final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);
final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
this.encoding = doRawStream(bom, pis, lenient);
this.reader = new InputStreamReader(pis, encoding);
}
/**
* Creates a Reader using the InputStream of a URL.
* <p>
* If the URL is not of type HTTP and there is not 'content-type' header in
* the fetched data it uses the same logic used for Files.
* <p>
* If the URL is a HTTP Url or there is a 'content-type' header in the
* fetched data it uses the same logic used for an InputStream with
* content-type.
* <p>
* It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details.
*
* @param url URL to create a Reader from.
* @throws IOException thrown if there is a problem reading the stream of
* the URL.
*/
@SuppressWarnings("unused")
public XmlStreamReader(final URL url) throws IOException {
this(Objects.requireNonNull(url, "url").openConnection(), null);
}
/**
* Creates a Reader using the InputStream of a URLConnection.
* <p>
* If the URLConnection is not of type HttpURLConnection and there is not
* 'content-type' header in the fetched data it uses the same logic used for
* files.
* <p>
* If the URLConnection is a HTTP Url or there is a 'content-type' header in
* the fetched data it uses the same logic used for an InputStream with
* content-type.
* <p>
* It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details.
*
* @param conn URLConnection to create a Reader from.
* @param defaultEncoding The default encoding
* @throws IOException thrown if there is a problem reading the stream of
* the URLConnection.
*/
public XmlStreamReader(final URLConnection conn, final String defaultEncoding) throws IOException {
Objects.requireNonNull(conn, "conm");
this.defaultEncoding = defaultEncoding;
final boolean lenient = true;
final String contentType = conn.getContentType();
final InputStream inputStream = conn.getInputStream();
final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);
final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
if (conn instanceof HttpURLConnection || contentType != null) {
this.encoding = processHttpStream(bom, pis, contentType, lenient);
} else {
this.encoding = doRawStream(bom, pis, lenient);
}
this.reader = new InputStreamReader(pis, encoding);
}
/**
* Creates a Reader using an InputStream and the associated content-type
* header.
* <p>
* First it checks if the stream has BOM. If there is not BOM checks the
* content-type encoding. If there is not content-type encoding checks the
* XML prolog encoding. If there is not XML prolog encoding uses the default
* encoding mandated by the content-type MIME type.
* <p>
* It does a lenient charset encoding detection, check the constructor with
* the lenient parameter for details.
*
* @param inputStream InputStream to create the reader from.
* @param httpContentType content-type header to use for the resolution of
* the charset encoding.
* @throws IOException thrown if there is a problem reading the file.
*/
public XmlStreamReader(final InputStream inputStream, final String httpContentType)
throws IOException {
this(inputStream, httpContentType, true);
}
/**
* Creates a Reader using an InputStream and the associated content-type
* header. This constructor is lenient regarding the encoding detection.
* <p>
* First it checks if the stream has BOM. If there is not BOM checks the
* content-type encoding. If there is not content-type encoding checks the
* XML prolog encoding. If there is not XML prolog encoding uses the default
* encoding mandated by the content-type MIME type.
* <p>
* If lenient detection is indicated and the detection above fails as per
* specifications it then attempts the following:
* <p>
* If the content type was 'text/html' it replaces it with 'text/xml' and
* tries the detection again.
* <p>
* Else if the XML prolog had a charset encoding that encoding is used.
* <p>
* Else if the content type had a charset encoding that encoding is used.
* <p>
* Else 'UTF-8' is used.
* <p>
* If lenient detection is indicated an XmlStreamReaderException is never
* thrown.
*
* @param inputStream InputStream to create the reader from.
* @param httpContentType content-type header to use for the resolution of
* the charset encoding.
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @param defaultEncoding The default encoding
* @throws IOException thrown if there is a problem reading the file.
* @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs.
*/
public XmlStreamReader(final InputStream inputStream, final String httpContentType,
final boolean lenient, final String defaultEncoding) throws IOException {
Objects.requireNonNull(inputStream, "inputStream");
this.defaultEncoding = defaultEncoding;
final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);
final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
this.encoding = processHttpStream(bom, pis, httpContentType, lenient);
this.reader = new InputStreamReader(pis, encoding);
}
/**
* Creates a Reader using an InputStream and the associated content-type
* header. This constructor is lenient regarding the encoding detection.
* <p>
* First it checks if the stream has BOM. If there is not BOM checks the
* content-type encoding. If there is not content-type encoding checks the
* XML prolog encoding. If there is not XML prolog encoding uses the default
* encoding mandated by the content-type MIME type.
* <p>
* If lenient detection is indicated and the detection above fails as per
* specifications it then attempts the following:
* <p>
* If the content type was 'text/html' it replaces it with 'text/xml' and
* tries the detection again.
* <p>
* Else if the XML prolog had a charset encoding that encoding is used.
* <p>
* Else if the content type had a charset encoding that encoding is used.
* <p>
* Else 'UTF-8' is used.
* <p>
* If lenient detection is indicated an XmlStreamReaderException is never
* thrown.
*
* @param inputStream InputStream to create the reader from.
* @param httpContentType content-type header to use for the resolution of
* the charset encoding.
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @throws IOException thrown if there is a problem reading the file.
* @throws XmlStreamReaderException thrown if the charset encoding could not
* be determined according to the specs.
*/
public XmlStreamReader(final InputStream inputStream, final String httpContentType,
final boolean lenient) throws IOException {
this(inputStream, httpContentType, lenient, null);
}
/**
* Returns the charset encoding of the XmlStreamReader.
*
* @return charset encoding.
*/
public String getEncoding() {
return encoding;
}
/**
* Invokes the underlying reader's <code>read(char[], int, int)</code> method.
*
* @param buf the buffer to read the characters into
* @param offset The start offset
* @param len The number of bytes to read
* @return the number of characters read or -1 if the end of stream
* @throws IOException if an I/O error occurs
*/
@Override
public int read(final char[] buf, final int offset, final int len) throws IOException {
return reader.read(buf, offset, len);
}
/**
* Closes the XmlStreamReader stream.
*
* @throws IOException thrown if there was a problem closing the stream.
*/
@Override
public void close() throws IOException {
reader.close();
}
/**
* Process the raw stream.
*
* @param bom BOMInputStream to detect byte order marks
* @param pis BOMInputStream to guess XML encoding
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @return the encoding to be used
* @throws IOException thrown if there is a problem reading the stream.
*/
private String doRawStream(final BOMInputStream bom, final BOMInputStream pis, final boolean lenient)
throws IOException {
final String bomEnc = bom.getBOMCharsetName();
final String xmlGuessEnc = pis.getBOMCharsetName();
final String xmlEnc = getXmlProlog(pis, xmlGuessEnc);
try {
return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);
} catch (final XmlStreamReaderException ex) {
if (lenient) {
return doLenientDetection(null, ex);
}
throw ex;
}
}
/**
* Process a HTTP stream.
*
* @param bom BOMInputStream to detect byte order marks
* @param pis BOMInputStream to guess XML encoding
* @param httpContentType The HTTP content type
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @return the encoding to be used
* @throws IOException thrown if there is a problem reading the stream.
*/
private String processHttpStream(final BOMInputStream bom, final BOMInputStream pis, final String httpContentType,
final boolean lenient) throws IOException {
final String bomEnc = bom.getBOMCharsetName();
final String xmlGuessEnc = pis.getBOMCharsetName();
final String xmlEnc = getXmlProlog(pis, xmlGuessEnc);
try {
return calculateHttpEncoding(httpContentType, bomEnc, xmlGuessEnc, xmlEnc, lenient);
} catch (final XmlStreamReaderException ex) {
if (lenient) {
return doLenientDetection(httpContentType, ex);
}
throw ex;
}
}
/**
* Do lenient detection.
*
* @param httpContentType content-type header to use for the resolution of
* the charset encoding.
* @param ex The thrown exception
* @return the encoding
* @throws IOException thrown if there is a problem reading the stream.
*/
private String doLenientDetection(String httpContentType,
XmlStreamReaderException ex) throws IOException {
if (httpContentType != null && httpContentType.startsWith("text/html")) {
httpContentType = httpContentType.substring("text/html".length());
httpContentType = "text/xml" + httpContentType;
try {
return calculateHttpEncoding(httpContentType, ex.getBomEncoding(),
ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true);
} catch (final XmlStreamReaderException ex2) {
ex = ex2;
}
}
String encoding = ex.getXmlEncoding();
if (encoding == null) {
encoding = ex.getContentTypeEncoding();
}
if (encoding == null) {
encoding = defaultEncoding == null ? UTF_8 : defaultEncoding;
}
return encoding;
}
/**
* Calculate the raw encoding.
*
* @param bomEnc BOM encoding
* @param xmlGuessEnc XML Guess encoding
* @param xmlEnc XML encoding
* @return the raw encoding
* @throws IOException thrown if there is a problem reading the stream.
*/
String calculateRawEncoding(final String bomEnc, final String xmlGuessEnc,
final String xmlEnc) throws IOException {
// BOM is Null
if (bomEnc == null) {
if (xmlGuessEnc == null || xmlEnc == null) {
return defaultEncoding == null ? UTF_8 : defaultEncoding;
}
if (xmlEnc.equals(UTF_16) &&
(xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) {
return xmlGuessEnc;
}
return xmlEnc;
}
// BOM is UTF-8
if (bomEnc.equals(UTF_8)) {
if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
if (xmlEnc != null && !xmlEnc.equals(UTF_8)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
return bomEnc;
}
// BOM is UTF-16BE or UTF-16LE
if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) {
if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
return bomEnc;
}
// BOM is UTF-32BE or UTF-32LE
if (bomEnc.equals(UTF_32BE) || bomEnc.equals(UTF_32LE)) {
if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
if (xmlEnc != null && !xmlEnc.equals(UTF_32) && !xmlEnc.equals(bomEnc)) {
final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
return bomEnc;
}
// BOM is something else
final String msg = MessageFormat.format(RAW_EX_2, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);
}
/**
* Calculate the HTTP encoding.
*
* @param httpContentType The HTTP content type
* @param bomEnc BOM encoding
* @param xmlGuessEnc XML Guess encoding
* @param xmlEnc XML encoding
* @param lenient indicates if the charset encoding detection should be
* relaxed.
* @return the HTTP encoding
* @throws IOException thrown if there is a problem reading the stream.
*/
String calculateHttpEncoding(final String httpContentType,
final String bomEnc, final String xmlGuessEnc, final String xmlEnc,
final boolean lenient) throws IOException {
// Lenient and has XML encoding
if (lenient && xmlEnc != null) {
return xmlEnc;
}
// Determine mime/encoding content types from HTTP Content Type
final String cTMime = getContentTypeMime(httpContentType);
final String cTEnc = getContentTypeEncoding(httpContentType);
final boolean appXml = isAppXml(cTMime);
final boolean textXml = isTextXml(cTMime);
// Mime type NOT "application/xml" or "text/xml"
if (!appXml && !textXml) {
final String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
// No content type encoding
if (cTEnc == null) {
if (appXml) {
return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);
}
return defaultEncoding == null ? US_ASCII : defaultEncoding;
}
// UTF-16BE or UTF-16LE content type encoding
if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) {
if (bomEnc != null) {
final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
return cTEnc;
}
// UTF-16 content type encoding
if (cTEnc.equals(UTF_16)) {
if (bomEnc != null && bomEnc.startsWith(UTF_16)) {
return bomEnc;
}
final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
// UTF-32BE or UTF-132E content type encoding
if (cTEnc.equals(UTF_32BE) || cTEnc.equals(UTF_32LE)) {
if (bomEnc != null) {
final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
return cTEnc;
}
// UTF-32 content type encoding
if (cTEnc.equals(UTF_32)) {
if (bomEnc != null && bomEnc.startsWith(UTF_32)) {
return bomEnc;
}
final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);
}
return cTEnc;
}
/**
* Returns MIME type or NULL if httpContentType is NULL.
*
* @param httpContentType the HTTP content type
* @return The mime content type
*/
static String getContentTypeMime(final String httpContentType) {
String mime = null;
if (httpContentType != null) {
final int i = httpContentType.indexOf(";");
if (i >= 0) {
mime = httpContentType.substring(0, i);
} else {
mime = httpContentType;
}
mime = mime.trim();
}
return mime;
}
private static final Pattern CHARSET_PATTERN = Pattern
.compile("charset=[\"']?([.[^; \"']]*)[\"']?");
/**
* Returns charset parameter value, NULL if not present, NULL if
* httpContentType is NULL.
*
* @param httpContentType the HTTP content type
* @return The content type encoding (upcased)
*/
static String getContentTypeEncoding(final String httpContentType) {
String encoding = null;
if (httpContentType != null) {
final int i = httpContentType.indexOf(";");
if (i > -1) {
final String postMime = httpContentType.substring(i + 1);
final Matcher m = CHARSET_PATTERN.matcher(postMime);
encoding = m.find() ? m.group(1) : null;
encoding = encoding != null ? encoding.toUpperCase(Locale.ROOT) : null;
}
}
return encoding;
}
/**
* Pattern capturing the encoding of the "xml" processing instruction.
*/
public static final Pattern ENCODING_PATTERN = Pattern.compile(
"<\\?xml.*encoding[\\s]*=[\\s]*((?:\".[^\"]*\")|(?:'.[^']*'))",
Pattern.MULTILINE);
/**
* Returns the encoding declared in the <?xml encoding=...?>, NULL if none.
*
* @param inputStream InputStream to create the reader from.
* @param guessedEnc guessed encoding
* @return the encoding declared in the <?xml encoding=...?>
* @throws IOException thrown if there is a problem reading the stream.
*/
private static String getXmlProlog(final InputStream inputStream, final String guessedEnc)
throws IOException {
String encoding = null;
if (guessedEnc != null) {
final byte[] bytes = new byte[BUFFER_SIZE];
inputStream.mark(BUFFER_SIZE);
int offset = 0;
int max = BUFFER_SIZE;
int c = inputStream.read(bytes, offset, max);
int firstGT = -1;
String xmlProlog = ""; // avoid possible NPE warning (cannot happen; this just silences the warning)
while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) {
offset += c;
max -= c;
c = inputStream.read(bytes, offset, max);
xmlProlog = new String(bytes, 0, offset, guessedEnc);
firstGT = xmlProlog.indexOf('>');
}
if (firstGT == -1) {
if (c == -1) {
throw new IOException("Unexpected end of XML stream");
}
throw new IOException(
"XML prolog or ROOT element not found on first "
+ offset + " bytes");
}
final int bytesRead = offset;
if (bytesRead > 0) {
inputStream.reset();
final BufferedReader bReader = new BufferedReader(new StringReader(
xmlProlog.substring(0, firstGT + 1)));
final StringBuffer prolog = new StringBuffer();
String line;
while ((line = bReader.readLine()) != null) {
prolog.append(line);
}
final Matcher m = ENCODING_PATTERN.matcher(prolog);
if (m.find()) {
encoding = Objects.requireNonNull(m.group(1)).toUpperCase(Locale.ROOT);
encoding = encoding.substring(1, encoding.length() - 1);
}
}
}
boolean isSupportedEncoding;
try {
isSupportedEncoding = Charset.isSupported(encoding);
} catch (Exception e) {
return null;
}
if (isSupportedEncoding) {
return encoding;
} else {
return null;
}
}
/**
* Indicates if the MIME type belongs to the APPLICATION XML family.
*
* @param mime The mime type
* @return true if the mime type belongs to the APPLICATION XML family,
* otherwise false
*/
static boolean isAppXml(final String mime) {
return mime != null &&
(mime.equals("application/xml") ||
mime.equals("application/xml-dtd") ||
mime.equals("application/xml-external-parsed-entity") ||
mime.startsWith("application/") && mime.endsWith("+xml"));
}
/**
* Indicates if the MIME type belongs to the TEXT XML family.
*
* @param mime The mime type
* @return true if the mime type belongs to the TEXT XML family,
* otherwise false
*/
static boolean isTextXml(final String mime) {
return mime != null &&
(mime.equals("text/xml") ||
mime.equals("text/xml-external-parsed-entity") ||
mime.startsWith("text/") && mime.endsWith("+xml"));
}
private static final String RAW_EX_1 =
"Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch";
private static final String RAW_EX_2 =
"Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM";
private static final String HTTP_EX_1 =
"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL";
private static final String HTTP_EX_2 =
"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch";
private static final String HTTP_EX_3 =
"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME";
}
@@ -0,0 +1,139 @@
package me.ag2s.epublib.util.commons.io;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.IOException;
/**
* The XmlStreamReaderException is thrown by the XmlStreamReader constructors if
* the charset encoding can not be determined according to the XML 1.0
* specification and RFC 3023.
* <p>
* The exception returns the unconsumed InputStream to allow the application to
* do an alternate processing with the stream. Note that the original
* InputStream given to the XmlStreamReader cannot be used as that one has been
* already read.
* </p>
*
* @since 2.0
*/
public class XmlStreamReaderException extends IOException {
private static final long serialVersionUID = 1L;
private final String bomEncoding;
private final String xmlGuessEncoding;
private final String xmlEncoding;
private final String contentTypeMime;
private final String contentTypeEncoding;
/**
* Creates an exception instance if the charset encoding could not be
* determined.
* <p>
* Instances of this exception are thrown by the XmlStreamReader.
* </p>
*
* @param msg message describing the reason for the exception.
* @param bomEnc BOM encoding.
* @param xmlGuessEnc XML guess encoding.
* @param xmlEnc XML prolog encoding.
*/
public XmlStreamReaderException(final String msg, final String bomEnc,
final String xmlGuessEnc, final String xmlEnc) {
this(msg, null, null, bomEnc, xmlGuessEnc, xmlEnc);
}
/**
* Creates an exception instance if the charset encoding could not be
* determined.
* <p>
* Instances of this exception are thrown by the XmlStreamReader.
* </p>
*
* @param msg message describing the reason for the exception.
* @param ctMime MIME type in the content-type.
* @param ctEnc encoding in the content-type.
* @param bomEnc BOM encoding.
* @param xmlGuessEnc XML guess encoding.
* @param xmlEnc XML prolog encoding.
*/
public XmlStreamReaderException(final String msg, final String ctMime, final String ctEnc,
final String bomEnc, final String xmlGuessEnc, final String xmlEnc) {
super(msg);
contentTypeMime = ctMime;
contentTypeEncoding = ctEnc;
bomEncoding = bomEnc;
xmlGuessEncoding = xmlGuessEnc;
xmlEncoding = xmlEnc;
}
/**
* Returns the BOM encoding found in the InputStream.
*
* @return the BOM encoding, null if none.
*/
public String getBomEncoding() {
return bomEncoding;
}
/**
* Returns the encoding guess based on the first bytes of the InputStream.
*
* @return the encoding guess, null if it couldn't be guessed.
*/
public String getXmlGuessEncoding() {
return xmlGuessEncoding;
}
/**
* Returns the encoding found in the XML prolog of the InputStream.
*
* @return the encoding of the XML prolog, null if none.
*/
public String getXmlEncoding() {
return xmlEncoding;
}
/**
* Returns the MIME type in the content-type used to attempt determining the
* encoding.
*
* @return the MIME type in the content-type, null if there was not
* content-type or the encoding detection did not involve HTTP.
*/
public String getContentTypeMime() {
return contentTypeMime;
}
/**
* Returns the encoding in the content-type used to attempt determining the
* encoding.
*
* @return the encoding in the content-type, null if there was not
* content-type, no encoding in it or the encoding detection did not
* involve HTTP.
*/
public String getContentTypeEncoding() {
return contentTypeEncoding;
}
}
@@ -0,0 +1,363 @@
package me.ag2s.epublib.util.zip;
import java.util.Calendar;
import java.util.Date;
import java.util.zip.ZipOutputStream;
/**
* This class represents a member of a zip archive. ZipFile and
* ZipInputStream will give you instances of this class as information
* about the members in an archive. On the other hand ZipOutputStream
* needs an instance of this class to create a new member.
*
* @author Jochen Hoenicke
*/
public class AndroidZipEntry implements ZipConstants, Cloneable {
private static final int KNOWN_SIZE = 1;
private static final int KNOWN_CSIZE = 2;
private static final int KNOWN_CRC = 4;
private static final int KNOWN_TIME = 8;
private static Calendar cal;
private final String name;
private final int nameLen;
private int size;
private int compressedSize;
private int crc;
private int dostime;
private short known = 0;
private short method = -1;
private byte[] extra = null;
private String comment = null;
int flags; /* used by ZipOutputStream */
int offset; /* used by ZipFile and ZipOutputStream */
/**
* Compression method. This method doesn't compress at all.
*/
public final static int STORED = 0;
/**
* Compression method. This method uses the Deflater.
*/
public final static int DEFLATED = 8;
/**
* Creates a zip entry with the given name.
*
* @param name the name. May include directory components separated
* by '/'.
* @throws NullPointerException when name is null.
* @throws IllegalArgumentException when name is bigger then 65535 chars.
*/
public AndroidZipEntry(String name, int nameLen) {
//int length = name.length();
this.nameLen = nameLen;
if (nameLen > 65535)
throw new IllegalArgumentException("name length is " + nameLen);
this.name = name;
}
/**
* Creates a copy of the given zip entry.
*
* @param e the entry to copy.
*/
public AndroidZipEntry(AndroidZipEntry e) {
name = e.name;
nameLen = e.nameLen;
known = e.known;
size = e.size;
compressedSize = e.compressedSize;
crc = e.crc;
dostime = e.dostime;
method = e.method;
extra = e.extra;
comment = e.comment;
}
final void setDOSTime(int dostime) {
this.dostime = dostime;
known |= KNOWN_TIME;
}
final int getDOSTime() {
if ((known & KNOWN_TIME) == 0)
return 0;
else
return dostime;
}
/**
* Creates a copy of this zip entry.
*/
/**
* Clones the entry.
*/
public Object clone() {
try {
// The JCL says that the `extra' field is also copied.
AndroidZipEntry clone = (AndroidZipEntry) super.clone();
if (extra != null)
clone.extra = extra.clone();
return clone;
} catch (CloneNotSupportedException ex) {
throw new InternalError();
}
}
/**
* Returns the entry name. The path components in the entry are
* always separated by slashes ('/').
*/
public String getName() {
return name;
}
public int getNameLen() {
return nameLen;
}
/**
* Sets the time of last modification of the entry.
*
* @time the time of last modification of the entry.
*/
public void setTime(long time) {
Calendar cal = getCalendar();
synchronized (cal) {
cal.setTime(new Date(time * 1000L));
dostime = (cal.get(Calendar.YEAR) - 1980 & 0x7f) << 25
| (cal.get(Calendar.MONTH) + 1) << 21
| (cal.get(Calendar.DAY_OF_MONTH)) << 16
| (cal.get(Calendar.HOUR_OF_DAY)) << 11
| (cal.get(Calendar.MINUTE)) << 5
| (cal.get(Calendar.SECOND)) >> 1;
}
dostime = (int) (dostime / 1000L);
this.known |= KNOWN_TIME;
}
/**
* Gets the time of last modification of the entry.
*
* @return the time of last modification of the entry, or -1 if unknown.
*/
public long getTime() {
if ((known & KNOWN_TIME) == 0)
return -1;
int sec = 2 * (dostime & 0x1f);
int min = (dostime >> 5) & 0x3f;
int hrs = (dostime >> 11) & 0x1f;
int day = (dostime >> 16) & 0x1f;
int mon = ((dostime >> 21) & 0xf) - 1;
int year = ((dostime >> 25) & 0x7f) + 1980; /* since 1900 */
try {
cal = getCalendar();
synchronized (cal) {
cal.set(year, mon, day, hrs, min, sec);
return cal.getTime().getTime();
}
} catch (RuntimeException ex) {
/* Ignore illegal time stamp */
known &= ~KNOWN_TIME;
return -1;
}
}
private static synchronized Calendar getCalendar() {
if (cal == null)
cal = Calendar.getInstance();
return cal;
}
/**
* Sets the size of the uncompressed data.
*
* @throws IllegalArgumentException if size is not in 0..0xffffffffL
*/
public void setSize(long size) {
if ((size & 0xffffffff00000000L) != 0)
throw new IllegalArgumentException();
this.size = (int) size;
this.known |= KNOWN_SIZE;
}
/**
* Gets the size of the uncompressed data.
*
* @return the size or -1 if unknown.
*/
public long getSize() {
return (known & KNOWN_SIZE) != 0 ? size & 0xffffffffL : -1L;
}
/**
* Sets the size of the compressed data.
*
* @throws IllegalArgumentException if size is not in 0..0xffffffffL
*/
public void setCompressedSize(long csize) {
if ((csize & 0xffffffff00000000L) != 0)
throw new IllegalArgumentException();
this.compressedSize = (int) csize;
this.known |= KNOWN_CSIZE;
}
/**
* Gets the size of the compressed data.
*
* @return the size or -1 if unknown.
*/
public long getCompressedSize() {
return (known & KNOWN_CSIZE) != 0 ? compressedSize & 0xffffffffL : -1L;
}
/**
* Sets the crc of the uncompressed data.
*
* @throws IllegalArgumentException if crc is not in 0..0xffffffffL
*/
public void setCrc(long crc) {
if ((crc & 0xffffffff00000000L) != 0)
throw new IllegalArgumentException();
this.crc = (int) crc;
this.known |= KNOWN_CRC;
}
/**
* Gets the crc of the uncompressed data.
*
* @return the crc or -1 if unknown.
*/
public long getCrc() {
return (known & KNOWN_CRC) != 0 ? crc & 0xffffffffL : -1L;
}
/**
* Sets the compression method. Only DEFLATED and STORED are
* supported.
*
* @throws IllegalArgumentException if method is not supported.
* @see ZipOutputStream#DEFLATED
* @see ZipOutputStream#STORED
*/
public void setMethod(int method) {
if (method != ZipOutputStream.STORED
&& method != ZipOutputStream.DEFLATED)
throw new IllegalArgumentException();
this.method = (short) method;
}
/**
* Gets the compression method.
*
* @return the compression method or -1 if unknown.
*/
public int getMethod() {
return method;
}
/**
* Sets the extra data.
*
* @throws IllegalArgumentException if extra is longer than 0xffff bytes.
*/
public void setExtra(byte[] extra) {
if (extra == null) {
this.extra = null;
return;
}
if (extra.length > 0xffff)
throw new IllegalArgumentException();
this.extra = extra;
try {
int pos = 0;
while (pos < extra.length) {
int sig = (extra[pos++] & 0xff)
| (extra[pos++] & 0xff) << 8;
int len = (extra[pos++] & 0xff)
| (extra[pos++] & 0xff) << 8;
if (sig == 0x5455) {
/* extended time stamp */
int flags = extra[pos];
if ((flags & 1) != 0) {
long time = ((extra[pos + 1] & 0xff)
| (extra[pos + 2] & 0xff) << 8
| (extra[pos + 3] & 0xff) << 16
| (extra[pos + 4] & 0xff) << 24);
setTime(time);
}
}
pos += len;
}
} catch (ArrayIndexOutOfBoundsException ex) {
/* be lenient */
return;
}
}
/**
* Gets the extra data.
*
* @return the extra data or null if not set.
*/
public byte[] getExtra() {
return extra;
}
/**
* Sets the entry comment.
*
* @throws IllegalArgumentException if comment is longer than 0xffff.
*/
public void setComment(String comment) {
if (comment != null && comment.length() > 0xffff)
throw new IllegalArgumentException();
this.comment = comment;
}
/**
* Gets the comment.
*
* @return the comment or null if not set.
*/
public String getComment() {
return comment;
}
/**
* Gets true, if the entry is a directory. This is solely
* determined by the name, a trailing slash '/' marks a directory.
*/
public boolean isDirectory() {
int nlen = name.length();
return nlen > 0 && name.charAt(nlen - 1) == '/';
}
/**
* Gets the string representation of this AndroidZipEntry. This is just
* the name as returned by getName().
*/
public String toString() {
return name;
}
/**
* Gets the hashCode of this AndroidZipEntry. This is just the hashCode
* of the name. Note that the equals method isn't changed, though.
*/
public int hashCode() {
return name.hashCode();
}
}
@@ -0,0 +1,490 @@
package me.ag2s.epublib.util.zip;
import static me.ag2s.base.PfdHelper.seek;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import java.io.BufferedInputStream;
import java.io.DataInput;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipOutputStream;
import me.ag2s.base.PfdHelper;
/**
* This class represents a Zip archive. You can ask for the contained
* entries, or get an input stream for a file entry. The entry is
* automatically decompressed.
* <p>
* This class is thread safe: You can open input streams for arbitrary
* entries in different threads.
*
* @author Jochen Hoenicke
* @author Artur Biesiadowski
*/
public class AndroidZipFile implements ZipConstants {
/**
* Mode flag to open a zip file for reading.
*/
public static final int OPEN_READ = 0x1;
/**
* Mode flag to delete a zip file after reading.
*/
public static final int OPEN_DELETE = 0x4;
// Name of this zip file.
private final String name;
// File from which zip entries are read.
//private final RandomAccessFile raf;
private final ParcelFileDescriptor pfd;
// The entries of this zip file when initialized and not yet closed.
private HashMap<String, AndroidZipEntry> entries;
private boolean closed = false;
/**
* Opens a Zip file with the given name for reading.
*
* @throws IOException if a i/o error occured.
* @throws ZipException if the file doesn't contain a valid zip
* archive.
*/
public AndroidZipFile(@NonNull ParcelFileDescriptor pfd, String name) throws ZipException, IOException {
this.pfd = pfd;
this.name = name;
}
/**
* Opens a Zip file reading the given File.
*
* @throws IOException if a i/o error occured.
* @throws ZipException if the file doesn't contain a valid zip
* archive.
*/
public AndroidZipFile(File file) throws ZipException, IOException {
this.pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
this.name = file.getPath();
}
/**
* Opens a Zip file reading the given File in the given mode.
* <p>
* If the OPEN_DELETE mode is specified, the zip file will be deleted at
* some time moment after it is opened. It will be deleted before the zip
* file is closed or the Virtual Machine exits.
* <p>
* The contents of the zip file will be accessible until it is closed.
* <p>
* The OPEN_DELETE mode is currently unimplemented in this library
*
* @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
* @throws IOException if a i/o error occured.
* @throws ZipException if the file doesn't contain a valid zip
* archive.
* @since JDK1.3
*/
// public AndroidZipFile(File file, int mode) throws ZipException, IOException {
// if ((mode & OPEN_DELETE) != 0) {
// throw new IllegalArgumentException
// ("OPEN_DELETE mode not supported yet in net.sf.jazzlib.AndroidZipFile");
// }
// this.raf = new RandomAccessFile(file, "r");
// this.name = file.getPath();
// }
/**
* Read an unsigned short in little endian byte order from the given
* DataInput stream using the given byte buffer.
*
* @param di DataInput stream to read from.
* @param b the byte buffer to read in (must be at least 2 bytes long).
* @return The value read.
* @throws IOException if a i/o error occured.
* @throws EOFException if the file ends prematurely
*/
private final int readLeShort(DataInput di, byte[] b) throws IOException {
di.readFully(b, 0, 2);
return (b[0] & 0xff) | (b[1] & 0xff) << 8;
}
private final int readLeShort(ParcelFileDescriptor pfd, byte[] b) throws IOException {
PfdHelper.readFully(pfd, b, 0, 2);//di.readFully(b, 0, 2);
return (b[0] & 0xff) | (b[1] & 0xff) << 8;
}
/**
* Read an int in little endian byte order from the given
* DataInput stream using the given byte buffer.
*
* @param di DataInput stream to read from.
* @param b the byte buffer to read in (must be at least 4 bytes long).
* @return The value read.
* @throws IOException if a i/o error occured.
* @throws EOFException if the file ends prematurely
*/
private final int readLeInt(DataInput di, byte[] b) throws IOException {
di.readFully(b, 0, 4);
return ((b[0] & 0xff) | (b[1] & 0xff) << 8)
| ((b[2] & 0xff) | (b[3] & 0xff) << 8) << 16;
}
private final int readLeInt(ParcelFileDescriptor pfd, byte[] b) throws IOException {
PfdHelper.readFully(pfd, b, 0, 4);//di.readFully(b, 0, 4);
return ((b[0] & 0xff) | (b[1] & 0xff) << 8)
| ((b[2] & 0xff) | (b[3] & 0xff) << 8) << 16;
}
/**
* Read an unsigned short in little endian byte order from the given
* byte buffer at the given offset.
*
* @param b the byte array to read from.
* @param off the offset to read from.
* @return The value read.
*/
private final int readLeShort(byte[] b, int off) {
return (b[off] & 0xff) | (b[off + 1] & 0xff) << 8;
}
/**
* Read an int in little endian byte order from the given
* byte buffer at the given offset.
*
* @param b the byte array to read from.
* @param off the offset to read from.
* @return The value read.
*/
private final int readLeInt(byte[] b, int off) {
return ((b[off] & 0xff) | (b[off + 1] & 0xff) << 8)
| ((b[off + 2] & 0xff) | (b[off + 3] & 0xff) << 8) << 16;
}
/**
* Read the central directory of a zip file and fill the entries
* array. This is called exactly once when first needed. It is called
* while holding the lock on <code>raf</code>.
*
* @throws IOException if a i/o error occured.
* @throws ZipException if the central directory is malformed
*/
private void readEntries() throws ZipException, IOException {
/* Search for the End Of Central Directory. When a zip comment is
* present the directory may start earlier.
* FIXME: This searches the whole file in a very slow manner if the
* file isn't a zip file.
*/
//long pos = raf.length() - ENDHDR;
long pos = PfdHelper.length(pfd) - ENDHDR;
byte[] ebs = new byte[CENHDR];
do {
if (pos < 0)
throw new ZipException
("central directory not found, probably not a zip file: " + name);
//raf.seek(pos--);
seek(pfd, pos--);
}
//while (readLeInt(raf, ebs) != ENDSIG);
while (readLeInt(pfd, ebs) != ENDSIG);
if (PfdHelper.skipBytes(pfd, ENDTOT - ENDNRD) != ENDTOT - ENDNRD)
throw new EOFException(name);
//int count = readLeShort(raf, ebs);
int count = readLeShort(pfd, ebs);
if (PfdHelper.skipBytes(pfd, ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ)
throw new EOFException(name);
int centralOffset = readLeInt(pfd, ebs);
entries = new HashMap<>(count + count / 2);
//raf.seek(centralOffset);
seek(pfd, centralOffset);
byte[] buffer = new byte[16];
for (int i = 0; i < count; i++) {
//raf.readFully(ebs);
PfdHelper.readFully(pfd, ebs);
if (readLeInt(ebs, 0) != CENSIG)
throw new ZipException("Wrong Central Directory signature: " + name);
int method = readLeShort(ebs, CENHOW);
int dostime = readLeInt(ebs, CENTIM);
int crc = readLeInt(ebs, CENCRC);
int csize = readLeInt(ebs, CENSIZ);
int size = readLeInt(ebs, CENLEN);
int nameLen = readLeShort(ebs, CENNAM);
int extraLen = readLeShort(ebs, CENEXT);
int commentLen = readLeShort(ebs, CENCOM);
int offset = readLeInt(ebs, CENOFF);
int needBuffer = Math.max(nameLen, commentLen);
if (buffer.length < needBuffer)
buffer = new byte[needBuffer];
PfdHelper.readFully(pfd, buffer, 0, nameLen);
String name = new String(buffer, 0, nameLen);
AndroidZipEntry entry = new AndroidZipEntry(name, nameLen);
entry.setMethod(method);
entry.setCrc(crc & 0xffffffffL);
entry.setSize(size & 0xffffffffL);
entry.setCompressedSize(csize & 0xffffffffL);
entry.setTime(dostime);
if (extraLen > 0) {
byte[] extra = new byte[extraLen];
PfdHelper.readFully(pfd, extra);
entry.setExtra(extra);
}
if (commentLen > 0) {
PfdHelper.readFully(pfd, buffer, 0, commentLen);
entry.setComment(new String(buffer, 0, commentLen));
}
entry.offset = offset;
//ZipEntryHelper.setOffset(entry,offset);
//entry. = offset;
entries.put(name, entry);
}
}
/**
* Closes the AndroidZipFile. This also closes all input streams given by
* this class. After this is called, no further method should be
* called.
*
* @throws IOException if a i/o error occured.
*/
public void close() throws IOException {
synchronized (pfd) {
closed = true;
entries = null;
pfd.close();
}
}
/**
* Calls the <code>close()</code> method when this AndroidZipFile has not yet
* been explicitly closed.
*/
protected void finalize() throws IOException {
if (!closed && pfd != null) close();
}
/**
* Returns an enumeration of all Zip entries in this Zip file.
*/
public Enumeration<AndroidZipEntry> entries() {
try {
return new ZipEntryEnumeration(getEntries().values().iterator());
} catch (IOException ioe) {
return null;
}
}
/**
* Checks that the AndroidZipFile is still open and reads entries when necessary.
*
* @throws IllegalStateException when the AndroidZipFile has already been closed.
* @throws java, IOEexception when the entries could not be read.
*/
private HashMap<String, AndroidZipEntry> getEntries() throws IOException {
synchronized (pfd) {
if (closed)
throw new IllegalStateException("AndroidZipFile has closed: " + name);
if (entries == null)
readEntries();
return entries;
}
}
/**
* Searches for a zip entry in this archive with the given name.
*
* @param name name. May contain directory components separated by
* slashes ('/').
* @return the zip entry, or null if no entry with that name exists.
*/
public AndroidZipEntry getEntry(String name) {
try {
HashMap<String, AndroidZipEntry> entries = getEntries();
AndroidZipEntry entry = entries.get(name);
return entry != null ? (AndroidZipEntry) entry.clone() : null;
} catch (IOException ioe) {
return null;
}
}
//access should be protected by synchronized(raf)
private final byte[] locBuf = new byte[LOCHDR];
/**
* Checks, if the local header of the entry at index i matches the
* central directory, and returns the offset to the data.
*
* @param entry to check.
* @return the start offset of the (compressed) data.
* @throws IOException if a i/o error occured.
* @throws ZipException if the local header doesn't match the
* central directory header
*/
private long checkLocalHeader(AndroidZipEntry entry) throws IOException {
synchronized (pfd) {
seek(pfd, entry.offset);
PfdHelper.readFully(pfd, locBuf);
if (readLeInt(locBuf, 0) != LOCSIG)
throw new ZipException("Wrong Local header signature: " + name);
if (entry.getMethod() != readLeShort(locBuf, LOCHOW))
throw new ZipException("Compression method mismatch: " + name);
if (entry.getNameLen() != readLeShort(locBuf, LOCNAM))
throw new ZipException("file name length mismatch: " + name);
int extraLen = entry.getNameLen() + readLeShort(locBuf, LOCEXT);
return entry.offset + LOCHDR + extraLen;
}
}
/**
* Creates an input stream reading the given zip entry as
* uncompressed data. Normally zip entry should be an entry
* returned by getEntry() or entries().
*
* @param entry the entry to create an InputStream for.
* @return the input stream.
* @throws IOException if a i/o error occured.
* @throws ZipException if the Zip archive is malformed.
*/
public InputStream getInputStream(AndroidZipEntry entry) throws IOException {
HashMap<String, AndroidZipEntry> entries = getEntries();
String name = entry.getName();
AndroidZipEntry zipEntry = entries.get(name);
if (zipEntry == null)
throw new NoSuchElementException(name);
long start = checkLocalHeader(zipEntry);
int method = zipEntry.getMethod();
InputStream is = new BufferedInputStream(new PartialInputStream
(pfd, start, zipEntry.getCompressedSize()));
switch (method) {
case ZipOutputStream.STORED:
return is;
case ZipOutputStream.DEFLATED:
return new InflaterInputStream(is, new Inflater(true));
default:
throw new ZipException("Unknown compression method " + method);
}
}
/**
* Returns the (path) name of this zip file.
*/
public String getName() {
return name;
}
/**
* Returns the number of entries in this zip file.
*/
public int size() {
try {
return getEntries().size();
} catch (IOException ioe) {
return 0;
}
}
private static class ZipEntryEnumeration implements Enumeration<AndroidZipEntry> {
private final Iterator<AndroidZipEntry> elements;
public ZipEntryEnumeration(Iterator<AndroidZipEntry> elements) {
this.elements = elements;
}
public boolean hasMoreElements() {
return elements.hasNext();
}
public AndroidZipEntry nextElement() {
/* We return a clone, just to be safe that the user doesn't
* change the entry.
*/
return (AndroidZipEntry) (elements.next()).clone();
}
}
private static class PartialInputStream extends InputStream {
private final ParcelFileDescriptor pfd;
long filepos, end;
public PartialInputStream(ParcelFileDescriptor pfd, long start, long len) {
this.pfd = pfd;
filepos = start;
end = start + len;
}
public int available() {
long amount = end - filepos;
if (amount > Integer.MAX_VALUE)
return Integer.MAX_VALUE;
return (int) amount;
}
public int read() throws IOException {
if (filepos == end)
return -1;
synchronized (pfd) {
seek(pfd, filepos++);
return PfdHelper.read(pfd);
}
}
public int read(byte[] b, int off, int len) throws IOException {
if (len > end - filepos) {
len = (int) (end - filepos);
if (len == 0)
return -1;
}
synchronized (pfd) {
seek(pfd, filepos);
int count = PfdHelper.read(pfd, b, off, len);
if (count > 0)
filepos += len;
return count;
}
}
public long skip(long amount) {
if (amount < 0)
throw new IllegalArgumentException();
if (amount > end - filepos)
amount = end - filepos;
filepos += amount;
return amount;
}
}
}
@@ -0,0 +1,61 @@
package me.ag2s.epublib.util.zip;
interface ZipConstants {
/* The local file header */
int LOCHDR = 30;
int LOCSIG = 'P' | ('K' << 8) | (3 << 16) | (4 << 24);
int LOCVER = 4;
int LOCFLG = 6;
int LOCHOW = 8;
int LOCTIM = 10;
int LOCCRC = 14;
int LOCSIZ = 18;
int LOCLEN = 22;
int LOCNAM = 26;
int LOCEXT = 28;
/* The Data descriptor */
int EXTSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24);
int EXTHDR = 16;
int EXTCRC = 4;
int EXTSIZ = 8;
int EXTLEN = 12;
/* The central directory file header */
int CENSIG = 'P' | ('K' << 8) | (1 << 16) | (2 << 24);
int CENHDR = 46;
int CENVEM = 4;
int CENVER = 6;
int CENFLG = 8;
int CENHOW = 10;
int CENTIM = 12;
int CENCRC = 16;
int CENSIZ = 20;
int CENLEN = 24;
int CENNAM = 28;
int CENEXT = 30;
int CENCOM = 32;
int CENDSK = 34;
int CENATT = 36;
int CENATX = 38;
int CENOFF = 42;
/* The entries in the end of central directory */
int ENDSIG = 'P' | ('K' << 8) | (5 << 16) | (6 << 24);
int ENDHDR = 22;
/* The following two fields are missing in SUN JDK */
int ENDNRD = 4;
int ENDDCD = 6;
int ENDSUB = 8;
int ENDTOT = 10;
int ENDSIZ = 12;
int ENDOFF = 16;
int ENDCOM = 20;
}
@@ -0,0 +1,75 @@
package me.ag2s.epublib.util.zip;
import androidx.annotation.NonNull;
import java.util.zip.ZipEntry;
public class ZipEntryWrapper {
@NonNull
private final Object zipEntry;
public void checkType() {
if (zipEntry instanceof java.util.zip.ZipEntry || zipEntry instanceof AndroidZipEntry) {
} else {
throw new RuntimeException("使用了不支持的类");
}
}
public ZipEntryWrapper(@NonNull ZipEntry zipEntry) {
this.zipEntry = zipEntry;
}
public ZipEntryWrapper(@NonNull AndroidZipEntry zipEntry) {
this.zipEntry = zipEntry;
}
public ZipEntryWrapper(@NonNull Object element) {
this.zipEntry = element;
checkType();
}
public boolean isDirectory() {
checkType();
if (zipEntry instanceof ZipEntry) {
return ((ZipEntry) zipEntry).isDirectory();
}
if (zipEntry instanceof AndroidZipEntry) {
return ((AndroidZipEntry) zipEntry).isDirectory();
}
return true;
}
public ZipEntry getZipEntry() {
return (ZipEntry) zipEntry;
}
public AndroidZipEntry getAndroidZipEntry() {
return (AndroidZipEntry) zipEntry;
}
public String getName() {
checkType();
if (zipEntry instanceof ZipEntry) {
return ((ZipEntry) zipEntry).getName();
}
if (zipEntry instanceof AndroidZipEntry) {
return ((AndroidZipEntry) zipEntry).getName();
}
return null;
}
public long getSize() {
checkType();
if (zipEntry instanceof ZipEntry) {
return ((ZipEntry) zipEntry).getSize();
}
if (zipEntry instanceof AndroidZipEntry) {
return ((AndroidZipEntry) zipEntry).getSize();
}
return -1;
}
}
@@ -0,0 +1,34 @@
package me.ag2s.epublib.util.zip;
import java.io.IOException;
/**
* Thrown during the creation or input of a zip file.
*
* @author Jochen Hoenicke
* @author Per Bothner
* @status updated to 1.4
*/
public class ZipException extends IOException {
/**
* Compatible with JDK 1.0+.
*/
private static final long serialVersionUID = 8000196834066748623L;
/**
* Create an exception without a message.
*/
public ZipException() {
}
/**
* Create an exception with a message.
*
* @param msg the message
*/
public ZipException(String msg) {
super(msg);
}
}
@@ -0,0 +1,103 @@
package me.ag2s.epublib.util.zip;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipFile;
/**
* 对ZipFile的包装
*/
public class ZipFileWrapper {
@NonNull
private final Object zipFile;
public void checkType() {
if (zipFile instanceof java.util.zip.ZipFile || zipFile instanceof AndroidZipFile) {
} else {
throw new RuntimeException("使用了不支持的类");
}
}
public ZipFileWrapper(@NonNull ZipFile zipFile) {
this.zipFile = zipFile;
checkType();
}
public ZipFileWrapper(@NonNull AndroidZipFile zipFile) {
this.zipFile = zipFile;
checkType();
}
public String getName() {
checkType();
if (zipFile instanceof java.util.zip.ZipFile) {
return ((ZipFile) zipFile).getName();
} else if (zipFile instanceof AndroidZipFile) {
return ((AndroidZipFile) zipFile).getName();
} else {
return null;
}
}
public String getComment() {
checkType();
if (zipFile instanceof java.util.zip.ZipFile) {
return ((ZipFile) zipFile).getComment();
} else if (zipFile instanceof AndroidZipFile) {
return ((AndroidZipFile) zipFile).getName();
} else {
return null;
}
}
public ZipEntryWrapper getEntry(String name) {
checkType();
if (zipFile instanceof java.util.zip.ZipFile) {
return new ZipEntryWrapper(((ZipFile) zipFile).getEntry(name));
} else if (zipFile instanceof AndroidZipFile) {
return new ZipEntryWrapper(((AndroidZipFile) zipFile).getEntry(name));
} else {
return null;
}
}
public Enumeration entries() {
checkType();
if (zipFile instanceof java.util.zip.ZipFile) {
return ((ZipFile) zipFile).entries();
} else if (zipFile instanceof AndroidZipFile) {
return ((AndroidZipFile) zipFile).entries();
} else {
return null;
}
}
public InputStream getInputStream(ZipEntryWrapper entry) throws IOException {
checkType();
if (zipFile instanceof java.util.zip.ZipFile) {
return ((ZipFile) zipFile).getInputStream(entry.getZipEntry());
} else if (zipFile instanceof AndroidZipFile) {
return ((AndroidZipFile) zipFile).getInputStream(entry.getAndroidZipEntry());
} else {
return null;
}
}
public void close() throws IOException {
checkType();
if (zipFile instanceof java.util.zip.ZipFile) {
((ZipFile) zipFile).close();
} else if (zipFile instanceof AndroidZipFile) {
((AndroidZipFile) zipFile).close();
}
}
}
@@ -0,0 +1,88 @@
package me.ag2s.umdlib.domain;
import java.io.IOException;
import java.io.OutputStream;
import me.ag2s.umdlib.tool.WrapOutputStream;
public class UmdBook {
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
private int num;
/**
* Header Part of UMD book
*/
private UmdHeader header = new UmdHeader();
/**
* Detail chapters Part of UMD book
* (include Titles & Contents of each chapter)
*/
private UmdChapters chapters = new UmdChapters();
/**
* Cover Part of UMD book (for example, and JPEG file)
*/
private UmdCover cover = new UmdCover();
/**
* End Part of UMD book
*/
private UmdEnd end = new UmdEnd();
/**
* Build the UMD file.
*
* @param os OutputStream
* @throws IOException
*/
public void buildUmd(OutputStream os) throws IOException {
WrapOutputStream wos = new WrapOutputStream(os);
header.buildHeader(wos);
chapters.buildChapters(wos);
cover.buildCover(wos);
end.buildEnd(wos);
}
public UmdHeader getHeader() {
return header;
}
public void setHeader(UmdHeader header) {
this.header = header;
}
public UmdChapters getChapters() {
return chapters;
}
public void setChapters(UmdChapters chapters) {
this.chapters = chapters;
}
public UmdCover getCover() {
return cover;
}
public void setCover(UmdCover cover) {
this.cover = cover;
}
public UmdEnd getEnd() {
return end;
}
public void setEnd(UmdEnd end) {
this.end = end;
}
}
@@ -0,0 +1,212 @@
package me.ag2s.umdlib.domain;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
import me.ag2s.umdlib.tool.UmdUtils;
import me.ag2s.umdlib.tool.WrapOutputStream;
/**
* It includes all titles and contents of each chapter in the UMD file.
* And the content has been compressed by zlib.
*
* @author Ray Liang (liangguanhui@qq.com)
* 2009-12-20
*/
public class UmdChapters {
private static final int DEFAULT_CHUNK_INIT_SIZE = 32768;
private int TotalContentLen;
public List<byte[]> getTitles() {
return titles;
}
private final List<byte[]> titles = new ArrayList<>();
public List<Integer> contentLengths = new ArrayList<>();
public ByteArrayOutputStream contents = new ByteArrayOutputStream();
public void addTitle(String s) {
titles.add(UmdUtils.stringToUnicodeBytes(s));
}
public void addTitle(byte[] s) {
titles.add(s);
}
public void addContentLength(Integer integer) {
contentLengths.add(integer);
}
public int getContentLength(int index) {
return contentLengths.get(index);
}
public byte[] getContent(int index) {
int st = contentLengths.get(index);
byte[] b = contents.toByteArray();
int end = index + 1 < contentLengths.size() ? contentLengths.get(index + 1) : getTotalContentLen();
System.out.println("总长度:" + contents.size());
System.out.println("起始值:" + st);
System.out.println("结束值:" + end);
byte[] bAr = new byte[end - st];
System.arraycopy(b, st, bAr, 0, bAr.length);
return bAr;
}
public String getContentString(int index) {
return UmdUtils.unicodeBytesToString(getContent(index)).replace((char) 0x2029, '\n');
}
public String getTitle(int index) {
return UmdUtils.unicodeBytesToString(titles.get(index));
}
public void buildChapters(WrapOutputStream wos) throws IOException {
writeChaptersHead(wos);
writeChaptersContentOffset(wos);
writeChaptersTitles(wos);
writeChaptersChunks(wos);
}
private void writeChaptersHead(WrapOutputStream wos) throws IOException {
wos.writeBytes('#', 0x0b, 0, 0, 0x09);
wos.writeInt(contents.size());
}
private void writeChaptersContentOffset(WrapOutputStream wos) throws IOException {
wos.writeBytes('#', 0x83, 0, 0, 0x09);
byte[] rb = UmdUtils.genRandomBytes(4);
wos.writeBytes(rb); //random numbers
wos.write('$');
wos.writeBytes(rb); //random numbers
wos.writeInt(contentLengths.size() * 4 + 9); // about the count of chapters
int offset = 0;
for (Integer n : contentLengths) {
wos.writeInt(offset);
offset += n;
}
}
private void writeChaptersTitles(WrapOutputStream wos) throws IOException {
wos.writeBytes('#', 0x84, 0, 0x01, 0x09);
byte[] rb = UmdUtils.genRandomBytes(4);
wos.writeBytes(rb); //random numbers
wos.write('$');
wos.writeBytes(rb); //random numbers
int totalTitlesLen = 0;
for (byte[] t : titles) {
totalTitlesLen += t.length;
}
// about the length of the titles
wos.writeInt(totalTitlesLen + titles.size() + 9);
for (byte[] t : titles) {
wos.writeByte(t.length);
wos.write(t);
}
}
private void writeChaptersChunks(WrapOutputStream wos) throws IOException {
byte[] allContents = contents.toByteArray();
byte[] zero16 = new byte[16];
Arrays.fill(zero16, 0, zero16.length, (byte) 0);
// write each package of content
int startPos = 0;
int len = 0;
int left = 0;
int chunkCnt = 0;
ByteArrayOutputStream bos = new ByteArrayOutputStream(DEFAULT_CHUNK_INIT_SIZE + 256);
List<byte[]> chunkRbList = new ArrayList<>();
while (startPos < allContents.length) {
left = allContents.length - startPos;
len = Math.min(DEFAULT_CHUNK_INIT_SIZE, left);
bos.reset();
DeflaterOutputStream zos = new DeflaterOutputStream(bos);
zos.write(allContents, startPos, len);
zos.close();
byte[] chunk = bos.toByteArray();
byte[] rb = UmdUtils.genRandomBytes(4);
wos.writeByte('$');
wos.writeBytes(rb); // 4 random
chunkRbList.add(rb);
wos.writeInt(chunk.length + 9);
wos.write(chunk);
// end of each chunk
wos.writeBytes('#', 0xF1, 0, 0, 0x15);
wos.write(zero16);
startPos += len;
chunkCnt++;
}
// end of all chunks
wos.writeBytes('#', 0x81, 0, 0x01, 0x09);
wos.writeBytes(0, 0, 0, 0); //random numbers
wos.write('$');
wos.writeBytes(0, 0, 0, 0); //random numbers
wos.writeInt(chunkCnt * 4 + 9);
for (int i = chunkCnt - 1; i >= 0; i--) {
// random. They are as the same as random numbers in the begin of each chunk
// use desc order to output these random
wos.writeBytes(chunkRbList.get(i));
}
}
public void addChapter(String title, String content) {
titles.add(UmdUtils.stringToUnicodeBytes(title));
byte[] b = UmdUtils.stringToUnicodeBytes(content);
contentLengths.add(b.length);
try {
contents.write(b);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void addFile(File f, String title) throws IOException {
byte[] temp = UmdUtils.readFile(f);
String s = new String(temp);
addChapter(title, s);
}
public void addFile(File f) throws IOException {
String s = f.getName();
int idx = s.lastIndexOf('.');
if (idx >= 0) {
s = s.substring(0, idx);
}
addFile(f, s);
}
public void clearChapters() {
titles.clear();
contentLengths.clear();
contents.reset();
}
public int getTotalContentLen() {
return TotalContentLen;
}
public void setTotalContentLen(int totalContentLen) {
TotalContentLen = totalContentLen;
}
}
@@ -0,0 +1,97 @@
package me.ag2s.umdlib.domain;
import java.io.File;
import java.io.IOException;
import me.ag2s.umdlib.tool.UmdUtils;
import me.ag2s.umdlib.tool.WrapOutputStream;
/**
* This is the cover part of the UMD file.
* <p>
* NOTICE: if the "coverData" is empty, it will be skipped when building UMD file.
* </P>
* There are 3 ways to load the image data:
* <ol>
* <li>new constructor function of UmdCover.</li>
* <li>use UmdCover.load function.</li>
* <li>use UmdCover.initDefaultCover, it will generate a simple image with text.</li>
* </ol>
*
* @author Ray Liang (liangguanhui@qq.com)
* 2009-12-20
*/
public class UmdCover {
private static final int DEFAULT_COVER_WIDTH = 120;
private static final int DEFAULT_COVER_HEIGHT = 160;
private byte[] coverData;
public UmdCover() {
}
public UmdCover(byte[] coverData) {
this.coverData = coverData;
}
public void load(File f) throws IOException {
this.coverData = UmdUtils.readFile(f);
}
public void load(String fileName) throws IOException {
load(new File(fileName));
}
public void initDefaultCover(String title) throws IOException {
// BufferedImage img = new BufferedImage(DEFAULT_COVER_WIDTH, DEFAULT_COVER_HEIGHT, BufferedImage.TYPE_INT_RGB);
// Graphics g = img.getGraphics();
// g.setColor(Color.BLACK);
// g.fillRect(0, 0, img.getWidth(), img.getHeight());
// g.setColor(Color.WHITE);
// g.setFont(new Font("", Font.PLAIN, 12));
//
// FontMetrics fm = g.getFontMetrics();
// int ascent = fm.getAscent();
// int descent = fm.getDescent();
// int strWidth = fm.stringWidth(title);
// int x = (img.getWidth() - strWidth) / 2;
// int y = (img.getHeight() - ascent - descent) / 2;
// g.drawString(title, x, y);
// g.dispose();
//
// ByteArrayOutputStream baos = new ByteArrayOutputStream();
//
// JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos);
// JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img);
// param.setQuality(0.5f, false);
// encoder.setJPEGEncodeParam(param);
// encoder.encode(img);
//
// coverData = baos.toByteArray();
}
public void buildCover(WrapOutputStream wos) throws IOException {
if (coverData == null || coverData.length == 0) {
return;
}
wos.writeBytes('#', 0x82, 0, 0x01, 0x0A, 0x01);
byte[] rb = UmdUtils.genRandomBytes(4);
wos.writeBytes(rb); //random numbers
wos.write('$');
wos.writeBytes(rb); //random numbers
wos.writeInt(coverData.length + 9);
wos.write(coverData);
}
public byte[] getCoverData() {
return coverData;
}
public void setCoverData(byte[] coverData) {
this.coverData = coverData;
}
}
@@ -0,0 +1,20 @@
package me.ag2s.umdlib.domain;
import java.io.IOException;
import me.ag2s.umdlib.tool.WrapOutputStream;
/**
* End part of UMD book, nothing to be special
*
* @author Ray Liang (liangguanhui@qq.com)
* 2009-12-20
*/
public class UmdEnd {
public void buildEnd(WrapOutputStream wos) throws IOException {
wos.writeBytes('#', 0x0C, 0, 0x01, 0x09);
wos.writeInt(wos.getWritten() + 4);
}
}
@@ -0,0 +1,164 @@
package me.ag2s.umdlib.domain;
import androidx.annotation.NonNull;
import java.io.IOException;
import me.ag2s.umdlib.tool.UmdUtils;
import me.ag2s.umdlib.tool.WrapOutputStream;
/**
* Header of UMD file.
* It includes a lot of properties of header.
* All the properties are String type.
*
* @author Ray Liang (liangguanhui@qq.com)
* 2009-12-20
*/
public class UmdHeader {
public byte getUmdType() {
return umdType;
}
public void setUmdType(byte umdType) {
this.umdType = umdType;
}
private byte umdType;
private String title;
private String author;
private String year;
private String month;
private String day;
private String bookType;
private String bookMan;
private String shopKeeper;
private final static byte B_type_umd = (byte) 0x01;
private final static byte B_type_title = (byte) 0x02;
private final static byte B_type_author = (byte) 0x03;
private final static byte B_type_year = (byte) 0x04;
private final static byte B_type_month = (byte) 0x05;
private final static byte B_type_day = (byte) 0x06;
private final static byte B_type_bookType = (byte) 0x07;
private final static byte B_type_bookMan = (byte) 0x08;
private final static byte B_type_shopKeeper = (byte) 0x09;
public void buildHeader(WrapOutputStream wos) throws IOException {
wos.writeBytes(0x89, 0x9b, 0x9a, 0xde); // UMD file type flags
wos.writeByte('#');
wos.writeBytes(0x01, 0x00, 0x00, 0x08); // Unknown
wos.writeByte(0x01); //0x01 is text type; while 0x02 is Image type.
wos.writeBytes(UmdUtils.genRandomBytes(2)); //random number
// start properties output
buildType(wos, B_type_title, getTitle());
buildType(wos, B_type_author, getAuthor());
buildType(wos, B_type_year, getYear());
buildType(wos, B_type_month, getMonth());
buildType(wos, B_type_day, getDay());
buildType(wos, B_type_bookType, getBookType());
buildType(wos, B_type_bookMan, getBookMan());
buildType(wos, B_type_shopKeeper, getShopKeeper());
}
public void buildType(WrapOutputStream wos, byte type, String content) throws IOException {
if (content == null || content.length() == 0) {
return;
}
wos.writeBytes('#', type, 0, 0);
byte[] temp = UmdUtils.stringToUnicodeBytes(content);
wos.writeByte(temp.length + 5);
wos.write(temp);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getBookMan() {
return bookMan;
}
public void setBookMan(String bookMan) {
this.bookMan = bookMan;
}
public String getShopKeeper() {
return shopKeeper;
}
public void setShopKeeper(String shopKeeper) {
this.shopKeeper = shopKeeper;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public String getDay() {
return day;
}
public void setDay(String day) {
this.day = day;
}
public String getBookType() {
return bookType;
}
public void setBookType(String bookType) {
this.bookType = bookType;
}
@Override
@NonNull
public String toString() {
return "UmdHeader{" +
"umdType=" + umdType +
", title='" + title + '\'' +
", author='" + author + '\'' +
", year='" + year + '\'' +
", month='" + month + '\'' +
", day='" + day + '\'' +
", bookType='" + bookType + '\'' +
", bookMan='" + bookMan + '\'' +
", shopKeeper='" + shopKeeper + '\'' +
'}';
}
}
@@ -0,0 +1,134 @@
package me.ag2s.umdlib.tool;
import java.io.IOException;
import java.io.InputStream;
public class StreamReader {
private final InputStream is;
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
private long offset;
private long size;
private void incCount(int value) {
int temp = (int) (offset + value);
if (temp < 0) {
temp = Integer.MAX_VALUE;
}
offset = temp;
}
public StreamReader(InputStream inputStream) throws IOException {
this.is = inputStream;
//this.size=inputStream.getChannel().size();
}
public short readUint8() throws IOException {
byte[] b = new byte[1];
is.read(b);
incCount(1);
return (short) ((b[0] & 0xFF));
}
public byte readByte() throws IOException {
byte[] b = new byte[1];
is.read(b);
incCount(1);
return b[0];
}
public byte[] readBytes(int len) throws IOException {
if (len < 0) {
System.out.println(len);
throw new IllegalArgumentException("Length must > 0: " + len);
}
if (len==0){
return null;
}
byte[] b = new byte[len];
is.read(b);
incCount(len);
return b;
}
public String readHex(int len) throws IOException {
if (len < 1) {
System.out.println(len);
throw new IllegalArgumentException("Length must > 0: " + len);
}
byte[] b = new byte[len];
is.read(b);
incCount(len);
return UmdUtils.toHex(b);
}
public short readShort() throws IOException {
byte[] b = new byte[2];
is.read(b);
incCount(2);
short x = (short) (((b[0] & 0xFF) << 8) | ((b[1] & 0xFF) << 0));
return x;
}
public short readShortLe() throws IOException {
byte[] b = new byte[2];
is.read(b);
incCount(2);
short x = (short) (((b[1] & 0xFF) << 8) | ((b[0] & 0xFF) << 0));
return x;
}
public int readInt() throws IOException {
byte[] b = new byte[4];
is.read(b);
incCount(4);
int x = ((b[0] & 0xFF) << 24) | ((b[1] & 0xFF) << 16) |
((b[2] & 0xFF) << 8) | ((b[3] & 0xFF) << 0);
return x;
}
public int readIntLe() throws IOException {
byte[] b = new byte[4];
is.read(b);
incCount(4);
int x = ((b[3] & 0xFF) << 24) | ((b[2] & 0xFF) << 16) |
((b[1] & 0xFF) << 8) | ((b[0] & 0xFF) << 0);
return x;
}
public void skip(int len) throws IOException {
readBytes(len);
}
public byte[] read(byte[] b) throws IOException {
is.read(b);
incCount(b.length);
return b;
}
public byte[] read(byte[] b, int off, int len) throws IOException {
is.read(b, off, len);
incCount(len);
return b;
}
}
@@ -0,0 +1,129 @@
package me.ag2s.umdlib.tool;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
import java.util.zip.Inflater;
import java.nio.charset.StandardCharsets;
public class UmdUtils {
private static final int EOF = -1;
private static final int BUFFER_SIZE = 8 * 1024;
/**
* 将字符串编码成Unicode形式的byte[]
*
* @param s 要编码的字符串
* @return 编码好的byte[]
*/
public static byte[] stringToUnicodeBytes(String s) {
if (s == null) {
throw new NullPointerException();
}
return s.getBytes(StandardCharsets.UTF_16LE);
}
/**
* 将编码成Unicode形式的byte[]解码成原始字符串
*
* @param bytes 编码成Unicode形式的byte[]
* @return 原始字符串
*/
public static String unicodeBytesToString(byte[] bytes) {
//修复一些文件属性空值的问题
if (bytes==null){
return "";
}
return new String(bytes, StandardCharsets.UTF_16LE);
}
/**
* 将byte[]转化成Hex形式
*
* @param bArr byte[]
* @return 目标HEX字符串
*/
public static String toHex(byte[] bArr) {
StringBuilder sb = new StringBuilder(bArr.length);
String sTmp;
for (int i = 0; i < bArr.length; i++) {
sTmp = Integer.toHexString(0xFF & bArr[i]);
if (sTmp.length() < 2)
sb.append(0);
sb.append(sTmp.toUpperCase());
}
return sb.toString();
}
/**
* 解压缩zip的byte[]
*
* @param compress zippered byte[]
* @return decompressed byte[]
* @throws Exception 解码时失败时
*/
public static byte[] decompress(byte[] compress) throws Exception {
Inflater inflater = new Inflater();
inflater.reset();
inflater.setInput(compress);
ByteArrayOutputStream baos = new ByteArrayOutputStream(compress.length);
try (baos) {
byte[] buff = new byte[BUFFER_SIZE];
while (!inflater.finished()) {
int count = inflater.inflate(buff);
baos.write(buff, 0, count);
}
}
inflater.end();
return baos.toByteArray();
}
public static void saveFile(File f, byte[] content) throws IOException {
try (FileOutputStream fos = new FileOutputStream(f)) {
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(content);
bos.flush();
}
}
public static byte[] readFile(File f) throws IOException {
try (FileInputStream fis = new FileInputStream(f)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedInputStream bis = new BufferedInputStream(fis);
int ch;
while ((ch = bis.read()) >= 0) {
baos.write(ch);
}
baos.flush();
return baos.toByteArray();
}
}
private static final Random random = new Random();
public static byte[] genRandomBytes(int len) {
if (len <= 0) {
throw new IllegalArgumentException("Length must > 0: " + len);
}
byte[] ret = new byte[len];
for (int i = 0; i < ret.length; i++) {
ret[i] = (byte) random.nextInt(256);
}
return ret;
}
}
@@ -0,0 +1,91 @@
package me.ag2s.umdlib.tool;
import java.io.IOException;
import java.io.OutputStream;
public class WrapOutputStream extends OutputStream {
private final OutputStream os;
private int written;
public WrapOutputStream(OutputStream os) {
this.os = os;
}
private void incCount(int value) {
int temp = written + value;
if (temp < 0) {
temp = Integer.MAX_VALUE;
}
written = temp;
}
// it is different from the writeInt of DataOutputStream
public void writeInt(int v) throws IOException {
os.write((v >>> 0) & 0xFF);
os.write((v >>> 8) & 0xFF);
os.write((v >>> 16) & 0xFF);
os.write((v >>> 24) & 0xFF);
incCount(4);
}
public void writeByte(byte b) throws IOException {
write(b);
}
public void writeByte(int n) throws IOException {
write(n);
}
public void writeBytes(byte... bytes) throws IOException {
write(bytes);
}
public void writeBytes(int... vals) throws IOException {
for (int v : vals) {
write(v);
}
}
public void write(byte[] b, int off, int len) throws IOException {
os.write(b, off, len);
incCount(len);
}
public void write(byte[] b) throws IOException {
os.write(b);
incCount(b.length);
}
public void write(int b) throws IOException {
os.write(b);
incCount(1);
}
/////////////////////////////////////////////////
public void close() throws IOException {
os.close();
}
public void flush() throws IOException {
os.flush();
}
public boolean equals(Object obj) {
return os.equals(obj);
}
public int hashCode() {
return os.hashCode();
}
public String toString() {
return os.toString();
}
public int getWritten() {
return written;
}
}
@@ -0,0 +1,223 @@
package me.ag2s.umdlib.umd;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
import me.ag2s.umdlib.domain.UmdBook;
import me.ag2s.umdlib.domain.UmdCover;
import me.ag2s.umdlib.domain.UmdHeader;
import me.ag2s.umdlib.tool.StreamReader;
import me.ag2s.umdlib.tool.UmdUtils;
/**
* UMD格式的电子书解析
* 格式规范参考:
* http://blog.sina.com.cn/s/blog_7c8dc2d501018o5d.html
* http://blog.sina.com.cn/s/blog_7c8dc2d501018o5l.html
*/
public class UmdReader {
UmdBook book;
InputStream inputStream;
int _AdditionalCheckNumber;
int _TotalContentLen;
boolean end = false;
public synchronized UmdBook read(InputStream inputStream) throws Exception {
book = new UmdBook();
this.inputStream = inputStream;
StreamReader reader = new StreamReader(inputStream);
UmdHeader umdHeader = new UmdHeader();
book.setHeader(umdHeader);
if (reader.readIntLe() != 0xde9a9b89) {
throw new IOException("Wrong header");
}
short num1 = -1;
byte ch = reader.readByte();
while (ch == 35) {
//int num2=reader.readByte();
short segType = reader.readShortLe();
byte segFlag = reader.readByte();
short len = (short) (reader.readUint8() - 5);
System.out.println("块标识:" + segType);
//short length1 = reader.readByte();
readSection(segType, segFlag, len, reader, umdHeader);
if ((int) segType == 241 || (int) segType == 10) {
segType = num1;
}
for (ch = reader.readByte(); ch == 36; ch = reader.readByte()) {
//int num3 = reader.readByte();
System.out.println(ch);
int additionalCheckNumber = reader.readIntLe();
int length2 = (reader.readIntLe() - 9);
readAdditionalSection(segType, additionalCheckNumber, length2, reader);
}
num1 = segType;
}
System.out.println(book.getHeader().toString());
return book;
}
private void readAdditionalSection(short segType, int additionalCheckNumber, int length, StreamReader reader) throws Exception {
switch (segType) {
case 14:
//this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length))));
break;
case 15:
//this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length))));
break;
case 129:
reader.readBytes(length);
break;
case 130:
//byte[] covers = reader.readBytes(length);
book.setCover(new UmdCover(reader.readBytes(length)));
//this._Book.Cover = BitmapImage.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length)));
break;
case 131:
System.out.println(length / 4);
book.setNum(length / 4);
for (int i = 0; i < length / 4; ++i) {
book.getChapters().addContentLength(reader.readIntLe());
}
break;
case 132:
//System.out.println(length/4);
System.out.println(_AdditionalCheckNumber);
System.out.println(additionalCheckNumber);
if (this._AdditionalCheckNumber != additionalCheckNumber) {
System.out.println(length);
book.getChapters().contents.write(UmdUtils.decompress(reader.readBytes(length)));
book.getChapters().contents.flush();
break;
} else {
for (int i = 0; i < book.getNum(); i++) {
short len = reader.readUint8();
byte[] title = reader.readBytes(len);
//System.out.println(UmdUtils.unicodeBytesToString(title));
book.getChapters().addTitle(title);
}
}
break;
default:
/*Console.WriteLine("未知内容");
Console.WriteLine("Seg Type = " + (object) segType);
Console.WriteLine("Seg Len = " + (object) length);
Console.WriteLine("content = " + (object) reader.ReadBytes((int) length));*/
break;
}
}
public void readSection(short segType, byte segFlag, short length, StreamReader reader, UmdHeader header) throws IOException {
switch (segType) {
case 1://umd文件头 DCTS_CMD_ID_VERSION
header.setUmdType(reader.readByte());
reader.readBytes(2);//Random 2
System.out.println("UMD文件类型:" + header.getUmdType());
break;
case 2://文件标题 DCTS_CMD_ID_TITLE
header.setTitle(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("文件标题:" + header.getTitle());
break;
case 3://作者
header.setAuthor(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("作者:" + header.getAuthor());
break;
case 4://年
header.setYear(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("年:" + header.getYear());
break;
case 5://月
header.setMonth(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("月:" + header.getMonth());
break;
case 6://日
header.setDay(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("日:" + header.getDay());
break;
case 7://小说类型
header.setBookType(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("小说类型:" + header.getBookType());
break;
case 8://出版商
header.setBookMan(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("出版商:" + header.getBookMan());
break;
case 9:// 零售商
header.setShopKeeper(UmdUtils.unicodeBytesToString(reader.readBytes(length)));
System.out.println("零售商:" + header.getShopKeeper());
break;
case 10://CONTENT ID
System.out.println("CONTENT ID:" + reader.readHex(length));
break;
case 11:
//内容长度 DCTS_CMD_ID_FILE_LENGTH
_TotalContentLen = reader.readIntLe();
book.getChapters().setTotalContentLen(_TotalContentLen);
System.out.println("内容长度:" + _TotalContentLen);
break;
case 12://UMD文件结束
end = true;
int num2 = reader.readIntLe();
System.out.println("整个文件长度" + num2);
break;
case 13:
break;
case 14:
int num3 = reader.readByte();
break;
case 15:
reader.readBytes(length);
break;
case 129://正文
case 131://章节偏移
_AdditionalCheckNumber = reader.readIntLe();
System.out.println("章节偏移:" + _AdditionalCheckNumber);
break;
case 132://章节标题,正文
_AdditionalCheckNumber = reader.readIntLe();
System.out.println("章节标题,正文:" + _AdditionalCheckNumber);
break;
case 130://封面(jpg
int num4 = reader.readByte();
_AdditionalCheckNumber = reader.readIntLe();
break;
case 135://页面偏移(Page Offset
reader.readUint8();//fontSize 一字节 字体大小
reader.readUint8();//screenWidth 屏幕宽度
reader.readBytes(4);//BlockRandom 指向一个页面偏移数据块
break;
case 240://CDS KEY
break;
case 241://许可证(LICENCE KEY)
//System.out.println("整个文件长度" + length);
System.out.println("许可证(LICENCE KEY):" + reader.readHex(16));
break;
default:
if (length > 0) {
byte[] numArray = reader.readBytes(length);
}
}
}
@Override
@NonNull
public String toString() {
return "UmdReader{" +
"book=" + book +
'}';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,277 @@
<!--
Title:
The Package Document Type Definition (DTD) for the Open
eBook Publication Structure Version 1.2
Version:
1.2
Revision:
20020930-x (supercedes 20020605-x)
Revision History Note:
This revision, 20020930-x, which supercedes the prior revision
20020605-x, solely updates the email addresses within this
comment prologue. No changes whatsover were made to the parsed
content of this DTD.
Previous Released Version:
1.0.1 (Revision of 01-February-2001, "Document Type
Definition for the Open eBook package version
1.0.1")
Authors:
Version 1.0; 1.0.1
Steve DeRose <sjd@stg.brown.edu>
Gunter Hille <hille@abc.de>
Ben Trafford <ben@legendary.org>
Garret Wilson <garret@globalmentor.com>
This Version 1.2 updated and edited by:
Jon Noring <jon@noring.name>
Benjamin Jung <benjamin.jung@deepx.com>
Usage:
<?xml version="1.0"?>
<!DOCTYPE package
PUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.2 Package//EN"
"http://openebook.org/dtds/oeb-1.2/oebpkg12.dtd">
<package unique-identifier="foo">
metadata
manifest
spine
tours
guide
</package>
Summary Description:
This is the Package Document Type Definition (DTD) for
the Open eBook Publication Structure version 1.2.
Changes to this DTD from version 1.0.1 include:
a. Upgrading the <dc-metadata> content model to
fully conform with the OEBPS 1.2 specification
requirements. Specifically, <dc:Language> is
now required, while in OEBPS 1.0.1 it was
optional.
b. Updating the mnemonic character entity
declaration to refer to version 1.2.
c. Updating the xmlns:dc namespace to refer to
version 1.1 of the Dublin Core Metadata
Initiative.
d. Editing and updating the various non-parsed
comments.
e. Revising the layout (e.g., white space
alteration) to aid in readability.
-->
<!-- *************************************************** -->
<!-- XHTML MNEMONIC CHARACTER ENTITIES ................. -->
<!ENTITY % OEBEntitiesPUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.2 Entities//EN""http://openebook.org/dtds/oeb-1.2/oeb12.ent">
%OEBEntities;
<!-- *************************************************** -->
<!-- DATATYPE ENTITIES ................................. -->
<!-- Uniform Resource Identifier (URI), per [RFC2396] -->
<!ENTITY % URI "CDATA">
<!-- Language code, per [RFC3066] -->
<!ENTITY % LanguageCode "NMTOKEN">
<!-- *************************************************** -->
<!-- NAMESPACE ENTITIES ................................ -->
<!ENTITY % dc.xmlns"'http://purl.org/dc/elements/1.1/'">
<!ENTITY % oebpk.xmlns"'http://openebook.org/namespaces/oeb-package/1.0/'">
<!-- *************************************************** -->
<!-- ELEMENT ENTITIES .................................. -->
<!-- The entity 'DCMetadataOpt' includes the 12 optional
<dc:Xxx> children elements of <dc-metadata>. It will
be used in the <dc-metadata> content model. -->
<!ENTITY % DCMetadataOpt"dc:Contributor |
dc:Coverage |
dc:Creator |
dc:Date |
dc:Description |
dc:Format |
dc:Publisher |
dc:Relation |
dc:Rights |
dc:Source |
dc:Subject |
dc:Type ">
<!-- *************************************************** -->
<!-- ATTRIBUTE ENTITIES ................................ -->
<!ENTITY % CoreAttributes"id ID #IMPLIED">
<!ENTITY % InternationalAttributes"xml:lang %LanguageCode; #IMPLIED">
<!ENTITY % CommonAttributes"%CoreAttributes;
%InternationalAttributes;">
<!-- 'DCNamespaceAttribute' is an attribute entity declaring
the Dublin Core namespace. Used on each <dc:Xxx> element
to accommodate XML parsers which unnecessarily require
this. -->
<!ENTITY % DCNamespaceAttribute"xmlns:dc %URI; #FIXED %dc.xmlns;">
<!-- *************************************************** -->
<!-- ELEMENTS AND ATTRIBUTES ........................... -->
<!-- <package> must have as children elements, in this order:
<metadata>, <manifest>, and <spine>, and optionally may
include <tours> and/or <guide>. The 'unique-identifier'
attribute is required for <package> (see comment for
<dc:Identifier>.) -->
<!ELEMENT package (metadata, manifest, spine, tours?, guide?)><!ATTLIST package%CommonAttributes;unique-identifier IDREF #REQUIREDxmlns %URI; #FIXED %oebpk.xmlns;>
<!-- <metadata> must contain one <dc-metadata>, and
optionally contain one <x-metadata>. There are no
attributes for <metadata>. -->
<!ELEMENT metadata (dc-metadata, x-metadata?)>
<!-- <dc-metadata> must contain at least one <dc:Title>,
one <dc:Identifier>, and one <dc:Language>, and may
contain one or more of each of the other twelve
optional <dc:XXX> elements, all in any order. -->
<!ELEMENT dc-metadata( (%DCMetadataOpt;)*,( (dc:Title, (%DCMetadataOpt; | dc:Title)*,( (dc:Identifier, (%DCMetadataOpt; | dc:Title | dc:Identifier)*,dc:Language) |(dc:Language, (%DCMetadataOpt; | dc:Title | dc:Language)*,dc:Identifier) ) ) |(dc:Identifier, (%DCMetadataOpt; | dc:Identifier)*,( (dc:Title, (%DCMetadataOpt; | dc:Identifier | dc:Title)*,dc:Language) |
(dc:Language, (%DCMetadataOpt; | dc:Identifier | dc:Language)*,dc:Title) ) ) |(dc:Language, (%DCMetadataOpt; | dc:Language)*,( (dc:Identifier, (%DCMetadataOpt; | dc:Language | dc:Identifier)*,dc:Title) |(dc:Title, (%DCMetadataOpt; | dc:Language | dc:Title)*,dc:Identifier) ) ) ),(%DCMetadataOpt; | dc:Title | dc:Identifier | dc:Language)* )>
<!ATTLIST dc-metadata%CommonAttributes;%DCNamespaceAttribute;xmlns:oebpackage %URI; #FIXED %oebpk.xmlns;>
<!-- Required elements for <dc-metadata>. -->
<!ELEMENT dc:Title (#PCDATA)><!ATTLIST dc:Title%CommonAttributes;%DCNamespaceAttribute;>
<!-- One <dc:Identifier> must specify an 'id' identical to
the value of the required <package> 'unique-identifier'
attribute. -->
<!ELEMENT dc:Identifier (#PCDATA)><!ATTLIST dc:Identifier%CommonAttributes;%DCNamespaceAttribute;scheme NMTOKEN #IMPLIED>
<!ELEMENT dc:Language (#PCDATA)><!ATTLIST dc:Language%CommonAttributes;%DCNamespaceAttribute;>
<!-- Optional elements for <dc-metadata>. -->
<!ELEMENT dc:Contributor (#PCDATA)><!ATTLIST dc:Contributor%CommonAttributes;%DCNamespaceAttribute;file-as CDATA #IMPLIEDrole NMTOKEN #IMPLIED>
<!ELEMENT dc:Coverage (#PCDATA)><!ATTLIST dc:Coverage%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Creator (#PCDATA)><!ATTLIST dc:Creator%CommonAttributes;%DCNamespaceAttribute;file-as CDATA #IMPLIEDrole NMTOKEN #IMPLIED>
<!ELEMENT dc:Date (#PCDATA)><!ATTLIST dc:Date%CommonAttributes;%DCNamespaceAttribute;event NMTOKEN #IMPLIED>
<!ELEMENT dc:Description (#PCDATA)><!ATTLIST dc:Description%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Format (#PCDATA)><!ATTLIST dc:Format%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Publisher (#PCDATA)><!ATTLIST dc:Publisher%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Relation (#PCDATA)><!ATTLIST dc:Relation%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Rights (#PCDATA)><!ATTLIST dc:Rights%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Source (#PCDATA)><!ATTLIST dc:Source%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Subject (#PCDATA)><!ATTLIST dc:Subject%CommonAttributes;%DCNamespaceAttribute;>
<!ELEMENT dc:Type (#PCDATA)>
<!ATTLIST dc:Type%CommonAttributes;%DCNamespaceAttribute;>
<!-- <x-metadata> must contain at least one <meta>. -->
<!ELEMENT x-metadata (meta+)><!ATTLIST x-metadata %CommonAttributes;>
<!-- Note that 'content' and 'name' are required attributes
for <meta>. -->
<!ELEMENT meta EMPTY><!ATTLIST meta%CommonAttributes;content CDATA #REQUIREDname NMTOKEN #REQUIREDscheme CDATA #IMPLIED>
<!-- <manifest> must contain at least one <item>. -->
<!ELEMENT manifest (item+)><!ATTLIST manifest %CommonAttributes;>
<!-- Note that 'href', 'id' and 'media-type' are required
attributes for <item>. -->
<!ELEMENT item EMPTY><!ATTLIST item%InternationalAttributes;fallback IDREF #IMPLIEDhref %URI; #REQUIREDid ID #REQUIREDmedia-type CDATA #REQUIRED>
<!-- <spine> must contain at least one <itemref>. -->
<!ELEMENT spine (itemref+)><!ATTLIST spine %CommonAttributes;>
<!-- Note that 'idref' is a required attribute for
<itemref>. -->
<!ELEMENT itemref EMPTY><!ATTLIST itemref%CommonAttributes;idref IDREF #REQUIRED>
<!-- <tours> must contain at least one <tour>. -->
<!ELEMENT tours (tour+)><!ATTLIST tours %CommonAttributes;>
<!-- <tour> must contain at least one <site>. Note that
'title' is a required attribute for <tour>. -->
<!ELEMENT tour (site+)><!ATTLIST tour%CommonAttributes;title CDATA #REQUIRED>
<!-- Note that 'href' and 'title' are required attributes
for <site>. -->
<!ELEMENT site EMPTY><!ATTLIST site%CommonAttributes;href %URI; #REQUIREDtitle CDATA #REQUIRED>
<!-- <guide> must contain at least one <reference>. -->
<!ELEMENT guide (reference+)><!ATTLIST guide %CommonAttributes;>
<!-- Note that 'href', 'title' and 'type' are required
attributes for <reference>. -->
<!ELEMENT reference EMPTY><!ATTLIST reference%CommonAttributes;href %URI; #REQUIREDtitle CDATA #REQUIREDtype NMTOKEN #REQUIRED>
@@ -0,0 +1,187 @@
<!-- NCX 2005-1 DTD 2005-06-26
file: ncx-2005-1.dtd
Authors: Mark Hakkinen, George Kerscher, Tom McLaughlin, James Pritchett, and Michael Moodie
Change list:
2002-02-12 M. Moodie. Changed content model of navLabel element to eliminate ambiguity.
2002-02-27 M. Moodie. Grammatical changes suggested by editor.
2004-03-31 J. Pritchett. Various changes per the 2004 change list:
- Changed internal version numbers from 1.1.0 to 1.2.0
- Made audio clipBegin/clipEnd mandatory (change #10)
- Dropped value attribute from navPoint (change #11)
- Replaced lang attribute with xml:lang (change #12)
- Added <pageList> and <pageTarget> elements (change #48)
- Dropped onFocus and onBlur attributes from navPoint and navTarget (change #49)
- Added <img> to content models of docTitle and docAuthor (change #50)
- Removed reference to pages in description of navList (change #52)
- Added <navInfo> element (change #53)
- Added default namespace attribute to description of <ncx> (change #L8)
- Removed pageRef and mapRef attributes
2004-04-05 J. Pritchett. Changes after feedback from MM and MG to 2004-03-31 version
- Changed internal version numbers from 1.2.0 to 1.1.2 (per MM e-mail of 3/31)
- Changed system identifier to use z3986/2004 as path instead of z3986/v100 (per 3/31 con call)
- Added class attribute to both pageTarget and pageList (per MG e-mail of 4/1)
- Added comment text describing value attribute for pageTarget and navTarget (per MM e-mail of 3/31)
- Changed declaration of type attribute on pageTarget to enumerate allowed values
- Added playOrder attribute to navPoint, navTarget, and pageTarget (per Lloyd's proposal)
2004-04-05 T. McLaughlin. In description of smilCustomTest, added id and defaultState are to be copied.
Version update to 1.1.3.
2004-05-14 T. McLaughlin. Reinstated override attribute to be copied also. Added bookStruct attribute
and enum list to smilCustomTest. Update to 1.1.4.
Revised, 4/5/2004: Changed version to 1.1.2
Revised, 4/5/2004: Changed system identifier to use '2004' path
Revised, 4/5/2004: TM, Changed version to 1.1.3
Revised, 5/14/2004: TM, Changed version to 1.1.4
2004-07-07 M. Moodie Updated version to 1.2.0 everywhere but at top, where version was set to 1.1.5.
2004-09-15 M. Moodie. Changed uri to URI throughout. Set version to 1.1.6.
2004-09-16 M. Moodie. Changed version to 1.2.0
2005-06-26 M. Gylling. Changed pid, sid, ns uri, and filename for Z3986-2005
Description:
NCX (Navigation Control for XML applications) is a generalized navigation definition DTD for application
to Digital Talking Books, eBooks, and general web content models.
This DTD is an XML application that layers navigation functionality on top of SMIL 2.0 content.
The NCX defines a navigation path/model that may be applied upon existing publications,
without modification of the existing publication source, so long as the navigation targets within
the source publication can be directly referenced via a URI.
The following identifiers apply to this DTD:
"-//NISO//DTD ncx 2005-1//EN"
"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd"
-->
<!-- Basic Entities -->
<!ENTITY % i18n"xml:lang NMTOKEN #IMPLIED
dir (ltr|rtl) #IMPLIED" >
<!ENTITY % SMILtimeVal "CDATA" ><!ENTITY % URI "CDATA" ><!ENTITY % script "CDATA" >
<!-- ELEMENTS -->
<!-- Top Level NCX Container. -->
<!-- Revised, 3/31/2004: Added pageList to content model -->
<!ELEMENT ncx (head, docTitle, docAuthor*, navMap, pageList?, navList*)><!-- Revised, 4/5/2004: Changed version to 1.1.2 -->
<!-- Revised 3/29/2004: Added xmlns -->
<!-- Revised, 4/5/2004: TM, Changed version to 1.1.3 -->
<!-- Revised, 5/14/2004: TM, Changed version to 1.1.4 -->
<!-- Revised, 7/7/2004: MM, Changed version to 1.2.0 -->
<!ATTLIST ncxversion CDATA #FIXED "2005-1"xmlns %URI; #FIXED "http://www.daisy.org/z3986/2005/ncx/"%i18n;>
<!-- Document Head - Contains all NCX metadata.
-->
<!ELEMENT head (smilCustomTest | meta)+>
<!-- 2004-04-05 TM - only id and defaultState are copied -->
<!-- 2004-05-14 TM - revert to override copied too; added bookStruct attribute -->
<!-- smilCustomTest - Duplicates customTest data found in SMIL files. Each unique customTest
element that appears in one or more SMIL files must have its id, defaultState and override
attributes duplicated in a smilCustomTest element in the NCX. The NCX thus gathers in one
place all customTest elements used in the SMIL files, for presentation to the user.
-->
<!ELEMENT smilCustomTest EMPTY><!ATTLIST smilCustomTestid ID #REQUIREDdefaultState (true|false) 'false'override (visible|hidden) 'hidden'bookStruct (PAGE_NUMBER|NOTE|NOTE_REFERENCE|ANNOTATION|LINE_NUMBER|OPTIONAL_SIDEBAR|OPTIONAL_PRODUCER_NOTE) #IMPLIED>
<!-- Meta Element - metadata about this NCX -->
<!ELEMENT meta EMPTY><!ATTLIST metaname CDATA #REQUIREDcontent CDATA #REQUIREDscheme CDATA #IMPLIED>
<!-- DocTitle - the title of the document, required and must immediately follow head.
-->
<!-- Revised, 3/31/2004: Added img to content model -->
<!ELEMENT docTitle (text, audio?, img?)><!ATTLIST docTitleid ID #IMPLIED%i18n;>
<!-- DocAuthor - the author of the document, immediately follows docTitle.
-->
<!-- Revised, 3/31/2004: Added img to content model -->
<!ELEMENT docAuthor (text, audio?, img?)><!ATTLIST docAuthorid ID #IMPLIED%i18n;>
<!-- Navigation Structure - container for all of the NCX objects that are part of the
hierarchical structure of the document.
-->
<!-- Revised, 3/31/2004: Added navInfo to content model -->
<!ELEMENT navMap (navInfo*, navLabel*, navPoint+)><!ATTLIST navMapid ID #IMPLIED>
<!-- Navigation Point - contains description(s) of target, as well as a pointer to
entire content of target.
Hierarchy is represented by nesting navPoints. "class" attribute describes the kind
of structural unit this object represents (e.g., "chapter", "section").
-->
<!ELEMENT navPoint (navLabel+, content, navPoint*)><!-- Revised, 3/29/2004: Removed onFocus/onBlur -->
<!-- Revised, 3/29/2004: Removed value -->
<!-- Revised, 3/31/2004: Removed pageRef -->
<!-- Revised, 4/5/2004: Added playOrder -->
<!ATTLIST navPointid ID #REQUIREDclass CDATA #IMPLIEDplayOrder CDATA #REQUIRED>
<!-- Revised, 3/31/2004: Added pageList element -->
<!-- Page List - Container for pagination information.
-->
<!ELEMENT pageList (navInfo*, navLabel*, pageTarget+)><!-- Revised, 4/5/2004: Added class attribute -->
<!ATTLIST pageListid ID #IMPLIEDclass CDATA #IMPLIED>
<!-- Revised, 3/31/2004: Added pageTarget element -->
<!-- Revised, 4/5/2004: Added description of value attribute to comment -->
<!-- Page Target - Container for
text, audio, image, and content elements containing navigational
information for pages. The "value" attribute is a positive integer representing
the numeric value associated with a page. Combination of values of type and
value attributes must be unique, when value attribute is present.
-->
<!ELEMENT pageTarget (navLabel+, content)><!-- Revised, 4/5/2004: Added class attribute -->
<!-- Revised, 4/5/2004: Changed declaration of type attribute to enumerate values -->
<!-- Revised, 4/5/2004: Added playOrder -->
<!ATTLIST pageTargetid ID #IMPLIEDvalue CDATA #IMPLIEDtype (front | normal | special) #REQUIREDclass CDATA #IMPLIEDplayOrder CDATA #REQUIRED>
<!-- Navigation List - container for distinct, flat sets of navigable elements, e.g.
notes, figures, tables, etc. Essentially a flat version of navMap. The "class" attribute
describes the type of object contained in this navList, using dtbook element names, e.g., note.
-->
<!-- Revised, 3/31/2004: Added navInfo to content model -->
<!ELEMENT navList (navInfo*, navLabel+, navTarget+) ><!ATTLIST navListid ID #IMPLIEDclass CDATA #IMPLIED>
<!-- Revised, 4/5/2004: Added description of value attribute to comment -->
<!-- Navigation Target - contains description(s) of target, as well as a pointer to
entire content of target.
navTargets are the equivalent of navPoints for use in navLists. "class" attribute
describes the kind of structure this target represents, using its dtbook element
name, e.g., note. The "value" attribute is a positive integer representing the
numeric value associated with the navTarget.
-->
<!ELEMENT navTarget (navLabel+, content) ><!-- Revised, 3/29/2004: Removed onFocus/onBlur -->
<!-- Revised, 3/31/2004: Removed mapRef -->
<!-- Revised, 4/5/2004: Added playOrder -->
<!ATTLIST navTargetid ID #REQUIREDclass CDATA #IMPLIEDvalue CDATA #IMPLIEDplayOrder CDATA #REQUIRED>
<!-- Revised, 3/31/2004: Added navInfo element -->
<!-- Navigation Information - Contains an informative comment
about a navMap, pageList, or navList in various media for presentation to the user.
-->
<!ELEMENT navInfo (((text, audio?) | audio), img?)><!ATTLIST navInfo%i18n;>
<!-- Navigation Label - Contains a description of a given <navMap>, <navPoint>,
<navList>, or <navTarget> in various media for presentation to the user. Can be
repeated so descriptions can be provided in multiple languages. -->
<!ELEMENT navLabel (((text, audio?) | audio), img?)><!ATTLIST navLabel%i18n;>
<!-- Content Element - pointer into SMIL to beginning of navPoint. -->
<!ELEMENT content EMPTY><!ATTLIST contentid ID #IMPLIEDsrc %URI; #REQUIRED>
<!-- Text Element - Contains text of docTitle, navPoint heading, navTarget (e.g., page number),
or label for navMap or navList. -->
<!ELEMENT text (#PCDATA)><!ATTLIST textid ID #IMPLIEDclass CDATA #IMPLIED>
<!-- Audio Element - audio clip of navPoint heading. -->
<!ELEMENT audio EMPTY><!-- Revised, 3/29/2004: clipBegin/clipEnd now REQUIRED -->
<!ATTLIST audioid ID #IMPLIEDclass CDATA #IMPLIEDsrc %URI; #REQUIREDclipBegin %SMILtimeVal; #REQUIREDclipEnd %SMILtimeVal; #REQUIRED>
<!-- Image Element - image that may accompany heading. -->
<!ELEMENT img EMPTY><!ATTLIST imgid ID #IMPLIEDclass CDATA #IMPLIEDsrc %URI; #REQUIRED>
@@ -0,0 +1,142 @@
<!-- ...................................................................... -->
<!-- XHTML Ruby Module .................................................... -->
<!-- file: xhtml-ruby-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1999-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
Revision: $Id: xhtml-ruby-1.mod,v 4.0 2001/04/03 23:14:33 altheim Exp $
This module is based on the W3C Ruby Annotation Specification:
http://www.w3.org/TR/ruby
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Ruby 1.0//EN"
SYSTEM "http://www.w3.org/TR/ruby/xhtml-ruby-1.mod"
...................................................................... -->
<!-- Ruby Elements
ruby, rbc, rtc, rb, rt, rp
This module declares the elements and their attributes used to
support ruby annotation markup.
-->
<!-- declare qualified element type names:
-->
<!ENTITY % ruby.qname "ruby" ><!ENTITY % rbc.qname "rbc" ><!ENTITY % rtc.qname "rtc" ><!ENTITY % rb.qname "rb" ><!ENTITY % rt.qname "rt" ><!ENTITY % rp.qname "rp" >
<!-- rp fallback is included by default.
-->
<!ENTITY % Ruby.fallback "INCLUDE" ><!ENTITY % Ruby.fallback.mandatory "IGNORE" >
<!-- Complex ruby is included by default; it may be
overridden by other modules to ignore it.
-->
<!ENTITY % Ruby.complex "INCLUDE" >
<!-- Fragments for the content model of the ruby element -->
<![%Ruby.fallback;[<![%Ruby.fallback.mandatory;[<!ENTITY % Ruby.content.simple"( %rb.qname;, %rp.qname;, %rt.qname;, %rp.qname; )">]]><!ENTITY % Ruby.content.simple"( %rb.qname;, ( %rt.qname; | ( %rp.qname;, %rt.qname;, %rp.qname; ) ) )">]]><!ENTITY % Ruby.content.simple "( %rb.qname;, %rt.qname; )" >
<![%Ruby.complex;[<!ENTITY % Ruby.content.complex"| ( %rbc.qname;, %rtc.qname;, %rtc.qname;? )">]]><!ENTITY % Ruby.content.complex "" >
<!-- Content models of the rb and the rt elements are intended to
allow other inline-level elements of its parent markup language,
but it should not include ruby descendent elements. The following
parameter entity %NoRuby.content; can be used to redefine
those content models with minimum effort. It's defined as
'( #PCDATA )' by default.
-->
<!ENTITY % NoRuby.content "( #PCDATA )" >
<!-- one or more digits (NUMBER) -->
<!ENTITY % Number.datatype "CDATA" >
<!-- ruby element ...................................... -->
<!ENTITY % ruby.element "INCLUDE" ><![%ruby.element;[<!ENTITY % ruby.content"( %Ruby.content.simple; %Ruby.content.complex; )"><!ELEMENT %ruby.qname; %ruby.content; ><!-- end of ruby.element -->]]>
<![%Ruby.complex;[<!-- rbc (ruby base component) element ................. -->
<!ENTITY % rbc.element "INCLUDE" ><![%rbc.element;[<!ENTITY % rbc.content"(%rb.qname;)+"><!ELEMENT %rbc.qname; %rbc.content; ><!-- end of rbc.element -->]]>
<!-- rtc (ruby text component) element ................. -->
<!ENTITY % rtc.element "INCLUDE" ><![%rtc.element;[<!ENTITY % rtc.content"(%rt.qname;)+"><!ELEMENT %rtc.qname; %rtc.content; ><!-- end of rtc.element -->]]>]]>
<!-- rb (ruby base) element ............................ -->
<!ENTITY % rb.element "INCLUDE" ><![%rb.element;[<!-- %rb.content; uses %NoRuby.content; as its content model,
which is '( #PCDATA )' by default. It may be overridden
by other modules to allow other inline-level elements
of its parent markup language, but it should not include
ruby descendent elements.
-->
<!ENTITY % rb.content "%NoRuby.content;" ><!ELEMENT %rb.qname; %rb.content; ><!-- end of rb.element -->]]>
<!-- rt (ruby text) element ............................ -->
<!ENTITY % rt.element "INCLUDE" ><![%rt.element;[<!-- %rt.content; uses %NoRuby.content; as its content model,
which is '( #PCDATA )' by default. It may be overridden
by other modules to allow other inline-level elements
of its parent markup language, but it should not include
ruby descendent elements.
-->
<!ENTITY % rt.content "%NoRuby.content;" >
<!ELEMENT %rt.qname; %rt.content; ><!-- end of rt.element -->]]>
<!-- rbspan attribute is used for complex ruby only ...... -->
<![%Ruby.complex;[<!ENTITY % rt.attlist "INCLUDE" ><![%rt.attlist;[<!ATTLIST %rt.qname;rbspan %Number.datatype; "1"><!-- end of rt.attlist -->]]>]]>
<!-- rp (ruby parenthesis) element ..................... -->
<![%Ruby.fallback;[<!ENTITY % rp.element "INCLUDE" ><![%rp.element;[<!ENTITY % rp.content"( #PCDATA )"><!ELEMENT %rp.qname; %rp.content; ><!-- end of rp.element -->]]>]]>
<!-- Ruby Common Attributes
The following optional ATTLIST declarations provide an easy way
to define common attributes for ruby elements. These declarations
are ignored by default.
Ruby elements are intended to have common attributes of its
parent markup language. For example, if a markup language defines
common attributes as a parameter entity %attrs;, you may add
those attributes by just declaring the following parameter entities
<!ENTITY % Ruby.common.attlists "INCLUDE" >
<!ENTITY % Ruby.common.attrib "%attrs;" >
before including the Ruby module.
-->
<!ENTITY % Ruby.common.attlists "IGNORE" ><![%Ruby.common.attlists;[<!ENTITY % Ruby.common.attrib "" >
<!-- common attributes for ruby ........................ -->
<!ENTITY % Ruby.common.attlist "INCLUDE" ><![%Ruby.common.attlist;[<!ATTLIST %ruby.qname;%Ruby.common.attrib;><!-- end of Ruby.common.attlist -->]]>
<![%Ruby.complex;[<!-- common attributes for rbc ......................... -->
<!ENTITY % Rbc.common.attlist "INCLUDE" ><![%Rbc.common.attlist;[<!ATTLIST %rbc.qname;%Ruby.common.attrib;><!-- end of Rbc.common.attlist -->]]>
<!-- common attributes for rtc ......................... -->
<!ENTITY % Rtc.common.attlist "INCLUDE" ><![%Rtc.common.attlist;[<!ATTLIST %rtc.qname;%Ruby.common.attrib;><!-- end of Rtc.common.attlist -->]]>]]>
<!-- common attributes for rb .......................... -->
<!ENTITY % Rb.common.attlist "INCLUDE" ><![%Rb.common.attlist;[<!ATTLIST %rb.qname;%Ruby.common.attrib;><!-- end of Rb.common.attlist -->]]>
<!-- common attributes for rt .......................... -->
<!ENTITY % Rt.common.attlist "INCLUDE" ><![%Rt.common.attlist;[<!ATTLIST %rt.qname;%Ruby.common.attrib;><!-- end of Rt.common.attlist -->]]>
<![%Ruby.fallback;[<!-- common attributes for rp .......................... -->
<!ENTITY % Rp.common.attlist "INCLUDE" ><![%Rp.common.attlist;[<!ATTLIST %rp.qname;%Ruby.common.attrib;><!-- end of Rp.common.attlist -->]]>]]>]]>
<!-- end of xhtml-ruby-1.mod -->
@@ -0,0 +1,36 @@
<!-- ...................................................................... -->
<!-- XHTML Base Architecture Module ...................................... -->
<!-- file: xhtml-arch-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-arch-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Base Architecture 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-arch-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- This optional module includes declarations that enable XHTML to be used
as a base architecture according to the 'Architectural Forms Definition
Requirements' (Annex A.3, ISO/IEC 10744, 2nd edition). For more information
on use of architectural forms, see the HyTime web site at:
http://www.hytime.org/
-->
<?IS10744 ArcBase xhtml ?>
<!NOTATION xhtml PUBLIC "-//W3C//NOTATION AFDR ARCBASE XHTML 1.1//EN" >
<!-- Entity declaration for associated Architectural DTD
-->
<!ENTITY xhtml-arch.dtdPUBLIC "-//W3C//DTD XHTML Architecture 1.1//EN""xhtml11-arch.dtd" >
<?IS10744:arch xhtmlpublic-id = "-//W3C//NOTATION AFDR ARCBASE XHTML 1.1//EN"dtd-public-id = "-//W3C//DTD XHTML 1.1//EN"dtd-system-id = "xhtml11.dtd"doc-elem-form = "html"form-att = "html"renamer-att = "htnames"suppressor-att = "htsupp"data-ignore-att = "htign"auto = "ArcAuto"options = "HtModReq HtModOpt"HtModReq = "Framework Text Hypertext Lists Structure"HtModOpt = "Standard"?>
<!-- end of xhtml-arch-1.mod -->
@@ -0,0 +1,96 @@
<!-- ...................................................................... -->
<!-- XHTML Common Attributes Module ...................................... -->
<!-- file: xhtml-attribs-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-attribs-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ENTITIES XHTML Common Attributes 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-attribs-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Common Attributes
This module declares many of the common attributes for the XHTML DTD.
%NS.decl.attrib; is declared in the XHTML Qname module.
Note that this file was extended in XHTML Modularization 1.1 to
include declarations of "global" versions of the attribute collections.
The global versions of the attributes are for use on elements in other
namespaces. The global version of "common" includes the xmlns declaration
for the prefixed version of the xhtml namespace. If you are only using a
specific attribute or an individual attribute collection, you must also
include the XHTML.xmlns.attrib.prefixed PE on your elements.
-->
<!ENTITY % id.attrib"id ID #IMPLIED">
<![%XHTML.global.attrs.prefixed;[<!ENTITY % XHTML.global.id.attrib"%XHTML.prefix;:id ID #IMPLIED">]]>
<!ENTITY % class.attrib"class CDATA #IMPLIED">
<![%XHTML.global.attrs.prefixed;[<!ENTITY % XHTML.global.class.attrib"%XHTML.prefix;:class CDATA #IMPLIED">]]>
<!ENTITY % title.attrib"title %Text.datatype; #IMPLIED">
<![%XHTML.global.attrs.prefixed;[<!ENTITY % XHTML.global.title.attrib"%XHTML.prefix;:title %Text.datatype; #IMPLIED">]]>
<!ENTITY % Core.extra.attrib "" >
<!ENTITY % Core.attrib"%XHTML.xmlns.attrib;
%id.attrib;
%class.attrib;
%title.attrib;
xml:space ( preserve ) #FIXED 'preserve'
%Core.extra.attrib;">
<!ENTITY % XHTML.global.core.extra.attrib "" >
<![%XHTML.global.attrs.prefixed;[
<!ENTITY % XHTML.global.core.attrib"%XHTML.global.id.attrib;
%XHTML.global.class.attrib;
%XHTML.global.title.attrib;
%XHTML.global.core.extra.attrib;">]]>
<!ENTITY % XHTML.global.core.attrib "" >
<!ENTITY % lang.attrib"xml:lang %LanguageCode.datatype; #IMPLIED">
<![%XHTML.bidi;[<!ENTITY % dir.attrib"dir ( ltr | rtl ) #IMPLIED">
<!ENTITY % I18n.attrib"%dir.attrib;
%lang.attrib;">
<![%XHTML.global.attrs.prefixed;[<!ENTITY XHTML.global.i18n.attrib"%XHTML.prefix;:dir ( ltr | rtl ) #IMPLIED
%lang.attrib;">]]><!ENTITY XHTML.global.i18n.attrib "" >
]]><!ENTITY % I18n.attrib"%lang.attrib;"><!ENTITY % XHTML.global.i18n.attrib"%lang.attrib;">
<!ENTITY % Common.extra.attrib "" ><!ENTITY % XHTML.global.common.extra.attrib "" >
<!-- intrinsic event attributes declared previously
-->
<!ENTITY % Events.attrib "" >
<!ENTITY % XHTML.global.events.attrib "" >
<!ENTITY % Common.attrib"%Core.attrib;
%I18n.attrib;
%Events.attrib;
%Common.extra.attrib;">
<!ENTITY % XHTML.global.common.attrib"%XHTML.xmlns.attrib.prefixed;
%XHTML.global.core.attrib;
%XHTML.global.i18n.attrib;
%XHTML.global.events.attrib;
%XHTML.global.common.extra.attrib;">
<!-- end of xhtml-attribs-1.mod -->
@@ -0,0 +1,40 @@
<!-- ...................................................................... -->
<!-- XHTML Base Element Module ........................................... -->
<!-- file: xhtml-base-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-base-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Base Element 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-base-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Base element
base
This module declares the base element type and its attributes,
used to define a base URI against which relative URIs in the
document will be resolved.
Note that this module also redeclares the content model for
the head element to include the base element.
-->
<!-- base: Document Base URI ........................... -->
<!ENTITY % base.element "INCLUDE" ><![%base.element;[<!ENTITY % base.content "EMPTY" ><!ENTITY % base.qname "base" ><!ELEMENT %base.qname; %base.content; ><!-- end of base.element -->]]>
<!ENTITY % base.attlist "INCLUDE" ><![%base.attlist;[<!ATTLIST %base.qname;%XHTML.xmlns.attrib;href %URI.datatype; #REQUIRED><!-- end of base.attlist -->]]>
<!ENTITY % head.content"( %HeadOpts.mix;,
( ( %title.qname;, %HeadOpts.mix;, ( %base.qname;, %HeadOpts.mix; )? )
| ( %base.qname;, %HeadOpts.mix;, ( %title.qname;, %HeadOpts.mix; ))))">
<!-- end of xhtml-base-1.mod -->
@@ -0,0 +1,33 @@
<!-- ...................................................................... -->
<!-- XHTML BDO Element Module ............................................. -->
<!-- file: xhtml-bdo-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-bdo-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML BDO Element 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-bdo-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Bidirectional Override (bdo) Element
This modules declares the element 'bdo', used to override the
Unicode bidirectional algorithm for selected fragments of text.
DEPENDENCIES:
Relies on the conditional section keyword %XHTML.bidi; declared
as "INCLUDE". Bidirectional text support includes both the bdo
element and the 'dir' attribute.
-->
<!ENTITY % bdo.element "INCLUDE" ><![%bdo.element;[<!ENTITY % bdo.content"( #PCDATA | %Inline.mix; )*"><!ENTITY % bdo.qname "bdo" ><!ELEMENT %bdo.qname; %bdo.content; ><!-- end of bdo.element -->]]>
<!ENTITY % bdo.attlist "INCLUDE" ><![%bdo.attlist;[<!ATTLIST %bdo.qname;%Core.attrib; %lang.attrib;dir ( ltr | rtl ) #REQUIRED>]]>
<!-- end of xhtml-bdo-1.mod -->
@@ -0,0 +1,74 @@
<!-- ...................................................................... -->
<!-- XHTML Block Phrasal Module .......................................... -->
<!-- file: xhtml-blkphras-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-blkphras-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Block Phrasal 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-blkphras-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Block Phrasal
address, blockquote, pre, h1, h2, h3, h4, h5, h6
This module declares the elements and their attributes used to
support block-level phrasal markup.
-->
<!ENTITY % address.element "INCLUDE" ><![%address.element;[<!ENTITY % address.content"( #PCDATA | %Inline.mix; )*" ><!ENTITY % address.qname "address" ><!ELEMENT %address.qname; %address.content; ><!-- end of address.element -->]]>
<!ENTITY % address.attlist "INCLUDE" ><![%address.attlist;[<!ATTLIST %address.qname;%Common.attrib;><!-- end of address.attlist -->]]>
<!ENTITY % blockquote.element "INCLUDE" ><![%blockquote.element;[<!ENTITY % blockquote.content"( %Block.mix; )*"><!ENTITY % blockquote.qname "blockquote" ><!ELEMENT %blockquote.qname; %blockquote.content; ><!-- end of blockquote.element -->]]>
<!ENTITY % blockquote.attlist "INCLUDE" ><![%blockquote.attlist;[<!ATTLIST %blockquote.qname;%Common.attrib;cite %URI.datatype; #IMPLIED><!-- end of blockquote.attlist -->]]>
<!ENTITY % pre.element "INCLUDE" ><![%pre.element;[<!ENTITY % pre.content"( #PCDATA
| %InlStruct.class;
%InlPhras.class;
| %tt.qname; | %i.qname; | %b.qname;
%I18n.class;
%Anchor.class;
| %map.qname;
%Misc.class;
%Inline.extra; )*"><!ENTITY % pre.qname "pre" ><!ELEMENT %pre.qname; %pre.content; ><!-- end of pre.element -->]]>
<!ENTITY % pre.attlist "INCLUDE" ><![%pre.attlist;[<!ATTLIST %pre.qname;%Common.attrib;><!-- end of pre.attlist -->]]>
<!-- ................... Heading Elements ................... -->
<!ENTITY % Heading.content "( #PCDATA | %Inline.mix; )*" >
<!ENTITY % h1.element "INCLUDE" ><![%h1.element;[<!ENTITY % h1.qname "h1" ><!ELEMENT %h1.qname; %Heading.content; ><!-- end of h1.element -->]]>
<!ENTITY % h1.attlist "INCLUDE" ><![%h1.attlist;[<!ATTLIST %h1.qname;%Common.attrib;><!-- end of h1.attlist -->]]>
<!ENTITY % h2.element "INCLUDE" ><![%h2.element;[<!ENTITY % h2.qname "h2" ><!ELEMENT %h2.qname; %Heading.content; ><!-- end of h2.element -->]]>
<!ENTITY % h2.attlist "INCLUDE" ><![%h2.attlist;[<!ATTLIST %h2.qname;%Common.attrib;><!-- end of h2.attlist -->]]>
<!ENTITY % h3.element "INCLUDE" ><![%h3.element;[<!ENTITY % h3.qname "h3" ><!ELEMENT %h3.qname; %Heading.content; ><!-- end of h3.element -->]]>
<!ENTITY % h3.attlist "INCLUDE" ><![%h3.attlist;[<!ATTLIST %h3.qname;%Common.attrib;><!-- end of h3.attlist -->]]>
<!ENTITY % h4.element "INCLUDE" ><![%h4.element;[<!ENTITY % h4.qname "h4" ><!ELEMENT %h4.qname; %Heading.content; ><!-- end of h4.element -->]]>
<!ENTITY % h4.attlist "INCLUDE" ><![%h4.attlist;[<!ATTLIST %h4.qname;%Common.attrib;><!-- end of h4.attlist -->]]>
<!ENTITY % h5.element "INCLUDE" ><![%h5.element;[<!ENTITY % h5.qname "h5" ><!ELEMENT %h5.qname; %Heading.content; ><!-- end of h5.element -->]]>
<!ENTITY % h5.attlist "INCLUDE" ><![%h5.attlist;[<!ATTLIST %h5.qname;%Common.attrib;><!-- end of h5.attlist -->]]>
<!ENTITY % h6.element "INCLUDE" ><![%h6.element;[<!ENTITY % h6.qname "h6" ><!ELEMENT %h6.qname; %Heading.content; ><!-- end of h6.element -->]]>
<!ENTITY % h6.attlist "INCLUDE" ><![%h6.attlist;[<!ATTLIST %h6.qname;%Common.attrib;><!-- end of h6.attlist -->]]>
<!-- end of xhtml-blkphras-1.mod -->
@@ -0,0 +1,30 @@
<!-- ...................................................................... -->
<!-- XHTML Block Presentation Module ..................................... -->
<!-- file: xhtml-blkpres-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-blkpres-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Block Presentation 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-blkpres-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Block Presentational Elements
hr
This module declares the elements and their attributes used to
support block-level presentational markup.
-->
<!ENTITY % hr.element "INCLUDE" ><![%hr.element;[<!ENTITY % hr.content "EMPTY" ><!ENTITY % hr.qname "hr" ><!ELEMENT %hr.qname; %hr.content; ><!-- end of hr.element -->]]>
<!ENTITY % hr.attlist "INCLUDE" ><![%hr.attlist;[<!ATTLIST %hr.qname;%Common.attrib;><!-- end of hr.attlist -->]]>
<!-- end of xhtml-blkpres-1.mod -->
@@ -0,0 +1,34 @@
<!-- ...................................................................... -->
<!-- XHTML Block Structural Module ....................................... -->
<!-- file: xhtml-blkstruct-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-blkstruct-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Block Structural 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-blkstruct-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Block Structural
div, p
This module declares the elements and their attributes used to
support block-level structural markup.
-->
<!ENTITY % div.element "INCLUDE" ><![%div.element;[<!ENTITY % div.content"( #PCDATA | %Flow.mix; )*"><!ENTITY % div.qname "div" ><!ELEMENT %div.qname; %div.content; ><!-- end of div.element -->]]>
<!ENTITY % div.attlist "INCLUDE" ><![%div.attlist;[<!ATTLIST %div.qname;%Common.attrib;><!-- end of div.attlist -->]]>
<!ENTITY % p.element "INCLUDE" ><![%p.element;[<!ENTITY % p.content"( #PCDATA | %Inline.mix; )*" ><!ENTITY % p.qname "p" ><!ELEMENT %p.qname; %p.content; ><!-- end of p.element -->]]>
<!ENTITY % p.attlist "INCLUDE" ><![%p.attlist;[<!ATTLIST %p.qname;%Common.attrib;><!-- end of p.attlist -->]]>
<!-- end of xhtml-blkstruct-1.mod -->
@@ -0,0 +1,30 @@
<!-- ...................................................................... -->
<!-- XHTML Character Entities Module ......................................... -->
<!-- file: xhtml-charent-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-charent-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ENTITIES XHTML Character Entities 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-charent-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Character Entities for XHTML
This module declares the set of character entities for XHTML,
including the Latin 1, Symbol and Special character collections.
-->
<!ENTITY % xhtml-lat1PUBLIC "-//W3C//ENTITIES Latin 1 for XHTML//EN""xhtml-lat1.ent" >%xhtml-lat1;
<!ENTITY % xhtml-symbolPUBLIC "-//W3C//ENTITIES Symbols for XHTML//EN""xhtml-symbol.ent" >%xhtml-symbol;
<!ENTITY % xhtml-specialPUBLIC "-//W3C//ENTITIES Special for XHTML//EN""xhtml-special.ent" >%xhtml-special;
<!-- end of xhtml-charent-1.mod -->
@@ -0,0 +1,69 @@
<!-- ...................................................................... -->
<!-- XHTML Client-side Image Map Module .................................. -->
<!-- file: xhtml-csismap-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-csismap-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Client-side Image Maps 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-csismap-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Client-side Image Maps
area, map
This module declares elements and attributes to support client-side
image maps. This requires that the Image Module (or a module
declaring the img element type) be included in the DTD.
These can be placed in the same document or grouped in a
separate document, although the latter isn't widely supported
-->
<!ENTITY % area.element "INCLUDE" ><![%area.element;[<!ENTITY % area.content "EMPTY" ><!ENTITY % area.qname "area" ><!ELEMENT %area.qname; %area.content; ><!-- end of area.element -->]]>
<!ENTITY % Shape.datatype "( rect | circle | poly | default )"><!ENTITY % Coords.datatype "CDATA" >
<!ENTITY % area.attlist "INCLUDE" ><![%area.attlist;[<!ATTLIST %area.qname;%Common.attrib;href %URI.datatype; #IMPLIEDshape %Shape.datatype; 'rect'coords %Coords.datatype; #IMPLIEDnohref ( nohref ) #IMPLIEDalt %Text.datatype; #REQUIREDtabindex %Number.datatype; #IMPLIEDaccesskey %Character.datatype; #IMPLIED><!-- end of area.attlist -->]]>
<!-- modify anchor attribute definition list
to allow for client-side image maps
-->
<!ATTLIST %a.qname;shape %Shape.datatype; 'rect'coords %Coords.datatype; #IMPLIED>
<!-- modify img attribute definition list
to allow for client-side image maps
-->
<!ATTLIST %img.qname;usemap %URIREF.datatype; #IMPLIED>
<!-- modify form input attribute definition list
to allow for client-side image maps
-->
<!ATTLIST %input.qname;usemap %URIREF.datatype; #IMPLIED>
<!-- modify object attribute definition list
to allow for client-side image maps
-->
<!ATTLIST %object.qname;usemap %URIREF.datatype; #IMPLIED>
<!-- 'usemap' points to the 'id' attribute of a <map> element,
which must be in the same document; support for external
document maps was not widely supported in HTML and is
eliminated in XHTML.
It is considered an error for the element pointed to by
a usemap URIREF to occur in anything but a <map> element.
-->
<!ENTITY % map.element "INCLUDE" ><![%map.element;[<!ENTITY % map.content"(( %Block.mix; ) | %area.qname; )+"><!ENTITY % map.qname "map" ><!ELEMENT %map.qname; %map.content; ><!-- end of map.element -->]]>
<!ENTITY % map.attlist "INCLUDE" ><![%map.attlist;[<!ATTLIST %map.qname;%XHTML.xmlns.attrib;id ID #REQUIRED%class.attrib;%title.attrib;%Core.extra.attrib;%I18n.attrib;%Events.attrib;><!-- end of map.attlist -->]]>
<!-- end of xhtml-csismap-1.mod -->
@@ -0,0 +1,97 @@
<!-- ...................................................................... -->
<!-- XHTML Datatypes Module .............................................. -->
<!-- file: xhtml-datatypes-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-datatypes-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ENTITIES XHTML Datatypes 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-datatypes-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Datatypes
defines containers for the following datatypes, many of
these imported from other specifications and standards.
-->
<!-- Length defined for cellpadding/cellspacing -->
<!-- nn for pixels or nn% for percentage length -->
<!ENTITY % Length.datatype "CDATA" >
<!-- space-separated list of link types -->
<!ENTITY % LinkTypes.datatype "NMTOKENS" >
<!-- single or comma-separated list of media descriptors -->
<!ENTITY % MediaDesc.datatype "CDATA" >
<!-- pixel, percentage, or relative -->
<!ENTITY % MultiLength.datatype "CDATA" >
<!-- one or more digits (NUMBER) -->
<!ENTITY % Number.datatype "CDATA" >
<!-- integer representing length in pixels -->
<!ENTITY % Pixels.datatype "CDATA" >
<!-- script expression -->
<!ENTITY % Script.datatype "CDATA" >
<!-- textual content -->
<!ENTITY % Text.datatype "CDATA" >
<!-- Placeholder Compact URI-related types -->
<!ENTITY % CURIE.datatype "CDATA" ><!ENTITY % CURIEs.datatype "CDATA" ><!ENTITY % SafeCURIE.datatype "CDATA" ><!ENTITY % SafeCURIEs.datatype "CDATA" ><!ENTITY % URIorSafeCURIE.datatype "CDATA" ><!ENTITY % URIorSafeCURIEs.datatype "CDATA" >
<!-- Imported Datatypes ................................ -->
<!-- a single character from [ISO10646] -->
<!ENTITY % Character.datatype "CDATA" >
<!-- a character encoding, as per [RFC2045] -->
<!ENTITY % Charset.datatype "CDATA" >
<!-- a space separated list of character encodings, as per [RFC2045] -->
<!ENTITY % Charsets.datatype "CDATA" >
<!-- Color specification using color name or sRGB (#RRGGBB) values -->
<!ENTITY % Color.datatype "CDATA" >
<!-- media type, as per [RFC2045] -->
<!ENTITY % ContentType.datatype "CDATA" >
<!-- comma-separated list of media types, as per [RFC2045] -->
<!ENTITY % ContentTypes.datatype "CDATA" >
<!-- date and time information. ISO date format -->
<!ENTITY % Datetime.datatype "CDATA" >
<!-- formal public identifier, as per [ISO8879] -->
<!ENTITY % FPI.datatype "CDATA" >
<!-- a language code, as per [RFC3066] or its successor -->
<!ENTITY % LanguageCode.datatype "CDATA" >
<!-- a comma separated list of language code ranges -->
<!ENTITY % LanguageCodes.datatype "CDATA" >
<!-- a qualified name , as per [XMLNS] or its successor -->
<!ENTITY % QName.datatype "CDATA" ><!ENTITY % QNames.datatype "CDATA" >
<!-- a Uniform Resource Identifier, see [URI] -->
<!ENTITY % URI.datatype "CDATA" >
<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->
<!ENTITY % URIs.datatype "CDATA" >
<!-- a relative URI reference consisting of an initial '#' and a fragment ID -->
<!ENTITY % URIREF.datatype "CDATA" >
<!-- end of xhtml-datatypes-1.mod -->
@@ -0,0 +1,103 @@
<!-- ...................................................................... -->
<!-- XHTML Datatypes Module .............................................. -->
<!-- file: xhtml-datatypes-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-datatypes-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ENTITIES XHTML Datatypes 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-datatypes-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Datatypes
defines containers for the following datatypes, many of
these imported from other specifications and standards.
-->
<!-- Length defined for cellpadding/cellspacing -->
<!-- nn for pixels or nn% for percentage length -->
<!ENTITY % Length.datatype "CDATA" >
<!-- space-separated list of link types -->
<!ENTITY % LinkTypes.datatype "NMTOKENS" >
<!-- single or comma-separated list of media descriptors -->
<!ENTITY % MediaDesc.datatype "CDATA" >
<!-- pixel, percentage, or relative -->
<!ENTITY % MultiLength.datatype "CDATA" >
<!-- one or more digits (NUMBER) -->
<!ENTITY % Number.datatype "CDATA" >
<!-- integer representing length in pixels -->
<!ENTITY % Pixels.datatype "CDATA" >
<!-- script expression -->
<!ENTITY % Script.datatype "CDATA" >
<!-- textual content -->
<!ENTITY % Text.datatype "CDATA" >
<!-- Placeholder Compact URI-related types -->
<!ENTITY % CURIE.datatype "CDATA" >
<!ENTITY % CURIEs.datatype "CDATA" >
<!ENTITY % SafeCURIE.datatype "CDATA" >
<!ENTITY % SafeCURIEs.datatype "CDATA" >
<!ENTITY % URIorSafeCURIE.datatype "CDATA" >
<!ENTITY % URIorSafeCURIEs.datatype "CDATA" >
<!-- Imported Datatypes ................................ -->
<!-- a single character from [ISO10646] -->
<!ENTITY % Character.datatype "CDATA" >
<!-- a character encoding, as per [RFC2045] -->
<!ENTITY % Charset.datatype "CDATA" >
<!-- a space separated list of character encodings, as per [RFC2045] -->
<!ENTITY % Charsets.datatype "CDATA" >
<!-- Color specification using color name or sRGB (#RRGGBB) values -->
<!ENTITY % Color.datatype "CDATA" >
<!-- media type, as per [RFC2045] -->
<!ENTITY % ContentType.datatype "CDATA" >
<!-- comma-separated list of media types, as per [RFC2045] -->
<!ENTITY % ContentTypes.datatype "CDATA" >
<!-- date and time information. ISO date format -->
<!ENTITY % Datetime.datatype "CDATA" >
<!-- formal public identifier, as per [ISO8879] -->
<!ENTITY % FPI.datatype "CDATA" >
<!-- a language code, as per [RFC3066] or its successor -->
<!ENTITY % LanguageCode.datatype "CDATA" >
<!-- a comma separated list of language code ranges -->
<!ENTITY % LanguageCodes.datatype "CDATA" >
<!-- a qualified name , as per [XMLNS] or its successor -->
<!ENTITY % QName.datatype "CDATA" >
<!ENTITY % QNames.datatype "CDATA" >
<!-- a Uniform Resource Identifier, see [URI] -->
<!ENTITY % URI.datatype "CDATA" >
<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->
<!ENTITY % URIs.datatype "CDATA" >
<!-- a relative URI reference consisting of an initial '#' and a fragment ID -->
<!ENTITY % URIREF.datatype "CDATA" >
<!-- end of xhtml-datatypes-1.mod -->
@@ -0,0 +1,38 @@
<!-- ...................................................................... -->
<!-- XHTML Editing Elements Module ....................................... -->
<!-- file: xhtml-edit-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-edit-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Editing Markup 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-edit-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Editing Elements
ins, del
This module declares element types and attributes used to indicate
inserted and deleted content while editing a document.
-->
<!-- ins: Inserted Text ............................... -->
<!ENTITY % ins.element "INCLUDE" ><![%ins.element;[<!ENTITY % ins.content"( #PCDATA | %Flow.mix; )*"><!ENTITY % ins.qname "ins" ><!ELEMENT %ins.qname; %ins.content; ><!-- end of ins.element -->]]>
<!ENTITY % ins.attlist "INCLUDE" ><![%ins.attlist;[<!ATTLIST %ins.qname;%Common.attrib;cite %URI.datatype; #IMPLIEDdatetime %Datetime.datatype; #IMPLIED><!-- end of ins.attlist -->]]>
<!-- del: Deleted Text ................................ -->
<!ENTITY % del.element "INCLUDE" ><![%del.element;[<!ENTITY % del.content"( #PCDATA | %Flow.mix; )*"><!ENTITY % del.qname "del" ><!ELEMENT %del.qname; %del.content; ><!-- end of del.element -->]]>
<!ENTITY % del.attlist "INCLUDE" ><![%del.attlist;[<!ATTLIST %del.qname;%Common.attrib;cite %URI.datatype; #IMPLIEDdatetime %Datetime.datatype; #IMPLIED><!-- end of del.attlist -->]]>
<!-- end of xhtml-edit-1.mod -->
@@ -0,0 +1,97 @@
<!-- ...................................................................... -->
<!-- XHTML Intrinsic Events Module ....................................... -->
<!-- file: xhtml-events-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-events-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ENTITIES XHTML Intrinsic Events 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-events-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Intrinsic Event Attributes
These are the event attributes defined in HTML 4,
Section 18.2.3 "Intrinsic Events". This module must be
instantiated prior to the Attributes Module but after
the Datatype Module in the Modular Framework module.
"Note: Authors of HTML documents are advised that changes
are likely to occur in the realm of intrinsic events
(e.g., how scripts are bound to events). Research in
this realm is carried on by members of the W3C Document
Object Model Working Group (see the W3C Web site at
http://www.w3.org/ for more information)."
-->
<!-- NOTE: Because the ATTLIST declarations in this module occur
before their respective ELEMENT declarations in other
modules, there may be a dependency on this module that
should be considered if any of the parameter entities used
for element type names (eg., %a.qname;) are redeclared.
-->
<!ENTITY % Events.attrib"onclick %Script.datatype; #IMPLIED
ondblclick %Script.datatype; #IMPLIED
onmousedown %Script.datatype; #IMPLIED
onmouseup %Script.datatype; #IMPLIED
onmouseover %Script.datatype; #IMPLIED
onmousemove %Script.datatype; #IMPLIED
onmouseout %Script.datatype; #IMPLIED
onkeypress %Script.datatype; #IMPLIED
onkeydown %Script.datatype; #IMPLIED
onkeyup %Script.datatype; #IMPLIED">
<![%XHTML.global.attrs.prefixed;[<!ENTITY % XHTML.global.events.attrib"%XHTML.prefix;:onclick %Script.datatype; #IMPLIED
%XHTML.prefix;:ondblclick %Script.datatype; #IMPLIED
%XHTML.prefix;:onmousedown %Script.datatype; #IMPLIED
%XHTML.prefix;:onmouseup %Script.datatype; #IMPLIED
%XHTML.prefix;:onmouseover %Script.datatype; #IMPLIED
%XHTML.prefix;:onmousemove %Script.datatype; #IMPLIED
%XHTML.prefix;:onmouseout %Script.datatype; #IMPLIED
%XHTML.prefix;:onkeypress %Script.datatype; #IMPLIED
%XHTML.prefix;:onkeydown %Script.datatype; #IMPLIED
%XHTML.prefix;:onkeyup %Script.datatype; #IMPLIED">]]>
<!-- additional attributes on anchor element
-->
<!ATTLIST %a.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIED>
<!-- additional attributes on form element
-->
<!ATTLIST %form.qname;onsubmit %Script.datatype; #IMPLIEDonreset %Script.datatype; #IMPLIED>
<!-- additional attributes on label element
-->
<!ATTLIST %label.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIED>
<!-- additional attributes on input element
-->
<!ATTLIST %input.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIEDonselect %Script.datatype; #IMPLIEDonchange %Script.datatype; #IMPLIED>
<!-- additional attributes on select element
-->
<!ATTLIST %select.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIEDonchange %Script.datatype; #IMPLIED>
<!-- additional attributes on textarea element
-->
<!ATTLIST %textarea.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIEDonselect %Script.datatype; #IMPLIEDonchange %Script.datatype; #IMPLIED>
<!-- additional attributes on button element
-->
<!ATTLIST %button.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIED>
<!-- additional attributes on body element
-->
<!ATTLIST %body.qname;onload %Script.datatype; #IMPLIEDonunload %Script.datatype; #IMPLIED>
<!-- additional attributes on area element
-->
<!ATTLIST %area.qname;onfocus %Script.datatype; #IMPLIEDonblur %Script.datatype; #IMPLIED>
<!-- end of xhtml-events-1.mod -->
@@ -0,0 +1,130 @@
<!-- ...................................................................... -->
<!-- XHTML Forms Module .................................................. -->
<!-- file: xhtml-form-1.mod
This is XHTML, a reformulation of HTML as a modular XML application.
Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
Revision: $Id: xhtml-form-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI
This DTD module is identified by the PUBLIC and SYSTEM identifiers:
PUBLIC "-//W3C//ELEMENTS XHTML Forms 1.0//EN"
SYSTEM "http://www.w3.org/MarkUp/DTD/xhtml-form-1.mod"
Revisions:
(none)
....................................................................... -->
<!-- Forms
form, label, input, select, optgroup, option,
textarea, fieldset, legend, button
This module declares markup to provide support for online
forms, based on the features found in HTML 4 forms.
-->
<!-- declare qualified element type names:
-->
<!ENTITY % form.qname "form" ><!ENTITY % label.qname "label" ><!ENTITY % input.qname "input" ><!ENTITY % select.qname "select" ><!ENTITY % optgroup.qname "optgroup" ><!ENTITY % option.qname "option" ><!ENTITY % textarea.qname "textarea" ><!ENTITY % fieldset.qname "fieldset" ><!ENTITY % legend.qname "legend" ><!ENTITY % button.qname "button" >
<!-- %BlkNoForm.mix; includes all non-form block elements,
plus %Misc.class;
-->
<!ENTITY % BlkNoForm.mix"%Heading.class;
| %List.class;
| %BlkStruct.class;
%BlkPhras.class;
%BlkPres.class;
%Table.class;
%Block.extra;
%Misc.class;">
<!-- form: Form Element ................................ -->
<!ENTITY % form.element "INCLUDE" ><![%form.element;[<!ENTITY % form.content"( %BlkNoForm.mix;
| %fieldset.qname; )+"><!ELEMENT %form.qname; %form.content; ><!-- end of form.element -->]]>
<!ENTITY % form.attlist "INCLUDE" ><![%form.attlist;[<!ATTLIST %form.qname;%Common.attrib;action %URI.datatype; #REQUIREDmethod ( get | post ) 'get'name CDATA #IMPLIEDenctype %ContentType.datatype; 'application/x-www-form-urlencoded'accept-charset %Charsets.datatype; #IMPLIEDaccept %ContentTypes.datatype; #IMPLIED><!-- end of form.attlist -->]]>
<!-- label: Form Field Label Text ...................... -->
<!-- Each label must not contain more than ONE field
-->
<!ENTITY % label.element "INCLUDE" ><![%label.element;[<!ENTITY % label.content"( #PCDATA
| %input.qname; | %select.qname; | %textarea.qname; | %button.qname;
| %InlStruct.class;
%InlPhras.class;
%I18n.class;
%InlPres.class;
%Anchor.class;
%InlSpecial.class;
%Inline.extra;
%Misc.class; )*"><!ELEMENT %label.qname; %label.content; ><!-- end of label.element -->]]>
<!ENTITY % label.attlist "INCLUDE" ><![%label.attlist;[<!ATTLIST %label.qname;%Common.attrib;for IDREF #IMPLIEDaccesskey %Character.datatype; #IMPLIED><!-- end of label.attlist -->]]>
<!-- input: Form Control ............................... -->
<!ENTITY % input.element "INCLUDE" ><![%input.element;[<!ENTITY % input.content "EMPTY" ><!ELEMENT %input.qname; %input.content; ><!-- end of input.element -->]]>
<!ENTITY % input.attlist "INCLUDE" ><![%input.attlist;[<!ENTITY % InputType.class"( text | password | checkbox | radio | submit
| reset | file | hidden | image | button )"><!-- attribute 'name' required for all but submit & reset
-->
<!ATTLIST %input.qname;%Common.attrib;type %InputType.class; 'text'name CDATA #IMPLIEDvalue CDATA #IMPLIEDchecked ( checked ) #IMPLIEDdisabled ( disabled ) #IMPLIEDreadonly ( readonly ) #IMPLIEDsize %Number.datatype; #IMPLIEDmaxlength %Number.datatype; #IMPLIEDsrc %URI.datatype; #IMPLIEDalt %Text.datatype; #IMPLIEDtabindex %Number.datatype; #IMPLIEDaccesskey %Character.datatype; #IMPLIEDaccept %ContentTypes.datatype; #IMPLIED><!-- end of input.attlist -->]]>
<!-- select: Option Selector ........................... -->
<!ENTITY % select.element "INCLUDE" ><![%select.element;[<!ENTITY % select.content"( %optgroup.qname; | %option.qname; )+"><!ELEMENT %select.qname; %select.content; ><!-- end of select.element -->]]>
<!ENTITY % select.attlist "INCLUDE" ><![%select.attlist;[<!ATTLIST %select.qname;%Common.attrib;name CDATA #IMPLIEDsize %Number.datatype; #IMPLIEDmultiple ( multiple ) #IMPLIEDdisabled ( disabled ) #IMPLIEDtabindex %Number.datatype; #IMPLIED><!-- end of select.attlist -->]]>
<!-- optgroup: Option Group ............................ -->
<!ENTITY % optgroup.element "INCLUDE" ><![%optgroup.element;[<!ENTITY % optgroup.content "( %option.qname; )+" ><!ELEMENT %optgroup.qname; %optgroup.content; ><!-- end of optgroup.element -->]]>
<!ENTITY % optgroup.attlist "INCLUDE" ><![%optgroup.attlist;[<!ATTLIST %optgroup.qname;%Common.attrib;disabled ( disabled ) #IMPLIEDlabel %Text.datatype; #REQUIRED><!-- end of optgroup.attlist -->]]>
<!-- option: Selectable Choice ......................... -->
<!ENTITY % option.element "INCLUDE" ><![%option.element;[<!ENTITY % option.content "( #PCDATA )" ><!ELEMENT %option.qname; %option.content; ><!-- end of option.element -->]]>
<!ENTITY % option.attlist "INCLUDE" ><![%option.attlist;[<!ATTLIST %option.qname;%Common.attrib;selected ( selected ) #IMPLIEDdisabled ( disabled ) #IMPLIEDlabel %Text.datatype; #IMPLIEDvalue CDATA #IMPLIED><!-- end of option.attlist -->]]>
<!-- textarea: Multi-Line Text Field ................... -->
<!ENTITY % textarea.element "INCLUDE" ><![%textarea.element;[<!ENTITY % textarea.content "( #PCDATA )" ><!ELEMENT %textarea.qname; %textarea.content; ><!-- end of textarea.element -->]]>
<!ENTITY % textarea.attlist "INCLUDE" ><![%textarea.attlist;[<!ATTLIST %textarea.qname;%Common.attrib;name CDATA #IMPLIEDrows %Number.datatype; #REQUIREDcols %Number.datatype; #REQUIREDdisabled ( disabled ) #IMPLIEDreadonly ( readonly ) #IMPLIEDtabindex %Number.datatype; #IMPLIEDaccesskey %Character.datatype; #IMPLIED><!-- end of textarea.attlist -->]]>
<!-- fieldset: Form Control Group ...................... -->
<!-- #PCDATA is to solve the mixed content problem,
per specification only whitespace is allowed
-->
<!ENTITY % fieldset.element "INCLUDE" ><![%fieldset.element;[<!ENTITY % fieldset.content"( #PCDATA | %legend.qname; | %Flow.mix; )*"><!ELEMENT %fieldset.qname; %fieldset.content; ><!-- end of fieldset.element -->]]>
<!ENTITY % fieldset.attlist "INCLUDE" ><![%fieldset.attlist;[<!ATTLIST %fieldset.qname;%Common.attrib;><!-- end of fieldset.attlist -->]]>
<!-- legend: Fieldset Legend ........................... -->
<!ENTITY % legend.element "INCLUDE" ><![%legend.element;[<!ENTITY % legend.content"( #PCDATA | %Inline.mix; )*"><!ELEMENT %legend.qname; %legend.content; ><!-- end of legend.element -->]]>
<!ENTITY % legend.attlist "INCLUDE" ><![%legend.attlist;[<!ATTLIST %legend.qname;%Common.attrib;accesskey %Character.datatype; #IMPLIED><!-- end of legend.attlist -->]]>
<!-- button: Push Button ............................... -->
<!ENTITY % button.element "INCLUDE" ><![%button.element;[<!ENTITY % button.content"( #PCDATA
| %BlkNoForm.mix;
| %InlStruct.class;
%InlPhras.class;
%InlPres.class;
%I18n.class;
%InlSpecial.class;
%Inline.extra; )*"><!ELEMENT %button.qname; %button.content; ><!-- end of button.element -->]]>
<!ENTITY % button.attlist "INCLUDE" ><![%button.attlist;[<!ATTLIST %button.qname;%Common.attrib;name CDATA #IMPLIEDvalue CDATA #IMPLIEDtype ( button | submit | reset ) 'submit'disabled ( disabled ) #IMPLIEDtabindex %Number.datatype; #IMPLIEDaccesskey %Character.datatype; #IMPLIED><!-- end of button.attlist -->]]>
<!-- end of xhtml-form-1.mod -->

Some files were not shown because too many files have changed in this diff Show More