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)));
+ }
+}