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 f08d5587..dc72636c 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 @@ -30,15 +30,25 @@ public class EpubWriter { // package static final String EMPTY_NAMESPACE_PREFIX = ""; - - private BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR; + + private final EpubWriterConfiguration configuration; + + private BookProcessor bookProcessor; public EpubWriter() { - this(BookProcessor.IDENTITY_BOOKPROCESSOR); + this(new EpubWriterConfiguration(), BookProcessor.IDENTITY_BOOKPROCESSOR); } - - + + public EpubWriter(EpubWriterConfiguration configuration) { + this(configuration, BookProcessor.IDENTITY_BOOKPROCESSOR); + } + public EpubWriter(BookProcessor bookProcessor) { + this(new EpubWriterConfiguration(), bookProcessor); + } + + public EpubWriter(EpubWriterConfiguration configuration, BookProcessor bookProcessor) { + this.configuration = configuration; this.bookProcessor = bookProcessor; } @@ -96,7 +106,7 @@ private void writeResource(Resource resource, ZipOutputStream resultStream) return; } try { - resultStream.putNextEntry(new ZipEntry("OEBPS/" + resource.getHref())); + resultStream.putNextEntry(new ZipEntry(configuration.getContentDirectoryName() + "/" + resource.getHref())); InputStream inputStream = resource.getInputStream(); IOUtil.copy(inputStream, resultStream); inputStream.close(); @@ -107,7 +117,7 @@ private void writeResource(Resource resource, ZipOutputStream resultStream) private void writePackageDocument(Book book, ZipOutputStream resultStream) throws IOException { - resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); + resultStream.putNextEntry(new ZipEntry(configuration.getContentDirectoryName() + "/content.opf")); XmlSerializer xmlSerializer = EpubProcessorSupport.createXmlSerializer(resultStream); PackageDocumentWriter.write(this, xmlSerializer, book); xmlSerializer.flush(); @@ -127,7 +137,7 @@ private void writeContainer(ZipOutputStream resultStream) throws IOException { out.write("\n"); out.write("\n"); out.write("\t\n"); - out.write("\t\t\n"); + out.write("\t\t\n"); out.write("\t\n"); out.write(""); out.flush(); diff --git a/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriterConfiguration.java b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriterConfiguration.java new file mode 100644 index 00000000..f872f0d0 --- /dev/null +++ b/epublib-core/src/main/java/nl/siegmann/epublib/epub/EpubWriterConfiguration.java @@ -0,0 +1,45 @@ +package nl.siegmann.epublib.epub; + +/** + * Allows for the configuration of an {@link EpubWriter}. + */ +public class EpubWriterConfiguration { + public static final String DEFAULT_CONTENT_DIRECTORY_NAME = "OEBPS"; + + private String contentDirectoryName = DEFAULT_CONTENT_DIRECTORY_NAME; + + /** + * Creates a default configuration. + */ + public EpubWriterConfiguration() { + } + + /** + * Builder-style method to change the directory name. + * + * @param contentDirectoryName New directory name. + * @return EpubWriterConfiguration + */ + public EpubWriterConfiguration withContentDirectoryName(String contentDirectoryName) { + this.contentDirectoryName = contentDirectoryName; + return this; + } + + /** + * Returns the directory name for the content directory. + * + * @return The directory name for the content directory. + */ + public String getContentDirectoryName() { + return contentDirectoryName; + } + + /** + * Sets the directory name for the content directory. + * + * @param contentDirectoryName The directory name for the content directory. + */ + public void setContentDirectoryName(String contentDirectoryName) { + this.contentDirectoryName = contentDirectoryName; + } +} diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/ContainerNamespaceContext.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/ContainerNamespaceContext.java new file mode 100644 index 00000000..74909fda --- /dev/null +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/ContainerNamespaceContext.java @@ -0,0 +1,39 @@ +package nl.siegmann.epublib.epub; + +import javax.xml.namespace.NamespaceContext; +import java.util.Iterator; + +import static java.util.Collections.singleton; +import static javax.xml.XMLConstants.*; + +class ContainerNamespaceContext implements NamespaceContext { + public static final String XMLNS_CONTAINER = "urn:oasis:names:tc:opendocument:xmlns:container"; + private static final String XMLNS_CONTAINER_PREFIX = "container"; + + @Override + public String getNamespaceURI(String prefix) { + if (prefix == null) throw new IllegalArgumentException(); + switch (prefix) { + case XMLNS_CONTAINER_PREFIX: return XMLNS_CONTAINER; + case XML_NS_PREFIX: return XML_NS_URI; + case XMLNS_ATTRIBUTE: return XMLNS_ATTRIBUTE_NS_URI; + default: return NULL_NS_URI; + } + } + + @Override + public String getPrefix(String namespaceURI) { + if (namespaceURI == null) throw new IllegalArgumentException(); + switch (namespaceURI) { + case "urn:oasis:names:tc:opendocument:xmlns:container": return XMLNS_CONTAINER_PREFIX; + case XML_NS_URI: return XML_NS_PREFIX; + case XMLNS_ATTRIBUTE_NS_URI: return XMLNS_ATTRIBUTE; + default: return null; + } + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return singleton(getPrefix(namespaceURI)).iterator(); + } +} diff --git a/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterConfigurabilityTest.java b/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterConfigurabilityTest.java new file mode 100644 index 00000000..1776691e --- /dev/null +++ b/epublib-core/src/test/java/nl/siegmann/epublib/epub/EpubWriterConfigurabilityTest.java @@ -0,0 +1,129 @@ +package nl.siegmann.epublib.epub; + +import net.sf.jazzlib.ZipEntry; +import net.sf.jazzlib.ZipInputStream; +import nl.siegmann.epublib.domain.Book; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.w3c.dom.bootstrap.DOMImplementationRegistry; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSInput; +import org.w3c.dom.ls.LSParser; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableSet; +import static org.junit.Assert.fail; + +/** + * Unit tests for the configurability of the {@link EpubWriter} using {@link EpubWriterConfiguration}. + */ +public class EpubWriterConfigurabilityTest { + + /** + * Tests that the behavior of {@link EpubWriter} without configuration uses a default configuration. + * The default configuration must result in an unmodified behavior of the {@link EpubWriter}. + */ + @Test + public void regressionTestDirectoryName() throws IOException, XPathExpressionException { + Book book = new Book(); + EpubWriter epubWriter = new EpubWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + epubWriter.write(book, out); + + assertZipFileContainsEntries(out.toByteArray(), "mimetype", "META-INF/container.xml", "OEBPS/toc.ncx", "OEBPS/content.opf"); + assertEpubIncludesContainerEntries(out.toByteArray(), "OEBPS/content.opf"); + } + + /** + * Tests that the behavior of {@link EpubWriter} with a configuration allows changing the content directory name. + */ + @Test + public void testConfigureContentDirectoryName() throws IOException, XPathExpressionException { + Book book = new Book(); + EpubWriter epubWriter = new EpubWriter(new EpubWriterConfiguration().withContentDirectoryName("OPS")); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + epubWriter.write(book, out); + + assertZipFileContainsEntries(out.toByteArray(), "mimetype", "META-INF/container.xml", "OPS/toc.ncx", "OPS/content.opf"); + assertEpubIncludesContainerEntries(out.toByteArray(), "OPS/content.opf"); + } + + private static void assertZipFileContainsEntries(byte[] zipFileData, String... expectedEntries) throws IOException { + assertZipFileContainsEntries(new ByteArrayInputStream(zipFileData), expectedEntries); + } + + private static void assertZipFileContainsEntries(InputStream in, String... expectedEntries) throws IOException { + ZipInputStream zipInputStream = new ZipInputStream(in); + Set actualNames = new HashSet<>(); + Set expectedNames = setOf(expectedEntries); + for (ZipEntry zipEntry; (zipEntry = zipInputStream.getNextEntry()) != null; ) + actualNames.add(zipEntry.getName()); + assertContainsAll(expectedNames, actualNames); + } + + private static void assertContainsAll(Set expected, Set actual) { + Set missing = new HashSet<>(expected); + missing.removeAll(actual); + if (!missing.isEmpty()) + fail("Expected set " + actual + " to contain all elements from set " + actual + " but was missing he following elements: " + missing); + } + + private static void assertEpubIncludesContainerEntries(byte[] zipFileData, String... expectedContainerEntries) throws IOException, XPathExpressionException { + assertEpubIncludesContainerEntries(new ByteArrayInputStream(zipFileData), expectedContainerEntries); + } + + private static void assertEpubIncludesContainerEntries(InputStream in, String... expectedContainerEntries) throws IOException, XPathExpressionException { + ZipInputStream zipInputStream = new ZipInputStream(in); + for (ZipEntry zipEntry; (zipEntry = zipInputStream.getNextEntry()) != null; ) + if ("META-INF/container.xml".equals(zipEntry.getName())) { + assertIncludesContainerEntries(zipInputStream, expectedContainerEntries); + return; + } + fail("Could not find META-INF/container.xml"); + } + + private static void assertIncludesContainerEntries(InputStream zipInputStream, String... expectedContainerEntries) throws XPathExpressionException { + Document doc = readDocument(zipInputStream); + XPath xPath = XPathFactory.newInstance().newXPath(); + xPath.setNamespaceContext(new ContainerNamespaceContext()); + + NodeList nodeList = (NodeList) xPath.evaluate("/container:container/container:rootfiles/container:rootfile/@full-path", doc, XPathConstants.NODESET); + Set actualPaths = new HashSet<>(); + for (int i = 0; i < nodeList.getLength(); i++) + actualPaths.add(nodeList.item(i).getNodeValue()); + assertContainsAll(setOf(expectedContainerEntries), actualPaths); + } + + private static Document readDocument(InputStream in) { + DOMImplementationLS domImplementationLS = (DOMImplementationLS) mustGetRegistry().getDOMImplementation("LS"); + LSInput lsInput = domImplementationLS.createLSInput(); + lsInput.setByteStream(in); + LSParser lsParser = domImplementationLS.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null); + return lsParser.parse(lsInput); + } + + private static DOMImplementationRegistry mustGetRegistry() { + try { + return DOMImplementationRegistry.newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new AssertionError("Could not initialize DOMImplementationRegistry", e); + } + } + + @SafeVarargs + private static Set setOf(T... elements) { + return unmodifiableSet(new HashSet<>(asList(elements))); + } +}