DocxStamper.java
package pro.verron.officestamper.core;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.relationships.Namespaces;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.NonNull;
import pro.verron.officestamper.api.*;
import java.io.InputStream;
import java.io.OutputStream;
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;
/// The DocxStamper class is an implementation of the [OfficeStamper]
/// interface that is used to stamp DOCX templates with a context object and
/// write the result to an output stream.
///
/// @author Tom Hombergs
/// @author Joseph Verron
/// @version ${version}
/// @since 1.0.0
public class DocxStamper
implements OfficeStamper<WordprocessingMLPackage> {
private final List<PreProcessor> preprocessors;
private final List<PostProcessor> postprocessors;
private final PlaceholderReplacer placeholderReplacer;
private final Function<DocxPart, CommentProcessorRegistry> commentProcessorRegistrySupplier;
/// Creates a new DocxStamper with the given configuration.
///
/// @param configuration the configuration to use for this DocxStamper.
public DocxStamper(OfficeStamperConfiguration configuration) {
this(
configuration.getLineBreakPlaceholder(),
configuration.getEvaluationContextConfigurer(),
configuration.getExpressionFunctions(),
configuration.customFunctions(),
configuration.getResolvers(),
configuration.getCommentProcessors(),
configuration.getPreprocessors(),
configuration.getPostprocessors(),
configuration.getSpelParserConfiguration(),
configuration.getExceptionResolver()
);
}
private DocxStamper(
@NonNull String lineBreakPlaceholder,
EvaluationContextConfigurer evaluationContextConfigurer,
Map<Class<?>, Object> expressionFunctions,
List<CustomFunction> functions,
List<ObjectResolver> resolvers,
Map<Class<?>, Function<ParagraphPlaceholderReplacer, CommentProcessor>> configurationCommentProcessors,
List<PreProcessor> preprocessors,
List<PostProcessor> postprocessors,
SpelParserConfiguration spelParserConfiguration,
ExceptionResolver exceptionResolver
) {
var expressionParser = new SpelExpressionParser(spelParserConfiguration);
var evaluationContext = new StandardEvaluationContext();
evaluationContextConfigurer.configureEvaluationContext(evaluationContext);
var expressionResolver = new ExpressionResolver(evaluationContext, expressionParser);
var typeResolverRegistry = new ObjectResolverRegistry(resolvers);
this.placeholderReplacer = new PlaceholderReplacer(
typeResolverRegistry,
expressionResolver,
Placeholders.raw(lineBreakPlaceholder),
exceptionResolver);
var commentProcessors = buildCommentProcessors(configurationCommentProcessors);
evaluationContext.addMethodResolver(new Invokers(streamInvokers(commentProcessors)));
evaluationContext.addMethodResolver(new Invokers(streamInvokers(expressionFunctions)));
evaluationContext.addMethodResolver(new Invokers(functions.stream()
.map(Invokers::ofCustomFunction)));
this.commentProcessorRegistrySupplier = source -> new CommentProcessorRegistry(
source,
expressionResolver,
commentProcessors,
exceptionResolver);
this.preprocessors = new ArrayList<>(preprocessors);
this.postprocessors = new ArrayList<>(postprocessors);
}
private CommentProcessors buildCommentProcessors(
Map<Class<?>, Function<ParagraphPlaceholderReplacer, CommentProcessor>> commentProcessors
) {
var processors = new HashMap<Class<?>, CommentProcessor>();
for (var entry : commentProcessors.entrySet()) {
processors.put(
entry.getKey(),
entry.getValue()
.apply(placeholderReplacer));
}
return new CommentProcessors(processors);
}
/// Reads in a .docx template and "stamps" it into the given OutputStream, using the specified context object to
/// fill out any expressions it finds.
///
/// In the .docx template you have the following options to influence the "stamping" process:
/// - Use expressions like ${name} or ${person.isOlderThan(18)} in the template's text. These expressions are
/// resolved
/// against the contextRoot object you pass into this method and are replaced by the results.
/// - Use comments within the .docx template to mark certain paragraphs to be manipulated.
///
/// Within comments, you can put expressions in which you can use the following methods by default:
/// - _displayParagraphIf(boolean)_ to conditionally display paragraphs or not
/// - _displayTableRowIf(boolean)_ to conditionally display table rows or not
/// - _displayTableIf(boolean)_ to conditionally display whole tables or not
/// - _repeatTableRow(List<Object>)_ to create a new table row for each object in the list and
/// resolve expressions
/// 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 [OfficeStamperConfiguration#addCommentProcessor(Class, Function)].
public void stamp(InputStream template, Object contextRoot, OutputStream out) {
try {
WordprocessingMLPackage document = WordprocessingMLPackage.load(template);
stamp(document, contextRoot, out);
} catch (Docx4JException e) {
throw new OfficeStamperException(e);
}
}
/// 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) {
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);
}
}
private void preprocess(WordprocessingMLPackage document) {
preprocessors.forEach(processor -> processor.process(document));
}
private void processComments(DocxPart document, Object contextObject) {
document.streamParts(Namespaces.HEADER)
.forEach(header -> runProcessors(header, contextObject));
runProcessors(document, contextObject);
document.streamParts(Namespaces.FOOTER)
.forEach(footer -> runProcessors(footer, contextObject));
}
private void replaceExpressions(DocxPart document, Object contextObject) {
document.streamParts(Namespaces.HEADER)
.forEach(s -> placeholderReplacer.resolveExpressions(s, contextObject));
placeholderReplacer.resolveExpressions(document, contextObject);
document.streamParts(Namespaces.FOOTER)
.forEach(s -> placeholderReplacer.resolveExpressions(s, contextObject));
}
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));
}
}