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
/// @version ${version}
37
public class RemoveMalformedComments
38
        implements PreProcessor {
39
    private static final Logger log = LoggerFactory.getLogger(RemoveMalformedComments.class);
40
41
    @Override
42
    public void process(WordprocessingMLPackage document) {
43
        var commentElements = WmlUtils.extractCommentElements(document);
44
45
        var commentIds = new ArrayList<BigInteger>(commentElements.size());
46
        var openedCommentsIds = new ArrayDeque<BigInteger>();
47
        for (Child commentElement : commentElements) {
48
            switch (commentElement) {
49
                case CommentRangeStart crs -> {
50
                    var lastOpenedCommentId = crs.getId();
51
                    assert lastOpenedCommentId != null;
52
                    log.trace("Comment {} opened.", lastOpenedCommentId);
53
                    commentIds.add(lastOpenedCommentId);
54
                    openedCommentsIds.add(lastOpenedCommentId);
55
                }
56
                case CommentRangeEnd cre -> {
57
                    var lastClosedCommentId = cre.getId();
58
                    assert lastClosedCommentId != null;
59
                    log.trace("Comment {} closed.", lastClosedCommentId);
60
                    commentIds.add(lastClosedCommentId);
61
62
                    var lastOpenedCommentId = openedCommentsIds.pollLast();
63 1 1. process : negated conditional → KILLED
                    if (!lastClosedCommentId.equals(lastOpenedCommentId)) {
64
                        log.debug("Comment {} is closing just after comment {} starts.",
65
                                lastClosedCommentId,
66
                                lastOpenedCommentId);
67
                        throw new OfficeStamperException("Cannot figure which comment contains the other !");
68
                    }
69
                }
70
                case R.CommentReference cr -> {
71
                    var commentId = cr.getId();
72
                    assert commentId != null;
73
                    log.trace("Comment {} referenced.", commentId);
74
                    commentIds.add(commentId);
75
                }
76
                default -> { /* Do Nothing */ }
77
            }
78
        }
79
80 1 1. process : negated conditional → SURVIVED
        if (!openedCommentsIds.isEmpty())
81
            log.debug("These comments have been opened, but never closed: {}", openedCommentsIds);
82
        var malformedCommentIds = new ArrayList<>(openedCommentsIds);
83
84
        var mainDocumentPart = document.getMainDocumentPart();
85
        var writtenCommentsId = Optional.ofNullable(mainDocumentPart.getCommentsPart())
86
                                        .map(RemoveMalformedComments::tryGetCommentsPart)
87
                                        .map(Comments::getComment)
88
                                        .orElse(emptyList())
89
                                        .stream()
90 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))
91
                                        .map(CTMarkup::getId)
92
                                        .collect(toSet());
93
94
        commentIds.removeAll(writtenCommentsId);
95
96
        if (!commentIds.isEmpty()) log.debug("Comments referenced in body, without related content: {}", commentIds);
97
        malformedCommentIds.addAll(commentIds);
98
99
        var crVisitor = new CommentReferenceRemoverVisitor(malformedCommentIds);
100
        var crsVisitor = new CommentRangeStartRemoverVisitor(malformedCommentIds);
101
        var creVisitor = new CommentRangeEndRemoverVisitor(malformedCommentIds);
102 1 1. process : removed call to org/docx4j/TraversalUtil::visit → SURVIVED
        TraversalUtil.visit(document, true, List.of(crVisitor, crsVisitor, creVisitor));
103 1 1. process : removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentReferenceRemoverVisitor::run → SURVIVED
        crVisitor.run();
104 1 1. process : removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentRangeStartRemoverVisitor::run → SURVIVED
        crsVisitor.run();
105 1 1. process : removed call to pro/verron/officestamper/preset/preprocessors/malformedcomments/CommentRangeEndRemoverVisitor::run → SURVIVED
        creVisitor.run();
106
    }
107
108
    private static Comments tryGetCommentsPart(CommentsPart commentsPart) {
109
        try {
110 1 1. tryGetCommentsPart : replaced return value with null for pro/verron/officestamper/preset/preprocessors/malformedcomments/RemoveMalformedComments::tryGetCommentsPart → KILLED
            return commentsPart.getContents();
111
        } catch (Docx4JException e) {
112
            throw new OfficeStamperException(e);
113
        }
114
    }
115
116
    private static boolean isEmpty(Comments.Comment c) {
117
        var content = c.getContent();
118 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();
119
    }
120
}

Mutations

63

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:#1]
negated conditional → KILLED

80

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

90

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

102

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

103

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

104

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

105

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

110

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

118

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.23.1 support