aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/zwobble/mammoth/internal/docx
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2016-02-24 08:16:16 +0000
committerMichael Williamson <mike@zwobble.org>2016-02-24 08:16:16 +0000
commit3e2786a004eddf968a5fa7d5a92dff5b1b5f7734 (patch)
tree7fa1bcc1e484cd0ef782f64baf504ee83333d397 /src/main/java/org/zwobble/mammoth/internal/docx
parent0dc3a12affc2e46c1a0df2899cf51a1ae3fdbd61 (diff)
Create internal package
Diffstat (limited to 'src/main/java/org/zwobble/mammoth/internal/docx')
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java329
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java45
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java38
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java115
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java23
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java15
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java8
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java46
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java55
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java23
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java56
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java58
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java91
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java13
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java20
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java22
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Styles.java28
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java33
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java30
19 files changed, 1048 insertions, 0 deletions
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java
new file mode 100644
index 0000000..43a275a
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java
@@ -0,0 +1,329 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableSet;
+import org.zwobble.mammoth.internal.documents.*;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.results.Warning;
+import org.zwobble.mammoth.internal.util.InputStreamSupplier;
+import org.zwobble.mammoth.internal.util.MammothOptionals;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementLike;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+import org.zwobble.mammoth.internal.xml.XmlNode;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+import static org.zwobble.mammoth.internal.docx.ReadResult.EMPTY_SUCCESS;
+import static org.zwobble.mammoth.internal.docx.ReadResult.success;
+import static org.zwobble.mammoth.results.Warning.warning;
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+import static org.zwobble.mammoth.internal.util.MammothStrings.trimLeft;
+
+public class BodyXmlReader {
+ private static final Set<String> IMAGE_TYPES_SUPPORTED_BY_BROWSERS = ImmutableSet.of(
+ "image/png", "image/gif", "image/jpeg", "image/svg+xml", "image/tiff");
+
+ private final Styles styles;
+ private final Numbering numbering;
+ private final Relationships relationships;
+ private final ContentTypes contentTypes;
+ private final DocxFile file;
+ private final FileReader fileReader;
+
+ public BodyXmlReader(
+ Styles styles,
+ Numbering numbering,
+ Relationships relationships,
+ ContentTypes contentTypes,
+ DocxFile file,
+ FileReader fileReader)
+ {
+ this.styles = styles;
+ this.numbering = numbering;
+ this.relationships = relationships;
+ this.contentTypes = contentTypes;
+ this.file = file;
+ this.fileReader = fileReader;
+ }
+
+ public ReadResult readElement(XmlElement element) {
+ switch (element.getName()) {
+ case "w:t":
+ return success(new Text(element.innerText()));
+ case "w:r":
+ return readRun(element);
+ case "w:p":
+ return readParagraph(element);
+
+ case "w:tab":
+ return success(Tab.TAB);
+ case "w:br":
+ return readBreak(element);
+
+ case "w:tbl":
+ return readElements(element.children()).map(Table::new);
+ case "w:tr":
+ return readElements(element.children()).map(TableRow::new);
+ case "w:tc":
+ return readElements(element.children()).map(TableCell::new);
+
+ case "w:hyperlink":
+ return readHyperlink(element);
+ case "w:bookmarkStart":
+ return readBookmark(element);
+ case "w:footnoteReference":
+ return readNoteReference(NoteType.FOOTNOTE, element);
+ case "w:endnoteReference":
+ return readNoteReference(NoteType.ENDNOTE, element);
+
+ case "w:pict":
+ return readPict(element);
+
+ case "v:imagedata":
+ return readImagedata(element);
+
+ case "wp:inline":
+ case "wp:anchor":
+ return readInline(element);
+
+ case "w:ins":
+ case "w:smartTag":
+ case "w:drawing":
+ case "v:roundrect":
+ case "v:shape":
+ case "v:textbox":
+ case "w:txbxContent":
+ return readElements(element.children());
+
+ case "office-word:wrap":
+ case "v:shadow":
+ case "v:shapetype":
+ case "w:bookmarkEnd":
+ case "w:sectPr":
+ case "w:proofErr":
+ case "w:lastRenderedPageBreak":
+ case "w:commentRangeStart":
+ case "w:commentRangeEnd":
+ case "w:commentReference":
+ case "w:del":
+ case "w:footnoteRef":
+ case "w:endnoteRef":
+ case "w:pPr":
+ case "w:rPr":
+ case "w:tblPr":
+ case "w:tblGrid":
+ case "w:tcPr":
+ return EMPTY_SUCCESS;
+
+ default:
+ Warning warning = warning("An unrecognised element was ignored: " + element.getName());
+ return ReadResult.emptyWithWarning(warning);
+ }
+ }
+
+ private ReadResult readRun(XmlElement element) {
+ XmlElementLike properties = element.findChildOrEmpty("w:rPr");
+ return ReadResult.map(
+ readRunStyle(properties),
+ readElements(element.children()),
+ (style, children) -> new Run(
+ isBold(properties),
+ isItalic(properties),
+ isUnderline(properties),
+ isStrikethrough(properties),
+ readVerticalAlignment(properties),
+ style,
+ children));
+ }
+
+ private boolean isBold(XmlElementLike properties) {
+ return properties.hasChild("w:b");
+ }
+
+ private boolean isItalic(XmlElementLike properties) {
+ return properties.hasChild("w:i");
+ }
+
+ private boolean isUnderline(XmlElementLike properties) {
+ return properties.hasChild("w:u");
+ }
+
+ private boolean isStrikethrough(XmlElementLike properties) {
+ return properties.hasChild("w:strike");
+ }
+
+ private VerticalAlignment readVerticalAlignment(XmlElementLike properties) {
+ String verticalAlignment = readVal(properties, "w:vertAlign").orElse("");
+ switch (verticalAlignment) {
+ case "superscript":
+ return VerticalAlignment.SUPERSCRIPT;
+ case "subscript":
+ return VerticalAlignment.SUBSCRIPT;
+ default:
+ // TODO: warn if set?
+ return VerticalAlignment.BASELINE;
+ }
+ }
+
+ private Result<Optional<Style>> readRunStyle(XmlElementLike properties) {
+ return readStyle(properties, "w:rStyle", "Run", styles::findCharacterStyleById);
+ }
+
+ public ReadResult readElements(Iterable<XmlNode> nodes) {
+ return ReadResult.concat(
+ transform(
+ filter(nodes, XmlElement.class),
+ this::readElement));
+ }
+
+ private ReadResult readParagraph(XmlElement element) {
+ XmlElementLike properties = element.findChildOrEmpty("w:pPr");
+ Optional<NumberingLevel> numbering = readNumbering(properties);
+ return ReadResult.map(
+ readParagraphStyle(properties),
+ readElements(element.children()),
+ (style, children) -> new Paragraph(style, numbering, children)).appendExtra();
+ }
+
+ private Result<Optional<Style>> readParagraphStyle(XmlElementLike properties) {
+ return readStyle(properties, "w:pStyle", "Paragraph", styles::findParagraphStyleById);
+ }
+
+ private Result<Optional<Style>> readStyle(
+ XmlElementLike properties,
+ String styleTagName,
+ String styleType,
+ Function<String, Optional<Style>> findStyleById)
+ {
+ return readVal(properties, styleTagName)
+ .map(styleId -> findStyleById(styleType, styleId, findStyleById))
+ .orElse(Result.empty());
+ }
+
+ private Result<Optional<Style>> findStyleById(
+ String styleType,
+ String styleId,
+ Function<String, Optional<Style>> findStyleById)
+ {
+ Optional<Style> style = findStyleById.apply(styleId);
+ if (style.isPresent()) {
+ return Result.success(style);
+ } else {
+ return new Result<>(
+ Optional.of(new Style(styleId, Optional.empty())),
+ list(warning(styleType + " style with ID " + styleId + " was referenced but not defined in the document")));
+ }
+
+ }
+
+ private Optional<NumberingLevel> readNumbering(XmlElementLike properties) {
+ XmlElementLike numberingProperties = properties.findChildOrEmpty("w:numPr");
+ return MammothOptionals.flatMap(
+ readVal(numberingProperties, "w:numId"),
+ readVal(numberingProperties, "w:ilvl"),
+ numbering::findLevel);
+ }
+
+ private ReadResult readBreak(XmlElement element) {
+ String breakType = element.getAttributeOrNone("w:type").orElse("");
+ if (breakType.equals("")) {
+ return success(LineBreak.LINE_BREAK);
+ } else {
+ return ReadResult.emptyWithWarning(warning("Unsupported break type: " + breakType));
+ }
+ }
+
+ private ReadResult readHyperlink(XmlElement element) {
+ Optional<String> relationshipId = element.getAttributeOrNone("r:id");
+ Optional<String> anchor = element.getAttributeOrNone("w:anchor");
+ ReadResult childrenResult = readElements(element.children());
+ if (relationshipId.isPresent()) {
+ return childrenResult.map(children -> Hyperlink.href(
+ relationships.findRelationshipById(relationshipId.get()).getTarget(), children));
+ } else if (anchor.isPresent()) {
+ return childrenResult.map(children -> Hyperlink.anchor(
+ anchor.get(), children));
+ } else {
+ return childrenResult;
+ }
+ }
+
+ private ReadResult readBookmark(XmlElement element) {
+ String name = element.getAttribute("w:name");
+ if (name.equals("_GoBack")) {
+ return ReadResult.EMPTY_SUCCESS;
+ } else {
+ return success(new Bookmark(name));
+ }
+ }
+
+ private ReadResult readNoteReference(NoteType noteType, XmlElement element) {
+ String noteId = element.getAttribute("w:id");
+ return success(new NoteReference(noteType, noteId));
+ }
+
+ private ReadResult readPict(XmlElement element) {
+ return readElements(element.children()).toExtra();
+ }
+
+ private ReadResult readImagedata(XmlElement element) {
+ Optional<String> title = element.getAttributeOrNone("o:title");
+ String relationshipId = element.getAttribute("r:id");
+ String imagePath = relationshipIdToDocxPath(relationshipId);
+ return readImage(imagePath, title, () -> DocxFile.getInputStream(file, imagePath));
+ }
+
+ private ReadResult readInline(XmlElement element) {
+ Optional<String> altText = element.findChildOrEmpty("wp:docPr").getAttributeOrNone("descr");
+ XmlElementList blips = element.findChildren("a:graphic")
+ .findChildren("a:graphicData")
+ .findChildren("pic:pic")
+ .findChildren("pic:blipFill")
+ .findChildren("a:blip");
+ return readBlips(blips, altText);
+ }
+
+ private ReadResult readBlips(XmlElementList blips, Optional<String> altText) {
+ return ReadResult.concat(transform(blips, blip -> readBlip(blip, altText)));
+ }
+
+ private ReadResult readBlip(XmlElement blip, Optional<String> altText) {
+ Optional<String> embedRelationshipId = blip.getAttributeOrNone("r:embed");
+ Optional<String> linkRelationshipId = blip.getAttributeOrNone("r:link");
+ if (embedRelationshipId.isPresent()) {
+ String imagePath = relationshipIdToDocxPath(embedRelationshipId.get());
+ return readImage(imagePath, altText, () -> DocxFile.getInputStream(file, imagePath));
+ } else if (linkRelationshipId.isPresent()) {
+ String imagePath = relationships.findRelationshipById(linkRelationshipId.get()).getTarget();
+ return readImage(imagePath, altText, () -> fileReader.getInputStream(imagePath));
+ } else {
+ // TODO: emit warning
+ return ReadResult.EMPTY_SUCCESS;
+ }
+ }
+
+ private ReadResult readImage(String imagePath, Optional<String> altText, InputStreamSupplier open) {
+ Optional<String> contentType = contentTypes.findContentType(imagePath);
+ Image image = new Image(altText, contentType, open);
+
+ String contentTypeString = contentType.orElse("(unknown)");
+ if (IMAGE_TYPES_SUPPORTED_BY_BROWSERS.contains(contentTypeString)) {
+ return success(image);
+ } else {
+ return ReadResult.withWarning(image, warning("Image of type " + contentTypeString + " is unlikely to display in web browsers"));
+ }
+ }
+
+ private String relationshipIdToDocxPath(String relationshipId) {
+ Relationship relationship = relationships.findRelationshipById(relationshipId);
+ return "word/" + trimLeft(relationship.getTarget(), '/');
+ }
+
+ private Optional<String> readVal(XmlElementLike element, String name) {
+ return element.findChildOrEmpty(name).getAttributeOrNone("w:val");
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java
new file mode 100644
index 0000000..ce4ebac
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java
@@ -0,0 +1,45 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class ContentTypes {
+ public static final ContentTypes DEFAULT = new ContentTypes(map(), map());
+
+ private static final Map<String, String> imageExtensions = ImmutableMap.<String, String>builder()
+ .put("png", "png")
+ .put("gif", "gif")
+ .put("jpeg", "jpeg")
+ .put("jpg", "jpeg")
+ .put("bmp", "bmp")
+ .put("tif", "tiff")
+ .put("tiff", "tiff")
+ .build();
+
+ private final Map<String, String> extensionDefaults;
+ private final Map<String, String> overrides;
+
+ public ContentTypes(Map<String, String> extensionDefaults, Map<String, String> overrides) {
+ this.extensionDefaults = extensionDefaults;
+ this.overrides = overrides;
+ }
+
+ public Optional<String> findContentType(String path) {
+ if (overrides.containsKey(path)) {
+ return Optional.ofNullable(overrides.get(path));
+ } else {
+ String extension = Files.getFileExtension(path);
+ if (extensionDefaults.containsKey(extension)) {
+ return Optional.ofNullable(extensionDefaults.get(extension));
+ } else {
+ return Optional.ofNullable(imageExtensions.get(extension.toLowerCase()))
+ .map(imageType -> "image/" + imageType);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java
new file mode 100644
index 0000000..4a7d3bd
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java
@@ -0,0 +1,38 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+import static org.zwobble.mammoth.internal.util.MammothStrings.trimLeft;
+
+public class ContentTypesXml {
+ public static ContentTypes readContentTypesXmlElement(XmlElement element) {
+ return new ContentTypes(
+ readDefaults(element.findChildren("content-types:Default")),
+ readOverrides(element.findChildren("content-types:Override")));
+ }
+
+ private static Map<String, String> readDefaults(XmlElementList children) {
+ return toMap(children, ContentTypesXml::readDefault);
+ }
+
+ private static Map.Entry<String, String> readDefault(XmlElement element) {
+ return immutableEntry(
+ element.getAttribute("Extension"),
+ element.getAttribute("ContentType"));
+ }
+
+ private static Map<String, String> readOverrides(XmlElementList children) {
+ return toMap(children, ContentTypesXml::readOverride);
+ }
+
+ private static Map.Entry<String, String> readOverride(XmlElement element) {
+ return immutableEntry(
+ trimLeft(element.getAttribute("PartName"), '/'),
+ element.getAttribute("ContentType"));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java
new file mode 100644
index 0000000..4341bbf
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java
@@ -0,0 +1,115 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Document;
+import org.zwobble.mammoth.internal.documents.Notes;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.internal.util.MammothLists;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class DocumentReader {
+ @FunctionalInterface
+ private interface BodyReaders {
+ BodyXmlReader forName(String name);
+ }
+
+ public static Result<Document> readDocument(Optional<Path> path, DocxFile zipFile) {
+ Styles styles = readStyles(zipFile);
+ Numbering numbering = readNumbering(zipFile);
+ ContentTypes contentTypes = readContentTypes(zipFile);
+ FileReader fileReader = uri -> {
+ try {
+ Optional<URI> absoluteUri = asAbsoluteUri(uri);
+ if (absoluteUri.isPresent()) {
+ return open(absoluteUri.get());
+ } else if (path.isPresent()) {
+ return open(path.get().toUri().resolve(uri));
+ } else {
+ throw new IOException("path of document is unknown, but is required for relative URI");
+ }
+ } catch (IOException exception) {
+ throw new IOException("could not open external image '" + uri + "': " + exception.getMessage());
+ }
+ };
+ BodyReaders bodyReaders = name -> {
+ Relationships relationships = readRelationships(zipFile, name);
+ return new BodyXmlReader(styles, numbering, relationships, contentTypes, zipFile, fileReader);
+ };
+ return readNotes(zipFile, bodyReaders)
+ .flatMap(notes -> {
+ DocumentXmlReader reader = new DocumentXmlReader(bodyReaders.forName("document"), notes);
+ return reader.readElement(parseOfficeXml(zipFile, "word/document.xml"));
+ });
+ }
+
+ private static InputStream open(URI uri) throws IOException {
+ return uri.toURL().openStream();
+ }
+
+ private static Optional<URI> asAbsoluteUri(String uriString) {
+ try {
+ URI uri = new URI(uriString);
+ return uri.isAbsolute() ? Optional.of(uri) : Optional.empty();
+ } catch (URISyntaxException exception) {
+ return Optional.empty();
+ }
+ }
+
+ private static Result<Notes> readNotes(DocxFile file, BodyReaders bodyReaders) {
+ return Result.map(
+ tryParseOfficeXml(file, "word/footnotes.xml")
+ .map(NotesXmlReader.footnote(bodyReaders.forName("footnotes"))::readElement)
+ .orElse(Result.success(list())),
+ tryParseOfficeXml(file, "word/endnotes.xml")
+ .map(NotesXmlReader.endnote(bodyReaders.forName("endnotes"))::readElement)
+ .orElse(Result.success(list())),
+ MammothLists::concat).map(Notes::new);
+ }
+
+ private static Styles readStyles(DocxFile file) {
+ return tryParseOfficeXml(file, "word/styles.xml")
+ .map(StylesXml::readStylesXmlElement)
+ .orElse(Styles.EMPTY);
+ }
+
+ private static Numbering readNumbering(DocxFile file) {
+ return tryParseOfficeXml(file, "word/numbering.xml")
+ .map(NumberingXml::readNumberingXmlElement)
+ .orElse(Numbering.EMPTY);
+ }
+
+ private static ContentTypes readContentTypes(DocxFile file) {
+ return tryParseOfficeXml(file, "[Content_Types].xml")
+ .map(ContentTypesXml::readContentTypesXmlElement)
+ .orElse(ContentTypes.DEFAULT);
+ }
+
+ private static Relationships readRelationships(DocxFile zipFile, String name) {
+ return tryParseOfficeXml(zipFile, "word/_rels/" + name + ".xml.rels")
+ .map(RelationshipsXml::readRelationshipsXmlElement)
+ .orElse(Relationships.EMPTY);
+ }
+
+ private static Optional<XmlElement> tryParseOfficeXml(DocxFile zipFile, String name) {
+ try {
+ return zipFile.tryGetInputStream(name).map(OfficeXml::parseXml);
+ } catch (IOException exception) {
+ // TODO: wrap in more specific exception and catch at the top
+ throw new RuntimeException(exception);
+ }
+ }
+
+ private static XmlElement parseOfficeXml(DocxFile zipFile, String name) {
+ return tryParseOfficeXml(zipFile, name)
+ // TODO: wrap in more specific exception and catch at the top
+ .orElseThrow(() -> new RuntimeException(new IOException("Missing entry in file: " + name)));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java
new file mode 100644
index 0000000..3c215fe
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java
@@ -0,0 +1,23 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Document;
+import org.zwobble.mammoth.internal.documents.Notes;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+public class DocumentXmlReader {
+ private final BodyXmlReader bodyReader;
+ private final Notes notes;
+
+ public DocumentXmlReader(BodyXmlReader bodyReader, Notes notes) {
+ this.bodyReader = bodyReader;
+ this.notes = notes;
+ }
+
+ public Result<Document> readElement(XmlElement element) {
+ XmlElement body = element.findChild("w:body");
+ return bodyReader.readElements(body.children())
+ .toResult()
+ .map(children -> new Document(children, notes));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java
new file mode 100644
index 0000000..8e73a04
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java
@@ -0,0 +1,15 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+public interface DocxFile extends Closeable {
+ static InputStream getInputStream(DocxFile file, String name) throws IOException {
+ return file.tryGetInputStream(name)
+ .orElseThrow(() -> new IOException("Missing entry in file: " + name));
+ }
+
+ Optional<InputStream> tryGetInputStream(String name) throws IOException;
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java
new file mode 100644
index 0000000..f25eb32
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java
@@ -0,0 +1,8 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface FileReader {
+ InputStream getInputStream(String uri) throws IOException;
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java b/src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java
new file mode 100644
index 0000000..7229ad5
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java
@@ -0,0 +1,46 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.io.ByteStreams;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class InMemoryDocxFile implements DocxFile {
+ public static DocxFile fromStream(InputStream stream) throws IOException {
+ ZipInputStream zipStream = new ZipInputStream(stream);
+ ImmutableMap.Builder<String, byte[]> entries = ImmutableMap.builder();
+ ZipEntry entry;
+ while ((entry = zipStream.getNextEntry()) != null) {
+ entries.put(entry.getName(), ByteStreams.toByteArray(zipStream));
+ }
+ return new InMemoryDocxFile(entries.build());
+ }
+
+ public static DocxFile fromStrings(Map<String, String> entries) {
+ return new InMemoryDocxFile(Maps.transformValues(entries, value -> value.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ private final Map<String, byte[]> entries;
+
+ public InMemoryDocxFile(Map<String, byte[]> entries) {
+ this.entries = entries;
+ }
+
+ @Override
+ public Optional<InputStream> tryGetInputStream(String name) throws IOException {
+ return Optional.ofNullable(entries.get(name))
+ .map(ByteArrayInputStream::new);
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java
new file mode 100644
index 0000000..2e31aa2
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java
@@ -0,0 +1,55 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.NoteType;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.internal.documents.Note;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+import java.util.List;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+
+public class NotesXmlReader {
+ public static NotesXmlReader footnote(BodyXmlReader bodyReader) {
+ return new NotesXmlReader(bodyReader, "footnote", NoteType.FOOTNOTE);
+ }
+
+ public static NotesXmlReader endnote(BodyXmlReader bodyReader) {
+ return new NotesXmlReader(bodyReader, "endnote", NoteType.ENDNOTE);
+ }
+
+ private final BodyXmlReader bodyReader;
+ private final String tagName;
+ private final NoteType noteType;
+
+ private NotesXmlReader(BodyXmlReader bodyReader, String tagName, NoteType noteType) {
+ this.bodyReader = bodyReader;
+ this.tagName = tagName;
+ this.noteType = noteType;
+ }
+
+ public Result<List<Note>> readElement(XmlElement element) {
+ Iterable<XmlElement> elements = filter(element.findChildren("w:" + tagName), this::isNoteElement);
+ return Result.concat(transform(elements, this::readNoteElement));
+ }
+
+ private boolean isNoteElement(XmlElement element) {
+ return element.getAttributeOrNone("w:type")
+ .map(type -> !isSeparatorType(type))
+ .orElse(true);
+ }
+
+ private boolean isSeparatorType(String type) {
+ return type.equals("continuationSeparator") || type.equals("separator");
+ }
+
+ private Result<Note> readNoteElement(XmlElement element) {
+ return bodyReader.readElements(element.children())
+ .toResult()
+ .map(children -> new Note(
+ noteType,
+ element.getAttribute("w:id"),
+ children));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java b/src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java
new file mode 100644
index 0000000..f61b5db
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java
@@ -0,0 +1,23 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.NumberingLevel;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Numbering {
+ public static final Numbering EMPTY = new Numbering(map());
+
+ private final Map<String, Map<String, NumberingLevel>> numbering;
+
+ public Numbering(Map<String, Map<String, NumberingLevel>> numbering) {
+ this.numbering = numbering;
+ }
+
+ public Optional<NumberingLevel> findLevel(String numId, String level) {
+ return Optional.ofNullable(numbering.get(numId))
+ .flatMap(levels -> Optional.ofNullable(levels.get(level)));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java
new file mode 100644
index 0000000..0ee480d
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java
@@ -0,0 +1,56 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.NumberingLevel;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+
+public class NumberingXml {
+ public static Numbering readNumberingXmlElement(XmlElement element) {
+ Map<String, Map<String, NumberingLevel>> abstractNums = readAbstractNums(element.findChildren("w:abstractNum"));
+ return new Numbering(readNums(element.findChildren("w:num"), abstractNums));
+ }
+
+ private static Map<String, Map<String, NumberingLevel>> readAbstractNums(XmlElementList children) {
+ return toMap(children, NumberingXml::readAbstractNum);
+ }
+
+ private static Map.Entry<String, Map<String, NumberingLevel>> readAbstractNum(XmlElement element) {
+ // TODO: in python-mammoth, we allow None here. Check whether that's actually possible or not
+ String abstractNumId = element.getAttribute("w:abstractNumId");
+ return immutableEntry(abstractNumId, readAbstractNumLevels(element));
+ }
+
+ private static Map<String, NumberingLevel> readAbstractNumLevels(XmlElement element) {
+ return toMap(element.findChildren("w:lvl"), NumberingXml::readAbstractNumLevel);
+ }
+
+ private static Map.Entry<String, NumberingLevel> readAbstractNumLevel(XmlElement element) {
+ String levelIndex = element.getAttribute("w:ilvl");
+ Optional<String> numFmt = element.findChildOrEmpty("w:numFmt").getAttributeOrNone("w:val");
+ boolean isOrdered = !numFmt.equals(Optional.of("bullet"));
+ return immutableEntry(levelIndex, new NumberingLevel(levelIndex, isOrdered));
+ }
+
+ private static Map<String, Map<String, NumberingLevel>> readNums(
+ XmlElementList numElements,
+ Map<String, Map<String, NumberingLevel>> abstractNums
+ ) {
+ return toMap(numElements, numElement -> readNum(numElement, abstractNums));
+ }
+
+ private static Map.Entry<String,Map<String,NumberingLevel>> readNum(
+ XmlElement numElement,
+ Map<String, Map<String, NumberingLevel>> abstractNums
+ ) {
+ // TODO: in python-mammoth, we allow None here. Check whether that's actually possible or not
+ String numId = numElement.getAttribute("w:numId");
+ String abstractNumId = numElement.findChild("w:abstractNumId").getAttribute("w:val");
+ return immutableEntry(numId, Optional.ofNullable(abstractNums.get(abstractNumId)).get());
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java
new file mode 100644
index 0000000..2b5c279
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java
@@ -0,0 +1,58 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlNode;
+import org.zwobble.mammoth.internal.xml.XmlNodeVisitor;
+import org.zwobble.mammoth.internal.xml.XmlTextNode;
+import org.zwobble.mammoth.internal.xml.parsing.XmlParser;
+
+import java.io.InputStream;
+import java.util.List;
+
+import static com.google.common.collect.Iterables.concat;
+import static com.google.common.collect.Iterables.transform;
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class OfficeXml {
+ private static ImmutableBiMap<String, String> XML_NAMESPACES = ImmutableBiMap.<String, String>builder()
+ .put("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main")
+ .put("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing")
+ .put("a", "http://schemas.openxmlformats.org/drawingml/2006/main")
+ .put("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture")
+ .put("content-types", "http://schemas.openxmlformats.org/package/2006/content-types")
+ .put("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships")
+ .put("relationships", "http://schemas.openxmlformats.org/package/2006/relationships")
+ .put("v", "urn:schemas-microsoft-com:vml")
+ .put("mc", "http://schemas.openxmlformats.org/markup-compatibility/2006")
+ .put("office-word", "urn:schemas-microsoft-com:office:word")
+ .build();
+
+ public static XmlElement parseXml(InputStream inputStream) {
+ XmlParser parser = new XmlParser(XML_NAMESPACES);
+ return (XmlElement)collapseAlternateContent(parser.parseStream(inputStream)).get(0);
+ }
+
+ private static List<XmlNode> collapseAlternateContent(XmlNode node) {
+ return node.accept(new XmlNodeVisitor<List<XmlNode>>() {
+ @Override
+ public List<XmlNode> visit(XmlElement element) {
+ if (element.getName().equals("mc:AlternateContent")) {
+ return element.findChild("mc:Fallback").getChildren();
+ } else {
+ XmlElement collapsedElement = new XmlElement(
+ element.getName(),
+ element.getAttributes(),
+ ImmutableList.copyOf(concat(transform(element.children(), OfficeXml::collapseAlternateContent))));
+ return list(collapsedElement);
+ }
+ }
+
+ @Override
+ public List<XmlNode> visit(XmlTextNode textNode) {
+ return list(textNode);
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java b/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java
new file mode 100644
index 0000000..366fd12
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java
@@ -0,0 +1,91 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableList;
+import org.zwobble.mammoth.internal.documents.DocumentElement;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.results.Warning;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class ReadResult {
+ public static final ReadResult EMPTY_SUCCESS = success(list());
+
+ public static ReadResult concat(Iterable<ReadResult> results) {
+ ImmutableList.Builder<DocumentElement> elements = ImmutableList.builder();
+ ImmutableList.Builder<DocumentElement> extra = ImmutableList.builder();
+ ImmutableList.Builder<Warning> warnings = ImmutableList.builder();
+ for (ReadResult result : results) {
+ elements.addAll(result.elements);
+ extra.addAll(result.extra);
+ warnings.addAll(result.warnings);
+ }
+ return new ReadResult(elements.build(), extra.build(), warnings.build());
+ }
+
+ public static <T> ReadResult map(
+ Result<T> first,
+ ReadResult second,
+ BiFunction<T, List<DocumentElement>, DocumentElement> function)
+ {
+ return new ReadResult(
+ list(function.apply(first.getValue(), second.elements)),
+ second.extra,
+ concat(first.getWarnings(), second.warnings));
+ }
+
+ public static ReadResult success(DocumentElement element) {
+ return success(list(element));
+ }
+
+ public static ReadResult success(List<DocumentElement> elements) {
+ return new ReadResult(elements, list(), list());
+ }
+
+ public static ReadResult emptyWithWarning(Warning warning) {
+ return new ReadResult(list(), list(), list(warning));
+ }
+
+ public static ReadResult withWarning(DocumentElement element, Warning warning) {
+ return new ReadResult(list(element), list(), list(warning));
+ }
+
+ private final List<DocumentElement> elements;
+ private final List<DocumentElement> extra;
+ private final List<Warning> warnings;
+
+ public ReadResult(List<DocumentElement> elements, List<DocumentElement> extra, List<Warning> warnings) {
+ this.elements = elements;
+ this.extra = extra;
+ this.warnings = warnings;
+ }
+
+ public ReadResult map(Function<List<DocumentElement>, DocumentElement> function) {
+ return new ReadResult(
+ list(function.apply(elements)),
+ extra,
+ warnings);
+ }
+
+ public ReadResult toExtra() {
+ return new ReadResult(list(), concat(extra, elements), warnings);
+ }
+
+ public ReadResult appendExtra() {
+ return new ReadResult(concat(elements, extra), list(), warnings);
+ }
+
+ public Result<List<DocumentElement>> toResult() {
+ return new Result<>(elements, warnings);
+ }
+
+ private static <T> List<T> concat(List<T> first, List<T> second) {
+ ImmutableList.Builder<T> builder = ImmutableList.builder();
+ builder.addAll(first);
+ builder.addAll(second);
+ return builder.build();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java b/src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java
new file mode 100644
index 0000000..dacd7a6
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java
@@ -0,0 +1,13 @@
+package org.zwobble.mammoth.internal.docx;
+
+public class Relationship {
+ private final String target;
+
+ public Relationship(String target) {
+ this.target = target;
+ }
+
+ public String getTarget() {
+ return target;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java b/src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java
new file mode 100644
index 0000000..d019b57
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java
@@ -0,0 +1,20 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Relationships {
+ public static final Relationships EMPTY = new Relationships(map());
+
+ private final Map<String, Relationship> relationships;
+
+ public Relationships(Map<String, Relationship> relationships) {
+ this.relationships = relationships;
+ }
+
+ public Relationship findRelationshipById(String id) {
+ return Optional.ofNullable(relationships.get(id)).get();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java
new file mode 100644
index 0000000..938776e
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java
@@ -0,0 +1,22 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+
+public class RelationshipsXml {
+ public static Relationships readRelationshipsXmlElement(XmlElement element) {
+ return new Relationships(toMap(
+ element.findChildren("relationships:Relationship"),
+ RelationshipsXml::readRelationship));
+ }
+
+ private static Map.Entry<String, Relationship> readRelationship(XmlElement element) {
+ return immutableEntry(
+ element.getAttribute("Id"),
+ new Relationship(element.getAttribute("Target")));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Styles.java b/src/main/java/org/zwobble/mammoth/internal/docx/Styles.java
new file mode 100644
index 0000000..d5e32e8
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Styles.java
@@ -0,0 +1,28 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Style;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Styles {
+ public static final Styles EMPTY = new Styles(map(), map());
+
+ private final Map<String, Style> paragraphStyles;
+ private final Map<String, Style> characterStyles;
+
+ public Styles(Map<String, Style> paragraphStyles, Map<String, Style> characterStyles) {
+ this.paragraphStyles = paragraphStyles;
+ this.characterStyles = characterStyles;
+ }
+
+ public Optional<Style> findParagraphStyleById(String id) {
+ return Optional.ofNullable(paragraphStyles.get(id));
+ }
+
+ public Optional<Style> findCharacterStyleById(String id) {
+ return Optional.ofNullable(characterStyles.get(id));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java
new file mode 100644
index 0000000..78ba65f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java
@@ -0,0 +1,33 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Style;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+
+public class StylesXml {
+ public static Styles readStylesXmlElement(XmlElement element) {
+ XmlElementList styleElements = element.findChildren("w:style");
+ return new Styles(
+ readStyles(styleElements, "paragraph"),
+ readStyles(styleElements, "character"));
+ }
+
+ private static Map<String, Style> readStyles(XmlElementList styleElements, String styleType) {
+ return toMap(
+ filter(styleElements, styleElement -> styleElement.getAttribute("w:type").equals(styleType)),
+ StylesXml::readStyle);
+ }
+
+ private static Map.Entry<String, Style> readStyle(XmlElement element) {
+ String styleId = element.getAttribute("w:styleId");
+ Optional<String> styleName = element.findChildOrEmpty("w:name").getAttributeOrNone("w:val");
+ return immutableEntry(styleId, new Style(styleId, styleName));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java b/src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java
new file mode 100644
index 0000000..d263578
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java
@@ -0,0 +1,30 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class ZippedDocxFile implements DocxFile {
+ private final ZipFile zipFile;
+
+ public ZippedDocxFile(ZipFile zipFile) {
+ this.zipFile = zipFile;
+ }
+
+ @Override
+ public Optional<InputStream> tryGetInputStream(String name) throws IOException {
+ ZipEntry entry = zipFile.getEntry(name);
+ if (entry == null) {
+ return Optional.empty();
+ } else {
+ return Optional.of(zipFile.getInputStream(entry));
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ zipFile.close();
+ }
+}