WmlFactory.java

package pro.verron.officestamper.utils;

import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.exceptions.InvalidFormatException;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart;
import org.docx4j.wml.*;
import org.springframework.lang.Nullable;
import pro.verron.officestamper.api.OfficeStamperException;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

/**
 * WmlFactory is a utility class providing methods to create and manipulate WordML objects.
 * It includes methods for creating paragraphs, runs, text elements, comments, bodies and drawings.
 * This factory encapsulates the complexity of WordML elements and simplifies the process of working with them.
 */
public class WmlFactory {
    private static final Random RANDOM = new Random();

    private WmlFactory() {
        throw new OfficeStamperException("Utility class");
    }

    /**
     * Creates a new paragraph containing a single drawing.
     *
     * @param drawing The Drawing object to be included in the new paragraph.
     *
     * @return A new paragraph encapsulating the provided drawing.
     */
    public static P newParagraph(Drawing drawing) {
        return newParagraph(List.of(newRun(drawing)));
    }

    /**
     * Creates a new paragraph containing the provided values.
     *
     * @param values A list of objects to be added to the new paragraph.
     *
     * @return A new paragraph containing the provided values.
     */
    public static P newParagraph(List<?> values) {
        var paragraph = new P();
        var paragraphContent = paragraph.getContent();
        paragraphContent.addAll(values);
        return paragraph;
    }

    /**
     * Creates a new run containing a single drawing.
     *
     * @param value The Drawing object to be included in the new run.
     *
     * @return A new run encapsulating the provided drawing.
     */
    public static R newRun(Drawing value) {
        return newRun(List.of(value));
    }

    private static R newRun(List<Object> values) {
        var run = new R();
        var runContent = run.getContent();
        runContent.addAll(values);
        return run;
    }

    /**
     * Creates a new comment with the provided value.
     *
     * @param value The string value to be included in the comment.
     *
     * @return A new Comments.Comment object containing the provided value.
     */
    public static Comments.Comment newComment(BigInteger id, String value) {
        var comment = new Comments.Comment();
        comment.setId(id);
        var commentContent = comment.getContent();
        commentContent.add(newParagraph(value));
        return comment;
    }

    /**
     * Creates a new paragraph containing the provided string value.
     *
     * @param value The string value to be added to the new paragraph.
     *
     * @return A new paragraph containing the provided string value.
     */
    public static P newParagraph(String value) {
        return newParagraph(newRun(value));
    }

    /**
     * Creates a new paragraph containing the provided run.
     *
     * @param run The R object (run) to be included in the new paragraph.
     *
     * @return A new paragraph containing the provided run.
     */
    public static P newParagraph(R run) {
        return newParagraph(List.of(run));
    }

    /**
     * Creates a new run containing the provided string value.
     *
     * @param value The string value to be included in the new run.
     *
     * @return A new run containing the provided string value.
     */
    public static R newRun(String value) {
        return newRun(newText(value));
    }

    /**
     * Creates a new run containing a single text object.
     *
     * @param value The Text object to be included in the new run.
     *
     * @return A new run encapsulating the provided text object.
     */
    public static R newRun(Text value) {
        return newRun(List.of(value));
    }

    /**
     * Creates a new Text object with the specified value, preserving spaces.
     *
     * @param value The string value to be set in the new Text object.
     *
     * @return A new Text object containing the provided value with space preserved.
     */
    public static Text newText(String value) {
        var text = new Text();
        text.setValue(value);
        text.setSpace("preserve");
        return text;
    }

    /**
     * Creates a new Body object containing the provided elements.
     *
     * @param elements A list of objects to be added to the new Body.
     *
     * @return A new Body containing the provided elements.
     */
    public static Body newBody(List<Object> elements) {
        Body body = new Body();
        var bodyContent = body.getContent();
        bodyContent.addAll(elements);
        return body;
    }

    /**
     * Creates a new paragraph containing the provided text values.
     *
     * @param texts The array of string values to be included in the new paragraph.
     *
     * @return A new paragraph containing the provided text values.
     */
    public static P newParagraph(String... texts) {
        return newParagraph(Arrays.stream(texts)
                                  .map(WmlFactory::newRun)
                                  .toList());
    }

    /**
     * Creates a new PPr (paragraph properties) object.
     *
     * @return A new PPr object.
     */
    public static PPr newPPr() {
        return new PPr();
    }

    /**
     * Creates a new Comments object and populates it with a list of Comment objects.
     *
     * @param list A list of Comments.Comment objects to be added to the new Comments object.
     *
     * @return A new Comments object containing the provided Comment objects.
     */
    public static Comments newComments(List<Comments.Comment> list) {
        Comments comments = new Comments();
        List<Comments.Comment> commentList = comments.getComment();
        commentList.addAll(list);
        return comments;
    }

    /**
     * Creates a new CommentsPart object.
     * This method attempts to create a new instance of CommentsPart.
     * If an InvalidFormatException occurs during the creation process, it wraps the exception in an
     * OfficeStamperException and throws it.
     *
     * @return A new instance of CommentsPart.
     */
    public static CommentsPart newCommentsPart() {
        try {
            return new CommentsPart();
        } catch (InvalidFormatException e) {
            throw new OfficeStamperException(e);
        }
    }

    /**
     * Creates a new run containing an image with the specified attributes.
     *
     * @param maxWidth      the maximum width of the image, it can be null
     * @param abstractImage the binary part abstract image to be included in the run
     * @param filenameHint  the filename hint for the image
     * @param altText       the alternative text for the image
     *
     * @return a new run element containing the image
     */
    public static R newRun(
            @Nullable Integer maxWidth, BinaryPartAbstractImage abstractImage, String filenameHint, String altText
    ) {
        var inline = newInline(abstractImage, filenameHint, altText, maxWidth);
        return newRun(newDrawing(inline));
    }

    /**
     * Creates a new Inline object for the given image part, filename hint, and alt text.
     *
     * @param imagePart    The binary part abstract image to be used.
     * @param filenameHint A hint for the filename of the image.
     * @param altText      Alternative text for the image.
     *
     * @return A new Inline object containing the specified image information.
     *
     * @throws OfficeStamperException If there is an error creating the image inline.
     */
    public static Inline newInline(
            BinaryPartAbstractImage imagePart, String filenameHint, String altText, @Nullable Integer maxWidth
    ) {
        // creating random ids assuming they are unique,
        // id must not be too large
        // otherwise Word cannot open the document
        var id1 = RANDOM.nextLong(100_000L);
        var id2 = RANDOM.nextInt(100_000);
        try {
            return maxWidth == null
                    ? imagePart.createImageInline(filenameHint, altText, id1, id2, false)
                    : imagePart.createImageInline(filenameHint, altText, id1, id2, false, maxWidth);
        } catch (Exception e) {
            throw new OfficeStamperException(e);
        }
    }

    /**
     * Creates a new Drawing object containing the provided Inline object.
     *
     * @param inline The Inline object to be contained within the new Drawing.
     *
     * @return A new Drawing object encapsulating the provided inline object.
     */
    public static Drawing newDrawing(Inline inline) {
        var drawing = new Drawing();
        var anchorOrInline = drawing.getAnchorOrInline();
        anchorOrInline.add(inline);
        return drawing;
    }

    public static CommentRangeStart newCommentRangeStart(BigInteger id, P parent) {
        var commentRangeStart = new CommentRangeStart();
        commentRangeStart.setId(id);
        commentRangeStart.setParent(parent);
        return commentRangeStart;
    }

    public static CommentRangeEnd newCommentRangeEnd(BigInteger id, P parent) {
        var commentRangeEnd = new CommentRangeEnd();
        commentRangeEnd.setId(id);
        commentRangeEnd.setParent(parent);
        return commentRangeEnd;
    }

    public static R.CommentReference newCommentReference(BigInteger id, P parent) {
        var commentReference = new R.CommentReference();
        commentReference.setId(id);
        commentReference.setParent(parent);
        return commentReference;
    }
}