StandardParagraph.java
package pro.verron.officestamper.core;
import org.docx4j.wml.*;
import org.jvnet.jaxb2_commons.ppp.Child;
import pro.verron.officestamper.api.*;
import pro.verron.officestamper.utils.wml.DocxIterator;
import pro.verron.officestamper.utils.wml.WmlUtils;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static java.util.stream.Collectors.joining;
import static pro.verron.officestamper.api.OfficeStamperException.throwing;
import static pro.verron.officestamper.utils.wml.WmlUtils.getFirstParentWithClass;
import static pro.verron.officestamper.utils.wml.WmlUtils.isTagElement;
/// Represents a wrapper for managing and manipulating DOCX paragraph elements. This class provides methods to
/// manipulate the underlying paragraph content, process placeholders, and interact with runs within the paragraph.
public class StandardParagraph
implements Paragraph {
private final DocxPart part;
private final ContentAccessor contents;
private final ArrayListWml<Object> p;
/// Constructs a new instance of the StandardParagraph class.
///
/// @param part the source DocxPart that contains the paragraph content.
/// @param paragraphContent the list of objects representing the paragraph content.
/// @param p the P object representing the paragraph's structure.
private StandardParagraph(DocxPart part, ContentAccessor paragraphContent, ArrayListWml<Object> p) {
this.part = part;
this.contents = paragraphContent;
this.p = p;
}
/// Creates a new instance of `StandardParagraph` from the provided [DocxPart] and parent object.
///
/// @param part the source DocxPart.
/// @param parent the parent object.
///
/// @return a new StandardParagraph instance.
public static StandardParagraph from(DocxPart part, Object parent) {
return switch (parent) {
case P p -> from(part, p);
case CTSdtContentRun contentRun -> from(part, contentRun);
case CTSmartTagRun smartTagRun when isTagElement(smartTagRun, "officestamper") ->
from(part, smartTagRun.getParent());
default -> throw new OfficeStamperException("Unsupported parent type: " + parent.getClass());
};
}
/// Creates a new instance of StandardParagraph using the provided DocxPart and P objects.
///
/// @param source the source DocxPart containing the paragraph.
/// @param paragraph the P object representing the structure and content of the paragraph.
///
/// @return a new instance of StandardParagraph constructed based on the provided source and paragraph.
public static StandardParagraph from(DocxPart source, P paragraph) {
return new StandardParagraph(source, paragraph, (ArrayListWml<Object>) paragraph.getContent());
}
/// Creates a new instance of StandardParagraph from the provided DocxPart and CTSdtContentRun objects.
///
/// @param source the source DocxPart containing the paragraph content.
/// @param paragraph the CTSdtContentRun object representing the content of the paragraph.
///
/// @return a new instance of StandardParagraph constructed based on the provided DocxPart and paragraph.
public static StandardParagraph from(DocxPart source, CTSdtContentRun paragraph) {
var parent = (SdtRun) paragraph.getParent();
var parentParent = (P) parent.getParent();
return new StandardParagraph(source, paragraph, (ArrayListWml<Object>) parentParent.getContent());
}
/// Replaces a set of paragraph elements with new ones within the current paragraph's siblings. Ensures that the
/// elements to be removed are replaced in the appropriate position.
///
/// @param toRemove the list of paragraph elements to be removed.
/// @param toAdd the list of paragraph elements to be added.
///
/// @throws OfficeStamperException if the current paragraph object is not found in its siblings.
@Override
public void replace(List<P> toRemove, List<P> toAdd) {
var siblings = siblings();
int index = siblings.indexOf(p.getParent());
if (index < 0) throw new OfficeStamperException("Impossible");
siblings.addAll(index, toAdd);
siblings.removeAll(toRemove);
}
private List<Object> siblings() {
return this.parent(ContentAccessor.class, 1)
.orElseThrow(throwing("This paragraph direct parent is not a classic parent object"))
.getContent();
}
private <T> Optional<T> parent(Class<T> aClass, int depth) {
return getFirstParentWithClass((Child) p.getParent(), aClass, depth);
}
/// Removes the paragraph represented by the current instance. Delegates the removal process to a utility method
/// that handles the underlying P object.
@Override
public void remove() {
WmlUtils.remove((Child) p.getParent());
}
@Override
public void replace(String expression, Insert insert) {
var newContents = WmlUtils.replaceExpressionWithRun(() -> p, expression, insert.elements(), insert::setRPr);
var content = contents.getContent();
content.clear();
content.addAll(newContents);
}
@Override
public void replace(Object start, Object end, Insert insert) {
var content = contents.getContent();
var fromIndex = content.indexOf(start);
var toIndex = content.indexOf(end);
if (fromIndex < 0) {
var msg = "The start element (%s) is not in the paragraph (%s)";
throw new OfficeStamperException(msg.formatted(start, this));
}
if (toIndex < 0) {
var msg = "The end element (%s) is not in the paragraph (%s)";
throw new OfficeStamperException(msg.formatted(end, this));
}
if (fromIndex > toIndex) {
var msg = "The start element (%s) is after the end element (%s)";
throw new OfficeStamperException(msg.formatted(end, this));
}
var expression = extractExpression(start, end);
var newContents = WmlUtils.replaceExpressionWithRun(() -> p, expression, insert.elements(), insert::setRPr);
content.clear();
content.addAll(newContents);
}
private String extractExpression(Object from, Object to) {
var content = contents.getContent();
var fromIndex = content.indexOf(from);
var toIndex = content.indexOf(to);
var subContent = content.subList(fromIndex, toIndex + 1);
ContentAccessor contentAccessor = () -> subContent;
return new DocxIterator(contentAccessor).selectClass(R.class)
.map(WmlUtils::asString)
.collect(joining());
}
/// Returns the aggregated text over all runs.
///
/// @return the text of all runs.
@Override
public String asString() {
return WmlUtils.asString(contents);
}
/// Applies the given consumer to the paragraph represented by the current instance. This method facilitates custom
/// processing by allowing the client to define specific operations to be performed on the paragraph's internal
/// structure.
///
/// @param pConsumer the consumer function to apply to the paragraph's structure.
@Override
public void apply(Consumer<ContentAccessor> pConsumer) {
pConsumer.accept(() -> p);
}
/// Retrieves the nearest parent of the specified type for the current paragraph. The search is performed starting
/// from the current paragraph and traversing up to the root, with a default maximum depth of Integer.MAX_VALUE.
///
/// @param aClass the class type of the parent to search for
/// @param <T> the generic type of the parent
///
/// @return an Optional containing the parent of the specified type if found, or an empty Optional if no parent of
/// the given type exists
@Override
public <T> Optional<T> parent(Class<T> aClass) {
return parent(aClass, Integer.MAX_VALUE);
}
/// Retrieves the collection of comments associated with the current paragraph.
///
/// @return a collection of [Comments.Comment] objects related to the paragraph.
@Override
public Collection<Comments.Comment> getComment() {
return CommentUtil.getCommentFor(() -> p, part.document());
}
@Override
public Optional<Table.Row> parentTableRow() {
return parent(Tr.class).map((Tr tr) -> new StandardRow(part, (Tbl) tr.getParent(), tr));
}
@Override
public Optional<Table> parentTable() {
return parent(Tbl.class).map(StandardTable::new);
}
/// Returns the string representation of the paragraph. This method delegates to the `asString` method to aggregate
/// the text content of all runs.
///
/// @return a string containing the combined text content of the paragraph's runs.
@Override
public String toString() {
return asString();
}
}