diff options
| author | Michael Williamson <mike@zwobble.org> | 2016-02-24 08:16:16 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2016-02-24 08:16:16 +0000 |
| commit | 3e2786a004eddf968a5fa7d5a92dff5b1b5f7734 (patch) | |
| tree | 7fa1bcc1e484cd0ef782f64baf504ee83333d397 /src/main/java/org/zwobble/mammoth/internal/docx | |
| parent | 0dc3a12affc2e46c1a0df2899cf51a1ae3fdbd61 (diff) | |
Create internal package
Diffstat (limited to 'src/main/java/org/zwobble/mammoth/internal/docx')
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(); + } +} |
