RepeatProcessor.java
package pro.verron.officestamper.preset.processors.repeat;
import org.docx4j.XmlUtils;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.wml.ContentAccessor;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.SectPr;
import org.jspecify.annotations.Nullable;
import org.jvnet.jaxb2_commons.ppp.Child;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.verron.officestamper.api.*;
import pro.verron.officestamper.preset.CommentProcessorFactory.IRepeatProcessor;
import pro.verron.officestamper.utils.wml.WmlFactory;
import pro.verron.officestamper.utils.wml.WmlUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toCollection;
public class RepeatProcessor
extends CommentProcessor
implements IRepeatProcessor {
private static final Logger log = LoggerFactory.getLogger(RepeatProcessor.class);
/// Constructs a new instance of CommentProcessor to process comments and placeholders within a paragraph.
///
/// @param context the context containing the paragraph, comment, and placeholder associated with the
/// processing of this CommentProcessor.
public RepeatProcessor(ProcessorContext context) {
super(context);
}
@Override
public void repeat(@Nullable Iterable<Object> items) {
if (items == null) return;
var comment = context().comment();
var elements = comment.getElements();
var contextHolder = context().contextHolder();
var parent = comment.getParent();
var siblings = parent.getContent();
var firstElement = elements.getFirst();
var previousSectionBreak = previousSectionBreak(firstElement, parent).orElse(documentSection(context().part()));
var index = siblings.indexOf(firstElement);
siblings.removeAll(elements);
var iterator = items.iterator();
// Iterates items; copies elements; conditionally adds section break; adds elements
while (iterator.hasNext()) {
var item = iterator.next();
var copiedElements = elements.stream()
.map(XmlUtils::deepCopy)
.collect(toCollection(ArrayList::new));
WmlUtils.deleteCommentFromElements(comment.getId(), copiedElements);
// Adds section break to last paragraph if needed
if (iterator.hasNext() && containsSectionBreaks(copiedElements)) {
var lastParagraph = lastParagraph(copiedElements).orElseGet(newEndParagraph(copiedElements));
if (!hasSectionBreak(lastParagraph)) {
addSectionBreak(previousSectionBreak, lastParagraph);
}
}
siblings.addAll(index, copiedElements);
index += copiedElements.size();
copiedElements.forEach(element -> {if (element instanceof Child child) child.setParent(parent);});
var subContextKey = contextHolder.addBranch(item);
Hooks.ofHooks(() -> copiedElements)
.forEachRemaining(hook -> hook.setContextKey(subContextKey));
}
}
private static Optional<SectPr> previousSectionBreak(Object firstObject, ContentAccessor parent) {
List<Object> parentContent = parent.getContent();
int pIndex = parentContent.indexOf(firstObject);
int i = pIndex - 1;
while (i >= 0) {
if (parentContent.get(i) instanceof P prevParagraph) {
// the first P preceding the object is the one carrying a section break
return ofNullable(prevParagraph.getPPr()).map(PPr::getSectPr);
}
else log.debug("The previous sibling was not a P, continuing search");
i--;
}
log.info("No previous section break found from : {}, first object index={}", parent, pIndex);
return Optional.empty();
}
private static SectPr documentSection(DocxPart part) {
try {
return part.document()
.getMainDocumentPart()
.getContents()
.getBody()
.getSectPr();
} catch (Docx4JException e) {
throw new OfficeStamperException(e);
}
}
private static boolean containsSectionBreaks(ArrayList<Object> elements) {
return elements.stream()
.filter(P.class::isInstance)
.map(P.class::cast)
.map(P::getPPr)
.filter(Objects::nonNull)
.map(PPr::getSectPr)
.anyMatch(Objects::nonNull);
}
private static Optional<P> lastParagraph(List<Object> elements) {
if (elements.getLast() instanceof P paragraph) return Optional.of(paragraph);
else return Optional.empty();
}
private static Supplier<P> newEndParagraph(ArrayList<Object> copiedElements) {
return () -> {
var p = WmlFactory.newParagraph();
copiedElements.addLast(p);
return p;
};
}
private static boolean hasSectionBreak(P lastParagraph) {
PPr pPr = lastParagraph.getPPr();
if (pPr == null) return false;
SectPr sectPr = pPr.getSectPr();
return sectPr != null;
}
private static void addSectionBreak(SectPr sectPr, P paragraph) {
PPr nextPPr = ofNullable(paragraph.getPPr()).orElseGet(WmlFactory::newPPr);
nextPPr.setSectPr(XmlUtils.deepCopy(sectPr));
paragraph.setPPr(nextPPr);
}
}