diff --git a/README.adoc b/README.adoc index c58dd142..74863142 100644 --- a/README.adoc +++ b/README.adoc @@ -8,7 +8,7 @@ == Introduction -OfficeStamper (formerly Docx-Stamper) is a Java template engine that allows for dynamic creation of docx documents at runtime. +OfficeStamper (formerly Docx-Stamper) is a Java template engine that allows for dynamic creation of DOCX documents at runtime. You design a template using your preferred Word processor; and OfficeStamper will generate documents based on that template. image:{proj}/actions/workflows/integrate-os.yml/badge.svg[Build Status,link={proj}/actions/workflows/integrate-os.yml] image:{proj}/actions/workflows/integrate-docx4j.yml/badge.svg[Build Status,link={proj}/actions/workflows/integrate-docx4j.yml] image:{proj}/actions/workflows/analyze.yml/badge.svg[Build Status,link={proj}/actions/workflows/analyze.yml] image:{proj}/actions/workflows/pages.yml/badge.svg[Build Status,link={proj}/actions/workflows/pages.yml] @@ -54,7 +54,6 @@ image:{proj}/actions/workflows/integrate-os.yml/badge.svg[Build Status,link={pro * *BREAK* Removed DocxDocument.commentsPart method. * *BREAK* DocxPart.streamParagraphs method now returns the Paragraph wrapper, instead of docx4j P. - ==== Tests * Improved test names, @@ -66,20 +65,20 @@ image:{proj}/actions/workflows/integrate-os.yml/badge.svg[Build Status,link={pro == Usage -Here is a simple code snippet exemplifying how to use OfficeStamper: +Here is a code snippet exemplifying how to use OfficeStamper: [source,java] ---- class Example { public static void main(String[] args) { - // your own POJO against which expressions found in the template will be resolved + // a java object to use as context for the expressions found in the template. var context = new YourPojoContext(_, _ , _); // an instance of the stamper var stamper = OfficeStampers.docxStamper(); try( - // Path to your .docx template file + // Path to the .docx template file var template = Files.newInputStream(Paths.get("your/docx/template/file.docx")); // Path to write the resulting .docx document var output = Files.newOutputStream(Paths.get("your/desired/output/path.docx")) @@ -173,7 +172,7 @@ Office-stamper provides some function already added to the standard configuratio === Custom resolvers -You can expand the resolution functionality by implementing custom `link:{engine}api/ObjectResolver.java[ObjectResolver]`. +You can expand the resolution capability by implementing custom `link:{engine}api/ObjectResolver.java[ObjectResolver]`. Here's a code snippet on how to proceed: @@ -198,7 +197,7 @@ class Main { === Custom functions -OfficeStamper lets you add custom functions to the tool’s expression language. +OfficeStamper lets you add custom functions to the tool's expression language. For example, if you need specific formats for numbers or dates, you can register such functions which can then be used in the placeholders throughout your template. Below is a sample code demonstrating how to extend the expression language with a custom function. @@ -206,20 +205,40 @@ This particular example adds a function `toUppercase(String)`, enabling you to c [source,java] ---- -class Main { +import java.time.LocalDate;import java.time.format.DateTimeFormatter;class Main { public static void main(String... args) { - interface UppercaseFunction { + var configuration = OfficeStamperConfigurations.standardWithPreprocessing(); + + // add `today()` function to use in the template to retrieve current date, at time of running the stamping + config.addCustomFunction("today", () -> LocalDate.now()); + + // add `censor(String)` function, to remove the f-word from resolved template values. + config.addCustomFunction("censor", String.class, input -> input.replace("f-word", "f**k")); + + // add `add(Integer, Integer)` function to sum 2 values together after their resolution. + config.addCustomFunction("add", Integer.class, Integer.class, (a, b) -> a + b); + + // add `format(Date, String, String)` function to format a date with a pattern and a locale. + config.addCustomFunction("format", LocalDate.class, String.class, String.class, (date, pattern, locale) -> DateTimeFormatter.ofPattern(pattern, locale).format(date)); + + // + interface StringFunctionProvider { String toUppercase(String string); + String toLowercase(String string); } - var configuration = OfficeStamperConfigurations.standardWithPreprocessing(); - configuration.exposeInterfaceToExpressionLanguage(UppercaseFunction.class, String::toUppercase); + class StringFunctionProviderImpl implements StringFunctionProvider { + String toUppercase(String string){return string.toUpperCase();} + String toLowercase(String string){return string.toUpperCase();} + } + + configuration.exposeInterfaceToExpressionLanguage(UppercaseFunction.class, new StringFunctionProviderImpl()); var stamper = OfficeStampers.docxStamper(configuration); } } ---- -Chains of such custom functions can enhance the versatility of OfficeStamper, making it capable of handling complex and unique templating situations. +Chains of such custom functions can enhance the versatility of OfficeStamper, making it able to handle complex and unique templating situations. === Custom Comment Processors diff --git a/engine/src/main/java/pro/verron/officestamper/core/BaseDocumentWalker.java b/engine/src/main/java/pro/verron/officestamper/core/BaseDocumentWalker.java deleted file mode 100644 index cf0eeacf..00000000 --- a/engine/src/main/java/pro/verron/officestamper/core/BaseDocumentWalker.java +++ /dev/null @@ -1,76 +0,0 @@ -package pro.verron.officestamper.core; - -import org.docx4j.wml.*; -import pro.verron.officestamper.api.DocxPart; - -/** - * This class is an abstract implementation of the {@link DocumentWalker} interface. - * It implements all methods of the interface and does nothing in the individual methods. - * This makes it easier to implement a custom {@link DocumentWalker} because the implementor - * only has to implement the methods that are of interest. - * - * @author Joseph Verron - * @author Tom Hombergs - * @version ${version} - * @since 1.0.0 - */ -public abstract class BaseDocumentWalker extends DocumentWalker { - - /** - * Creates a new document walker that walks through the given document. - * - */ - protected BaseDocumentWalker(DocxPart contentAccessor) { - super(contentAccessor); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onParagraph(P paragraph) { - - } - - /** {@inheritDoc} */ - @Override - protected void onRun(R run) { - - } - - /** {@inheritDoc} */ - @Override - protected void onTable(Tbl table) { - - } - - /** {@inheritDoc} */ - @Override - protected void onTableCell(Tc tableCell) { - - } - - /** {@inheritDoc} */ - @Override - protected void onTableRow(Tr tableRow) { - - } - - /** {@inheritDoc} */ - @Override - protected void onCommentRangeStart(CommentRangeStart commentRangeStart) { - - } - - /** {@inheritDoc} */ - @Override - protected void onCommentRangeEnd(CommentRangeEnd commentRangeEnd) { - - } - - /** {@inheritDoc} */ - @Override - protected void onCommentReference(R.CommentReference commentReference) { - - } -} diff --git a/engine/src/main/java/pro/verron/officestamper/core/CommentCollectorWalker.java b/engine/src/main/java/pro/verron/officestamper/core/CommentCollectorWalker.java deleted file mode 100644 index 15ca0074..00000000 --- a/engine/src/main/java/pro/verron/officestamper/core/CommentCollectorWalker.java +++ /dev/null @@ -1,95 +0,0 @@ -package pro.verron.officestamper.core; - -import org.docx4j.wml.CommentRangeEnd; -import org.docx4j.wml.CommentRangeStart; -import org.docx4j.wml.Comments; -import org.docx4j.wml.R; -import pro.verron.officestamper.api.Comment; -import pro.verron.officestamper.api.DocxPart; -import pro.verron.officestamper.api.OfficeStamperException; - -import java.math.BigInteger; -import java.util.*; - -class CommentCollectorWalker - extends BaseDocumentWalker { - private final DocxPart document; - private final Map allComments; - private final Queue stack; - private final Map rootComments; - - private CommentCollectorWalker( - DocxPart document, - Map rootComments, - Map allComments - ) { - super(document); - this.document = document; - this.allComments = allComments; - this.stack = Collections.asLifoQueue(new ArrayDeque<>()); - this.rootComments = rootComments; - } - - static Map collectComments(DocxPart docxPart) { - var rootComments = new HashMap(); - var allComments = new HashMap(); - new CommentCollectorWalker(docxPart, rootComments, allComments).walk(); - - var sourceDocument = docxPart.document(); - CommentUtil.getCommentsPart(sourceDocument.getParts()) - .map(CommentUtil::extractContent) - .map(Comments::getComment) - .stream() - .flatMap(Collection::stream) - .filter(comment -> allComments.containsKey(comment.getId())) - .forEach(comment -> allComments.get(comment.getId()) - .setComment(comment)); - return new HashMap<>(rootComments); - } - - - @Override - protected void onCommentRangeStart(CommentRangeStart commentRangeStart) { - Comment comment = allComments.get(commentRangeStart.getId()); - if (comment == null) { - comment = new StandardComment(document.document()); - allComments.put(commentRangeStart.getId(), comment); - if (stack.isEmpty()) { - rootComments.put(commentRangeStart.getId(), comment); - } - else { - stack.peek() - .getChildren() - .add(comment); - } - } - comment.setCommentRangeStart(commentRangeStart); - stack.add(comment); - } - - @Override - protected void onCommentRangeEnd(CommentRangeEnd commentRangeEnd) { - Comment comment = allComments.get(commentRangeEnd.getId()); - if (comment == null) - throw new OfficeStamperException("Found a comment range end before the comment range start !"); - - comment.setCommentRangeEnd(commentRangeEnd); - - if (stack.isEmpty()) return; - - var peek = stack.peek(); - if (peek.equals(comment)) - stack.remove(); - else throw new OfficeStamperException("Cannot figure which comment contains the other !"); - } - - @Override - protected void onCommentReference(R.CommentReference commentReference) { - Comment comment = allComments.get(commentReference.getId()); - if (comment == null) { - comment = new StandardComment(document.document()); - allComments.put(commentReference.getId(), comment); - } - comment.setCommentReference(commentReference); - } -} diff --git a/engine/src/main/java/pro/verron/officestamper/core/CommentProcessorRegistry.java b/engine/src/main/java/pro/verron/officestamper/core/CommentProcessorRegistry.java index 33a18bf6..c8b11a3c 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/CommentProcessorRegistry.java +++ b/engine/src/main/java/pro/verron/officestamper/core/CommentProcessorRegistry.java @@ -1,20 +1,19 @@ package pro.verron.officestamper.core; -import org.docx4j.wml.P; -import org.docx4j.wml.R; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.*; +import org.jvnet.jaxb2_commons.ppp.Child; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelParseException; import pro.verron.officestamper.api.*; import pro.verron.officestamper.utils.WmlFactory; +import pro.verron.officestamper.utils.WmlUtils; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Map; -import java.util.Optional; +import java.util.*; -import static pro.verron.officestamper.core.CommentCollectorWalker.collectComments; import static pro.verron.officestamper.core.Placeholders.findProcessors; /** @@ -61,7 +60,7 @@ public void runProcessors(T expressionContext) { source.streamRun() .forEach(run -> { - var comments = collectComments(source); + var comments = collectComments(); var runParent = StandardParagraph.from(source, (P) run.getParent()); var optional = runProcessorsOnRunComment(comments, expressionContext, run, runParent); commentProcessors.commitChanges(source); @@ -71,7 +70,7 @@ public void runProcessors(T expressionContext) { // we run the paragraph afterward so that the comments inside work before the whole paragraph comments source.streamParagraphs() .forEach(p -> { - var comments = collectComments(source); + var comments = collectComments(); var paragraphComment = p.getComment(); paragraphComment.ifPresent((pc -> { var optional = runProcessorsOnParagraphComment(comments, expressionContext, p, pc.getId()); @@ -86,18 +85,42 @@ public void runProcessors(T expressionContext) { proceedComments.forEach(CommentUtil::deleteComment); } + private Map collectComments() { + var rootComments = new HashMap(); + var allComments = new HashMap(); + var stack = Collections.asLifoQueue(new ArrayDeque()); + + var list = WmlUtils.extractCommentElements(document()); + for (Child commentElement : list) { + if (commentElement instanceof CommentRangeStart crs) onRangeStart(crs, allComments, stack, rootComments); + else if (commentElement instanceof CommentRangeEnd cre) onRangeEnd(cre, allComments, stack); + else if (commentElement instanceof R.CommentReference cr) onReference(cr, allComments); + } + CommentUtil.getCommentsPart(document().getParts()) + .map(CommentUtil::extractContent) + .map(Comments::getComment) + .stream() + .flatMap(Collection::stream) + .filter(comment -> allComments.containsKey(comment.getId())) + .forEach(comment -> allComments.get(comment.getId()) + .setComment(comment)); + return new HashMap<>(rootComments); + } + private Optional runProcessorsOnRunComment( Map comments, T expressionContext, R run, Paragraph paragraph ) { - return CommentUtil.getCommentAround(run, source.document()) + return CommentUtil.getCommentAround(run, document()) .flatMap(c -> Optional.ofNullable(comments.get(c.getId()))) .flatMap(c -> { - var context = new ProcessorContext(paragraph, run, c, c.asPlaceholder()); - commentProcessors.setContext(context); - var comment = runCommentProcessors(expressionContext, c); - comments.remove(c.getComment() - .getId()); - return comment; + var cPlaceholder = c.asPlaceholder(); + var cComment = c.getComment(); + comments.remove(cComment.getId()); + commentProcessors.setContext(new ProcessorContext(paragraph, run, c, cPlaceholder)); + return runCommentProcessors(expressionContext, cPlaceholder) + ? Optional.of(c) + : Optional.empty(); + }); } @@ -111,21 +134,16 @@ private Optional runProcessorsOnRunComment( * @param the type of the context root object. */ private Optional runProcessorsOnParagraphComment( - Map comments, - T expressionContext, - Paragraph paragraph, - BigInteger paragraphCommentId + Map comments, T expressionContext, Paragraph paragraph, BigInteger paragraphCommentId ) { if (!comments.containsKey(paragraphCommentId)) return Optional.empty(); var c = comments.get(paragraphCommentId); var cPlaceholder = c.asPlaceholder(); var cComment = c.getComment(); - var context = new ProcessorContext(paragraph, null, c, cPlaceholder); - commentProcessors.setContext(context); - var comment = runCommentProcessors(expressionContext, c); comments.remove(cComment.getId()); - return comment; + commentProcessors.setContext(new ProcessorContext(paragraph, null, c, cPlaceholder)); + return runCommentProcessors(expressionContext, c.asPlaceholder()) ? Optional.of(c) : Optional.empty(); } /** @@ -156,17 +174,68 @@ private void runProcessorsOnInlineContent(T context, Paragraph paragraph) { } } - private Optional runCommentProcessors(T context, Comment comment) { - var placeholder = comment.asPlaceholder(); + private WordprocessingMLPackage document() { + return source.document(); + } + + private void onRangeStart( + CommentRangeStart crs, + HashMap allComments, + Queue stack, + HashMap rootComments + ) { + Comment comment = allComments.get(crs.getId()); + if (comment == null) { + comment = new StandardComment(document()); + allComments.put(crs.getId(), comment); + if (stack.isEmpty()) { + rootComments.put(crs.getId(), comment); + } + else { + stack.peek() + .getChildren() + .add(comment); + } + } + comment.setCommentRangeStart(crs); + stack.add(comment); + } + + private void onRangeEnd( + CommentRangeEnd cre, HashMap allComments, Queue stack + ) { + Comment comment = allComments.get(cre.getId()); + if (comment == null) + throw new OfficeStamperException("Found a comment range end before the comment range start !"); + + comment.setCommentRangeEnd(cre); + + if (!stack.isEmpty()) { + var peek = stack.peek(); + if (peek.equals(comment)) stack.remove(); + else throw new OfficeStamperException("Cannot figure which comment contains the other !"); + } + } + + private void onReference(R.CommentReference cr, HashMap allComments) { + Comment comment = allComments.get(cr.getId()); + if (comment == null) { + comment = new StandardComment(document()); + allComments.put(cr.getId(), comment); + } + comment.setCommentReference(cr); + } + + private boolean runCommentProcessors(T context, Placeholder commentPlaceholder) { try { expressionResolver.setContext(context); - expressionResolver.resolve(placeholder); - logger.debug("Comment '{}' successfully processed by a comment processor.", placeholder); - return Optional.of(comment); + expressionResolver.resolve(commentPlaceholder); + logger.debug("Comment '{}' successfully processed by a comment processor.", commentPlaceholder); + return true; } catch (SpelEvaluationException | SpelParseException e) { - var message = "Comment '%s' failed to process.".formatted(placeholder.expression()); - exceptionResolver.resolve(placeholder, message, e); - return Optional.empty(); + var message = "Comment '%s' failed to process.".formatted(commentPlaceholder.expression()); + exceptionResolver.resolve(commentPlaceholder, message, e); + return false; } } diff --git a/engine/src/main/java/pro/verron/officestamper/core/DocumentWalker.java b/engine/src/main/java/pro/verron/officestamper/core/DocumentWalker.java deleted file mode 100644 index c7743d30..00000000 --- a/engine/src/main/java/pro/verron/officestamper/core/DocumentWalker.java +++ /dev/null @@ -1,191 +0,0 @@ -package pro.verron.officestamper.core; - -import org.docx4j.XmlUtils; -import org.docx4j.wml.*; -import org.docx4j.wml.R.CommentReference; -import pro.verron.officestamper.api.DocxPart; - -/** - * This class walks the document and calls abstract methods for each element it encounters. - * The following elements are supported: - *
    - *
  • {@link P}
  • - *
  • {@link R}
  • - *
  • {@link Tbl}
  • - *
  • {@link Tr}
  • - *
  • {@link Tc}
  • - *
  • {@link CommentRangeStart}
  • - *
  • {@link CommentRangeEnd}
  • - *
  • {@link CommentReference}
  • - *
- * The following elements are not supported: - *
    - *
  • {@link SdtBlock}
  • - *
  • {@link SdtRun}
  • - *
  • {@link SdtElement}
  • - *
  • {@link CTSimpleField}
  • - *
  • {@link CTSdtCell}
  • - *
  • {@link CTSdtContentCell}
  • - *
- * - * @author Joseph Verron - * @author Tom Hombergs - * @version ${version} - * @since 1.0.0 - */ -public abstract class DocumentWalker { - - private final DocxPart source; - - /** - * Creates a new DocumentWalker that will traverse the given document. - * - * @param source the document to traverse. - */ - protected DocumentWalker(DocxPart source) { - this.source = source; - } - - /** - * Starts the traversal of the document. - */ - public void walk() { - for (Object content : source.content()) { - Object ue = XmlUtils.unwrap(content); - if (ue instanceof P o) walkParagraph(o); - else if (ue instanceof R o) walkRun(o); - else if (ue instanceof Tbl o) walkTable(o); - else if (ue instanceof Tr o) walkTableRow(o); - else if (ue instanceof Tc o) walkTableCell(o); - else if (ue instanceof CommentRangeStart o) onCommentRangeStart(o); - else if (ue instanceof CommentRangeEnd o) onCommentRangeEnd(o); - else if (ue instanceof CommentReference o) onCommentReference(o); - } - } - - private void walkTable(Tbl table) { - onTable(table); - for (Object contentElement : table.getContent()) { - Object unwrappedObject = XmlUtils.unwrap(contentElement); - if (unwrappedObject instanceof Tr row) { - walkTableRow(row); - } - } - } - - private void walkTableRow(Tr row) { - onTableRow(row); - for (Object rowContentElement : row.getContent()) { - Object unwrappedObject = XmlUtils.unwrap(rowContentElement); - if (unwrappedObject instanceof Tc cell) { - walkTableCell(cell); - } - } - } - - private void walkTableCell(Tc cell) { - onTableCell(cell); - for (Object cellContentElement : cell.getContent()) { - Object unwrappedObject = XmlUtils.unwrap(cellContentElement); - if (unwrappedObject instanceof P) { - P p = (P) cellContentElement; - walkParagraph(p); - } - else if (unwrappedObject instanceof R) { - R r = (R) cellContentElement; - walkRun(r); - } - else if (unwrappedObject instanceof Tbl nestedTable) { - walkTable(nestedTable); - } - else if (unwrappedObject instanceof CommentRangeStart commentRangeStart) { - onCommentRangeStart(commentRangeStart); - } - else if (unwrappedObject instanceof CommentRangeEnd commentRangeEnd) { - onCommentRangeEnd(commentRangeEnd); - } - } - } - - private void walkParagraph(P p) { - onParagraph(p); - for (Object element : p.getContent()) { - Object unwrappedObject = XmlUtils.unwrap(element); - if (unwrappedObject instanceof R r) { - walkRun(r); - } - else if (unwrappedObject instanceof CommentRangeStart commentRangeStart) { - onCommentRangeStart(commentRangeStart); - } - else if (unwrappedObject instanceof CommentRangeEnd commentRangeEnd) { - onCommentRangeEnd(commentRangeEnd); - } - } - } - - private void walkRun(R r) { - onRun(r); - for (Object element : r.getContent()) { - Object unwrappedObject = XmlUtils.unwrap(element); - if (unwrappedObject instanceof CommentReference commentReference) { - onCommentReference(commentReference); - } - } - } - - /** - * This method is called for every {@link R} element in the document. - * - * @param run the {@link R} element to process. - */ - protected abstract void onRun(R run); - - /** - * This method is called for every {@link P} element in the document. - * - * @param paragraph the {@link P} element to process. - */ - protected abstract void onParagraph(P paragraph); - - /** - * This method is called for every {@link Tbl} element in the document. - * - * @param table the {@link Tbl} element to process. - */ - protected abstract void onTable(Tbl table); - - /** - * This method is called for every {@link Tc} element in the document. - * - * @param tableCell the {@link Tc} element to process. - */ - protected abstract void onTableCell(Tc tableCell); - - /** - * This method is called for every {@link Tr} element in the document. - * - * @param tableRow the {@link Tr} element to process. - */ - protected abstract void onTableRow(Tr tableRow); - - /** - * This method is called for every {@link CommentRangeStart} element in the document. - * - * @param commentRangeStart the {@link CommentRangeStart} element to process. - */ - protected abstract void onCommentRangeStart(CommentRangeStart commentRangeStart); - - /** - * This method is called for every {@link CommentRangeEnd} element in the document. - * - * @param commentRangeEnd the {@link CommentRangeEnd} element to process. - */ - protected abstract void onCommentRangeEnd(CommentRangeEnd commentRangeEnd); - - /** - * This method is called for every {@link CommentReference} element in the document. - * - * @param commentReference the {@link CommentReference} element to process. - */ - protected abstract void onCommentReference(CommentReference commentReference); -} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/Resolvers.java b/engine/src/main/java/pro/verron/officestamper/preset/Resolvers.java index 7a4641f4..9bbda668 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/Resolvers.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/Resolvers.java @@ -1,6 +1,7 @@ package pro.verron.officestamper.preset; -import pro.verron.officestamper.api.*; +import pro.verron.officestamper.api.ObjectResolver; +import pro.verron.officestamper.api.OfficeStamperException; import pro.verron.officestamper.preset.resolvers.date.DateResolver; import pro.verron.officestamper.preset.resolvers.image.ImageResolver; import pro.verron.officestamper.preset.resolvers.localdate.LocalDateResolver; @@ -16,8 +17,6 @@ import java.time.format.DateTimeFormatter; import java.util.Date; -import static pro.verron.officestamper.utils.WmlFactory.newRun; - /** * This class provides static methods to create different types of * {@link ObjectResolver}. diff --git a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments.java b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments.java index 499e53cd..9e75a32d 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments.java @@ -1,7 +1,6 @@ package pro.verron.officestamper.preset.preprocessors.malformedcomments; import org.docx4j.TraversalUtil; -import org.docx4j.finders.CommentFinder; import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart; @@ -11,6 +10,7 @@ import org.slf4j.LoggerFactory; import pro.verron.officestamper.api.OfficeStamperException; import pro.verron.officestamper.api.PreProcessor; +import pro.verron.officestamper.utils.WmlUtils; import java.math.BigInteger; import java.util.*; @@ -22,7 +22,7 @@ public class RemoveMalformedComments private static final Logger log = LoggerFactory.getLogger(RemoveMalformedComments.class); @Override public void process(WordprocessingMLPackage document) { - var commentElements = getCommentElements(document); + var commentElements = WmlUtils.extractCommentElements(document); var commentIds = new ArrayList(commentElements.size()); var openedCommentsIds = new ArrayDeque(); @@ -59,41 +59,28 @@ else if (commentElement instanceof R.CommentReference cr) { log.debug("These comments have been opened, but never closed: {}", openedCommentsIds); var malformedCommentIds = new ArrayList<>(openedCommentsIds); - Set writtenCommentsId = Optional.ofNullable(document.getMainDocumentPart() - .getCommentsPart()) + var mainDocumentPart = document.getMainDocumentPart(); + Set writtenCommentsId = Optional.ofNullable(mainDocumentPart.getCommentsPart()) .map(RemoveMalformedComments::tryGetCommentsPart) .map(Comments::getComment) .orElse(Collections.emptyList()) .stream() - .filter(c -> c.getContent() != null) - .filter(c -> !c.getContent() - .isEmpty()) + .filter(c -> !isEmpty(c)) .map(CTMarkup::getId) .collect(toSet()); - commentIds.removeAll(writtenCommentsId); log.debug("These comments have been referenced in body, but have no related content: {}", commentIds); malformedCommentIds.addAll(commentIds); - var commentReferenceRemoverVisitor = new CommentReferenceRemoverVisitor(malformedCommentIds); - var commentRangeStartRemoverVisitor = new CommentRangeStartRemoverVisitor(malformedCommentIds); - var commentRangeEndRemoverVisitor = new CommentRangeEndRemoverVisitor(malformedCommentIds); - TraversalUtil.visit(document, - true, - List.of(commentReferenceRemoverVisitor, - commentRangeStartRemoverVisitor, - commentRangeEndRemoverVisitor)); - commentReferenceRemoverVisitor.run(); - commentRangeStartRemoverVisitor.run(); - commentRangeEndRemoverVisitor.run(); - } - - private static List getCommentElements(WordprocessingMLPackage document) { - var commentFinder = new CommentFinder(); - TraversalUtil.visit(document, true, commentFinder); - return commentFinder.getCommentElements(); + var crVisitor = new CommentReferenceRemoverVisitor(malformedCommentIds); + var crsVisitor = new CommentRangeStartRemoverVisitor(malformedCommentIds); + var creVisitor = new CommentRangeEndRemoverVisitor(malformedCommentIds); + TraversalUtil.visit(document, true, List.of(crVisitor, crsVisitor, creVisitor)); + crVisitor.run(); + crsVisitor.run(); + creVisitor.run(); } private static Comments tryGetCommentsPart(CommentsPart commentsPart) { @@ -104,4 +91,9 @@ private static Comments tryGetCommentsPart(CommentsPart commentsPart) { } } + private static boolean isEmpty(Comments.Comment c) { + var content = c.getContent(); + return content == null || content.isEmpty(); + } + } diff --git a/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/ParagraphResolverDocumentWalker.java b/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/ParagraphResolverDocumentWalker.java deleted file mode 100644 index 9ff0f22c..00000000 --- a/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/ParagraphResolverDocumentWalker.java +++ /dev/null @@ -1,48 +0,0 @@ -package pro.verron.officestamper.preset.processors.repeat; - -import org.docx4j.wml.P; -import org.docx4j.wml.Tr; -import pro.verron.officestamper.api.DocxPart; -import pro.verron.officestamper.api.ParagraphPlaceholderReplacer; -import pro.verron.officestamper.core.BaseDocumentWalker; -import pro.verron.officestamper.core.StandardParagraph; - -/** - * Walks through a document and replaces expressions with values from the given - * expression context. - * This walker only replaces expressions in paragraphs, not in tables. - * - * @author Joseph Verron - * @version ${version} - * @since 1.4.7 - */ -class ParagraphResolverDocumentWalker - extends BaseDocumentWalker { - private final Object expressionContext; - private final DocxPart docxPart; - private final ParagraphPlaceholderReplacer placeholderReplacer; - - /** - *

Constructor for ParagraphResolverDocumentWalker.

- * - * @param rowClone The row to start with - * @param expressionContext The context of the expressions to resolve - * @param replacer The placeholderReplacer to use for resolving - */ - public ParagraphResolverDocumentWalker( - DocxPart docxPart, Tr rowClone, Object expressionContext, ParagraphPlaceholderReplacer replacer - ) { - super(docxPart.from(rowClone)); - this.expressionContext = expressionContext; - this.docxPart = docxPart; - this.placeholderReplacer = replacer; - } - - /** - * {@inheritDoc} - */ - @Override protected void onParagraph(P paragraph) { - var standardParagraph = StandardParagraph.from(docxPart, paragraph); - placeholderReplacer.resolveExpressionsForParagraph(docxPart, standardParagraph, expressionContext); - } -} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/RepeatProcessor.java b/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/RepeatProcessor.java index 59f03f1c..2aa66b84 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/RepeatProcessor.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/processors/repeat/RepeatProcessor.java @@ -1,13 +1,17 @@ package pro.verron.officestamper.preset.processors.repeat; +import org.docx4j.TraversalUtil; import org.docx4j.XmlUtils; +import org.docx4j.finders.ClassFinder; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.Comments; +import org.docx4j.wml.P; import org.docx4j.wml.Tbl; import org.docx4j.wml.Tr; import org.springframework.lang.Nullable; import pro.verron.officestamper.api.*; import pro.verron.officestamper.core.CommentUtil; +import pro.verron.officestamper.core.StandardParagraph; import pro.verron.officestamper.preset.CommentProcessorFactory; import java.math.BigInteger; @@ -82,10 +86,14 @@ private void repeatRows(DocxPart source) { Comments.Comment comment = requireNonNull(commentWrapper.getComment()); BigInteger commentId = comment.getId(); CommentUtil.deleteCommentFromElements(rowClone.getContent(), commentId); - new ParagraphResolverDocumentWalker(source, - rowClone, - expressionContext, - this.placeholderReplacer).walk(); + var classFinder = new ClassFinder(P.class); + TraversalUtil.visit(rowClone, classFinder); + var objects = classFinder.results; + for (Object object : objects) { + P result = (P) object; + StandardParagraph paragraph = StandardParagraph.from(source, result); + placeholderReplacer.resolveExpressionsForParagraph(source, paragraph, expressionContext); + } changes.add(rowClone); } } @@ -102,8 +110,8 @@ private void repeatRows(DocxPart source) { /** {@inheritDoc} */ @Override public void repeatTableRow(@Nullable List objects) { var tr = this.getParagraph() - .parent(Tr.class) - .orElseThrow(OfficeStamperException.throwing("This paragraph is not in a table row.")); + .parent(Tr.class) + .orElseThrow(OfficeStamperException.throwing("This paragraph is not in a table row.")); tableRowsToRepeat.put(tr, objects); tableRowsCommentsToRemove.put(tr, getCurrentCommentWrapper()); } diff --git a/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2DefaultResolver.java b/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2DefaultResolver.java index 69cd8739..c9124bf1 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2DefaultResolver.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2DefaultResolver.java @@ -4,7 +4,6 @@ import org.springframework.lang.Nullable; import pro.verron.officestamper.api.DocxPart; import pro.verron.officestamper.api.ObjectResolver; -import pro.verron.officestamper.preset.Resolvers; import static pro.verron.officestamper.utils.WmlFactory.newRun; diff --git a/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2PlaceholderResolver.java b/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2PlaceholderResolver.java index 8d4b4a9c..18bf4f1a 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2PlaceholderResolver.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/resolvers/nulls/Null2PlaceholderResolver.java @@ -6,7 +6,6 @@ import pro.verron.officestamper.api.ObjectResolver; import pro.verron.officestamper.api.OfficeStamperException; import pro.verron.officestamper.api.Placeholder; -import pro.verron.officestamper.preset.Resolvers; import static pro.verron.officestamper.utils.WmlFactory.newRun; diff --git a/engine/src/main/java/pro/verron/officestamper/utils/WmlUtils.java b/engine/src/main/java/pro/verron/officestamper/utils/WmlUtils.java index ccec0443..e312f29f 100644 --- a/engine/src/main/java/pro/verron/officestamper/utils/WmlUtils.java +++ b/engine/src/main/java/pro/verron/officestamper/utils/WmlUtils.java @@ -1,8 +1,12 @@ package pro.verron.officestamper.utils; +import org.docx4j.TraversalUtil; +import org.docx4j.finders.CommentFinder; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.jvnet.jaxb2_commons.ppp.Child; import pro.verron.officestamper.api.OfficeStamperException; +import java.util.List; import java.util.Optional; public final class WmlUtils { @@ -21,4 +25,10 @@ public static Optional getFirstParentWithClass(Child child, Class aCla } return Optional.empty(); } + + public static List extractCommentElements(WordprocessingMLPackage document) { + var commentFinder = new CommentFinder(); + TraversalUtil.visit(document, true, commentFinder); + return commentFinder.getCommentElements(); + } } diff --git a/engine/src/test/java/pro/verron/officestamper/test/DefaultTests.java b/engine/src/test/java/pro/verron/officestamper/test/DefaultTests.java index b8aebe81..91a3e866 100644 --- a/engine/src/test/java/pro/verron/officestamper/test/DefaultTests.java +++ b/engine/src/test/java/pro/verron/officestamper/test/DefaultTests.java @@ -717,7 +717,7 @@ private static Arguments changingPageLayoutTest_shouldKeepSectionBreakOrientatio First page is landscape. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< Second page is portrait, layout change should survive to repeatParagraph processor (Homer). @@ -732,7 +732,7 @@ Second page is portrait, layout change should survive to repeatParagraph process <<< - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Fourth page is set to landscape again. """); @@ -745,24 +745,24 @@ private static Arguments changingPageLayoutTest_shouldKeepSectionBreakOrientatio First page is landscape. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< Second page is portrait, layout change should survive to repeatParagraph processor (Homer). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< With a page break changing the layout in between. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< Second page is portrait, layout change should survive to repeatParagraph processor (Marge). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< With a page break changing the layout in between. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< Fourth page is set to portrait again. """; @@ -784,26 +784,26 @@ private static Arguments changingPageLayoutTest_shouldKeepPageBreakOrientationIn First page is portrait. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Second page is landscape, layout change should survive to repeatDocPart (Homer). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< With a break setting the layout to portrait in between. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Second page is landscape, layout change should survive to repeatDocPart (Marge). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< With a break setting the layout to portrait in between. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Fourth page is set to landscape again. """); @@ -830,12 +830,12 @@ private static Arguments changingPageLayoutTest_shouldKeepPageBreakOrientationIn First page is portrait. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Second page is landscape, layout change should survive to repeatDocPart (Homer). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< With a break setting the layout to portrait in between. |=== @@ -844,12 +844,12 @@ Second page is landscape, layout change should survive to repeatDocPart (Homer). |=== - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Second page is landscape, layout change should survive to repeatDocPart (Marge). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< With a break setting the layout to portrait in between. |=== @@ -858,10 +858,10 @@ Second page is landscape, layout change should survive to repeatDocPart (Marge). |=== - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Fourth page is set to landscape again. """); @@ -876,7 +876,7 @@ private static Arguments changingPageLayoutTest_shouldKeepPageBreakOrientationIn First page is landscape. - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=11906,orient=LANDSCAPE,w=16838}}] <<< Second page is portrait, layout change should survive to repeatDocPart (Homer). @@ -891,7 +891,7 @@ Second page is portrait, layout change should survive to repeatDocPart (Marge). Without a break changing the layout in between (page break should be repeated). - [section-break, docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1418,footer=709,gutter=0,header=709,left=1418,right=1418,top=1418},pgSz={h=16838,w=11906}}] <<< Fourth page is set to landscape again. """); diff --git a/engine/src/test/java/pro/verron/officestamper/test/MultiSectionTest.java b/engine/src/test/java/pro/verron/officestamper/test/MultiSectionTest.java index 837f9e74..ef420890 100644 --- a/engine/src/test/java/pro/verron/officestamper/test/MultiSectionTest.java +++ b/engine/src/test/java/pro/verron/officestamper/test/MultiSectionTest.java @@ -22,7 +22,7 @@ void expressionsInMultipleSections() { Homer - [section-break, docGrid={linePitch=360},pgMar={bottom=1417,footer=708,gutter=0,header=708,left=1417,right=1417,top=1417},pgSz={h=16838,w=11906}] + [section-break, {docGrid={linePitch=360},pgMar={bottom=1417,footer=708,gutter=0,header=708,left=1417,right=1417,top=1417},pgSz={h=16838,w=11906}}] <<< Marge """; diff --git a/engine/src/test/java/pro/verron/officestamper/test/Stringifier.java b/engine/src/test/java/pro/verron/officestamper/test/Stringifier.java index 302fa1a9..bfb33ce8 100644 --- a/engine/src/test/java/pro/verron/officestamper/test/Stringifier.java +++ b/engine/src/test/java/pro/verron/officestamper/test/Stringifier.java @@ -40,7 +40,7 @@ import java.util.function.Supplier; import java.util.stream.Stream; -import static java.util.Optional.ofNullable; +import static java.util.Optional.*; import static java.util.stream.Collectors.joining; /** @@ -62,10 +62,9 @@ public class Stringifier { */ public Stringifier(Supplier documentSupplier) { this.documentSupplier = documentSupplier; - this.styleDefinitionsPartSupplier = - () -> documentSupplier.get() - .getMainDocumentPart() - .getStyleDefinitionsPart(true); + this.styleDefinitionsPartSupplier = () -> documentSupplier.get() + .getMainDocumentPart() + .getStyleDefinitionsPart(true); } public static String stringifyPowerpoint(PresentationMLPackage presentation) { @@ -129,14 +128,33 @@ private static Predicate idEqual(BigInteger id) { }; } - private static void extract( - Map map, String key, Object value - ) { - if (value != null) map.put(key, value); - } - private static Function, String> format(String format) { - return entry -> format.formatted(entry.getKey(), entry.getValue()); + /** + *

stringify.

+ * + * @param spacing a {@link PPrBase.Spacing} object + * + * @return a {@link Optional} object + * + * @since 1.6.6 + */ + private Optional stringify(PPrBase.Spacing spacing) { + if (spacing == null) return empty(); + var map = new TreeMap(); + ofNullable(spacing.getAfter()).ifPresent(value -> map.put("after", String.valueOf(value))); + ofNullable(spacing.getBefore()).ifPresent(value -> map.put("before", String.valueOf(value))); + ofNullable(spacing.getBeforeLines()).ifPresent(value -> map.put("beforeLines", String.valueOf(value))); + ofNullable(spacing.getAfterLines()).ifPresent(value -> map.put("afterLines", String.valueOf(value))); + ofNullable(spacing.getLine()).ifPresent(value -> map.put("line", String.valueOf(value))); + ofNullable(spacing.getLineRule()).ifPresent(value -> map.put("lineRule", value.value())); + return map.isEmpty() ? empty() : of(stringify(map)); + } + + private static String stringify(Map map) { + return map.entrySet() + .stream() + .map(e -> "%s=%s".formatted(e.getKey(), e.getValue())) + .collect(joining(",", "{", "}")); } private String stringify(Text text) { @@ -228,16 +246,7 @@ private String sha1b64(byte[] imageBytes) { */ public String stringify(Object o) { if (o instanceof JAXBElement jaxb) return stringify(jaxb.getValue()); - if (o instanceof WordprocessingMLPackage mlPackage) { - var header = stringifyHeaders(getHeaderPart(mlPackage)); - var body = stringify(mlPackage.getMainDocumentPart()); - var footer = stringifyFooters(getFooterPart(mlPackage)); - var hStr = header.map(h -> h + "\n\n") - .orElse(""); - var fStr = footer.map(f -> "\n" + f + "\n") - .orElse(""); - return hStr + body + fStr; - } + if (o instanceof WordprocessingMLPackage mlPackage) return stringify(mlPackage); if (o instanceof Tbl tbl) return stringify(tbl); if (o instanceof Tr tr) return stringify(tr); if (o instanceof Tc tc) return stringify(tc); @@ -245,7 +254,7 @@ public String stringify(Object o) { if (o instanceof Body body) return stringify(body.getContent()); if (o instanceof List list) return stringify(list); if (o instanceof Text text) return stringify(text); - if (o instanceof P p) return stringify(p) + "\n"; + if (o instanceof P p) return stringify(p); if (o instanceof R r) return stringify(r); if (o instanceof Drawing drawing) return stringify(drawing); if (o instanceof Inline inline) return stringify(inline); @@ -263,21 +272,43 @@ public String stringify(Object o) { if (o instanceof ProofErr) return ""; if (o instanceof CommentRangeStart) return ""; if (o instanceof CommentRangeEnd) return ""; - if (o instanceof SdtBlock block) return stringify(block.getSdtContent()) + "\n"; + if (o instanceof SdtBlock block) return stringify(block); if (o instanceof AlternateContent) return ""; if (o instanceof Pict pict) return stringify(pict.getAnyAndAny()); if (o instanceof CTShapetype) return ""; - if (o instanceof VmlShapeElements vmlShapeElements) - return "[" + stringify(vmlShapeElements.getEGShapeElements()).trim() + "]\n"; + if (o instanceof VmlShapeElements vmlShapeElements) return stringify(vmlShapeElements); if (o instanceof CTTextbox ctTextbox) return stringify(ctTextbox.getTxbxContent()); if (o instanceof CTTxbxContent content) return stringify(content.getContent()); if (o instanceof CTShadow) return ""; if (o instanceof SdtRun run) return stringify(run.getSdtContent()); - if (o instanceof SdtContent content) return "[" + stringify(content.getContent()).trim() + "]"; + if (o instanceof SdtContent content) return stringify(content); if (o == null) throw new RuntimeException("Unsupported content: NULL"); throw new RuntimeException("Unsupported content: " + o.getClass()); } + private String stringify(SdtBlock block) { + return stringify(block.getSdtContent()) + "\n"; + } + + private String stringify(SdtContent content) { + return "[" + stringify(content.getContent()).trim() + "]"; + } + + private String stringify(VmlShapeElements vmlShapeElements) { + return "[" + stringify(vmlShapeElements.getEGShapeElements()).trim() + "]\n"; + } + + private String stringify(WordprocessingMLPackage mlPackage) { + var header = stringifyHeaders(getHeaderPart(mlPackage)); + var body = stringify(mlPackage.getMainDocumentPart()); + var footer = stringifyFooters(getFooterPart(mlPackage)); + var hStr = header.map(h -> h + "\n\n") + .orElse(""); + var fStr = footer.map(f -> "\n" + f + "\n") + .orElse(""); + return hStr + body + fStr; + } + private String stringify(Tc tc) { var content = stringify(tc.getContent()); return """ @@ -315,8 +346,8 @@ private Optional stringifyHeaders(Stream headerPart) { private Optional stringify(HeaderPart part) { var content = stringify(part.getContent()); - if (content.isEmpty()) return Optional.empty(); - return Optional.of(""" + if (content.isEmpty()) return empty(); + return of(""" [header, name="%s"] ---- %s @@ -325,15 +356,14 @@ private Optional stringify(HeaderPart part) { private Optional stringify(FooterPart part) { var content = stringify(part.getContent()); - if (content.isEmpty()) return Optional.empty(); - return Optional.of(""" + if (content.isEmpty()) return empty(); + return of(""" [footer, name="%s"] ---- %s ----""".formatted(part.getPartName(), content)); } - private Stream getHeaderPart(WordprocessingMLPackage document) { var sections = document.getDocumentModel() .getSections(); @@ -392,8 +422,9 @@ private String stringify(CTBlipFillProperties blipFillProperties) { private String stringify(R.CommentReference commentReference) { try { - return findComment(document(), commentReference.getId()).map(c -> stringify(c.getContent())) - .orElseThrow(); + return Stringifier.findComment(document(), commentReference.getId()) + .map(c -> stringify(c.getContent())) + .orElseThrow(); } catch (Docx4JException e) { throw new RuntimeException(e); } @@ -426,27 +457,51 @@ private String stringify(List list) { /** *

stringify.

* - * @param spacing a {@link PPrBase.Spacing} object + * @param rPr a {@link RPrAbstract} object * - * @return a {@link Optional} object + * @return a {@link String} object * * @since 1.6.6 */ - private Optional stringify(PPrBase.Spacing spacing) { - if (spacing == null) return Optional.empty(); - SortedMap map = new TreeMap<>(); - extract(map, "after", spacing.getAfter()); - extract(map, "before", spacing.getBefore()); - extract(map, "beforeLines", spacing.getBeforeLines()); - extract(map, "afterLines", spacing.getAfterLines()); - extract(map, "line", spacing.getLine()); - extract(map, "lineRule", spacing.getLineRule()); - return map.isEmpty() - ? Optional.empty() - : Optional.of(map.entrySet() - .stream() - .map(format("%s=%s")) - .collect(joining(",", "{", "}"))); + private Optional stringify(RPrAbstract rPr) { + if (rPr == null) return empty(); + var map = new TreeMap(); + ofNullable(rPr.getB()).ifPresent(value -> map.put("b", String.valueOf(value.isVal()))); + ofNullable(rPr.getBdr()).ifPresent(value -> map.put("bdr", "xxx")); + ofNullable(rPr.getCaps()).ifPresent(value -> map.put("caps", String.valueOf(value.isVal()))); + ofNullable(rPr.getColor()).ifPresent(value -> map.put("color", value.getVal())); + ofNullable(rPr.getDstrike()).ifPresent(value -> map.put("dstrike", String.valueOf(value.isVal()))); + ofNullable(rPr.getI()).ifPresent(value -> map.put("i", String.valueOf(value.isVal()))); + ofNullable(rPr.getKern()).ifPresent(value -> map.put("kern", String.valueOf(value.getVal()))); + ofNullable(rPr.getLang()).ifPresent(value -> map.put("lang", value.getVal())); + stringify(rPr.getRFonts()).ifPresent(e -> map.put("rFont", e)); + ofNullable(rPr.getRPrChange()).ifPresent(value -> map.put("rPrChange", "xxx")); + ofNullable(rPr.getRStyle()).ifPresent(value -> map.put("rStyle", value.getVal())); + ofNullable(rPr.getRtl()).ifPresent(value -> map.put("rtl", String.valueOf(value.isVal()))); + ofNullable(rPr.getShadow()).ifPresent(value -> map.put("shadow", String.valueOf(value.isVal()))); + ofNullable(rPr.getShd()).ifPresent(value -> map.put("shd", value.getColor())); + ofNullable(rPr.getSmallCaps()).ifPresent(value -> map.put("smallCaps", String.valueOf(value.isVal()))); + ofNullable(rPr.getVertAlign()).ifPresent(value -> map.put("vertAlign", + value.getVal() + .value())); + ofNullable(rPr.getSpacing()).ifPresent(value -> map.put("spacing", String.valueOf(value.getVal()))); + ofNullable(rPr.getStrike()).ifPresent(value -> map.put("strike", String.valueOf(value.isVal()))); + ofNullable(rPr.getOutline()).ifPresent(value -> map.put("outline", String.valueOf(value.isVal()))); + ofNullable(rPr.getEmboss()).ifPresent(value -> map.put("emboss", String.valueOf(value.isVal()))); + ofNullable(rPr.getImprint()).ifPresent(value -> map.put("imprint", String.valueOf(value.isVal()))); + ofNullable(rPr.getNoProof()).ifPresent(value -> map.put("noProof", String.valueOf(value.isVal()))); + ofNullable(rPr.getSpecVanish()).ifPresent(value -> map.put("specVanish", String.valueOf(value.isVal()))); + ofNullable(rPr.getU()).ifPresent(value -> map.put("u", + value.getVal() + .value())); + ofNullable(rPr.getVanish()).ifPresent(value -> map.put("vanish", String.valueOf(value.isVal()))); + ofNullable(rPr.getW()).ifPresent(value -> map.put("w", String.valueOf(value.getVal()))); + ofNullable(rPr.getWebHidden()).ifPresent(value -> map.put("webHidden", String.valueOf(value.isVal()))); + ofNullable(rPr.getHighlight()).ifPresent(value -> map.put("highlight", value.getVal())); + ofNullable(rPr.getEffect()).ifPresent(value -> map.put("effect", + value.getVal() + .value())); + return map.isEmpty() ? empty() : of(stringify(map)); } /** @@ -461,7 +516,7 @@ private Optional stringify(PPrBase.Spacing spacing) { private String stringify(P p) { var runs = stringify(p.getContent()); var ppr = stringify(p.getPPr()); - return ppr.apply(runs); + return ppr.apply(runs) + "\n"; } private Function stringify(PPr pPr) { @@ -501,13 +556,11 @@ private Function stringify(PPr pPr) { ofNullable(pPr.getCnfStyle()).ifPresent(style -> set.put("cnfStyle", style.getVal())); return set.entrySet() .stream() - .reduce(Function.identity(), - (f, entry) -> switch (entry.getKey()) { - case "pStyle" -> f.compose(decorateWithStyle(entry.getValue())); - case "sectPr" -> f.compose(str -> str+"\n[section-break, " + entry.getValue() +"]\n<<<"); - default -> f.andThen(s -> s + "<%s=%s>".formatted(entry.getKey(), entry.getValue())); - }, - Function::andThen); + .reduce(Function.identity(), (f, entry) -> switch (entry.getKey()) { + case "pStyle" -> f.compose(decorateWithStyle(entry.getValue())); + case "sectPr" -> f.compose(str -> str + "\n[section-break, " + entry.getValue() + "]\n<<<"); + default -> f.andThen(s -> s + "<%s=%s>".formatted(entry.getKey(), entry.getValue())); + }, Function::andThen); } private Function decorateWithStyle(String value) { @@ -541,140 +594,85 @@ private String stringify(R run) { .orElse(serialized); } - /** - *

stringify.

- * - * @param rPr a {@link RPrAbstract} object - * - * @return a {@link String} object - * - * @since 1.6.6 - */ - private Optional stringify(RPrAbstract rPr) { - if (rPr == null) return Optional.empty(); - var set = new TreeSet(); - if (rPr.getB() != null) set.add("b=" + rPr.getB() - .isVal()); - if (rPr.getBdr() != null) set.add("bdr=xxx"); - if (rPr.getCaps() != null) set.add("caps=" + rPr.getCaps() - .isVal()); - if (rPr.getColor() != null) set.add("color=" + rPr.getColor() - .getVal()); - if (rPr.getDstrike() != null) set.add("dstrike=" + rPr.getDstrike() - .isVal()); - if (rPr.getI() != null) set.add("i=" + rPr.getI() - .isVal()); - if (rPr.getKern() != null) set.add("kern=" + rPr.getKern() - .getVal() - .intValue()); - if (rPr.getLang() != null) set.add("lang=" + rPr.getLang() - .getVal()); - if (rPr.getRFonts() != null) {/* DO NOTHING */} - if (rPr.getRPrChange() != null) set.add("rPrChange=xxx"); - if (rPr.getRStyle() != null) set.add("rStyle=" + rPr.getRStyle() - .getVal()); - if (rPr.getRtl() != null) set.add("rtl=" + rPr.getRtl() - .isVal()); - if (rPr.getShadow() != null) set.add("shadow=" + rPr.getShadow() - .isVal()); - if (rPr.getShd() != null) set.add("shd=" + rPr.getShd() - .getColor()); - if (rPr.getSmallCaps() != null) set.add("smallCaps=" + rPr.getSmallCaps() - .isVal()); - if (rPr.getVertAlign() != null) set.add("vertAlign=" + rPr.getVertAlign() - .getVal() - .value()); - if (rPr.getSpacing() != null) set.add("spacing=" + rPr.getSpacing() - .getVal() - .intValue()); - if (rPr.getStrike() != null) set.add("strike=" + rPr.getStrike() - .isVal()); - if (rPr.getOutline() != null) set.add("outline=" + rPr.getOutline() - .isVal()); - if (rPr.getEmboss() != null) set.add("emboss=" + rPr.getEmboss() - .isVal()); - if (rPr.getImprint() != null) set.add("imprint=" + rPr.getImprint() - .isVal()); - if (rPr.getNoProof() != null) set.add("noProof=" + rPr.getNoProof() - .isVal()); - if (rPr.getSpecVanish() != null) set.add("specVanish=" + rPr.getSpecVanish() - .isVal()); - if (rPr.getU() != null) set.add("u=" + rPr.getU() - .getVal() - .value()); - if (rPr.getVanish() != null) set.add("vanish=" + rPr.getVanish() - .isVal()); - if (rPr.getW() != null) set.add("w=" + rPr.getW() - .getVal()); - if (rPr.getWebHidden() != null) set.add("webHidden=" + rPr.getWebHidden() - .isVal()); - if (rPr.getHighlight() != null) set.add("highlight=" + rPr.getHighlight() - .getVal()); - if (rPr.getEffect() != null) set.add("effect=" + rPr.getEffect() - .getVal() - .value()); - if (set.isEmpty()) return Optional.empty(); - return Optional.of("{" + String.join(",", set) + "}"); + private Optional stringify(RFonts rFonts) { + if (rFonts == null) return empty(); + var map = new TreeMap(); + ofNullable(rFonts.getAscii()).ifPresent(value -> map.put("ascii", value)); + ofNullable(rFonts.getHAnsi()).ifPresent(value -> map.put("hAnsi", value)); + ofNullable(rFonts.getCs()).ifPresent(value -> map.put("cs", value)); + ofNullable(rFonts.getEastAsia()).ifPresent(value -> map.put("eastAsia", value)); + ofNullable(rFonts.getAsciiTheme()).ifPresent(value -> map.put("asciiTheme", value.value())); + ofNullable(rFonts.getHAnsiTheme()).ifPresent(value -> map.put("hAnsiTheme", value.value())); + ofNullable(rFonts.getCstheme()).ifPresent(value -> map.put("cstheme", value.value())); + ofNullable(rFonts.getEastAsiaTheme()).ifPresent(value -> map.put("eastAsiaTheme", value.value())); + return map.isEmpty() ? empty() : of(stringify(map)); } private Optional stringify(SectPr sectPr) { - if (sectPr == null) return Optional.empty(); - var set = new TreeSet(); - if (sectPr.getEGHdrFtrReferences() != null && !sectPr.getEGHdrFtrReferences() - .isEmpty()) - set.add("eGHdrFtrReferences=%s".formatted(sectPr.getEGHdrFtrReferences() - .stream() - .map(this::stringify) - .collect(joining(",", "[", "]")))); - if (sectPr.getPgSz() != null) set.add("pgSz={" + stringify(sectPr.getPgSz()) + "}"); - if (sectPr.getPgMar() != null) set.add("pgMar={" + stringify(sectPr.getPgMar()) + "}"); - if (sectPr.getPaperSrc() != null) set.add("paperSrc=xxx"); - if (sectPr.getBidi() != null) set.add("bidi=xxx"); - if (sectPr.getRtlGutter() != null) set.add("rtlGutter=xxx"); - if (sectPr.getDocGrid() != null) set.add("docGrid={" + stringify(sectPr.getDocGrid()) + "}"); - if (sectPr.getFormProt() != null) set.add("formProt=xxx"); - if (sectPr.getVAlign() != null) set.add("vAlign=xxx"); - if (sectPr.getNoEndnote() != null) set.add("noEndnote=xxx"); - if (sectPr.getTitlePg() != null) set.add("titlePg=xxx"); - if (sectPr.getTextDirection() != null) set.add("textDirection=xxx"); - if (sectPr.getRtlGutter() != null) set.add("rtlGutter=xxx"); - if (set.isEmpty()) return Optional.empty(); - return Optional.of(String.join(",", set)); - } - - private String stringify(CTDocGrid ctDocGrid) { - var set = new TreeSet(); - if (ctDocGrid.getCharSpace() != null) set.add("charSpace=" + ctDocGrid.getCharSpace()); - if (ctDocGrid.getLinePitch() != null) set.add("linePitch=" + ctDocGrid.getLinePitch() - .intValue()); - if (ctDocGrid.getType() != null) set.add("type=" + ctDocGrid.getType()); - return String.join(",", set); - } - - private String stringify(CTRel ctRel) { - var set = new TreeSet(); - if (ctRel.getId() != null) set.add("id=" + ctRel.getId()); - return String.join(",", set); - } - - private String stringify(SectPr.PgMar pgMar) { - var set = new TreeSet(); - if (pgMar.getHeader() != null) set.add("header=" + pgMar.getHeader()); - if (pgMar.getFooter() != null) set.add("footer=" + pgMar.getFooter()); - if (pgMar.getGutter() != null) set.add("gutter=" + pgMar.getGutter()); - if (pgMar.getTop() != null) set.add("top=" + pgMar.getTop()); - if (pgMar.getLeft() != null) set.add("left=" + pgMar.getLeft()); - if (pgMar.getBottom() != null) set.add("bottom=" + pgMar.getBottom()); - if (pgMar.getRight() != null) set.add("right=" + pgMar.getRight()); - return String.join(",", set); - } - - private String stringify(SectPr.PgSz pgSz) { - var set = new TreeSet(); - if (pgSz.getOrient() != null) set.add("orient=" + pgSz.getOrient()); - if (pgSz.getW() != null) set.add("w=" + pgSz.getW()); - if (pgSz.getH() != null) set.add("h=" + pgSz.getH()); - if (pgSz.getCode() != null) set.add("code=" + pgSz.getCode()); - return String.join(",", set); + if (sectPr == null) return empty(); + var map = new TreeMap(); + stringify(sectPr.getEGHdrFtrReferences(), this::stringify).ifPresent(value -> map.put("eGHdrFtrReferences", + value)); + stringify(sectPr.getPgSz()).ifPresent(value -> map.put("pgSz", value)); + stringify(sectPr.getPgMar()).ifPresent(value -> map.put("pgMar", value)); + ofNullable(sectPr.getPaperSrc()).ifPresent(value -> map.put("paperSrc", "xxx")); + ofNullable(sectPr.getBidi()).ifPresent(value -> map.put("bidi", "xxx")); + ofNullable(sectPr.getRtlGutter()).ifPresent(value -> map.put("rtlGutter", "xxx")); + stringify(sectPr.getDocGrid()).ifPresent(value -> map.put("docGrid", value)); + ofNullable(sectPr.getFormProt()).ifPresent(value -> map.put("formProt", "xxx")); + ofNullable(sectPr.getVAlign()).ifPresent(value -> map.put("vAlign", "xxx")); + ofNullable(sectPr.getNoEndnote()).ifPresent(value -> map.put("noEndnote", "xxx")); + ofNullable(sectPr.getTitlePg()).ifPresent(value -> map.put("titlePg", "xxx")); + ofNullable(sectPr.getTextDirection()).ifPresent(value -> map.put("textDirection", "xxx")); + ofNullable(sectPr.getRtlGutter()).ifPresent(value -> map.put("rtlGutter", "xxx")); + return map.isEmpty() ? empty() : of(stringify(map)); + } + + private static Optional stringify(List list, Function> stringify) { + if (list == null) return empty(); + if (list.isEmpty()) return empty(); + return of(list.stream() + .map(stringify) + .flatMap(Optional::stream) + .collect(joining(",", "[", "]"))); + } + + private Optional stringify(CTRel ctRel) { + if (ctRel == null) return empty(); + var map = new TreeMap(); + ofNullable(ctRel.getId()).ifPresent(value -> map.put("id", value)); + return map.isEmpty() ? empty() : of(stringify(map)); + } + + private Optional stringify(SectPr.PgSz pgSz) { + if (pgSz == null) return empty(); + var map = new TreeMap(); + ofNullable(pgSz.getOrient()).ifPresent(value -> map.put("orient", String.valueOf(value))); + ofNullable(pgSz.getW()).ifPresent(value -> map.put("w", String.valueOf(value))); + ofNullable(pgSz.getH()).ifPresent(value -> map.put("h", String.valueOf(value))); + ofNullable(pgSz.getCode()).ifPresent(value -> map.put("code", String.valueOf(value))); + return map.isEmpty() ? empty() : of(stringify(map)); + } + + private Optional stringify(SectPr.PgMar pgMar) { + if (pgMar == null) return empty(); + var map = new TreeMap(); + ofNullable(pgMar.getHeader()).ifPresent(value -> map.put("header", String.valueOf(value))); + ofNullable(pgMar.getFooter()).ifPresent(value -> map.put("footer", String.valueOf(value))); + ofNullable(pgMar.getGutter()).ifPresent(value -> map.put("gutter", String.valueOf(value))); + ofNullable(pgMar.getTop()).ifPresent(value -> map.put("top", String.valueOf(value))); + ofNullable(pgMar.getLeft()).ifPresent(value -> map.put("left", String.valueOf(value))); + ofNullable(pgMar.getBottom()).ifPresent(value -> map.put("bottom", String.valueOf(value))); + ofNullable(pgMar.getRight()).ifPresent(value -> map.put("right", String.valueOf(value))); + return map.isEmpty() ? empty() : of(stringify(map)); + } + + private Optional stringify(CTDocGrid ctDocGrid) { + if (ctDocGrid == null) return empty(); + var map = new TreeMap(); + ofNullable(ctDocGrid.getCharSpace()).ifPresent(value -> map.put("charSpace", String.valueOf(value))); + ofNullable(ctDocGrid.getLinePitch()).ifPresent(value -> map.put("linePitch", String.valueOf(value))); + ofNullable(ctDocGrid.getType()).ifPresent(value -> map.put("type", String.valueOf(value))); + return map.isEmpty() ? empty() : of(stringify(map)); } }