Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)));
}
}