src/main/java/org/wickedsource/docxstamper/api/commentprocessor/ICommentProcessor.java
public interface ICommentProcessor {

    /**
     * This method is called after all comments in the .docx template have been passed to the comment processor.
     * All manipulations of the .docx document SHOULD BE done in this method. If certain manipulations are already done
     * within in the custom methods of a comment processor, the ongoing iteration over the paragraphs in the document
     * may be disturbed.
     *
     * @param document The word document that can be manipulated by using the DOCX4J api.
     */
    void commitChanges(WordprocessingMLPackage document);

    /**
     * Passes the paragraph that is currently being processed (i.e. the paragraph that is commented in the
     * .docx template. This method is always called BEFORE the custom methods of the custom comment processor interface
     * are called.
     *
     * @param coordinates coordinates of the currently processed paragraph within the template.
     */
    void setCurrentParagraphCoordinates(ParagraphCoordinates coordinates);


    /**
     * Passes the run that is currently being processed (i.e. the run that is commented in the
     * .docx template. This method is always called BEFORE the custom methods of the custom comment processor interface
     * are called.
     *
     * @param coordinates coordinates of the currently processed run within the template.
     */
    void setCurrentRunCoordinates(RunCoordinates coordinates);


    /**
     * Passes the comment range wrapper that is currently being processed
     * (i.e. the start and end of comment that in the .docx template.
     * This method is always called BEFORE the custom methods of the custom comment
     * processor interface are called.
     *
     *  @param commentWrapper of the currently processed comment within the template.
     */
    void setCurrentCommentWrapper(CommentWrapper commentWrapper);

    /**
     * Resets all state in the comment processor so that it can be re-used in another stamping process.
     */
    void reset();

}
src/main/java/org/wickedsource/docxstamper/util/CommentUtil.java
public class CommentUtil {

	private static Logger logger = LoggerFactory.getLogger(CommentUtil.class);

	private CommentUtil() {

	}

	/**
	 * Returns the comment the given DOCX4J run is commented with.
	 * @param run the DOCX4J run whose comment to retrieve.
	 * @param document the document that contains the run.
	 * @return the comment, if found, null otherwise.
	 */
	public static Comments.Comment getCommentAround(R run,
			WordprocessingMLPackage document) {
		try {
			if (run instanceof Child) {
				Child child = (Child) run;
				ContentAccessor parent = (ContentAccessor) child.getParent();
				if (parent == null)
					return null;
				CommentRangeStart possibleComment = null;
				boolean foundChild = false;
				for (Object contentElement : parent.getContent()) {

					// so first we look for the start of the comment
					if (XmlUtils.unwrap(contentElement) instanceof CommentRangeStart) {
						possibleComment = (CommentRangeStart) contentElement;
					}
					// then we check if the child we are looking for is ours
					else if (possibleComment != null && child.equals(contentElement)) {
						foundChild = true;
					}
					// and then if we have an end of a comment we are good!
					else if (possibleComment != null && foundChild && XmlUtils
							.unwrap(contentElement) instanceof CommentRangeEnd) {
						try {
							BigInteger id = possibleComment.getId();
							CommentsPart commentsPart = (CommentsPart) document.getParts()
									.get(new PartName("/word/comments.xml"));
							Comments comments = commentsPart.getContents();
							for (Comments.Comment comment : comments.getComment()) {
								if (comment.getId().equals(id)) {
									return comment;
								}
							}
						}
						catch (InvalidFormatException e) {
							logger.warn(String.format(
									"Error while searching comment. Skipping run %s.",
									run), e);
						}
					}
					// else restart
					else {
						possibleComment = null;
						foundChild = false;
					}
				}
			}
			return null;
		}
		catch (Docx4JException e) {
			throw new DocxStamperException(
					"error accessing the comments of the document!", e);
		}
	}

	/**
	 * Returns the first comment found for the given docx object. Note that an object is
	 * only considered commented if the comment STARTS within the object. Comments
	 * spanning several objects are not supported by this method.
	 *
	 * @param object the object whose comment to load.
	 * @param document the document in which the object is embedded (needed to load the
	 * comment from the comments.xml part).
	 * @return the concatenated string of all paragraphs of text within the comment or
	 * null if the specified object is not commented.
	 * @throws Docx4JException in case of a Docx4J processing error.
	 */
	public static Comments.Comment getCommentFor(ContentAccessor object,
			WordprocessingMLPackage document) {
		try {
			for (Object contentObject : object.getContent()) {
				if (contentObject instanceof CommentRangeStart) {
					try {
						BigInteger id = ((CommentRangeStart) contentObject).getId();
						CommentsPart commentsPart = (CommentsPart) document.getParts()
								.get(new PartName("/word/comments.xml"));
						Comments comments = commentsPart.getContents();
						for (Comments.Comment comment : comments.getComment()) {
							if (comment.getId().equals(id)) {
								return comment;
							}
						}
					}
					catch (InvalidFormatException e) {
						logger.warn(String.format(
								"Error while searching comment. Skipping object %s.",
								object), e);
					}
				}
			}
			return null;
		}
		catch (Docx4JException e) {
			throw new DocxStamperException(
					"error accessing the comments of the document!", e);
		}
	}

	public static String getCommentStringFor(ContentAccessor object,
			WordprocessingMLPackage document) throws Docx4JException {
		Comments.Comment comment = getCommentFor(object, document);
		return getCommentString(comment);
	}

	/**
	 * Returns the string value of the specified comment object.
	 */
	public static String getCommentString(Comments.Comment comment) {
		StringBuilder builder = new StringBuilder();
		for (Object commentChildObject : comment.getContent()) {
			if (commentChildObject instanceof P) {
				builder.append(new ParagraphWrapper((P) commentChildObject).getText());
			}
		}
		return builder.toString();
	}

	public static void deleteComment(CommentWrapper comment) {
		if (comment.getCommentRangeEnd() != null) {
			ContentAccessor commentRangeEndParent = (ContentAccessor) comment
					.getCommentRangeEnd().getParent();
			commentRangeEndParent.getContent().remove(comment.getCommentRangeEnd());
			deleteCommentReference(commentRangeEndParent,
					comment.getCommentRangeEnd().getId());
		}
		if (comment.getCommentRangeStart() != null) {
			ContentAccessor commentRangeStartParent = (ContentAccessor) comment
					.getCommentRangeStart().getParent();
			commentRangeStartParent.getContent().remove(comment.getCommentRangeStart());
			deleteCommentReference(commentRangeStartParent,
					comment.getCommentRangeStart().getId());
		}
		// TODO: also delete comment from comments.xml
	}

	private static boolean deleteCommentReference(ContentAccessor parent,
			BigInteger commentId) {
		for (int i = 0; i < parent.getContent().size(); i++) {
			Object contentObject = XmlUtils.unwrap(parent.getContent().get(i));
			if (contentObject instanceof ContentAccessor) {
				if (deleteCommentReference((ContentAccessor) contentObject, commentId)) {
					return true;
				}
			}
			if (contentObject instanceof R) {
				for (Object runContentObject : ((R) contentObject).getContent()) {
					Object unwrapped = XmlUtils.unwrap(runContentObject);
					if (unwrapped instanceof R.CommentReference) {
						BigInteger foundCommentId = ((R.CommentReference) unwrapped)
								.getId();
						if (foundCommentId.equals(commentId)) {
							parent.getContent().remove(i);
							return true;
						}
					}
				}
			}
		}
		return false;
	}

	public static Map<BigInteger, CommentWrapper> getComments(
			WordprocessingMLPackage document) {
		Map<BigInteger, CommentWrapper> comments = new HashMap<>();
		collectCommentRanges(comments, document);
		collectComments(comments, document);
		return comments;
	}

	private static void collectCommentRanges(
			final Map<BigInteger, CommentWrapper> comments,
			WordprocessingMLPackage document) {
		DocumentWalker documentWalker = new BaseDocumentWalker(
				document.getMainDocumentPart()) {
			@Override
			protected void onCommentRangeStart(CommentRangeStart commentRangeStart) {
				CommentWrapper commentWrapper = comments.get(commentRangeStart.getId());
				if (commentWrapper == null) {
					commentWrapper = new CommentWrapper();
					comments.put(commentRangeStart.getId(), commentWrapper);
				}
				commentWrapper.setCommentRangeStart(commentRangeStart);
			}

			@Override
			protected void onCommentRangeEnd(CommentRangeEnd commentRangeEnd) {
				CommentWrapper commentWrapper = comments.get(commentRangeEnd.getId());
				if (commentWrapper == null) {
					commentWrapper = new CommentWrapper();
					comments.put(commentRangeEnd.getId(), commentWrapper);
				}
				commentWrapper.setCommentRangeEnd(commentRangeEnd);
			}
		};
		documentWalker.walk();
	}

	private static void collectComments(final Map<BigInteger, CommentWrapper> comments,
			WordprocessingMLPackage document) {
		try {
			CommentsPart commentsPart = (CommentsPart) document.getParts()
					.get(new PartName("/word/comments.xml"));
			if (commentsPart != null) {
				for (Comments.Comment comment : commentsPart.getContents().getComment()) {
					CommentWrapper commentWrapper = comments.get(comment.getId());
					if (commentWrapper != null) {
						commentWrapper.setComment(comment);
					}
				}
			}
		}
		catch (Docx4JException e) {
			throw new IllegalStateException(e);
		}
	}

}
src/main/java/org/wickedsource/docxstamper/api/coordinates/ParagraphCoordinates.java
public class ParagraphCoordinates extends AbstractCoordinates {

    private final P paragraph;

    private final int index;

    private final TableCellCoordinates parentTableCellCoordinates;


    public ParagraphCoordinates(P paragraph, int index, TableCellCoordinates parentTableCellCoordinates) {
        this.paragraph = paragraph;
        this.index = index;
        this.parentTableCellCoordinates = parentTableCellCoordinates;
    }

    public ParagraphCoordinates(P paragraph, int index) {
        this.paragraph = paragraph;
        this.index = index;
        this.parentTableCellCoordinates = null;
    }

    public P getParagraph() {
        return paragraph;
    }

    public int getIndex() {
        return index;
    }

    public TableCellCoordinates getParentTableCellCoordinates() {
        return parentTableCellCoordinates;
    }

    public String toString() {
        String toString = String.format("paragraph at index %d", index);
        if (parentTableCellCoordinates != null) {
            toString = parentTableCellCoordinates.toString() + "\n" + toString;
        }
        return toString;
    }
}
src/main/java/org/wickedsource/docxstamper/el/ExpressionResolver.java
public class ExpressionResolver {

    private static final ExpressionUtil expressionUtil = new ExpressionUtil();

    private final EvaluationContextConfigurer evaluationContextConfigurer;

    public ExpressionResolver() {
        this.evaluationContextConfigurer = new NoOpEvaluationContextConfigurer();
    }

    public ExpressionResolver(EvaluationContextConfigurer evaluationContextConfigurer) {
        this.evaluationContextConfigurer = evaluationContextConfigurer;
    }

    /**
     * Runs the given expression against the given context object and returns the result of the evaluated expression.
     *
     * @param expressionString the expression to evaluate.
     * @param contextRoot      the context object against which the expression is evaluated.
     * @return the result of the evaluated expression.
     */
    public Object resolveExpression(String expressionString, Object contextRoot) {
        if ((expressionString.startsWith("${") || expressionString.startsWith("#{")) && expressionString.endsWith("}")) {
            expressionString = expressionUtil.stripExpression(expressionString);
        }
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext evaluationContext = new StandardEvaluationContext(contextRoot);
        evaluationContextConfigurer.configureEvaluationContext(evaluationContext);
        Expression expression = parser.parseExpression(expressionString);
        return expression.getValue(evaluationContext);
    }

}
src/main/java/org/wickedsource/docxstamper/proxy/ProxyException.java
public class ProxyException extends Exception {

    public ProxyException(Throwable cause) {
        super(cause);
    }

}
src/main/java/org/wickedsource/docxstamper/util/ParagraphWrapper.java
public class ParagraphWrapper {

    private int currentPosition = 0;

    private List<IndexedRun> runs = new ArrayList<>();

    private P paragraph;

    public ParagraphWrapper(P paragraph) {
        this.paragraph = paragraph;
        recalculateRuns();
    }

    public void recalculateRuns() {
        currentPosition = 0;
        this.runs.clear();
        int index = 0;
        for (Object contentElement : paragraph.getContent()) {
            if (contentElement instanceof R && !"".equals(RunUtil.getText((R) contentElement))) {
                this.addRun((R) contentElement, index);
            }
            index++;
        }
    }

    /**
     * Adds a run to the aggregation.
     *
     * @param run the run to add.
     */
    private void addRun(R run, int index) {
        int startIndex = currentPosition;
        int endIndex = currentPosition + RunUtil.getText(run).length() - 1;
        runs.add(new IndexedRun(startIndex, endIndex, index, run));
        currentPosition = endIndex + 1;
    }


    /**
     * Replaces the given placeholder String with the replacement object within the paragraph.
     * The replacement object must be a valid DOCX4J Object.
     *
     * @param placeholder the placeholder to be replaced.
     * @param replacement the object to replace the placeholder String.
     */
    public void replace(String placeholder, Object replacement) {
        String text = getText();
        int matchStartIndex = text.indexOf(placeholder);
        if (matchStartIndex == -1) {
            // nothing to replace
            return;
        }
        int matchEndIndex = matchStartIndex + placeholder.length() - 1;
        List<IndexedRun> affectedRuns = getAffectedRuns(matchStartIndex, matchEndIndex);

        boolean singleRun = affectedRuns.size() == 1;

        if (singleRun) {
            IndexedRun run = affectedRuns.get(0);

            boolean placeholderSpansCompleteRun = placeholder.length() == RunUtil.getText(run.getRun()).length();
            boolean placeholderAtStartOfRun = matchStartIndex == run.getStartIndex();
            boolean placeholderAtEndOfRun = matchEndIndex == run.getEndIndex();
            boolean placeholderWithinRun = matchStartIndex > run.getStartIndex() && matchEndIndex < run.getEndIndex();

            if (placeholderSpansCompleteRun) {
                this.paragraph.getContent().remove(run.getRun());
                this.paragraph.getContent().add(run.getIndexInParent(), replacement);
                recalculateRuns();
            } else if (placeholderAtStartOfRun) {
                run.replace(matchStartIndex, matchEndIndex, "");
                this.paragraph.getContent().add(run.getIndexInParent(), replacement);
                recalculateRuns();
            } else if (placeholderAtEndOfRun) {
                run.replace(matchStartIndex, matchEndIndex, "");
                this.paragraph.getContent().add(run.getIndexInParent() + 1, replacement);
                recalculateRuns();
            } else if (placeholderWithinRun) {
                String runText = RunUtil.getText(run.getRun());
                int startIndex = runText.indexOf(placeholder);
                int endIndex = startIndex + placeholder.length();
                R run1 = RunUtil.create(runText.substring(0, startIndex), this.paragraph);
                R run2 = RunUtil.create(runText.substring(endIndex), this.paragraph);
                this.paragraph.getContent().add(run.getIndexInParent(), run2);
                this.paragraph.getContent().add(run.getIndexInParent(), replacement);
                this.paragraph.getContent().add(run.getIndexInParent(), run1);
                this.paragraph.getContent().remove(run.getRun());
                recalculateRuns();
            }

        } else {
            IndexedRun firstRun = affectedRuns.get(0);
            IndexedRun lastRun = affectedRuns.get(affectedRuns.size() - 1);

            // remove the placeholder from first and last run
            firstRun.replace(matchStartIndex, matchEndIndex, "");
            lastRun.replace(matchStartIndex, matchEndIndex, "");

            // remove all runs between first and last
            for (IndexedRun run : affectedRuns) {
                if (run != firstRun && run != lastRun) {
                    this.paragraph.getContent().remove(run.getRun());
                }
            }

            // add replacement run between first and last run
            this.paragraph.getContent().add(firstRun.getIndexInParent() + 1, replacement);

            recalculateRuns();
        }
    }

    private List<IndexedRun> getAffectedRuns(int startIndex, int endIndex) {
        List<IndexedRun> affectedRuns = new ArrayList<>();
        for (IndexedRun run : runs) {
            if (run.isTouchedByRange(startIndex, endIndex)) {
                affectedRuns.add(run);
            }
        }
        return affectedRuns;
    }

    /**
     * Returns the aggregated text over all runs.
     *
     * @return the text of all runs.
     */
    public String getText() {
        return getText(this.runs);
    }

    private String getText(List<IndexedRun> runs) {
        StringBuilder builder = new StringBuilder();
        for (IndexedRun run : runs) {
            builder.append(RunUtil.getText(run.getRun()));
        }
        return builder.toString();
    }

    /**
     * Returns the list of runs that are aggregated. Depending on what modifications were done to the aggregated text
     * this list may not return the same runs that were initially added to the aggregator.
     *
     * @return the list of aggregated runs.
     */
    public List<R> getRuns() {
        List<R> resultList = new ArrayList<>();
        for (IndexedRun run : runs) {
            resultList.add(run.getRun());
        }
        return resultList;
    }

    @Override
    public String toString() {
        return getText();
    }

    public P getParagraph() {
        return paragraph;
    }
}
src/main/java/org/wickedsource/docxstamper/util/walk/CoordinatesWalker.java
public abstract class CoordinatesWalker {

    private WordprocessingMLPackage document;

    public CoordinatesWalker(WordprocessingMLPackage document) {
        this.document = document;
    }

    public void walk() {

        RelationshipsPart relationshipsPart = document.getMainDocumentPart().getRelationshipsPart();

        // walk through elements in headers
        List<Relationship> headerRelationships = getRelationshipsOfType(document, Namespaces.HEADER);
        for (Relationship header : headerRelationships) {
            HeaderPart headerPart = (HeaderPart) relationshipsPart.getPart(header.getId());
            walkContent(headerPart.getContent());
        }

        // walk through elements in main document part
        walkContent(document.getMainDocumentPart().getContent());

        // walk through elements in headers
        List<Relationship> footerRelationships = getRelationshipsOfType(document, Namespaces.FOOTER);
        for (Relationship footer : footerRelationships) {
            FooterPart footerPart = (FooterPart) relationshipsPart.getPart(footer.getId());
            walkContent(footerPart.getContent());
        }
    }

    private List<Relationship> getRelationshipsOfType(WordprocessingMLPackage document, String type) {
        List<Relationship> allRelationhips = document
                .getMainDocumentPart()
                .getRelationshipsPart()
                .getRelationships()
                .getRelationship();
        List<Relationship> headerRelationships = new ArrayList<>();
        for (Relationship r : allRelationhips) {
            if (r.getType().equals(type)) {
                headerRelationships.add(r);
            }
        }
        return headerRelationships;
    }

    private void walkContent(List<Object> contentElements) {
        for (int i = 0; i < contentElements.size(); i++) {
            Object contentElement = contentElements.get(i);
            Object unwrappedObject = XmlUtils.unwrap(contentElement);
            if (unwrappedObject instanceof P) {
                P p = (P) unwrappedObject;
                ParagraphCoordinates coordinates = new ParagraphCoordinates(p, i);
                walkParagraph(coordinates);
            } else if (unwrappedObject instanceof Tbl) {
                Tbl table = (Tbl) unwrappedObject;
                TableCoordinates tableCoordinates = new TableCoordinates(table, i);
                walkTable(tableCoordinates);
            }
        }
    }
    
    private void walkParagraph(ParagraphCoordinates paragraphCoordinates){
    	int rowIndex = 0;
    	for (Object contentElement : paragraphCoordinates.getParagraph().getContent()){
    		 if (XmlUtils.unwrap(contentElement) instanceof R) {
    			 R run = (R) contentElement;
    			 RunCoordinates runCoordinates = new RunCoordinates(run, rowIndex);
    			 onRun(runCoordinates, paragraphCoordinates);
    		 }
    	}
    	// we run the paragraph afterwards so that the comments inside work before the whole paragraph comments
    	onParagraph(paragraphCoordinates);

    }
    
    private void walkTable(TableCoordinates tableCoordinates) {
        onTable(tableCoordinates);
        int rowIndex = 0;
        for (Object contentElement : tableCoordinates.getTable().getContent()) {
            if (XmlUtils.unwrap(contentElement) instanceof Tr) {
                Tr row = (Tr) contentElement;
                TableRowCoordinates rowCoordinates = new TableRowCoordinates(row, rowIndex, tableCoordinates);
                walkTableRow(rowCoordinates);
            }
            rowIndex++;
        }
    }


    private void walkTableRow(TableRowCoordinates rowCoordinates) {
        onTableRow(rowCoordinates);
        int cellIndex = 0;
        for (Object rowContentElement : rowCoordinates.getRow().getContent()) {
            if (XmlUtils.unwrap(rowContentElement) instanceof Tc) {
                Tc cell = rowContentElement instanceof Tc ? (Tc) rowContentElement : (Tc) ((JAXBElement) rowContentElement).getValue();
                TableCellCoordinates cellCoordinates = new TableCellCoordinates(cell, cellIndex, rowCoordinates);
                walkTableCell(cellCoordinates);
            }
            cellIndex++;
        }
    }

    private void walkTableCell(TableCellCoordinates cellCoordinates) {
        onTableCell(cellCoordinates);
        int elementIndex = 0;
        for (Object cellContentElement : cellCoordinates.getCell().getContent()) {
            if (XmlUtils.unwrap(cellContentElement) instanceof P) {
                P p = (P) cellContentElement;
                ParagraphCoordinates paragraphCoordinates = new ParagraphCoordinates(p, elementIndex, cellCoordinates);
                onParagraph(paragraphCoordinates);
            } else if (XmlUtils.unwrap(cellContentElement) instanceof Tbl) {
                Tbl nestedTable = (Tbl) ((JAXBElement) cellContentElement).getValue();
                TableCoordinates innerTableCoordinates = new TableCoordinates(nestedTable, elementIndex, cellCoordinates);
                walkTable(innerTableCoordinates);
            }
            elementIndex++;
        }
    }

    protected abstract void onParagraph(ParagraphCoordinates paragraphCoordinates);

    protected abstract void onRun(RunCoordinates runCoordinates, ParagraphCoordinates paragraphCoordinates);
    
    protected abstract void onTable(TableCoordinates tableCoordinates);

    protected abstract void onTableCell(TableCellCoordinates tableCellCoordinates);

    protected abstract void onTableRow(TableRowCoordinates tableRowCoordinates);

}
src/main/java/org/wickedsource/docxstamper/el/ExpressionUtil.java
public class ExpressionUtil {

    /**
     * Finds all variable expressions in a text and returns them as list. Example expression: "${myObject.property}".
     *
     * @param text the text to find expressions in.
     * @return a list of expressions (including the starting "${" and trailing "}").
     */
    public List<String> findVariableExpressions(String text) {
        return findExpressions(text, "\\$\\{.*?\\}");
    }

    /**
     * Finds all processor expressions in a text and returns them as list. Example expression: "#{myObject.property}".
     *
     * @param text the text to find expressions in.
     * @return a list of expressions (including the starting "#{" and trailing "}").
     */
    public List<String> findProcessorExpressions(String text) {
        return findExpressions(text, "\\#\\{.*?\\}");
    }

    private List<String> findExpressions(String text, String expressionPattern) {
        List<String> matches = new ArrayList<>();
        if ("".equals(text) || text == null) {
            return matches;
        }
        Pattern pattern = Pattern.compile(expressionPattern);
        Matcher matcher = pattern.matcher(text);
        int index = 0;
        while (matcher.find(index)) {
            String match = matcher.group();
            matches.add(match);
            index = matcher.end();
        }
        return matches;
    }

    /**
     * Strips an expression of the leading "${" or "#{" and the trailing "}".
     *
     * @param expression the expression to strip.
     * @return the expression without the leading "${" or "#{" and the trailing "}".
     */
    public String stripExpression(String expression) {
        if (expression == null) {
            throw new IllegalArgumentException("Cannot strip NULL expression!");
        }
        expression = expression.replaceAll("^\\$\\{", "").replaceAll("\\}$", "");
        expression = expression.replaceAll("^#\\{", "").replaceAll("\\}$", "");
        return expression;
    }

}
src/main/java/org/wickedsource/docxstamper/api/coordinates/RunCoordinates.java
public class RunCoordinates extends AbstractCoordinates {

	private R run;
	private int index;

	public RunCoordinates(R run, int index) {
		this.run = run;
		this.index = index;
	}

	public R getRun() {
		return run;
	}

	public int getIndex() {
		return index;
	}

	@Override
	public String toString() {
		String toString = String.format("run at index %d", index);
		return toString;
	}

}
src/main/java/org/wickedsource/docxstamper/api/UnresolvedExpressionException.java
public class UnresolvedExpressionException extends DocxStamperException {

    public UnresolvedExpressionException(String expression, Throwable cause) {
        super(String.format("The following expression could not be processed by any comment processor: %s", expression), cause);
    }
}
src/main/java/org/wickedsource/docxstamper/replace/PlaceholderReplacer.java
public class PlaceholderReplacer<T> {

  private Logger logger = LoggerFactory.getLogger(PlaceholderReplacer.class);

  private ExpressionUtil expressionUtil = new ExpressionUtil();

  private ExpressionResolver expressionResolver = new ExpressionResolver();

  private TypeResolverRegistry typeResolverRegistry;

  private String lineBreakPlaceholder;

  private boolean leaveEmptyOnExpressionError = false;

  private boolean replaceNullValues = false;

  public PlaceholderReplacer(TypeResolverRegistry typeResolverRegistry) {
    this.typeResolverRegistry = typeResolverRegistry;
  }

  public PlaceholderReplacer(TypeResolverRegistry typeResolverRegistry, String lineBreakPlaceholder) {
    this.typeResolverRegistry = typeResolverRegistry;
    this.lineBreakPlaceholder = lineBreakPlaceholder;
  }

  public boolean isLeaveEmptyOnExpressionError() {
    return leaveEmptyOnExpressionError;
  }

  public void setLeaveEmptyOnExpressionError(boolean leaveEmptyOnExpressionError) {
    this.leaveEmptyOnExpressionError = leaveEmptyOnExpressionError;
  }

  public void setReplaceNullValues(boolean replaceNullValues) {
    this.replaceNullValues = replaceNullValues;
  }
  
  public void setExpressionResolver(ExpressionResolver expressionResolver) {
    this.expressionResolver = expressionResolver;
  }

  /**
   * Finds expressions in a document and resolves them against the specified context object. The expressions in the
   * document are then replaced by the resolved values.
   *
   * @param document          the document in which to replace all expressions.
   * @param proxyBuilder      builder for a proxy around the context root to customize its interface
   */
  public void resolveExpressions(final WordprocessingMLPackage document, ProxyBuilder<T> proxyBuilder) {
    try {
      final T expressionContext = proxyBuilder.build();
      CoordinatesWalker walker = new BaseCoordinatesWalker(document) {
        @Override
        protected void onParagraph(ParagraphCoordinates paragraphCoordinates) {
          resolveExpressionsForParagraph(paragraphCoordinates.getParagraph(), expressionContext, document);
        }
      };
      walker.walk();
    } catch (ProxyException e) {
      throw new DocxStamperException("could not create proxy around context root!", e);
    }
  }

  @SuppressWarnings("unchecked")
  public void resolveExpressionsForParagraph(P p, T expressionContext, WordprocessingMLPackage document) {
    ParagraphWrapper paragraphWrapper = new ParagraphWrapper(p);
    List<String> placeholders = expressionUtil.findVariableExpressions(paragraphWrapper.getText());
    for (String placeholder : placeholders) {
      try {
        Object replacement = expressionResolver.resolveExpression(placeholder, expressionContext);
        if (replacement != null) {
          ITypeResolver resolver = typeResolverRegistry.getResolverForType(replacement.getClass());
          Object replacementObject = resolver.resolve(document, replacement);
          replace(paragraphWrapper, placeholder, replacementObject);
          logger.debug(String.format("Replaced expression '%s' with value provided by TypeResolver %s", placeholder, resolver.getClass()));
        } else if(replaceNullValues) {
            ITypeResolver resolver = typeResolverRegistry.getDefaultResolver();
            Object replacementObject = resolver.resolve(document, replacement);
            replace(paragraphWrapper, placeholder, replacementObject);
            logger.debug(String.format("Replaced expression '%s' with value provided by TypeResolver %s", placeholder, resolver.getClass()));
        }
      } catch (SpelEvaluationException | SpelParseException e) {
        logger.warn(String.format(
                "Expression %s could not be resolved against context root of type %s. Reason: %s. Set log level to TRACE to view Stacktrace.",
                placeholder, expressionContext.getClass(), e.getMessage()));
        logger.trace("Reason for skipping expression:", e);

        if(isLeaveEmptyOnExpressionError()) {
          replace(paragraphWrapper,placeholder,null);
        }
      }
    }
    if (this.lineBreakPlaceholder != null) {
      replaceLineBreaks(paragraphWrapper);
    }
  }

  private void replaceLineBreaks(ParagraphWrapper paragraphWrapper) {
    Br lineBreak = Context.getWmlObjectFactory().createBr();
    R run = RunUtil.create(lineBreak);
    while (paragraphWrapper.getText().contains(this.lineBreakPlaceholder)) {
      replace(paragraphWrapper, this.lineBreakPlaceholder, run);
    }
  }

  public void replace(ParagraphWrapper p, String placeholder, Object replacementObject) {
    if (replacementObject == null) {
      replacementObject = RunUtil.create("");
    }
    if (replacementObject instanceof R) {
      RunUtil.applyParagraphStyle(p.getParagraph(), (R) replacementObject);
    }
    p.replace(placeholder, replacementObject);
  }

}
src/main/java/org/wickedsource/docxstamper/api/DocxStamperException.java
public class DocxStamperException extends RuntimeException {

    public DocxStamperException(String message) {
        super(message);
    }

    public DocxStamperException(String message, Throwable cause) {
        super(message, cause);
    }

    public DocxStamperException(Throwable cause) {
        super(cause);
    }

}
src/main/java/org/wickedsource/docxstamper/util/CommentWrapper.java
public class CommentWrapper {

    private Comments.Comment comment;

    private CommentRangeStart commentRangeStart;

    private CommentRangeEnd commentRangeEnd;

    public CommentWrapper(Comments.Comment comment, CommentRangeStart commentRangeStart, CommentRangeEnd commentRangeEnd) {
        this.comment = comment;
        this.commentRangeStart = commentRangeStart;
        this.commentRangeEnd = commentRangeEnd;
    }

    public CommentWrapper() {
    }

    public Comments.Comment getComment() {
        return comment;
    }

    public CommentRangeStart getCommentRangeStart() {
        return commentRangeStart;
    }

    public CommentRangeEnd getCommentRangeEnd() {
        return commentRangeEnd;
    }

    void setComment(Comments.Comment comment) {
        this.comment = comment;
    }

    void setCommentRangeStart(CommentRangeStart commentRangeStart) {
        this.commentRangeStart = commentRangeStart;
    }

    void setCommentRangeEnd(CommentRangeEnd commentRangeEnd) {
        this.commentRangeEnd = commentRangeEnd;
    }
}
src/main/java/org/wickedsource/docxstamper/util/walk/BaseCoordinatesWalker.java
public abstract class BaseCoordinatesWalker extends CoordinatesWalker {

    public BaseCoordinatesWalker(WordprocessingMLPackage document) {
        super(document);
    }

    @Override
    protected void onParagraph(ParagraphCoordinates paragraphCoordinates) {

    }

    @Override
    protected void onTable(TableCoordinates tableCoordinates) {

    }

    @Override
    protected void onTableCell(TableCellCoordinates tableCellCoordinates) {

    }

    @Override
    protected void onTableRow(TableRowCoordinates tableRowCoordinates) {

    }
    
    @Override
	protected void onRun(RunCoordinates runCoordinates, ParagraphCoordinates paragraphCoordinates) {

	}
}
src/main/java/org/wickedsource/docxstamper/proxy/ProxyBuilder.java
public class ProxyBuilder<T> {

  private T root;

  private Map<Class<?>, Object> interfacesToImplementations = new HashMap<>();

  /**
   * Specifies the root object for the proxy that shall be enhanced.
   *
   * @param rootObject the root object.
   * @return this builder for chaining.
   */
  public ProxyBuilder<T> withRoot(T rootObject) {
    this.root = rootObject;
    return this;
  }

  /**
   * Specifies an interfaces and an implementation of an interface by which the root object
   * shall be extended.
   *
   * @param interfaceClass the class of the interface
   * @param interfaceImpl  an implementation of the interface
   * @return this builder for chaining.
   */
  public ProxyBuilder<T> withInterface(Class<?> interfaceClass, Object interfaceImpl) {
    this.interfacesToImplementations.put(interfaceClass, interfaceImpl);
    return this;
  }

  /**
   * Creates a proxy object out of the specified root object and the specified interfaces
   * and implementations.
   *
   * @return a proxy object that is still of type T but additionally implements all specified
   * interfaces.
   * @throws ProxyException if the proxy could not be created.
   */
  public T build() throws ProxyException {

    if (this.root == null) {
      throw new IllegalArgumentException("root must not be null!");
    }

    if (this.interfacesToImplementations.isEmpty()) {
      // nothing to proxy
      return this.root;
    }

    try {
      ProxyMethodHandler methodHandler = new ProxyMethodHandler(root,
              interfacesToImplementations);
      ProxyFactory proxyFactory = new ProxyFactory();
      proxyFactory.setSuperclass(root.getClass());
      proxyFactory.setInterfaces(interfacesToImplementations.keySet().toArray(new Class[]{}));
      return (T) proxyFactory.create(new Class[0], new Object[0], methodHandler);
    } catch (Exception e) {
      throw new ProxyException(e);
    }
  }


}


Question: package org.wickedsource.docxstamper.processor;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.Comments;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelParseException;
import org.wickedsource.docxstamper.api.DocxStamperException;
import org.wickedsource.docxstamper.api.UnresolvedExpressionException;
import org.wickedsource.docxstamper.api.commentprocessor.ICommentProcessor;
import org.wickedsource.docxstamper.api.coordinates.ParagraphCoordinates;
import org.wickedsource.docxstamper.api.coordinates.RunCoordinates;
import org.wickedsource.docxstamper.el.ExpressionResolver;
import org.wickedsource.docxstamper.el.ExpressionUtil;
import org.wickedsource.docxstamper.proxy.ProxyBuilder;
import org.wickedsource.docxstamper.proxy.ProxyException;
import org.wickedsource.docxstamper.replace.PlaceholderReplacer;
import org.wickedsource.docxstamper.util.CommentUtil;
import org.wickedsource.docxstamper.util.CommentWrapper;
import org.wickedsource.docxstamper.util.ParagraphWrapper;
import org.wickedsource.docxstamper.util.walk.BaseCoordinatesWalker;
import org.wickedsource.docxstamper.util.walk.CoordinatesWalker;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;



/**
 * Allows registration of ICommentProcessor objects. Each registered
 * ICommentProcessor must implement an interface which has to be specified at
 * registration time. Provides several getter methods to access the registered
 * ICommentProcessors.
 */
public class CommentProcessorRegistry {

  private Logger logger = LoggerFactory.getLogger(CommentProcessorRegistry.class);

  private Map<ICommentProcessor, Class<?>> commentProcessorInterfaces = new HashMap<>();

  private List<ICommentProcessor> commentProcessors = new ArrayList<>();

  private ExpressionResolver expressionResolver = new ExpressionResolver();

  private ExpressionUtil expressionUtil = new ExpressionUtil();

  private PlaceholderReplacer placeholderReplacer;

  private boolean failOnInvalidExpression = true;

  public CommentProcessorRegistry(PlaceholderReplacer placeholderReplacer) {
    this.placeholderReplacer = placeholderReplacer;
  }

  public void setExpressionResolver(ExpressionResolver expressionResolver) {
    this.expressionResolver = expressionResolver;
  }

  public void registerCommentProcessor(Class<?> interfaceClass,
                                       ICommentProcessor commentProcessor) {
    this.commentProcessorInterfaces.put(commentProcessor, interfaceClass);
    this.commentProcessors.add(commentProcessor);
  }

  /**
   * Lets each registered ICommentProcessor have a run on the specified docx
   * document. At the end of the document the commit method is called for each
   * ICommentProcessor. The ICommentProcessors are run in the order they were
   * registered.
   *
   * @param document    the docx document over which to run the registered ICommentProcessors.
   * @param proxyBuilder a builder for a proxy around the context root object to customize its interface
   * @param <T>         type of the contextRoot object.
   */
  public <T> void runProcessors(final WordprocessingMLPackage document, final ProxyBuilder<T> proxyBuilder) {
Answer:     final Map<BigInteger, CommentWrapper> comments = CommentUtil.getComments(document);