RemoveMalformedComments.java

1
package pro.verron.officestamper.preset.preprocessors.malformedcomments;
2
3
import org.docx4j.TraversalUtil;
4
import org.docx4j.openpackaging.exceptions.Docx4JException;
5
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
6
import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart;
7
import org.docx4j.wml.*;
8
import org.jvnet.jaxb2_commons.ppp.Child;
9
import org.slf4j.Logger;
10
import org.slf4j.LoggerFactory;
11
import pro.verron.officestamper.api.OfficeStamperException;
12
import pro.verron.officestamper.api.PreProcessor;
13
import pro.verron.officestamper.utils.wml.WmlUtils;
14
15
import java.math.BigInteger;
16
import java.util.ArrayDeque;
17
import java.util.ArrayList;
18
import java.util.List;
19
import java.util.Optional;
20
21
import static java.util.Collections.emptyList;
22
import static java.util.stream.Collectors.toSet;
23
24
/// This pre-processor removes malformed comments from a WordprocessingMLPackage document.
25
///
26
/// Malformed comments are those that:
27
///
28
///     - Are opened but never closed (unbalanced comments)
29
///     - Are referenced in the document body but have no corresponding comment content
30
///
31
///
32
/// The processor traverses all comment-related elements in the document, validates their structure, and removes any
33
/// malformed comment references, range starts, and range ends.
34
///
35
/// @author Joseph Verron
36
public class RemoveMalformedComments
37
        implements PreProcessor {
38
    private static final Logger log = LoggerFactory.getLogger(RemoveMalformedComments.class);
39
40
    @Override
41
    public void process(WordprocessingMLPackage document) {
42
        var commentElements = WmlUtils.extractCommentElements(document);
43
44
        var commentIds = new ArrayList<BigInteger>(commentElements.size());
45
        var openedCommentsIds = new ArrayDeque<BigInteger>();
46
        for (Child commentElement : commentElements) {
47
            switch (commentElement) {
48
                case CommentRangeStart crs -> {
49
                    var lastOpenedCommentId = crs.getId();
50
                    assert lastOpenedCommentId != null;
51
                    log.trace("Comment {} opened.", lastOpenedCommentId);
52
                    commentIds.add(lastOpenedCommentId);
53
                    openedCommentsIds.add(lastOpenedCommentId);
54
                }
55
                case CommentRangeEnd cre -> {
56
                    var lastClosedCommentId = cre.getId();
57
                    assert lastClosedCommentId != null;
58
                    log.trace("Comment {} closed.", lastClosedCommentId);
59
                    commentIds.add(lastClosedCommentId);
60
61
                    var lastOpenedCommentId = openedCommentsIds.pollLast();
62 1 1. process : negated conditional → KILLED
                    if (!lastClosedCommentId.equals(lastOpenedCommentId)) {
63
                        log.debug("Comment {} is closing just after comment {} starts.",
64
                                lastClosedCommentId,
65
                                lastOpenedCommentId);
66
                        throw new OfficeStamperException("Cannot figure which comment contains the other !");
67
                    }
68
                }
69
                case R.CommentReference cr -> {
70
                    var commentId = cr.getId();
71
                    assert commentId != null;
72
                    log.trace("Comment {} referenced.", commentId);
73
                    commentIds.add(commentId);
74
                }
75
                default -> { /* Do Nothing */ }
76
            }
77
        }
78
79 1 1. process : negated conditional → SURVIVED
        if (!openedCommentsIds.isEmpty())
80
            log.debug("These comments have been opened, but never closed: {}", openedCommentsIds);
81
        var malformedCommentIds = new ArrayList<>(openedCommentsIds);
82
83
        var mainDocumentPart = document.getMainDocumentPart();
84
        var writtenCommentsId = Optional.ofNullable(mainDocumentPart.getCommentsPart())
85
                                        .map(RemoveMalformedComments::tryGetCommentsPart)
86
                                        .map(Comments::getComment)
87
                                        .orElse(emptyList())
88
                                        .stream()
89 2 1. lambda$process$0 : replaced boolean return with true for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::lambda$process$0 → SURVIVED
2. lambda$process$0 : negated conditional → KILLED
                                        .filter(c -> !isEmpty(c))
90
                                        .map(CTMarkup::getId)
91
                                        .collect(toSet());
92
93
        commentIds.removeAll(writtenCommentsId);
94
95
        if (!commentIds.isEmpty()) log.debug("Comments referenced in body, without related content: {}", commentIds);
96
        malformedCommentIds.addAll(commentIds);
97
98
        var crVisitor = new CommentReferenceRemoverVisitor(malformedCommentIds);
99
        var crsVisitor = new CommentRangeStartRemoverVisitor(malformedCommentIds);
100
        var creVisitor = new CommentRangeEndRemoverVisitor(malformedCommentIds);
101 1 1. process : removed call to org/docx4j/TraversalUtil::visit → SURVIVED
        TraversalUtil.visit(document, true, List.of(crVisitor, crsVisitor, creVisitor));
102 1 1. process : removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentReferenceRemoverVisitor::run → SURVIVED
        crVisitor.run();
103 1 1. process : removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentRangeStartRemoverVisitor::run → SURVIVED
        crsVisitor.run();
104 1 1. process : removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentRangeEndRemoverVisitor::run → SURVIVED
        creVisitor.run();
105
    }
106
107
    private static Comments tryGetCommentsPart(CommentsPart commentsPart) {
108
        try {
109 1 1. tryGetCommentsPart : replaced return value with null for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::tryGetCommentsPart → KILLED
            return commentsPart.getContents();
110
        } catch (Docx4JException e) {
111
            throw new OfficeStamperException(e);
112
        }
113
    }
114
115
    private static boolean isEmpty(Comments.Comment c) {
116
        var content = c.getContent();
117 3 1. isEmpty : negated conditional → KILLED
2. isEmpty : replaced boolean return with true for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::isEmpty → KILLED
3. isEmpty : negated conditional → KILLED
        return content == null || content.isEmpty();
118
    }
119
}

Mutations

62

1.1
Location : process
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:doesNotFail(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
negated conditional → KILLED

79

1.1
Location : process
Killed by : none
negated conditional → SURVIVED
Covering tests

89

1.1
Location : lambda$process$0
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
negated conditional → KILLED

2.2
Location : lambda$process$0
Killed by : none
replaced boolean return with true for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::lambda$process$0 → SURVIVED
Covering tests

101

1.1
Location : process
Killed by : none
removed call to org/docx4j/TraversalUtil::visit → SURVIVED
Covering tests

102

1.1
Location : process
Killed by : none
removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentReferenceRemoverVisitor::run → SURVIVED
Covering tests

103

1.1
Location : process
Killed by : none
removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentRangeStartRemoverVisitor::run → SURVIVED
Covering tests

104

1.1
Location : process
Killed by : none
removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentRangeEndRemoverVisitor::run → SURVIVED
Covering tests

109

1.1
Location : tryGetCommentsPart
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
replaced return value with null for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::tryGetCommentsPart → KILLED

117

1.1
Location : isEmpty
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
negated conditional → KILLED

2.2
Location : isEmpty
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
replaced boolean return with true for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::isEmpty → KILLED

3.3
Location : isEmpty
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
negated conditional → KILLED

Active mutators

Tests examined


Report generated by PIT 1.25.5 support