Skip to content

Commit

Permalink
Issue psiegman#127 The directory name for content should be configura…
Browse files Browse the repository at this point in the history
…ble.
  • Loading branch information
christianhujer committed Dec 16, 2019
1 parent 5df3765 commit ef9d4fb
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -127,7 +137,7 @@ private void writeContainer(ZipOutputStream resultStream) throws IOException {
out.write("<?xml version=\"1.0\"?>\n");
out.write("<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
out.write("\t<rootfiles>\n");
out.write("\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
out.write("\t\t<rootfile full-path=\""+ configuration.getContentDirectoryName() + "/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
out.write("\t</rootfiles>\n");
out.write("</container>");
out.flush();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> getPrefixes(String namespaceURI) {
return singleton(getPrefix(namespaceURI)).iterator();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> actualNames = new HashSet<>();
Set<String> expectedNames = setOf(expectedEntries);
for (ZipEntry zipEntry; (zipEntry = zipInputStream.getNextEntry()) != null; )
actualNames.add(zipEntry.getName());
assertContainsAll(expectedNames, actualNames);
}

private static <T> void assertContainsAll(Set<T> expected, Set<T> actual) {
Set<T> 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<String> 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 <T> Set<T> setOf(T... elements) {
return unmodifiableSet(new HashSet<>(asList(elements)));
}
}

1 comment on commit ef9d4fb

@christianhujer
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design questions to consider:

  • Should the BookProcessor become part of the EpubWriterConfiguration? I suggest: yes
  • How to deal with EpubWriter.getBookProcessor() and EpubWriter.setBookProcessor()? I suggest: Delegate to EpubWriterConfiguration, deprecate, delete after 2 years.
    I can happily perform these changes if you agree.

Please sign in to comment.