diff --git a/engine/pom.xml b/engine/pom.xml index d550ca65..df52f827 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -148,7 +148,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.1 + 3.11.2 Joseph Verron ${project.version} @@ -251,7 +251,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.1 + 3.11.2 @@ -296,7 +296,7 @@ org.springframework spring-expression - 6.2.0 + 6.2.1 org.springframework @@ -313,7 +313,7 @@ org.springframework spring-context - 6.2.0 + 6.2.1 test diff --git a/engine/src/main/java/pro/verron/officestamper/api/CustomFunction.java b/engine/src/main/java/pro/verron/officestamper/api/CustomFunction.java index f4704b32..36ad8d67 100644 --- a/engine/src/main/java/pro/verron/officestamper/api/CustomFunction.java +++ b/engine/src/main/java/pro/verron/officestamper/api/CustomFunction.java @@ -22,5 +22,4 @@ public interface NeedsBiFunctionImpl { public interface NeedsTriFunctionImpl { void withImplementation(TriFunction function); } - } diff --git a/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperConfiguration.java b/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperConfiguration.java index e2a7c05f..741ce163 100644 --- a/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperConfiguration.java +++ b/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperConfiguration.java @@ -283,4 +283,9 @@ OfficeStamperConfiguration setSpelParserConfiguration( NeedsTriFunctionImpl addCustomFunction( String name, Class class0, Class class1, Class class2 ); + + List getPostprocessors(); + + void addPostprocessor(PostProcessor postProcessor); + } diff --git a/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperException.java b/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperException.java index ba236709..05a28b87 100644 --- a/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperException.java +++ b/engine/src/main/java/pro/verron/officestamper/api/OfficeStamperException.java @@ -1,5 +1,8 @@ package pro.verron.officestamper.api; +import org.springframework.util.function.ThrowingFunction; + +import java.util.function.Function; import java.util.function.Supplier; /** @@ -56,4 +59,8 @@ public OfficeStamperException() { public static Supplier throwing(String message) { return () -> new OfficeStamperException(message); } + + public static Function throwing(ThrowingFunction function) { + return ThrowingFunction.of(function, OfficeStamperException::new); + } } diff --git a/engine/src/main/java/pro/verron/officestamper/api/Paragraph.java b/engine/src/main/java/pro/verron/officestamper/api/Paragraph.java index 45ec697f..2beb81bc 100644 --- a/engine/src/main/java/pro/verron/officestamper/api/Paragraph.java +++ b/engine/src/main/java/pro/verron/officestamper/api/Paragraph.java @@ -4,6 +4,7 @@ import org.docx4j.wml.P; import org.docx4j.wml.R; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -107,5 +108,5 @@ public interface Paragraph { */ Optional parent(Class aClass); - Optional getComment(); + Collection getComment(); } diff --git a/engine/src/main/java/pro/verron/officestamper/api/PostProcessor.java b/engine/src/main/java/pro/verron/officestamper/api/PostProcessor.java new file mode 100644 index 00000000..c44625bb --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/api/PostProcessor.java @@ -0,0 +1,7 @@ +package pro.verron.officestamper.api; + +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; + +public interface PostProcessor { + void process(WordprocessingMLPackage document); +} 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 c8b11a3c..2fc6d057 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/CommentProcessorRegistry.java +++ b/engine/src/main/java/pro/verron/officestamper/core/CommentProcessorRegistry.java @@ -63,16 +63,16 @@ public void runProcessors(T expressionContext) { var comments = collectComments(); var runParent = StandardParagraph.from(source, (P) run.getParent()); var optional = runProcessorsOnRunComment(comments, expressionContext, run, runParent); - commentProcessors.commitChanges(source); optional.ifPresent(proceedComments::add); }); + commentProcessors.commitChanges(source); // we run the paragraph afterward so that the comments inside work before the whole paragraph comments source.streamParagraphs() .forEach(p -> { var comments = collectComments(); var paragraphComment = p.getComment(); - paragraphComment.ifPresent((pc -> { + paragraphComment.forEach((pc -> { var optional = runProcessorsOnParagraphComment(comments, expressionContext, p, pc.getId()); commentProcessors.commitChanges(source); optional.ifPresent(proceedComments::add); diff --git a/engine/src/main/java/pro/verron/officestamper/core/CommentUtil.java b/engine/src/main/java/pro/verron/officestamper/core/CommentUtil.java index 3aa5617d..24219b1e 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/CommentUtil.java +++ b/engine/src/main/java/pro/verron/officestamper/core/CommentUtil.java @@ -52,7 +52,7 @@ private CommentUtil() { * @return Optional of the comment, if found, Optional.empty() otherwise. */ public static Optional getCommentAround(R run, WordprocessingMLPackage document) { - ContentAccessor parent = (ContentAccessor) ((Child) run).getParent(); + ContentAccessor parent = (ContentAccessor) run.getParent(); if (parent == null) return Optional.empty(); return getComment(run, document, parent); } @@ -99,16 +99,6 @@ private static Optional findComment(WordprocessingMLPackage do } - /** - * Retrieves the CommentsPart from the given Parts object. - * - * @param parts the Parts object containing the various parts of the document. - * @return an Optional containing the CommentsPart if found, or an empty Optional if not found. - */ - public static Optional getCommentsPart(Parts parts) { - return Optional.ofNullable((CommentsPart) parts.get(WORD_COMMENTS_PART_NAME)); - } - /** * Retrieves the comment associated with a given paragraph content within a WordprocessingMLPackage document. * @@ -118,7 +108,7 @@ public static Optional getCommentsPart(Parts parts) { * @return an Optional containing the found comment, or Optional.empty() if no comment is associated with the given * paragraph content. */ - public static Optional getCommentFor( + public static Collection getCommentFor( List paragraphContent, WordprocessingMLPackage document ) { var comments = getCommentsPart(document.getParts()).map(CommentUtil::extractContent) @@ -130,9 +120,28 @@ public static Optional getCommentFor( return paragraphContent.stream() .filter(CommentRangeStart.class::isInstance) .map(CommentRangeStart.class::cast) - .findFirst() .map(CommentRangeStart::getId) - .flatMap(commentId -> findCommentById(comments, commentId)); + .flatMap(commentId -> findCommentById(comments, commentId).stream()) + .toList(); + } + + /** + * Retrieves the CommentsPart from the given Parts object. + * + * @param parts the Parts object containing the various parts of the document. + * + * @return an Optional containing the CommentsPart if found, or an empty Optional if not found. + */ + public static Optional getCommentsPart(Parts parts) { + return Optional.ofNullable((CommentsPart) parts.get(WORD_COMMENTS_PART_NAME)); + } + + public static Comments extractContent(CommentsPart commentsPart) { + try { + return commentsPart.getContents(); + } catch (Docx4JException e) { + throw new OfficeStamperException("Error while searching comment.", e); + } } private static Optional findCommentById(List comments, BigInteger id) { @@ -270,12 +279,4 @@ private static Comments extractComments(Set commentChildren) { } return newComments(list); } - - public static Comments extractContent(CommentsPart commentsPart) { - try { - return commentsPart.getContents(); - } catch (Docx4JException e) { - throw new OfficeStamperException("Error while searching comment.", e); - } - } } diff --git a/engine/src/main/java/pro/verron/officestamper/core/DocumentUtil.java b/engine/src/main/java/pro/verron/officestamper/core/DocumentUtil.java index 140d66b7..d95cff5f 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/DocumentUtil.java +++ b/engine/src/main/java/pro/verron/officestamper/core/DocumentUtil.java @@ -4,16 +4,24 @@ import org.docx4j.TraversalUtil; import org.docx4j.XmlUtils; import org.docx4j.finders.ClassFinder; +import org.docx4j.model.structure.HeaderFooterPolicy; +import org.docx4j.model.structure.SectionWrapper; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.JaxbXmlPart; import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage; +import org.docx4j.utils.TraversalUtilVisitor; import org.docx4j.wml.*; import org.jvnet.jaxb2_commons.ppp.Child; +import org.springframework.lang.Nullable; +import org.springframework.util.function.ThrowingFunction; import pro.verron.officestamper.api.DocxPart; import pro.verron.officestamper.api.OfficeStamperException; import java.util.*; import java.util.stream.Stream; +import static java.util.Optional.ofNullable; +import static java.util.stream.Stream.Builder; import static pro.verron.officestamper.utils.WmlFactory.newRun; /** @@ -30,10 +38,7 @@ private DocumentUtil() { throw new OfficeStamperException("Utility classes shouldn't be instantiated"); } - public static Stream streamObjectElements( - DocxPart source, - Class elementClass - ) { + public static Stream streamObjectElements(DocxPart source, Class elementClass) { ClassFinder finder = new ClassFinder(elementClass); TraversalUtil.visit(source.part(), finder); return finder.results.stream() @@ -60,13 +65,8 @@ public static List allElements(WordprocessingMLPackage subDocument) { * * @return a {@link Map} object */ - public static Map walkObjectsAndImportImages( - WordprocessingMLPackage source, - WordprocessingMLPackage target - ) { - return walkObjectsAndImportImages(source.getMainDocumentPart(), - source, - target); + public static Map walkObjectsAndImportImages(WordprocessingMLPackage source, WordprocessingMLPackage target) { + return walkObjectsAndImportImages(source.getMainDocumentPart(), source, target); } /** @@ -122,10 +122,7 @@ private static boolean isImageRun(R run) { .anyMatch(Drawing.class::isInstance); } - private static BinaryPartAbstractImage tryCreateImagePart( - WordprocessingMLPackage destDocument, - byte[] imageData - ) { + private static BinaryPartAbstractImage tryCreateImagePart(WordprocessingMLPackage destDocument, byte[] imageData) { try { return BinaryPartAbstractImage.createImagePart(destDocument, imageData); } catch (Exception e) { @@ -146,10 +143,8 @@ private static BinaryPartAbstractImage tryCreateImagePart( public static ContentAccessor findSmallestCommonParent(Object o1, Object o2) { if (depthElementSearch(o1, o2) && o2 instanceof ContentAccessor contentAccessor) return findInsertableParent(contentAccessor); - else if (o2 instanceof Child child) - return findSmallestCommonParent(o1, child.getParent()); - else - throw new OfficeStamperException(); + else if (o2 instanceof Child child) return findSmallestCommonParent(o1, child.getParent()); + else throw new OfficeStamperException(); } /** @@ -168,8 +163,7 @@ public static boolean depthElementSearch(Object searchTarget, Object content) { else if (content instanceof ContentAccessor contentAccessor) { for (Object object : contentAccessor.getContent()) { Object unwrappedObject = XmlUtils.unwrap(object); - if (searchTarget.equals(unwrappedObject) - || depthElementSearch(searchTarget, unwrappedObject)) { + if (searchTarget.equals(unwrappedObject) || depthElementSearch(searchTarget, unwrappedObject)) { return true; } } @@ -185,4 +179,38 @@ private static ContentAccessor findInsertableParent(Object searchFrom) { default -> throw new OfficeStamperException("Unexpected parent " + searchFrom.getClass()); }; } + + public static void visitDocument(WordprocessingMLPackage document, TraversalUtilVisitor visitor) { + var mainDocumentPart = document.getMainDocumentPart(); + TraversalUtil.visit(mainDocumentPart, visitor); + streamHeaderFooterPart(document).forEach(f -> TraversalUtil.visit(f, visitor)); + visitPartIfExists(visitor, mainDocumentPart.getFootnotesPart()); + visitPartIfExists(visitor, mainDocumentPart.getEndNotesPart()); + } + + private static Stream streamHeaderFooterPart(WordprocessingMLPackage document) { + return document.getDocumentModel() + .getSections() + .stream() + .map(SectionWrapper::getHeaderFooterPolicy) + .flatMap(DocumentUtil::extractHeaderFooterParts); + } + + private static void visitPartIfExists(TraversalUtilVisitor visitor, @Nullable JaxbXmlPart part) { + ThrowingFunction, Object> throwingFunction = JaxbXmlPart::getContents; + Optional.ofNullable(part) + .map(c -> throwingFunction.apply(c, OfficeStamperException::new)) + .ifPresent(c -> TraversalUtil.visit(c, visitor)); + } + + private static Stream> extractHeaderFooterParts(HeaderFooterPolicy hfp) { + Builder> builder = Stream.builder(); + ofNullable(hfp.getFirstHeader()).ifPresent(builder::add); + ofNullable(hfp.getDefaultHeader()).ifPresent(builder::add); + ofNullable(hfp.getEvenHeader()).ifPresent(builder::add); + ofNullable(hfp.getFirstFooter()).ifPresent(builder::add); + ofNullable(hfp.getDefaultFooter()).ifPresent(builder::add); + ofNullable(hfp.getEvenFooter()).ifPresent(builder::add); + return builder.build(); + } } diff --git a/engine/src/main/java/pro/verron/officestamper/core/DocxStamper.java b/engine/src/main/java/pro/verron/officestamper/core/DocxStamper.java index 53b9a1b2..863261a6 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/DocxStamper.java +++ b/engine/src/main/java/pro/verron/officestamper/core/DocxStamper.java @@ -3,7 +3,6 @@ import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.relationships.Namespaces; -import org.springframework.expression.TypedValue; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -12,7 +11,10 @@ import java.io.InputStream; import java.io.OutputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.Function; import static pro.verron.officestamper.core.Invokers.streamInvokers; @@ -29,6 +31,7 @@ public class DocxStamper implements OfficeStamper { private final List preprocessors; + private final List postprocessors; private final PlaceholderReplacer placeholderReplacer; private final Function commentProcessorRegistrySupplier; @@ -44,8 +47,10 @@ public DocxStamper(OfficeStamperConfiguration configuration) { configuration.getResolvers(), configuration.getCommentProcessors(), configuration.getPreprocessors(), + configuration.getPostprocessors(), configuration.getSpelParserConfiguration(), - configuration.getExceptionResolver()); + configuration.getExceptionResolver() + ); } private DocxStamper( @@ -56,6 +61,7 @@ private DocxStamper( List resolvers, Map, Function> configurationCommentProcessors, List preprocessors, + List postprocessors, SpelParserConfiguration spelParserConfiguration, ExceptionResolver exceptionResolver ) { @@ -75,9 +81,8 @@ private DocxStamper( var commentProcessors = buildCommentProcessors(configurationCommentProcessors); evaluationContext.addMethodResolver(new Invokers(streamInvokers(commentProcessors))); evaluationContext.addMethodResolver(new Invokers(streamInvokers(expressionFunctions))); - evaluationContext.addMethodResolver(new Invokers(functions.stream().map(cf -> new Invoker(cf.name(), - new Invokers.Args(cf.parameterTypes()), - (context, target, arguments) -> new TypedValue(cf.function().apply(Arrays.asList(arguments))))))); + evaluationContext.addMethodResolver(new Invokers(functions.stream() + .map(Invokers::ofCustomFunction))); this.commentProcessorRegistrySupplier = source -> new CommentProcessorRegistry( source, @@ -86,6 +91,7 @@ private DocxStamper( exceptionResolver); this.preprocessors = new ArrayList<>(preprocessors); + this.postprocessors = new ArrayList<>(postprocessors); } private CommentProcessors buildCommentProcessors( @@ -119,7 +125,7 @@ private CommentProcessors buildCommentProcessors( /// within the table cells against one of the objects within the list. /// /// If you need a wider vocabulary of methods available in the comments, you can create your own ICommentProcessor - /// and register it via [#addCommentProcessor(Class,Function)]. + /// and register it via [OfficeStamperConfiguration#addCommentProcessor(Class, Function)]. public void stamp(InputStream template, Object contextRoot, OutputStream out) { try { WordprocessingMLPackage document = WordprocessingMLPackage.load(template); @@ -130,14 +136,16 @@ public void stamp(InputStream template, Object contextRoot, OutputStream out) { } - /// Same as [#stamp(InputStream,Object,OutputStream)] except that you + /// Same as [#stamp(InputStream, Object, OutputStream)] except that you /// may pass in a DOCX4J document as a template instead of an InputStream. - @Override public void stamp(WordprocessingMLPackage document, Object contextRoot, OutputStream out) { + @Override + public void stamp(WordprocessingMLPackage document, Object contextRoot, OutputStream out) { try { var source = new TextualDocxPart(document); preprocess(document); processComments(source, contextRoot); replaceExpressions(source, contextRoot); + postprocess(document); document.save(out); } catch (Docx4JException e) { throw new OfficeStamperException(e); @@ -145,9 +153,7 @@ public void stamp(InputStream template, Object contextRoot, OutputStream out) { } private void preprocess(WordprocessingMLPackage document) { - for (PreProcessor preprocessor : preprocessors) { - preprocessor.process(document); - } + preprocessors.forEach(processor -> processor.process(document)); } private void processComments(DocxPart document, Object contextObject) { @@ -170,4 +176,8 @@ private void runProcessors(DocxPart source, Object contextObject) { var processors = commentProcessorRegistrySupplier.apply(source); processors.runProcessors(contextObject); } + + private void postprocess(WordprocessingMLPackage document) { + postprocessors.forEach(processor -> processor.process(document)); + } } diff --git a/engine/src/main/java/pro/verron/officestamper/core/DocxStamperConfiguration.java b/engine/src/main/java/pro/verron/officestamper/core/DocxStamperConfiguration.java index 8b3944bd..6150e0ee 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/DocxStamperConfiguration.java +++ b/engine/src/main/java/pro/verron/officestamper/core/DocxStamperConfiguration.java @@ -1,7 +1,6 @@ package pro.verron.officestamper.core; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.lang.NonNull; import pro.verron.officestamper.api.*; @@ -10,10 +9,8 @@ import pro.verron.officestamper.core.functions.BiFunctionBuilder; import pro.verron.officestamper.core.functions.FunctionBuilder; import pro.verron.officestamper.core.functions.TriFunctionBuilder; -import pro.verron.officestamper.preset.CommentProcessorFactory; import pro.verron.officestamper.preset.EvaluationContextConfigurers; import pro.verron.officestamper.preset.ExceptionResolvers; -import pro.verron.officestamper.preset.Resolvers; import java.util.ArrayList; import java.util.HashMap; @@ -22,179 +19,145 @@ import java.util.function.Function; import java.util.function.Supplier; -/** - * The {@link DocxStamperConfiguration} class represents the configuration for - * the {@link DocxStamper} class. - * It provides methods to customize the behavior of the stamper. - * - * @author Joseph Verron - * @author Tom Hombergs - * @version ${version} - * @since 1.0.3 - */ +/// The [DocxStamperConfiguration] class represents the configuration for the [DocxStamper] class. +/// It provides methods to customize the behavior of the stamper. +/// +/// @author Joseph Verron +/// @author Tom Hombergs +/// @version ${version} +/// @since 1.0.3 public class DocxStamperConfiguration implements OfficeStamperConfiguration { - private final Map, Function> commentProcessors = - new HashMap<>(); - private final List resolvers = new ArrayList<>(); - private final Map, Object> expressionFunctions = new HashMap<>(); - private final List preprocessors = new ArrayList<>(); - private final List functions = new ArrayList<>(); - private String lineBreakPlaceholder = "\n"; - private EvaluationContextConfigurer evaluationContextConfigurer = EvaluationContextConfigurers.defaultConfigurer(); - private boolean failOnUnresolvedExpression = true; - private boolean leaveEmptyOnExpressionError = false; - private boolean replaceUnresolvedExpressions = false; - private String unresolvedExpressionsDefaultValue = null; - private SpelParserConfiguration spelParserConfiguration = new SpelParserConfiguration(); - private ExceptionResolver exceptionResolver = computeExceptionResolver(); - - /** - * Creates a new configuration with default values. - */ + private final Map, Function> commentProcessors; + private final List resolvers; + private final Map, Object> expressionFunctions; + private final List preprocessors; + private final List postprocessors; + private final List functions; + private String lineBreakPlaceholder; + private EvaluationContextConfigurer evaluationContextConfigurer; + private boolean failOnUnresolvedExpression; + private boolean leaveEmptyOnExpressionError; + private boolean replaceUnresolvedExpressions; + private String unresolvedExpressionsDefaultValue; + private SpelParserConfiguration spelParserConfiguration; + private ExceptionResolver exceptionResolver; + public DocxStamperConfiguration() { - CommentProcessorFactory pf = new CommentProcessorFactory(this); - commentProcessors.put(CommentProcessorFactory.IRepeatProcessor.class, pf::repeat); - commentProcessors.put(CommentProcessorFactory.IParagraphRepeatProcessor.class, pf::repeatParagraph); - commentProcessors.put(CommentProcessorFactory.IRepeatDocPartProcessor.class, pf::repeatDocPart); - commentProcessors.put(CommentProcessorFactory.ITableResolver.class, pf::tableResolver); - commentProcessors.put(CommentProcessorFactory.IDisplayIfProcessor.class, pf::displayIf); - commentProcessors.put(CommentProcessorFactory.IReplaceWithProcessor.class, pf::replaceWith); - - resolvers.addAll(List.of(Resolvers.image(), - Resolvers.legacyDate(), - Resolvers.isoDate(), - Resolvers.isoTime(), - Resolvers.isoDateTime(), - Resolvers.nullToEmpty(), - Resolvers.fallback())); - } - - /** - * Resets all the comment processors in the configuration. This method clears the - * map of comment processors, effectively removing all registered comment processors. - * Comment processors are used to process comments within the document. - */ + commentProcessors = new HashMap<>(); + resolvers = new ArrayList<>(); + expressionFunctions = new HashMap<>(); + preprocessors = new ArrayList<>(); + postprocessors = new ArrayList<>(); + functions = new ArrayList<>(); + evaluationContextConfigurer = EvaluationContextConfigurers.defaultConfigurer(); + lineBreakPlaceholder = "\n"; + failOnUnresolvedExpression = true; + leaveEmptyOnExpressionError = false; + replaceUnresolvedExpressions = false; + unresolvedExpressionsDefaultValue = null; + spelParserConfiguration = new SpelParserConfiguration(); + exceptionResolver = computeExceptionResolver(); + } + + private ExceptionResolver computeExceptionResolver() { + if (failOnUnresolvedExpression) return ExceptionResolvers.throwing(); + if (replaceWithDefaultOnError()) return ExceptionResolvers.defaulting(replacementDefault()); + return ExceptionResolvers.passing(); + } + + private boolean replaceWithDefaultOnError() { + return isLeaveEmptyOnExpressionError() || isReplaceUnresolvedExpressions(); + } + + private String replacementDefault() { + return isLeaveEmptyOnExpressionError() ? "" : getUnresolvedExpressionsDefaultValue(); + } + + /// Resets all processors in the configuration. public void resetCommentProcessors() { this.commentProcessors.clear(); } - /** - * Resets all the resolvers in the DocxStamperConfiguration object. - * This method clears the list of resolvers, effectively removing all registered resolvers. - * Resolvers are used to resolve objects during the stamping process. - */ + /// Resets all resolvers in the configuration. public void resetResolvers() { this.resolvers.clear(); } - /** - *

isFailOnUnresolvedExpression.

- * - * @return a boolean - */ - @Deprecated(since = "2.5", forRemoval = true) @Override public boolean isFailOnUnresolvedExpression() { + @Deprecated(since = "2.5", forRemoval = true) + @Override + public boolean isFailOnUnresolvedExpression() { return failOnUnresolvedExpression; } - /** - * If set to true, stamper will throw an {@link OfficeStamperException} - * if a variable expression or processor expression within the document or within the comments is encountered that - * cannot be resolved. Is set to true by default. - * - * @param failOnUnresolvedExpression a boolean - * - * @return a {@link DocxStamperConfiguration} object - */ - @Deprecated(since = "2.5", forRemoval = true) @Override + /// If true, stamper throws an [OfficeStamperException] if an expression within the document can’t be resolved. + /// Set to `TRUE` by default. + /// + /// @param failOnUnresolvedExpression a boolean + /// + /// @return the same [DocxStamperConfiguration] object + @Deprecated(since = "2.5", forRemoval = true) + @Override public DocxStamperConfiguration setFailOnUnresolvedExpression(boolean failOnUnresolvedExpression) { this.failOnUnresolvedExpression = failOnUnresolvedExpression; this.exceptionResolver = computeExceptionResolver(); return this; } - private ExceptionResolver computeExceptionResolver() { - if (failOnUnresolvedExpression) return ExceptionResolvers.throwing(); - if (replaceWithDefaultOnError()) return ExceptionResolvers.defaulting(replacementDefault()); - return ExceptionResolvers.passing(); - } - - private boolean replaceWithDefaultOnError() { - return isLeaveEmptyOnExpressionError() || isReplaceUnresolvedExpressions(); - } - - private String replacementDefault() { - return isLeaveEmptyOnExpressionError() ? "" : getUnresolvedExpressionsDefaultValue(); - } - - /** - *

isLeaveEmptyOnExpressionError.

- * - * @return a boolean - */ - @Override public boolean isLeaveEmptyOnExpressionError() { + @Override + public boolean isLeaveEmptyOnExpressionError() { return leaveEmptyOnExpressionError; } - /** - *

isReplaceUnresolvedExpressions.

- * - * @return a boolean - */ - @Override public boolean isReplaceUnresolvedExpressions() { + @Override + public boolean isReplaceUnresolvedExpressions() { return replaceUnresolvedExpressions; } - /** - *

Getter for the field unresolvedExpressionsDefaultValue.

- * - * @return a {@link String} object - */ - @Override public String getUnresolvedExpressionsDefaultValue() { + @Override + public String getUnresolvedExpressionsDefaultValue() { return unresolvedExpressionsDefaultValue; } - /** - * Indicates the default value to use for expressions that doesn't resolve. - * - * @param unresolvedExpressionsDefaultValue value to use instead for expression that doesn't resolve - * - * @return a {@link DocxStamperConfiguration} object - * - * @see DocxStamperConfiguration#replaceUnresolvedExpressions - */ - @Deprecated(since = "2.5", forRemoval = true) @Override + /// Default value to use for expressions that doesn't resolve. + /// + /// @param unresolvedExpressionsDefaultValue value to use instead for expression that doesn't resolve + /// + /// @return a [DocxStamperConfiguration] object + /// + /// @see DocxStamperConfiguration#replaceUnresolvedExpressions + @Deprecated(since = "2.5", forRemoval = true) + @Override public DocxStamperConfiguration unresolvedExpressionsDefaultValue(String unresolvedExpressionsDefaultValue) { this.unresolvedExpressionsDefaultValue = unresolvedExpressionsDefaultValue; this.exceptionResolver = computeExceptionResolver(); return this; } - /** - * Indicates if a default value should replace expressions that don't resolve. - * - * @param replaceUnresolvedExpressions true to replace null value expression with resolved value (which is null), - * false to leave the expression as is - * - * @return a {@link DocxStamperConfiguration} object - */ - @Deprecated(since = "2.5", forRemoval = true) @Override + /// Indicates if a default value should replace expressions that don't resolve. + /// + /// @param replaceUnresolvedExpressions true to replace expression with resolved value `null` + /// + /// + /// false to leave the expression as is. + /// + /// @return a [DocxStamperConfiguration] object + @Deprecated(since = "2.5", forRemoval = true) + @Override public DocxStamperConfiguration replaceUnresolvedExpressions(boolean replaceUnresolvedExpressions) { this.replaceUnresolvedExpressions = replaceUnresolvedExpressions; this.exceptionResolver = computeExceptionResolver(); return this; } - /** - * If an error is caught while evaluating an expression, the expression will be replaced with an empty string - * instead - * of leaving the original expression in the document. - * - * @param leaveEmpty true to replace expressions with empty string when an error is caught while evaluating - * - * @return a {@link DocxStamperConfiguration} object - */ - @Deprecated(since = "2.5", forRemoval = true) @Override public DocxStamperConfiguration leaveEmptyOnExpressionError( + /// Indicate if expressions failing during evaluation needs removal. + /// + /// @param leaveEmpty true to replace expressions with empty string when an error occurs during evaluation. + /// + /// @return a [DocxStamperConfiguration] object + @Deprecated(since = "2.5", forRemoval = true) + @Override + public DocxStamperConfiguration leaveEmptyOnExpressionError( boolean leaveEmpty ) { this.leaveEmptyOnExpressionError = leaveEmpty; @@ -202,225 +165,204 @@ public DocxStamperConfiguration replaceUnresolvedExpressions(boolean replaceUnre return this; } - /** - * Exposes all methods of a given interface to the expression language. - * - * @param interfaceClass the interface whose methods should be exposed in the expression language. - * @param implementation the implementation that should be called to evaluate invocations of the interface methods - * within the expression language. Must implement the interface above. - * - * @return a {@link DocxStamperConfiguration} object - */ - @Override public DocxStamperConfiguration exposeInterfaceToExpressionLanguage( - Class interfaceClass, Object implementation + /// Exposes all methods of a given interface to the expression language. + /// + /// @param interfaceClass the interface holding methods to expose in the expression language. + /// @param implementation the implementation to call to evaluate invocations of those methods. + /// + /// Must implement the + /// mentioned interface. + /// + /// @return a [DocxStamperConfiguration] object + @Override + public DocxStamperConfiguration exposeInterfaceToExpressionLanguage( + Class interfaceClass, + Object implementation ) { this.expressionFunctions.put(interfaceClass, implementation); return this; } - /** - * Registers the specified ICommentProcessor as an implementation of the - * specified interface. - * - * @param interfaceClass the Interface which is implemented by the commentProcessor. - * @param commentProcessorFactory the commentProcessor factory generating the specified interface. - * - * @return a {@link DocxStamperConfiguration} object - */ - @Override public DocxStamperConfiguration addCommentProcessor( - Class interfaceClass, Function commentProcessorFactory + /// Registers the specified ICommentProcessor as an implementation of the specified interface. + /// + /// @param interfaceClass the interface, implemented by the commentProcessor. + /// @param commentProcessorFactory the commentProcessor factory generating instances of the specified interface. + /// + /// @return a [DocxStamperConfiguration] object + @Override + public DocxStamperConfiguration addCommentProcessor( + Class interfaceClass, + Function commentProcessorFactory ) { this.commentProcessors.put(interfaceClass, commentProcessorFactory); return this; } - /** - * Adds a preprocessor to the configuration. - * - * @param preprocessor the preprocessor to add. - */ - @Override public void addPreprocessor(PreProcessor preprocessor) { + /// Adds a preprocessor to the configuration. + /// + /// @param preprocessor the preprocessor to add. + @Override + public void addPreprocessor(PreProcessor preprocessor) { preprocessors.add(preprocessor); } - /** - *

Getter for the field lineBreakPlaceholder.

- * - * @return a {@link String} object - */ - @Override public String getLineBreakPlaceholder() { + + @Override + public String getLineBreakPlaceholder() { return lineBreakPlaceholder; } - /** - * The String provided as lineBreakPlaceholder will be replaced with a line break - * when stamping a document. If no lineBreakPlaceholder is provided, no replacement - * will take place. - * - * @param lineBreakPlaceholder the String that should be replaced with line breaks during stamping. - * - * @return the configuration object for chaining. - */ - @Override public DocxStamperConfiguration setLineBreakPlaceholder(@NonNull String lineBreakPlaceholder) { + /// String to replace with a line break when stamping a document. + /// By default, `\\n` is the placeholder. + /// + /// @param lineBreakPlaceholder string to replace with line breaks during stamping. + /// + /// @return the configuration object for chaining. + @Override + public DocxStamperConfiguration setLineBreakPlaceholder(@NonNull String lineBreakPlaceholder) { this.lineBreakPlaceholder = lineBreakPlaceholder; return this; } - /** - *

Getter for the field evaluationContextConfigurer.

- * - * @return a {@link EvaluationContextConfigurer} object - */ - @Override public EvaluationContextConfigurer getEvaluationContextConfigurer() { + @Override + public EvaluationContextConfigurer getEvaluationContextConfigurer() { return evaluationContextConfigurer; } - /** - * Provides an {@link EvaluationContextConfigurer} which may change the configuration of a Spring - * {@link EvaluationContext} which is used for evaluating expressions - * in comments and text. - * - * @param evaluationContextConfigurer the configurer to use. - * - * @return a {@link DocxStamperConfiguration} object - */ - @Override public DocxStamperConfiguration setEvaluationContextConfigurer( + /// Provides an [EvaluationContextConfigurer] which may change the configuration of a Spring + /// [EvaluationContext] used for evaluating expressions in comments and text. + /// + /// @param evaluationContextConfigurer the configurer to use. + /// + /// @return the configuration object for chaining. + @Override + public DocxStamperConfiguration setEvaluationContextConfigurer( EvaluationContextConfigurer evaluationContextConfigurer ) { this.evaluationContextConfigurer = evaluationContextConfigurer; return this; } - /** - *

Getter for the field spelParserConfiguration.

- * - * @return a {@link SpelParserConfiguration} object - */ - @Override public SpelParserConfiguration getSpelParserConfiguration() { + @Override + public SpelParserConfiguration getSpelParserConfiguration() { return spelParserConfiguration; } - /** - * Sets the {@link SpelParserConfiguration} to use for expression parsing. - *

- * Note that this configuration will be used for all expressions in the document, including expressions in comments! - *

- * - * @param spelParserConfiguration the configuration to use. - * - * @return a {@link DocxStamperConfiguration} object - */ - @Override public DocxStamperConfiguration setSpelParserConfiguration( + /// Sets the [SpelParserConfiguration] used for expression parsing. + /// Note that this configuration is the same for all expressions in the document, including expressions in comments. + /// + /// @param spelParserConfiguration the configuration to use. + /// + /// @return the configuration object for chaining. + @Override + public DocxStamperConfiguration setSpelParserConfiguration( SpelParserConfiguration spelParserConfiguration ) { this.spelParserConfiguration = spelParserConfiguration; return this; } - /** - *

Getter for the field expressionFunctions.

- * - * @return a {@link Map} object - */ - @Override public Map, Object> getExpressionFunctions() { + @Override + public Map, Object> getExpressionFunctions() { return expressionFunctions; } - /** - *

Getter for the field commentProcessors.

- * - * @return a {@link Map} object - */ - @Override public Map, Function> getCommentProcessors() { + @Override + public Map, Function> getCommentProcessors() { return commentProcessors; } - /** - *

Getter for the field preprocessors.

- * - * @return a {@link List} object - */ - @Override public List getPreprocessors() { + @Override + public List getPreprocessors() { return preprocessors; } - /** - * Retrieves the list of resolvers. - * - * @return The list of object resolvers. - */ - @Override public List getResolvers() { + @Override + public List getResolvers() { return resolvers; } - /** - * Sets the resolvers for resolving objects in the DocxStamperConfiguration. - *

- * This method is the evolution of the method {@code addTypeResolver}, - * and the order in which the resolvers are ordered is determinant - the first resolvers - * in the list will be tried first. If a fallback resolver is desired, it should be placed last in the list. - * - * @param resolvers The list of ObjectResolvers to be set. - * - * @return The updated DocxStamperConfiguration instance. - */ - @Override public DocxStamperConfiguration setResolvers( - List resolvers - ) { + /// Sets resolvers for resolving objects in the DocxStamperConfiguration. + /// + /// This method is the evolution of the method `addTypeResolver`, + /// and the order in which the resolvers are ordered is determinant - the first resolvers + /// in the list will be tried first. If a fallback resolver is desired, it should be placed last in the list. + /// + /// @param resolvers The list of ObjectResolvers to be set. + /// + /// @return the configuration object for chaining. + @Override + public DocxStamperConfiguration setResolvers(List resolvers) { this.resolvers.clear(); this.resolvers.addAll(resolvers); return this; } - /** - * Adds a resolver to the list of resolvers in the `DocxStamperConfiguration` object. - * Resolvers are used to resolve objects during the stamping process. - * - * @param resolver The resolver to be added. This resolver should implement the `ObjectResolver` interface. - * - * @return The modified `DocxStamperConfiguration` object, with the resolver added to the beginning of the - * resolver list. - */ - @Override public DocxStamperConfiguration addResolver(ObjectResolver resolver) { + /// Adds a resolver to the list of resolvers in the `DocxStamperConfiguration` object. + /// Resolvers are used to resolve objects during the stamping process. + /// + /// @param resolver The resolver to be added. This resolver should implement the `ObjectResolver` interface. + /// + /// @return The modified `DocxStamperConfiguration` object, with the resolver added to the beginning of the + /// resolver list. + @Override + public DocxStamperConfiguration addResolver(ObjectResolver resolver) { resolvers.addFirst(resolver); return this; } - @Override public ExceptionResolver getExceptionResolver() { + @Override + public ExceptionResolver getExceptionResolver() { return exceptionResolver; } - @Override public DocxStamperConfiguration setExceptionResolver(ExceptionResolver exceptionResolver) { + @Override + public DocxStamperConfiguration setExceptionResolver(ExceptionResolver exceptionResolver) { this.exceptionResolver = exceptionResolver; return this; } - public void addCustomFunction(CustomFunction function) { - this.functions.add(function); - } - - @Override public List customFunctions() { + @Override + public List customFunctions() { return functions; } - - @Override public void addCustomFunction(String name, Supplier implementation) { + @Override + public void addCustomFunction(String name, Supplier implementation) { this.addCustomFunction(new CustomFunction(name, List.of(), args -> implementation.get())); } + public void addCustomFunction(CustomFunction function) { + this.functions.add(function); + } - @Override public NeedsFunctionImpl addCustomFunction(String name, Class class0) { + @Override + public NeedsFunctionImpl addCustomFunction(String name, Class class0) { return new FunctionBuilder<>(this, name, class0); } - @Override public NeedsBiFunctionImpl addCustomFunction(String name, Class class0, Class class1) { + @Override + public NeedsBiFunctionImpl addCustomFunction(String name, Class class0, Class class1) { return new BiFunctionBuilder<>(this, name, class0, class1); } - @Override public CustomFunction.NeedsTriFunctionImpl addCustomFunction( + @Override + public CustomFunction.NeedsTriFunctionImpl addCustomFunction( String name, - Class class0, Class class1, Class class2 + Class class0, + Class class1, + Class class2 ) { - return new TriFunctionBuilder<>(this, name, class0, class1,class2); + return new TriFunctionBuilder<>(this, name, class0, class1, class2); + } + + @Override + public List getPostprocessors() { + return postprocessors; + } + + @Override + public void addPostprocessor(PostProcessor postprocessor) { + postprocessors.add(postprocessor); } } diff --git a/engine/src/main/java/pro/verron/officestamper/core/Invokers.java b/engine/src/main/java/pro/verron/officestamper/core/Invokers.java index d17d3f7b..24254f75 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/Invokers.java +++ b/engine/src/main/java/pro/verron/officestamper/core/Invokers.java @@ -1,25 +1,32 @@ package pro.verron.officestamper.core; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.expression.*; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.TypedValue; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import pro.verron.officestamper.api.CustomFunction; -import java.util.*; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.function.Function; import java.util.stream.Stream; +import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toMap; -/** - * Resolves methods that are used as expression functions or comment processors. - * - * @author Joseph Verron - * @version ${version} - * @since 1.6.2 - */ +/// Resolves methods used as expression functions or comment processors. +/// +/// @author Joseph Verron +/// @version ${version} +/// @since 1.6.2 public class Invokers implements MethodResolver { private final Map> map; @@ -42,15 +49,23 @@ private static Stream streamInvokers(Class key, Object obj) { return stream(key.getDeclaredMethods()).map(method -> new Invoker(obj, method)); } - /** {@inheritDoc} */ - @Override public MethodExecutor resolve( + static Invoker ofCustomFunction(CustomFunction cf) { + var cfName = cf.name(); + var cfArgs = new Args(cf.parameterTypes()); + var cfExecutor = new CustomFunctionExecutor(cf.function()); + return new Invoker(cfName, cfArgs, cfExecutor); + } + + @Override + public MethodExecutor resolve( @NonNull EvaluationContext context, @NonNull Object targetObject, @NonNull String name, @NonNull List argumentTypes ) { - List> argumentClasses = new ArrayList<>(); - argumentTypes.forEach(at -> argumentClasses.add(at.getType())); + var argumentClasses = argumentTypes.stream() + .map(this::typeDescriptor2Class) + .toList(); return map.getOrDefault(name, emptyMap()) .entrySet() .stream() @@ -61,8 +76,13 @@ private static Stream streamInvokers(Class key, Object obj) { .orElse(null); } + /// When null, consider it as compatible with any type argument, so return Any.class placeholder + private Class typeDescriptor2Class(@Nullable TypeDescriptor typeDescriptor) { + return typeDescriptor == null ? Any.class : typeDescriptor.getType(); + } + public record Args(List> sourceTypes) { - public boolean validate(List> searchedTypes) { + public boolean validate(List searchedTypes) { if (searchedTypes.size() != sourceTypes.size()) return false; var sourceTypesQ = new ArrayDeque<>(sourceTypes); @@ -71,9 +91,21 @@ public boolean validate(List> searchedTypes) { while (!sourceTypesQ.isEmpty() && valid) { Class parameterType = sourceTypesQ.remove(); Class searchedType = searchedTypesQ.remove(); - valid = parameterType.isAssignableFrom(searchedType); + valid = searchedType == Any.class || parameterType.isAssignableFrom(searchedType); } return valid; } } + + /// Represent a placeholder validating all other classes as possible candidate for validation + private class Any {} + + private record CustomFunctionExecutor(Function, Object> function) + implements MethodExecutor { + + @Override + public TypedValue execute(EvaluationContext context, Object target, Object... arguments) { + return new TypedValue(function.apply(asList(arguments))); + } + } } diff --git a/engine/src/main/java/pro/verron/officestamper/core/ObjectDeleter.java b/engine/src/main/java/pro/verron/officestamper/core/ObjectDeleter.java deleted file mode 100644 index 65062f4a..00000000 --- a/engine/src/main/java/pro/verron/officestamper/core/ObjectDeleter.java +++ /dev/null @@ -1,96 +0,0 @@ -package pro.verron.officestamper.core; - -import jakarta.xml.bind.JAXBElement; -import org.docx4j.wml.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import pro.verron.officestamper.api.OfficeStamperException; - -import java.util.Iterator; - -/** - * Utility class for deleting objects from a {@link Document}. - * - * @author Joseph Verron - * @author Tom Hombergs - * @version ${version} - * @since 1.0.0 - */ -public class ObjectDeleter { - private static final Logger log = LoggerFactory.getLogger(ObjectDeleter.class); - - private ObjectDeleter() { - throw new OfficeStamperException("Utility class shouldn't be instantiated"); - } - - /** - * Deletes the given paragraph from the document. - * - * @param paragraph the paragraph to delete. - */ - public static void deleteParagraph(P paragraph) { - if (paragraph.getParent() instanceof Tc parentCell) { - // paragraph within a table cell - ObjectDeleter.deleteFromCell(parentCell, paragraph); - } else { - ((ContentAccessor) paragraph.getParent()).getContent().remove(paragraph); - } - } - - /** - * Deletes the given table from the document. - * - * @param table the table to delete. - */ - public static void deleteTable(Tbl table) { - if (table.getParent() instanceof Tc parentCell) { - // nested table within a table cell - ObjectDeleter.deleteFromCell(parentCell, table); - } else { - // global table - ((ContentAccessor) table.getParent()).getContent().remove(table.getParent()); - // iterate through the containing list to find the jaxb element that contains the table. - for (Iterator iterator = ((ContentAccessor) table.getParent()).getContent() - .listIterator(); iterator.hasNext(); ) { - Object next = iterator.next(); - if (next instanceof JAXBElement element && element.getValue().equals(table)) { - iterator.remove(); - break; - } - } - } - } - - private static void deleteFromCell(Tc cell, P paragraph) { - cell.getContent().remove(paragraph); - if (TableCellUtil.hasNoParagraphOrTable(cell)) { - TableCellUtil.addEmptyParagraph(cell); - } - // TODO: find out why border lines are removed in some cells after having deleted a paragraph - } - - private static void deleteFromCell(Tc cell, Object obj) { - if (!(obj instanceof Tbl || obj instanceof P)) { - throw new AssertionError("Only delete Tables or Paragraphs with this method."); - } - cell.getContent().remove(obj); - if (TableCellUtil.hasNoParagraphOrTable(cell)) { - TableCellUtil.addEmptyParagraph(cell); - } - // TODO: find out why border lines are removed in some cells after having deleted a paragraph - } - - /** - * Deletes the given table row from the document. - * - * @param tableRow the table row to delete. - */ - public static void deleteTableRow(Tr tableRow) { - if (tableRow.getParent() instanceof Tbl table) { - table.getContent().remove(tableRow); - } else { - log.error("Table row is not contained within a table. Unable to remove"); - } - } - -} diff --git a/engine/src/main/java/pro/verron/officestamper/core/RunUtil.java b/engine/src/main/java/pro/verron/officestamper/core/RunUtil.java index bda437a6..2e3d851c 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/RunUtil.java +++ b/engine/src/main/java/pro/verron/officestamper/core/RunUtil.java @@ -11,7 +11,8 @@ import java.util.Objects; import static java.util.stream.Collectors.joining; -import static pro.verron.officestamper.utils.WmlFactory.*; +import static pro.verron.officestamper.utils.WmlFactory.newRun; +import static pro.verron.officestamper.utils.WmlFactory.newText; /** * Utility class to handle runs. @@ -68,6 +69,8 @@ public static CharSequence getText(Object content) { case R.AnnotationRef ignored -> ""; case R.CommentReference ignored -> ""; case Drawing ignored -> ""; + case CTFtnEdnRef ref -> ref.getId() + .toString(); case R.Sym sym -> "".formatted(sym.getFont(), sym.getChar()); default -> { log.debug("Unhandled object type: {}", content.getClass()); diff --git a/engine/src/main/java/pro/verron/officestamper/core/StandardParagraph.java b/engine/src/main/java/pro/verron/officestamper/core/StandardParagraph.java index 2b9b2088..0318aa52 100644 --- a/engine/src/main/java/pro/verron/officestamper/core/StandardParagraph.java +++ b/engine/src/main/java/pro/verron/officestamper/core/StandardParagraph.java @@ -4,6 +4,7 @@ import org.docx4j.wml.*; import pro.verron.officestamper.api.*; import pro.verron.officestamper.utils.WmlFactory; +import pro.verron.officestamper.utils.WmlUtils; import java.math.BigInteger; import java.util.*; @@ -108,7 +109,7 @@ private Optional parent(Class aClass, int depth) { } @Override public void remove() { - ObjectDeleter.deleteParagraph(p); + WmlUtils.remove(p); } /** @@ -131,14 +132,10 @@ private Optional parent(Class aClass, int depth) { * @param replacement the object to replace the expression. */ @Override public void replace(Placeholder placeholder, Object replacement) { - if (replacement instanceof R run) { - replaceWithRun(placeholder, run); - } - else if (replacement instanceof Br br) { - replaceWithBr(placeholder, br); - } - else { - throw new AssertionError("Replacement must be a R or Br, but was a " + replacement.getClass()); + switch (replacement) { + case R run -> replaceWithRun(placeholder, run); + case Br br -> replaceWithBr(placeholder, br); + default -> throw new AssertionError("Replacement must be a R or Br, but was a " + replacement.getClass()); } } @@ -162,7 +159,8 @@ else if (replacement instanceof Br br) { return parent(aClass, Integer.MAX_VALUE); } - @Override public Optional getComment() { + @Override + public Collection getComment() { return CommentUtil.getCommentFor(contents, source.document()); } diff --git a/engine/src/main/java/pro/verron/officestamper/experimental/PowerpointParagraph.java b/engine/src/main/java/pro/verron/officestamper/experimental/PowerpointParagraph.java index d26d2b78..12a07705 100644 --- a/engine/src/main/java/pro/verron/officestamper/experimental/PowerpointParagraph.java +++ b/engine/src/main/java/pro/verron/officestamper/experimental/PowerpointParagraph.java @@ -9,7 +9,6 @@ import org.docx4j.wml.R; import pro.verron.officestamper.api.*; import pro.verron.officestamper.core.CommentUtil; -import pro.verron.officestamper.core.ObjectDeleter; import pro.verron.officestamper.core.StandardComment; import pro.verron.officestamper.utils.WmlFactory; import pro.verron.officestamper.utils.WmlUtils; @@ -18,6 +17,7 @@ import java.util.*; import java.util.function.Consumer; +import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.joining; import static pro.verron.officestamper.api.OfficeStamperException.throwing; @@ -83,14 +83,56 @@ private void addRun(CTRegularTextRun run, int index) { currentPosition = endIndex + 1; } - @Override public ProcessorContext processorContext(Placeholder placeholder) { + private static CTTextCharacterProperties apply( + CTTextCharacterProperties source, + CTTextCharacterProperties destination + ) { + ofNullable(source.getAltLang()).ifPresent(destination::setAltLang); + ofNullable(source.getBaseline()).ifPresent(destination::setBaseline); + ofNullable(source.getBmk()).ifPresent(destination::setBmk); + ofNullable(source.getBlipFill()).ifPresent(destination::setBlipFill); + ofNullable(source.getCap()).ifPresent(destination::setCap); + ofNullable(source.getCs()).ifPresent(destination::setCs); + ofNullable(source.getGradFill()).ifPresent(destination::setGradFill); + ofNullable(source.getGrpFill()).ifPresent(destination::setGrpFill); + ofNullable(source.getHighlight()).ifPresent(destination::setHighlight); + ofNullable(source.getHlinkClick()).ifPresent(destination::setHlinkClick); + ofNullable(source.getHlinkMouseOver()).ifPresent(destination::setHlinkMouseOver); + ofNullable(source.getKern()).ifPresent(destination::setKern); + ofNullable(source.getLang()).ifPresent(destination::setLang); + ofNullable(source.getLn()).ifPresent(destination::setLn); + ofNullable(source.getLatin()).ifPresent(destination::setLatin); + ofNullable(source.getNoFill()).ifPresent(destination::setNoFill); + ofNullable(source.getPattFill()).ifPresent(destination::setPattFill); + ofNullable(source.getSpc()).ifPresent(destination::setSpc); + ofNullable(source.getSym()).ifPresent(destination::setSym); + ofNullable(source.getStrike()).ifPresent(destination::setStrike); + ofNullable(source.getSz()).ifPresent(destination::setSz); + destination.setSmtId(source.getSmtId()); + ofNullable(source.getU()).ifPresent(destination::setU); + ofNullable(source.getUFill()).ifPresent(destination::setUFill); + ofNullable(source.getUFillTx()).ifPresent(destination::setUFillTx); + ofNullable(source.getULn()).ifPresent(destination::setULn); + ofNullable(source.getULnTx()).ifPresent(destination::setULnTx); + ofNullable(source.getULnTx()).ifPresent(destination::setULnTx); + return destination; + } + + @Override + public ProcessorContext processorContext(Placeholder placeholder) { var comment = comment(placeholder); var firstRun = (R) paragraph.getEGTextRun() .getFirst(); return new ProcessorContext(this, firstRun, comment, placeholder); } - @Override public void replace(List

toRemove, List

toAdd) { + @Override + public void remove() { + WmlUtils.remove(getP()); + } + + @Override + public void replace(List

toRemove, List

toAdd) { int index = siblings().indexOf(getP()); if (index < 0) throw new OfficeStamperException("Impossible"); @@ -98,21 +140,8 @@ private void addRun(CTRegularTextRun run, int index) { siblings().removeAll(toRemove); } - private List siblings() { - return this.parent(ContentAccessor.class, 1) - .orElseThrow(throwing("Not a standard Child with common parent")) - .getContent(); - } - - private Optional parent(Class aClass, int depth) { - return WmlUtils.getFirstParentWithClass(getP(), aClass, depth); - } - - @Override public void remove() { - ObjectDeleter.deleteParagraph(getP()); - } - - @Override public P getP() { + @Override + public P getP() { var p = WmlFactory.newParagraph(paragraph.getEGTextRun()); p.setParent(paragraph.getParent()); return p; @@ -126,7 +155,8 @@ private Optional parent(Class aClass, int depth) { * @param placeholder the expression to be replaced. * @param replacement the object to replace the expression. */ - @Override public void replace(Placeholder placeholder, Object replacement) { + @Override + public void replace(Placeholder placeholder, Object replacement) { if (!(replacement instanceof CTRegularTextRun replacementRun)) throw new AssertionError("replacement is not a CTRegularTextRun"); String text = asString(); @@ -141,72 +171,118 @@ private Optional parent(Class aClass, int depth) { boolean singleRun = affectedRuns.size() == 1; - List runs = this.paragraph.getEGTextRun(); - if (singleRun) { - PowerpointRun run = affectedRuns.getFirst(); + List textRun = this.paragraph.getEGTextRun(); + replacementRun.setRPr(affectedRuns.getFirst() + .run() + .getRPr()); + if (singleRun) singleRun(replacement, + full, + matchStartIndex, + matchEndIndex, + textRun, + affectedRuns.getFirst(), + affectedRuns.getLast()); + else multipleRuns(replacement, + affectedRuns, + matchStartIndex, + matchEndIndex, + textRun, + affectedRuns.getFirst(), + affectedRuns.getLast()); + + } - boolean expressionSpansCompleteRun = full.length() == run.run() - .getT() - .length(); - boolean expressionAtStartOfRun = matchStartIndex == run.startIndex(); - boolean expressionAtEndOfRun = matchEndIndex == run.endIndex(); - boolean expressionWithinRun = matchStartIndex > run.startIndex() && matchEndIndex < run.endIndex(); + private void singleRun( + Object replacement, + String full, + int matchStartIndex, + int matchEndIndex, + List runs, + PowerpointRun firstRun, + PowerpointRun lastRun + ) { + assert firstRun == lastRun; + boolean expressionSpansCompleteRun = full.length() == firstRun.run() + .getT() + .length(); + boolean expressionAtStartOfRun = matchStartIndex == firstRun.startIndex(); + boolean expressionAtEndOfRun = matchEndIndex == firstRun.endIndex(); + boolean expressionWithinRun = matchStartIndex > firstRun.startIndex() && matchEndIndex < firstRun.endIndex(); + + + if (expressionSpansCompleteRun) { + runs.remove(firstRun.run()); + runs.add(firstRun.indexInParent(), replacement); + recalculateRuns(); + } + else if (expressionAtStartOfRun) { + firstRun.replace(matchStartIndex, matchEndIndex, ""); + runs.add(firstRun.indexInParent(), replacement); + recalculateRuns(); + } + else if (expressionAtEndOfRun) { + firstRun.replace(matchStartIndex, matchEndIndex, ""); + runs.add(firstRun.indexInParent() + 1, replacement); + recalculateRuns(); + } + else if (expressionWithinRun) { + String runText = firstRun.run() + .getT(); + int startIndex = runText.indexOf(full); + int endIndex = startIndex + full.length(); + String substring1 = runText.substring(0, startIndex); + CTRegularTextRun run1 = create(substring1, this.paragraph); + String substring2 = runText.substring(endIndex); + CTRegularTextRun run2 = create(substring2, this.paragraph); + runs.add(firstRun.indexInParent(), run2); + runs.add(firstRun.indexInParent(), replacement); + runs.add(firstRun.indexInParent(), run1); + runs.remove(firstRun.run()); + recalculateRuns(); + } + } - replacementRun.setRPr(run.run() - .getRPr()); + private void multipleRuns( + Object replacement, + List affectedRuns, + int matchStartIndex, + int matchEndIndex, + List runs, + PowerpointRun firstRun, + PowerpointRun lastRun + ) { + // remove the expression from first and last run + firstRun.replace(matchStartIndex, matchEndIndex, ""); + lastRun.replace(matchStartIndex, matchEndIndex, ""); - if (expressionSpansCompleteRun) { + // remove all runs between first and last + for (PowerpointRun run : affectedRuns) { + if (!Objects.equals(run, firstRun) && !Objects.equals(run, lastRun)) { runs.remove(run.run()); - runs.add(run.indexInParent(), replacement); - recalculateRuns(); - } - else if (expressionAtStartOfRun) { - run.replace(matchStartIndex, matchEndIndex, ""); - runs.add(run.indexInParent(), replacement); - recalculateRuns(); - } - else if (expressionAtEndOfRun) { - run.replace(matchStartIndex, matchEndIndex, ""); - runs.add(run.indexInParent() + 1, replacement); - recalculateRuns(); - } - else if (expressionWithinRun) { - String runText = run.run() - .getT(); - int startIndex = runText.indexOf(full); - int endIndex = startIndex + full.length(); - String substring1 = runText.substring(0, startIndex); - CTRegularTextRun run1 = create(substring1, this.paragraph); - String substring2 = runText.substring(endIndex); - CTRegularTextRun run2 = create(substring2, this.paragraph); - runs.add(run.indexInParent(), run2); - runs.add(run.indexInParent(), replacement); - runs.add(run.indexInParent(), run1); - runs.remove(run.run()); - recalculateRuns(); } } - else { - PowerpointRun firstRun = affectedRuns.getFirst(); - PowerpointRun lastRun = affectedRuns.getLast(); - replacementRun.setRPr(firstRun.run() - .getRPr()); - // remove the expression from first and last run - firstRun.replace(matchStartIndex, matchEndIndex, ""); - lastRun.replace(matchStartIndex, matchEndIndex, ""); - // remove all runs between first and last - for (PowerpointRun run : affectedRuns) { - if (!Objects.equals(run, firstRun) && !Objects.equals(run, lastRun)) { - runs.remove(run.run()); - } - } + // add replacement run between first and last run + runs.add(firstRun.indexInParent() + 1, replacement); - // add replacement run between first and last run - runs.add(firstRun.indexInParent() + 1, replacement); + recalculateRuns(); + } - recalculateRuns(); - } + private static CTRegularTextRun create(String text, CTTextParagraph parentParagraph) { + CTRegularTextRun run = new CTRegularTextRun(); + run.setT(text); + applyParagraphStyle(parentParagraph, run); + return run; + } + + private static void applyParagraphStyle(CTTextParagraph p, CTRegularTextRun run) { + var properties = p.getPPr(); + if (properties == null) return; + + var textCharacterProperties = properties.getDefRPr(); + if (textCharacterProperties == null) return; + + run.setRPr(apply(textCharacterProperties)); } /** @@ -214,22 +290,21 @@ else if (expressionWithinRun) { * * @return the text of all runs. */ - @Override public String asString() { + @Override + public String asString() { return runs.stream() .map(PowerpointRun::run) .map(CTRegularTextRun::getT) .collect(joining()) + "\n"; } - @Override public void apply(Consumer

pConsumer) { + @Override + public void apply(Consumer

pConsumer) { pConsumer.accept(getP()); } - @Override public Optional parent(Class aClass) { - return parent(aClass, Integer.MAX_VALUE); - } - - @Override public Optional getComment() { + @Override + public Collection getComment() { return CommentUtil.getCommentFor(paragraph.getEGTextRun(), source.document()); } @@ -239,25 +314,15 @@ private List getAffectedRuns(int startIndex, int endIndex) { .toList(); } - private static CTRegularTextRun create( - String text, CTTextParagraph parentParagraph - ) { - CTRegularTextRun run = new CTRegularTextRun(); - run.setT(text); - applyParagraphStyle(parentParagraph, run); - return run; + @Override + public Optional parent(Class aClass) { + return parent(aClass, Integer.MAX_VALUE); } - private static void applyParagraphStyle( - CTTextParagraph p, CTRegularTextRun run - ) { - var properties = p.getPPr(); - if (properties == null) return; - - var textCharacterProperties = properties.getDefRPr(); - if (textCharacterProperties == null) return; - - run.setRPr(apply(textCharacterProperties)); + private List siblings() { + return this.parent(ContentAccessor.class, 1) + .orElseThrow(throwing("Not a standard Child with common parent")) + .getContent(); } private static CTTextCharacterProperties apply( @@ -266,38 +331,8 @@ private static CTTextCharacterProperties apply( return apply(source, new CTTextCharacterProperties()); } - private static CTTextCharacterProperties apply( - CTTextCharacterProperties source, CTTextCharacterProperties destination - ) { - if (source.getAltLang() != null) destination.setAltLang(source.getAltLang()); - if (source.getBaseline() != null) destination.setBaseline(source.getBaseline()); - if (source.getBmk() != null) destination.setBmk(source.getBmk()); - if (source.getBlipFill() != null) destination.setBlipFill(source.getBlipFill()); - if (source.getCap() != null) destination.setCap(source.getCap()); - if (source.getCs() != null) destination.setCs(source.getCs()); - if (source.getGradFill() != null) destination.setGradFill(source.getGradFill()); - if (source.getGrpFill() != null) destination.setGrpFill(source.getGrpFill()); - if (source.getHighlight() != null) destination.setHighlight(source.getHighlight()); - if (source.getHlinkClick() != null) destination.setHlinkClick(source.getHlinkClick()); - if (source.getHlinkMouseOver() != null) destination.setHlinkMouseOver(source.getHlinkMouseOver()); - if (source.getKern() != null) destination.setKern(source.getKern()); - if (source.getLang() != null) destination.setLang(source.getLang()); - if (source.getLn() != null) destination.setLn(source.getLn()); - if (source.getLatin() != null) destination.setLatin(source.getLatin()); - if (source.getNoFill() != null) destination.setNoFill(source.getNoFill()); - if (source.getPattFill() != null) destination.setPattFill(source.getPattFill()); - if (source.getSpc() != null) destination.setSpc(source.getSpc()); - if (source.getSym() != null) destination.setSym(source.getSym()); - if (source.getStrike() != null) destination.setStrike(source.getStrike()); - if (source.getSz() != null) destination.setSz(source.getSz()); - if (source.getSmtId() != 0) destination.setSmtId(source.getSmtId()); - if (source.getU() != null) destination.setU(source.getU()); - if (source.getUFill() != null) destination.setUFill(source.getUFill()); - if (source.getUFillTx() != null) destination.setUFillTx(source.getUFillTx()); - if (source.getULn() != null) destination.setULn(source.getULn()); - if (source.getULnTx() != null) destination.setULnTx(source.getULnTx()); - if (source.getULnTx() != null) destination.setULnTx(source.getULnTx()); - return destination; + private Optional parent(Class aClass, int depth) { + return WmlUtils.getFirstParentWithClass(getP(), aClass, depth); } private Comment comment(Placeholder placeholder) { @@ -309,7 +344,8 @@ private Comment comment(Placeholder placeholder) { /** * {@inheritDoc} */ - @Override public String toString() { + @Override + public String toString() { return asString(); } } diff --git a/engine/src/main/java/pro/verron/officestamper/preset/CommentProcessorFactory.java b/engine/src/main/java/pro/verron/officestamper/preset/CommentProcessorFactory.java index df8a4757..6df05601 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/CommentProcessorFactory.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/CommentProcessorFactory.java @@ -1,18 +1,6 @@ package pro.verron.officestamper.preset; -import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.springframework.lang.Nullable; -import pro.verron.officestamper.api.CommentProcessor; -import pro.verron.officestamper.api.OfficeStamper; -import pro.verron.officestamper.api.OfficeStamperConfiguration; -import pro.verron.officestamper.api.ParagraphPlaceholderReplacer; -import pro.verron.officestamper.core.DocxStamper; -import pro.verron.officestamper.preset.processors.displayif.DisplayIfProcessor; -import pro.verron.officestamper.preset.processors.repeat.RepeatProcessor; -import pro.verron.officestamper.preset.processors.repeatdocpart.RepeatDocPartProcessor; -import pro.verron.officestamper.preset.processors.repeatparagraph.ParagraphRepeatProcessor; -import pro.verron.officestamper.preset.processors.replacewith.ReplaceWithProcessor; -import pro.verron.officestamper.preset.processors.table.TableResolver; /// Factory class to create the correct comment processor for a given comment. /// @@ -20,73 +8,6 @@ /// @version ${version} /// @since 1.6.4 public class CommentProcessorFactory { - private final OfficeStamperConfiguration configuration; - - /// Creates a new CommentProcessorFactory. - /// - /// @param configuration the configuration to use for the created processors. - public CommentProcessorFactory(OfficeStamperConfiguration configuration) { - this.configuration = configuration; - } - - /// Creates new repeatParagraph [CommentProcessor] with default configuration. - /// - /// @param pr a [ParagraphPlaceholderReplacer] object - /// - /// @return a [CommentProcessor] object - public CommentProcessor repeatParagraph(ParagraphPlaceholderReplacer pr) { - return ParagraphRepeatProcessor.newInstance(pr); - } - - /// Creates new repeatDocPart [CommentProcessor] with default configuration. - /// - /// @param pr a [ParagraphPlaceholderReplacer] object - /// - /// @return a [CommentProcessor] object - public CommentProcessor repeatDocPart(ParagraphPlaceholderReplacer pr) { - return RepeatDocPartProcessor.newInstance(pr, getStamper()); - } - - private OfficeStamper getStamper() { - return (template, context, output) -> new DocxStamper(configuration).stamp(template, context, output); - } - - /// Creates new repeating [CommentProcessor] with default configuration. - /// - /// @param pr a [ParagraphPlaceholderReplacer] object - /// - /// @return a [CommentProcessor] object - public CommentProcessor repeat(ParagraphPlaceholderReplacer pr) { - return RepeatProcessor.newInstance(pr); - } - - /// Creates new tableResolver [CommentProcessor] with default configuration. - /// - /// @param pr a [ParagraphPlaceholderReplacer] object - /// - /// @return a [CommentProcessor] object - public CommentProcessor tableResolver(ParagraphPlaceholderReplacer pr) { - return TableResolver.newInstance(pr); - } - - /// Creates new displayIf [CommentProcessor] with default configuration. - /// - /// @param pr a [ParagraphPlaceholderReplacer] object - /// - /// @return a [CommentProcessor] object - public CommentProcessor displayIf(ParagraphPlaceholderReplacer pr) { - return DisplayIfProcessor.newInstance(pr); - } - - /// Creates new replaceWith [CommentProcessor] with default configuration. - /// - /// @param pr a [ParagraphPlaceholderReplacer] object - /// - /// @return a [CommentProcessor] object - public CommentProcessor replaceWith(ParagraphPlaceholderReplacer pr) { - return ReplaceWithProcessor.newInstance(pr); - } - /// Used to resolve a table in the template document. /// Take the table passed-in to fill an existing Tbl object in the document. /// @@ -169,6 +90,8 @@ public interface IRepeatProcessor { /// @since 1.0.0 public interface IDisplayIfProcessor { + void displayParagraphIfAbsent(@Nullable Object condition); + /// @param condition if true, keep the paragraph surrounding the comment, else remove. void displayParagraphIf(@Nullable Boolean condition); @@ -181,22 +104,30 @@ public interface IDisplayIfProcessor { /// @param condition if non-null, keep the table row surrounding the comment, else remove. void displayTableRowIfPresent(@Nullable Object condition); + void displayTableRowIfAbsent(@Nullable Object condition); + /// @param condition if true, keep the table surrounding the comment, else remove. void displayTableIf(@Nullable Boolean condition); /// @param condition if non-null, keep the table surrounding the comment, else remove. void displayTableIfPresent(@Nullable Object condition); + void displayTableIfAbsent(@Nullable Object condition); + /// @param condition if true, keep the selected words surrounding the comment, else remove. void displayWordsIf(@Nullable Boolean condition); /// @param condition if non-null, keep the selected words surrounding the comment, else remove. void displayWordsIfPresent(@Nullable Object condition); + void displayWordsIfAbsent(@Nullable Object condition); + /// @param condition if true, keep the selected elements surrounding the comment, else remove. void displayDocPartIf(@Nullable Boolean condition); /// @param condition if non-null, keep the selected elements surrounding the comment, else remove. void displayDocPartIfPresent(@Nullable Object condition); + + void displayDocPartIfAbsent(@Nullable Object condition); } } diff --git a/engine/src/main/java/pro/verron/officestamper/preset/OfficeStamperConfigurations.java b/engine/src/main/java/pro/verron/officestamper/preset/OfficeStamperConfigurations.java index a8e1ac89..e3a3785f 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/OfficeStamperConfigurations.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/OfficeStamperConfigurations.java @@ -2,9 +2,18 @@ import pro.verron.officestamper.api.OfficeStamperConfiguration; import pro.verron.officestamper.api.OfficeStamperException; +import pro.verron.officestamper.core.DocxStamper; import pro.verron.officestamper.core.DocxStamperConfiguration; +import pro.verron.officestamper.preset.CommentProcessorFactory.*; +import pro.verron.officestamper.preset.processors.displayif.DisplayIfProcessor; +import pro.verron.officestamper.preset.processors.repeat.RepeatProcessor; +import pro.verron.officestamper.preset.processors.repeatdocpart.RepeatDocPartProcessor; +import pro.verron.officestamper.preset.processors.repeatparagraph.ParagraphRepeatProcessor; +import pro.verron.officestamper.preset.processors.replacewith.ReplaceWithProcessor; +import pro.verron.officestamper.preset.processors.table.TableResolver; import java.time.temporal.TemporalAccessor; +import java.util.List; import static java.time.format.DateTimeFormatter.*; import static java.time.format.FormatStyle.valueOf; @@ -32,7 +41,10 @@ private OfficeStamperConfigurations() { public static OfficeStamperConfiguration standardWithPreprocessing() { var configuration = standard(); configuration.addPreprocessor(Preprocessors.removeLanguageProof()); + configuration.addPreprocessor(Preprocessors.removeLanguageInfo()); configuration.addPreprocessor(Preprocessors.mergeSimilarRuns()); + configuration.addPostprocessor(Postprocessors.removeOrphanedFootnotes()); + configuration.addPostprocessor(Postprocessors.removeOrphanedEndnotes()); return configuration; } @@ -43,7 +55,27 @@ public static OfficeStamperConfiguration standardWithPreprocessing() { */ public static OfficeStamperConfiguration standard() { var configuration = new DocxStamperConfiguration(); + + configuration.addCommentProcessor(IRepeatProcessor.class, RepeatProcessor::newInstance); + configuration.addCommentProcessor(IParagraphRepeatProcessor.class, ParagraphRepeatProcessor::newInstance); + configuration.addCommentProcessor(IRepeatDocPartProcessor.class, + pr -> RepeatDocPartProcessor.newInstance(pr, + (template, context, output) -> new DocxStamper(configuration) + .stamp(template, context, output))); + configuration.addCommentProcessor(ITableResolver.class, TableResolver::newInstance); + configuration.addCommentProcessor(IDisplayIfProcessor.class, DisplayIfProcessor::newInstance); + configuration.addCommentProcessor(IReplaceWithProcessor.class, ReplaceWithProcessor::newInstance); + + configuration.setResolvers(List.of(Resolvers.image(), + Resolvers.legacyDate(), + Resolvers.isoDate(), + Resolvers.isoTime(), + Resolvers.isoDateTime(), + Resolvers.nullToEmpty(), + Resolvers.fallback())); + configuration.addPreprocessor(Preprocessors.removeMalformedComments()); + configuration.addCustomFunction("ftime", TemporalAccessor.class) .withImplementation(ISO_TIME::format); configuration.addCustomFunction("fdate", TemporalAccessor.class) @@ -86,8 +118,8 @@ public static OfficeStamperConfiguration standard() { configuration.addCustomFunction("fpattern", TemporalAccessor.class, String.class) .withImplementation((date, pattern) -> ofPattern(pattern).format(date)); configuration.addCustomFunction("fpattern", TemporalAccessor.class, String.class, String.class) - .withImplementation((date, pattern, locale) -> ofPattern(pattern, forLanguageTag(locale)).format( - date)); + .withImplementation((date, pattern, locale) -> ofPattern(pattern, forLanguageTag(locale)) + .format(date)); return configuration; } diff --git a/engine/src/main/java/pro/verron/officestamper/preset/Postprocessors.java b/engine/src/main/java/pro/verron/officestamper/preset/Postprocessors.java new file mode 100644 index 00000000..0dc52c14 --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/Postprocessors.java @@ -0,0 +1,20 @@ +package pro.verron.officestamper.preset; + +import pro.verron.officestamper.api.OfficeStamperException; +import pro.verron.officestamper.api.PostProcessor; +import pro.verron.officestamper.preset.postprocessors.cleanendnotes.RemoveOrphanedEndnotesProcessor; +import pro.verron.officestamper.preset.postprocessors.cleanfootnotes.RemoveOrphanedFootnotesProcessor; + +public class Postprocessors { + private Postprocessors() { + throw new OfficeStamperException("This is a utility class and cannot be instantiated"); + } + + public static PostProcessor removeOrphanedFootnotes() { + return new RemoveOrphanedFootnotesProcessor(); + } + + public static PostProcessor removeOrphanedEndnotes() { + return new RemoveOrphanedEndnotesProcessor(); + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/Preprocessors.java b/engine/src/main/java/pro/verron/officestamper/preset/Preprocessors.java index 32d2b986..280c1f87 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/Preprocessors.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/Preprocessors.java @@ -5,6 +5,7 @@ import pro.verron.officestamper.api.PreProcessor; import pro.verron.officestamper.preset.preprocessors.malformedcomments.RemoveMalformedComments; import pro.verron.officestamper.preset.preprocessors.prooferror.RemoveProofErrors; +import pro.verron.officestamper.preset.preprocessors.rmlang.RemoveLang; import pro.verron.officestamper.preset.preprocessors.similarrun.MergeSameStyleRuns; /** @@ -36,6 +37,10 @@ public static PreProcessor removeLanguageProof() { return new RemoveProofErrors(); } + public static PreProcessor removeLanguageInfo() { + return new RemoveLang(); + } + public static PreProcessor removeMalformedComments() { return new RemoveMalformedComments(); } diff --git a/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/NoteRefsVisitor.java b/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/NoteRefsVisitor.java new file mode 100644 index 00000000..1e1d615e --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/NoteRefsVisitor.java @@ -0,0 +1,22 @@ +package pro.verron.officestamper.preset.postprocessors; + +import org.docx4j.utils.TraversalUtilVisitor; +import org.docx4j.wml.CTFtnEdnRef; + +import java.math.BigInteger; +import java.util.SortedSet; +import java.util.TreeSet; + +public class NoteRefsVisitor + extends TraversalUtilVisitor { + private final SortedSet ids = new TreeSet<>(); + + @Override + public void apply(CTFtnEdnRef element) { + ids.add(element.getId()); + } + + public SortedSet referencedNoteIds() { + return ids; + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/cleanendnotes/RemoveOrphanedEndnotesProcessor.java b/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/cleanendnotes/RemoveOrphanedEndnotesProcessor.java new file mode 100644 index 00000000..b21a5f6c --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/cleanendnotes/RemoveOrphanedEndnotesProcessor.java @@ -0,0 +1,44 @@ +package pro.verron.officestamper.preset.postprocessors.cleanendnotes; + +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.EndnotesPart; +import org.docx4j.wml.CTEndnotes; +import org.docx4j.wml.CTFtnEdn; +import pro.verron.officestamper.api.PostProcessor; +import pro.verron.officestamper.preset.postprocessors.NoteRefsVisitor; +import pro.verron.officestamper.utils.WmlUtils; + +import java.util.Collection; +import java.util.Optional; + +import static org.docx4j.wml.STFtnEdn.NORMAL; +import static pro.verron.officestamper.api.OfficeStamperException.throwing; +import static pro.verron.officestamper.core.DocumentUtil.visitDocument; + +public class RemoveOrphanedEndnotesProcessor + implements PostProcessor { + @Override + public void process(WordprocessingMLPackage document) { + var visitor = new NoteRefsVisitor(); + visitDocument(document, visitor); + var referencedNoteIds = visitor.referencedNoteIds(); + var mainDocumentPart = document.getMainDocumentPart(); + + var ednPart = mainDocumentPart.getEndNotesPart(); + Optional.ofNullable(ednPart) + .stream() + .map(throwing(EndnotesPart::getContents)) + .map(CTEndnotes::getEndnote) + .flatMap(Collection::stream) + .filter(RemoveOrphanedEndnotesProcessor::normalNotes) + .filter(note -> !referencedNoteIds.contains(note.getId())) + .toList() + .forEach(WmlUtils::remove); + } + + private static boolean normalNotes(CTFtnEdn note) { + return Optional.ofNullable(note.getType()) + .orElse(NORMAL) + .equals(NORMAL); + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/cleanfootnotes/RemoveOrphanedFootnotesProcessor.java b/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/cleanfootnotes/RemoveOrphanedFootnotesProcessor.java new file mode 100644 index 00000000..673d0ede --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/postprocessors/cleanfootnotes/RemoveOrphanedFootnotesProcessor.java @@ -0,0 +1,44 @@ +package pro.verron.officestamper.preset.postprocessors.cleanfootnotes; + +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.FootnotesPart; +import org.docx4j.wml.CTFootnotes; +import org.docx4j.wml.CTFtnEdn; +import pro.verron.officestamper.api.PostProcessor; +import pro.verron.officestamper.preset.postprocessors.NoteRefsVisitor; +import pro.verron.officestamper.utils.WmlUtils; + +import java.util.Collection; +import java.util.Optional; + +import static org.docx4j.wml.STFtnEdn.NORMAL; +import static pro.verron.officestamper.api.OfficeStamperException.throwing; +import static pro.verron.officestamper.core.DocumentUtil.visitDocument; + +public class RemoveOrphanedFootnotesProcessor + implements PostProcessor { + @Override + public void process(WordprocessingMLPackage document) { + var visitor = new NoteRefsVisitor(); + visitDocument(document, visitor); + var referencedNoteIds = visitor.referencedNoteIds(); + var mainDocumentPart = document.getMainDocumentPart(); + + var ftnPart = mainDocumentPart.getFootnotesPart(); + Optional.ofNullable(ftnPart) + .stream() + .map(throwing(FootnotesPart::getContents)) + .map(CTFootnotes::getFootnote) + .flatMap(Collection::stream) + .filter(RemoveOrphanedFootnotesProcessor::normalNotes) + .filter(note -> !referencedNoteIds.contains(note.getId())) + .toList() + .forEach(WmlUtils::remove); + } + + private static boolean normalNotes(CTFtnEdn note) { + return Optional.ofNullable(note.getType()) + .orElse(NORMAL) + .equals(NORMAL); + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/prooferror/RemoveProofErrors.java b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/prooferror/RemoveProofErrors.java index fad620ce..e9fe1a95 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/prooferror/RemoveProofErrors.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/prooferror/RemoveProofErrors.java @@ -1,11 +1,12 @@ package pro.verron.officestamper.preset.preprocessors.prooferror; -import org.docx4j.TraversalUtil; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.ContentAccessor; import org.docx4j.wml.ProofErr; import pro.verron.officestamper.api.PreProcessor; +import static pro.verron.officestamper.core.DocumentUtil.visitDocument; + public class RemoveProofErrors implements PreProcessor { @@ -14,9 +15,8 @@ public class RemoveProofErrors */ @Override public void process(WordprocessingMLPackage document) { - var mainDocumentPart = document.getMainDocumentPart(); var visitor = new ProofErrVisitor(); - TraversalUtil.visit(mainDocumentPart, visitor); + visitDocument(document, visitor); for (ProofErr proofErr : visitor.getProofErrs()) { var proofErrParent = proofErr.getParent(); if (proofErrParent instanceof ContentAccessor parent) { diff --git a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/PprLangVisitor.java b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/PprLangVisitor.java new file mode 100644 index 00000000..59d8f3d4 --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/PprLangVisitor.java @@ -0,0 +1,27 @@ +package pro.verron.officestamper.preset.preprocessors.rmlang; + +import org.docx4j.utils.TraversalUtilVisitor; +import org.docx4j.wml.P; +import org.docx4j.wml.ParaRPr; + +import java.util.ArrayList; +import java.util.List; + +public class PprLangVisitor + extends TraversalUtilVisitor

{ + private final List rPrs = new ArrayList<>(); + + @Override + public void apply(P element, Object parent1, List siblings) { + if (element.getPPr() != null && element.getPPr() + .getRPr() != null && element.getPPr() + .getRPr() + .getLang() != null) + rPrs.add(element.getPPr() + .getRPr()); + } + + public List getrPrs() { + return rPrs; + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/RemoveLang.java b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/RemoveLang.java new file mode 100644 index 00000000..84b0c929 --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/RemoveLang.java @@ -0,0 +1,26 @@ +package pro.verron.officestamper.preset.preprocessors.rmlang; + +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.wml.ParaRPr; +import org.docx4j.wml.RPr; +import pro.verron.officestamper.api.PreProcessor; + +import static pro.verron.officestamper.core.DocumentUtil.visitDocument; + +public class RemoveLang + implements PreProcessor { + + @Override + public void process(WordprocessingMLPackage document) { + var visitor = new RprLangVisitor(); + visitDocument(document, visitor); + for (RPr rPr : visitor.getrPrs()) { + rPr.setLang(null); + } + var visitor2 = new PprLangVisitor(); + visitDocument(document, visitor2); + for (ParaRPr rPr : visitor2.getrPrs()) { + rPr.setLang(null); + } + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/RprLangVisitor.java b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/RprLangVisitor.java new file mode 100644 index 00000000..71dd0105 --- /dev/null +++ b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/rmlang/RprLangVisitor.java @@ -0,0 +1,24 @@ +package pro.verron.officestamper.preset.preprocessors.rmlang; + +import org.docx4j.utils.TraversalUtilVisitor; +import org.docx4j.wml.R; +import org.docx4j.wml.RPr; + +import java.util.ArrayList; +import java.util.List; + +public class RprLangVisitor + extends TraversalUtilVisitor { + private final List rPrs = new ArrayList<>(); + + @Override + public void apply(R element, Object parent1, List siblings) { + if (element.getRPr() != null && element.getRPr() + .getLang() != null) + rPrs.add(element.getRPr()); + } + + public List getrPrs() { + return rPrs; + } +} diff --git a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/similarrun/MergeSameStyleRuns.java b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/similarrun/MergeSameStyleRuns.java index 389be204..58212699 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/similarrun/MergeSameStyleRuns.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/preprocessors/similarrun/MergeSameStyleRuns.java @@ -1,10 +1,10 @@ package pro.verron.officestamper.preset.preprocessors.similarrun; -import org.docx4j.TraversalUtil; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.wml.ContentAccessor; import org.docx4j.wml.R; import pro.verron.officestamper.api.PreProcessor; +import pro.verron.officestamper.core.DocumentUtil; import java.util.LinkedHashSet; import java.util.List; @@ -17,9 +17,8 @@ public class MergeSameStyleRuns */ @Override public void process(WordprocessingMLPackage document) { - var mainDocumentPart = document.getMainDocumentPart(); var visitor = new SimilarRunVisitor(); - TraversalUtil.visit(mainDocumentPart, visitor); + DocumentUtil.visitDocument(document, visitor); for (List similarStyleRuns : visitor.getSimilarStyleRuns()) { R firstRun = similarStyleRuns.getFirst(); var runContent = firstRun.getContent(); diff --git a/engine/src/main/java/pro/verron/officestamper/preset/processors/displayif/DisplayIfProcessor.java b/engine/src/main/java/pro/verron/officestamper/preset/processors/displayif/DisplayIfProcessor.java index e3180ebc..1f085756 100644 --- a/engine/src/main/java/pro/verron/officestamper/preset/processors/displayif/DisplayIfProcessor.java +++ b/engine/src/main/java/pro/verron/officestamper/preset/processors/displayif/DisplayIfProcessor.java @@ -1,12 +1,13 @@ package pro.verron.officestamper.preset.processors.displayif; +import org.docx4j.wml.ContentAccessor; import org.docx4j.wml.Tbl; import org.docx4j.wml.Tr; +import org.jvnet.jaxb2_commons.ppp.Child; import org.springframework.lang.Nullable; import pro.verron.officestamper.api.*; -import pro.verron.officestamper.core.ObjectDeleter; -import pro.verron.officestamper.core.PlaceholderReplacer; import pro.verron.officestamper.preset.CommentProcessorFactory; +import pro.verron.officestamper.utils.WmlUtils; import java.util.ArrayList; import java.util.List; @@ -24,8 +25,7 @@ public class DisplayIfProcessor implements CommentProcessorFactory.IDisplayIfProcessor { private List paragraphsToBeRemoved = new ArrayList<>(); - private List tablesToBeRemoved = new ArrayList<>(); - private List tableRowsToBeRemoved = new ArrayList<>(); + private List elementsToBeRemoved = new ArrayList<>(); private DisplayIfProcessor(ParagraphPlaceholderReplacer placeholderReplacer) { super(placeholderReplacer); @@ -33,83 +33,112 @@ private DisplayIfProcessor(ParagraphPlaceholderReplacer placeholderReplacer) { /// Creates a new DisplayIfProcessor instance. /// - /// @param pr the [PlaceholderReplacer] used for replacing expressions. + /// @param pr the [ParagraphPlaceholderReplacer] used for replacing expressions. /// /// @return a new DisplayIfProcessor instance. public static CommentProcessor newInstance(ParagraphPlaceholderReplacer pr) { return new DisplayIfProcessor(pr); } - @Override public void commitChanges(DocxPart source) { - removeParagraphs(); - removeTables(); - removeTableRows(); - } - - private void removeParagraphs() { + @Override + public void commitChanges(DocxPart source) { paragraphsToBeRemoved.forEach(Paragraph::remove); + elementsToBeRemoved.forEach(WmlUtils::remove); } - private void removeTables() { - tablesToBeRemoved.forEach(ObjectDeleter::deleteTable); - } - private void removeTableRows() { - tableRowsToBeRemoved.forEach(ObjectDeleter::deleteTableRow); + @Override + public void reset() { + paragraphsToBeRemoved = new ArrayList<>(); + elementsToBeRemoved = new ArrayList<>(); } - @Override public void reset() { - paragraphsToBeRemoved = new ArrayList<>(); - tablesToBeRemoved = new ArrayList<>(); - tableRowsToBeRemoved = new ArrayList<>(); + @Override + public void displayParagraphIfAbsent(@Nullable Object condition) { + displayParagraphIf(condition == null); } - @Override public void displayParagraphIf(@Nullable Boolean condition) { + @Override + public void displayParagraphIf(@Nullable Boolean condition) { if (Boolean.TRUE.equals(condition)) return; paragraphsToBeRemoved.add(this.getParagraph()); } - @Override public void displayParagraphIfPresent(@Nullable Object condition) { + @Override + public void displayParagraphIfPresent(@Nullable Object condition) { displayParagraphIf(condition != null); } - @Override public void displayTableRowIf(@Nullable Boolean condition) { + + @Override + public void displayTableRowIf(@Nullable Boolean condition) { if (Boolean.TRUE.equals(condition)) return; var tr = this.getParagraph() .parent(Tr.class) .orElseThrow(throwing("Paragraph is not within a row!")); - tableRowsToBeRemoved.add(tr); + elementsToBeRemoved.add(tr); } - @Override public void displayTableRowIfPresent(@Nullable Object condition) { + @Override + public void displayTableRowIfPresent(@Nullable Object condition) { displayTableRowIf(condition != null); } - @Override public void displayTableIf(Boolean condition) { + @Override + public void displayTableRowIfAbsent(@Nullable Object condition) { + displayTableRowIf(condition == null); + } + + @Override + public void displayTableIf(Boolean condition) { if (Boolean.TRUE.equals(condition)) return; var tbl = this.getParagraph() .parent(Tbl.class) .orElseThrow(throwing("Paragraph is not within a table!")); - tablesToBeRemoved.add(tbl); + elementsToBeRemoved.add(tbl); } - @Override public void displayTableIfPresent(@Nullable Object condition) { + @Override + public void displayTableIfPresent(@Nullable Object condition) { displayTableIf(condition != null); } - @Override public void displayWordsIf(@Nullable Boolean condition) { + @Override + public void displayTableIfAbsent(@Nullable Object condition) { + displayTableIf(condition == null); + } + + @Override + public void displayWordsIf(@Nullable Boolean condition) { if (Boolean.TRUE.equals(condition)) return; var commentWrapper = getCurrentCommentWrapper(); - commentWrapper.getParent() - .getContent() - .removeAll(commentWrapper.getElements()); + var start = commentWrapper.getCommentRangeStart(); + var end = commentWrapper.getCommentRangeEnd(); + var parent = (ContentAccessor) start.getParent(); + var startIndex = parent.getContent() + .indexOf(start); + var iterator = parent.getContent() + .listIterator(startIndex); + while (iterator.hasNext()) { + var it = iterator.next(); + elementsToBeRemoved.add((Child) it); + if (it.equals(end)) + break; + } + } + + @Override + public void displayWordsIfPresent(@Nullable Object condition) { + displayWordsIf(condition != null); } - @Override public void displayWordsIfPresent(@Nullable Object condition) { - displayWordsIf(condition != null); + @Override + public void displayWordsIfAbsent(@Nullable Object condition) { + displayWordsIf(condition == null); } - @Override public void displayDocPartIf(@Nullable Boolean condition) { + @Override + public void displayDocPartIf(@Nullable Boolean condition) { if (Boolean.TRUE.equals(condition)) return; var commentWrapper = getCurrentCommentWrapper(); commentWrapper.getParent() @@ -117,7 +146,13 @@ private void removeTableRows() { .removeAll(commentWrapper.getElements()); } - @Override public void displayDocPartIfPresent(@Nullable Object condition) { + @Override + public void displayDocPartIfPresent(@Nullable Object condition) { displayDocPartIf(condition != null); } + + @Override + public void displayDocPartIfAbsent(@Nullable Object condition) { + displayDocPartIf(condition == null); + } } diff --git a/engine/src/main/java/pro/verron/officestamper/utils/WmlFactory.java b/engine/src/main/java/pro/verron/officestamper/utils/WmlFactory.java index f2cbd842..2b975cb3 100644 --- a/engine/src/main/java/pro/verron/officestamper/utils/WmlFactory.java +++ b/engine/src/main/java/pro/verron/officestamper/utils/WmlFactory.java @@ -323,13 +323,10 @@ public static WordprocessingMLPackage newWord() { try { var aPackage = WordprocessingMLPackage.createPackage(); var mainDocumentPart = aPackage.getMainDocumentPart(); - var cp = newCommentsPart(); cp.init(); cp.setJaxbElement(newComments()); - var cpPackage = cp.getPackage(); mainDocumentPart.addTargetPart(cp); - aPackage.setSourcePartStore(cpPackage.getSourcePartStore()); return aPackage; } catch (InvalidFormatException e) { throw new OfficeStamperException(e); 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 824ab9cf..39657e9c 100644 --- a/engine/src/main/java/pro/verron/officestamper/utils/WmlUtils.java +++ b/engine/src/main/java/pro/verron/officestamper/utils/WmlUtils.java @@ -1,5 +1,6 @@ package pro.verron.officestamper.utils; +import jakarta.xml.bind.JAXBElement; import org.docx4j.TraversalUtil; import org.docx4j.finders.CommentFinder; import org.docx4j.openpackaging.exceptions.Docx4JException; @@ -7,12 +8,14 @@ import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.PartName; import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart; -import org.docx4j.wml.Comments; +import org.docx4j.wml.*; import org.jvnet.jaxb2_commons.ppp.Child; import pro.verron.officestamper.api.OfficeStamperException; +import pro.verron.officestamper.core.TableCellUtil; import java.math.BigInteger; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; @@ -78,4 +81,45 @@ private static Predicate idEqual(BigInteger id) { return commentId.equals(id); }; } + + public static void remove(Child child) { + switch (child.getParent()) { + case ContentAccessor parent -> remove(parent, child); + case CTFootnotes parent -> remove(parent, child); + case CTEndnotes parent -> remove(parent, child); + default -> throw new OfficeStamperException("Unexpected value: " + child.getParent()); + } + if (child.getParent() instanceof Tc cell && TableCellUtil.hasNoParagraphOrTable(cell)) { + TableCellUtil.addEmptyParagraph(cell); + } + } + + @SuppressWarnings("SuspiciousMethodCalls") + private static void remove(CTFootnotes parent, Child child) { + parent.getFootnote() + .remove(child); + } + + @SuppressWarnings("SuspiciousMethodCalls") + private static void remove(CTEndnotes parent, Child child) { + parent.getEndnote() + .remove(child); + } + + private static void remove(ContentAccessor parent, Child child) { + var siblings = parent.getContent(); + var iterator = siblings.listIterator(); + while (iterator.hasNext()) { + if (equals(iterator.next(), child)) { + iterator.remove(); + break; + } + } + } + + private static boolean equals(Object o1, Object o2) { + if (o1 instanceof JAXBElement e1) o1 = e1.getValue(); + if (o2 instanceof JAXBElement e2) o2 = e2.getValue(); + return Objects.equals(o1, o2); + } } diff --git a/engine/src/test/java/pro/verron/officestamper/test/ConditionalDisplayTest.java b/engine/src/test/java/pro/verron/officestamper/test/ConditionalDisplayTest.java new file mode 100644 index 00000000..79bea338 --- /dev/null +++ b/engine/src/test/java/pro/verron/officestamper/test/ConditionalDisplayTest.java @@ -0,0 +1,818 @@ +package pro.verron.officestamper.test; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static pro.verron.officestamper.preset.OfficeStamperConfigurations.standard; +import static pro.verron.officestamper.preset.OfficeStamperConfigurations.standardWithPreprocessing; +import static pro.verron.officestamper.test.ContextFactory.mapContextFactory; +import static pro.verron.officestamper.test.ContextFactory.objectContextFactory; +import static pro.verron.officestamper.test.TestUtils.getResource; + +class ConditionalDisplayTest { + + public static Stream factories() { + return Stream.of(objectContextFactory(), mapContextFactory()); + } + + @DisplayName("Display Bart elements") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfBart(ContextFactory factory) { + var context = factory.name("Bart"); + var template = getResource(Path.of("ConditionalDisplayTest.docx")); + var expected = """ + == Conditional Display + + === Paragraphs + + This paragraph 1 stays untouched. + This paragraph 2 stays if “name” is “Bart”. + This paragraph 4 stays if “name” is “Bart”. + This paragraph 6 stays if “name” is not null. + This paragraph 7 stays if “name” is not null. + ==== Paragraphs in table + + |=== + |Works in tables + + | + |This paragraph 1.2 stays if “name” is “Bart”. + + |This paragraph 2.1 stays if “name” is “Bart”. + |This paragraph 2.2 stays if “name” is not null. + + + |=== + ==== Paragraphs in nested table + + |=== + |Works in nested tables + + ||=== + |Really + + |This paragraph stays if “name” is “Bart”. + + + |=== + + + |=== + + [page-break] + <<< + + === Table Rows + + ==== Rows in table + + |=== + |Works in tables + + |This row 1 is: + |Untouched. + + |This row 2 stays: + |if “name” is “Bart”. + + |This row 4 stays: + |if “name” is “Bart”. + + |This row 6 stays: + |if “name” is not null. + + |This row 7 stays: + |if “name” is not null. + + + |=== + ==== Rows in nested table + + |=== + |Works in nested tables + + ||=== + |Really' + + |This row stays if “name” is “Bart”. + + + |=== + + + |=== + + [page-break] + <<< + + === Tables + + ==== Mono-cell fully commented. + + |=== + |A mono-cell table. + + + |=== + ==== Mono-cell partially commented. + + |=== + |Another mono-cell table. + + + |=== + ==== Multi-cell fully commented. + + |=== + |Cell 1.1 + |Cell 1.2 + + |Cell 2.1 + |Cell 2.2 + + + |=== + ==== Multi-cell partially commented. + + |=== + |Cell 1.1 + |Cell 1.2 + + |Cell 2.1 + |Cell 2.2 + + + |=== + ==== If present Case. + + |=== + |Cell 1.1 + |Cell 1.2 + + |Cell 2.1 + |Cell 2.2 + + + |=== + ==== If absent Case. + + ==== Works in nested tables + + |=== + |Cell 1.1 + + ||=== + |Cell 2.1, Sub cell 1.1 + + |Cell 2.1, Sub cell 2.1 + + + |=== + + + |=== + + [page-break] + <<< + + === Words + + These words should appear conditionally: Bart . + These words should appear conditionally: Bart Simpson . + + [page-break] + <<< + + === Doc Parts + + These 1❬sts❘{vertAlign=superscript}❭ multiple paragraph block stays untouched. + To show how comments spanning multiple paragraphs works. + These 2❬nd❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is “Bart”. + To show how comments spanning multiple paragraphs works. + These 4❬th❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is “Bart”. + To show how comments spanning multiple paragraphs works. + These 6❬th❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is not “null”. + To show how comments spanning multiple paragraphs works. + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display footnotes elements") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfFootnotes(ContextFactory factory) { + var context = factory.name("Bart"); + var template = getResource(Path.of("footnotes.docx")); + var expected = """ + = Springfield Chronicles: The Simpsons Edition + + == Introduction + + [Quote] "Springfield, USA is a town like no other, brought to life through the antics of the Simpson family. Here, in the heart of Springfield, every day is an adventure." + == Homer Simpson's Favorite Pastimes + + == Marge Simpson: The Heart of the Family + + Marge Simpson, with her iconic blue hair, is the moral center of the family. She manages the household with the chaos around her, Marge always finds a way to keep the family together. + |=== + |Character + |Role + |Fun Fact + + |Marge Simpson + |Matriarch + |Her hair once hid an entire toolbox❬[6]❘{rStyle=Appelnotedebasdep}❭. + + |Bart Simpson + |Eldest Child + |Bart's famous catchphrase is "Eat my shorts!"❬[7]❘{rStyle=Appelnotedebasdep}❭. + + |Lisa Simpson + |Middle Child + |Lisa is a talented saxophonist❬[8]❘{rStyle=Appelnotedebasdep}❭. + + |Maggie Simpson + |Youngest Child + |Maggie is known for her pacifier and silent wisdom❬[9]❘{rStyle=Appelnotedebasdep}❭. + + + |=== + == Conclusion + + [Quote] "From the simplicity of everyday life to the extraordinary events in Springfield, The Simpsons continue to entertain audiences with their unique charm and wit." + [footnotes] + --- + [6] Marge's hairdo was designed to hide various items, a nod to cartoon logic. + + [7] Bart's rebellious attitude is encapsulated in this catchphrase. + + [8] Lisa's musical talent often shines through her saxophone solos. + + [9] Despite her silence, Maggie has saved her family on multiple occasions. + + --- + """; + + var config = standardWithPreprocessing(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display endnotes elements") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfEndnotes(ContextFactory factory) { + var context = factory.name("Bart"); + var template = getResource(Path.of("endnotes.docx")); + var expected = """ + = Springfield Chronicles: The Simpsons Edition + + == Introduction + + [Quote] "Springfield, USA is a town like no other, brought to life through the antics of the Simpson family. Here, in the heart of Springfield, every day is an adventure." + == Homer Simpson's Favorite Pastimes + + == Marge Simpson: The Heart of the Family + + Marge Simpson, with her iconic blue hair, is the moral center of the family. She manages the household with the chaos around her, Marge always finds a way to keep the family together. + |=== + |Character + |Role + |Fun Fact + + |Marge Simpson + |Matriarch + |Her hair once hid an entire toolbox❬[6]❘{rStyle=Appeldenotedefin}❭. + + |Bart Simpson + |Eldest Child + |Bart's famous catchphrase is "Eat my shorts!"❬[7]❘{rStyle=Appeldenotedefin}❭. + + |Lisa Simpson + |Middle Child + |Lisa is a talented saxophonist❬[8]❘{rStyle=Appeldenotedefin}❭. + + |Maggie Simpson + |Youngest Child + |Maggie is known for her pacifier and silent wisdom❬[9]❘{rStyle=Appeldenotedefin}❭. + + + |=== + == Conclusion + + [Quote] "From the simplicity of everyday life to the extraordinary events in Springfield, The Simpsons continue to entertain audiences with their unique charm and wit." + [endnotes] + --- + [6] Marge's hairdo was designed to hide various items, a nod to cartoon logic. + + [7] Bart's rebellious attitude is encapsulated in this catchphrase. + + [8] Lisa's musical talent often shines through her saxophone solos. + + [9] Despite her silence, Maggie has saved her family on multiple occasions. + + --- + """; + + var config = standardWithPreprocessing(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display Homer elements") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfHomer(ContextFactory factory) { + var context = factory.name("Homer"); + var template = getResource(Path.of("ConditionalDisplayTest.docx")); + var expected = """ + == Conditional Display + + === Paragraphs + + This paragraph 1 stays untouched. + This paragraph 3 stays if “name” is not “Bart”. + This paragraph 5 stays if “name” is not “Bart”. + This paragraph 6 stays if “name” is not null. + This paragraph 7 stays if “name” is not null. + ==== Paragraphs in table + + |=== + |Works in tables + + | + | + + | + |This paragraph 2.2 stays if “name” is not null. + + + |=== + ==== Paragraphs in nested table + + |=== + |Works in nested tables + + ||=== + |Really + + | + + + |=== + + + |=== + + [page-break] + <<< + + === Table Rows + + ==== Rows in table + + |=== + |Works in tables + + |This row 1 is: + |Untouched. + + |This row 3 stays: + |if “name” is not “Bart”. + + |This row 5 stays: + |if “name” is not “Bart”. + + |This row 6 stays: + |if “name” is not null. + + |This row 7 stays: + |if “name” is not null. + + + |=== + ==== Rows in nested table + + |=== + |Works in nested tables + + ||=== + |Really' + + + |=== + + + |=== + + [page-break] + <<< + + === Tables + + ==== Mono-cell fully commented. + + ==== Mono-cell partially commented. + + ==== Multi-cell fully commented. + + ==== Multi-cell partially commented. + + ==== If present Case. + + |=== + |Cell 1.1 + |Cell 1.2 + + |Cell 2.1 + |Cell 2.2 + + + |=== + ==== If absent Case. + + ==== Works in nested tables + + |=== + |Cell 1.1 + + | + + + |=== + + [page-break] + <<< + + === Words + + These words should appear conditionally: Homer . + These words should appear conditionally: Homer Simpson . + + [page-break] + <<< + + === Doc Parts + + These 1❬sts❘{vertAlign=superscript}❭ multiple paragraph block stays untouched. + To show how comments spanning multiple paragraphs works. + These 3❬rd❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is not “Bart”. + To show how comments spanning multiple paragraphs works. + These 5❬th❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is not “Bart”. + To show how comments spanning multiple paragraphs works. + These 6❬th❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is not “null”. + To show how comments spanning multiple paragraphs works. + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display 'null' elements") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfAbsentValue(ContextFactory factory) { + var context = factory.name(null); + var template = getResource(Path.of("ConditionalDisplayTest.docx")); + var expected = """ + == Conditional Display + + === Paragraphs + + This paragraph 1 stays untouched. + This paragraph 3 stays if “name” is not “Bart”. + This paragraph 5 stays if “name” is not “Bart”. + This paragraph 8 stays if “name” is null. + This paragraph 9 stays if “name” is null. + ==== Paragraphs in table + + |=== + |Works in tables + + |This paragraph 1.1 stays if “name” is null. + | + + | + | + + + |=== + ==== Paragraphs in nested table + + |=== + |Works in nested tables + + ||=== + |Really + + | + + + |=== + + + |=== + + [page-break] + <<< + + === Table Rows + + ==== Rows in table + + |=== + |Works in tables + + |This row 1 is: + |Untouched. + + |This row 3 stays: + |if “name” is not “Bart”. + + |This row 5 stays: + |if “name” is not “Bart”. + + |This row 8 stays: + |if “name” is null. + + |This row 9 stays: + |if “name” is null. + + + |=== + ==== Rows in nested table + + |=== + |Works in nested tables + + ||=== + |Really' + + + |=== + + + |=== + + [page-break] + <<< + + === Tables + + ==== Mono-cell fully commented. + + ==== Mono-cell partially commented. + + ==== Multi-cell fully commented. + + ==== Multi-cell partially commented. + + ==== If present Case. + + ==== If absent Case. + + |=== + |Cell 1.1 + |Cell 1.2 + + |Cell 2.1 + |Cell 2.2 + + + |=== + ==== Works in nested tables + + |=== + |Cell 1.1 + + | + + + |=== + + [page-break] + <<< + + === Words + + None. + No Simpsons. + + [page-break] + <<< + + === Doc Parts + + These 1❬sts❘{vertAlign=superscript}❭ multiple paragraph block stays untouched. + To show how comments spanning multiple paragraphs works. + These 3❬rd❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is not “Bart”. + To show how comments spanning multiple paragraphs works. + These 5❬th❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is not “Bart”. + To show how comments spanning multiple paragraphs works. + These 7❬th❘{vertAlign=superscript}❭ multiple paragraph block stays if “name” is “null”. + To show how comments spanning multiple paragraphs works. + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display Paragraph If Integration test (off case) + Inline processors Integration test") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfParagraphsTest_inlineProcessorExpressionsAreResolved(ContextFactory factory) { + var context = factory.name("Homer"); + var template = getResource(Path.of("ConditionalDisplayOfParagraphsWithoutCommentTest.docx")); + var expected = """ + == Conditional Display of Paragraphs + + Paragraph 1 stays untouched. + Paragraph 3 stays untouched. + |=== + |=== Conditional Display of paragraphs also works in tables + + |Paragraph 4 in cell 2,1 stays untouched. + | + + ||=== + |=== Also works in nested tables + + |Paragraph 6 in cell 2,1 in cell 3,1 stays untouched. + + + |=== + + + |=== + + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display Paragraph If Integration test (on case) + Inline processors Integration test") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfParagraphsTest_unresolvedInlineProcessorExpressionsAreRemoved(ContextFactory factory) { + var context = factory.name("Bart"); + var template = getResource(Path.of("ConditionalDisplayOfParagraphsWithoutCommentTest.docx")); + var expected = """ + == Conditional Display of Paragraphs + + Paragraph 1 stays untouched. + Paragraph 2 is only included if the “name” is “Bart”. + Paragraph 3 stays untouched. + |=== + |=== Conditional Display of paragraphs also works in tables + + |Paragraph 4 in cell 2,1 stays untouched. + |Paragraph 5 in cell 2,2 is only included if the “name” is “Bart”. + + ||=== + |=== Also works in nested tables + + |Paragraph 6 in cell 2,1 in cell 3,1 stays untouched. + Paragraph 7 in cell 2,1 in cell 3,1 is only included if the “name” is “Bart”. + + + |=== + + + |=== + + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display Table If Bug32 Regression test") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfTableRowsTest(ContextFactory factory) { + var context = factory.name("Homer"); + var template = getResource(Path.of("ConditionalDisplayOfTableRowsTest.docx")); + var expected = """ + == Conditional Display of Table Rows + + This paragraph stays untouched. + |=== + |This row stays untouched. + + |This row stays untouched. + + ||=== + |Also works on nested Tables + + |This row stays untouched. + + + |=== + + + |=== + + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display Table If Bug32 Regression test") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfTableBug32Test(ContextFactory factory) { + var context = factory.name("Homer"); + var template = getResource(Path.of("ConditionalDisplayOfTablesBug32Test.docx")); + var expected = """ + == Conditional Display of Tables + + This paragraph stays untouched. + + |=== + |This table stays untouched. + | + + | + | + + + |=== + + |=== + |Also works on nested tables + + | + + + |=== + + This paragraph stays untouched. + """; + + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } + + @DisplayName("Display Table If Integration test") + @ParameterizedTest + @MethodSource("factories") + void conditionalDisplayOfTableTest(ContextFactory factory) { + var context = factory.name("Homer"); + var template = getResource(Path.of("ConditionalDisplayOfTablesTest.docx")); + var expected = """ + == Conditional Display of Tables + + This paragraph stays untouched. + + |=== + |This table stays untouched. + | + + | + | + + + |=== + + |=== + |Also works on nested tables + + | + + + |=== + + This paragraph stays untouched. + """; + var config = standard(); + var stamper = new TestDocxStamper<>(config); + var actual = stamper.stampAndLoadAndExtract(template, context); + assertEquals(expected, actual); + } +} 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 93741615..a1afd171f 100644 --- a/engine/src/test/java/pro/verron/officestamper/test/DefaultTests.java +++ b/engine/src/test/java/pro/verron/officestamper/test/DefaultTests.java @@ -41,12 +41,6 @@ private static Stream tests() { pipe.accept(replaceWordWithIntegrationTest(factory)); pipe.accept(replaceNullExpressionTest(factory)); pipe.accept(replaceNullExpressionTest2(factory)); - pipe.accept(conditionalDisplayOfParagraphsTest_processorExpressionsInCommentsAreResolved(factory)); - pipe.accept(conditionalDisplayOfParagraphsTest_inlineProcessorExpressionsAreResolved(factory)); - pipe.accept(conditionalDisplayOfParagraphsTest_unresolvedInlineProcessorExpressionsAreRemoved(factory)); - pipe.accept(conditionalDisplayOfTableRowsTest(factory)); - pipe.accept(conditionalDisplayOfTableBug32Test(factory)); - pipe.accept(conditionalDisplayOfTableTest(factory)); pipe.accept(customEvaluationContextConfigurerTest_customEvaluationContextConfigurerIsHonored(factory)); pipe.accept(expressionReplacementInGlobalParagraphsTest(factory)); pipe.accept(expressionReplacementInTablesTest(factory)); @@ -64,7 +58,7 @@ private static Stream tests() { }), Stream.of(nullPointerResolutionTest_testWithCustomSpel(ContextFactory.objectContextFactory()))); } - static Stream factories() { + private static Stream factories() { return Stream.of(objectContextFactory(), mapContextFactory()); } @@ -122,195 +116,6 @@ private static Arguments replaceNullExpressionTest2(ContextFactory factory) { } - private static Arguments conditionalDisplayOfParagraphsTest_processorExpressionsInCommentsAreResolved(ContextFactory factory) { - var context = factory.name("Homer"); - var template = getResource(Path.of("ConditionalDisplayOfParagraphsTest.docx")); - var expected = """ - == Conditional Display of Paragraphs - - This paragraph stays untouched. - This paragraph stays untouched. - |=== - |=== Conditional Display of paragraphs also works in tables - - |This paragraph stays untouched. - | - - ||=== - |=== Also works in nested tables - - |This paragraph stays untouched. - - - |=== - - - |=== - - """; - - return arguments("Display Paragraph If Integration test", standard(), context, template, expected); - } - - private static Arguments conditionalDisplayOfParagraphsTest_inlineProcessorExpressionsAreResolved(ContextFactory factory) { - var context = factory.name("Homer"); - var template = getResource(Path.of("ConditionalDisplayOfParagraphsWithoutCommentTest.docx")); - var expected = """ - == Conditional Display of Paragraphs - - Paragraph 1 stays untouched. - Paragraph 3 stays untouched. - |=== - |=== Conditional Display of paragraphs also works in tables - - |Paragraph 4 in cell 2,1 stays untouched. - | - - ||=== - |=== Also works in nested tables - - |Paragraph 6 in cell 2,1 in cell 3,1 stays untouched. - - - |=== - - - |=== - - """; - return arguments("Display Paragraph If Integration test (off case) + Inline processors Integration test", - standard(), - context, - template, - expected); - } - - private static Arguments conditionalDisplayOfParagraphsTest_unresolvedInlineProcessorExpressionsAreRemoved( - ContextFactory factory - ) { - var context = factory.name("Bart"); - var template = getResource(Path.of("ConditionalDisplayOfParagraphsWithoutCommentTest.docx")); - var expected = """ - == Conditional Display of Paragraphs - - Paragraph 1 stays untouched. - Paragraph 2 is only included if the “name” is “Bart”. - Paragraph 3 stays untouched. - |=== - |=== Conditional Display of paragraphs also works in tables - - |Paragraph 4 in cell 2,1 stays untouched. - |Paragraph 5 in cell 2,2 is only included if the “name” is “Bart”. - - ||=== - |=== Also works in nested tables - - |Paragraph 6 in cell 2,1 in cell 3,1 stays untouched. - Paragraph 7 in cell 2,1 in cell 3,1 is only included if the “name” is “Bart”. - - - |=== - - - |=== - - """; - return arguments("Display Paragraph If Integration test (on case) + Inline processors Integration test", - standard(), - context, - template, - expected); - } - - private static Arguments conditionalDisplayOfTableRowsTest(ContextFactory factory) { - var context = factory.name("Homer"); - var template = getResource(Path.of("ConditionalDisplayOfTableRowsTest.docx")); - var expected = """ - == Conditional Display of Table Rows - - This paragraph stays untouched. - |=== - |This row stays untouched. - - |This row stays untouched. - - ||=== - |Also works on nested Tables - - |This row stays untouched. - - - |=== - - - |=== - - """; - return arguments("Display Table Row If Integration test", standard(), context, template, expected); - } - - private static Arguments conditionalDisplayOfTableBug32Test(ContextFactory factory) { - var context = factory.name("Homer"); - var template = getResource(Path.of("ConditionalDisplayOfTablesBug32Test.docx")); - var expected = """ - == Conditional Display of Tables - - This paragraph stays untouched. - - |=== - |This table stays untouched. - | - - | - | - - - |=== - - |=== - |Also works on nested tables - - | - - - |=== - - This paragraph stays untouched. - """; - return arguments("Display Table If Bug32 Regression test", standard(), context, template, expected); - } - - private static Arguments conditionalDisplayOfTableTest(ContextFactory factory) { - var context = factory.name("Homer"); - var template = getResource(Path.of("ConditionalDisplayOfTablesTest" + ".docx")); - var expected = """ - == Conditional Display of Tables - - This paragraph stays untouched. - - |=== - |This table stays untouched. - | - - | - | - - - |=== - - |=== - |Also works on nested tables - - | - - - |=== - - This paragraph stays untouched. - """; - return arguments("Display Table If Integration test", standard(), context, template, expected); - } - private static Arguments customEvaluationContextConfigurerTest_customEvaluationContextConfigurerIsHonored( ContextFactory factory ) { 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 0f251234..010990df 100644 --- a/engine/src/test/java/pro/verron/officestamper/test/Stringifier.java +++ b/engine/src/test/java/pro/verron/officestamper/test/Stringifier.java @@ -8,6 +8,7 @@ import org.docx4j.mce.AlternateContent; import org.docx4j.model.structure.HeaderFooterPolicy; import org.docx4j.model.structure.SectionWrapper; +import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.PresentationMLPackage; import org.docx4j.openpackaging.packages.SpreadsheetMLPackage; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; @@ -211,10 +212,60 @@ public String stringify(Object o) { if (o instanceof CTShadow) return ""; if (o instanceof SdtRun run) return stringify(run.getSdtContent()); if (o instanceof SdtContent content) return stringify(content); + if (o instanceof R.AnnotationRef) return ""; + if (o instanceof CTFtnEdnRef ref) return "[" + ref.getId() + "]"; + if (o instanceof FootnotesPart footnotesPart) return stringify(footnotesPart).orElse(""); + if (o instanceof EndnotesPart endnotesPart) return stringify(endnotesPart).orElse(""); + if (o instanceof R.Separator) return "\n"; + if (o instanceof R.ContinuationSeparator) return "\n"; + if (o instanceof R.FootnoteRef) return ""; + if (o instanceof R.EndnoteRef) return ""; if (o == null) throw new RuntimeException("Unsupported content: NULL"); throw new RuntimeException("Unsupported content: " + o.getClass()); } + private Optional stringify(EndnotesPart endnotesPart) { + if (endnotesPart == null) return Optional.empty(); + try { + var list = endnotesPart.getContents() + .getEndnote() + .stream() + .map(this::stringify) + .flatMap(Optional::stream) + .toList(); + if (list.isEmpty()) return Optional.empty(); + return Optional.of(list.stream() + .collect(joining("\n", "[endnotes]\n---\n", "\n---\n"))); + + } catch (Docx4JException e) { + throw new OfficeStamperException("Error processing footnotes", e); + } + } + + private Optional stringify(FootnotesPart footnotesPart) { + if (footnotesPart == null) return Optional.empty(); + try { + var list = footnotesPart.getContents() + .getFootnote() + .stream() + .map(this::stringify) + .flatMap(Optional::stream) + .toList(); + if (list.isEmpty()) return Optional.empty(); + return Optional.of(list.stream() + .collect(joining("\n", "[footnotes]\n---\n", "\n---\n"))); + + } catch (Docx4JException e) { + throw new OfficeStamperException("Error processing footnotes", e); + } + } + + private Optional stringify(CTFtnEdn c) { + var type = ofNullable(c.getType()).orElse(STFtnEdn.NORMAL); + if (STFtnEdn.NORMAL != type) return Optional.empty(); + return Optional.of("[%s]%s".formatted(c.getId(), stringify(c.getContent()))); + } + private String stringify(SdtBlock block) { return stringify(block.getSdtContent()) + "\n"; } @@ -237,13 +288,17 @@ private String stringify(CommentRangeEnd cre) { private String stringify(WordprocessingMLPackage mlPackage) { var header = stringifyHeaders(getHeaderPart(mlPackage)); - var body = stringify(mlPackage.getMainDocumentPart()); + var mainDocumentPart = mlPackage.getMainDocumentPart(); + var body = stringify(mainDocumentPart); + var footer = stringifyFooters(getFooterPart(mlPackage)); - var hStr = header.map(h -> h + "\n\n") + var hStr = header.map("%s\n\n"::formatted) .orElse(""); - var fStr = footer.map(f -> "\n" + f + "\n") + var fStr = footer.map("\n%s\n"::formatted) .orElse(""); - return hStr + body + fStr; + var footnotesPart = mainDocumentPart.getFootnotesPart(); + var endnotesPart = mainDocumentPart.getEndNotesPart(); + return hStr + body + stringify(footnotesPart).orElse("") + stringify(endnotesPart).orElse("") + fStr; } private String stringify(Tc tc) { @@ -515,6 +570,7 @@ private Function decorateWithStyle(String value) { case "heading 5" -> "====== %s\n"::formatted; case "heading 6" -> "======= %s\n"::formatted; case "caption" -> ".%s"::formatted; + case "annotation text", "footnote text", "endnote text" -> string -> string; default -> "[%s] %%s".formatted(value)::formatted; }; } diff --git a/test/sources/ConditionalDisplayOfParagraphsTest.docx b/test/sources/ConditionalDisplayOfParagraphsTest.docx deleted file mode 100644 index be6b6527..00000000 Binary files a/test/sources/ConditionalDisplayOfParagraphsTest.docx and /dev/null differ diff --git a/test/sources/ConditionalDisplayTest.docx b/test/sources/ConditionalDisplayTest.docx new file mode 100644 index 00000000..b65dcfb2 Binary files /dev/null and b/test/sources/ConditionalDisplayTest.docx differ diff --git a/test/sources/endnotes.docx b/test/sources/endnotes.docx new file mode 100644 index 00000000..523086a2 Binary files /dev/null and b/test/sources/endnotes.docx differ diff --git a/test/sources/footnotes.docx b/test/sources/footnotes.docx new file mode 100644 index 00000000..b2ef2514 Binary files /dev/null and b/test/sources/footnotes.docx differ