StandardComment.java

package pro.verron.officestamper.core;

import org.docx4j.TextUtils;
import org.docx4j.wml.*;
import org.docx4j.wml.R.CommentReference;
import org.jspecify.annotations.Nullable;
import pro.verron.officestamper.api.Comment;
import pro.verron.officestamper.api.DocxPart;
import pro.verron.officestamper.api.Paragraph;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import static java.util.stream.Collectors.joining;
import static pro.verron.officestamper.utils.wml.WmlFactory.*;

/// Standard implementation of the [Comment] interface. Represents a comment in a DOCX document with its associated
/// range markers and content.
///
/// @author Joseph Verron
/// @author Tom Hombergs
/// @version ${version}
/// @since 1.0.2
public class StandardComment
        implements Comment {
    private final DocxPart part;
    private final Comments.Comment comment;
    private final CommentRangeStart commentRangeStart;
    private final CommentRangeEnd commentRangeEnd;
    private final @Nullable CommentReference commentReference;
    private final CTSmartTagRun startTagRun;

    /// Constructs a new `StandardComment` object.
    ///
    /// @param part              the [DocxPart] representing the document section this comment belongs to.
    /// @param startTagRun       the start tag run.
    /// @param commentRangeStart the comment range start.
    /// @param commentRangeEnd   the comment range end.
    /// @param comment           the comment.
    /// @param commentReference the comment reference.
    public StandardComment(
            DocxPart part,
            CTSmartTagRun startTagRun,
            CommentRangeStart commentRangeStart,
            CommentRangeEnd commentRangeEnd,
            Comments.Comment comment,
            @Nullable CommentReference commentReference
    ) {
        this.part = part;
        this.startTagRun = startTagRun;
        this.commentRangeStart = commentRangeStart;
        this.commentRangeEnd = commentRangeEnd;
        this.comment = comment;
        this.commentReference = commentReference;
    }

    /// Creates a new instance of [StandardComment] and initializes it with the given parameters, including a comment,
    /// comment range start, comment range end, and a comment reference.
    ///
    /// @param document the [DocxPart] representing the document to which this comment belongs
    /// @param parent the [ContentAccessor] representing the parent content of the comment range
    /// @param expression the [String] content to be included in the comment
    /// @param id the unique [BigInteger] identifier for the comment
    ///
    /// @return a [StandardComment] instance initialized with the specified parameters
    public static StandardComment create(DocxPart document, ContentAccessor parent, String expression, BigInteger id) {
        var start = newCommentRangeStart(id, parent);
        return new StandardComment(document,
                newSmartTag("officestamper", newCtAttr("type", "processor"), start),
                start,
                newCommentRangeEnd(id, parent),
                newComment(id, expression),
                newCommentReference(id, parent));
    }

    /// Generates a string representation of the [StandardComment] object, including its ID, content, and the amount
    /// children comment.
    ///
    /// @return a formatted string describing the [StandardComment]'s properties, including its ID, content, and the
    ///         size of its children.
    @Override
    public String toString() {
        return "StandardComment{comment={id=%s, content=%s}}}".formatted(comment.getId(),
                comment.getContent()
                       .stream()
                       .map(TextUtils::getText)
                       .collect(joining(",")));
    }

    @Override
    public Paragraph getParagraph() {
        var parent = commentRangeStart.getParent();
        return StandardParagraph.from(part, parent);
    }

    @Override
    public CTSmartTagRun getStartTagRun() {
        return startTagRun;
    }

    @Override
    public CommentRangeStart getCommentRangeStart() {
        return commentRangeStart;
    }

    @Override
    public ContentAccessor getParent() {
        return DocumentUtil.findSmallestCommonParent(commentRangeStart, commentRangeEnd);
    }

    @Override
    public List<Object> getElements() {
        List<Object> elements = new ArrayList<>();
        boolean startFound = false;
        boolean endFound = false;
        var siblings = getParent().getContent();
        for (Object element : siblings) {
            startFound = startFound || DocumentUtil.depthElementSearch(commentRangeStart, element);
            if (startFound && !endFound) elements.add(element);
            endFound = endFound || DocumentUtil.depthElementSearch(commentRangeEnd, element);
        }
        return elements;
    }

    @Override
    public CommentRangeEnd getCommentRangeEnd() {
        return commentRangeEnd;
    }

    @Override
    public @Nullable CommentReference getCommentReference() {
        return commentReference;
    }

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

    @Override
    public String expression() {
        return this.getComment()
                   .getContent()
                   .stream()
                   .filter(P.class::isInstance)
                   .map(P.class::cast)
                   .map(p -> StandardParagraph.from(new TextualDocxPart(part.document()), p))
                   .map(StandardParagraph::asString)
                   .collect(joining());
    }

    @Override
    public BigInteger getId() {
        return comment.getId();
    }
}