diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 00000000..91106d3f --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,17 @@ +name: Java CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..18f095e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: java + +script: mvn -f epublib-parent/pom.xml clean package + +dist: trusty + +jdk: + - openjdk7 + - openjdk8 \ No newline at end of file diff --git a/README b/README deleted file mode 100644 index a6265567..00000000 --- a/README +++ /dev/null @@ -1,50 +0,0 @@ -Epublib is a java library for creating epub files. -Its focus is on creating epubs from existing html files using the command line or a java program. -Another intended use is to generate documentation as part of a build process. -text added for git testing - -Writing epubs works pretty well, reading them has recently started to work. - -a small change - -Right now it's useful in 2 cases: -Creating an epub programmatically or converting a bunch of html's to an epub from the command-line. - -Creating an epub programmatically -================================= - - // Create new Book - Book book = new Book(); - - // Set the title - book.getMetadata().addTitle("Epublib test book 1"); - - // Add an Author - book.getMetadata().addAuthor(new Author("Joe", "Tester")); - - // Set cover image - book.getMetadata().setCoverImage(new InputStreamResource(Simple1.class.getResourceAsStream("/book1/test_cover.png"), "cover.png")); - - // Add Chapter 1 - book.addResourceAsSection("Introduction", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter1.html"), "chapter1.html")); - - // Add css file - book.getResources().add(new InputStreamResource(Simple1.class.getResourceAsStream("/book1/book1.css"), "book1.css")); - - // Add Chapter 2 - Section chapter2 = book.addResourceAsSection("Second Chapter", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter2.html"), "chapter2.html")); - - // Add image used by Chapter 2 - book.getResources().add(new InputStreamResource(Simple1.class.getResourceAsStream("/book1/flowers_320x240.jpg"), "flowers.jpg")); - - // Add Chapter2, Section 1 - book.addResourceAsSubSection(chapter2, "Chapter 2, section 1", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter2_1.html"), "chapter2_1.html")); - - // Add Chapter 3 - book.addResourceAsSection("Conclusion", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter3.html"), "chapter3.html")); - - // Create EpubWriter - EpubWriter epubWriter = new EpubWriter(); - - // Write the Book as Epub - epubWriter.write(book, new FileOutputStream("test1_book1.epub")); diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 4c226235..00000000 --- a/README.markdown +++ /dev/null @@ -1,75 +0,0 @@ -# epublib -Epublib is a java library for reading/writing/manipulating epub files. - -I consists of 2 parts: a core that reads/writes epub and a collection of tools. -The tools contain an epub cleanup tool, a tool to create epubs from html files, a tool to create an epub from an uncompress html file. -It also contains a swing-based epub viewer. -![Epublib viewer](http://www.siegmann.nl/wp-content/uploads/Alice%E2%80%99s-Adventures-in-Wonderland_2011-01-30_18-17-30.png) - -The core runs both on android and a standard java environment. The tools run only on a standard java environment. - -This means that reading/writing epub files works on Android. - -## Command line examples - -Set the author of an existing epub - java -jar epublib-3.0-SNAPSHOT.one-jar.jar --in input.epub --out result.epub --author Tester,Joe - -Set the cover image of an existing epub - java -jar epublib-3.0-SNAPSHOT.one-jar.jar --in input.epub --out result.epub --cover-image my_cover.jpg - -## Creating an epub programmatically - - package nl.siegmann.epublib.examples; - - import java.io.FileOutputStream; - - import nl.siegmann.epublib.domain.Author; - import nl.siegmann.epublib.domain.Book; - import nl.siegmann.epublib.domain.InputStreamResource; - import nl.siegmann.epublib.domain.Section; - import nl.siegmann.epublib.epub.EpubWriter; - - public class Simple1 { - public static void main(String[] args) { - try { - // Create new Book - Book book = new Book(); - - // Set the title - book.getMetadata().addTitle("Epublib test book 1"); - - // Add an Author - book.getMetadata().addAuthor(new Author("Joe", "Tester")); - - // Set cover image - book.getMetadata().setCoverImage(new InputStreamResource(Simple1.class.getResourceAsStream("/book1/test_cover.png"), "cover.png")); - - // Add Chapter 1 - book.addResourceAsSection("Introduction", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter1.html"), "chapter1.html")); - - // Add css file - book.getResources().add(new InputStreamResource(Simple1.class.getResourceAsStream("/book1/book1.css"), "book1.css")); - - // Add Chapter 2 - Section chapter2 = book.addResourceAsSection("Second Chapter", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter2.html"), "chapter2.html")); - - // Add image used by Chapter 2 - book.getResources().add(new InputStreamResource(Simple1.class.getResourceAsStream("/book1/flowers_320x240.jpg"), "flowers.jpg")); - - // Add Chapter2, Section 1 - book.addResourceAsSubSection(chapter2, "Chapter 2, section 1", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter2_1.html"), "chapter2_1.html")); - - // Add Chapter 3 - book.addResourceAsSection("Conclusion", new InputStreamResource(Simple1.class.getResourceAsStream("/book1/chapter3.html"), "chapter3.html")); - - // Create EpubWriter - EpubWriter epubWriter = new EpubWriter(); - - // Write the Book as Epub - epubWriter.write(book, new FileOutputStream("test1_book1.epub")); - } catch(Exception e) { - e.printStackTrace(); - } - } - } diff --git a/README.md b/README.md new file mode 100644 index 00000000..b0cc05c1 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# epublib +Epublib is a java library for reading/writing/manipulating epub files. + +It consists of 2 parts: a core that reads/writes epub and a collection of tools. +The tools contain an epub cleanup tool, a tool to create epubs from html files, a tool to create an epub from an uncompress html file. +It also contains a swing-based epub viewer. +![Epublib viewer](http://www.siegmann.nl/wp-content/uploads/Alice%E2%80%99s-Adventures-in-Wonderland_2011-01-30_18-17-30.png) + +The core runs both on android and a standard java environment. The tools run only on a standard java environment. + +This means that reading/writing epub files works on Android. + +## Build status +* Travis Build Status: [![Build Status](https://travis-ci.org/psiegman/epublib.svg?branch=master)](https://travis-ci.org/psiegman/epublib) + +## Command line examples + +Set the author of an existing epub + java -jar epublib-3.0-SNAPSHOT.one-jar.jar --in input.epub --out result.epub --author Tester,Joe + +Set the cover image of an existing epub + java -jar epublib-3.0-SNAPSHOT.one-jar.jar --in input.epub --out result.epub --cover-image my_cover.jpg + +## Creating an epub programmatically + + package nl.siegmann.epublib.examples; + + import java.io.InputStream; + import java.io.FileOutputStream; + + import nl.siegmann.epublib.domain.Author; + import nl.siegmann.epublib.domain.Book; + import nl.siegmann.epublib.domain.Metadata; + import nl.siegmann.epublib.domain.Resource; + import nl.siegmann.epublib.domain.TOCReference; + + import nl.siegmann.epublib.epub.EpubWriter; + + public class Translator { + private static InputStream getResource( String path ) { + return Translator.class.getResourceAsStream( path ); + } + + private static Resource getResource( String path, String href ) { + return new Resource( getResource( path ), href ); + } + + public static void main(String[] args) { + try { + // Create new Book + Book book = new Book(); + Metadata metadata = book.getMetadata(); + + // Set the title + metadata.addTitle("Epublib test book 1"); + + // Add an Author + metadata.addAuthor(new Author("Joe", "Tester")); + + // Set cover image + book.setCoverImage( + getResource("/book1/test_cover.png", "cover.png") ); + + // Add Chapter 1 + book.addSection("Introduction", + getResource("/book1/chapter1.html", "chapter1.html") ); + + // Add css file + book.getResources().add( + getResource("/book1/book1.css", "book1.css") ); + + // Add Chapter 2 + TOCReference chapter2 = book.addSection( "Second Chapter", + getResource("/book1/chapter2.html", "chapter2.html") ); + + // Add image used by Chapter 2 + book.getResources().add( + getResource("/book1/flowers_320x240.jpg", "flowers.jpg")); + + // Add Chapter2, Section 1 + book.addSection(chapter2, "Chapter 2, section 1", + getResource("/book1/chapter2_1.html", "chapter2_1.html")); + + // Add Chapter 3 + book.addSection("Conclusion", + getResource("/book1/chapter3.html", "chapter3.html")); + + // Create EpubWriter + EpubWriter epubWriter = new EpubWriter(); + + // Write the Book as Epub + epubWriter.write(book, new FileOutputStream("test1_book1.epub")); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + +## Usage in Android + +Add the following lines to your `app` module's `build.gradle` file: + + repositories { + maven { + url 'https://github.com/psiegman/mvn-repo/raw/master/releases' + } + } + + dependencies { + implementation('nl.siegmann.epublib:epublib-core:4.0') { + exclude group: 'org.slf4j' + exclude group: 'xmlpull' + } + implementation 'org.slf4j:slf4j-android:1.7.25' + } diff --git a/epublib-core/build.sbt b/epublib-core/build.sbt new file mode 100644 index 00000000..b75cb493 --- /dev/null +++ b/epublib-core/build.sbt @@ -0,0 +1,25 @@ +autoScalaLibrary := false + +crossPaths := false + +name := "epublib-core" + +organization := "nl.siegmann.epublib" + +version := "4.0" + +publishMavenStyle := true + +javacOptions in doc += "-Xdoclint:none" + +libraryDependencies += "net.sf.kxml" % "kxml2" % "2.3.0" + +libraryDependencies += "xmlpull" % "xmlpull" % "1.1.3.4d_b4_min" + +libraryDependencies += "org.slf4j" % "slf4j-api" % "1.6.1" + +libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.6.1" + +libraryDependencies += "junit" % "junit" % "4.10" + + diff --git a/epublib-core/pom.xml b/epublib-core/pom.xml index ec676b6a..208226f6 100644 --- a/epublib-core/pom.xml +++ b/epublib-core/pom.xml @@ -8,7 +8,6 @@ nl.siegmann.epublib epublib-core epublib-core - 3.1 A java library for reading/writing/manipulating epub files http://www.siegmann.nl/epublib 2009 @@ -16,42 +15,37 @@ nl.siegmann.epublib epublib-parent - 3.1 + 4.0 + ../epublib-parent/pom.xml net.sf.kxml kxml2 - 2.3.0 xmlpull xmlpull - 1.1.3.4d_b4_min org.slf4j slf4j-api - ${slf4j.version} org.slf4j slf4j-simple - ${slf4j.version} junit junit - 4.10 test - - commons-io - commons-io - 2.1 - jar - + + org.mockito + mockito-all + test + @@ -59,7 +53,7 @@ org.apache.maven.plugins maven-shade-plugin - 1.4 + ${maven-shade-plugin.version} package @@ -76,10 +70,10 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + ${maven-compiler-plugin.version} - 1.6 - 1.6 + ${source.version} + ${target.version} @@ -89,7 +83,7 @@ org.apache.maven.plugins maven-site-plugin - 3.0-beta-3 + ${maven-site-plugin.version} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/Adler32.java b/epublib-core/src/main/java/net/sf/jazzlib/Adler32.java new file mode 100644 index 00000000..198189a2 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/Adler32.java @@ -0,0 +1,198 @@ +/* Adler32.java - Computes Adler32 data checksum of a data stream + Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/* + * Written using on-line Java Platform 1.2 API Specification, as well + * as "The Java Class Libraries", 2nd edition (Addison-Wesley, 1998). + * The actual Adler32 algorithm is taken from RFC 1950. + * Status: Believed complete and correct. + */ + +/** + * Computes Adler32 checksum for a stream of data. An Adler32 checksum is not as + * reliable as a CRC32 checksum, but a lot faster to compute. + *

+ * The specification for Adler32 may be found in RFC 1950. (ZLIB Compressed Data + * Format Specification version 3.3) + *

+ *

+ * From that document: + *

+ * "ADLER32 (Adler-32 checksum) This contains a checksum value of the + * uncompressed data (excluding any dictionary data) computed according to + * Adler-32 algorithm. This algorithm is a 32-bit extension and improvement of + * the Fletcher algorithm, used in the ITU-T X.224 / ISO 8073 standard. + *

+ * Adler-32 is composed of two sums accumulated per byte: s1 is the sum of all + * bytes, s2 is the sum of all s1 values. Both sums are done modulo 65521. s1 is + * initialized to 1, s2 to zero. The Adler-32 checksum is stored as s2*65536 + + * s1 in most- significant-byte first (network) order." + *

+ * "8.2. The Adler-32 algorithm + *

+ * The Adler-32 algorithm is much faster than the CRC32 algorithm yet still + * provides an extremely low probability of undetected errors. + *

+ * The modulo on unsigned long accumulators can be delayed for 5552 bytes, so + * the modulo operation time is negligible. If the bytes are a, b, c, the second + * sum is 3a + 2b + c + 3, and so is position and order sensitive, unlike the + * first sum, which is just a checksum. That 65521 is prime is important to + * avoid a possible large class of two-byte errors that leave the check + * unchanged. (The Fletcher checksum uses 255, which is not prime and which also + * makes the Fletcher check insensitive to single byte changes 0 <-> 255.) + *

+ * The sum s1 is initialized to 1 instead of zero to make the length of the + * sequence part of s2, so that the length does not have to be checked + * separately. (Any sequence of zeroes has a Fletcher checksum of zero.)" + * + * @author John Leuner, Per Bothner + * @since JDK 1.1 + * + * @see InflaterInputStream + * @see DeflaterOutputStream + */ +public class Adler32 implements Checksum { + + /** largest prime smaller than 65536 */ + private static final int BASE = 65521; + + private int checksum; // we do all in int. + + // Note that java doesn't have unsigned integers, + // so we have to be careful with what arithmetic + // we do. We return the checksum as a long to + // avoid sign confusion. + + /** + * Creates a new instance of the Adler32 class. The checksum + * starts off with a value of 1. + */ + public Adler32() { + reset(); + } + + /** + * Resets the Adler32 checksum to the initial value. + */ + @Override + public void reset() { + checksum = 1; // Initialize to 1 + } + + /** + * Updates the checksum with the byte b. + * + * @param bval + * the data value to add. The high byte of the int is ignored. + */ + @Override + public void update(final int bval) { + // We could make a length 1 byte array and call update again, but I + // would rather not have that overhead + int s1 = checksum & 0xffff; + int s2 = checksum >>> 16; + + s1 = (s1 + (bval & 0xFF)) % BASE; + s2 = (s1 + s2) % BASE; + + checksum = (s2 << 16) + s1; + } + + /** + * Updates the checksum with the bytes taken from the array. + * + * @param buffer + * an array of bytes + */ + public void update(final byte[] buffer) { + update(buffer, 0, buffer.length); + } + + /** + * Updates the checksum with the bytes taken from the array. + * + * @param buf + * an array of bytes + * @param off + * the start of the data used for this update + * @param len + * the number of bytes to use for this update + */ + @Override + public void update(final byte[] buf, int off, int len) { + // (By Per Bothner) + int s1 = checksum & 0xffff; + int s2 = checksum >>> 16; + + while (len > 0) { + // We can defer the modulo operation: + // s1 maximally grows from 65521 to 65521 + 255 * 3800 + // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 + int n = 3800; + if (n > len) { + n = len; + } + len -= n; + while (--n >= 0) { + s1 = s1 + (buf[off++] & 0xFF); + s2 = s2 + s1; + } + s1 %= BASE; + s2 %= BASE; + } + + /* + * Old implementation, borrowed from somewhere: int n; + * + * while (len-- > 0) { + * + * s1 = (s1 + (bs[offset++] & 0xff)) % BASE; s2 = (s2 + s1) % BASE; } + */ + + checksum = (s2 << 16) | s1; + } + + /** + * Returns the Adler32 data checksum computed so far. + */ + @Override + public long getValue() { + return checksum & 0xffffffffL; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/CRC32.java b/epublib-core/src/main/java/net/sf/jazzlib/CRC32.java new file mode 100644 index 00000000..f5d40cd4 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/CRC32.java @@ -0,0 +1,138 @@ +/* CRC32.java - Computes CRC32 data checksum of a data stream + Copyright (C) 1999. 2000, 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/* + * Written using on-line Java Platform 1.2 API Specification, as well + * as "The Java Class Libraries", 2nd edition (Addison-Wesley, 1998). + * The actual CRC32 algorithm is taken from RFC 1952. + * Status: Believed complete and correct. + */ + +/** + * Computes CRC32 data checksum of a data stream. The actual CRC32 algorithm is + * described in RFC 1952 (GZIP file format specification version 4.3). Can be + * used to get the CRC32 over a stream if used with checked input/output + * streams. + * + * @see InflaterInputStream + * @see DeflaterOutputStream + * + * @author Per Bothner + * @date April 1, 1999. + */ +public class CRC32 implements Checksum { + /** The crc data checksum so far. */ + private int crc = 0; + + /** The fast CRC table. Computed once when the CRC32 class is loaded. */ + private static int[] crc_table = make_crc_table(); + + /** Make the table for a fast CRC. */ + private static int[] make_crc_table() { + final int[] crc_table = new int[256]; + for (int n = 0; n < 256; n++) { + int c = n; + for (int k = 8; --k >= 0;) { + if ((c & 1) != 0) { + c = 0xedb88320 ^ (c >>> 1); + } else { + c = c >>> 1; + } + } + crc_table[n] = c; + } + return crc_table; + } + + /** + * Returns the CRC32 data checksum computed so far. + */ + @Override + public long getValue() { + return crc & 0xffffffffL; + } + + /** + * Resets the CRC32 data checksum as if no update was ever called. + */ + @Override + public void reset() { + crc = 0; + } + + /** + * Updates the checksum with the int bval. + * + * @param bval + * (the byte is taken as the lower 8 bits of bval) + */ + + @Override + public void update(final int bval) { + int c = ~crc; + c = crc_table[(c ^ bval) & 0xff] ^ (c >>> 8); + crc = ~c; + } + + /** + * Adds the byte array to the data checksum. + * + * @param buf + * the buffer which contains the data + * @param off + * the offset in the buffer where the data starts + * @param len + * the length of the data + */ + @Override + public void update(final byte[] buf, int off, int len) { + int c = ~crc; + while (--len >= 0) { + c = crc_table[(c ^ buf[off++]) & 0xff] ^ (c >>> 8); + } + crc = ~c; + } + + /** + * Adds the complete byte array to the data checksum. + */ + public void update(final byte[] buf) { + update(buf, 0, buf.length); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/CheckedInputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/CheckedInputStream.java new file mode 100644 index 00000000..80289057 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/CheckedInputStream.java @@ -0,0 +1,135 @@ +/* CheckedInputStream.java - Compute checksum of data being read + Copyright (C) 1999, 2000 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/* Written using on-line Java Platform 1.2 API Specification + * and JCL book. + * Believed complete and correct. + */ + +/** + * InputStream that computes a checksum of the data being read using a supplied + * Checksum object. + * + * @see Checksum + * + * @author Tom Tromey + * @date May 17, 1999 + */ +public class CheckedInputStream extends FilterInputStream { + /** + * Creates a new CheckInputStream on top of the supplied OutputStream using + * the supplied Checksum. + */ + public CheckedInputStream(final InputStream in, final Checksum sum) { + super(in); + this.sum = sum; + } + + /** + * Returns the Checksum object used. To get the data checksum computed so + * far call getChecksum.getValue(). + */ + public Checksum getChecksum() { + return sum; + } + + /** + * Reads one byte, updates the checksum and returns the read byte (or -1 + * when the end of file was reached). + */ + @Override + public int read() throws IOException { + final int x = in.read(); + if (x != -1) { + sum.update(x); + } + return x; + } + + /** + * Reads at most len bytes in the supplied buffer and updates the checksum + * with it. Returns the number of bytes actually read or -1 when the end of + * file was reached. + */ + @Override + public int read(final byte[] buf, final int off, final int len) + throws IOException { + final int r = in.read(buf, off, len); + if (r != -1) { + sum.update(buf, off, r); + } + return r; + } + + /** + * Skips n bytes by reading them in a temporary buffer and updating the the + * checksum with that buffer. Returns the actual number of bytes skiped + * which can be less then requested when the end of file is reached. + */ + @Override + public long skip(long n) throws IOException { + if (n == 0) { + return 0; + } + + int min = (int) Math.min(n, 1024); + final byte[] buf = new byte[min]; + + long s = 0; + while (n > 0) { + final int r = in.read(buf, 0, min); + if (r == -1) { + break; + } + n -= r; + s += r; + min = (int) Math.min(n, 1024); + sum.update(buf, 0, r); + } + + return s; + } + + /** The checksum object. */ + private final Checksum sum; +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/CheckedOutputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/CheckedOutputStream.java new file mode 100644 index 00000000..7077ec09 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/CheckedOutputStream.java @@ -0,0 +1,97 @@ +/* CheckedOutputStream.java - Compute checksum of data being written. + Copyright (C) 1999, 2000 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/* Written using on-line Java Platform 1.2 API Specification + * and JCL book. + * Believed complete and correct. + */ + +/** + * OutputStream that computes a checksum of data being written using a supplied + * Checksum object. + * + * @see Checksum + * + * @author Tom Tromey + * @date May 17, 1999 + */ +public class CheckedOutputStream extends FilterOutputStream { + /** + * Creates a new CheckInputStream on top of the supplied OutputStream using + * the supplied Checksum. + */ + public CheckedOutputStream(final OutputStream out, final Checksum cksum) { + super(out); + this.sum = cksum; + } + + /** + * Returns the Checksum object used. To get the data checksum computed so + * far call getChecksum.getValue(). + */ + public Checksum getChecksum() { + return sum; + } + + /** + * Writes one byte to the OutputStream and updates the Checksum. + */ + @Override + public void write(final int bval) throws IOException { + out.write(bval); + sum.update(bval); + } + + /** + * Writes the byte array to the OutputStream and updates the Checksum. + */ + @Override + public void write(final byte[] buf, final int off, final int len) + throws IOException { + out.write(buf, off, len); + sum.update(buf, off, len); + } + + /** The checksum object. */ + private final Checksum sum; +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/Checksum.java b/epublib-core/src/main/java/net/sf/jazzlib/Checksum.java new file mode 100644 index 00000000..7bae782c --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/Checksum.java @@ -0,0 +1,89 @@ +/* Checksum.java - Interface to compute a data checksum + Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/* + * Written using on-line Java Platform 1.2 API Specification, as well + * as "The Java Class Libraries", 2nd edition (Addison-Wesley, 1998). + * Status: Believed complete and correct. + */ + +/** + * Interface to compute a data checksum used by checked input/output streams. A + * data checksum can be updated by one byte or with a byte array. After each + * update the value of the current checksum can be returned by calling + * getValue. The complete checksum object can also be reset so it + * can be used again with new data. + * + * @see CheckedInputStream + * @see CheckedOutputStream + * + * @author Per Bothner + * @author Jochen Hoenicke + */ +public interface Checksum { + /** + * Returns the data checksum computed so far. + */ + long getValue(); + + /** + * Resets the data checksum as if no update was ever called. + */ + void reset(); + + /** + * Adds one byte to the data checksum. + * + * @param bval + * the data value to add. The high byte of the int is ignored. + */ + void update(int bval); + + /** + * Adds the byte array to the data checksum. + * + * @param buf + * the buffer which contains the data + * @param off + * the offset in the buffer where the data starts + * @param len + * the length of the data + */ + void update(byte[] buf, int off, int len); +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/DataFormatException.java b/epublib-core/src/main/java/net/sf/jazzlib/DataFormatException.java new file mode 100644 index 00000000..79501ec8 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/DataFormatException.java @@ -0,0 +1,69 @@ +/* DataformatException.java -- thrown when compressed data is corrupt + Copyright (C) 1999, 2000, 2001, 2002 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/** + * Exception thrown when compressed data is corrupt. + * + * @author Tom Tromey + * @author John Leuner + * @since 1.1 + * @status updated to 1.4 + */ +public class DataFormatException extends Exception { + /** + * Compatible with JDK 1.1+. + */ + private static final long serialVersionUID = 2219632870893641452L; + + /** + * Create an exception without a message. + */ + public DataFormatException() { + } + + /** + * Create an exception with a message. + * + * @param msg + * the message + */ + public DataFormatException(final String msg) { + super(msg); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/Deflater.java b/epublib-core/src/main/java/net/sf/jazzlib/Deflater.java new file mode 100644 index 00000000..c9af5fe0 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/Deflater.java @@ -0,0 +1,511 @@ +/* Deflater.java - Compress a data stream + Copyright (C) 1999, 2000, 2001, 2004 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/** + * This is the Deflater class. The deflater class compresses input with the + * deflate algorithm described in RFC 1951. It has several compression levels + * and three different strategies described below. + * + * This class is not thread safe. This is inherent in the API, due to the + * split of deflate and setInput. + * + * @author Jochen Hoenicke + * @author Tom Tromey + */ +public class Deflater { + /** + * The best and slowest compression level. This tries to find very long and + * distant string repetitions. + */ + public static final int BEST_COMPRESSION = 9; + /** + * The worst but fastest compression level. + */ + public static final int BEST_SPEED = 1; + /** + * The default compression level. + */ + public static final int DEFAULT_COMPRESSION = -1; + /** + * This level won't compress at all but output uncompressed blocks. + */ + public static final int NO_COMPRESSION = 0; + + /** + * The default strategy. + */ + public static final int DEFAULT_STRATEGY = 0; + /** + * This strategy will only allow longer string repetitions. It is useful for + * random data with a small character set. + */ + public static final int FILTERED = 1; + + /** + * This strategy will not look for string repetitions at all. It only + * encodes with Huffman trees (which means, that more common characters get + * a smaller encoding. + */ + public static final int HUFFMAN_ONLY = 2; + + /** + * The compression method. This is the only method supported so far. There + * is no need to use this constant at all. + */ + public static final int DEFLATED = 8; + + /* + * The Deflater can do the following state transitions: + * + * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. / | (2) (5) | / v (5) | + * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) \ | (3) | ,-------' + * | | | (3) / v v (5) v v (1) -> BUSY_STATE ----> FINISHING_STATE | (6) v + * FINISHED_STATE \_____________________________________/ | (7) v + * CLOSED_STATE + * + * (1) If we should produce a header we start in INIT_STATE, otherwise we + * start in BUSY_STATE. (2) A dictionary may be set only when we are in + * INIT_STATE, then we change the state as indicated. (3) Whether a + * dictionary is set or not, on the first call of deflate we change to + * BUSY_STATE. (4) -- intentionally left blank -- :) (5) FINISHING_STATE is + * entered, when flush() is called to indicate that there is no more INPUT. + * There are also states indicating, that the header wasn't written yet. (6) + * FINISHED_STATE is entered, when everything has been flushed to the + * internal pending output buffer. (7) At any time (7) + */ + + private static final int IS_SETDICT = 0x01; + private static final int IS_FLUSHING = 0x04; + private static final int IS_FINISHING = 0x08; + + private static final int INIT_STATE = 0x00; + private static final int SETDICT_STATE = 0x01; + private static final int BUSY_STATE = 0x10; + private static final int FLUSHING_STATE = 0x14; + private static final int FINISHING_STATE = 0x1c; + private static final int FINISHED_STATE = 0x1e; + private static final int CLOSED_STATE = 0x7f; + + /** Compression level. */ + private int level; + + /** should we include a header. */ + private final boolean noHeader; + + /** The current state. */ + private int state; + + /** The total bytes of output written. */ + private int totalOut; + + /** The pending output. */ + private DeflaterPending pending; + + /** The deflater engine. */ + private DeflaterEngine engine; + + /** + * Creates a new deflater with default compression level. + */ + public Deflater() { + this(DEFAULT_COMPRESSION, false); + } + + /** + * Creates a new deflater with given compression level. + * + * @param lvl + * the compression level, a value between NO_COMPRESSION and + * BEST_COMPRESSION, or DEFAULT_COMPRESSION. + * @exception IllegalArgumentException + * if lvl is out of range. + */ + public Deflater(final int lvl) { + this(lvl, false); + } + + /** + * Creates a new deflater with given compression level. + * + * @param lvl + * the compression level, a value between NO_COMPRESSION and + * BEST_COMPRESSION. + * @param nowrap + * true, iff we should suppress the deflate header at the + * beginning and the adler checksum at the end of the output. + * This is useful for the GZIP format. + * @exception IllegalArgumentException + * if lvl is out of range. + */ + public Deflater(int lvl, final boolean nowrap) { + if (lvl == DEFAULT_COMPRESSION) { + lvl = 6; + } else if ((lvl < NO_COMPRESSION) || (lvl > BEST_COMPRESSION)) { + throw new IllegalArgumentException(); + } + + pending = new DeflaterPending(); + engine = new DeflaterEngine(pending); + this.noHeader = nowrap; + setStrategy(DEFAULT_STRATEGY); + setLevel(lvl); + reset(); + } + + /** + * Resets the deflater. The deflater acts afterwards as if it was just + * created with the same compression level and strategy as it had before. + */ + public void reset() { + state = (noHeader ? BUSY_STATE : INIT_STATE); + totalOut = 0; + pending.reset(); + engine.reset(); + } + + /** + * Frees all objects allocated by the compressor. There's no reason to call + * this, since you can just rely on garbage collection. Exists only for + * compatibility against Sun's JDK, where the compressor allocates native + * memory. If you call any method (even reset) afterwards the behaviour is + * undefined. + * + * @deprecated Just clear all references to deflater instead. + */ + @Deprecated + public void end() { + engine = null; + pending = null; + state = CLOSED_STATE; + } + + /** + * Gets the current adler checksum of the data that was processed so far. + */ + public int getAdler() { + return engine.getAdler(); + } + + /** + * Gets the number of input bytes processed so far. + */ + public int getTotalIn() { + return engine.getTotalIn(); + } + + /** + * Gets the number of output bytes so far. + */ + public int getTotalOut() { + return totalOut; + } + + /** + * Finalizes this object. + */ + @Override + protected void finalize() { + /* Exists solely for compatibility. We don't have any native state. */ + } + + /** + * Flushes the current input block. Further calls to deflate() will produce + * enough output to inflate everything in the current input block. This is + * not part of Sun's JDK so I have made it package private. It is used by + * DeflaterOutputStream to implement flush(). + */ + void flush() { + state |= IS_FLUSHING; + } + + /** + * Finishes the deflater with the current input block. It is an error to + * give more input after this method was called. This method must be called + * to force all bytes to be flushed. + */ + public void finish() { + state |= IS_FLUSHING | IS_FINISHING; + } + + /** + * Returns true iff the stream was finished and no more output bytes are + * available. + */ + public boolean finished() { + return (state == FINISHED_STATE) && pending.isFlushed(); + } + + /** + * Returns true, if the input buffer is empty. You should then call + * setInput().
+ * + * NOTE: This method can also return true when the stream was + * finished. + */ + public boolean needsInput() { + return engine.needsInput(); + } + + /** + * Sets the data which should be compressed next. This should be only called + * when needsInput indicates that more input is needed. If you call setInput + * when needsInput() returns false, the previous input that is still pending + * will be thrown away. The given byte array should not be changed, before + * needsInput() returns true again. This call is equivalent to + * setInput(input, 0, input.length). + * + * @param input + * the buffer containing the input data. + * @exception IllegalStateException + * if the buffer was finished() or ended(). + */ + public void setInput(final byte[] input) { + setInput(input, 0, input.length); + } + + /** + * Sets the data which should be compressed next. This should be only called + * when needsInput indicates that more input is needed. The given byte array + * should not be changed, before needsInput() returns true again. + * + * @param input + * the buffer containing the input data. + * @param off + * the start of the data. + * @param len + * the length of the data. + * @exception IllegalStateException + * if the buffer was finished() or ended() or if previous + * input is still pending. + */ + public void setInput(final byte[] input, final int off, final int len) { + if ((state & IS_FINISHING) != 0) { + throw new IllegalStateException("finish()/end() already called"); + } + engine.setInput(input, off, len); + } + + /** + * Sets the compression level. There is no guarantee of the exact position + * of the change, but if you call this when needsInput is true the change of + * compression level will occur somewhere near before the end of the so far + * given input. + * + * @param lvl + * the new compression level. + */ + public void setLevel(int lvl) { + if (lvl == DEFAULT_COMPRESSION) { + lvl = 6; + } else if ((lvl < NO_COMPRESSION) || (lvl > BEST_COMPRESSION)) { + throw new IllegalArgumentException(); + } + + if (level != lvl) { + level = lvl; + engine.setLevel(lvl); + } + } + + /** + * Sets the compression strategy. Strategy is one of DEFAULT_STRATEGY, + * HUFFMAN_ONLY and FILTERED. For the exact position where the strategy is + * changed, the same as for setLevel() applies. + * + * @param stgy + * the new compression strategy. + */ + public void setStrategy(final int stgy) { + if ((stgy != DEFAULT_STRATEGY) && (stgy != FILTERED) + && (stgy != HUFFMAN_ONLY)) { + throw new IllegalArgumentException(); + } + engine.setStrategy(stgy); + } + + /** + * Deflates the current input block to the given array. It returns the + * number of bytes compressed, or 0 if either needsInput() or finished() + * returns true or length is zero. + * + * @param output + * the buffer where to write the compressed data. + */ + public int deflate(final byte[] output) { + return deflate(output, 0, output.length); + } + + /** + * Deflates the current input block to the given array. It returns the + * number of bytes compressed, or 0 if either needsInput() or finished() + * returns true or length is zero. + * + * @param output + * the buffer where to write the compressed data. + * @param offset + * the offset into the output array. + * @param length + * the maximum number of bytes that may be written. + * @exception IllegalStateException + * if end() was called. + * @exception IndexOutOfBoundsException + * if offset and/or length don't match the array length. + */ + public int deflate(final byte[] output, int offset, int length) { + final int origLength = length; + + if (state == CLOSED_STATE) { + throw new IllegalStateException("Deflater closed"); + } + + if (state < BUSY_STATE) { + /* output header */ + int header = (DEFLATED + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int level_flags = (level - 1) >> 1; + if ((level_flags < 0) || (level_flags > 3)) { + level_flags = 3; + } + header |= level_flags << 6; + if ((state & IS_SETDICT) != 0) { + /* Dictionary was set */ + header |= DeflaterConstants.PRESET_DICT; + } + header += 31 - (header % 31); + + pending.writeShortMSB(header); + if ((state & IS_SETDICT) != 0) { + final int chksum = engine.getAdler(); + engine.resetAdler(); + pending.writeShortMSB(chksum >> 16); + pending.writeShortMSB(chksum & 0xffff); + } + + state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + } + + for (;;) { + final int count = pending.flush(output, offset, length); + offset += count; + totalOut += count; + length -= count; + if ((length == 0) || (state == FINISHED_STATE)) { + break; + } + + if (!engine.deflate((state & IS_FLUSHING) != 0, + (state & IS_FINISHING) != 0)) { + if (state == BUSY_STATE) { + /* We need more input now */ + return origLength - length; + } else if (state == FLUSHING_STATE) { + if (level != NO_COMPRESSION) { + /* + * We have to supply some lookahead. 8 bit lookahead are + * needed by the zlib inflater, and we must fill the + * next byte, so that all bits are flushed. + */ + int neededbits = 8 + ((-pending.getBitCount()) & 7); + while (neededbits > 0) { + /* + * write a static tree block consisting solely of an + * EOF: + */ + pending.writeBits(2, 10); + neededbits -= 10; + } + } + state = BUSY_STATE; + } else if (state == FINISHING_STATE) { + pending.alignToByte(); + /* We have completed the stream */ + if (!noHeader) { + final int adler = engine.getAdler(); + pending.writeShortMSB(adler >> 16); + pending.writeShortMSB(adler & 0xffff); + } + state = FINISHED_STATE; + } + } + } + + return origLength - length; + } + + /** + * Sets the dictionary which should be used in the deflate process. This + * call is equivalent to setDictionary(dict, 0, + * dict.length). + * + * @param dict + * the dictionary. + * @exception IllegalStateException + * if setInput () or deflate () were already called or + * another dictionary was already set. + */ + public void setDictionary(final byte[] dict) { + setDictionary(dict, 0, dict.length); + } + + /** + * Sets the dictionary which should be used in the deflate process. The + * dictionary should be a byte array containing strings that are likely to + * occur in the data which should be compressed. The dictionary is not + * stored in the compressed output, only a checksum. To decompress the + * output you need to supply the same dictionary again. + * + * @param dict + * the dictionary. + * @param offset + * an offset into the dictionary. + * @param length + * the length of the dictionary. + * @exception IllegalStateException + * if setInput () or deflate () were already called or + * another dictionary was already set. + */ + public void setDictionary(final byte[] dict, final int offset, + final int length) { + if (state != INIT_STATE) { + throw new IllegalStateException(); + } + + state = SETDICT_STATE; + engine.setDictionary(dict, offset, length); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/DeflaterConstants.java b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterConstants.java new file mode 100644 index 00000000..b3985f99 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterConstants.java @@ -0,0 +1,77 @@ +/* net.sf.jazzlib.DeflaterConstants + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +interface DeflaterConstants { + final static boolean DEBUGGING = false; + + final static int STORED_BLOCK = 0; + final static int STATIC_TREES = 1; + final static int DYN_TREES = 2; + final static int PRESET_DICT = 0x20; + + final static int DEFAULT_MEM_LEVEL = 8; + + final static int MAX_MATCH = 258; + final static int MIN_MATCH = 3; + + final static int MAX_WBITS = 15; + final static int WSIZE = 1 << MAX_WBITS; + final static int WMASK = WSIZE - 1; + + final static int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + final static int HASH_SIZE = 1 << HASH_BITS; + final static int HASH_MASK = HASH_SIZE - 1; + final static int HASH_SHIFT = ((HASH_BITS + MIN_MATCH) - 1) / MIN_MATCH; + + final static int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + final static int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + final static int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + final static int MAX_BLOCK_SIZE = Math.min(65535, PENDING_BUF_SIZE - 5); + + final static int DEFLATE_STORED = 0; + final static int DEFLATE_FAST = 1; + final static int DEFLATE_SLOW = 2; + + final static int GOOD_LENGTH[] = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; + final static int MAX_LAZY[] = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; + final static int NICE_LENGTH[] = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; + final static int MAX_CHAIN[] = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; + final static int COMPR_FUNC[] = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/DeflaterEngine.java b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterEngine.java new file mode 100644 index 00000000..814d0c32 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterEngine.java @@ -0,0 +1,674 @@ +/* net.sf.jazzlib.DeflaterEngine + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +class DeflaterEngine implements DeflaterConstants { + private final static int TOO_FAR = 4096; + + private int ins_h; + + /** + * Hashtable, hashing three characters to an index for window, so that + * window[index]..window[index+2] have this hash code. Note that the array + * should really be unsigned short, so you need to and the values with + * 0xffff. + */ + private final short[] head; + + /** + * prev[index & WMASK] points to the previous index that has the same hash + * code as the string starting at index. This way entries with the same hash + * code are in a linked list. Note that the array should really be unsigned + * short, so you need to and the values with 0xffff. + */ + private final short[] prev; + + private int matchStart, matchLen; + private boolean prevAvailable; + private int blockStart; + + /** + * strstart points to the current character in window. + */ + private int strstart; + + /** + * lookahead is the number of characters starting at strstart in window that + * are valid. So window[strstart] until window[strstart+lookahead-1] are + * valid characters. + */ + private int lookahead; + + /** + * This array contains the part of the uncompressed stream that is of + * relevance. The current character is indexed by strstart. + */ + private final byte[] window; + + private int strategy, max_chain, max_lazy, niceLength, goodLength; + + /** The current compression function. */ + private int comprFunc; + + /** The input data for compression. */ + private byte[] inputBuf; + + /** The total bytes of input read. */ + private int totalIn; + + /** The offset into inputBuf, where input data starts. */ + private int inputOff; + + /** The end offset of the input data. */ + private int inputEnd; + + private final DeflaterPending pending; + private final DeflaterHuffman huffman; + + /** The adler checksum */ + private final Adler32 adler; + + /* + * DEFLATE ALGORITHM: + * + * The uncompressed stream is inserted into the window array. When the + * window array is full the first half is thrown away and the second half is + * copied to the beginning. + * + * The head array is a hash table. Three characters build a hash value and + * they the value points to the corresponding index in window of the last + * string with this hash. The prev array implements a linked list of matches + * with the same hash: prev[index & WMASK] points to the previous index with + * the same hash. + */ + + DeflaterEngine(final DeflaterPending pending) { + this.pending = pending; + huffman = new DeflaterHuffman(pending); + adler = new Adler32(); + + window = new byte[2 * WSIZE]; + head = new short[HASH_SIZE]; + prev = new short[WSIZE]; + + /* + * We start at index 1, to avoid a implementation deficiency, that we + * cannot build a repeat pattern at index 0. + */ + blockStart = strstart = 1; + } + + public void reset() { + huffman.reset(); + adler.reset(); + blockStart = strstart = 1; + lookahead = 0; + totalIn = 0; + prevAvailable = false; + matchLen = MIN_MATCH - 1; + for (int i = 0; i < HASH_SIZE; i++) { + head[i] = 0; + } + for (int i = 0; i < WSIZE; i++) { + prev[i] = 0; + } + } + + public final void resetAdler() { + adler.reset(); + } + + public final int getAdler() { + final int chksum = (int) adler.getValue(); + return chksum; + } + + public final int getTotalIn() { + return totalIn; + } + + public final void setStrategy(final int strat) { + strategy = strat; + } + + public void setLevel(final int lvl) { + goodLength = DeflaterConstants.GOOD_LENGTH[lvl]; + max_lazy = DeflaterConstants.MAX_LAZY[lvl]; + niceLength = DeflaterConstants.NICE_LENGTH[lvl]; + max_chain = DeflaterConstants.MAX_CHAIN[lvl]; + + if (DeflaterConstants.COMPR_FUNC[lvl] != comprFunc) { + if (DeflaterConstants.DEBUGGING) { + System.err.println("Change from " + comprFunc + " to " + + DeflaterConstants.COMPR_FUNC[lvl]); + } + switch (comprFunc) { + case DEFLATE_STORED: + if (strstart > blockStart) { + huffman.flushStoredBlock(window, blockStart, strstart + - blockStart, false); + blockStart = strstart; + } + updateHash(); + break; + case DEFLATE_FAST: + if (strstart > blockStart) { + huffman.flushBlock(window, blockStart, strstart + - blockStart, false); + blockStart = strstart; + } + break; + case DEFLATE_SLOW: + if (prevAvailable) { + huffman.tallyLit(window[strstart - 1] & 0xff); + } + if (strstart > blockStart) { + huffman.flushBlock(window, blockStart, strstart + - blockStart, false); + blockStart = strstart; + } + prevAvailable = false; + matchLen = MIN_MATCH - 1; + break; + } + comprFunc = COMPR_FUNC[lvl]; + } + } + + private final void updateHash() { + if (DEBUGGING) { + System.err.println("updateHash: " + strstart); + } + ins_h = (window[strstart] << HASH_SHIFT) ^ window[strstart + 1]; + } + + /** + * Inserts the current string in the head hash and returns the previous + * value for this hash. + */ + private final int insertString() { + short match; + final int hash = ((ins_h << HASH_SHIFT) ^ window[strstart + + (MIN_MATCH - 1)]) + & HASH_MASK; + + if (DEBUGGING) { + if (hash != (((window[strstart] << (2 * HASH_SHIFT)) + ^ (window[strstart + 1] << HASH_SHIFT) ^ (window[strstart + 2])) & HASH_MASK)) { + throw new InternalError("hash inconsistent: " + hash + "/" + + window[strstart] + "," + window[strstart + 1] + "," + + window[strstart + 2] + "," + HASH_SHIFT); + } + } + + prev[strstart & WMASK] = match = head[hash]; + head[hash] = (short) strstart; + ins_h = hash; + return match & 0xffff; + } + + private void slideWindow() { + System.arraycopy(window, WSIZE, window, 0, WSIZE); + matchStart -= WSIZE; + strstart -= WSIZE; + blockStart -= WSIZE; + + /* + * Slide the hash table (could be avoided with 32 bit values at the + * expense of memory usage). + */ + for (int i = 0; i < HASH_SIZE; i++) { + final int m = head[i] & 0xffff; + head[i] = m >= WSIZE ? (short) (m - WSIZE) : 0; + } + + /* + * Slide the prev table. + */ + for (int i = 0; i < WSIZE; i++) { + final int m = prev[i] & 0xffff; + prev[i] = m >= WSIZE ? (short) (m - WSIZE) : 0; + } + } + + /** + * Fill the window when the lookahead becomes insufficient. Updates strstart + * and lookahead. + * + * OUT assertions: strstart + lookahead <= 2*WSIZE lookahead >= + * MIN_LOOKAHEAD or inputOff == inputEnd + */ + private void fillWindow() { + /* + * If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (strstart >= (WSIZE + MAX_DIST)) { + slideWindow(); + } + + /* + * If there is not enough lookahead, but still some input left, read in + * the input + */ + while ((lookahead < DeflaterConstants.MIN_LOOKAHEAD) + && (inputOff < inputEnd)) { + int more = (2 * WSIZE) - lookahead - strstart; + + if (more > (inputEnd - inputOff)) { + more = inputEnd - inputOff; + } + + System.arraycopy(inputBuf, inputOff, window, strstart + lookahead, + more); + adler.update(inputBuf, inputOff, more); + inputOff += more; + totalIn += more; + lookahead += more; + } + + if (lookahead >= MIN_MATCH) { + updateHash(); + } + } + + /** + * Find the best (longest) string in the window matching the string starting + * at strstart. + * + * Preconditions: strstart + MAX_MATCH <= window.length. + * + * + * @param curMatch + */ + private boolean findLongestMatch(int curMatch) { + int chainLength = this.max_chain; + int niceLength = this.niceLength; + final short[] prev = this.prev; + int scan = this.strstart; + int match; + int best_end = this.strstart + matchLen; + int best_len = Math.max(matchLen, MIN_MATCH - 1); + + final int limit = Math.max(strstart - MAX_DIST, 0); + + final int strend = (scan + MAX_MATCH) - 1; + byte scan_end1 = window[best_end - 1]; + byte scan_end = window[best_end]; + + /* Do not waste too much time if we already have a good match: */ + if (best_len >= this.goodLength) { + chainLength >>= 2; + } + + /* + * Do not look for matches beyond the end of the input. This is + * necessary to make deflate deterministic. + */ + if (niceLength > lookahead) { + niceLength = lookahead; + } + + if (DeflaterConstants.DEBUGGING + && (strstart > ((2 * WSIZE) - MIN_LOOKAHEAD))) { + throw new InternalError("need lookahead"); + } + + do { + if (DeflaterConstants.DEBUGGING && (curMatch >= strstart)) { + throw new InternalError("future match"); + } + if ((window[curMatch + best_len] != scan_end) + || (window[(curMatch + best_len) - 1] != scan_end1) + || (window[curMatch] != window[scan]) + || (window[curMatch + 1] != window[scan + 1])) { + continue; + } + + match = curMatch + 2; + scan += 2; + + /* + * We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart+258. + */ + while ((window[++scan] == window[++match]) + && (window[++scan] == window[++match]) + && (window[++scan] == window[++match]) + && (window[++scan] == window[++match]) + && (window[++scan] == window[++match]) + && (window[++scan] == window[++match]) + && (window[++scan] == window[++match]) + && (window[++scan] == window[++match]) && (scan < strend)) { + ; + } + + if (scan > best_end) { + // if (DeflaterConstants.DEBUGGING && ins_h == 0) + // System.err.println("Found match: "+curMatch+"-"+(scan-strstart)); + matchStart = curMatch; + best_end = scan; + best_len = scan - strstart; + if (best_len >= niceLength) { + break; + } + + scan_end1 = window[best_end - 1]; + scan_end = window[best_end]; + } + scan = strstart; + } while (((curMatch = (prev[curMatch & WMASK] & 0xffff)) > limit) + && (--chainLength != 0)); + + matchLen = Math.min(best_len, lookahead); + return matchLen >= MIN_MATCH; + } + + void setDictionary(final byte[] buffer, int offset, int length) { + if (DeflaterConstants.DEBUGGING && (strstart != 1)) { + throw new IllegalStateException("strstart not 1"); + } + adler.update(buffer, offset, length); + if (length < MIN_MATCH) { + return; + } + if (length > MAX_DIST) { + offset += length - MAX_DIST; + length = MAX_DIST; + } + + System.arraycopy(buffer, offset, window, strstart, length); + + updateHash(); + length--; + while (--length > 0) { + insertString(); + strstart++; + } + strstart += 2; + blockStart = strstart; + } + + private boolean deflateStored(final boolean flush, final boolean finish) { + if (!flush && (lookahead == 0)) { + return false; + } + + strstart += lookahead; + lookahead = 0; + + int storedLen = strstart - blockStart; + + if ((storedLen >= DeflaterConstants.MAX_BLOCK_SIZE) + /* Block is full */ + || ((blockStart < WSIZE) && (storedLen >= MAX_DIST)) + /* Block may move out of window */ + || flush) { + boolean lastBlock = finish; + if (storedLen > DeflaterConstants.MAX_BLOCK_SIZE) { + storedLen = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + + if (DeflaterConstants.DEBUGGING) { + System.err.println("storedBlock[" + storedLen + "," + lastBlock + + "]"); + } + + huffman.flushStoredBlock(window, blockStart, storedLen, lastBlock); + blockStart += storedLen; + return !lastBlock; + } + return true; + } + + private boolean deflateFast(final boolean flush, final boolean finish) { + if ((lookahead < MIN_LOOKAHEAD) && !flush) { + return false; + } + + while ((lookahead >= MIN_LOOKAHEAD) || flush) { + if (lookahead == 0) { + /* We are flushing everything */ + huffman.flushBlock(window, blockStart, strstart - blockStart, + finish); + blockStart = strstart; + return false; + } + + if (strstart > ((2 * WSIZE) - MIN_LOOKAHEAD)) { + /* + * slide window, as findLongestMatch need this. This should only + * happen when flushing and the window is almost full. + */ + slideWindow(); + } + + int hashHead; + if ((lookahead >= MIN_MATCH) && ((hashHead = insertString()) != 0) + && (strategy != Deflater.HUFFMAN_ONLY) + && ((strstart - hashHead) <= MAX_DIST) + && findLongestMatch(hashHead)) { + /* longestMatch sets matchStart and matchLen */ + if (DeflaterConstants.DEBUGGING) { + for (int i = 0; i < matchLen; i++) { + if (window[strstart + i] != window[matchStart + i]) { + throw new InternalError(); + } + } + } + huffman.tallyDist(strstart - matchStart, matchLen); + + lookahead -= matchLen; + if ((matchLen <= max_lazy) && (lookahead >= MIN_MATCH)) { + while (--matchLen > 0) { + strstart++; + insertString(); + } + strstart++; + } else { + strstart += matchLen; + if (lookahead >= (MIN_MATCH - 1)) { + updateHash(); + } + } + matchLen = MIN_MATCH - 1; + continue; + } else { + /* No match found */ + huffman.tallyLit(window[strstart] & 0xff); + strstart++; + lookahead--; + } + + if (huffman.isFull()) { + final boolean lastBlock = finish && (lookahead == 0); + huffman.flushBlock(window, blockStart, strstart - blockStart, + lastBlock); + blockStart = strstart; + return !lastBlock; + } + } + return true; + } + + private boolean deflateSlow(final boolean flush, final boolean finish) { + if ((lookahead < MIN_LOOKAHEAD) && !flush) { + return false; + } + + while ((lookahead >= MIN_LOOKAHEAD) || flush) { + if (lookahead == 0) { + if (prevAvailable) { + huffman.tallyLit(window[strstart - 1] & 0xff); + } + prevAvailable = false; + + /* We are flushing everything */ + if (DeflaterConstants.DEBUGGING && !flush) { + throw new InternalError("Not flushing, but no lookahead"); + } + huffman.flushBlock(window, blockStart, strstart - blockStart, + finish); + blockStart = strstart; + return false; + } + + if (strstart >= ((2 * WSIZE) - MIN_LOOKAHEAD)) { + /* + * slide window, as findLongestMatch need this. This should only + * happen when flushing and the window is almost full. + */ + slideWindow(); + } + + final int prevMatch = matchStart; + int prevLen = matchLen; + if (lookahead >= MIN_MATCH) { + final int hashHead = insertString(); + if ((strategy != Deflater.HUFFMAN_ONLY) && (hashHead != 0) + && ((strstart - hashHead) <= MAX_DIST) + && findLongestMatch(hashHead)) { + /* longestMatch sets matchStart and matchLen */ + + /* Discard match if too small and too far away */ + if ((matchLen <= 5) + && ((strategy == Deflater.FILTERED) || ((matchLen == MIN_MATCH) && ((strstart - matchStart) > TOO_FAR)))) { + matchLen = MIN_MATCH - 1; + } + } + } + + /* previous match was better */ + if ((prevLen >= MIN_MATCH) && (matchLen <= prevLen)) { + if (DeflaterConstants.DEBUGGING) { + for (int i = 0; i < matchLen; i++) { + if (window[(strstart - 1) + i] != window[prevMatch + i]) { + throw new InternalError(); + } + } + } + huffman.tallyDist(strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do { + strstart++; + lookahead--; + if (lookahead >= MIN_MATCH) { + insertString(); + } + } while (--prevLen > 0); + strstart++; + lookahead--; + prevAvailable = false; + matchLen = MIN_MATCH - 1; + } else { + if (prevAvailable) { + huffman.tallyLit(window[strstart - 1] & 0xff); + } + prevAvailable = true; + strstart++; + lookahead--; + } + + if (huffman.isFull()) { + int len = strstart - blockStart; + if (prevAvailable) { + len--; + } + final boolean lastBlock = (finish && (lookahead == 0) && !prevAvailable); + huffman.flushBlock(window, blockStart, len, lastBlock); + blockStart += len; + return !lastBlock; + } + } + return true; + } + + public boolean deflate(final boolean flush, final boolean finish) { + boolean progress; + do { + fillWindow(); + final boolean canFlush = flush && (inputOff == inputEnd); + if (DeflaterConstants.DEBUGGING) { + System.err.println("window: [" + blockStart + "," + strstart + + "," + lookahead + "], " + comprFunc + "," + canFlush); + } + switch (comprFunc) { + case DEFLATE_STORED: + progress = deflateStored(canFlush, finish); + break; + case DEFLATE_FAST: + progress = deflateFast(canFlush, finish); + break; + case DEFLATE_SLOW: + progress = deflateSlow(canFlush, finish); + break; + default: + throw new InternalError(); + } + } while (pending.isFlushed() /* repeat while we have no pending output */ + && progress); /* and progress was made */ + + return progress; + } + + public void setInput(final byte[] buf, final int off, final int len) { + if (inputOff < inputEnd) { + throw new IllegalStateException( + "Old input was not completely processed"); + } + + final int end = off + len; + + /* + * We want to throw an ArrayIndexOutOfBoundsException early. The check + * is very tricky: it also handles integer wrap around. + */ + if ((0 > off) || (off > end) || (end > buf.length)) { + throw new ArrayIndexOutOfBoundsException(); + } + + inputBuf = buf; + inputOff = off; + inputEnd = end; + } + + public final boolean needsInput() { + return inputEnd == inputOff; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/DeflaterHuffman.java b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterHuffman.java new file mode 100644 index 00000000..75913ac6 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterHuffman.java @@ -0,0 +1,748 @@ +/* net.sf.jazzlib.DeflaterHuffman + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/** + * This is the DeflaterHuffman class. + * + * This class is not thread safe. This is inherent in the API, due to the + * split of deflate and setInput. + * + * @author Jochen Hoenicke + * @date Jan 6, 2000 + */ +class DeflaterHuffman { + private static final int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + private static final int LITERAL_NUM = 286; + private static final int DIST_NUM = 30; + private static final int BITLEN_NUM = 19; + private static final int REP_3_6 = 16; + private static final int REP_3_10 = 17; + private static final int REP_11_138 = 18; + private static final int EOF_SYMBOL = 256; + private static final int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, + 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + private final static String bit4Reverse = "\000\010\004\014\002\012\006\016\001\011\005\015\003\013\007\017"; + + class Tree { + short[] freqs; + short[] codes; + byte[] length; + int[] bl_counts; + int minNumCodes, numCodes; + int maxLength; + + Tree(final int elems, final int minCodes, final int maxLength) { + this.minNumCodes = minCodes; + this.maxLength = maxLength; + freqs = new short[elems]; + bl_counts = new int[maxLength]; + } + + void reset() { + for (int i = 0; i < freqs.length; i++) { + freqs[i] = 0; + } + codes = null; + length = null; + } + + final void writeSymbol(final int code) { + if (DeflaterConstants.DEBUGGING) { + freqs[code]--; + // System.err.print("writeSymbol("+freqs.length+","+code+"): "); + } + pending.writeBits(codes[code] & 0xffff, length[code]); + } + + final void checkEmpty() { + boolean empty = true; + for (int i = 0; i < freqs.length; i++) { + if (freqs[i] != 0) { + System.err.println("freqs[" + i + "] == " + freqs[i]); + empty = false; + } + } + if (!empty) { + throw new InternalError(); + } + System.err.println("checkEmpty suceeded!"); + } + + void setStaticCodes(final short[] stCodes, final byte[] stLength) { + codes = stCodes; + length = stLength; + } + + public void buildCodes() { + final int[] nextCode = new int[maxLength]; + int code = 0; + codes = new short[freqs.length]; + + if (DeflaterConstants.DEBUGGING) { + System.err.println("buildCodes: " + freqs.length); + } + for (int bits = 0; bits < maxLength; bits++) { + nextCode[bits] = code; + code += bl_counts[bits] << (15 - bits); + if (DeflaterConstants.DEBUGGING) { + System.err.println("bits: " + (bits + 1) + " count: " + + bl_counts[bits] + " nextCode: " + + Integer.toHexString(code)); + } + } + if (DeflaterConstants.DEBUGGING && (code != 65536)) { + throw new RuntimeException("Inconsistent bl_counts!"); + } + + for (int i = 0; i < numCodes; i++) { + final int bits = length[i]; + if (bits > 0) { + if (DeflaterConstants.DEBUGGING) { + System.err.println("codes[" + i + "] = rev(" + + Integer.toHexString(nextCode[bits - 1]) + + ")," + bits); + } + codes[i] = bitReverse(nextCode[bits - 1]); + nextCode[bits - 1] += 1 << (16 - bits); + } + } + } + + private void buildLength(final int childs[]) { + this.length = new byte[freqs.length]; + final int numNodes = childs.length / 2; + final int numLeafs = (numNodes + 1) / 2; + int overflow = 0; + + for (int i = 0; i < maxLength; i++) { + bl_counts[i] = 0; + } + + /* First calculate optimal bit lengths */ + final int lengths[] = new int[numNodes]; + lengths[numNodes - 1] = 0; + for (int i = numNodes - 1; i >= 0; i--) { + if (childs[(2 * i) + 1] != -1) { + int bitLength = lengths[i] + 1; + if (bitLength > maxLength) { + bitLength = maxLength; + overflow++; + } + lengths[childs[2 * i]] = lengths[childs[(2 * i) + 1]] = bitLength; + } else { + /* A leaf node */ + final int bitLength = lengths[i]; + bl_counts[bitLength - 1]++; + this.length[childs[2 * i]] = (byte) lengths[i]; + } + } + + if (DeflaterConstants.DEBUGGING) { + System.err.println("Tree " + freqs.length + " lengths:"); + for (int i = 0; i < numLeafs; i++) { + System.err.println("Node " + childs[2 * i] + " freq: " + + freqs[childs[2 * i]] + " len: " + + length[childs[2 * i]]); + } + } + + if (overflow == 0) { + return; + } + + int incrBitLen = maxLength - 1; + do { + /* Find the first bit length which could increase: */ + while (bl_counts[--incrBitLen] == 0) { + ; + } + + /* + * Move this node one down and remove a corresponding amount of + * overflow nodes. + */ + do { + bl_counts[incrBitLen]--; + bl_counts[++incrBitLen]++; + overflow -= 1 << (maxLength - 1 - incrBitLen); + } while ((overflow > 0) && (incrBitLen < (maxLength - 1))); + } while (overflow > 0); + + /* + * We may have overshot above. Move some nodes from maxLength to + * maxLength-1 in that case. + */ + bl_counts[maxLength - 1] += overflow; + bl_counts[maxLength - 2] -= overflow; + + /* + * Now recompute all bit lengths, scanning in increasing frequency. + * It is simpler to reconstruct all lengths instead of fixing only + * the wrong ones. This idea is taken from 'ar' written by Haruhiko + * Okumura. + * + * The nodes were inserted with decreasing frequency into the childs + * array. + */ + int nodePtr = 2 * numLeafs; + for (int bits = maxLength; bits != 0; bits--) { + int n = bl_counts[bits - 1]; + while (n > 0) { + final int childPtr = 2 * childs[nodePtr++]; + if (childs[childPtr + 1] == -1) { + /* We found another leaf */ + length[childs[childPtr]] = (byte) bits; + n--; + } + } + } + if (DeflaterConstants.DEBUGGING) { + System.err.println("*** After overflow elimination. ***"); + for (int i = 0; i < numLeafs; i++) { + System.err.println("Node " + childs[2 * i] + " freq: " + + freqs[childs[2 * i]] + " len: " + + length[childs[2 * i]]); + } + } + } + + void buildTree() { + final int numSymbols = freqs.length; + + /* + * heap is a priority queue, sorted by frequency, least frequent + * nodes first. The heap is a binary tree, with the property, that + * the parent node is smaller than both child nodes. This assures + * that the smallest node is the first parent. + * + * The binary tree is encoded in an array: 0 is root node and the + * nodes 2*n+1, 2*n+2 are the child nodes of node n. + */ + final int[] heap = new int[numSymbols]; + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) { + final int freq = freqs[n]; + if (freq != 0) { + /* Insert n into heap */ + int pos = heapLen++; + int ppos; + while ((pos > 0) + && (freqs[heap[ppos = (pos - 1) / 2]] > freq)) { + heap[pos] = heap[ppos]; + pos = ppos; + } + heap[pos] = n; + maxCode = n; + } + } + + /* + * We could encode a single literal with 0 bits but then we don't + * see the literals. Therefore we force at least two literals to + * avoid this case. We don't care about order in this case, both + * literals get a 1 bit code. + */ + while (heapLen < 2) { + final int node = maxCode < 2 ? ++maxCode : 0; + heap[heapLen++] = node; + } + + numCodes = Math.max(maxCode + 1, minNumCodes); + + final int numLeafs = heapLen; + final int[] childs = new int[(4 * heapLen) - 2]; + final int[] values = new int[(2 * heapLen) - 1]; + int numNodes = numLeafs; + for (int i = 0; i < heapLen; i++) { + final int node = heap[i]; + childs[2 * i] = node; + childs[(2 * i) + 1] = -1; + values[i] = freqs[node] << 8; + heap[i] = i; + } + + /* + * Construct the Huffman tree by repeatedly combining the least two + * frequent nodes. + */ + do { + final int first = heap[0]; + int last = heap[--heapLen]; + + /* Propagate the hole to the leafs of the heap */ + int ppos = 0; + int path = 1; + while (path < heapLen) { + if (((path + 1) < heapLen) + && (values[heap[path]] > values[heap[path + 1]])) { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = (path * 2) + 1; + } + + /* + * Now propagate the last element down along path. Normally it + * shouldn't go too deep. + */ + int lastVal = values[last]; + while (((path = ppos) > 0) + && (values[heap[ppos = (path - 1) / 2]] > lastVal)) { + heap[path] = heap[ppos]; + } + heap[path] = last; + + final int second = heap[0]; + + /* Create a new node father of first and second */ + last = numNodes++; + childs[2 * last] = first; + childs[(2 * last) + 1] = second; + final int mindepth = Math.min(values[first] & 0xff, + values[second] & 0xff); + values[last] = lastVal = ((values[first] + values[second]) - mindepth) + 1; + + /* Again, propagate the hole to the leafs */ + ppos = 0; + path = 1; + while (path < heapLen) { + if (((path + 1) < heapLen) + && (values[heap[path]] > values[heap[path + 1]])) { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = (ppos * 2) + 1; + } + + /* Now propagate the new element down along path */ + while (((path = ppos) > 0) + && (values[heap[ppos = (path - 1) / 2]] > lastVal)) { + heap[path] = heap[ppos]; + } + heap[path] = last; + } while (heapLen > 1); + + if (heap[0] != ((childs.length / 2) - 1)) { + throw new RuntimeException("Weird!"); + } + + buildLength(childs); + } + + int getEncodedLength() { + int len = 0; + for (int i = 0; i < freqs.length; i++) { + len += freqs[i] * length[i]; + } + return len; + } + + void calcBLFreq(final Tree blTree) { + int max_count; /* max repeat count */ + int min_count; /* min repeat count */ + int count; /* repeat count of the current code */ + int curlen = -1; /* length of current code */ + + int i = 0; + while (i < numCodes) { + count = 1; + final int nextlen = length[i]; + if (nextlen == 0) { + max_count = 138; + min_count = 3; + } else { + max_count = 6; + min_count = 3; + if (curlen != nextlen) { + blTree.freqs[nextlen]++; + count = 0; + } + } + curlen = nextlen; + i++; + + while ((i < numCodes) && (curlen == length[i])) { + i++; + if (++count >= max_count) { + break; + } + } + + if (count < min_count) { + blTree.freqs[curlen] += count; + } else if (curlen != 0) { + blTree.freqs[REP_3_6]++; + } else if (count <= 10) { + blTree.freqs[REP_3_10]++; + } else { + blTree.freqs[REP_11_138]++; + } + } + } + + void writeTree(final Tree blTree) { + int max_count; /* max repeat count */ + int min_count; /* min repeat count */ + int count; /* repeat count of the current code */ + int curlen = -1; /* length of current code */ + + int i = 0; + while (i < numCodes) { + count = 1; + final int nextlen = length[i]; + if (nextlen == 0) { + max_count = 138; + min_count = 3; + } else { + max_count = 6; + min_count = 3; + if (curlen != nextlen) { + blTree.writeSymbol(nextlen); + count = 0; + } + } + curlen = nextlen; + i++; + + while ((i < numCodes) && (curlen == length[i])) { + i++; + if (++count >= max_count) { + break; + } + } + + if (count < min_count) { + while (count-- > 0) { + blTree.writeSymbol(curlen); + } + } else if (curlen != 0) { + blTree.writeSymbol(REP_3_6); + pending.writeBits(count - 3, 2); + } else if (count <= 10) { + blTree.writeSymbol(REP_3_10); + pending.writeBits(count - 3, 3); + } else { + blTree.writeSymbol(REP_11_138); + pending.writeBits(count - 11, 7); + } + } + } + } + + DeflaterPending pending; + private final Tree literalTree, distTree, blTree; + + private final short d_buf[]; + private final byte l_buf[]; + private int last_lit; + private int extra_bits; + + private static short staticLCodes[]; + private static byte staticLLength[]; + private static short staticDCodes[]; + private static byte staticDLength[]; + + /** + * Reverse the bits of a 16 bit value. + */ + static short bitReverse(final int value) { + return (short) ((bit4Reverse.charAt(value & 0xf) << 12) + | (bit4Reverse.charAt((value >> 4) & 0xf) << 8) + | (bit4Reverse.charAt((value >> 8) & 0xf) << 4) | bit4Reverse + .charAt(value >> 12)); + } + + static { + /* See RFC 1951 3.2.6 */ + /* Literal codes */ + staticLCodes = new short[LITERAL_NUM]; + staticLLength = new byte[LITERAL_NUM]; + int i = 0; + while (i < 144) { + staticLCodes[i] = bitReverse((0x030 + i) << 8); + staticLLength[i++] = 8; + } + while (i < 256) { + staticLCodes[i] = bitReverse(((0x190 - 144) + i) << 7); + staticLLength[i++] = 9; + } + while (i < 280) { + staticLCodes[i] = bitReverse(((0x000 - 256) + i) << 9); + staticLLength[i++] = 7; + } + while (i < LITERAL_NUM) { + staticLCodes[i] = bitReverse(((0x0c0 - 280) + i) << 8); + staticLLength[i++] = 8; + } + + /* Distant codes */ + staticDCodes = new short[DIST_NUM]; + staticDLength = new byte[DIST_NUM]; + for (i = 0; i < DIST_NUM; i++) { + staticDCodes[i] = bitReverse(i << 11); + staticDLength[i] = 5; + } + } + + public DeflaterHuffman(final DeflaterPending pending) { + this.pending = pending; + + literalTree = new Tree(LITERAL_NUM, 257, 15); + distTree = new Tree(DIST_NUM, 1, 15); + blTree = new Tree(BITLEN_NUM, 4, 7); + + d_buf = new short[BUFSIZE]; + l_buf = new byte[BUFSIZE]; + } + + public final void reset() { + last_lit = 0; + extra_bits = 0; + literalTree.reset(); + distTree.reset(); + blTree.reset(); + } + + private final int l_code(int len) { + if (len == 255) { + return 285; + } + + int code = 257; + while (len >= 8) { + code += 4; + len >>= 1; + } + return code + len; + } + + private final int d_code(int distance) { + int code = 0; + while (distance >= 4) { + code += 2; + distance >>= 1; + } + return code + distance; + } + + public void sendAllTrees(final int blTreeCodes) { + blTree.buildCodes(); + literalTree.buildCodes(); + distTree.buildCodes(); + pending.writeBits(literalTree.numCodes - 257, 5); + pending.writeBits(distTree.numCodes - 1, 5); + pending.writeBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) { + pending.writeBits(blTree.length[BL_ORDER[rank]], 3); + } + literalTree.writeTree(blTree); + distTree.writeTree(blTree); + if (DeflaterConstants.DEBUGGING) { + blTree.checkEmpty(); + } + } + + public void compressBlock() { + for (int i = 0; i < last_lit; i++) { + final int litlen = l_buf[i] & 0xff; + int dist = d_buf[i]; + if (dist-- != 0) { + if (DeflaterConstants.DEBUGGING) { + System.err.print("[" + (dist + 1) + "," + (litlen + 3) + + "]: "); + } + + final int lc = l_code(litlen); + literalTree.writeSymbol(lc); + + int bits = (lc - 261) / 4; + if ((bits > 0) && (bits <= 5)) { + pending.writeBits(litlen & ((1 << bits) - 1), bits); + } + + final int dc = d_code(dist); + distTree.writeSymbol(dc); + + bits = (dc / 2) - 1; + if (bits > 0) { + pending.writeBits(dist & ((1 << bits) - 1), bits); + } + } else { + if (DeflaterConstants.DEBUGGING) { + if ((litlen > 32) && (litlen < 127)) { + System.err.print("(" + (char) litlen + "): "); + } else { + System.err.print("{" + litlen + "}: "); + } + } + literalTree.writeSymbol(litlen); + } + } + if (DeflaterConstants.DEBUGGING) { + System.err.print("EOF: "); + } + literalTree.writeSymbol(EOF_SYMBOL); + if (DeflaterConstants.DEBUGGING) { + literalTree.checkEmpty(); + distTree.checkEmpty(); + } + } + + public void flushStoredBlock(final byte[] stored, final int stored_offset, + final int stored_len, final boolean lastBlock) { + if (DeflaterConstants.DEBUGGING) { + System.err.println("Flushing stored block " + stored_len); + } + pending.writeBits((DeflaterConstants.STORED_BLOCK << 1) + + (lastBlock ? 1 : 0), 3); + pending.alignToByte(); + pending.writeShort(stored_len); + pending.writeShort(~stored_len); + pending.writeBlock(stored, stored_offset, stored_len); + reset(); + } + + public void flushBlock(final byte[] stored, final int stored_offset, + final int stored_len, final boolean lastBlock) { + literalTree.freqs[EOF_SYMBOL]++; + + /* Build trees */ + literalTree.buildTree(); + distTree.buildTree(); + + /* Calculate bitlen frequency */ + literalTree.calcBLFreq(blTree); + distTree.calcBLFreq(blTree); + + /* Build bitlen tree */ + blTree.buildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) { + if (blTree.length[BL_ORDER[i]] > 0) { + blTreeCodes = i + 1; + } + } + int opt_len = 14 + (blTreeCodes * 3) + blTree.getEncodedLength() + + literalTree.getEncodedLength() + distTree.getEncodedLength() + + extra_bits; + + int static_len = extra_bits; + for (int i = 0; i < LITERAL_NUM; i++) { + static_len += literalTree.freqs[i] * staticLLength[i]; + } + for (int i = 0; i < DIST_NUM; i++) { + static_len += distTree.freqs[i] * staticDLength[i]; + } + if (opt_len >= static_len) { + /* Force static trees */ + opt_len = static_len; + } + + if ((stored_offset >= 0) && ((stored_len + 4) < (opt_len >> 3))) { + /* Store Block */ + if (DeflaterConstants.DEBUGGING) { + System.err.println("Storing, since " + stored_len + " < " + + opt_len + " <= " + static_len); + } + flushStoredBlock(stored, stored_offset, stored_len, lastBlock); + } else if (opt_len == static_len) { + /* Encode with static tree */ + pending.writeBits((DeflaterConstants.STATIC_TREES << 1) + + (lastBlock ? 1 : 0), 3); + literalTree.setStaticCodes(staticLCodes, staticLLength); + distTree.setStaticCodes(staticDCodes, staticDLength); + compressBlock(); + reset(); + } else { + /* Encode with dynamic tree */ + pending.writeBits((DeflaterConstants.DYN_TREES << 1) + + (lastBlock ? 1 : 0), 3); + sendAllTrees(blTreeCodes); + compressBlock(); + reset(); + } + } + + public final boolean isFull() { + return last_lit == BUFSIZE; + } + + public final boolean tallyLit(final int lit) { + if (DeflaterConstants.DEBUGGING) { + if ((lit > 32) && (lit < 127)) { + System.err.println("(" + (char) lit + ")"); + } else { + System.err.println("{" + lit + "}"); + } + } + d_buf[last_lit] = 0; + l_buf[last_lit++] = (byte) lit; + literalTree.freqs[lit]++; + return last_lit == BUFSIZE; + } + + public final boolean tallyDist(final int dist, final int len) { + if (DeflaterConstants.DEBUGGING) { + System.err.println("[" + dist + "," + len + "]"); + } + + d_buf[last_lit] = (short) dist; + l_buf[last_lit++] = (byte) (len - 3); + + final int lc = l_code(len - 3); + literalTree.freqs[lc]++; + if ((lc >= 265) && (lc < 285)) { + extra_bits += (lc - 261) / 4; + } + + final int dc = d_code(dist - 1); + distTree.freqs[dc]++; + if (dc >= 4) { + extra_bits += (dc / 2) - 1; + } + return last_lit == BUFSIZE; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/DeflaterOutputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterOutputStream.java new file mode 100644 index 00000000..bbc021fd --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterOutputStream.java @@ -0,0 +1,210 @@ +/* DeflaterOutputStream.java - Output filter for compressing. + Copyright (C) 1999, 2000, 2001, 2004 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/* Written using on-line Java Platform 1.2 API Specification + * and JCL book. + * Believed complete and correct. + */ + +/** + * This is a special FilterOutputStream deflating the bytes that are written + * through it. It uses the Deflater for deflating. + * + * A special thing to be noted is that flush() doesn't flush everything in Sun's + * JDK, but it does so in jazzlib. This is because Sun's Deflater doesn't have a + * way to flush() everything, without finishing the stream. + * + * @author Tom Tromey, Jochen Hoenicke + * @date Jan 11, 2001 + */ +public class DeflaterOutputStream extends FilterOutputStream { + /** + * This buffer is used temporarily to retrieve the bytes from the deflater + * and write them to the underlying output stream. + */ + protected byte[] buf; + + /** + * The deflater which is used to deflate the stream. + */ + protected Deflater def; + + /** + * Deflates everything in the def's input buffers. This will call + * def.deflate() until all bytes from the input buffers are + * processed. + */ + protected void deflate() throws IOException { + while (!def.needsInput()) { + final int len = def.deflate(buf, 0, buf.length); + + // System.err.println("DOS deflated " + len + " out of " + + // buf.length); + if (len <= 0) { + break; + } + out.write(buf, 0, len); + } + + if (!def.needsInput()) { + throw new InternalError("Can't deflate all input?"); + } + } + + /** + * Creates a new DeflaterOutputStream with a default Deflater and default + * buffer size. + * + * @param out + * the output stream where deflated output should be written. + */ + public DeflaterOutputStream(final OutputStream out) { + this(out, new Deflater(), 512); + } + + /** + * Creates a new DeflaterOutputStream with the given Deflater and default + * buffer size. + * + * @param out + * the output stream where deflated output should be written. + * @param defl + * the underlying deflater. + */ + public DeflaterOutputStream(final OutputStream out, final Deflater defl) { + this(out, defl, 512); + } + + /** + * Creates a new DeflaterOutputStream with the given Deflater and buffer + * size. + * + * @param out + * the output stream where deflated output should be written. + * @param defl + * the underlying deflater. + * @param bufsize + * the buffer size. + * @exception IllegalArgumentException + * if bufsize isn't positive. + */ + public DeflaterOutputStream(final OutputStream out, final Deflater defl, + final int bufsize) { + super(out); + if (bufsize <= 0) { + throw new IllegalArgumentException("bufsize <= 0"); + } + buf = new byte[bufsize]; + def = defl; + } + + /** + * Flushes the stream by calling flush() on the deflater and then on the + * underlying stream. This ensures that all bytes are flushed. This function + * doesn't work in Sun's JDK, but only in jazzlib. + */ + @Override + public void flush() throws IOException { + def.flush(); + deflate(); + out.flush(); + } + + /** + * Finishes the stream by calling finish() on the deflater. This was the + * only way to ensure that all bytes are flushed in Sun's JDK. + */ + public void finish() throws IOException { + def.finish(); + while (!def.finished()) { + final int len = def.deflate(buf, 0, buf.length); + if (len <= 0) { + break; + } + out.write(buf, 0, len); + } + if (!def.finished()) { + throw new InternalError("Can't deflate all input?"); + } + out.flush(); + } + + /** + * Calls finish () and closes the stream. + */ + @Override + public void close() throws IOException { + finish(); + out.close(); + } + + /** + * Writes a single byte to the compressed output stream. + * + * @param bval + * the byte value. + */ + @Override + public void write(final int bval) throws IOException { + final byte[] b = new byte[1]; + b[0] = (byte) bval; + write(b, 0, 1); + } + + /** + * Writes a len bytes from an array to the compressed stream. + * + * @param buf + * the byte array. + * @param off + * the offset into the byte array where to start. + * @param len + * the number of bytes to write. + */ + @Override + public void write(final byte[] buf, final int off, final int len) + throws IOException { + def.setInput(buf, off, len); + deflate(); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/DeflaterPending.java b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterPending.java new file mode 100644 index 00000000..e3f0dcaa --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/DeflaterPending.java @@ -0,0 +1,51 @@ +/* net.sf.jazzlib.DeflaterPending + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/** + * This class stores the pending output of the Deflater. + * + * @author Jochen Hoenicke + * @date Jan 5, 2000 + */ + +class DeflaterPending extends PendingBuffer { + public DeflaterPending() { + super(DeflaterConstants.PENDING_BUF_SIZE); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/GZIPInputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/GZIPInputStream.java new file mode 100644 index 00000000..e9111ede --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/GZIPInputStream.java @@ -0,0 +1,369 @@ +/* GZIPInputStream.java - Input filter for reading gzip file + Copyright (C) 1999, 2000, 2001, 2002, 2004 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * This filter stream is used to decompress a "GZIP" format stream. The "GZIP" + * format is described in RFC 1952. + * + * @author John Leuner + * @author Tom Tromey + * @since JDK 1.1 + */ +public class GZIPInputStream extends InflaterInputStream { + /** + * The magic number found at the start of a GZIP stream. + */ + public static final int GZIP_MAGIC = 0x1f8b; + + /** + * The mask for bit 0 of the flag byte. + */ + static final int FTEXT = 0x1; + + /** + * The mask for bit 1 of the flag byte. + */ + static final int FHCRC = 0x2; + + /** + * The mask for bit 2 of the flag byte. + */ + static final int FEXTRA = 0x4; + + /** + * The mask for bit 3 of the flag byte. + */ + static final int FNAME = 0x8; + + /** + * The mask for bit 4 of the flag byte. + */ + static final int FCOMMENT = 0x10; + + /** + * The CRC-32 checksum value for uncompressed data. + */ + protected CRC32 crc; + + /** + * Indicates whether or not the end of the stream has been reached. + */ + protected boolean eos; + + /** + * Indicates whether or not the GZIP header has been read in. + */ + private boolean readGZIPHeader; + + /** + * Creates a GZIPInputStream with the default buffer size. + * + * @param in + * The stream to read compressed data from (in GZIP format). + * + * @throws IOException + * if an error occurs during an I/O operation. + */ + public GZIPInputStream(final InputStream in) throws IOException { + this(in, 4096); + } + + /** + * Creates a GZIPInputStream with the specified buffer size. + * + * @param in + * The stream to read compressed data from (in GZIP format). + * @param size + * The size of the buffer to use. + * + * @throws IOException + * if an error occurs during an I/O operation. + * @throws IllegalArgumentException + * if size is less than or equal to 0. + */ + public GZIPInputStream(final InputStream in, final int size) + throws IOException { + super(in, new Inflater(true), size); + crc = new CRC32(); + } + + /** + * Closes the input stream. + * + * @throws IOException + * if an error occurs during an I/O operation. + */ + @Override + public void close() throws IOException { + // Nothing to do here. + super.close(); + } + + /** + * Reads in GZIP-compressed data and stores it in uncompressed form into an + * array of bytes. The method will block until either enough input data + * becomes available or the compressed stream reaches its end. + * + * @param buf + * the buffer into which the uncompressed data will be stored. + * @param offset + * the offset indicating where in buf the + * uncompressed data should be placed. + * @param len + * the number of uncompressed bytes to be read. + */ + @Override + public int read(final byte[] buf, final int offset, final int len) + throws IOException { + // We first have to slurp in the GZIP header, then we feed all the + // rest of the data to the superclass. + // + // As we do that we continually update the CRC32. Once the data is + // finished, we check the CRC32. + // + // This means we don't need our own buffer, as everything is done + // in the superclass. + if (!readGZIPHeader) { + readHeader(); + } + + if (eos) { + return -1; + } + + // System.err.println("GZIPIS.read(byte[], off, len ... " + offset + + // " and len " + len); + + /* + * We don't have to read the header, so we just grab data from the + * superclass. + */ + final int numRead = super.read(buf, offset, len); + if (numRead > 0) { + crc.update(buf, offset, numRead); + } + + if (inf.finished()) { + readFooter(); + } + return numRead; + } + + /** + * Reads in the GZIP header. + */ + private void readHeader() throws IOException { + /* 1. Check the two magic bytes */ + final CRC32 headCRC = new CRC32(); + int magic = in.read(); + if (magic < 0) { + eos = true; + return; + } + headCRC.update(magic); + if (magic != (GZIP_MAGIC >> 8)) { + throw new IOException( + "Error in GZIP header, first byte doesn't match"); + } + + magic = in.read(); + if (magic != (GZIP_MAGIC & 0xff)) { + throw new IOException( + "Error in GZIP header, second byte doesn't match"); + } + headCRC.update(magic); + + /* 2. Check the compression type (must be 8) */ + final int CM = in.read(); + if (CM != 8) { + throw new IOException( + "Error in GZIP header, data not in deflate format"); + } + headCRC.update(CM); + + /* 3. Check the flags */ + final int flags = in.read(); + if (flags < 0) { + throw new EOFException("Early EOF in GZIP header"); + } + headCRC.update(flags); + + /* + * This flag byte is divided into individual bits as follows: + * + * bit 0 FTEXT bit 1 FHCRC bit 2 FEXTRA bit 3 FNAME bit 4 FCOMMENT bit 5 + * reserved bit 6 reserved bit 7 reserved + */ + + /* 3.1 Check the reserved bits are zero */ + if ((flags & 0xd0) != 0) { + throw new IOException("Reserved flag bits in GZIP header != 0"); + } + + /* 4.-6. Skip the modification time, extra flags, and OS type */ + for (int i = 0; i < 6; i++) { + final int readByte = in.read(); + if (readByte < 0) { + throw new EOFException("Early EOF in GZIP header"); + } + headCRC.update(readByte); + } + + /* 7. Read extra field */ + if ((flags & FEXTRA) != 0) { + /* Skip subfield id */ + for (int i = 0; i < 2; i++) { + final int readByte = in.read(); + if (readByte < 0) { + throw new EOFException("Early EOF in GZIP header"); + } + headCRC.update(readByte); + } + if ((in.read() < 0) || (in.read() < 0)) { + throw new EOFException("Early EOF in GZIP header"); + } + + int len1, len2, extraLen; + len1 = in.read(); + len2 = in.read(); + if ((len1 < 0) || (len2 < 0)) { + throw new EOFException("Early EOF in GZIP header"); + } + headCRC.update(len1); + headCRC.update(len2); + + extraLen = (len1 << 8) | len2; + for (int i = 0; i < extraLen; i++) { + final int readByte = in.read(); + if (readByte < 0) { + throw new EOFException("Early EOF in GZIP header"); + } + headCRC.update(readByte); + } + } + + /* 8. Read file name */ + if ((flags & FNAME) != 0) { + int readByte; + while ((readByte = in.read()) > 0) { + headCRC.update(readByte); + } + if (readByte < 0) { + throw new EOFException("Early EOF in GZIP file name"); + } + headCRC.update(readByte); + } + + /* 9. Read comment */ + if ((flags & FCOMMENT) != 0) { + int readByte; + while ((readByte = in.read()) > 0) { + headCRC.update(readByte); + } + + if (readByte < 0) { + throw new EOFException("Early EOF in GZIP comment"); + } + headCRC.update(readByte); + } + + /* 10. Read header CRC */ + if ((flags & FHCRC) != 0) { + int tempByte; + int crcval = in.read(); + if (crcval < 0) { + throw new EOFException("Early EOF in GZIP header"); + } + + tempByte = in.read(); + if (tempByte < 0) { + throw new EOFException("Early EOF in GZIP header"); + } + + crcval = (crcval << 8) | tempByte; + if (crcval != ((int) headCRC.getValue() & 0xffff)) { + throw new IOException("Header CRC value mismatch"); + } + } + + readGZIPHeader = true; + // System.err.println("Read GZIP header"); + } + + private void readFooter() throws IOException { + final byte[] footer = new byte[8]; + int avail = inf.getRemaining(); + if (avail > 8) { + avail = 8; + } + System.arraycopy(buf, len - inf.getRemaining(), footer, 0, avail); + int needed = 8 - avail; + while (needed > 0) { + final int count = in.read(footer, 8 - needed, needed); + if (count <= 0) { + throw new EOFException("Early EOF in GZIP footer"); + } + needed -= count; // Jewel Jan 16 + } + + final int crcval = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8) + | ((footer[2] & 0xff) << 16) | (footer[3] << 24); + if (crcval != (int) crc.getValue()) { + throw new IOException("GZIP crc sum mismatch, theirs \"" + + Integer.toHexString(crcval) + "\" and ours \"" + + Integer.toHexString((int) crc.getValue())); + } + + final int total = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8) + | ((footer[6] & 0xff) << 16) | (footer[7] << 24); + if (total != inf.getTotalOut()) { + throw new IOException("Number of bytes mismatch"); + } + + /* + * FIXME" XXX Should we support multiple members. Difficult, since there + * may be some bytes still in buf + */ + eos = true; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/GZIPOutputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/GZIPOutputStream.java new file mode 100644 index 00000000..26d27c4d --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/GZIPOutputStream.java @@ -0,0 +1,150 @@ +/* GZIPOutputStream.java - Create a file in gzip format + Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * This filter stream is used to compress a stream into a "GZIP" stream. The + * "GZIP" format is described in RFC 1952. + * + * @author John Leuner + * @author Tom Tromey + * @since JDK 1.1 + */ + +/* + * Written using on-line Java Platform 1.2 API Specification and JCL book. + * Believed complete and correct. + */ + +public class GZIPOutputStream extends DeflaterOutputStream { + /** + * CRC-32 value for uncompressed data + */ + protected CRC32 crc; + + /* + * Creates a GZIPOutputStream with the default buffer size + * + * + * @param out The stream to read data (to be compressed) from + */ + public GZIPOutputStream(final OutputStream out) throws IOException { + this(out, 4096); + } + + /** + * Creates a GZIPOutputStream with the specified buffer size + * + * @param out + * The stream to read compressed data from + * @param size + * Size of the buffer to use + */ + public GZIPOutputStream(final OutputStream out, final int size) + throws IOException { + super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true), size); + + crc = new CRC32(); + final int mod_time = (int) (System.currentTimeMillis() / 1000L); + final byte[] gzipHeader = { + /* The two magic bytes */ + (byte) (GZIPInputStream.GZIP_MAGIC >> 8), + (byte) GZIPInputStream.GZIP_MAGIC, + + /* The compression type */ + (byte) Deflater.DEFLATED, + + /* The flags (not set) */ + 0, + + /* The modification time */ + (byte) mod_time, (byte) (mod_time >> 8), + (byte) (mod_time >> 16), (byte) (mod_time >> 24), + + /* The extra flags */ + 0, + + /* The OS type (unknown) */ + (byte) 255 }; + + out.write(gzipHeader); + // System.err.println("wrote GZIP header (" + gzipHeader.length + + // " bytes )"); + } + + @Override + public synchronized void write(final byte[] buf, final int off, + final int len) throws IOException { + super.write(buf, off, len); + crc.update(buf, off, len); + } + + /** + * Writes remaining compressed output data to the output stream and closes + * it. + */ + @Override + public void close() throws IOException { + finish(); + out.close(); + } + + @Override + public void finish() throws IOException { + super.finish(); + + final int totalin = def.getTotalIn(); + final int crcval = (int) (crc.getValue() & 0xffffffff); + + // System.err.println("CRC val is " + Integer.toHexString( crcval ) + + // " and length " + Integer.toHexString(totalin)); + + final byte[] gzipFooter = { (byte) crcval, (byte) (crcval >> 8), + (byte) (crcval >> 16), (byte) (crcval >> 24), + + (byte) totalin, (byte) (totalin >> 8), (byte) (totalin >> 16), + (byte) (totalin >> 24) }; + + out.write(gzipFooter); + // System.err.println("wrote GZIP trailer (" + gzipFooter.length + + // " bytes )"); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/Inflater.java b/epublib-core/src/main/java/net/sf/jazzlib/Inflater.java new file mode 100644 index 00000000..9e5b9b61 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/Inflater.java @@ -0,0 +1,710 @@ +/* Inflater.java - Decompress a data stream + Copyright (C) 1999, 2000, 2001, 2003 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/* Written using on-line Java Platform 1.2 API Specification + * and JCL book. + * Believed complete and correct. + */ + +/** + * Inflater is used to decompress data that has been compressed according to the + * "deflate" standard described in rfc1950. + * + * The usage is as following. First you have to set some input with + * setInput(), then inflate() it. If inflate doesn't inflate any + * bytes there may be three reasons: + *

    + *
  • needsInput() returns true because the input buffer is empty. You have to + * provide more input with setInput(). NOTE: needsInput() also + * returns true when, the stream is finished.
  • + *
  • needsDictionary() returns true, you have to provide a preset dictionary + * with setDictionary().
  • + *
  • finished() returns true, the inflater has finished.
  • + *
+ * Once the first output byte is produced, a dictionary will not be needed at a + * later stage. + * + * @author John Leuner, Jochen Hoenicke + * @author Tom Tromey + * @date May 17, 1999 + * @since JDK 1.1 + */ +public class Inflater { + /* Copy lengths for literal codes 257..285 */ + private static final int CPLENS[] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, + 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, + 227, 258 }; + + /* Extra bits for literal codes 257..285 */ + private static final int CPLEXT[] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + + /* Copy offsets for distance codes 0..29 */ + private static final int CPDIST[] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, + 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, + 4097, 6145, 8193, 12289, 16385, 24577 }; + + /* Extra bits for distance codes */ + private static final int CPDEXT[] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, + 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; + + /* This are the state in which the inflater can be. */ + private static final int DECODE_HEADER = 0; + private static final int DECODE_DICT = 1; + private static final int DECODE_BLOCKS = 2; + private static final int DECODE_STORED_LEN1 = 3; + private static final int DECODE_STORED_LEN2 = 4; + private static final int DECODE_STORED = 5; + private static final int DECODE_DYN_HEADER = 6; + private static final int DECODE_HUFFMAN = 7; + private static final int DECODE_HUFFMAN_LENBITS = 8; + private static final int DECODE_HUFFMAN_DIST = 9; + private static final int DECODE_HUFFMAN_DISTBITS = 10; + private static final int DECODE_CHKSUM = 11; + private static final int FINISHED = 12; + + /** This variable contains the current state. */ + private int mode; + + /** + * The adler checksum of the dictionary or of the decompressed stream, as it + * is written in the header resp. footer of the compressed stream.
+ * + * Only valid if mode is DECODE_DICT or DECODE_CHKSUM. + */ + private int readAdler; + /** + * The number of bits needed to complete the current state. This is valid, + * if mode is DECODE_DICT, DECODE_CHKSUM, DECODE_HUFFMAN_LENBITS or + * DECODE_HUFFMAN_DISTBITS. + */ + private int neededBits; + private int repLength, repDist; + private int uncomprLen; + /** + * True, if the last block flag was set in the last block of the inflated + * stream. This means that the stream ends after the current block. + */ + private boolean isLastBlock; + + /** + * The total number of inflated bytes. + */ + private int totalOut; + /** + * The total number of bytes set with setInput(). This is not the value + * returned by getTotalIn(), since this also includes the unprocessed input. + */ + private int totalIn; + /** + * This variable stores the nowrap flag that was given to the constructor. + * True means, that the inflated stream doesn't contain a header nor the + * checksum in the footer. + */ + private final boolean nowrap; + + private StreamManipulator input; + private OutputWindow outputWindow; + private InflaterDynHeader dynHeader; + private InflaterHuffmanTree litlenTree, distTree; + private Adler32 adler; + + /** + * Creates a new inflater. + */ + public Inflater() { + this(false); + } + + /** + * Creates a new inflater. + * + * @param nowrap + * true if no header and checksum field appears in the stream. + * This is used for GZIPed input. For compatibility with Sun JDK + * you should provide one byte of input more than needed in this + * case. + */ + public Inflater(final boolean nowrap) { + this.nowrap = nowrap; + this.adler = new Adler32(); + input = new StreamManipulator(); + outputWindow = new OutputWindow(); + mode = nowrap ? DECODE_BLOCKS : DECODE_HEADER; + } + + /** + * Finalizes this object. + */ + @Override + protected void finalize() { + /* Exists only for compatibility */ + } + + /** + * Frees all objects allocated by the inflater. There's no reason to call + * this, since you can just rely on garbage collection (even for the Sun + * implementation). Exists only for compatibility with Sun's JDK, where the + * compressor allocates native memory. If you call any method (even reset) + * afterwards the behaviour is undefined. + * + * @deprecated Just clear all references to inflater instead. + */ + @Deprecated + public void end() { + outputWindow = null; + input = null; + dynHeader = null; + litlenTree = null; + distTree = null; + adler = null; + } + + /** + * Returns true, if the inflater has finished. This means, that no input is + * needed and no output can be produced. + */ + public boolean finished() { + return (mode == FINISHED) && (outputWindow.getAvailable() == 0); + } + + /** + * Gets the adler checksum. This is either the checksum of all uncompressed + * bytes returned by inflate(), or if needsDictionary() returns true (and + * thus no output was yet produced) this is the adler checksum of the + * expected dictionary. + * + * @returns the adler checksum. + */ + public int getAdler() { + return needsDictionary() ? readAdler : (int) adler.getValue(); + } + + /** + * Gets the number of unprocessed input. Useful, if the end of the stream is + * reached and you want to further process the bytes after the deflate + * stream. + * + * @return the number of bytes of the input which were not processed. + */ + public int getRemaining() { + return input.getAvailableBytes(); + } + + /** + * Gets the total number of processed compressed input bytes. + * + * @return the total number of bytes of processed input bytes. + */ + public int getTotalIn() { + return totalIn - getRemaining(); + } + + /** + * Gets the total number of output bytes returned by inflate(). + * + * @return the total number of output bytes. + */ + public int getTotalOut() { + return totalOut; + } + + /** + * Inflates the compressed stream to the output buffer. If this returns 0, + * you should check, whether needsDictionary(), needsInput() or finished() + * returns true, to determine why no further output is produced. + * + * @param buffer + * the output buffer. + * @return the number of bytes written to the buffer, 0 if no further output + * can be produced. + * @exception DataFormatException + * if deflated stream is invalid. + * @exception IllegalArgumentException + * if buf has length 0. + */ + public int inflate(final byte[] buf) throws DataFormatException { + return inflate(buf, 0, buf.length); + } + + /** + * Inflates the compressed stream to the output buffer. If this returns 0, + * you should check, whether needsDictionary(), needsInput() or finished() + * returns true, to determine why no further output is produced. + * + * @param buffer + * the output buffer. + * @param off + * the offset into buffer where the output should start. + * @param len + * the maximum length of the output. + * @return the number of bytes written to the buffer, 0 if no further output + * can be produced. + * @exception DataFormatException + * if deflated stream is invalid. + * @exception IndexOutOfBoundsException + * if the off and/or len are wrong. + */ + public int inflate(final byte[] buf, int off, int len) + throws DataFormatException { + /* Special case: len may be zero */ + if (len == 0) { + return 0; + } + /* Check for correct buff, off, len triple */ + if ((0 > off) || (off > (off + len)) || ((off + len) > buf.length)) { + throw new ArrayIndexOutOfBoundsException(); + } + int count = 0; + int more; + do { + if (mode != DECODE_CHKSUM) { + /* + * Don't give away any output, if we are waiting for the + * checksum in the input stream. + * + * With this trick we have always: needsInput() and not + * finished() implies more output can be produced. + */ + more = outputWindow.copyOutput(buf, off, len); + adler.update(buf, off, more); + off += more; + count += more; + totalOut += more; + len -= more; + if (len == 0) { + return count; + } + } + } while (decode() + || ((outputWindow.getAvailable() > 0) && (mode != DECODE_CHKSUM))); + return count; + } + + /** + * Returns true, if a preset dictionary is needed to inflate the input. + */ + public boolean needsDictionary() { + return (mode == DECODE_DICT) && (neededBits == 0); + } + + /** + * Returns true, if the input buffer is empty. You should then call + * setInput().
+ * + * NOTE: This method also returns true when the stream is finished. + */ + public boolean needsInput() { + return input.needsInput(); + } + + /** + * Resets the inflater so that a new stream can be decompressed. All pending + * input and output will be discarded. + */ + public void reset() { + mode = nowrap ? DECODE_BLOCKS : DECODE_HEADER; + totalIn = totalOut = 0; + input.reset(); + outputWindow.reset(); + dynHeader = null; + litlenTree = null; + distTree = null; + isLastBlock = false; + adler.reset(); + } + + /** + * Sets the preset dictionary. This should only be called, if + * needsDictionary() returns true and it should set the same dictionary, + * that was used for deflating. The getAdler() function returns the checksum + * of the dictionary needed. + * + * @param buffer + * the dictionary. + * @exception IllegalStateException + * if no dictionary is needed. + * @exception IllegalArgumentException + * if the dictionary checksum is wrong. + */ + public void setDictionary(final byte[] buffer) { + setDictionary(buffer, 0, buffer.length); + } + + /** + * Sets the preset dictionary. This should only be called, if + * needsDictionary() returns true and it should set the same dictionary, + * that was used for deflating. The getAdler() function returns the checksum + * of the dictionary needed. + * + * @param buffer + * the dictionary. + * @param off + * the offset into buffer where the dictionary starts. + * @param len + * the length of the dictionary. + * @exception IllegalStateException + * if no dictionary is needed. + * @exception IllegalArgumentException + * if the dictionary checksum is wrong. + * @exception IndexOutOfBoundsException + * if the off and/or len are wrong. + */ + public void setDictionary(final byte[] buffer, final int off, final int len) { + if (!needsDictionary()) { + throw new IllegalStateException(); + } + + adler.update(buffer, off, len); + if ((int) adler.getValue() != readAdler) { + throw new IllegalArgumentException("Wrong adler checksum"); + } + adler.reset(); + outputWindow.copyDict(buffer, off, len); + mode = DECODE_BLOCKS; + } + + /** + * Sets the input. This should only be called, if needsInput() returns true. + * + * @param buffer + * the input. + * @exception IllegalStateException + * if no input is needed. + */ + public void setInput(final byte[] buf) { + setInput(buf, 0, buf.length); + } + + /** + * Sets the input. This should only be called, if needsInput() returns true. + * + * @param buffer + * the input. + * @param off + * the offset into buffer where the input starts. + * @param len + * the length of the input. + * @exception IllegalStateException + * if no input is needed. + * @exception IndexOutOfBoundsException + * if the off and/or len are wrong. + */ + public void setInput(final byte[] buf, final int off, final int len) { + input.setInput(buf, off, len); + totalIn += len; + } + + /** + * Decodes the deflate header. + * + * @return false if more input is needed. + * @exception DataFormatException + * if header is invalid. + */ + private boolean decodeHeader() throws DataFormatException { + int header = input.peekBits(16); + if (header < 0) { + return false; + } + input.dropBits(16); + + /* The header is written in "wrong" byte order */ + header = ((header << 8) | (header >> 8)) & 0xffff; + if ((header % 31) != 0) { + throw new DataFormatException("Header checksum illegal"); + } + + if ((header & 0x0f00) != (Deflater.DEFLATED << 8)) { + throw new DataFormatException("Compression Method unknown"); + } + + /* + * Maximum size of the backwards window in bits. We currently ignore + * this, but we could use it to make the inflater window more space + * efficient. On the other hand the full window (15 bits) is needed most + * times, anyway. int max_wbits = ((header & 0x7000) >> 12) + 8; + */ + + if ((header & 0x0020) == 0) // Dictionary flag? + { + mode = DECODE_BLOCKS; + } else { + mode = DECODE_DICT; + neededBits = 32; + } + return true; + } + + /** + * Decodes the dictionary checksum after the deflate header. + * + * @return false if more input is needed. + */ + private boolean decodeDict() { + while (neededBits > 0) { + final int dictByte = input.peekBits(8); + if (dictByte < 0) { + return false; + } + input.dropBits(8); + readAdler = (readAdler << 8) | dictByte; + neededBits -= 8; + } + return false; + } + + /** + * Decodes the huffman encoded symbols in the input stream. + * + * @return false if more input is needed, true if output window is full or + * the current block ends. + * @exception DataFormatException + * if deflated stream is invalid. + */ + private boolean decodeHuffman() throws DataFormatException { + int free = outputWindow.getFreeSpace(); + while (free >= 258) { + int symbol; + switch (mode) { + case DECODE_HUFFMAN: + /* This is the inner loop so it is optimized a bit */ + while (((symbol = litlenTree.getSymbol(input)) & ~0xff) == 0) { + outputWindow.write(symbol); + if (--free < 258) { + return true; + } + } + if (symbol < 257) { + if (symbol < 0) { + return false; + } else { + /* symbol == 256: end of block */ + distTree = null; + litlenTree = null; + mode = DECODE_BLOCKS; + return true; + } + } + + try { + repLength = CPLENS[symbol - 257]; + neededBits = CPLEXT[symbol - 257]; + } catch (final ArrayIndexOutOfBoundsException ex) { + throw new DataFormatException("Illegal rep length code"); + } + /* fall through */ + case DECODE_HUFFMAN_LENBITS: + if (neededBits > 0) { + mode = DECODE_HUFFMAN_LENBITS; + final int i = input.peekBits(neededBits); + if (i < 0) { + return false; + } + input.dropBits(neededBits); + repLength += i; + } + mode = DECODE_HUFFMAN_DIST; + /* fall through */ + case DECODE_HUFFMAN_DIST: + symbol = distTree.getSymbol(input); + if (symbol < 0) { + return false; + } + try { + repDist = CPDIST[symbol]; + neededBits = CPDEXT[symbol]; + } catch (final ArrayIndexOutOfBoundsException ex) { + throw new DataFormatException("Illegal rep dist code"); + } + /* fall through */ + case DECODE_HUFFMAN_DISTBITS: + if (neededBits > 0) { + mode = DECODE_HUFFMAN_DISTBITS; + final int i = input.peekBits(neededBits); + if (i < 0) { + return false; + } + input.dropBits(neededBits); + repDist += i; + } + outputWindow.repeat(repLength, repDist); + free -= repLength; + mode = DECODE_HUFFMAN; + break; + default: + throw new IllegalStateException(); + } + } + return true; + } + + /** + * Decodes the adler checksum after the deflate stream. + * + * @return false if more input is needed. + * @exception DataFormatException + * if checksum doesn't match. + */ + private boolean decodeChksum() throws DataFormatException { + while (neededBits > 0) { + final int chkByte = input.peekBits(8); + if (chkByte < 0) { + return false; + } + input.dropBits(8); + readAdler = (readAdler << 8) | chkByte; + neededBits -= 8; + } + if ((int) adler.getValue() != readAdler) { + throw new DataFormatException("Adler chksum doesn't match: " + + Integer.toHexString((int) adler.getValue()) + " vs. " + + Integer.toHexString(readAdler)); + } + mode = FINISHED; + return false; + } + + /** + * Decodes the deflated stream. + * + * @return false if more input is needed, or if finished. + * @exception DataFormatException + * if deflated stream is invalid. + */ + private boolean decode() throws DataFormatException { + switch (mode) { + case DECODE_HEADER: + return decodeHeader(); + case DECODE_DICT: + return decodeDict(); + case DECODE_CHKSUM: + return decodeChksum(); + + case DECODE_BLOCKS: + if (isLastBlock) { + if (nowrap) { + mode = FINISHED; + return false; + } else { + input.skipToByteBoundary(); + neededBits = 32; + mode = DECODE_CHKSUM; + return true; + } + } + + final int type = input.peekBits(3); + if (type < 0) { + return false; + } + input.dropBits(3); + + if ((type & 1) != 0) { + isLastBlock = true; + } + switch (type >> 1) { + case DeflaterConstants.STORED_BLOCK: + input.skipToByteBoundary(); + mode = DECODE_STORED_LEN1; + break; + case DeflaterConstants.STATIC_TREES: + litlenTree = InflaterHuffmanTree.defLitLenTree; + distTree = InflaterHuffmanTree.defDistTree; + mode = DECODE_HUFFMAN; + break; + case DeflaterConstants.DYN_TREES: + dynHeader = new InflaterDynHeader(); + mode = DECODE_DYN_HEADER; + break; + default: + throw new DataFormatException("Unknown block type " + type); + } + return true; + + case DECODE_STORED_LEN1: { + if ((uncomprLen = input.peekBits(16)) < 0) { + return false; + } + input.dropBits(16); + mode = DECODE_STORED_LEN2; + } + /* fall through */ + case DECODE_STORED_LEN2: { + final int nlen = input.peekBits(16); + if (nlen < 0) { + return false; + } + input.dropBits(16); + if (nlen != (uncomprLen ^ 0xffff)) { + throw new DataFormatException("broken uncompressed block"); + } + mode = DECODE_STORED; + } + /* fall through */ + case DECODE_STORED: { + final int more = outputWindow.copyStored(input, uncomprLen); + uncomprLen -= more; + if (uncomprLen == 0) { + mode = DECODE_BLOCKS; + return true; + } + return !input.needsInput(); + } + + case DECODE_DYN_HEADER: + if (!dynHeader.decode(input)) { + return false; + } + litlenTree = dynHeader.buildLitLenTree(); + distTree = dynHeader.buildDistTree(); + mode = DECODE_HUFFMAN; + /* fall through */ + case DECODE_HUFFMAN: + case DECODE_HUFFMAN_LENBITS: + case DECODE_HUFFMAN_DIST: + case DECODE_HUFFMAN_DISTBITS: + return decodeHuffman(); + case FINISHED: + return false; + default: + throw new IllegalStateException(); + } + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/InflaterDynHeader.java b/epublib-core/src/main/java/net/sf/jazzlib/InflaterDynHeader.java new file mode 100644 index 00000000..47e1eac5 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/InflaterDynHeader.java @@ -0,0 +1,195 @@ +/* net.sf.jazzlib.InflaterDynHeader + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +class InflaterDynHeader { + private static final int LNUM = 0; + private static final int DNUM = 1; + private static final int BLNUM = 2; + private static final int BLLENS = 3; + private static final int LENS = 4; + private static final int REPS = 5; + + private static final int repMin[] = { 3, 3, 11 }; + private static final int repBits[] = { 2, 3, 7 }; + + private byte[] blLens; + private byte[] litdistLens; + + private InflaterHuffmanTree blTree; + + private int mode; + private int lnum, dnum, blnum, num; + private int repSymbol; + private byte lastLen; + private int ptr; + + private static final int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, + 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + public InflaterDynHeader() { + } + + public boolean decode(final StreamManipulator input) + throws DataFormatException { + decode_loop: for (;;) { + switch (mode) { + case LNUM: + lnum = input.peekBits(5); + if (lnum < 0) { + return false; + } + lnum += 257; + input.dropBits(5); + // System.err.println("LNUM: "+lnum); + mode = DNUM; + /* fall through */ + case DNUM: + dnum = input.peekBits(5); + if (dnum < 0) { + return false; + } + dnum++; + input.dropBits(5); + // System.err.println("DNUM: "+dnum); + num = lnum + dnum; + litdistLens = new byte[num]; + mode = BLNUM; + /* fall through */ + case BLNUM: + blnum = input.peekBits(4); + if (blnum < 0) { + return false; + } + blnum += 4; + input.dropBits(4); + blLens = new byte[19]; + ptr = 0; + // System.err.println("BLNUM: "+blnum); + mode = BLLENS; + /* fall through */ + case BLLENS: + while (ptr < blnum) { + final int len = input.peekBits(3); + if (len < 0) { + return false; + } + input.dropBits(3); + // System.err.println("blLens["+BL_ORDER[ptr]+"]: "+len); + blLens[BL_ORDER[ptr]] = (byte) len; + ptr++; + } + blTree = new InflaterHuffmanTree(blLens); + blLens = null; + ptr = 0; + mode = LENS; + /* fall through */ + case LENS: { + int symbol; + while (((symbol = blTree.getSymbol(input)) & ~15) == 0) { + /* Normal case: symbol in [0..15] */ + + // System.err.println("litdistLens["+ptr+"]: "+symbol); + litdistLens[ptr++] = lastLen = (byte) symbol; + + if (ptr == num) { + /* Finished */ + return true; + } + } + + /* need more input ? */ + if (symbol < 0) { + return false; + } + + /* otherwise repeat code */ + if (symbol >= 17) { + /* repeat zero */ + // System.err.println("repeating zero"); + lastLen = 0; + } else { + if (ptr == 0) { + throw new DataFormatException(); + } + } + repSymbol = symbol - 16; + mode = REPS; + } + /* fall through */ + + case REPS: { + final int bits = repBits[repSymbol]; + int count = input.peekBits(bits); + if (count < 0) { + return false; + } + input.dropBits(bits); + count += repMin[repSymbol]; + // System.err.println("litdistLens repeated: "+count); + + if ((ptr + count) > num) { + throw new DataFormatException(); + } + while (count-- > 0) { + litdistLens[ptr++] = lastLen; + } + + if (ptr == num) { + /* Finished */ + return true; + } + } + mode = LENS; + continue decode_loop; + } + } + } + + public InflaterHuffmanTree buildLitLenTree() throws DataFormatException { + final byte[] litlenLens = new byte[lnum]; + System.arraycopy(litdistLens, 0, litlenLens, 0, lnum); + return new InflaterHuffmanTree(litlenLens); + } + + public InflaterHuffmanTree buildDistTree() throws DataFormatException { + final byte[] distLens = new byte[dnum]; + System.arraycopy(litdistLens, lnum, distLens, 0, dnum); + return new InflaterHuffmanTree(distLens); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/InflaterHuffmanTree.java b/epublib-core/src/main/java/net/sf/jazzlib/InflaterHuffmanTree.java new file mode 100644 index 00000000..164fabac --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/InflaterHuffmanTree.java @@ -0,0 +1,199 @@ +/* net.sf.jazzlib.InflaterHuffmanTree + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +public class InflaterHuffmanTree { + private final static int MAX_BITLEN = 15; + private short[] tree; + + public static InflaterHuffmanTree defLitLenTree, defDistTree; + + static { + try { + byte[] codeLengths = new byte[288]; + int i = 0; + while (i < 144) { + codeLengths[i++] = 8; + } + while (i < 256) { + codeLengths[i++] = 9; + } + while (i < 280) { + codeLengths[i++] = 7; + } + while (i < 288) { + codeLengths[i++] = 8; + } + defLitLenTree = new InflaterHuffmanTree(codeLengths); + + codeLengths = new byte[32]; + i = 0; + while (i < 32) { + codeLengths[i++] = 5; + } + defDistTree = new InflaterHuffmanTree(codeLengths); + } catch (final DataFormatException ex) { + throw new InternalError( + "InflaterHuffmanTree: static tree length illegal"); + } + } + + /** + * Constructs a Huffman tree from the array of code lengths. + * + * @param codeLengths + * the array of code lengths + */ + public InflaterHuffmanTree(final byte[] codeLengths) + throws DataFormatException { + buildTree(codeLengths); + } + + private void buildTree(final byte[] codeLengths) throws DataFormatException { + final int[] blCount = new int[MAX_BITLEN + 1]; + final int[] nextCode = new int[MAX_BITLEN + 1]; + for (final byte codeLength : codeLengths) { + final int bits = codeLength; + if (bits > 0) { + blCount[bits]++; + } + } + + int code = 0; + int treeSize = 512; + for (int bits = 1; bits <= MAX_BITLEN; bits++) { + nextCode[bits] = code; + code += blCount[bits] << (16 - bits); + if (bits >= 10) { + /* We need an extra table for bit lengths >= 10. */ + final int start = nextCode[bits] & 0x1ff80; + final int end = code & 0x1ff80; + treeSize += (end - start) >> (16 - bits); + } + } + if (code != 65536) { + throw new DataFormatException("Code lengths don't add up properly."); + } + + /* + * Now create and fill the extra tables from longest to shortest bit + * len. This way the sub trees will be aligned. + */ + tree = new short[treeSize]; + int treePtr = 512; + for (int bits = MAX_BITLEN; bits >= 10; bits--) { + final int end = code & 0x1ff80; + code -= blCount[bits] << (16 - bits); + final int start = code & 0x1ff80; + for (int i = start; i < end; i += 1 << 7) { + tree[DeflaterHuffman.bitReverse(i)] = (short) ((-treePtr << 4) | bits); + treePtr += 1 << (bits - 9); + } + } + + for (int i = 0; i < codeLengths.length; i++) { + final int bits = codeLengths[i]; + if (bits == 0) { + continue; + } + code = nextCode[bits]; + int revcode = DeflaterHuffman.bitReverse(code); + if (bits <= 9) { + do { + tree[revcode] = (short) ((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < 512); + } else { + int subTree = tree[revcode & 511]; + final int treeLen = 1 << (subTree & 15); + subTree = -(subTree >> 4); + do { + tree[subTree | (revcode >> 9)] = (short) ((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < treeLen); + } + nextCode[bits] = code + (1 << (16 - bits)); + } + } + + /** + * Reads the next symbol from input. The symbol is encoded using the huffman + * tree. + * + * @param input + * the input source. + * @return the next symbol, or -1 if not enough input is available. + */ + public int getSymbol(final StreamManipulator input) + throws DataFormatException { + int lookahead, symbol; + if ((lookahead = input.peekBits(9)) >= 0) { + if ((symbol = tree[lookahead]) >= 0) { + input.dropBits(symbol & 15); + return symbol >> 4; + } + final int subtree = -(symbol >> 4); + final int bitlen = symbol & 15; + if ((lookahead = input.peekBits(bitlen)) >= 0) { + symbol = tree[subtree | (lookahead >> 9)]; + input.dropBits(symbol & 15); + return symbol >> 4; + } else { + final int bits = input.getAvailableBits(); + lookahead = input.peekBits(bits); + symbol = tree[subtree | (lookahead >> 9)]; + if ((symbol & 15) <= bits) { + input.dropBits(symbol & 15); + return symbol >> 4; + } else { + return -1; + } + } + } else { + final int bits = input.getAvailableBits(); + lookahead = input.peekBits(bits); + symbol = tree[lookahead]; + if ((symbol >= 0) && ((symbol & 15) <= bits)) { + input.dropBits(symbol & 15); + return symbol >> 4; + } else { + return -1; + } + } + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/InflaterInputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/InflaterInputStream.java new file mode 100644 index 00000000..3241aa23 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/InflaterInputStream.java @@ -0,0 +1,260 @@ +/* InflaterInputStream.java - Input stream filter for decompressing + Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004 + Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * This filter stream is used to decompress data compressed in the "deflate" + * format. The "deflate" format is described in RFC 1951. + * + * This stream may form the basis for other decompression filters, such as the + * GZIPInputStream. + * + * @author John Leuner + * @author Tom Tromey + * @since 1.1 + */ +public class InflaterInputStream extends FilterInputStream { + /** + * Decompressor for this filter + */ + protected Inflater inf; + + /** + * Byte array used as a buffer + */ + protected byte[] buf; + + /** + * Size of buffer + */ + protected int len; + + /* + * We just use this if we are decoding one byte at a time with the read() + * call + */ + private final byte[] onebytebuffer = new byte[1]; + + /** + * Create an InflaterInputStream with the default decompresseor and a + * default buffer size. + * + * @param in + * the InputStream to read bytes from + */ + public InflaterInputStream(final InputStream in) { + this(in, new Inflater(), 4096); + } + + /** + * Create an InflaterInputStream with the specified decompresseor and a + * default buffer size. + * + * @param in + * the InputStream to read bytes from + * @param inf + * the decompressor used to decompress data read from in + */ + public InflaterInputStream(final InputStream in, final Inflater inf) { + this(in, inf, 4096); + } + + /** + * Create an InflaterInputStream with the specified decompresseor and a + * specified buffer size. + * + * @param in + * the InputStream to read bytes from + * @param inf + * the decompressor used to decompress data read from in + * @param size + * size of the buffer to use + */ + public InflaterInputStream(final InputStream in, final Inflater inf, + final int size) { + super(in); + this.len = 0; + + if (in == null) { + throw new NullPointerException("in may not be null"); + } + if (inf == null) { + throw new NullPointerException("inf may not be null"); + } + if (size < 0) { + throw new IllegalArgumentException("size may not be negative"); + } + + this.inf = inf; + this.buf = new byte[size]; + } + + /** + * Returns 0 once the end of the stream (EOF) has been reached. Otherwise + * returns 1. + */ + @Override + public int available() throws IOException { + // According to the JDK 1.2 docs, this should only ever return 0 + // or 1 and should not be relied upon by Java programs. + return inf.finished() ? 0 : 1; + } + + /** + * Closes the input stream + */ + @Override + public synchronized void close() throws IOException { + if (in != null) { + in.close(); + } + in = null; + } + + /** + * Fills the buffer with more data to decompress. + */ + protected void fill() throws IOException { + if (in == null) { + throw new ZipException("InflaterInputStream is closed"); + } + + len = in.read(buf, 0, buf.length); + + if (len < 0) { + throw new ZipException("Deflated stream ends early."); + } + + inf.setInput(buf, 0, len); + } + + /** + * Reads one byte of decompressed data. + * + * The byte is in the lower 8 bits of the int. + */ + @Override + public int read() throws IOException { + final int nread = read(onebytebuffer, 0, 1); // read one byte + + if (nread > 0) { + return onebytebuffer[0] & 0xff; + } + + return -1; + } + + /** + * Decompresses data into the byte array + * + * @param b + * the array to read and decompress data into + * @param off + * the offset indicating where the data should be placed + * @param len + * the number of bytes to decompress + */ + @Override + public int read(final byte[] b, final int off, final int len) + throws IOException { + if (len == 0) { + return 0; + } + + for (;;) { + int count; + + try { + count = inf.inflate(b, off, len); + } catch (final DataFormatException dfe) { + throw new ZipException(dfe.getMessage()); + } + + if (count > 0) { + return count; + } + + if (inf.needsDictionary() | inf.finished()) { + return -1; + } else if (inf.needsInput()) { + fill(); + } else { + throw new InternalError("Don't know what to do"); + } + } + } + + /** + * Skip specified number of bytes of uncompressed data + * + * @param n + * number of bytes to skip + */ + @Override + public long skip(long n) throws IOException { + if (n < 0) { + throw new IllegalArgumentException(); + } + + if (n == 0) { + return 0; + } + + // Implementation copied from InputStream + // Throw away n bytes by reading them into a temp byte[]. + // Limit the temp array to 2Kb so we don't grab too much memory. + final int buflen = n > 2048 ? 2048 : (int) n; + final byte[] tmpbuf = new byte[buflen]; + final long origN = n; + + while (n > 0L) { + final int numread = read(tmpbuf, 0, n > buflen ? buflen : (int) n); + if (numread <= 0) { + break; + } + n -= numread; + } + + return origN - n; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/OutputWindow.java b/epublib-core/src/main/java/net/sf/jazzlib/OutputWindow.java new file mode 100644 index 00000000..c06b33ae --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/OutputWindow.java @@ -0,0 +1,168 @@ +/* net.sf.jazzlib.OutputWindow + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/* + * Contains the output from the Inflation process. + * + * We need to have a window so that we can refer backwards into the output stream + * to repeat stuff. + * + * @author John Leuner + * @since JDK 1.1 + */ + +class OutputWindow { + private final int WINDOW_SIZE = 1 << 15; + private final int WINDOW_MASK = WINDOW_SIZE - 1; + + private final byte[] window = new byte[WINDOW_SIZE]; // The window is 2^15 + // bytes + private int window_end = 0; + private int window_filled = 0; + + public void write(final int abyte) { + if (window_filled++ == WINDOW_SIZE) { + throw new IllegalStateException("Window full"); + } + window[window_end++] = (byte) abyte; + window_end &= WINDOW_MASK; + } + + private final void slowRepeat(int rep_start, int len, final int dist) { + while (len-- > 0) { + window[window_end++] = window[rep_start++]; + window_end &= WINDOW_MASK; + rep_start &= WINDOW_MASK; + } + } + + public void repeat(int len, final int dist) { + if ((window_filled += len) > WINDOW_SIZE) { + throw new IllegalStateException("Window full"); + } + + int rep_start = (window_end - dist) & WINDOW_MASK; + final int border = WINDOW_SIZE - len; + if ((rep_start <= border) && (window_end < border)) { + if (len <= dist) { + System.arraycopy(window, rep_start, window, window_end, len); + window_end += len; + } else { + /* + * We have to copy manually, since the repeat pattern overlaps. + */ + while (len-- > 0) { + window[window_end++] = window[rep_start++]; + } + } + } else { + slowRepeat(rep_start, len, dist); + } + } + + public int copyStored(final StreamManipulator input, int len) { + len = Math.min(Math.min(len, WINDOW_SIZE - window_filled), + input.getAvailableBytes()); + int copied; + + final int tailLen = WINDOW_SIZE - window_end; + if (len > tailLen) { + copied = input.copyBytes(window, window_end, tailLen); + if (copied == tailLen) { + copied += input.copyBytes(window, 0, len - tailLen); + } + } else { + copied = input.copyBytes(window, window_end, len); + } + + window_end = (window_end + copied) & WINDOW_MASK; + window_filled += copied; + return copied; + } + + public void copyDict(final byte[] dict, int offset, int len) { + if (window_filled > 0) { + throw new IllegalStateException(); + } + + if (len > WINDOW_SIZE) { + offset += len - WINDOW_SIZE; + len = WINDOW_SIZE; + } + System.arraycopy(dict, offset, window, 0, len); + window_end = len & WINDOW_MASK; + } + + public int getFreeSpace() { + return WINDOW_SIZE - window_filled; + } + + public int getAvailable() { + return window_filled; + } + + public int copyOutput(final byte[] output, int offset, int len) { + int copy_end = window_end; + if (len > window_filled) { + len = window_filled; + } else { + copy_end = ((window_end - window_filled) + len) & WINDOW_MASK; + } + + final int copied = len; + final int tailLen = len - copy_end; + + if (tailLen > 0) { + System.arraycopy(window, WINDOW_SIZE - tailLen, output, offset, + tailLen); + offset += tailLen; + len = copy_end; + } + System.arraycopy(window, copy_end - len, output, offset, len); + window_filled -= copied; + if (window_filled < 0) { + throw new IllegalStateException(); + } + return copied; + } + + public void reset() { + window_filled = window_end = 0; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/PendingBuffer.java b/epublib-core/src/main/java/net/sf/jazzlib/PendingBuffer.java new file mode 100644 index 00000000..8966d860 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/PendingBuffer.java @@ -0,0 +1,199 @@ +/* net.sf.jazzlib.PendingBuffer + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/** + * This class is general purpose class for writing data to a buffer. + * + * It allows you to write bits as well as bytes + * + * Based on DeflaterPending.java + * + * @author Jochen Hoenicke + * @date Jan 5, 2000 + */ + +class PendingBuffer { + protected byte[] buf; + int start; + int end; + + int bits; + int bitCount; + + public PendingBuffer() { + this(4096); + } + + public PendingBuffer(final int bufsize) { + buf = new byte[bufsize]; + } + + public final void reset() { + start = end = bitCount = 0; + } + + public final void writeByte(final int b) { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + buf[end++] = (byte) b; + } + + public final void writeShort(final int s) { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + buf[end++] = (byte) s; + buf[end++] = (byte) (s >> 8); + } + + public final void writeInt(final int s) { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + buf[end++] = (byte) s; + buf[end++] = (byte) (s >> 8); + buf[end++] = (byte) (s >> 16); + buf[end++] = (byte) (s >> 24); + } + + public final void writeBlock(final byte[] block, final int offset, + final int len) { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + System.arraycopy(block, offset, buf, end, len); + end += len; + } + + public final int getBitCount() { + return bitCount; + } + + public final void alignToByte() { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + if (bitCount > 0) { + buf[end++] = (byte) bits; + if (bitCount > 8) { + buf[end++] = (byte) (bits >>> 8); + } + } + bits = 0; + bitCount = 0; + } + + public final void writeBits(final int b, final int count) { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + if (DeflaterConstants.DEBUGGING) { + System.err.println("writeBits(" + Integer.toHexString(b) + "," + + count + ")"); + } + bits |= b << bitCount; + bitCount += count; + if (bitCount >= 16) { + buf[end++] = (byte) bits; + buf[end++] = (byte) (bits >>> 8); + bits >>>= 16; + bitCount -= 16; + } + } + + public final void writeShortMSB(final int s) { + if (DeflaterConstants.DEBUGGING && (start != 0)) { + throw new IllegalStateException(); + } + buf[end++] = (byte) (s >> 8); + buf[end++] = (byte) s; + } + + public final boolean isFlushed() { + return end == 0; + } + + /** + * Flushes the pending buffer into the given output array. If the output + * array is to small, only a partial flush is done. + * + * @param output + * the output array; + * @param offset + * the offset into output array; + * @param length + * the maximum number of bytes to store; + * @exception IndexOutOfBoundsException + * if offset or length are invalid. + */ + public final int flush(final byte[] output, final int offset, int length) { + if (bitCount >= 8) { + buf[end++] = (byte) bits; + bits >>>= 8; + bitCount -= 8; + } + if (length > (end - start)) { + length = end - start; + System.arraycopy(buf, start, output, offset, length); + start = 0; + end = 0; + } else { + System.arraycopy(buf, start, output, offset, length); + start += length; + } + return length; + } + + /** + * Flushes the pending buffer and returns that data in a new array + * + * @param output + * the output stream + */ + + public final byte[] toByteArray() { + final byte[] ret = new byte[end - start]; + System.arraycopy(buf, start, ret, 0, ret.length); + start = 0; + end = 0; + return ret; + } + +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/StreamManipulator.java b/epublib-core/src/main/java/net/sf/jazzlib/StreamManipulator.java new file mode 100644 index 00000000..d0a8fc8c --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/StreamManipulator.java @@ -0,0 +1,215 @@ +/* net.sf.jazzlib.StreamManipulator + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +/** + * This class allows us to retrieve a specified amount of bits from the input + * buffer, as well as copy big byte blocks. + * + * It uses an int buffer to store up to 31 bits for direct manipulation. This + * guarantees that we can get at least 16 bits, but we only need at most 15, so + * this is all safe. + * + * There are some optimizations in this class, for example, you must never peek + * more then 8 bits more than needed, and you must first peek bits before you + * may drop them. This is not a general purpose class but optimized for the + * behaviour of the Inflater. + * + * @author John Leuner, Jochen Hoenicke + */ + +class StreamManipulator { + private byte[] window; + private int window_start = 0; + private int window_end = 0; + + private int buffer = 0; + private int bits_in_buffer = 0; + + /** + * Get the next n bits but don't increase input pointer. n must be less or + * equal 16 and if you if this call succeeds, you must drop at least n-8 + * bits in the next call. + * + * @return the value of the bits, or -1 if not enough bits available. + */ + public final int peekBits(final int n) { + if (bits_in_buffer < n) { + if (window_start == window_end) { + return -1; + } + buffer |= ((window[window_start++] & 0xff) | ((window[window_start++] & 0xff) << 8)) << bits_in_buffer; + bits_in_buffer += 16; + } + return buffer & ((1 << n) - 1); + } + + /* + * Drops the next n bits from the input. You should have called peekBits + * with a bigger or equal n before, to make sure that enough bits are in the + * bit buffer. + */ + public final void dropBits(final int n) { + buffer >>>= n; + bits_in_buffer -= n; + } + + /** + * Gets the next n bits and increases input pointer. This is equivalent to + * peekBits followed by dropBits, except for correct error handling. + * + * @return the value of the bits, or -1 if not enough bits available. + */ + public final int getBits(final int n) { + final int bits = peekBits(n); + if (bits >= 0) { + dropBits(n); + } + return bits; + } + + /** + * Gets the number of bits available in the bit buffer. This must be only + * called when a previous peekBits() returned -1. + * + * @return the number of bits available. + */ + public final int getAvailableBits() { + return bits_in_buffer; + } + + /** + * Gets the number of bytes available. + * + * @return the number of bytes available. + */ + public final int getAvailableBytes() { + return (window_end - window_start) + (bits_in_buffer >> 3); + } + + /** + * Skips to the next byte boundary. + */ + public void skipToByteBoundary() { + buffer >>= (bits_in_buffer & 7); + bits_in_buffer &= ~7; + } + + public final boolean needsInput() { + return window_start == window_end; + } + + /* + * Copies length bytes from input buffer to output buffer starting at + * output[offset]. You have to make sure, that the buffer is byte aligned. + * If not enough bytes are available, copies fewer bytes. + * + * @param length the length to copy, 0 is allowed. + * + * @return the number of bytes copied, 0 if no byte is available. + */ + public int copyBytes(final byte[] output, int offset, int length) { + if (length < 0) { + throw new IllegalArgumentException("length negative"); + } + if ((bits_in_buffer & 7) != 0) { + /* bits_in_buffer may only be 0 or 8 */ + throw new IllegalStateException("Bit buffer is not aligned!"); + } + + int count = 0; + while ((bits_in_buffer > 0) && (length > 0)) { + output[offset++] = (byte) buffer; + buffer >>>= 8; + bits_in_buffer -= 8; + length--; + count++; + } + if (length == 0) { + return count; + } + + final int avail = window_end - window_start; + if (length > avail) { + length = avail; + } + System.arraycopy(window, window_start, output, offset, length); + window_start += length; + + if (((window_start - window_end) & 1) != 0) { + /* We always want an even number of bytes in input, see peekBits */ + buffer = (window[window_start++] & 0xff); + bits_in_buffer = 8; + } + return count + length; + } + + public StreamManipulator() { + } + + public void reset() { + window_start = window_end = buffer = bits_in_buffer = 0; + } + + public void setInput(final byte[] buf, int off, final int len) { + if (window_start < window_end) { + throw new IllegalStateException( + "Old input was not completely processed"); + } + + final int end = off + len; + + /* + * We want to throw an ArrayIndexOutOfBoundsException early. The check + * is very tricky: it also handles integer wrap around. + */ + if ((0 > off) || (off > end) || (end > buf.length)) { + throw new ArrayIndexOutOfBoundsException(); + } + + if ((len & 1) != 0) { + /* We always want an even number of bytes in input, see peekBits */ + buffer |= (buf[off++] & 0xff) << bits_in_buffer; + bits_in_buffer += 8; + } + + window = buf; + window_start = off; + window_end = end; + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/ZipConstants.java b/epublib-core/src/main/java/net/sf/jazzlib/ZipConstants.java new file mode 100644 index 00000000..bc2a803c --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/ZipConstants.java @@ -0,0 +1,95 @@ +/* net.sf.jazzlib.ZipConstants + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +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; +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/ZipEntry.java b/epublib-core/src/main/java/net/sf/jazzlib/ZipEntry.java new file mode 100644 index 00000000..33f5c9dd --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/ZipEntry.java @@ -0,0 +1,409 @@ +/* net.sf.jazzlib.ZipEntry + Copyright (C) 2001, 2002 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.util.Calendar; +import java.util.Date; + +/** + * 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 ZipEntry implements ZipConstants, Cloneable { + private static int KNOWN_SIZE = 1; + private static int KNOWN_CSIZE = 2; + private static int KNOWN_CRC = 4; + private static int KNOWN_TIME = 8; + + private static Calendar cal; + + private final String name; + 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 '/'. + * + * @exception NullPointerException + * when name is null. + * @exception IllegalArgumentException + * when name is bigger then 65535 chars. + */ + public ZipEntry(final String name) { + final int length = name.length(); + if (length > 65535) { + throw new IllegalArgumentException("name length is " + length); + } + this.name = name; + } + + /** + * Creates a copy of the given zip entry. + * + * @param e + * the entry to copy. + */ + public ZipEntry(final ZipEntry e) { + name = e.name; + 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(final 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. + */ + @Override + public Object clone() { + try { + // The JCL says that the `extra' field is also copied. + final ZipEntry clone = (ZipEntry) super.clone(); + if (extra != null) { + clone.extra = extra.clone(); + } + return clone; + } catch (final 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; + } + + /** + * Sets the time of last modification of the entry. + * + * @time the time of last modification of the entry. + */ + public void setTime(final long time) { + final 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; + } + + final int sec = 2 * (dostime & 0x1f); + final int min = (dostime >> 5) & 0x3f; + final int hrs = (dostime >> 11) & 0x1f; + final int day = (dostime >> 16) & 0x1f; + final int mon = ((dostime >> 21) & 0xf) - 1; + final 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 (final 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. + * + * @exception IllegalArgumentException + * if size is not in 0..0xffffffffL + */ + public void setSize(final 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. + * + * @exception IllegalArgumentException + * if size is not in 0..0xffffffffL + */ + public void setCompressedSize(final 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. + * + * @exception IllegalArgumentException + * if crc is not in 0..0xffffffffL + */ + public void setCrc(final 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. + * + * @exception IllegalArgumentException + * if method is not supported. + * @see ZipOutputStream#DEFLATED + * @see ZipOutputStream#STORED + */ + public void setMethod(final 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. + * + * @exception IllegalArgumentException + * if extra is longer than 0xffff bytes. + */ + public void setExtra(final 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) { + final int sig = (extra[pos++] & 0xff) + | ((extra[pos++] & 0xff) << 8); + final int len = (extra[pos++] & 0xff) + | ((extra[pos++] & 0xff) << 8); + if (sig == 0x5455) { + /* extended time stamp */ + final int flags = extra[pos]; + if ((flags & 1) != 0) { + final 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 (final 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. + * + * @exception IllegalArgumentException + * if comment is longer than 0xffff. + */ + public void setComment(final 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() { + final int nlen = name.length(); + return (nlen > 0) && (name.charAt(nlen - 1) == '/'); + } + + /** + * Gets the string representation of this ZipEntry. This is just the name as + * returned by getName(). + */ + @Override + public String toString() { + return name; + } + + /** + * Gets the hashCode of this ZipEntry. This is just the hashCode of the + * name. Note that the equals method isn't changed, though. + */ + @Override + public int hashCode() { + return name.hashCode(); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/ZipException.java b/epublib-core/src/main/java/net/sf/jazzlib/ZipException.java new file mode 100644 index 00000000..61d8b157 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/ZipException.java @@ -0,0 +1,70 @@ +/* ZipException.java - exception representing a zip related error + Copyright (C) 1998, 1999, 2000, 2001, 2002 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +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(final String msg) { + super(msg); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/ZipFile.java b/epublib-core/src/main/java/net/sf/jazzlib/ZipFile.java new file mode 100644 index 00000000..2b6b0482 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/ZipFile.java @@ -0,0 +1,557 @@ +/* net.sf.jazzlib.ZipFile + Copyright (C) 2001, 2002, 2003 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +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.io.RandomAccessFile; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + * 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. + * + * This class is thread safe: You can open input streams for arbitrary entries + * in different threads. + * + * @author Jochen Hoenicke + * @author Artur Biesiadowski + */ +public class ZipFile 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; + + // The entries of this zip file when initialized and not yet closed. + private Map entries; + + private boolean closed = false; + + /** + * Opens a Zip file with the given name for reading. + * + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the file doesn't contain a valid zip archive. + */ + public ZipFile(final String name) throws ZipException, IOException { + this.raf = new RandomAccessFile(name, "r"); + this.name = name; + } + + /** + * Opens a Zip file reading the given File. + * + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the file doesn't contain a valid zip archive. + */ + public ZipFile(final File file) throws ZipException, IOException { + this.raf = new RandomAccessFile(file, "r"); + this.name = file.getPath(); + } + + /** + * Opens a Zip file reading the given File in the given mode. + * + * 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. + * + * The contents of the zip file will be accessible until it is closed. + * + * The OPEN_DELETE mode is currently unimplemented in this library + * + * @since JDK1.3 + * @param mode + * Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE + * + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the file doesn't contain a valid zip archive. + */ + public ZipFile(final File file, final int mode) throws ZipException, + IOException { + if ((mode & OPEN_DELETE) != 0) { + throw new IllegalArgumentException( + "OPEN_DELETE mode not supported yet in net.sf.jazzlib.ZipFile"); + } + 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. + * + * @exception IOException + * if a i/o error occured. + * @exception EOFException + * if the file ends prematurely + */ + private final int readLeShort(final DataInput di, final byte[] b) + throws IOException { + 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. + * + * @exception IOException + * if a i/o error occured. + * @exception EOFException + * if the file ends prematurely + */ + private final int readLeInt(final DataInput di, final 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); + } + + /** + * 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(final byte[] b, final 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(final byte[] b, final 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 raf. + * + * @exception IOException + * if a i/o error occured. + * @exception 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; + final 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--); + } while (readLeInt(raf, ebs) != ENDSIG); + + if (raf.skipBytes(ENDTOT - ENDNRD) != (ENDTOT - ENDNRD)) { + throw new EOFException(name); + } + final int count = readLeShort(raf, ebs); + if (raf.skipBytes(ENDOFF - ENDSIZ) != (ENDOFF - ENDSIZ)) { + throw new EOFException(name); + } + final int centralOffset = readLeInt(raf, ebs); + + entries = new HashMap(count + (count / 2)); + raf.seek(centralOffset); + + byte[] buffer = new byte[16]; + for (int i = 0; i < count; i++) { + raf.readFully(ebs); + if (readLeInt(ebs, 0) != CENSIG) { + throw new ZipException("Wrong Central Directory signature: " + + name); + } + + final int method = readLeShort(ebs, CENHOW); + final int dostime = readLeInt(ebs, CENTIM); + final int crc = readLeInt(ebs, CENCRC); + final int csize = readLeInt(ebs, CENSIZ); + final int size = readLeInt(ebs, CENLEN); + final int nameLen = readLeShort(ebs, CENNAM); + final int extraLen = readLeShort(ebs, CENEXT); + final int commentLen = readLeShort(ebs, CENCOM); + + final int offset = readLeInt(ebs, CENOFF); + + final int needBuffer = Math.max(nameLen, commentLen); + if (buffer.length < needBuffer) { + buffer = new byte[needBuffer]; + } + + raf.readFully(buffer, 0, nameLen); + final String name = new String(buffer, 0, 0, nameLen); + + final ZipEntry entry = new ZipEntry(name); + entry.setMethod(method); + entry.setCrc(crc & 0xffffffffL); + entry.setSize(size & 0xffffffffL); + entry.setCompressedSize(csize & 0xffffffffL); + entry.setDOSTime(dostime); + if (extraLen > 0) { + final byte[] extra = new byte[extraLen]; + raf.readFully(extra); + entry.setExtra(extra); + } + if (commentLen > 0) { + raf.readFully(buffer, 0, commentLen); + entry.setComment(new String(buffer, 0, commentLen)); + } + entry.offset = offset; + entries.put(name, entry); + } + } + + /** + * Closes the ZipFile. This also closes all input streams given by this + * class. After this is called, no further method should be called. + * + * @exception IOException + * if a i/o error occured. + */ + public void close() throws IOException { + synchronized (raf) { + closed = true; + entries = null; + raf.close(); + } + } + + /** + * Calls the close() method when this ZipFile has not yet been + * explicitly closed. + */ + @Override + protected void finalize() throws IOException { + if (!closed && (raf != null)) { + close(); + } + } + + /** + * Returns an enumeration of all Zip entries in this Zip file. + */ + public Enumeration entries() { + try { + return new ZipEntryEnumeration(getEntries().values().iterator()); + } catch (final IOException ioe) { + return null; + } + } + + /** + * Checks that the ZipFile is still open and reads entries when necessary. + * + * @exception IllegalStateException + * when the ZipFile has already been closed. + * @exception IOEexception + * when the entries could not be read. + */ + private Map getEntries() throws IOException { + synchronized (raf) { + if (closed) { + throw new IllegalStateException("ZipFile has closed: " + name); + } + + if (entries == null) { + readEntries(); + } + + return entries; + } + } + + /** + * Searches for a zip entry in this archive with the given name. + * + * @param the + * name. May contain directory components separated by slashes + * ('/'). + * @return the zip entry, or null if no entry with that name exists. + */ + public ZipEntry getEntry(final String name) { + try { + final Map entries = getEntries(); + final ZipEntry entry = entries.get(name); + return entry != null ? (ZipEntry) entry.clone() : null; + } catch (final 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. + * + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the local header doesn't match the central directory + * header + */ + private long checkLocalHeader(final ZipEntry entry) throws IOException { + synchronized (raf) { + raf.seek(entry.offset); + raf.readFully(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.getName().length() != readLeShort(locBuf, LOCNAM)) { + throw new ZipException("file name length mismatch: " + name); + } + + final int extraLen = entry.getName().length() + + 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. + * + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the Zip archive is malformed. + */ + public InputStream getInputStream(final ZipEntry entry) throws IOException { + final Map entries = getEntries(); + final String name = entry.getName(); + final ZipEntry zipEntry = entries.get(name); + if (zipEntry == null) { + throw new NoSuchElementException(name); + } + + final long start = checkLocalHeader(zipEntry); + final int method = zipEntry.getMethod(); + final InputStream is = new BufferedInputStream(new PartialInputStream( + raf, 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 (final IOException ioe) { + return 0; + } + } + + private static class ZipEntryEnumeration implements Enumeration { + private final Iterator elements; + + public ZipEntryEnumeration(final Iterator elements) { + this.elements = elements; + } + + @Override + public boolean hasMoreElements() { + return elements.hasNext(); + } + + @Override + public Object nextElement() { + /* + * We return a clone, just to be safe that the user doesn't change + * the entry. + */ + return ((ZipEntry) elements.next()).clone(); + } + } + + private static class PartialInputStream extends InputStream { + private final RandomAccessFile raf; + long filepos, end; + + public PartialInputStream(final RandomAccessFile raf, final long start, + final long len) { + this.raf = raf; + filepos = start; + end = start + len; + } + + @Override + public int available() { + final long amount = end - filepos; + if (amount > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) amount; + } + + @Override + public int read() throws IOException { + if (filepos == end) { + return -1; + } + synchronized (raf) { + raf.seek(filepos++); + return raf.read(); + } + } + + @Override + public int read(final byte[] b, final int off, int len) + throws IOException { + if (len > (end - filepos)) { + len = (int) (end - filepos); + if (len == 0) { + return -1; + } + } + synchronized (raf) { + raf.seek(filepos); + final int count = raf.read(b, off, len); + if (count > 0) { + filepos += len; + } + return count; + } + } + + @Override + public long skip(long amount) { + if (amount < 0) { + throw new IllegalArgumentException(); + } + if (amount > (end - filepos)) { + amount = end - filepos; + } + filepos += amount; + return amount; + } + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/ZipInputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/ZipInputStream.java new file mode 100644 index 00000000..8caf80f3 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/ZipInputStream.java @@ -0,0 +1,380 @@ +/* net.sf.jazzlib.ZipInputStream + Copyright (C) 2001, 2002, 2003 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * This is a FilterInputStream that reads the files in an zip archive one after + * another. It has a special method to get the zip entry of the next file. The + * zip entry contains information about the file name size, compressed size, + * CRC, etc. + * + * It includes support for STORED and DEFLATED entries. + * + * @author Jochen Hoenicke + */ +public class ZipInputStream extends InflaterInputStream implements ZipConstants { + private CRC32 crc = new CRC32(); + private ZipEntry entry = null; + + private int csize; + private int size; + private int method; + private int flags; + private int avail; + private boolean entryAtEOF; + + /** + * Creates a new Zip input stream, reading a zip archive. + */ + public ZipInputStream(final InputStream in) { + super(in, new Inflater(true)); + } + + private void fillBuf() throws IOException { + avail = len = in.read(buf, 0, buf.length); + } + + private int readBuf(final byte[] out, final int offset, int length) + throws IOException { + if (avail <= 0) { + fillBuf(); + if (avail <= 0) { + return -1; + } + } + if (length > avail) { + length = avail; + } + System.arraycopy(buf, len - avail, out, offset, length); + avail -= length; + return length; + } + + private void readFully(final byte[] out) throws IOException { + int off = 0; + int len = out.length; + while (len > 0) { + final int count = readBuf(out, off, len); + if (count == -1) { + throw new EOFException(); + } + off += count; + len -= count; + } + } + + private final int readLeByte() throws IOException { + if (avail <= 0) { + fillBuf(); + if (avail <= 0) { + throw new ZipException("EOF in header"); + } + } + return buf[len - avail--] & 0xff; + } + + /** + * Read an unsigned short in little endian byte order. + */ + private final int readLeShort() throws IOException { + return readLeByte() | (readLeByte() << 8); + } + + /** + * Read an int in little endian byte order. + */ + private final int readLeInt() throws IOException { + return readLeShort() | (readLeShort() << 16); + } + + /** + * Open the next entry from the zip archive, and return its description. If + * the previous entry wasn't closed, this method will close it. + */ + public ZipEntry getNextEntry() throws IOException { + if (crc == null) { + throw new IOException("Stream closed."); + } + if (entry != null) { + closeEntry(); + } + + final int header = readLeInt(); + if (header == CENSIG) { + /* Central Header reached. */ + close(); + return null; + } + if (header != LOCSIG) { + throw new ZipException("Wrong Local header signature: " + + Integer.toHexString(header)); + } + /* skip version */ + readLeShort(); + flags = readLeShort(); + method = readLeShort(); + final int dostime = readLeInt(); + final int crc = readLeInt(); + csize = readLeInt(); + size = readLeInt(); + final int nameLen = readLeShort(); + final int extraLen = readLeShort(); + + if ((method == ZipOutputStream.STORED) && (csize != size)) { + throw new ZipException("Stored, but compressed != uncompressed"); + } + + final byte[] buffer = new byte[nameLen]; + readFully(buffer); + final String name = new String(buffer); + + entry = createZipEntry(name); + entryAtEOF = false; + entry.setMethod(method); + if ((flags & 8) == 0) { + entry.setCrc(crc & 0xffffffffL); + entry.setSize(size & 0xffffffffL); + entry.setCompressedSize(csize & 0xffffffffL); + } + entry.setDOSTime(dostime); + if (extraLen > 0) { + final byte[] extra = new byte[extraLen]; + readFully(extra); + entry.setExtra(extra); + } + + if ((method == ZipOutputStream.DEFLATED) && (avail > 0)) { + System.arraycopy(buf, len - avail, buf, 0, avail); + len = avail; + avail = 0; + inf.setInput(buf, 0, len); + } + return entry; + } + + private void readDataDescr() throws IOException { + if (readLeInt() != EXTSIG) { + throw new ZipException("Data descriptor signature not found"); + } + entry.setCrc(readLeInt() & 0xffffffffL); + csize = readLeInt(); + size = readLeInt(); + entry.setSize(size & 0xffffffffL); + entry.setCompressedSize(csize & 0xffffffffL); + } + + /** + * Closes the current zip entry and moves to the next one. + */ + public void closeEntry() throws IOException { + if (crc == null) { + throw new IOException("Stream closed."); + } + if (entry == null) { + return; + } + + if (method == ZipOutputStream.DEFLATED) { + if ((flags & 8) != 0) { + /* We don't know how much we must skip, read until end. */ + final byte[] tmp = new byte[2048]; + while (read(tmp) > 0) { + ; + } + /* read will close this entry */ + return; + } + csize -= inf.getTotalIn(); + avail = inf.getRemaining(); + } + + if ((avail > csize) && (csize >= 0)) { + avail -= csize; + } else { + csize -= avail; + avail = 0; + while (csize != 0) { + final long skipped = in.skip(csize & 0xffffffffL); + if (skipped <= 0) { + throw new ZipException("zip archive ends early."); + } + csize -= skipped; + } + } + + size = 0; + crc.reset(); + if (method == ZipOutputStream.DEFLATED) { + inf.reset(); + } + entry = null; + entryAtEOF = true; + } + + @Override + public int available() throws IOException { + return entryAtEOF ? 0 : 1; + } + + /** + * Reads a byte from the current zip entry. + * + * @return the byte or -1 on EOF. + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the deflated stream is corrupted. + */ + @Override + public int read() throws IOException { + final byte[] b = new byte[1]; + if (read(b, 0, 1) <= 0) { + return -1; + } + return b[0] & 0xff; + } + + /** + * Reads a block of bytes from the current zip entry. + * + * @return the number of bytes read (may be smaller, even before EOF), or -1 + * on EOF. + * @exception IOException + * if a i/o error occured. + * @exception ZipException + * if the deflated stream is corrupted. + */ + @Override + public int read(final byte[] b, final int off, int len) throws IOException { + if (len == 0) { + return 0; + } + if (crc == null) { + throw new IOException("Stream closed."); + } + if (entry == null) { + return -1; + } + boolean finished = false; + switch (method) { + case ZipOutputStream.DEFLATED: + len = super.read(b, off, len); + if (len < 0) { + if (!inf.finished()) { + throw new ZipException("Inflater not finished!?"); + } + avail = inf.getRemaining(); + if ((flags & 8) != 0) { + readDataDescr(); + } + + if ((inf.getTotalIn() != csize) || (inf.getTotalOut() != size)) { + throw new ZipException("size mismatch: " + csize + ";" + + size + " <-> " + inf.getTotalIn() + ";" + + inf.getTotalOut()); + } + inf.reset(); + finished = true; + } + break; + + case ZipOutputStream.STORED: + + if ((len > csize) && (csize >= 0)) { + len = csize; + } + + len = readBuf(b, off, len); + if (len > 0) { + csize -= len; + size -= len; + } + + if (csize == 0) { + finished = true; + } else if (len < 0) { + throw new ZipException("EOF in stored block"); + } + break; + } + + if (len > 0) { + crc.update(b, off, len); + } + + if (finished) { + final long entryCrc = entry.getCrc(); + if ((entryCrc >= 0) && ((crc.getValue() & 0xffffffffL) != entryCrc)) { + throw new ZipException("CRC mismatch"); + } + crc.reset(); + entry = null; + entryAtEOF = true; + } + return len; + } + + /** + * Closes the zip file. + * + * @exception IOException + * if a i/o error occured. + */ + @Override + public void close() throws IOException { + super.close(); + crc = null; + entry = null; + entryAtEOF = true; + } + + /** + * Creates a new zip entry for the given name. This is equivalent to new + * ZipEntry(name). + * + * @param name + * the name of the zip entry. + */ + protected ZipEntry createZipEntry(final String name) { + return new ZipEntry(name); + } +} diff --git a/epublib-core/src/main/java/net/sf/jazzlib/ZipOutputStream.java b/epublib-core/src/main/java/net/sf/jazzlib/ZipOutputStream.java new file mode 100644 index 00000000..0ae94b85 --- /dev/null +++ b/epublib-core/src/main/java/net/sf/jazzlib/ZipOutputStream.java @@ -0,0 +1,425 @@ +/* net.sf.jazzlib.ZipOutputStream + Copyright (C) 2001 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +02111-1307 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + +package net.sf.jazzlib; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.Vector; + +/** + * This is a FilterOutputStream that writes the files into a zip archive one + * after another. It has a special method to start a new zip entry. The zip + * entries contains information about the file name size, compressed size, CRC, + * etc. + * + * It includes support for STORED and DEFLATED entries. + * + * This class is not thread safe. + * + * @author Jochen Hoenicke + */ +public class ZipOutputStream extends DeflaterOutputStream implements +ZipConstants { + private Vector entries = new Vector(); + private final CRC32 crc = new CRC32(); + private ZipEntry curEntry = null; + + private int curMethod; + private int size; + private int offset = 0; + + private byte[] zipComment = new byte[0]; + private int defaultMethod = DEFLATED; + + /** + * Our Zip version is hard coded to 1.0 resp. 2.0 + */ + private final static int ZIP_STORED_VERSION = 10; + private final static int ZIP_DEFLATED_VERSION = 20; + + /** + * 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 new Zip output stream, writing a zip archive. + * + * @param out + * the output stream to which the zip archive is written. + */ + public ZipOutputStream(final OutputStream out) { + super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); + } + + /** + * Set the zip file comment. + * + * @param comment + * the comment. + * @exception IllegalArgumentException + * if encoding of comment is longer than 0xffff bytes. + */ + public void setComment(final String comment) { + byte[] commentBytes; + commentBytes = comment.getBytes(); + if (commentBytes.length > 0xffff) { + throw new IllegalArgumentException("Comment too long."); + } + zipComment = commentBytes; + } + + /** + * Sets default compression method. If the Zip entry specifies another + * method its method takes precedence. + * + * @param method + * the method. + * @exception IllegalArgumentException + * if method is not supported. + * @see #STORED + * @see #DEFLATED + */ + public void setMethod(final int method) { + if ((method != STORED) && (method != DEFLATED)) { + throw new IllegalArgumentException("Method not supported."); + } + defaultMethod = method; + } + + /** + * Sets default compression level. The new level will be activated + * immediately. + * + * @exception IllegalArgumentException + * if level is not supported. + * @see Deflater + */ + public void setLevel(final int level) { + def.setLevel(level); + } + + /** + * Write an unsigned short in little endian byte order. + */ + private final void writeLeShort(final int value) throws IOException { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + } + + /** + * Write an int in little endian byte order. + */ + private final void writeLeInt(final int value) throws IOException { + writeLeShort(value); + writeLeShort(value >> 16); + } + + /** + * Starts a new Zip entry. It automatically closes the previous entry if + * present. If the compression method is stored, the entry must have a valid + * size and crc, otherwise all elements (except name) are optional, but must + * be correct if present. If the time is not set in the entry, the current + * time is used. + * + * @param entry + * the entry. + * @exception IOException + * if an I/O error occured. + * @exception ZipException + * if stream was finished. + */ + public void putNextEntry(final ZipEntry entry) throws IOException { + if (entries == null) { + throw new ZipException("ZipOutputStream was finished"); + } + + int method = entry.getMethod(); + int flags = 0; + if (method == -1) { + method = defaultMethod; + } + + if (method == STORED) { + if (entry.getCompressedSize() >= 0) { + if (entry.getSize() < 0) { + entry.setSize(entry.getCompressedSize()); + } else if (entry.getSize() != entry.getCompressedSize()) { + throw new ZipException( + "Method STORED, but compressed size != size"); + } + } else { + entry.setCompressedSize(entry.getSize()); + } + + if (entry.getSize() < 0) { + throw new ZipException("Method STORED, but size not set"); + } + if (entry.getCrc() < 0) { + throw new ZipException("Method STORED, but crc not set"); + } + } else if (method == DEFLATED) { + if ((entry.getCompressedSize() < 0) || (entry.getSize() < 0) + || (entry.getCrc() < 0)) { + flags |= 8; + } + } + + if (curEntry != null) { + closeEntry(); + } + + if (entry.getTime() < 0) { + entry.setTime(System.currentTimeMillis()); + } + + entry.flags = flags; + entry.offset = offset; + entry.setMethod(method); + curMethod = method; + /* Write the local file header */ + writeLeInt(LOCSIG); + writeLeShort(method == STORED ? ZIP_STORED_VERSION + : ZIP_DEFLATED_VERSION); + writeLeShort(flags); + writeLeShort(method); + writeLeInt(entry.getDOSTime()); + if ((flags & 8) == 0) { + writeLeInt((int) entry.getCrc()); + writeLeInt((int) entry.getCompressedSize()); + writeLeInt((int) entry.getSize()); + } else { + writeLeInt(0); + writeLeInt(0); + writeLeInt(0); + } + final byte[] name = entry.getName().getBytes(); + if (name.length > 0xffff) { + throw new ZipException("Name too long."); + } + byte[] extra = entry.getExtra(); + if (extra == null) { + extra = new byte[0]; + } + writeLeShort(name.length); + writeLeShort(extra.length); + out.write(name); + out.write(extra); + + offset += LOCHDR + name.length + extra.length; + + /* Activate the entry. */ + + curEntry = entry; + crc.reset(); + if (method == DEFLATED) { + def.reset(); + } + size = 0; + } + + /** + * Closes the current entry. + * + * @exception IOException + * if an I/O error occured. + * @exception ZipException + * if no entry is active. + */ + public void closeEntry() throws IOException { + if (curEntry == null) { + throw new ZipException("No open entry"); + } + + /* First finish the deflater, if appropriate */ + if (curMethod == DEFLATED) { + super.finish(); + } + + final int csize = curMethod == DEFLATED ? def.getTotalOut() : size; + + if (curEntry.getSize() < 0) { + curEntry.setSize(size); + } else if (curEntry.getSize() != size) { + throw new ZipException("size was " + size + ", but I expected " + + curEntry.getSize()); + } + + if (curEntry.getCompressedSize() < 0) { + curEntry.setCompressedSize(csize); + } else if (curEntry.getCompressedSize() != csize) { + throw new ZipException("compressed size was " + csize + + ", but I expected " + curEntry.getSize()); + } + + if (curEntry.getCrc() < 0) { + curEntry.setCrc(crc.getValue()); + } else if (curEntry.getCrc() != crc.getValue()) { + throw new ZipException("crc was " + + Long.toHexString(crc.getValue()) + ", but I expected " + + Long.toHexString(curEntry.getCrc())); + } + + offset += csize; + + /* Now write the data descriptor entry if needed. */ + if ((curMethod == DEFLATED) && ((curEntry.flags & 8) != 0)) { + writeLeInt(EXTSIG); + writeLeInt((int) curEntry.getCrc()); + writeLeInt((int) curEntry.getCompressedSize()); + writeLeInt((int) curEntry.getSize()); + offset += EXTHDR; + } + + entries.addElement(curEntry); + curEntry = null; + } + + /** + * Writes the given buffer to the current entry. + * + * @exception IOException + * if an I/O error occured. + * @exception ZipException + * if no entry is active. + */ + @Override + public void write(final byte[] b, final int off, final int len) + throws IOException { + if (curEntry == null) { + throw new ZipException("No open entry."); + } + + switch (curMethod) { + case DEFLATED: + super.write(b, off, len); + break; + + case STORED: + out.write(b, off, len); + break; + } + + crc.update(b, off, len); + size += len; + } + + /** + * Finishes the stream. This will write the central directory at the end of + * the zip file and flush the stream. + * + * @exception IOException + * if an I/O error occured. + */ + @Override + public void finish() throws IOException { + if (entries == null) { + return; + } + if (curEntry != null) { + closeEntry(); + } + + int numEntries = 0; + int sizeEntries = 0; + + final Enumeration elements = entries.elements(); + while (elements.hasMoreElements()) { + final ZipEntry entry = (ZipEntry) elements.nextElement(); + + final int method = entry.getMethod(); + writeLeInt(CENSIG); + writeLeShort(method == STORED ? ZIP_STORED_VERSION + : ZIP_DEFLATED_VERSION); + writeLeShort(method == STORED ? ZIP_STORED_VERSION + : ZIP_DEFLATED_VERSION); + writeLeShort(entry.flags); + writeLeShort(method); + writeLeInt(entry.getDOSTime()); + writeLeInt((int) entry.getCrc()); + writeLeInt((int) entry.getCompressedSize()); + writeLeInt((int) entry.getSize()); + + final byte[] name = entry.getName().getBytes(); + if (name.length > 0xffff) { + throw new ZipException("Name too long."); + } + byte[] extra = entry.getExtra(); + if (extra == null) { + extra = new byte[0]; + } + final String strComment = entry.getComment(); + final byte[] comment = strComment != null ? strComment.getBytes() + : new byte[0]; + if (comment.length > 0xffff) { + throw new ZipException("Comment too long."); + } + + writeLeShort(name.length); + writeLeShort(extra.length); + writeLeShort(comment.length); + writeLeShort(0); /* disk number */ + writeLeShort(0); /* internal file attr */ + writeLeInt(0); /* external file attr */ + writeLeInt(entry.offset); + + out.write(name); + out.write(extra); + out.write(comment); + numEntries++; + sizeEntries += CENHDR + name.length + extra.length + comment.length; + } + + writeLeInt(ENDSIG); + writeLeShort(0); /* disk number */ + writeLeShort(0); /* disk with start of central dir */ + writeLeShort(numEntries); + writeLeShort(numEntries); + writeLeInt(sizeEntries); + writeLeInt(offset); + writeLeShort(zipComment.length); + out.write(zipComment); + out.flush(); + entries = null; + } +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationEvent.java b/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationEvent.java index 590433f1..e3a96b04 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationEvent.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationEvent.java @@ -40,7 +40,7 @@ public NavigationEvent(Object source, Navigator navigator) { /** * The previous position within the section. * - * @return + * @return The previous position within the section. */ public int getOldSectionPos() { return oldSectionPos; @@ -151,4 +151,4 @@ public String toString() { public boolean isSectionPosChanged() { return oldSectionPos != getCurrentSectionPos(); } -} \ No newline at end of file +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationHistory.java b/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationHistory.java index 61849900..1d7aed5c 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationHistory.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/NavigationHistory.java @@ -76,7 +76,7 @@ public void initBook(Book book) { * If the time between a navigation event is less than the historyWaitTime then the new location is not added to the history. * 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 + * @return the time we wait before adding the page to the history */ public long getHistoryWaitTime() { return historyWaitTime; @@ -101,7 +101,6 @@ public void addLocation(Resource resource) { * If this nr of locations becomes larger then the historySize then the first item(s) will be removed. * * @param location - * @return */ public void addLocation(Location location) { // do nothing if the new location matches the current location diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/Navigator.java b/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/Navigator.java index dfa352c5..7f1e6643 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/Navigator.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/browsersupport/Navigator.java @@ -137,7 +137,7 @@ public int gotoSpineSection(int newSpinePos, Object source) { * * @param newSpinePos * @param source - * @return + * @return The current position within the spine */ public int gotoSpineSection(int newSpinePos, int newPagePos, Object source) { if (newSpinePos == currentSpinePos) { @@ -203,7 +203,6 @@ public Book getBook() { * * If you want the eventListeners called use gotoSection(index); * - * @param currentSpinePos */ public int setCurrentResource(Resource currentResource) { this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Book.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Book.java index 277f144b..152d5b69 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Book.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Book.java @@ -312,7 +312,7 @@ public class Book implements Serializable { * @param parentSection * @param sectionTitle * @param resource - * @return + * @return The table of contents */ public TOCReference addSection(TOCReference parentSection, String sectionTitle, Resource resource) { @@ -337,7 +337,7 @@ public void generateSpineFromTableOfContents() { * * @param title * @param resource - * @return + * @return The table of contents */ public TOCReference addSection(String title, Resource resource) { getResources().add(resource); @@ -352,7 +352,7 @@ public TOCReference addSection(String title, Resource resource) { /** * The Book's metadata (titles, authors, etc) * - * @return + * @return The Book's metadata (titles, authors, etc) */ public Metadata getMetadata() { return metadata; @@ -374,7 +374,7 @@ public Resource addResource(Resource resource) { /** * The collection of all images, chapters, sections, xhtml files, stylesheets, etc that make up the book. * - * @return + * @return The collection of all images, chapters, sections, xhtml files, stylesheets, etc that make up the book. */ public Resources getResources() { return resources; @@ -384,7 +384,7 @@ public Resources getResources() { /** * The sections of the book that should be shown if a user reads the book from start to finish. * - * @return + * @return The Spine */ public Spine getSpine() { return spine; @@ -399,7 +399,7 @@ public void setSpine(Spine spine) { /** * The Table of Contents of the book. * - * @return + * @return The Table of Contents of the book. */ public TableOfContents getTableOfContents() { return tableOfContents; @@ -411,10 +411,10 @@ public void setTableOfContents(TableOfContents tableOfContents) { } /** - * The book's cover page. + * The book's cover page as a Resource. * An XHTML document containing a link to the cover image. * - * @return + * @return The book's cover page as a Resource */ public Resource getCoverPage() { Resource coverPage = guide.getCoverPage(); @@ -438,7 +438,7 @@ public void setCoverPage(Resource coverPage) { /** * Gets the first non-blank title from the book's metadata. * - * @return + * @return the first non-blank title from the book's metadata. */ public String getTitle() { return getMetadata().getFirstTitle(); @@ -448,7 +448,7 @@ public String getTitle() { /** * The book's cover image. * - * @return + * @return The book's cover image. */ public Resource getCoverImage() { return coverImage; @@ -467,7 +467,7 @@ public void setCoverImage(Resource coverImage) { /** * The guide; contains references to special sections of the book like colophon, glossary, etc. * - * @return + * @return The guide; contains references to special sections of the book like colophon, glossary, etc. */ public Guide getGuide() { return guide; @@ -483,9 +483,8 @@ public Guide getGuide() { *
  • The resources of the Table of Contents that are not already in the result
  • *
  • The resources of the Guide that are not already in the result
  • * - * To get all html files that make up the epub file use - * @see getResources().getAll() - * @return + * 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 getContents() { Map result = new LinkedHashMap(); diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Guide.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Guide.java index 58e1e018..e18d7167 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Guide.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Guide.java @@ -83,7 +83,7 @@ private void initCoverPage() { /** * The coverpage of the book. * - * @return + * @return The coverpage of the book. */ public Resource getCoverPage() { GuideReference guideReference = getCoverReference(); @@ -109,7 +109,7 @@ public ResourceReference addReference(GuideReference reference) { * A list of all GuideReferences that have the given referenceTypeName (ignoring case). * * @param referenceTypeName - * @return + * @return A list of all GuideReferences that have the given referenceTypeName (ignoring case). */ public List getGuideReferencesByType(String referenceTypeName) { List result = new ArrayList(); @@ -120,4 +120,4 @@ public List getGuideReferencesByType(String referenceTypeName) { } return result; } -} \ No newline at end of file +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Identifier.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Identifier.java index 90e7944f..a6e30acd 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Identifier.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Identifier.java @@ -50,7 +50,7 @@ public Identifier(String scheme, String value) { * If no identifier has bookId == true then the first bookId identifier is written as the primary. * * @param identifiers - * @return + * @return The first identifier for which the bookId is true is made the bookId identifier. */ public static Identifier getBookIdIdentifier(List identifiers) { if(identifiers == null || identifiers.isEmpty()) { @@ -97,7 +97,7 @@ public void setBookId(boolean bookId) { * 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 + * @return whether this is the unique book id. */ public boolean isBookId() { return bookId; diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/LazyResource.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/LazyResource.java new file mode 100644 index 00000000..1983dd31 --- /dev/null +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/LazyResource.java @@ -0,0 +1,166 @@ +package nl.siegmann.epublib.domain; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import nl.siegmann.epublib.service.MediatypeService; +import nl.siegmann.epublib.util.IOUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Resource that loads its data only on-demand. + * 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 String filename; + private long cachedSize; + + private static final Logger LOG = LoggerFactory.getLogger(LazyResource.class); + + /** + * Creates a Lazy resource, by not actually loading the data for this entry. + * + * The data will be loaded on the first call to getData() + * + * @param filename the file name for the epub we're created from. + * @param size the size of this resource. + * @param href The resource's href within the epub. + */ + public LazyResource(String filename, long size, String href) { + super( null, null, href, MediatypeService.determineMediaType(href)); + this.filename = filename; + this.cachedSize = size; + } + + /** + * Creates a Resource that tries to load the data, but falls back to lazy loading. + * + * If the size of the resource is known ahead of time we can use that to allocate + * a matching byte[]. If this succeeds we can safely load the data. + * + * If it fails we leave the data null for now and it will be lazy-loaded when + * it is accessed. + * + * @param in + * @param fileName + * @param length + * @param href + * @throws IOException + */ + public LazyResource(InputStream in, String filename, int length, String href) throws IOException { + super(null, IOUtil.toByteArray(in, length), href, MediatypeService.determineMediaType(href)); + this.filename = filename; + this.cachedSize = length; + } + + /** + * Gets the contents of the Resource as an InputStream. + * + * @return The contents of the Resource. + * + * @throws IOException + */ + public InputStream getInputStream() throws IOException { + if (isInitialized()) { + return new ByteArrayInputStream(getData()); + } else { + return getResourceStream(); + } + } + + /** + * Initializes the resource by loading its data into memory. + * + * @throws IOException + */ + public void initialize() throws IOException { + getData(); + } + + /** + * The contents of the resource as a byte[] + * + * 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.debug("Initializing lazy resource " + filename + "#" + this.getHref() ); + + InputStream in = getResourceStream(); + byte[] readData = IOUtil.toByteArray(in, (int) this.cachedSize); + if ( readData == null ) { + throw new IOException("Could not load the contents of entry " + this.getHref() + " from epub file " + filename); + } else { + this.data = readData; + } + + in.close(); + } + + return data; + } + + + private InputStream getResourceStream() throws FileNotFoundException, + IOException { + ZipFile zipFile = new ZipFile(filename); + ZipEntry zipEntry = zipFile.getEntry(originalHref); + if (zipEntry == null) { + zipFile.close(); + throw new IllegalStateException("Cannot find entry " + originalHref + " in epub file " + filename); + } + return new ResourceInputStream(zipFile.getInputStream(zipEntry), zipFile); + } + + /** + * Tells this resource to release its cached data. + * + * If this resource was not lazy-loaded, this is a no-op. + */ + public void close() { + if ( this.filename != 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; + } +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Metadata.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Metadata.java index 680ca886..4fbbc312 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Metadata.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Metadata.java @@ -41,8 +41,8 @@ public class Metadata implements Serializable { private List types = new ArrayList(); private List descriptions = new ArrayList(); private List publishers = new ArrayList(); - private Resource coverImage; - + private Map metaAttributes = new HashMap(); + public Metadata() { identifiers.add(new Identifier()); autoGeneratedId = true; @@ -55,7 +55,7 @@ public boolean isAutoGeneratedId() { /** * Metadata properties not hard-coded like the author, title, etc. * - * @return + * @return Metadata properties not hard-coded like the author, title, etc. */ public Map getOtherProperties() { return otherProperties; @@ -124,7 +124,7 @@ public List getRights() { * Gets the first non-blank title of the book. * Will return "" if no title found. * - * @return + * @return the first non-blank title of the book. */ public String getFirstTitle() { if (titles == null || titles.isEmpty()) { @@ -207,4 +207,12 @@ public List getTypes() { public void setTypes(List types) { this.types = types; } + + public String getMetaAttribute(String name) { + return metaAttributes.get(name); + } + + public void setMetaAttributes(Map metaAttributes) { + this.metaAttributes = metaAttributes; + } } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Relator.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Relator.java index 3dc5bc88..4ce05796 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Relator.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Relator.java @@ -8,7 +8,7 @@ * * This is contains the complete Library of Concress relator list. * - * @see http://www.loc.gov/marc/relators/relaterm.html + * @see MARC Code List for Relators * * @author paul * diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resource.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resource.java index 7b46e8f1..e8fefc18 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resource.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resource.java @@ -1,16 +1,10 @@ package nl.siegmann.epublib.domain; import java.io.ByteArrayInputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.Serializable; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import nl.siegmann.epublib.Constants; import nl.siegmann.epublib.service.MediatypeService; @@ -34,14 +28,10 @@ public class Resource implements Serializable { private String id; private String title; private String href; + protected String originalHref; private MediaType mediaType; private String inputEncoding = Constants.CHARACTER_ENCODING; - private byte[] data; - - private String fileName; - private long cachedSize; - - private static final Logger LOG = LoggerFactory.getLogger(Resource.class); + protected byte[] data; /** * Creates an empty Resource with the given href. @@ -73,7 +63,7 @@ public Resource(byte[] data, MediaType mediaType) { * * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * - * @see nl.siegmann.epublib.service.MediatypeService.determineMediaType(String) + * @see nl.siegmann.epublib.service.MediatypeService#determineMediaType(String) * * @param data The Resource's contents * @param href The location of the resource within the epub. Example: "chapter1.html". @@ -85,7 +75,8 @@ public Resource(byte[] data, String href) { /** * Creates a resource with the data from the given Reader at the specified href. * The MediaType will be determined based on the href extension. - * @see nl.siegmann.epublib.service.MediatypeService.determineMediaType(String) + * + * @see nl.siegmann.epublib.service.MediatypeService#determineMediaType(String) * * @param in The Resource's contents * @param href The location of the resource within the epub. Example: "cover.jpg". @@ -97,13 +88,13 @@ public Resource(Reader in, String href) throws IOException { /** * Creates a resource with the data from the given InputStream at the specified href. * The MediaType will be determined based on the href extension. - * @see nl.siegmann.epublib.service.MediatypeService.determineMediaType(String) + * + * @see nl.siegmann.epublib.service.MediatypeService#determineMediaType(String) * * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 * - * It is recommended to us the - * @see nl.siegmann.epublib.domain.Resource.Resource(Reader, String) - * method for creating textual (html/css/etc) resources to prevent encoding problems. + * 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. * * @@ -114,21 +105,6 @@ public Resource(InputStream in, String href) throws IOException { this(null, IOUtil.toByteArray(in), href, MediatypeService.determineMediaType(href)); } - /** - * Creates a Lazy resource, by not actually loading the data for this entry. - * - * The data will be loaded on the first call to getData() - * - * @param fileName the fileName for the epub we're created from. - * @param size the size of this resource. - * @param href The resource's href within the epub. - */ - public Resource( String fileName, long size, String href) { - this( null, null, href, MediatypeService.determineMediaType(href)); - this.fileName = fileName; - this.cachedSize = size; - } - /** * 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 @@ -141,7 +117,8 @@ public Resource( String fileName, long size, String href) { public Resource(String id, byte[] data, String href, MediaType mediaType) { this(id, data, href, 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. @@ -155,6 +132,7 @@ public Resource(String id, byte[] data, String href, MediaType mediaType) { 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; @@ -170,49 +148,22 @@ public Resource(String id, byte[] data, String href, MediaType mediaType, String public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(getData()); } - + /** * The contents of the resource as a byte[] * - * 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.info("Initializing lazy resource " + fileName + "#" + this.href ); - - ZipInputStream in = new ZipInputStream(new FileInputStream(this.fileName)); - - for(ZipEntry zipEntry = in.getNextEntry(); zipEntry != null; zipEntry = in.getNextEntry()) { - if(zipEntry.isDirectory()) { - continue; - } - - if ( zipEntry.getName().endsWith(this.href)) { - this.data = IOUtil.toByteArray(in); - } - } - - in.close(); - } - return data; } - + /** * Tells this resource to release its cached data. * * If this resource was not lazy-loaded, this is a no-op. */ public void close() { - if ( this.fileName != null ) { - this.data = null; - } } /** @@ -225,32 +176,19 @@ public void setData(byte[] data) { this.data = data; } - /** - * 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; + return data.length; } /** * If the title is found by scanning the underlying html document then it is cached here. * - * @return + * @return the title */ public String getTitle() { return title; @@ -269,7 +207,7 @@ public void setId(String id) { * The resources Id. * * Must be both unique within all the resources of this book and a valid identifier. - * @return + * @return The resources Id. */ public String getId() { return id; @@ -282,7 +220,7 @@ public String getId() { * images/cover.jpg
    * content/chapter1.xhtml
    * - * @return + * @return The location of the resource within the contents folder of the epub file. */ public String getHref() { return href; @@ -301,7 +239,7 @@ public void setHref(String href) { * The character encoding of the resource. * Is allowed to be null for non-text resources like images. * - * @return + * @return The character encoding of the resource. */ public String getInputEncoding() { return inputEncoding; @@ -321,8 +259,7 @@ public void setInputEncoding(String encoding) { * * Does all sorts of smart things (courtesy of apache commons io XMLStreamREader) to handle encodings, byte order markers, etc. * - * @param resource - * @return + * @return the contents of the Resource as Reader. * @throws IOException */ public Reader getReader() throws IOException { @@ -340,6 +277,7 @@ public int 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)) { @@ -351,7 +289,7 @@ public boolean equals(Object resourceObject) { /** * This resource's mediaType. * - * @return + * @return This resource's mediaType. */ public MediaType getMediaType() { return mediaType; diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceInputStream.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceInputStream.java new file mode 100644 index 00000000..92b305ff --- /dev/null +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceInputStream.java @@ -0,0 +1,37 @@ +package nl.siegmann.epublib.domain; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipFile; + + +/** + * A wrapper class for closing a ZipFile 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. + * @param zipFile + * The ZipFile object. + */ + public ResourceInputStream(InputStream in, ZipFile zipFile) { + super(in); + this.zipFile = zipFile; + } + + @Override + public void close() throws IOException { + super.close(); + zipFile.close(); + } +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceReference.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceReference.java index 096aa466..9ba8cea0 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceReference.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/ResourceReference.java @@ -34,7 +34,7 @@ public void setResource(Resource resource) { * * null of the reference is null or has a null id itself. * - * @return + * @return The id of the reference referred to. */ public String getResourceId() { if (resource != null) { diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resources.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resources.java index 85ed1233..d7b88a9c 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resources.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Resources.java @@ -37,7 +37,7 @@ public class Resources implements Serializable { * Fixes the resources id and href if necessary. * * @param resource - * @return + * @return the newly added resource */ public Resource add(Resource resource) { fixResourceHref(resource); @@ -73,7 +73,7 @@ public void fixResourceId(Resource resource) { * Check if the id is a valid identifier. if not: prepend with valid identifier * * @param resource - * @return + * @return a valid id */ private String makeValidId(String resourceId, Resource resource) { if (StringUtil.isNotBlank(resourceId) && ! Character.isJavaIdentifierStart(resourceId.charAt(0))) { @@ -93,10 +93,10 @@ private String getResourceItemPrefix(Resource resource) { } /** - * Creates a new resource id that is guarenteed to be unique for this set of Resources + * Creates a new resource id that is guaranteed to be unique for this set of Resources * * @param resource - * @return + * @return a new resource id that is guaranteed to be unique for this set of Resources */ private String createUniqueResourceId(Resource resource) { int counter = lastId; @@ -120,7 +120,7 @@ private String createUniqueResourceId(Resource resource) { * Whether the map of resources already contains a resource with the given id. * * @param id - * @return + * @return Whether the map of resources already contains a resource with the given id. */ public boolean containsId(String id) { if (StringUtil.isBlank(id)) { @@ -195,7 +195,7 @@ public boolean isEmpty() { /** * The number of resources - * @return + * @return The number of resources */ public int size() { return resources.size(); @@ -205,7 +205,7 @@ public int size() { * The resources that make up this book. * Resources can be xhtml pages, images, xml documents, etc. * - * @return + * @return The resources that make up this book. */ public Map getResourceMap() { return resources; @@ -219,7 +219,7 @@ public Collection getAll() { /** * Whether there exists a resource with the given href * @param href - * @return + * @return Whether there exists a resource with the given href */ public boolean containsByHref(String href) { if (StringUtil.isBlank(href)) { @@ -265,7 +265,7 @@ public void set(Map resources) { * fails it tries to find one with the idOrHref as href. * * @param idOrHref - * @return + * @return the found Resource */ public Resource getByIdOrHref(String idOrHref) { Resource resource = getById(idOrHref); @@ -298,7 +298,7 @@ public Resource getByHref(String href) { * Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype. * * @param mediaType - * @return + * @return the first resource (random order) with the give mediatype. */ public Resource findFirstResourceByMediaType(MediaType mediaType) { return findFirstResourceByMediaType(resources.values(), mediaType); @@ -310,7 +310,7 @@ public Resource findFirstResourceByMediaType(MediaType mediaType) { * Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype. * * @param mediaType - * @return + * @return the first resource (random order) with the give mediatype. */ public static Resource findFirstResourceByMediaType(Collection resources, MediaType mediaType) { for (Resource resource: resources) { @@ -325,7 +325,7 @@ public static Resource findFirstResourceByMediaType(Collection resourc * All resources that have the given MediaType. * * @param mediaType - * @return + * @return All resources that have the given MediaType. */ public List getResourcesByMediaType(MediaType mediaType) { List result = new ArrayList(); @@ -344,7 +344,7 @@ public List getResourcesByMediaType(MediaType mediaType) { * All Resources that match any of the given list of MediaTypes * * @param mediaTypes - * @return + * @return All Resources that match any of the given list of MediaTypes */ public List getResourcesByMediaTypes(MediaType[] mediaTypes) { List result = new ArrayList(); @@ -364,6 +364,11 @@ public List getResourcesByMediaTypes(MediaType[] mediaTypes) { } + /** + * All resource hrefs + * + * @return all resource hrefs + */ public Collection getAllHrefs() { return resources.keySet(); } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Spine.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Spine.java index d0cda7ea..069ac5f7 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/Spine.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/Spine.java @@ -63,7 +63,7 @@ public void setSpineReferences(List spineReferences) { * Null if not found. * * @param index - * @return + * @return the resource at the given index. */ public Resource getResource(int index) { if (index < 0 || index >= spineReferences.size()) { @@ -78,7 +78,7 @@ public Resource getResource(int index) { * Null if not found. * * @param resourceId - * @return + * @return the first resource that has the given resourceId. */ public int findFirstResourceById(String resourceId) { if (StringUtil.isBlank(resourceId)) { @@ -98,7 +98,7 @@ public int findFirstResourceById(String resourceId) { * Adds the given spineReference to the spine references and returns it. * * @param spineReference - * @return + * @return the given spineReference */ public SpineReference addSpineReference(SpineReference spineReference) { if (spineReferences == null) { @@ -111,8 +111,7 @@ public SpineReference addSpineReference(SpineReference spineReference) { /** * Adds the given resource to the spine references and returns it. * - * @param spineReference - * @return + * @return the given spineReference */ public SpineReference addResource(Resource resource) { return addSpineReference(new SpineReference(resource)); @@ -121,7 +120,7 @@ public SpineReference addResource(Resource resource) { /** * The number of elements in the spine. * - * @return + * @return The number of elements in the spine. */ public int size() { return spineReferences.size(); @@ -142,7 +141,7 @@ public void setTocResource(Resource tocResource) { * The resource containing the XML for the tableOfContents. * When saving an epub file this resource needs to be in this place. * - * @return + * @return The resource containing the XML for the tableOfContents. */ public Resource getTocResource() { return tocResource; @@ -182,6 +181,10 @@ public int getResourceIndex(String resourceHref) { return result; } + /** + * Whether the spine has any references + * @return Whether the spine has any references + */ public boolean isEmpty() { return spineReferences.isEmpty(); } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/SpineReference.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/SpineReference.java index db2804b2..6b545f43 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/SpineReference.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/SpineReference.java @@ -41,9 +41,9 @@ public SpineReference(Resource resource, boolean linear) { * 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.) - * @see http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html#TOC2.4 + * @see OPF Spine specification * - * @return + * @return whether the section is Primary or Auxiliary. */ public boolean isLinear() { return linear; diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/TableOfContents.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/TableOfContents.java index 94d65d15..56cf6d0e 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/TableOfContents.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/TableOfContents.java @@ -50,6 +50,7 @@ public void setTocReferences(List tocReferences) { /** * Calls addTOCReferenceAtLocation after splitting the path using the DEFAULT_PATH_SEPARATOR. + * @return the new TOCReference */ public TOCReference addSection(Resource resource, String path) { return addSection(resource, path, DEFAULT_PATH_SEPARATOR); @@ -61,7 +62,7 @@ public TOCReference addSection(Resource resource, String path) { * @param resource * @param path * @param pathSeparator - * @return + * @return the new TOCReference */ public TOCReference addSection(Resource resource, String path, String pathSeparator) { String[] pathElements = path.split(pathSeparator); @@ -98,7 +99,7 @@ private static TOCReference findTocReferenceByTitle(String title, List getAllUniqueResources() { Set uniqueHrefs = new HashSet(); @@ -218,7 +219,7 @@ private static void getAllUniqueResources(Set uniqueHrefs, List tocReferences) { return result; } + /** + * The maximum depth of the reference tree + * @return The maximum depth of the reference tree + */ public int calculateDepth() { return calculateDepth(tocReferences, 0); } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/domain/TitledResourceReference.java b/epublib-core/src/main/java/nl/siegmann/epublib/domain/TitledResourceReference.java index 173943e9..81cee4b3 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/domain/TitledResourceReference.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/domain/TitledResourceReference.java @@ -48,7 +48,7 @@ public void setTitle(String title) { /** * If the fragmentId is blank it returns the resource href, otherwise it returns the resource href + '#' + the fragmentId. * - * @return + * @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)) { diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubProcessorSupport.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubProcessorSupport.java index 0a4cfc64..70faba6a 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubProcessorSupport.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubProcessorSupport.java @@ -106,7 +106,7 @@ public DocumentBuilderFactory getDocumentBuilderFactory() { /** * Creates a DocumentBuilder that looks up dtd's and schema's from epublib's classpath. * - * @return + * @return a DocumentBuilder that looks up dtd's and schema's from epublib's classpath. */ public static DocumentBuilder createDocumentBuilder() { DocumentBuilder result = null; diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubReader.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubReader.java index 1fcb7cea..3765d53e 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubReader.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubReader.java @@ -1,13 +1,12 @@ package nl.siegmann.epublib.epub; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; +import net.sf.jazzlib.ZipFile; +import net.sf.jazzlib.ZipInputStream; import nl.siegmann.epublib.Constants; import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.MediaType; @@ -40,68 +39,79 @@ public Book readEpub(InputStream in) throws IOException { public Book readEpub(ZipInputStream in) throws IOException { return readEpub(in, Constants.CHARACTER_ENCODING); } - + + public Book 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 + * @return the Book as read from the inputstream * @throws IOException */ public Book readEpub(InputStream in, String encoding) throws IOException { return readEpub(new ZipInputStream(in), encoding); } - /** - * Reads this EPUB without loading all resources into memory. - * - * @param fileName the file to load - * @param encoding the encoding for XHTML files - * @param lazyLoadedTypes a list of the MediaType to load lazily - * @return - * @throws IOException - */ - public Book readEpubLazy( String fileName, String encoding, List lazyLoadedTypes ) throws IOException { - Book result = new Book(); - Resources resources = readLazyResources(fileName, encoding, lazyLoadedTypes); - 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; - } /** * Reads this EPUB without loading any resources into memory. * - * @param fileName the file to load + * @param zipFile the file to load * @param encoding the encoding for XHTML files * - * @return + * @return this Book without loading all resources into memory. * @throws IOException */ - public Book readEpubLazy( String fileName, String encoding ) throws IOException { - return readEpubLazy(fileName, encoding, Arrays.asList(MediatypeService.mediatypes) ); + public Book readEpubLazy(ZipFile zipFile, String encoding ) throws IOException { + return readEpubLazy(zipFile, encoding, Arrays.asList(MediatypeService.mediatypes) ); } public Book readEpub(ZipInputStream in, String encoding) throws IOException { - Book result = new Book(); - Resources resources = readResources(in, encoding); - 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; + return readEpub(ResourcesLoader.loadResources(in, encoding)); } + public Book readEpub(ZipFile in, String encoding) throws IOException { + return readEpub(ResourcesLoader.loadResources(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 + */ + public Book readEpubLazy(ZipFile zipFile, String encoding, List lazyLoadedTypes ) throws IOException { + Resources resources = ResourcesLoader.loadResources(zipFile, encoding, lazyLoadedTypes); + return readEpub(resources); + } + + public Book readEpub(Resources resources) throws IOException{ + return readEpub(resources, new Book()); + } + + public Book readEpub(Resources resources, Book result) throws IOException{ + if (result == null) { + result = new Book(); + } + 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 Book postProcessBook(Book book) { if (bookProcessor != null) { book = bookProcessor.processBook(book); @@ -147,50 +157,4 @@ private String getPackageResourceHref(Resources resources) { private void handleMimeType(Book result, Resources resources) { resources.remove("mimetype"); } - - private Resources readLazyResources( String fileName, String defaultHtmlEncoding, - List lazyLoadedTypes) throws IOException { - - ZipInputStream in = new ZipInputStream(new FileInputStream(fileName)); - - Resources result = new Resources(); - for(ZipEntry zipEntry = in.getNextEntry(); zipEntry != null; zipEntry = in.getNextEntry()) { - if(zipEntry.isDirectory()) { - continue; - } - - String href = zipEntry.getName(); - MediaType mediaType = MediatypeService.determineMediaType(href); - - Resource resource; - - if ( lazyLoadedTypes.contains(mediaType) ) { - resource = new Resource(fileName, zipEntry.getSize(), href); - } else { - resource = new Resource( in, href ); - } - - if(resource.getMediaType() == MediatypeService.XHTML) { - resource.setInputEncoding(defaultHtmlEncoding); - } - result.add(resource); - } - - return result; - } - - private Resources readResources(ZipInputStream in, String defaultHtmlEncoding) throws IOException { - Resources result = new Resources(); - for(ZipEntry zipEntry = in.getNextEntry(); zipEntry != null; zipEntry = in.getNextEntry()) { - if(zipEntry.isDirectory()) { - continue; - } - Resource resource = ResourceUtil.createResource(zipEntry, in); - if(resource.getMediaType() == MediatypeService.XHTML) { - resource.setInputEncoding(defaultHtmlEncoding); - } - result.add(resource); - } - return result; - } } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriter.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriter.java index 3037c9b9..f08d5587 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriter.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriter.java @@ -9,15 +9,15 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmlpull.v1.XmlSerializer; + import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.Resource; import nl.siegmann.epublib.service.MediatypeService; import nl.siegmann.epublib.util.IOUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xmlpull.v1.XmlSerializer; - /** * Generates an epub file. Not thread-safe, single use object. * @@ -164,7 +164,7 @@ String getNcxHref() { } String getNcxMediaType() { - return "application/x-dtbncx+xml"; + return MediatypeService.NCX.getName(); } public BookProcessor getBookProcessor() { diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/NCXDocument.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/NCXDocument.java index af089132..6ec61026 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/NCXDocument.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/NCXDocument.java @@ -21,7 +21,6 @@ import nl.siegmann.epublib.service.MediatypeService; import nl.siegmann.epublib.util.ResourceUtil; import nl.siegmann.epublib.util.StringUtil; -import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -117,9 +116,15 @@ private static List readTOCReferences(NodeList navpoints, Book boo return result; } - private static TOCReference readTOCReference(Element navpointElement, Book book) { + static TOCReference readTOCReference(Element navpointElement, Book book) { String label = readNavLabel(navpointElement); - String reference = FilenameUtils.getPath(book.getSpine().getTocResource().getHref())+readNavReference(navpointElement); + 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); @@ -127,11 +132,10 @@ private static TOCReference readTOCReference(Element navpointElement, Book book) log.error("Resource with href " + href + " in NCX document not found"); } TOCReference result = new TOCReference(label, resource, fragmentId); - readTOCReferences(navpointElement.getChildNodes(), book); - result.setChildren(readTOCReferences(navpointElement.getChildNodes(), book)); + List 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); @@ -161,10 +165,9 @@ public static void write(EpubWriter epubWriter, Book book, ZipOutputStream resul /** * Generates a resource containing an xml document containing the table of contents of the book in ncx format. * - * @param epubWriter - * @param book - * @return - * @ + * @param xmlSerializer the serializer used + * @param book the book to serialize + * * @throws FactoryConfigurationError * @throws IOException * @throws IllegalStateException diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReader.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReader.java index 33dd8d7f..2ce2de0b 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReader.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReader.java @@ -11,7 +11,6 @@ import nl.siegmann.epublib.domain.Date; import nl.siegmann.epublib.domain.Identifier; import nl.siegmann.epublib.domain.Metadata; -import nl.siegmann.epublib.domain.Resources; import nl.siegmann.epublib.util.StringUtil; import org.slf4j.Logger; @@ -34,7 +33,7 @@ class PackageDocumentMetadataReader extends PackageDocumentBase { private static final Logger log = LoggerFactory.getLogger(PackageDocumentMetadataReader.class); - public static Metadata readMetadata(Document packageDocument, Resources resources) { + public static Metadata readMetadata(Document packageDocument) { Metadata result = new Metadata(); Element metadataElement = DOMUtil.getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.metadata); if(metadataElement == null) { @@ -52,7 +51,7 @@ public static Metadata readMetadata(Document packageDocument, Resources resource 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)); @@ -71,7 +70,7 @@ public static Metadata readMetadata(Document packageDocument, Resources resource private static Map readOtherProperties(Element metadataElement) { Map result = new HashMap(); - NodeList metaTags = metadataElement.getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.meta); + 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); @@ -85,6 +84,25 @@ private static Map readOtherProperties(Element metadataElement) { return result; } + /** + * consumes meta tags that have a property attribute as defined in the standard. For example: + * <meta property="rendition:layout">pre-paginated</meta> + * @param metadataElement + * @return + */ + private static Map readMetaProperties(Element metadataElement) { + Map 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); diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataWriter.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataWriter.java index 858eff99..58839fc4 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataWriter.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentMetadataWriter.java @@ -26,7 +26,6 @@ public class PackageDocumentMetadataWriter extends PackageDocumentBase { * @throws IOException * @throws IllegalStateException * @throws IllegalArgumentException - * @ */ public static void writeMetaData(Book book, XmlSerializer serializer) throws IllegalArgumentException, IllegalStateException, IOException { serializer.startTag(NAMESPACE_OPF, OPFTags.metadata); @@ -79,9 +78,10 @@ public static void writeMetaData(Book book, XmlSerializer serializer) throws Ill // write other properties if(book.getMetadata().getOtherProperties() != null) { for(Map.Entry mapEntry: book.getMetadata().getOtherProperties().entrySet()) { - serializer.startTag(mapEntry.getKey().getNamespaceURI(), mapEntry.getKey().getLocalPart()); + 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(), mapEntry.getKey().getLocalPart()); + serializer.endTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta); } } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentReader.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentReader.java index 9c3d64d3..8fa34895 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentReader.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/PackageDocumentReader.java @@ -43,7 +43,7 @@ public class PackageDocumentReader extends PackageDocumentBase { private static final Logger log = LoggerFactory.getLogger(PackageDocumentReader.class); - private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[] {"toc", "ncx"}; + private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[] {"toc", "ncx", "ncxtoc"}; public static void read(Resource packageResource, EpubReader epubReader, Book book, Resources resources) throws UnsupportedEncodingException, SAXException, IOException, ParserConfigurationException { @@ -58,8 +58,8 @@ public static void read(Resource packageResource, EpubReader epubReader, Book bo resources = readManifest(packageDocument, packageHref, epubReader, resources, idMapping); book.setResources(resources); readCover(packageDocument, book); - book.setMetadata(PackageDocumentMetadataReader.readMetadata(packageDocument, book.getResources())); - book.setSpine(readSpine(packageDocument, epubReader, book.getResources(), idMapping)); + 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) { @@ -177,9 +177,9 @@ private static void readGuide(Document packageDocument, * * @param packageHref * @param resourcesByHref - * @return + * @return The stripped package href */ - private static Resources fixHrefs(String packageHref, + static Resources fixHrefs(String packageHref, Resources resourcesByHref) { int lastSlashPos = packageHref.lastIndexOf('/'); if(lastSlashPos < 0) { @@ -188,7 +188,7 @@ private static Resources fixHrefs(String packageHref, Resources result = new Resources(); for(Resource resource: resourcesByHref.getAll()) { if(StringUtil.isNotBlank(resource.getHref()) - || resource.getHref().length() > lastSlashPos) { + && resource.getHref().length() > lastSlashPos) { resource.setHref(resource.getHref().substring(lastSlashPos + 1)); } result.add(resource); @@ -203,9 +203,9 @@ private static Resources fixHrefs(String packageHref, * @param epubReader * @param book * @param resourcesById - * @return + * @return the document's spine, containing all sections in reading order. */ - private static Spine readSpine(Document packageDocument, EpubReader epubReader, Resources resources, Map idMapping) { + private static Spine readSpine(Document packageDocument, Resources resources, Map idMapping) { Element spineElement = DOMUtil.getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.spine); if (spineElement == null) { @@ -213,7 +213,8 @@ private static Spine readSpine(Document packageDocument, EpubReader epubReader, return generateSpineFromResources(resources); } Spine result = new Spine(); - result.setTocResource(findTableOfContentsResource(spineElement, resources)); + String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc); + result.setTocResource(findTableOfContentsResource(tocResourceId, resources)); NodeList spineNodes = packageDocument.getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.itemref); List spineReferences = new ArrayList(spineNodes.getLength()); for(int i = 0; i < spineNodes.getLength(); i++) { @@ -248,7 +249,7 @@ private static Spine readSpine(Document packageDocument, EpubReader epubReader, * The generated spine consists of all XHTML pages in order of their href. * * @param resources - * @return + * @return a spine created out of all resources in the resources. */ private static Spine generateSpineFromResources(Resources resources) { Spine result = new Spine(); @@ -275,10 +276,9 @@ private static Spine generateSpineFromResources(Resources resources) { * * @param spineElement * @param resourcesById - * @return + * @return the Resource containing the table of contents */ - private static Resource findTableOfContentsResource(Element spineElement, Resources resources) { - String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc); + static Resource findTableOfContentsResource(String tocResourceId, Resources resources) { Resource tocResource = null; if (StringUtil.isNotBlank(tocResourceId)) { tocResource = resources.getByIdOrHref(tocResourceId); @@ -288,20 +288,22 @@ private static Resource findTableOfContentsResource(Element spineElement, Resour return tocResource; } - for (int i = 0; i < POSSIBLE_NCX_ITEM_IDS.length; i++) { - tocResource = resources.getByIdOrHref(POSSIBLE_NCX_ITEM_IDS[i]); - if (tocResource != null) { - return tocResource; - } - tocResource = resources.getByIdOrHref(POSSIBLE_NCX_ITEM_IDS[i].toUpperCase()); - if (tocResource != null) { - return tocResource; - } - } - // get the first resource with the NCX mediatype tocResource = resources.findFirstResourceByMediaType(MediatypeService.NCX); + if (tocResource == null) { + for (int i = 0; i < POSSIBLE_NCX_ITEM_IDS.length; i++) { + tocResource = resources.getByIdOrHref(POSSIBLE_NCX_ITEM_IDS[i]); + if (tocResource != null) { + break; + } + tocResource = resources.getByIdOrHref(POSSIBLE_NCX_ITEM_IDS[i].toUpperCase()); + if (tocResource != null) { + break; + } + } + } + if (tocResource == null) { log.error("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."); } @@ -314,7 +316,7 @@ private static Resource findTableOfContentsResource(Element spineElement, Resour * Search the meta tags and the guide references * * @param packageDocument - * @return + * @return all resources that have something to do with the coverpage and the cover image. */ // package static Set findCoverHrefs(Document packageDocument) { @@ -352,7 +354,6 @@ static Set findCoverHrefs(Document packageDocument) { * @param packageDocument * @param book * @param resources - * @return */ private static void readCover(Document packageDocument, Book book) { @@ -372,4 +373,4 @@ private static void readCover(Document packageDocument, Book book) { } -} \ No newline at end of file +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/ResourcesLoader.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/ResourcesLoader.java new file mode 100644 index 00000000..19fb3e15 --- /dev/null +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/ResourcesLoader.java @@ -0,0 +1,154 @@ +package nl.siegmann.epublib.epub; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import net.sf.jazzlib.ZipEntry; +import net.sf.jazzlib.ZipException; +import net.sf.jazzlib.ZipFile; +import net.sf.jazzlib.ZipInputStream; +import nl.siegmann.epublib.domain.LazyResource; +import nl.siegmann.epublib.domain.MediaType; +import nl.siegmann.epublib.domain.Resource; +import nl.siegmann.epublib.domain.Resources; +import nl.siegmann.epublib.service.MediatypeService; +import nl.siegmann.epublib.util.CollectionUtil; +import nl.siegmann.epublib.util.ResourceUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Loads Resources from inputStreams, ZipFiles, etc + * + * @author paul + * + */ +public class ResourcesLoader { + private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); + + /** + * Loads the entries of the zipFile as resources. + * + * The MediaTypes that are in the lazyLoadedTypes will not get their contents loaded, but are stored as references to + * entries into the ZipFile and are loaded on demand by the Resource system. + * + * @param zipFile + * @param defaultHtmlEncoding + * @param lazyLoadedTypes + * @return + * @throws IOException + */ + public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding, + List lazyLoadedTypes) throws IOException { + + Resources result = new Resources(); + Enumeration entries = zipFile.entries(); + + while( entries.hasMoreElements() ) { + ZipEntry zipEntry = entries.nextElement(); + + if(zipEntry == null || zipEntry.isDirectory()) { + continue; + } + + String href = zipEntry.getName(); + + Resource resource; + + if (shouldLoadLazy(href, lazyLoadedTypes)) { + resource = new LazyResource(zipFile.getName(), zipEntry.getSize(), href); + } else { + resource = ResourceUtil.createResource(zipEntry, zipFile.getInputStream(zipEntry)); + } + + if(resource.getMediaType() == MediatypeService.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 + * @param lazilyLoadedMediaTypes + * @return Whether the given href will load a mediaType that is in the collection of lazilyLoadedMediaTypes. + */ + private static boolean shouldLoadLazy(String href, Collection lazilyLoadedMediaTypes) { + if (CollectionUtil.isEmpty(lazilyLoadedMediaTypes)) { + return false; + } + MediaType mediaType = MediatypeService.determineMediaType(href); + return lazilyLoadedMediaTypes.contains(mediaType); + } + + /** + * Loads all entries from the ZipInputStream as Resources. + * + * 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 + * @param defaultHtmlEncoding + * @return + * @throws 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; + } + + // store resource + Resource resource = ResourceUtil.createResource(zipEntry, zipInputStream); + if(resource.getMediaType() == MediatypeService.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 Issue #122 Infinite loop. + //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); + try { zipInputStream.closeEntry(); } catch (Exception ignored) {} + throw e; + } + } + + /** + * Loads all entries from the ZipInputStream as Resources. + * + * 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 + * @param defaultHtmlEncoding + * @return + * @throws IOException + */ + public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding) throws IOException { + return loadResources(zipFile, defaultHtmlEncoding, Collections.emptyList()); + } + +} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/service/MediatypeService.java b/epublib-core/src/main/java/nl/siegmann/epublib/service/MediatypeService.java index d9074082..be689b6f 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/service/MediatypeService.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/service/MediatypeService.java @@ -36,14 +36,17 @@ public class MediatypeService { // audio public static final MediaType MP3 = new MediaType("audio/mpeg", ".mp3"); - public static final MediaType MP4 = new MediaType("audio/mp4", ".mp4"); + 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 MediaType[] mediatypes = new MediaType[] { - XHTML, EPUB, JPG, PNG, GIF, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF, SMIL, PLS, JAVASCRIPT, MP3, MP4 + XHTML, EPUB, JPG, PNG, GIF, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF, SMIL, PLS, JAVASCRIPT, MP3, MP4, OGG }; public static Map mediaTypesByName = new HashMap(); @@ -62,14 +65,13 @@ public static boolean isBitmapImage(MediaType mediaType) { * Null of no matching extension found. * * @param filename - * @return + * @return the MediaType based on the file extension. */ public static MediaType determineMediaType(String filename) { - for(int i = 0; i < mediatypes.length; i++) { - MediaType mediatype = mediatypes[i]; - for(String extension: mediatype.getExtensions()) { + for (MediaType mediaType: mediaTypesByName.values()) { + for(String extension: mediaType.getExtensions()) { if(StringUtil.endsWithIgnoreCase(filename, extension)) { - return mediatype; + return mediaType; } } } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/CollectionUtil.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/CollectionUtil.java index 7c33206e..f780cb68 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/CollectionUtil.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/CollectionUtil.java @@ -1,5 +1,6 @@ package nl.siegmann.epublib.util; +import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.List; @@ -34,7 +35,7 @@ public T nextElement() { * Creates an Enumeration out of the given Iterator. * @param * @param it - * @return + * @return an Enumeration created out of the given Iterator. */ public static Enumeration createEnumerationFromIterator(Iterator it) { return new IteratorEnumerationAdapter(it); @@ -46,7 +47,7 @@ public static Enumeration createEnumerationFromIterator(Iterator it) { * * @param * @param list - * @return + * @return the first element of the list, null if the list is null or empty. */ public static T first(List list) { if(list == null || list.isEmpty()) { @@ -54,4 +55,14 @@ public static T first(List list) { } return list.get(0); } + + /** + * Whether the given collection is null or has no elements. + * + * @param collection + * @return Whether the given collection is null or has no elements. + */ + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } } diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/DomUtil.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/DomUtil.java deleted file mode 100644 index 4ca8dc37..00000000 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/DomUtil.java +++ /dev/null @@ -1,9 +0,0 @@ -package nl.siegmann.epublib.util; - -import org.w3c.dom.Node; - -public class DomUtil { - - public static void getFirstChildWithTagname(String tagname, Node parentNode) { - } -} diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/IOUtil.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/IOUtil.java index 163b720c..4d2dd804 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/IOUtil.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/IOUtil.java @@ -1,12 +1,6 @@ package nl.siegmann.epublib.util; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.StringWriter; -import java.io.Writer; +import java.io.*; /** * Most of the functions herein are re-implementations of the ones in apache io IOUtils. @@ -21,7 +15,7 @@ public class IOUtil { * * @param in * @param encoding - * @return + * @return the contents of the Reader as a byte[], with the given character encoding. * @throws IOException */ public static byte[] toByteArray(Reader in, String encoding) throws IOException { @@ -35,7 +29,7 @@ public static byte[] toByteArray(Reader in, String encoding) throws IOException * Returns the contents of the InputStream as a byte[] * * @param in - * @return + * @return the contents of the InputStream as a byte[] * @throws IOException */ public static byte[] toByteArray(InputStream in) throws IOException { @@ -45,11 +39,44 @@ public static byte[] toByteArray(InputStream in) throws IOException { return result.toByteArray(); } - /** + /** + * Reads data from the InputStream, using the specified buffer size. + * + * 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 + */ + 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 < 0 then totalNrRead is returned, if (nrRead + totalNrRead) < Integer.MAX_VALUE then nrRead + totalNrRead is returned, -1 otherwise. * @param nrRead * @param totalNrNread - * @return + * @return if totalNrRead < 0 then totalNrRead is returned, if (nrRead + totalNrRead) < Integer.MAX_VALUE then nrRead + totalNrRead is returned, -1 otherwise. */ protected static int calcNewNrReadSize(int nrRead, int totalNrNread) { if (totalNrNread < 0) { @@ -62,12 +89,12 @@ protected static int calcNewNrReadSize(int nrRead, int totalNrNread) { } } - /** + /** * Copies the contents of the InputStream to the OutputStream. * * @param in * @param out - * @return the nr of bytes read, or -1 if the amount > Integer.MAX_VALUE + * @return the nr of bytes read, or -1 if the amount > Integer.MAX_VALUE * @throws IOException */ public static int copy(InputStream in, OutputStream out) @@ -88,7 +115,7 @@ public static int copy(InputStream in, OutputStream out) * * @param in * @param out - * @return the nr of characters read, or -1 if the amount > Integer.MAX_VALUE + * @return the nr of characters read, or -1 if the amount > Integer.MAX_VALUE * @throws IOException */ public static int copy(Reader in, Writer out) throws IOException { diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/NoCloseOutputStream.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/NoCloseOutputStream.java index ddf21261..4161eba1 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/NoCloseOutputStream.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/NoCloseOutputStream.java @@ -11,7 +11,8 @@ * * @author paul * - */public class NoCloseOutputStream extends OutputStream { + */ +public class NoCloseOutputStream extends OutputStream { private OutputStream outputStream; diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/ResourceUtil.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/ResourceUtil.java index 36ea1a04..066f33e4 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/ResourceUtil.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/ResourceUtil.java @@ -3,14 +3,15 @@ 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.ZipEntry; -import java.util.zip.ZipInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; +import net.sf.jazzlib.ZipEntry; +import net.sf.jazzlib.ZipInputStream; import nl.siegmann.epublib.Constants; import nl.siegmann.epublib.domain.MediaType; import nl.siegmann.epublib.domain.Resource; @@ -45,7 +46,7 @@ public static Resource createResource(File file) throws IOException { * * @param title * @param href - * @return + * @return a resource with as contents a html page with the given title. */ public static Resource createResource(String title, String href) { String content = "" + title + "

    " + title + "

    "; @@ -57,14 +58,18 @@ public static Resource createResource(String title, String href) { * * @param zipEntry * @param zipInputStream - * @return + * @return a resource created out of the given zipEntry and zipInputStream. * @throws IOException */ public static Resource createResource(ZipEntry zipEntry, ZipInputStream zipInputStream) throws IOException { return new Resource(zipInputStream, zipEntry.getName()); } - + + public static Resource createResource(ZipEntry zipEntry, InputStream zipInputStream) throws IOException { + return new Resource(zipInputStream, zipEntry.getName()); + + } /** * Converts a given string from given input character encoding to the requested output character encoding. @@ -72,7 +77,7 @@ public static Resource createResource(ZipEntry zipEntry, ZipInputStream zipInput * @param inputEncoding * @param outputEncoding * @param input - * @return + * @return the string from given input character encoding converted to the requested output character encoding. * @throws UnsupportedEncodingException */ public static byte[] recode(String inputEncoding, String outputEncoding, byte[] input) throws UnsupportedEncodingException { @@ -108,8 +113,8 @@ public static Document getAsDocument(Resource resource) throws UnsupportedEncodi * Reads the given resources inputstream, parses the xml therein and returns the result as a Document * * @param resource - * @param documentBuilderFactory - * @return + * @param documentBuilder + * @return the document created from the given resource * @throws UnsupportedEncodingException * @throws SAXException * @throws IOException diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/StringUtil.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/StringUtil.java index eadae95a..0f60b923 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/StringUtil.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/StringUtil.java @@ -7,55 +7,96 @@ /** * Various String utility functions. * - * 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. + * 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 { /** - * Whether the String is not null, not zero-length and does not contain of only whitespace. + * 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 + * @return the normalized path + */ + public static String collapsePathDots(String path) { + String[] stringParts = path.split("/"); + List 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 - * @return + * @return Whether the String is not null, not zero-length and does not contain of */ public static boolean isNotBlank(String text) { - return ! isBlank(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))) { + if (!Character.isWhitespace(text.charAt(i))) { return false; } } return true; } - + /** * Whether the given string is null or zero-length. * - * @param text - * @return + * @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. + * Whether the given source string ends with the given suffix, ignoring + * case. * * @param source * @param suffix - * @return + * @return Whether the given source string ends with the given suffix, ignoring case. */ public static boolean endsWithIgnoreCase(String source, String suffix) { if (isEmpty(suffix)) { @@ -67,14 +108,15 @@ public static boolean endsWithIgnoreCase(String source, String suffix) { if (suffix.length() > source.length()) { return false; } - return source.substring(source.length() - suffix.length()).toLowerCase().endsWith(suffix.toLowerCase()); + return source.substring(source.length() - suffix.length()) + .toLowerCase().endsWith(suffix.toLowerCase()); } - - /** + + /** * If the given text is null return "", the original text otherwise. * * @param text - * @return + * @return If the given text is null "", the original text otherwise. */ public static String defaultIfNull(String text) { return defaultIfNull(text, ""); @@ -85,7 +127,7 @@ public static String defaultIfNull(String text) { * * @param text * @param defaultValue - * @return + * @return If the given text is null "", the given defaultValue otherwise. */ public static String defaultIfNull(String text, String defaultValue) { if (text == null) { @@ -99,7 +141,7 @@ public static String defaultIfNull(String text, String defaultValue) { * * @param text1 * @param text2 - * @return + * @return whether the two strings are equal */ public static boolean equals(String text1, String text2) { if (text1 == null) { @@ -112,9 +154,9 @@ public static boolean equals(String text1, String text2) { * Pretty toString printer. * * @param keyValues - * @return + * @return a string representation of the input values */ - public static String toString(Object ... keyValues) { + public static String toString(Object... keyValues) { StringBuilder result = new StringBuilder(); result.append('['); for (int i = 0; i < keyValues.length; i += 2) { @@ -139,7 +181,7 @@ public static String toString(Object ... keyValues) { return result.toString(); } - public static int hashCode(String ... values) { + public static int hashCode(String... values) { int result = 31; for (int i = 0; i < values.length; i++) { result ^= String.valueOf(values[i]).hashCode(); @@ -150,11 +192,12 @@ public static int hashCode(String ... values) { /** * Gives the substring of the given text before the given separator. * - * If the text does not contain the given separator then the given text is returned. + * If the text does not contain the given separator then the given text is + * returned. * * @param text * @param separator - * @return + * @return the substring of the given text before the given separator. */ public static String substringBefore(String text, char separator) { if (isEmpty(text)) { @@ -168,13 +211,15 @@ public static String substringBefore(String text, char separator) { } /** - * Gives the substring of the given text before the last occurrence of the given separator. + * Gives the substring of the given text before the last occurrence of the + * given separator. * - * If the text does not contain the given separator then the given text is returned. + * If the text does not contain the given separator then the given text is + * returned. * * @param text * @param separator - * @return + * @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)) { @@ -188,13 +233,14 @@ public static String substringBeforeLast(String text, char separator) { } /** - * Gives the substring of the given text after the last occurrence of the given separator. + * Gives the substring of the given text after the last occurrence of the + * given separator. * * If the text does not contain the given separator then "" is returned. * * @param text * @param separator - * @return + * @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)) { @@ -212,9 +258,9 @@ public static String substringAfterLast(String text, char separator) { * * If the text does not contain the given separator then "" is returned. * - * @param text - * @param separator - * @return + * @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)) { diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/BOMInputStream.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/BOMInputStream.java index 4680d3e6..367a9127 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/BOMInputStream.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/BOMInputStream.java @@ -65,7 +65,7 @@ * } * * - * @see org.apache.commons.io.ByteOrderMark + * @see ByteOrderMark * @see Wikipedia - Byte Order Mark * @version $Revision: 1052095 $ $Date: 2010-12-22 23:03:20 +0000 (Wed, 22 Dec 2010) $ * @since Commons IO 2.0 diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/ByteOrderMark.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/ByteOrderMark.java index ce7235f6..55ceeea8 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/ByteOrderMark.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/ByteOrderMark.java @@ -21,9 +21,9 @@ /** * Byte Order Mark (BOM) representation - - * see {@link org.apache.commons.io.input.BOMInputStream}. + * see {@link BOMInputStream}. * - * @see org.apache.commons.io.input.BOMInputStream + * @see BOMInputStream * @see Wikipedia - Byte Order Mark * @version $Id: ByteOrderMark.java 1005099 2010-10-06 16:13:01Z niallp $ * @since Commons IO 2.0 diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/XmlStreamReader.java b/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/XmlStreamReader.java index 037e4582..1a5f18c9 100644 --- a/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/XmlStreamReader.java +++ b/epublib-core/src/main/java/nl/siegmann/epublib/util/commons/io/XmlStreamReader.java @@ -689,7 +689,7 @@ private static String getXmlProlog(InputStream is, String guessedEnc) is.reset(); BufferedReader bReader = new BufferedReader(new StringReader( xmlProlog.substring(0, firstGT + 1))); - StringBuffer prolog = new StringBuffer(); + StringBuilder prolog = new StringBuilder(); String line = bReader.readLine(); while (line != null) { prolog.append(line); diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/browsersupport/NavigationHistoryTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/browsersupport/NavigationHistoryTest.java index d0b0906b..f0c75a7a 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/browsersupport/NavigationHistoryTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/browsersupport/NavigationHistoryTest.java @@ -1,15 +1,16 @@ package nl.siegmann.epublib.browsersupport; +import static org.junit.Assert.assertEquals; + import java.util.HashMap; import java.util.Map; -import junit.framework.TestCase; - - import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.Resource; -public class NavigationHistoryTest extends TestCase { +import org.junit.Test; + +public class NavigationHistoryTest { private static final Resource mockResource = new Resource("mockResource.html"); @@ -85,7 +86,8 @@ public Resource getMockResource() { return mockResource; } } - + + @Test public void test1() { MockSectionWalker navigator = new MockSectionWalker(new MockBook()); NavigationHistory browserHistory = new NavigationHistory(navigator); @@ -135,7 +137,7 @@ public void test1() { assertEquals(2, browserHistory.getCurrentSize()); } - + @Test public void test2() { MockSectionWalker navigator = new MockSectionWalker(new MockBook()); NavigationHistory browserHistory = new NavigationHistory(navigator); @@ -173,6 +175,7 @@ public void test2() { } + @Test public void test3() { MockSectionWalker navigator = new MockSectionWalker(new MockBook()); NavigationHistory browserHistory = new NavigationHistory(navigator); diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/domain/BookTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/domain/BookTest.java index cf032df0..6ddf8684 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/domain/BookTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/domain/BookTest.java @@ -1,27 +1,32 @@ package nl.siegmann.epublib.domain; import nl.siegmann.epublib.service.MediatypeService; -import junit.framework.TestCase; -public class BookTest extends TestCase { +import org.junit.Assert; +import org.junit.Test; +public class BookTest { + + @Test public void testGetContents1() { Book book = new Book(); Resource resource1 = new Resource("id1", "Hello, world !".getBytes(), "chapter1.html", MediatypeService.XHTML); book.getSpine().addResource(resource1); book.getTableOfContents().addSection(resource1, "My first chapter"); - assertEquals(1, book.getContents().size()); + Assert.assertEquals(1, book.getContents().size()); } + @Test public void testGetContents2() { Book book = new Book(); Resource resource1 = new Resource("id1", "Hello, world !".getBytes(), "chapter1.html", MediatypeService.XHTML); book.getSpine().addResource(resource1); Resource resource2 = new Resource("id1", "Hello, world !".getBytes(), "chapter2.html", MediatypeService.XHTML); book.getTableOfContents().addSection(resource2, "My first chapter"); - assertEquals(2, book.getContents().size()); + Assert.assertEquals(2, book.getContents().size()); } + @Test public void testGetContents3() { Book book = new Book(); Resource resource1 = new Resource("id1", "Hello, world !".getBytes(), "chapter1.html", MediatypeService.XHTML); @@ -29,9 +34,10 @@ public void testGetContents3() { Resource resource2 = new Resource("id1", "Hello, world !".getBytes(), "chapter2.html", MediatypeService.XHTML); book.getTableOfContents().addSection(resource2, "My first chapter"); book.getGuide().addReference(new GuideReference(resource2, GuideReference.FOREWORD, "The Foreword")); - assertEquals(2, book.getContents().size()); + Assert.assertEquals(2, book.getContents().size()); } + @Test public void testGetContents4() { Book book = new Book(); @@ -44,6 +50,6 @@ public void testGetContents4() { Resource resource3 = new Resource("id1", "Hello, world !".getBytes(), "foreword.html", MediatypeService.XHTML); book.getGuide().addReference(new GuideReference(resource3, GuideReference.FOREWORD, "The Foreword")); - assertEquals(3, book.getContents().size()); + Assert.assertEquals(3, book.getContents().size()); } } diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/domain/ResourcesTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/domain/ResourcesTest.java index 2d1b5f12..ea852644 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/domain/ResourcesTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/domain/ResourcesTest.java @@ -1,28 +1,32 @@ package nl.siegmann.epublib.domain; -import junit.framework.TestCase; import nl.siegmann.epublib.service.MediatypeService; -public class ResourcesTest extends TestCase { +import org.junit.Assert; +import org.junit.Test; + +public class ResourcesTest { + @Test public void testGetResourcesByMediaType1() { Resources resources = new Resources(); resources.add(new Resource("foo".getBytes(), MediatypeService.XHTML)); resources.add(new Resource("bar".getBytes(), MediatypeService.XHTML)); - assertEquals(0, resources.getResourcesByMediaType(MediatypeService.PNG).size()); - assertEquals(2, resources.getResourcesByMediaType(MediatypeService.XHTML).size()); - assertEquals(2, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.XHTML}).size()); + Assert.assertEquals(0, resources.getResourcesByMediaType(MediatypeService.PNG).size()); + Assert.assertEquals(2, resources.getResourcesByMediaType(MediatypeService.XHTML).size()); + Assert.assertEquals(2, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.XHTML}).size()); } + @Test public void testGetResourcesByMediaType2() { Resources resources = new Resources(); resources.add(new Resource("foo".getBytes(), MediatypeService.XHTML)); resources.add(new Resource("bar".getBytes(), MediatypeService.PNG)); resources.add(new Resource("baz".getBytes(), MediatypeService.PNG)); - assertEquals(2, resources.getResourcesByMediaType(MediatypeService.PNG).size()); - assertEquals(1, resources.getResourcesByMediaType(MediatypeService.XHTML).size()); - assertEquals(1, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.XHTML}).size()); - assertEquals(3, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.XHTML, MediatypeService.PNG}).size()); - assertEquals(3, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.CSS, MediatypeService.XHTML, MediatypeService.PNG}).size()); + Assert.assertEquals(2, resources.getResourcesByMediaType(MediatypeService.PNG).size()); + Assert.assertEquals(1, resources.getResourcesByMediaType(MediatypeService.XHTML).size()); + Assert.assertEquals(1, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.XHTML}).size()); + Assert.assertEquals(3, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.XHTML, MediatypeService.PNG}).size()); + Assert.assertEquals(3, resources.getResourcesByMediaTypes(new MediaType[] {MediatypeService.CSS, MediatypeService.XHTML, MediatypeService.PNG}).size()); } } diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/domain/TableOfContentsTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/domain/TableOfContentsTest.java index 064072b7..9b058d2d 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/domain/TableOfContentsTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/domain/TableOfContentsTest.java @@ -1,20 +1,26 @@ package nl.siegmann.epublib.domain; -import junit.framework.TestCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; -public class TableOfContentsTest extends TestCase { +import org.junit.Test; + +public class TableOfContentsTest{ + @Test public void testCalculateDepth_simple1() { TableOfContents tableOfContents = new TableOfContents(); assertEquals(0, tableOfContents.calculateDepth()); } + @Test public void testCalculateDepth_simple2() { TableOfContents tableOfContents = new TableOfContents(); tableOfContents.addTOCReference(new TOCReference()); assertEquals(1, tableOfContents.calculateDepth()); } + @Test public void testCalculateDepth_simple3() { TableOfContents tableOfContents = new TableOfContents(); tableOfContents.addTOCReference(new TOCReference()); @@ -25,6 +31,7 @@ public void testCalculateDepth_simple3() { assertEquals(2, tableOfContents.calculateDepth()); } + @Test public void testAddResource1() { Resource resource = new Resource("foo"); TableOfContents toc = new TableOfContents(); @@ -35,6 +42,7 @@ public void testAddResource1() { assertEquals("pear", tocReference.getTitle()); } + @Test public void testAddResource2() { Resource resource = new Resource("foo"); TableOfContents toc = new TableOfContents(); @@ -57,6 +65,7 @@ public void testAddResource2() { assertEquals("apple", tocReference3.getTitle()); } + @Test public void testAddResource3() { Resource resource = new Resource("foo"); TableOfContents toc = new TableOfContents(); @@ -69,6 +78,7 @@ public void testAddResource3() { assertEquals("pear", tocReference.getTitle()); } + @Test public void testAddResourceWithIndexes() { Resource resource = new Resource("foo"); TableOfContents toc = new TableOfContents(); diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubReaderTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubReaderTest.java index 16df2fa9..b3ea9691 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubReaderTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubReaderTest.java @@ -1,78 +1,76 @@ package nl.siegmann.epublib.epub; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Test; -import junit.framework.TestCase; import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.Resource; import nl.siegmann.epublib.service.MediatypeService; -public class EpubReaderTest extends TestCase { - - public void testCover_only_cover() { - try { - Book book = new Book(); - - book.setCoverImage(new Resource(this.getClass().getResourceAsStream("/book1/cover.png"), "cover.png")); +public class EpubReaderTest { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - (new EpubWriter()).write(book, out); - byte[] epubData = out.toByteArray(); - Book readBook = new EpubReader().readEpub(new ByteArrayInputStream(epubData)); - assertNotNull(readBook.getCoverImage()); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - assertTrue(false); - } + @Test + public void testCover_only_cover() throws IOException { + Book book = new Book(); + book.setCoverImage(new Resource(this.getClass().getResourceAsStream( + "/book1/cover.png"), "cover.png")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + (new EpubWriter()).write(book, out); + byte[] epubData = out.toByteArray(); + Book readBook = new EpubReader().readEpub(new ByteArrayInputStream( + epubData)); + assertNotNull(readBook.getCoverImage()); } - public void testCover_cover_one_section() { - try { - Book book = new Book(); - - book.setCoverImage(new Resource(this.getClass().getResourceAsStream("/book1/cover.png"), "cover.png")); - book.addSection("Introduction", new Resource(this.getClass().getResourceAsStream("/book1/chapter1.html"), "chapter1.html")); - book.generateSpineFromTableOfContents(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - (new EpubWriter()).write(book, out); - byte[] epubData = out.toByteArray(); - Book readBook = new EpubReader().readEpub(new ByteArrayInputStream(epubData)); - assertNotNull(readBook.getCoverPage()); - assertEquals(1, readBook.getSpine().size()); - assertEquals(1, readBook.getTableOfContents().size()); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - assertTrue(false); - } + @Test + public void testCover_cover_one_section() throws IOException { + Book book = new Book(); + + book.setCoverImage(new Resource(this.getClass().getResourceAsStream( + "/book1/cover.png"), "cover.png")); + book.addSection("Introduction", new Resource(this.getClass() + .getResourceAsStream("/book1/chapter1.html"), "chapter1.html")); + book.generateSpineFromTableOfContents(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + (new EpubWriter()).write(book, out); + byte[] epubData = out.toByteArray(); + Book readBook = new EpubReader().readEpub(new ByteArrayInputStream( + epubData)); + assertNotNull(readBook.getCoverPage()); + assertEquals(1, readBook.getSpine().size()); + assertEquals(1, readBook.getTableOfContents().size()); } - public void testReadEpub_opf_ncx_docs() { - try { - Book book = new Book(); - - book.setCoverImage(new Resource(this.getClass().getResourceAsStream("/book1/cover.png"), "cover.png")); - book.addSection("Introduction", new Resource(this.getClass().getResourceAsStream("/book1/chapter1.html"), "chapter1.html")); - book.generateSpineFromTableOfContents(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - (new EpubWriter()).write(book, out); - byte[] epubData = out.toByteArray(); - Book readBook = new EpubReader().readEpub(new ByteArrayInputStream(epubData)); - assertNotNull(readBook.getCoverPage()); - assertEquals(1, readBook.getSpine().size()); - assertEquals(1, readBook.getTableOfContents().size()); - assertNotNull(readBook.getOpfResource()); - assertNotNull(readBook.getNcxResource()); - assertEquals(MediatypeService.NCX, readBook.getNcxResource().getMediaType()); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - assertTrue(false); - } + @Test + public void testReadEpub_opf_ncx_docs() throws IOException { + Book book = new Book(); + + book.setCoverImage(new Resource(this.getClass().getResourceAsStream( + "/book1/cover.png"), "cover.png")); + book.addSection("Introduction", new Resource(this.getClass() + .getResourceAsStream("/book1/chapter1.html"), "chapter1.html")); + book.generateSpineFromTableOfContents(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + (new EpubWriter()).write(book, out); + byte[] epubData = out.toByteArray(); + Book readBook = new EpubReader().readEpub(new ByteArrayInputStream( + epubData)); + assertNotNull(readBook.getCoverPage()); + assertEquals(1, readBook.getSpine().size()); + assertEquals(1, readBook.getTableOfContents().size()); + assertNotNull(readBook.getOpfResource()); + assertNotNull(readBook.getNcxResource()); + assertEquals(MediatypeService.NCX, readBook.getNcxResource() + .getMediaType()); } } diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterTest.java index fb3c0289..1951031d 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterTest.java @@ -1,12 +1,17 @@ package nl.siegmann.epublib.epub; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import junit.framework.TestCase; import nl.siegmann.epublib.domain.Author; import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.GuideReference; @@ -15,62 +20,59 @@ import nl.siegmann.epublib.domain.TOCReference; import nl.siegmann.epublib.util.CollectionUtil; -public class EpubWriterTest extends TestCase { +import org.junit.Assert; +import org.junit.Test; - public void testBook1() { - try { - // create test book - Book book = createTestBook(); - - // write book to byte[] - byte[] bookData = writeBookToByteArray(book); -// FileOutputStream fileOutputStream = new FileOutputStream("foo.zip"); -// fileOutputStream.write(bookData); -// fileOutputStream.flush(); -// fileOutputStream.close(); - assertNotNull(bookData); - assertTrue(bookData.length > 0); - - // read book from byte[] - Book readBook = new EpubReader().readEpub(new ByteArrayInputStream(bookData)); - - // assert book values are correct - assertEquals(book.getMetadata().getTitles(), readBook.getMetadata().getTitles()); - assertEquals(Identifier.Scheme.ISBN, CollectionUtil.first(readBook.getMetadata().getIdentifiers()).getScheme()); - assertEquals(CollectionUtil.first(book.getMetadata().getIdentifiers()).getValue(), CollectionUtil.first(readBook.getMetadata().getIdentifiers()).getValue()); - assertEquals(CollectionUtil.first(book.getMetadata().getAuthors()), CollectionUtil.first(readBook.getMetadata().getAuthors())); - assertEquals(1, readBook.getGuide().getGuideReferencesByType(GuideReference.COVER).size()); - assertEquals(5, readBook.getSpine().size()); - assertNotNull(book.getCoverPage()); - assertNotNull(book.getCoverImage()); - assertEquals(4, readBook.getTableOfContents().size()); +public class EpubWriterTest { + + @Test + public void testBook1() throws IOException { + // create test book + Book book = createTestBook(); + + // write book to byte[] + byte[] bookData = writeBookToByteArray(book); + FileOutputStream fileOutputStream = new FileOutputStream("foo.zip"); + fileOutputStream.write(bookData); + fileOutputStream.flush(); + fileOutputStream.close(); + Assert.assertNotNull(bookData); + Assert.assertTrue(bookData.length > 0); + + // read book from byte[] + Book readBook = new EpubReader().readEpub(new ByteArrayInputStream(bookData)); + + // assert book values are correct + Assert.assertEquals(book.getMetadata().getTitles(), readBook.getMetadata().getTitles()); + Assert.assertEquals(Identifier.Scheme.ISBN, CollectionUtil.first(readBook.getMetadata().getIdentifiers()).getScheme()); + Assert.assertEquals(CollectionUtil.first(book.getMetadata().getIdentifiers()).getValue(), CollectionUtil.first(readBook.getMetadata().getIdentifiers()).getValue()); + Assert.assertEquals(CollectionUtil.first(book.getMetadata().getAuthors()), CollectionUtil.first(readBook.getMetadata().getAuthors())); + Assert.assertEquals(1, readBook.getGuide().getGuideReferencesByType(GuideReference.COVER).size()); + Assert.assertEquals(5, readBook.getSpine().size()); + Assert.assertNotNull(book.getCoverPage()); + Assert.assertNotNull(book.getCoverImage()); + Assert.assertEquals(4, readBook.getTableOfContents().size()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } } /** * Test for a very old bug where epublib would throw a NullPointerException when writing a book with a cover that has no id. + * @throws IOException + * @throws FileNotFoundException * */ - public void testWritingBookWithCoverWithNullId() { - try { - Book book = new Book(); - book.getMetadata().addTitle("Epub test book 1"); - book.getMetadata().addAuthor(new Author("Joe", "Tester")); - InputStream is = this.getClass().getResourceAsStream("/book1/cover.png"); - book.setCoverImage(new Resource(is, "cover.png")); - // Add Chapter 1 - InputStream is1 = this.getClass().getResourceAsStream("/book1/chapter1.html"); - book.addSection("Introduction", new Resource(is1, "chapter1.html")); - - EpubWriter epubWriter = new EpubWriter(); - epubWriter.write(book, new FileOutputStream("test1_book1.epub")); - } catch (IOException e) { - fail(e.getMessage()); - } + public void testWritingBookWithCoverWithNullId() throws FileNotFoundException, IOException { + Book book = new Book(); + book.getMetadata().addTitle("Epub test book 1"); + book.getMetadata().addAuthor(new Author("Joe", "Tester")); + InputStream is = this.getClass().getResourceAsStream("/book1/cover.png"); + book.setCoverImage(new Resource(is, "cover.png")); + // Add Chapter 1 + InputStream is1 = this.getClass().getResourceAsStream("/book1/chapter1.html"); + book.addSection("Introduction", new Resource(is1, "chapter1.html")); + + EpubWriter epubWriter = new EpubWriter(); + epubWriter.write(book, new FileOutputStream("test1_book1.epub")); } private Book createTestBook() throws IOException { diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/NCXDocumentTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/NCXDocumentTest.java index e8905ff8..a64f9c2c 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/epub/NCXDocumentTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/NCXDocumentTest.java @@ -1,20 +1,21 @@ package nl.siegmann.epublib.epub; +import static org.junit.Assert.assertEquals; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; + import nl.siegmann.epublib.domain.Book; -import nl.siegmann.epublib.domain.GuideReference; import nl.siegmann.epublib.domain.Resource; -import nl.siegmann.epublib.domain.TOCReference; import nl.siegmann.epublib.service.MediatypeService; -import org.apache.commons.io.FileUtils; +import nl.siegmann.epublib.util.IOUtil; + import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import static org.junit.Assert.*; public class NCXDocumentTest { @@ -33,13 +34,19 @@ public static void tearDownClass() { @Before public void setUp() throws IOException { - ncxData = FileUtils.readFileToByteArray(new File("src/test/resources/toc.xml")); + ncxData = IOUtil.toByteArray(new FileInputStream(new File("src/test/resources/toc.xml"))); } @After public void tearDown() { } + private void addResource(Book book, String filename) { + Resource chapterResource = new Resource("id1", "Hello, world !".getBytes(), filename, MediatypeService.XHTML); + book.addResource(chapterResource); + book.getSpine().addResource(chapterResource); + } + /** * Test of read method, of class NCXDocument. */ @@ -49,9 +56,10 @@ public void testReadWithNonRootLevelTOC() { // If the tox.ncx file is not in the root, the hrefs it refers to need to preserve its path. Book book = new Book(); Resource ncxResource = new Resource(ncxData, "xhtml/toc.ncx"); - Resource chapterResource = new Resource("id1", "Hello, world !".getBytes(), "xhtml/chapter1.html", MediatypeService.XHTML); - book.addResource(chapterResource); - book.getSpine().addResource(chapterResource); + addResource(book, "xhtml/chapter1.html"); + addResource(book, "xhtml/chapter2.html"); + addResource(book, "xhtml/chapter2_1.html"); + addResource(book, "xhtml/chapter3.html"); book.setNcxResource(ncxResource); book.getSpine().setTocResource(ncxResource); diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReaderTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReaderTest.java index a2e7d872..d6603e53 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReaderTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentMetadataReaderTest.java @@ -1,18 +1,27 @@ package nl.siegmann.epublib.epub; -import junit.framework.TestCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.StringReader; + +import nl.siegmann.epublib.domain.Identifier; import nl.siegmann.epublib.domain.Metadata; -import nl.siegmann.epublib.domain.Resources; +import org.junit.Assert; +import org.junit.Test; import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; -public class PackageDocumentMetadataReaderTest extends TestCase { +public class PackageDocumentMetadataReaderTest { + @Test public void test1() { try { Document document = EpubProcessorSupport.createDocumentBuilder().parse(PackageDocumentMetadataReader.class.getResourceAsStream("/opf/test2.opf")); - Resources resources = new Resources(); - Metadata metadata = PackageDocumentMetadataReader.readMetadata(document, resources); + Metadata metadata = PackageDocumentMetadataReader.readMetadata(document); assertEquals(1, metadata.getAuthors().size()); } catch (Exception e) { e.printStackTrace(); @@ -20,11 +29,13 @@ public void test1() { } } + @Test public void testReadsLanguage() { Metadata metadata = getMetadata("/opf/test_language.opf"); assertEquals("fi", metadata.getLanguage()); } + @Test public void testDefaultsToEnglish() { Metadata metadata = getMetadata("/opf/test_default_language.opf"); assertEquals("en", metadata.getLanguage()); @@ -33,9 +44,8 @@ public void testDefaultsToEnglish() { private Metadata getMetadata(String file) { try { Document document = EpubProcessorSupport.createDocumentBuilder().parse(PackageDocumentMetadataReader.class.getResourceAsStream(file)); - Resources resources = new Resources(); - return PackageDocumentMetadataReader.readMetadata(document, resources); + return PackageDocumentMetadataReader.readMetadata(document); } catch (Exception e) { e.printStackTrace(); assertTrue(false); @@ -43,4 +53,47 @@ private Metadata getMetadata(String file) { return null; } } + + @Test + public void test2() throws SAXException, IOException { + // given + String input = "" + + "" + + "Three Men in a Boat" + + "Jerome K. Jerome" + + "A. Frederics" + + "en" + + "1889" + + "2009-05-17" + + "zelda@mobileread.com:2010040720" + + "zelda pinwheel" + + "zelda pinwheel" + + "Public Domain" + + "Text" + + "Image" + + "Travel" + + "Humour" + + "Three Men in a Boat (To Say Nothing of the Dog), published in 1889, is a humorous account by Jerome K. Jerome of a boating holiday on the Thames between Kingston and Oxford. The book was initially intended to be a serious travel guide, with accounts of local history along the route, but the humorous elements took over to the point where the serious and somewhat sentimental passages seem a distraction to the comic novel. One of the most praised things about Three Men in a Boat is how undated it appears to modern readers, the jokes seem fresh and witty even today." + + "" + + "" + + "" + + ""; + + // when + Document metadataDocument = EpubProcessorSupport.createDocumentBuilder().parse(new InputSource(new StringReader(input))); + Metadata metadata = PackageDocumentMetadataReader.readMetadata(metadataDocument); + + // then + Assert.assertEquals("Three Men in a Boat", metadata.getFirstTitle()); + + // test identifier + Assert.assertNotNull(metadata.getIdentifiers()); + Assert.assertEquals(1, metadata.getIdentifiers().size()); + Identifier identifier = metadata.getIdentifiers().get(0); + Assert.assertEquals("URI", identifier.getScheme()); + Assert.assertEquals("zelda@mobileread.com:2010040720", identifier.getValue()); + + Assert.assertEquals("8", metadata.getMetaAttribute("calibre:rating")); + Assert.assertEquals("cover_pic", metadata.getMetaAttribute("cover")); + } } diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentReaderTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentReaderTest.java index 6f7e1a6a..c331dac5 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentReaderTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/PackageDocumentReaderTest.java @@ -1,24 +1,151 @@ package nl.siegmann.epublib.epub; -import java.util.Collection; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import junit.framework.TestCase; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import nl.siegmann.epublib.domain.Resource; +import nl.siegmann.epublib.domain.Resources; +import nl.siegmann.epublib.service.MediatypeService; -public class PackageDocumentReaderTest extends TestCase { +public class PackageDocumentReaderTest { - public void testFindCoverHref_content1() { - EpubReader epubReader = new EpubReader(); + @Test + public void testFindCoverHref_content1() throws SAXException, IOException { Document packageDocument; - try { - packageDocument = EpubProcessorSupport.createDocumentBuilder().parse(PackageDocumentReaderTest.class.getResourceAsStream("/opf/test1.opf")); - Collection coverHrefs = PackageDocumentReader.findCoverHrefs(packageDocument); - assertEquals(1, coverHrefs.size()); - assertEquals("cover.html", coverHrefs.iterator().next()); - } catch (Exception e) { - e.printStackTrace(); - assertTrue(false); - } + packageDocument = EpubProcessorSupport.createDocumentBuilder().parse(PackageDocumentReaderTest.class.getResourceAsStream("/opf/test1.opf")); + Collection coverHrefs = PackageDocumentReader.findCoverHrefs(packageDocument); + assertEquals(1, coverHrefs.size()); + assertEquals("cover.html", coverHrefs.iterator().next()); + } + + @Test + public void testFindTableOfContentsResource_simple_correct_toc_id() { + // given + String tocResourceId = "foo"; + Resources resources = mock(Resources.class); + Resource resource = mock(Resource.class); + when(resources.getByIdOrHref(tocResourceId)).thenReturn(resource); + + // when + Resource actualResult = PackageDocumentReader.findTableOfContentsResource("foo", resources); + + // then + Assert.assertEquals(resource, actualResult); + Mockito.verify(resources).getByIdOrHref(tocResourceId); + Mockito.verifyNoMoreInteractions(resources); + } + + @Test + public void testFindTableOfContentsResource_NCX_media_resource() { + // given + String tocResourceId = "foo"; + Resources resources = mock(Resources.class); + Resource resource = mock(Resource.class); + when(resources.getByIdOrHref(tocResourceId)).thenReturn(null); + when(resources.findFirstResourceByMediaType(MediatypeService.NCX)).thenReturn(resource); + + // when + Resource actualResult = PackageDocumentReader.findTableOfContentsResource("foo", resources); + + // then + Assert.assertEquals(resource, actualResult); + Mockito.verify(resources).getByIdOrHref(tocResourceId); + Mockito.verify(resources).findFirstResourceByMediaType(MediatypeService.NCX); + Mockito.verifyNoMoreInteractions(resources); + } + + @Test + public void testFindTableOfContentsResource_by_possible_id() { + // given + String tocResourceId = "foo"; + Resources resources = mock(Resources.class); + Resource resource = mock(Resource.class); + when(resources.getByIdOrHref(tocResourceId)).thenReturn(null); + when(resources.findFirstResourceByMediaType(MediatypeService.NCX)).thenReturn(null); + when(resources.getByIdOrHref("NCX")).thenReturn(resource); + + // when + Resource actualResult = PackageDocumentReader.findTableOfContentsResource("foo", resources); + + // then + Assert.assertEquals(resource, actualResult); + Mockito.verify(resources).getByIdOrHref(tocResourceId); + Mockito.verify(resources).getByIdOrHref("toc"); + Mockito.verify(resources).getByIdOrHref("TOC"); + Mockito.verify(resources).getByIdOrHref("ncx"); + Mockito.verify(resources).getByIdOrHref("NCX"); + Mockito.verify(resources).findFirstResourceByMediaType(MediatypeService.NCX); + Mockito.verifyNoMoreInteractions(resources); + } + + @Test + public void testFindTableOfContentsResource_nothing_found() { + // given + String tocResourceId = "foo"; + Resources resources = mock(Resources.class); + Resource resource = mock(Resource.class); + when(resources.getByIdOrHref(Mockito.anyString())).thenReturn(null); + when(resources.findFirstResourceByMediaType(MediatypeService.NCX)).thenReturn(null); + + // when + Resource actualResult = PackageDocumentReader.findTableOfContentsResource("foo", resources); + + // then + Assert.assertNull(actualResult); + Mockito.verify(resources).getByIdOrHref(tocResourceId); + Mockito.verify(resources).getByIdOrHref("toc"); + Mockito.verify(resources).getByIdOrHref("TOC"); + Mockito.verify(resources).getByIdOrHref("ncx"); + Mockito.verify(resources).getByIdOrHref("NCX"); + Mockito.verify(resources).getByIdOrHref("ncxtoc"); + Mockito.verify(resources).getByIdOrHref("NCXTOC"); + Mockito.verify(resources).findFirstResourceByMediaType(MediatypeService.NCX); + Mockito.verifyNoMoreInteractions(resources); + } + + @Test + public void testFixHrefs_simple_correct() { + // given + String packageHref = "OEBPS/content.opf"; + String resourceHref = "OEBPS/foo/bar.html"; + Resources resources = mock(Resources.class); + Resource resource = mock(Resource.class); + when(resources.getAll()).thenReturn(Arrays.asList(resource)); + when(resource.getHref()).thenReturn(resourceHref); + + // when + PackageDocumentReader.fixHrefs(packageHref, resources); + + // then + Mockito.verify(resource).setHref("foo/bar.html"); + } + + + @Test + public void testFixHrefs_invalid_prefix() { + // given + String packageHref = "123456789/"; + String resourceHref = "1/2.html"; + Resources resources = mock(Resources.class); + Resource resource = mock(Resource.class); + when(resources.getAll()).thenReturn(Arrays.asList(resource)); + when(resource.getHref()).thenReturn(resourceHref); + + // when + PackageDocumentReader.fixHrefs(packageHref, resources); + + // then + Assert.assertTrue(true); } } diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/ResourcesLoaderTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/ResourcesLoaderTest.java new file mode 100644 index 00000000..a4ea4da9 --- /dev/null +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/ResourcesLoaderTest.java @@ -0,0 +1,169 @@ +package nl.siegmann.epublib.epub; + +import net.sf.jazzlib.ZipException; +import net.sf.jazzlib.ZipFile; +import net.sf.jazzlib.ZipInputStream; +import nl.siegmann.epublib.domain.LazyResource; +import nl.siegmann.epublib.domain.Resource; +import nl.siegmann.epublib.domain.Resources; +import nl.siegmann.epublib.service.MediatypeService; +import nl.siegmann.epublib.util.IOUtil; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ResourcesLoaderTest { + + private static final String encoding = "UTF-8"; + private static String testBookFilename; + + @BeforeClass + public static void setUpClass() throws IOException { + File testBook = File.createTempFile("testBook", ".epub"); + OutputStream out = new FileOutputStream(testBook); + IOUtil.copy(ResourcesLoaderTest.class.getResourceAsStream("/testbook1.epub"), out); + out.close(); + + ResourcesLoaderTest.testBookFilename = testBook.getAbsolutePath(); + } + + @AfterClass + public static void tearDownClass() { + //noinspection ResultOfMethodCallIgnored + new File(testBookFilename).delete(); + } + + /** + * Loads the Resources from a ZipInputStream + */ + @Test + public void testLoadResources_ZipInputStream() throws IOException { + // given + ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(new File(testBookFilename))); + + // when + Resources resources = ResourcesLoader.loadResources(zipInputStream, encoding); + + // then + verifyResources(resources); + } + + /** + * Loads the Resources from a zero length file, using ZipInputStream
    + * See Issue #122 Infinite loop. + */ + @Test(expected = ZipException.class) + public void testLoadResources_ZipInputStream_WithZeroLengthFile() throws IOException { + // given + ZipInputStream zipInputStream = new ZipInputStream(this.getClass().getResourceAsStream("/zero_length_file.epub")); + + // when + ResourcesLoader.loadResources(zipInputStream, encoding); + } + + /** + * Loads the Resources from a file that is not a valid zip, using ZipInputStream
    + * See Issue #122 Infinite loop. + */ + @Test(expected = ZipException.class) + public void testLoadResources_ZipInputStream_WithInvalidFile() throws IOException { + // given + ZipInputStream zipInputStream = new ZipInputStream(this.getClass().getResourceAsStream("/not_a_zip.epub")); + + // when + ResourcesLoader.loadResources(zipInputStream, encoding); + } + + /** + * Loads the Resources from a ZipFile + */ + @Test + public void testLoadResources_ZipFile() throws IOException { + // given + ZipFile zipFile = new ZipFile(testBookFilename); + + // when + Resources resources = ResourcesLoader.loadResources(zipFile, encoding); + + // then + verifyResources(resources); + } + + /** + * Loads all Resources lazily from a ZipFile + */ + @Test + public void testLoadResources_ZipFile_lazy_all() throws IOException { + // given + ZipFile zipFile = new ZipFile(testBookFilename); + + // when + Resources resources = ResourcesLoader.loadResources(zipFile, encoding, Arrays.asList(MediatypeService.mediatypes)); + + // then + verifyResources(resources); + Assert.assertEquals(Resource.class, resources.getById("container").getClass()); + Assert.assertEquals(LazyResource.class, resources.getById("book1").getClass()); + } + + /** + * Loads the Resources from a ZipFile, some of them lazily. + */ + @Test + public void testLoadResources_ZipFile_partial_lazy() throws IOException { + // given + ZipFile zipFile = new ZipFile(testBookFilename); + + // when + Resources resources = ResourcesLoader.loadResources(zipFile, encoding, Collections.singletonList(MediatypeService.CSS)); + + // then + verifyResources(resources); + Assert.assertEquals(Resource.class, resources.getById("container").getClass()); + Assert.assertEquals(LazyResource.class, resources.getById("book1").getClass()); + Assert.assertEquals(Resource.class, resources.getById("chapter1").getClass()); + } + + private void verifyResources(Resources resources) throws IOException { + Assert.assertNotNull(resources); + Assert.assertEquals(12, resources.getAll().size()); + List allHrefs = new ArrayList<>(resources.getAllHrefs()); + Collections.sort(allHrefs); + + Resource resource; + byte[] expectedData; + + // container + resource = resources.getByHref(allHrefs.get(0)); + Assert.assertEquals("container", resource.getId()); + Assert.assertEquals("META-INF/container.xml", resource.getHref()); + Assert.assertNull(resource.getMediaType()); + Assert.assertEquals(230, resource.getData().length); + + // book1.css + resource = resources.getByHref(allHrefs.get(1)); + Assert.assertEquals("book1", resource.getId()); + Assert.assertEquals("OEBPS/book1.css", resource.getHref()); + Assert.assertEquals(MediatypeService.CSS, resource.getMediaType()); + Assert.assertEquals(65, resource.getData().length); + expectedData = IOUtil.toByteArray(this.getClass().getResourceAsStream("/book1/book1.css")); + Assert.assertArrayEquals(expectedData, resource.getData()); + + + // chapter1 + resource = resources.getByHref(allHrefs.get(2)); + Assert.assertEquals("chapter1", resource.getId()); + Assert.assertEquals("OEBPS/chapter1.html", resource.getHref()); + Assert.assertEquals(MediatypeService.XHTML, resource.getMediaType()); + Assert.assertEquals(247, resource.getData().length); + expectedData = IOUtil.toByteArray(this.getClass().getResourceAsStream("/book1/chapter1.html")); + Assert.assertArrayEquals(expectedData, resource.getData()); + } +} diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/util/CollectionUtilTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/util/CollectionUtilTest.java new file mode 100644 index 00000000..ef9e7c26 --- /dev/null +++ b/epublib-core/src/test/java/nl/siegmann/epublib/util/CollectionUtilTest.java @@ -0,0 +1,25 @@ +package nl.siegmann.epublib.util; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Test; + +public class CollectionUtilTest { + + @Test + public void testIsEmpty_null() { + Assert.assertTrue(CollectionUtil.isEmpty(null)); + } + + @Test + public void testIsEmpty_empty() { + Assert.assertTrue(CollectionUtil.isEmpty(new ArrayList())); + } + + @Test + public void testIsEmpty_elements() { + Assert.assertFalse(CollectionUtil.isEmpty(Arrays.asList("foo"))); + } +} diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/util/IOUtilTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/util/IOUtilTest.java index 3cd6f6b4..19ed7282 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/util/IOUtilTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/util/IOUtilTest.java @@ -1,15 +1,19 @@ package nl.siegmann.epublib.util; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Random; -import junit.framework.TestCase; +import org.junit.Test; -public class IOUtilTest extends TestCase { +public class IOUtilTest { + @Test public void testToByteArray1() { byte[] testArray = new byte[Byte.MAX_VALUE - Byte.MIN_VALUE]; for (int i = Byte.MIN_VALUE; i < Byte.MAX_VALUE; i++) { @@ -24,6 +28,7 @@ public void testToByteArray1() { } } + @Test public void testToByteArray2() { byte[] testArray = new byte[IOUtil.IO_COPY_BUFFER_SIZE + 1]; Random random = new Random(); @@ -37,6 +42,7 @@ public void testToByteArray2() { } } + @Test public void testCopyInputStream1() { byte[] testArray = new byte[(IOUtil.IO_COPY_BUFFER_SIZE * 3) + 10]; Random random = new Random(); @@ -52,6 +58,7 @@ public void testCopyInputStream1() { } } + @Test public void testCalcNrRead() { Integer[] testData = new Integer[] { // nrRead, totalNrRead, reault diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/util/NoCloseOutputStreamTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/util/NoCloseOutputStreamTest.java new file mode 100644 index 00000000..3d48ecd8 --- /dev/null +++ b/epublib-core/src/test/java/nl/siegmann/epublib/util/NoCloseOutputStreamTest.java @@ -0,0 +1,60 @@ +package nl.siegmann.epublib.util; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.io.OutputStream; + +public class NoCloseOutputStreamTest { + + @Mock + private OutputStream outputStream; + + private NoCloseOutputStream noCloseOutputStream; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + this.noCloseOutputStream = new NoCloseOutputStream(outputStream); + } + + @Test + public void testWrite() throws IOException { + // given + + // when + noCloseOutputStream.write(17); + + // then + Mockito.verify(outputStream).write(17); + Mockito.verifyNoMoreInteractions(outputStream); + } + + @Test + public void testClose() throws IOException { + // given + + // when + noCloseOutputStream.close(); + + // then + Mockito.verifyNoMoreInteractions(outputStream); + } + + @Test + public void testWriteClose() throws IOException { + // given + + // when + noCloseOutputStream.write(17); + noCloseOutputStream.close(); + + // then + Mockito.verify(outputStream).write(17); + Mockito.verifyNoMoreInteractions(outputStream); + } +} diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/util/NoCloseWriterTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/util/NoCloseWriterTest.java new file mode 100644 index 00000000..3e35d349 --- /dev/null +++ b/epublib-core/src/test/java/nl/siegmann/epublib/util/NoCloseWriterTest.java @@ -0,0 +1,72 @@ +package nl.siegmann.epublib.util; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.io.Writer; + +public class NoCloseWriterTest { + + @Mock + private Writer delegateWriter; + + private NoCloseWriter noCloseWriter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + this.noCloseWriter = new NoCloseWriter(delegateWriter); + } + + @Test + public void testWrite() throws IOException { + // given + + // when + noCloseWriter.write(new char[]{'e','f','g'},2,1); + + // then + Mockito.verify(delegateWriter).write(new char[]{'e','f','g'},2,1); + Mockito.verifyNoMoreInteractions(delegateWriter); + } + + @Test + public void testFlush() throws IOException { + // given + + // when + noCloseWriter.flush(); + + // then + Mockito.verify(delegateWriter).flush(); + Mockito.verifyNoMoreInteractions(delegateWriter); + } + + @Test + public void testClose() throws IOException { + // given + + // when + noCloseWriter.close(); + + // then + Mockito.verifyNoMoreInteractions(delegateWriter); + } + + @Test + public void testWriteClose() throws IOException { + // given + + // when + noCloseWriter.write(new char[]{'e','f','g'},2,1); + noCloseWriter.close(); + + // then + Mockito.verify(delegateWriter).write(new char[]{'e','f','g'},2,1); + Mockito.verifyNoMoreInteractions(delegateWriter); + } +} diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/util/StringUtilTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/util/StringUtilTest.java index 062bd0f8..2d2b277c 100644 --- a/epublib-core/src/test/java/nl/siegmann/epublib/util/StringUtilTest.java +++ b/epublib-core/src/test/java/nl/siegmann/epublib/util/StringUtilTest.java @@ -1,65 +1,56 @@ package nl.siegmann.epublib.util; +import static org.junit.Assert.assertEquals; + import java.io.IOException; -import junit.framework.TestCase; -import org.apache.commons.io.FilenameUtils; -public class StringUtilTest extends TestCase { +import org.junit.Test; + +public class StringUtilTest { + @Test public void testDefaultIfNull() { - Object[] testData = new Object[] { - null, "", - "", "", - " ", " ", - "foo", "foo" - }; + Object[] testData = new Object[] { null, "", "", "", " ", " ", "foo", + "foo" }; for (int i = 0; i < testData.length; i += 2) { - String actualResult = StringUtil.defaultIfNull((String) testData[i]); + String actualResult = StringUtil + .defaultIfNull((String) testData[i]); String expectedResult = (String) testData[i + 1]; - assertEquals((i / 2) + " : " + testData[i], expectedResult, actualResult); + assertEquals((i / 2) + " : " + testData[i], expectedResult, + actualResult); } } + @Test public void testDefaultIfNull_with_default() { - Object[] testData = new Object[] { - null, null, null, - "", null, "", - null, "", "", - "foo", "", "foo", - "", "foo", "", - " ", " ", " ", - null, "foo", "foo", - }; + Object[] testData = new Object[] { null, null, null, "", null, "", + null, "", "", "foo", "", "foo", "", "foo", "", " ", " ", " ", + null, "foo", "foo", }; for (int i = 0; i < testData.length; i += 3) { - String actualResult = StringUtil.defaultIfNull((String) testData[i], (String) testData[i + 1]); + String actualResult = StringUtil.defaultIfNull( + (String) testData[i], (String) testData[i + 1]); String expectedResult = (String) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } - + + @Test public void testIsEmpty() { - Object[] testData = new Object[] { - null, true, - "", true, - " ", false, - "asdfasfd", false - }; + Object[] testData = new Object[] { null, true, "", true, " ", false, + "asdfasfd", false }; for (int i = 0; i < testData.length; i += 2) { boolean actualResult = StringUtil.isEmpty((String) testData[i]); boolean expectedResult = (Boolean) testData[i + 1]; assertEquals(expectedResult, actualResult); } } - + @Test public void testIsBlank() { - Object[] testData = new Object[] { - null, true, - "", true, - " ", true, - "\t\t \n\n", true, - "asdfasfd", false - }; + Object[] testData = new Object[] { null, true, "", true, " ", true, + "\t\t \n\n", true, "asdfasfd", false }; for (int i = 0; i < testData.length; i += 2) { boolean actualResult = StringUtil.isBlank((String) testData[i]); boolean expectedResult = (Boolean) testData[i + 1]; @@ -67,151 +58,143 @@ public void testIsBlank() { } } + @Test public void testIsNotBlank() { - Object[] testData = new Object[] { - null, ! true, - "", ! true, - " ", ! true, - "\t\t \n\n", ! true, - "asdfasfd", ! false - }; + Object[] testData = new Object[] { null, !true, "", !true, " ", !true, + "\t\t \n\n", !true, "asdfasfd", !false }; for (int i = 0; i < testData.length; i += 2) { boolean actualResult = StringUtil.isNotBlank((String) testData[i]); boolean expectedResult = (Boolean) testData[i + 1]; - assertEquals((i / 2) + " : " + testData[i], expectedResult, actualResult); + assertEquals((i / 2) + " : " + testData[i], expectedResult, + actualResult); } } - + @Test public void testEquals() { - Object[] testData = new Object[] { - null, null, true, - "", "", true, - null, "", false, - "", null, false, - null, "foo", false, - "foo", null, false, - "", "foo", false, - "foo", "", false, - "foo", "bar", false, - "foo", "foo", true - }; + Object[] testData = new Object[] { null, null, true, "", "", true, + null, "", false, "", null, false, null, "foo", false, "foo", + null, false, "", "foo", false, "foo", "", false, "foo", "bar", + false, "foo", "foo", true }; for (int i = 0; i < testData.length; i += 3) { - boolean actualResult = StringUtil.equals( (String) testData[i], (String) testData[i + 1]); + boolean actualResult = StringUtil.equals((String) testData[i], + (String) testData[i + 1]); boolean expectedResult = (Boolean) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } - + + @Test public void testEndWithIgnoreCase() { - Object[] testData = new Object[] { - null, null, true, - "", "", true, - "", "foo", false, - "foo", "foo", true, - "foo.bar", "bar", true, - "foo.bar", "barX", false, - "foo.barX", "bar", false, - "foo", "bar", false, - "foo.BAR", "bar", true, - "foo.bar", "BaR", true - }; + Object[] testData = new Object[] { null, null, true, "", "", true, "", + "foo", false, "foo", "foo", true, "foo.bar", "bar", true, + "foo.bar", "barX", false, "foo.barX", "bar", false, "foo", + "bar", false, "foo.BAR", "bar", true, "foo.bar", "BaR", true }; for (int i = 0; i < testData.length; i += 3) { - boolean actualResult = StringUtil.endsWithIgnoreCase( (String) testData[i], (String) testData[i + 1]); + boolean actualResult = StringUtil.endsWithIgnoreCase( + (String) testData[i], (String) testData[i + 1]); boolean expectedResult = (Boolean) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } + @Test public void testSubstringBefore() { - Object[] testData = new Object[] { - "", ' ', "", - "", 'X', "", - "fox", 'x', "fo", - "foo.bar", 'b', "foo.", - "aXbXc", 'X', "a", - }; + Object[] testData = new Object[] { "", ' ', "", "", 'X', "", "fox", + 'x', "fo", "foo.bar", 'b', "foo.", "aXbXc", 'X', "a", }; for (int i = 0; i < testData.length; i += 3) { - String actualResult = StringUtil.substringBefore((String) testData[i], (Character) testData[i + 1]); + String actualResult = StringUtil.substringBefore( + (String) testData[i], (Character) testData[i + 1]); String expectedResult = (String) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } + @Test public void testSubstringBeforeLast() { - Object[] testData = new Object[] { - "", ' ', "", - "", 'X', "", - "fox", 'x', "fo", - "foo.bar", 'b', "foo.", - "aXbXc", 'X', "aXb", - }; + Object[] testData = new Object[] { "", ' ', "", "", 'X', "", "fox", + 'x', "fo", "foo.bar", 'b', "foo.", "aXbXc", 'X', "aXb", }; for (int i = 0; i < testData.length; i += 3) { - String actualResult = StringUtil.substringBeforeLast((String) testData[i], (Character) testData[i + 1]); + String actualResult = StringUtil.substringBeforeLast( + (String) testData[i], (Character) testData[i + 1]); String expectedResult = (String) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } + @Test public void testSubstringAfter() { - Object[] testData = new Object[] { - "", ' ', "", - "", 'X', "", - "fox", 'f', "ox", - "foo.bar", 'b', "ar", - "aXbXc", 'X', "bXc", - }; + Object[] testData = new Object[] { "", ' ', "", "", 'X', "", "fox", + 'f', "ox", "foo.bar", 'b', "ar", "aXbXc", 'X', "bXc", }; for (int i = 0; i < testData.length; i += 3) { - String actualResult = StringUtil.substringAfter((String) testData[i], (Character) testData[i + 1]); + String actualResult = StringUtil.substringAfter( + (String) testData[i], (Character) testData[i + 1]); String expectedResult = (String) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } + @Test public void testSubstringAfterLast() { - Object[] testData = new Object[] { - "", ' ', "", - "", 'X', "", - "fox", 'f', "ox", - "foo.bar", 'b', "ar", - "aXbXc", 'X', "c", - }; + Object[] testData = new Object[] { "", ' ', "", "", 'X', "", "fox", + 'f', "ox", "foo.bar", 'b', "ar", "aXbXc", 'X', "c", }; for (int i = 0; i < testData.length; i += 3) { - String actualResult = StringUtil.substringAfterLast((String) testData[i], (Character) testData[i + 1]); + String actualResult = StringUtil.substringAfterLast( + (String) testData[i], (Character) testData[i + 1]); String expectedResult = (String) testData[i + 2]; - assertEquals((i / 3) + " : " + testData[i] + ", " + testData[i + 1], expectedResult, actualResult); + assertEquals( + (i / 3) + " : " + testData[i] + ", " + testData[i + 1], + expectedResult, actualResult); } } + @Test public void testToString() { assertEquals("[name: 'paul']", StringUtil.toString("name", "paul")); - assertEquals("[name: 'paul', address: 'a street']", StringUtil.toString("name", "paul", "address", "a street")); + assertEquals("[name: 'paul', address: 'a street']", + StringUtil.toString("name", "paul", "address", "a street")); assertEquals("[name: ]", StringUtil.toString("name", null)); - assertEquals("[name: 'paul', address: ]", StringUtil.toString("name", "paul", "address")); + assertEquals("[name: 'paul', address: ]", + StringUtil.toString("name", "paul", "address")); } - + + @Test public void testHashCode() { assertEquals(2522795, StringUtil.hashCode("isbn", "1234")); assertEquals(3499691, StringUtil.hashCode("ISBN", "1234")); } - + + @Test public void testReplacementForCollapsePathDots() throws IOException { - // This used to test StringUtil.collapsePathDots(String path). - // I have left it to confirm that the Apache commons FilenameUtils.normalize - // is a suitable replacement, but works where for "/a/b/../../c", which - // the old method did not. - String[] testData = new String[] { - "/foo/bar.html", "/foo/bar.html", - "/foo/../bar.html", "/bar.html", - "/foo/moo/../../bar.html", "/bar.html", - "/foo//bar.html", "/foo/bar.html", - "/foo/./bar.html", "/foo/bar.html", - "/foo/../sub/bar.html", "/sub/bar.html" + // This used to test StringUtil.collapsePathDots(String path). + // I have left it to confirm that the Apache commons + // FilenameUtils.normalize + // is a suitable replacement, but works where for "/a/b/../../c", which + // the old method did not. + String[] testData = new String[] { // + "/foo/bar.html", "/foo/bar.html", + "/foo/../bar.html", "/bar.html", // + "/foo/moo/../../bar.html", // + "/bar.html", "/foo//bar.html", // + "/foo/bar.html", "/foo/./bar.html", // + "/foo/bar.html", // + "/a/b/../../c", "/c", // + "/foo/../sub/bar.html", "/sub/bar.html" // }; - for (int i = 0; i < testData.length; i += 2) { - String actualResult = FilenameUtils.normalize(testData[i], true); - assertEquals(testData[i + 1], actualResult); - } + for (int i = 0; i < testData.length; i += 2) { + String actualResult = StringUtil.collapsePathDots(testData[i]); + assertEquals(testData[i], testData[i + 1], actualResult); + } } } diff --git a/epublib-core/src/test/resources/not_a_zip.epub b/epublib-core/src/test/resources/not_a_zip.epub new file mode 100644 index 00000000..a977c666 --- /dev/null +++ b/epublib-core/src/test/resources/not_a_zip.epub @@ -0,0 +1,2 @@ +This is not a valid zip file. +Used for testing LoadResources. \ No newline at end of file diff --git a/epublib-core/src/test/resources/testbook1.epub b/epublib-core/src/test/resources/testbook1.epub new file mode 100644 index 00000000..25992d8d Binary files /dev/null and b/epublib-core/src/test/resources/testbook1.epub differ diff --git a/epublib-core/src/test/resources/zero_length_file.epub b/epublib-core/src/test/resources/zero_length_file.epub new file mode 100644 index 00000000..e69de29b diff --git a/epublib-parent/pom.xml b/epublib-parent/pom.xml new file mode 100644 index 00000000..e2ac8d8e --- /dev/null +++ b/epublib-parent/pom.xml @@ -0,0 +1,200 @@ + + + + + 4.0.0 + + nl.siegmann.epublib + epublib-parent + epublib-parent + pom + 4.0 + A java library for reading/writing/manipulating epub files + http://www.siegmann.nl/epublib + 2009 + + + 4.0 + UTF-8 + 1.6.1 + 3.8.1 + 3.8.2 + 3.2.1 + 3.2.0 + 3.1.1 + 1.7 + 1.7 + + + + ../epublib-core + ../epublib-tools + + + + + LGPL + http://www.gnu.org/licenses/lgpl.html + repo + + + + + + paul + Paul Siegmann + paul.siegmann+epublib@gmail.com + http://www.siegmann.nl/ + +1 + + + + + github + http://github.com/psiegman/epublib/issues + + + + + + net.sf.kxml + kxml2 + 2.3.0 + + + xmlpull + xmlpull + 1.1.3.4d_b4_min + + + net.sourceforge.htmlcleaner + htmlcleaner + 2.2 + + + commons-io + commons-io + 2.0.1 + + + commons-lang + commons-lang + 2.4 + + + net.sf.kxml + kxml2 + 2.3.0 + + + xmlpull + xmlpull + 1.1.3.4d_b4_min + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + commons-vfs + commons-vfs + 1.0 + + + junit + junit + 4.10 + + + org.mockito + mockito-all + 1.10.19 + + + + + + + github.repo + file:///D:/private/project/git-maven-repo/mvn-repo/releases + + + + + http://github.com/psiegman/epublib + scm:git:https://psiegman@github.com/psiegman/epublib.git + scm:git:https://psiegman@github.com/psiegman/epublib.git + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${source.version} + ${target.version} + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + 8 + none + + + + attach-javadocs + + jar + + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + + + + maven + http://repo1.maven.org/maven2/ + + + jboss + https://repository.jboss.org/nexus/ + + + + diff --git a/epublib-tools/README.md b/epublib-tools/README.md new file mode 100644 index 00000000..a0cd99fb --- /dev/null +++ b/epublib-tools/README.md @@ -0,0 +1,28 @@ +## Epub Viewer + +A simple epub viewer built with java Swing. + +### Startup + + java nl.siegmann.epublib.viewer.Viewer + +## Fileset2epub + +A tool to generate an epub from a windows help / chm file or from a set of html files. + + java nl.siegmann.epublib.Fileset2Epub + +Arguments: + + --author [lastname,firstname] + --cover-image [image to use as cover] + --input-ecoding [text encoding] # The encoding of the input html files. If funny characters show + # up in the result try 'iso-8859-1', 'windows-1252' or 'utf-8' + # If that doesn't work try to find an appropriate one from + # this list: http://en.wikipedia.org/wiki/Character_encoding + --in [input directory] + --isbn [isbn number] + --out [output epub file] + --title [book title] + --type [input type, can be 'epub', 'chm' or empty] + --xsl [html post processing file] diff --git a/epublib-tools/pom.xml b/epublib-tools/pom.xml index 6b2e5c1f..66035581 100644 --- a/epublib-tools/pom.xml +++ b/epublib-tools/pom.xml @@ -8,7 +8,6 @@ nl.siegmann.epublib epublib-tools epublib-tools - 3.1 A java library for reading/writing/manipulating epub files http://www.siegmann.nl/epublib 2009 @@ -16,19 +15,19 @@ nl.siegmann.epublib epublib-parent - 3.1 + 4.0 + ../epublib-parent/pom.xml nl.siegmann.epublib epublib-core - 3.1 + ${epublib.version} net.sourceforge.htmlcleaner htmlcleaner - 2.2 org.jdom @@ -43,42 +42,34 @@ commons-lang commons-lang - 2.4 net.sf.kxml kxml2 - 2.3.0 xmlpull xmlpull - 1.1.3.4d_b4_min commons-io commons-io - 2.0.1 org.slf4j slf4j-api - ${slf4j.version} org.slf4j slf4j-simple - ${slf4j.version} commons-vfs commons-vfs - 1.0 junit junit - 3.8.1 test @@ -105,10 +96,10 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + ${maven-compiler-plugin.version} - 1.6 - 1.6 + ${source.version} + ${target.version} @@ -145,7 +136,7 @@ org.apache.maven.plugins maven-site-plugin - 3.0-beta-3 + ${maven-site-plugin.version} diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/chm/ChmParser.java b/epublib-tools/src/main/java/nl/siegmann/epublib/chm/ChmParser.java index f9ca6d65..501a9f5b 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/chm/ChmParser.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/chm/ChmParser.java @@ -22,8 +22,6 @@ import org.apache.commons.vfs.FileSystemException; import org.apache.commons.vfs.FileType; -import com.sun.org.apache.bcel.internal.Constants; - /** * Reads the files that are extracted from a windows help ('.chm') file and creates a epublib Book out of it. * @@ -61,11 +59,11 @@ public static Book parseChm(FileObject chmRootDir, String inputHtmlEncoding) /** - * Finds in the '#SYSTEM' file the 3rd set of characters that have ascii value >= 32 and <= 126 and is more than 3 characters long. + * Finds in the '#SYSTEM' file the 3rd set of characters that have ascii value >= 32 and >= 126 and is more than 3 characters long. * Assumes that that is then the title of the book. * * @param chmRootDir - * @return + * @return Finds in the '#SYSTEM' file the 3rd set of characters that have ascii value >= 32 and >= 126 and is more than 3 characters long. * @throws IOException */ protected static String findTitle(FileObject chmRootDir) throws IOException { diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/fileset/FilesetBookCreator.java b/epublib-tools/src/main/java/nl/siegmann/epublib/fileset/FilesetBookCreator.java index c38f1cdd..66f41280 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/fileset/FilesetBookCreator.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/fileset/FilesetBookCreator.java @@ -61,7 +61,7 @@ public static Book createBookFromDirectory(FileObject rootDirectory) throws IOEx * * @see nl.siegmann.epublib.domain.MediaTypeService * @param rootDirectory - * @return + * @return the newly created Book * @throws IOException */ public static Book createBookFromDirectory(FileObject rootDirectory, String encoding) throws IOException { diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/search/SearchIndex.java b/epublib-tools/src/main/java/nl/siegmann/epublib/search/SearchIndex.java index e3ce2e84..1c1c5d11 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/search/SearchIndex.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/search/SearchIndex.java @@ -142,7 +142,7 @@ public static String getSearchContent(Reader content) { * Checks whether the given character is a java whitespace or a non-breaking-space (&nbsp;). * * @param c - * @return + * @return whether the given character is a java whitespace or a non-breaking-space (&nbsp;). */ private static boolean isHtmlWhitespace(int c) { return c == NBSP || Character.isWhitespace(c); @@ -177,7 +177,7 @@ public static String unicodeTrim(String text) { * Replaces multiple whitespaces with a single space.
    * * @param text - * @return + * @return html encoded text turned into plain text. */ public static String cleanText(String text) { text = unicodeTrim(text); diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/util/DesktopUtil.java b/epublib-tools/src/main/java/nl/siegmann/epublib/util/DesktopUtil.java index b052e386..aa18159f 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/util/DesktopUtil.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/util/DesktopUtil.java @@ -13,7 +13,7 @@ public class DesktopUtil { /** * Open a URL in the default web browser. * - * @param a URL to open in a web browser. + * @param url a URL to open in a web browser. * @return true if a browser has been launched. */ public static boolean launchBrowser(URL url) throws BrowserLaunchException { diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/util/ToolsResourceUtil.java b/epublib-tools/src/main/java/nl/siegmann/epublib/util/ToolsResourceUtil.java index 7e23646c..3f84e175 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/util/ToolsResourceUtil.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/util/ToolsResourceUtil.java @@ -56,11 +56,11 @@ public static String getTitle(Resource resource) { /** - * Retrieves whatever it finds between ... or .... + * Retrieves whatever it finds between <title>...</title> or <h1-7>...</h1-7>. * The first match is returned, even if it is a blank string. * If it finds nothing null is returned. * @param resource - * @return + * @return whatever it finds in the resource between <title>...</title> or <h1-7>...</h1-7>. */ public static String findTitleFromXhtml(Resource resource) { if (resource == null) { diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/util/VFSUtil.java b/epublib-tools/src/main/java/nl/siegmann/epublib/util/VFSUtil.java index 248e1059..4124ca42 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/util/VFSUtil.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/util/VFSUtil.java @@ -46,7 +46,7 @@ public static String calculateHref(FileObject rootDir, FileObject currentFile) t /** * First tries to load the inputLocation via VFS; if that doesn't work it tries to load it as a local File * @param inputLocation - * @return + * @return the FileObject referred to by the inputLocation * @throws FileSystemException */ public static FileObject resolveFileObject(String inputLocation) throws FileSystemException { @@ -69,7 +69,7 @@ public static FileObject resolveFileObject(String inputLocation) throws FileSyst * First tries to load the inputLocation via VFS; if that doesn't work it tries to load it as a local File * * @param inputLocation - * @return + * @return the InputStream referred to by the inputLocation * @throws FileSystemException */ public static InputStream resolveInputStream(String inputLocation) throws FileSystemException { diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ButtonBar.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ButtonBar.java index b1cda782..553f74b3 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ButtonBar.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ButtonBar.java @@ -12,7 +12,6 @@ /** * Creates a panel with the first,previous,next and last buttons. * - * @return */ class ButtonBar extends JPanel { private static final long serialVersionUID = 6431437924245035812L; @@ -95,4 +94,4 @@ public void actionPerformed(ActionEvent e) { } }); } -} \ No newline at end of file +} diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ContentPane.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ContentPane.java index 6cb59583..ae76fac6 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ContentPane.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ContentPane.java @@ -38,7 +38,6 @@ /** * Displays a page * - * @return */ public class ContentPane extends JPanel implements NavigationEventListener, HyperlinkListener { @@ -140,7 +139,7 @@ private void initBook(Book book) { * * @param searchString * @param possibleValues - * @return + * @return Whether the given searchString matches any of the possibleValues. */ private static boolean matchesAny(String searchString, String... possibleValues) { for (int i = 0; i < possibleValues.length; i++) { @@ -337,7 +336,7 @@ public void gotoNextPage() { * Property handles http encoded spaces and such. * * @param clickUrl - * @return + * @return a link generated by a click on a link transformed into a document to a resource href. */ private String calculateTargetHref(URL clickUrl) { String resourceHref = clickUrl.toString(); @@ -383,4 +382,4 @@ public void navigationPerformed(NavigationEvent navigationEvent) { } -} \ No newline at end of file +} diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/HTMLDocumentFactory.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/HTMLDocumentFactory.java index edd5edc6..3af997b0 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/HTMLDocumentFactory.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/HTMLDocumentFactory.java @@ -84,7 +84,7 @@ private void putDocument(Resource resource, HTMLDocument document) { * the resource and adds it to the cache. * * @param resource - * @return + * @return the HTMLDocument representation of the resource. */ public HTMLDocument getDocument(Resource resource) { HTMLDocument document = null; @@ -122,7 +122,7 @@ private String stripHtml(String input) { * these confuse the html viewer. * * @param input - * @return + * @return the input stripped of control characters */ private static String removeControlTags(String input) { StringBuilder result = new StringBuilder(); @@ -150,7 +150,7 @@ private static String removeControlTags(String input) { * If the resources is not of type XHTML then null is returned. * * @param resource - * @return + * @return a swing HTMLDocument created from the given resource. */ private HTMLDocument createDocument(Resource resource) { HTMLDocument result = null; diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ImageLoaderCache.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ImageLoaderCache.java index dc921466..2ca5a250 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ImageLoaderCache.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ImageLoaderCache.java @@ -85,6 +85,9 @@ private String getResourceHref(String requestUrl) { String resourceHref = requestUrl.toString().substring(IMAGE_URL_PREFIX.length()); resourceHref = currentFolder + resourceHref; resourceHref = FilenameUtils.normalize(resourceHref); + // normalize uses the SYSTEM_SEPARATOR, which on windows is a '\' + // replace with '/' to make it href '/' + resourceHref = resourceHref.replaceAll("\\\\", "/"); return resourceHref; } diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/TableOfContentsPane.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/TableOfContentsPane.java index 8c73a8f8..5bd6631a 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/TableOfContentsPane.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/TableOfContentsPane.java @@ -46,7 +46,6 @@ public class TableOfContentsPane extends JPanel implements NavigationEventListen * Also sets up a selectionListener that updates the SectionWalker when an item in the tree is selected. * * @param navigator - * @return */ public TableOfContentsPane(Navigator navigator) { super(new GridLayout(1, 0)); diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/Viewer.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/Viewer.java index 6506e933..eea823f8 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/Viewer.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/Viewer.java @@ -40,8 +40,6 @@ public class Viewer { - private static final long serialVersionUID = 1610691708767665447L; - static final Logger log = LoggerFactory.getLogger(Viewer.class); private final JFrame mainWindow; private BrowseBar browseBar; @@ -49,7 +47,7 @@ public class Viewer { private JSplitPane leftSplitPane; private JSplitPane rightSplitPane; private Navigator navigator = new Navigator(); - NavigationHistory browserHistory; + private NavigationHistory browserHistory; private BookProcessorPipeline epubCleaner = new BookProcessorPipeline(Collections.emptyList()); public Viewer(InputStream bookStream) { diff --git a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ViewerUtil.java b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ViewerUtil.java index 57739988..7f9e3b0e 100644 --- a/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ViewerUtil.java +++ b/epublib-tools/src/main/java/nl/siegmann/epublib/viewer/ViewerUtil.java @@ -19,7 +19,7 @@ public class ViewerUtil { * * @param iconName * @param backupLabel - * @return + * @return a button with the given icon. */ // package static JButton createButton(String iconName, String backupLabel) { diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 886d3b2e..00000000 --- a/pom.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - - 4.0.0 - - nl.siegmann.epublib - epublib-parent - epublib-parent - pom - 3.1 - A java library for reading/writing/manipulating epub files - http://www.siegmann.nl/epublib - 2009 - - - UTF-8 - 1.6.1 - - - - epublib-core - epublib-tools - - - - - LGPL - http://www.gnu.org/licenses/lgpl.html - repo - - - - - - paul - Paul Siegmann - paul.siegmann+epublib@gmail.com - http://www.siegmann.nl/ - +1 - - - - - github - http://github.com/psiegman/epublib/issues - - - - - github.repo - file:///D:/private/project/git-maven-repo/mvn-repo - - - - - http://github.com/psiegman/epublib - scm:git:https://psiegman@github.com/psiegman/epublib.git - scm:git:https://psiegman@github.com/psiegman/epublib.git - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-source-plugin - 2.1.2 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - - jar - - - - - - - - - - org.apache.maven.plugins - maven-site-plugin - 3.0-beta-3 - - - - - - maven - http://repo1.maven.org/maven2/ - - - jboss - https://repository.jboss.org/nexus/ - - -