1 | package pro.verron.officestamper.core; | |
2 | ||
3 | import org.docx4j.XmlUtils; | |
4 | import org.docx4j.openpackaging.exceptions.Docx4JException; | |
5 | import org.docx4j.openpackaging.exceptions.InvalidFormatException; | |
6 | import org.docx4j.openpackaging.packages.WordprocessingMLPackage; | |
7 | import org.docx4j.openpackaging.parts.PartName; | |
8 | import org.docx4j.openpackaging.parts.Parts; | |
9 | import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart; | |
10 | import org.docx4j.wml.*; | |
11 | import pro.verron.officestamper.api.Comment; | |
12 | import pro.verron.officestamper.api.OfficeStamperException; | |
13 | ||
14 | import java.math.BigInteger; | |
15 | import java.util.*; | |
16 | import java.util.stream.Collectors; | |
17 | ||
18 | import static org.docx4j.XmlUtils.unwrap; | |
19 | import static pro.verron.officestamper.utils.WmlFactory.newBody; | |
20 | import static pro.verron.officestamper.utils.WmlFactory.newComments; | |
21 | ||
22 | /// Utility class for working with comments in a DOCX document. | |
23 | /// | |
24 | /// @author Joseph Verron | |
25 | /// @author Tom Hombergs | |
26 | /// @version ${version} | |
27 | /// @since 1.0.0 | |
28 | public class CommentUtil { | |
29 | private static final PartName WORD_COMMENTS_PART_NAME; | |
30 | ||
31 | static { | |
32 | try { | |
33 | WORD_COMMENTS_PART_NAME = new PartName("/word/comments.xml"); | |
34 | } catch (InvalidFormatException e) { | |
35 | throw new OfficeStamperException(e); | |
36 | } | |
37 | } | |
38 | ||
39 | private CommentUtil() { | |
40 | throw new OfficeStamperException("Utility class shouldn't be instantiated"); | |
41 | } | |
42 | ||
43 | /// Retrieves the comment associated with or around the specified `R` run within a WordprocessingMLPackage document. | |
44 | /// | |
45 | /// @param run the run to search for an associated comment | |
46 | /// @param document the WordprocessingMLPackage document containing the run and its possible comments | |
47 | /// | |
48 | /// @return an Optional containing the found comment, or Optional.empty() if no comment is associated | |
49 | public static Optional<Comments.Comment> getCommentAround(R run, WordprocessingMLPackage document) { | |
50 | ContentAccessor parent = (ContentAccessor) run.getParent(); | |
51 |
1
1. getCommentAround : negated conditional → SURVIVED |
if (parent == null) return Optional.empty(); |
52 |
1
1. getCommentAround : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::getCommentAround → SURVIVED |
return getComment(run, document, parent); |
53 | } | |
54 | ||
55 | private static Optional<Comments.Comment> getComment( | |
56 | R run, | |
57 | WordprocessingMLPackage document, | |
58 | ContentAccessor parent | |
59 | ) { | |
60 | CommentRangeStart possibleComment = null; | |
61 | boolean foundChild = false; | |
62 | for (Object contentElement : parent.getContent()) { | |
63 | // so first we look for the start of the comment | |
64 |
1
1. getComment : negated conditional → KILLED |
if (unwrap(contentElement) instanceof CommentRangeStart crs) possibleComment = crs; |
65 | // then we check if the child we are looking for is ours | |
66 |
2
1. getComment : negated conditional → SURVIVED 2. getComment : negated conditional → SURVIVED |
else if (possibleComment != null && run.equals(contentElement)) foundChild = true; |
67 | // and then, if we have an end of a comment, we are good! | |
68 |
3
1. getComment : negated conditional → SURVIVED 2. getComment : negated conditional → SURVIVED 3. getComment : negated conditional → SURVIVED |
else if (possibleComment != null && foundChild && unwrap(contentElement) instanceof CommentRangeEnd) { |
69 |
1
1. getComment : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::getComment → SURVIVED |
return findComment(document, possibleComment.getId()); |
70 | } | |
71 | // else restart | |
72 | else { | |
73 | possibleComment = null; | |
74 | foundChild = false; | |
75 | } | |
76 | } | |
77 | return Optional.empty(); | |
78 | } | |
79 | ||
80 | private static Optional<Comments.Comment> findComment(WordprocessingMLPackage document, BigInteger id) { | |
81 |
1
1. findComment : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::findComment → SURVIVED |
return getCommentsPart(document.getParts()).map(CommentUtil::extractContent) |
82 | .map(Comments::getComment) | |
83 | .stream() | |
84 | .flatMap(Collection::stream) | |
85 |
2
1. lambda$findComment$0 : replaced boolean return with false for pro/verron/officestamper/core/CommentUtil::lambda$findComment$0 → SURVIVED 2. lambda$findComment$0 : replaced boolean return with true for pro/verron/officestamper/core/CommentUtil::lambda$findComment$0 → KILLED |
.filter(comment -> id.equals(comment.getId())) |
86 | .findFirst(); | |
87 | ||
88 | } | |
89 | ||
90 | /// Retrieves the CommentsPart from the given Parts object. | |
91 | /// | |
92 | /// @param parts the Parts object containing the various parts of the document. | |
93 | /// | |
94 | /// @return an Optional containing the CommentsPart if found, or an empty Optional if not found. | |
95 | public static Optional<CommentsPart> getCommentsPart(Parts parts) { | |
96 |
1
1. getCommentsPart : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::getCommentsPart → KILLED |
return Optional.ofNullable((CommentsPart) parts.get(WORD_COMMENTS_PART_NAME)); |
97 | } | |
98 | ||
99 | /// Extracts the contents of a given [CommentsPart]. | |
100 | /// | |
101 | /// @param commentsPart the [CommentsPart] from which content will be extracted | |
102 | /// | |
103 | /// @return the [Comments] instance containing the content of the provided comments part | |
104 | /// | |
105 | /// @throws OfficeStamperException if an error occurs while retrieving the content | |
106 | public static Comments extractContent(CommentsPart commentsPart) { | |
107 | try { | |
108 |
1
1. extractContent : replaced return value with null for pro/verron/officestamper/core/CommentUtil::extractContent → KILLED |
return commentsPart.getContents(); |
109 | } catch (Docx4JException e) { | |
110 | throw new OfficeStamperException("Error while searching comment.", e); | |
111 | } | |
112 | } | |
113 | ||
114 | /// Retrieves the comment associated with a given paragraph content within a WordprocessingMLPackage document. | |
115 | /// | |
116 | /// @param paragraphContent the content of the paragraph to search for a comment. | |
117 | /// @param document the WordprocessingMLPackage document containing the paragraph and its comments. | |
118 | /// | |
119 | /// @return an Optional containing the found comment, or Optional.empty() if no comment is associated with the given | |
120 | /// paragraph content. | |
121 | public static Collection<Comments.Comment> getCommentFor( | |
122 | List<Object> paragraphContent, | |
123 | WordprocessingMLPackage document | |
124 | ) { | |
125 | var comments = getCommentsPart(document.getParts()).map(CommentUtil::extractContent) | |
126 | .map(Comments::getComment) | |
127 | .stream() | |
128 | .flatMap(Collection::stream) | |
129 | .toList(); | |
130 | ||
131 |
1
1. getCommentFor : replaced return value with Collections.emptyList for pro/verron/officestamper/core/CommentUtil::getCommentFor → KILLED |
return paragraphContent.stream() |
132 | .filter(CommentRangeStart.class::isInstance) | |
133 | .map(CommentRangeStart.class::cast) | |
134 | .map(CommentRangeStart::getId) | |
135 |
1
1. lambda$getCommentFor$0 : replaced return value with Stream.empty for pro/verron/officestamper/core/CommentUtil::lambda$getCommentFor$0 → KILLED |
.flatMap(commentId -> findCommentById(comments, commentId).stream()) |
136 | .toList(); | |
137 | } | |
138 | ||
139 | private static Optional<Comments.Comment> findCommentById(List<Comments.Comment> comments, BigInteger id) { | |
140 | for (Comments.Comment comment : comments) { | |
141 |
1
1. findCommentById : negated conditional → KILLED |
if (id.equals(comment.getId())) { |
142 |
1
1. findCommentById : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::findCommentById → KILLED |
return Optional.of(comment); |
143 | } | |
144 | } | |
145 | return Optional.empty(); | |
146 | } | |
147 | ||
148 | /// Returns the string value of the specified comment object. | |
149 | /// | |
150 | /// @param comment a [Comment] object | |
151 | public static void deleteComment(Comment comment) { | |
152 | CommentRangeEnd end = comment.getCommentRangeEnd(); | |
153 |
1
1. deleteComment : negated conditional → KILLED |
if (end != null) { |
154 | ContentAccessor endParent = (ContentAccessor) end.getParent(); | |
155 | endParent.getContent() | |
156 | .remove(end); | |
157 | } | |
158 | CommentRangeStart start = comment.getCommentRangeStart(); | |
159 |
1
1. deleteComment : negated conditional → KILLED |
if (start != null) { |
160 | ContentAccessor startParent = (ContentAccessor) start.getParent(); | |
161 | startParent.getContent() | |
162 | .remove(start); | |
163 | } | |
164 | R.CommentReference reference = comment.getCommentReference(); | |
165 |
1
1. deleteComment : negated conditional → KILLED |
if (reference != null) { |
166 | ContentAccessor referenceParent = (ContentAccessor) reference.getParent(); | |
167 | referenceParent.getContent() | |
168 | .remove(reference); | |
169 | } | |
170 | } | |
171 | ||
172 | private static List<DeletableItems> findDeletableItemsForComment(List<Object> items, BigInteger commentId) { | |
173 | List<DeletableItems> elementsToRemove = new ArrayList<>(); | |
174 | for (Object item : items) { | |
175 | Object unwrapped = unwrap(item); | |
176 |
2
1. findDeletableItemsForComment : negated conditional → KILLED 2. findDeletableItemsForComment : negated conditional → KILLED |
if (unwrapped instanceof CommentRangeStart crs && Objects.equals(commentId, crs.getId())) |
177 | elementsToRemove.add(new DeletableItems(items, List.of(item))); | |
178 |
2
1. findDeletableItemsForComment : negated conditional → KILLED 2. findDeletableItemsForComment : negated conditional → KILLED |
else if (unwrapped instanceof CommentRangeEnd cre && Objects.equals(commentId, cre.getId())) |
179 | elementsToRemove.add(new DeletableItems(items, List.of(item))); | |
180 |
2
1. findDeletableItemsForComment : negated conditional → KILLED 2. findDeletableItemsForComment : negated conditional → KILLED |
else if (unwrapped instanceof R.CommentReference rcr && Objects.equals(commentId, rcr.getId())) |
181 | elementsToRemove.add(new DeletableItems(items, List.of(item))); | |
182 |
1
1. findDeletableItemsForComment : negated conditional → KILLED |
else if (unwrapped instanceof ContentAccessor ca) |
183 | elementsToRemove.addAll(findDeletableItemsForComment(ca.getContent(), commentId)); | |
184 |
1
1. findDeletableItemsForComment : negated conditional → KILLED |
else if (unwrapped instanceof SdtRun sdtRun) |
185 | elementsToRemove.addAll(findDeletableItemsForComment(sdtRun.getSdtContent() | |
186 | .getContent(), commentId)); | |
187 | } | |
188 |
1
1. findDeletableItemsForComment : replaced return value with Collections.emptyList for pro/verron/officestamper/core/CommentUtil::findDeletableItemsForComment → KILLED |
return elementsToRemove; |
189 | } | |
190 | ||
191 | /// Deletes all elements associated with the specified comment from the provided list of items. | |
192 | /// | |
193 | /// @param comment the comment whose associated elements should be removed | |
194 | /// @param items the list of items from which elements associated with the comment will be deleted | |
195 | public static void deleteCommentFromElements(Comment comment, List<Object> items) { | |
196 | var docx4jComment = comment.getComment(); | |
197 | var commentId = docx4jComment.getId(); | |
198 |
1
1. deleteCommentFromElements : removed call to java/util/List::forEach → KILLED |
findDeletableItemsForComment(items, commentId).forEach(p -> p.container.removeAll(p.items)); |
199 | } | |
200 | ||
201 | /// Creates a sub Word document | |
202 | /// by extracting a specified comment and its associated content from the original document. | |
203 | /// | |
204 | /// @param comment The comment to be extracted from the original document. | |
205 | /// | |
206 | /// @return The sub Word document containing the content of the specified comment. | |
207 | public static WordprocessingMLPackage createSubWordDocument(Comment comment) { | |
208 | var elements = comment.getElements(); | |
209 | ||
210 | var target = createWordPackageWithCommentsPart(); | |
211 | ||
212 | // copy the elements without comment range anchors | |
213 | var finalElements = elements.stream() | |
214 | .map(XmlUtils::deepCopy) | |
215 | .collect(Collectors.toCollection(ArrayList::new)); | |
216 |
1
1. createSubWordDocument : removed call to pro/verron/officestamper/core/CommentUtil::deleteCommentFromElements → SURVIVED |
deleteCommentFromElements(comment, finalElements); |
217 | target.getMainDocumentPart() | |
218 | .getContent() | |
219 | .addAll(finalElements); | |
220 | ||
221 | // copy the images from parent document using the original repeat elements | |
222 | var fakeBody = newBody(elements); | |
223 | DocumentUtil.walkObjectsAndImportImages(fakeBody, comment.getDocument(), target); | |
224 | ||
225 | var comments = extractComments(comment.getChildren()); | |
226 | target.getMainDocumentPart() | |
227 | .getCommentsPart() | |
228 |
1
1. createSubWordDocument : removed call to org/docx4j/openpackaging/parts/WordprocessingML/CommentsPart::setContents → KILLED |
.setContents(comments); |
229 |
1
1. createSubWordDocument : replaced return value with null for pro/verron/officestamper/core/CommentUtil::createSubWordDocument → KILLED |
return target; |
230 | } | |
231 | ||
232 | private static WordprocessingMLPackage createWordPackageWithCommentsPart() { | |
233 | try { | |
234 | CommentsPart targetCommentsPart = new CommentsPart(); | |
235 | var target = WordprocessingMLPackage.createPackage(); | |
236 | var mainDocumentPart = target.getMainDocumentPart(); | |
237 | mainDocumentPart.addTargetPart(targetCommentsPart); | |
238 |
1
1. createWordPackageWithCommentsPart : replaced return value with null for pro/verron/officestamper/core/CommentUtil::createWordPackageWithCommentsPart → KILLED |
return target; |
239 | } catch (InvalidFormatException e) { | |
240 | throw new OfficeStamperException("Failed to create a Word package with comment Part", e); | |
241 | } | |
242 | } | |
243 | ||
244 | private static Comments extractComments(Set<Comment> commentChildren) { | |
245 | var list = new ArrayList<Comments.Comment>(); | |
246 | var queue = new ArrayDeque<>(commentChildren); | |
247 |
1
1. extractComments : negated conditional → KILLED |
while (!queue.isEmpty()) { |
248 | var comment = queue.remove(); | |
249 | list.add(comment.getComment()); | |
250 |
1
1. extractComments : negated conditional → KILLED |
if (comment.getChildren() != null) { |
251 | queue.addAll(comment.getChildren()); | |
252 | } | |
253 | } | |
254 |
1
1. extractComments : replaced return value with null for pro/verron/officestamper/core/CommentUtil::extractComments → KILLED |
return newComments(list); |
255 | } | |
256 | ||
257 | private record DeletableItems(List<Object> container, List<Object> items) {} | |
258 | } | |
Mutations | ||
51 |
1.1 |
|
52 |
1.1 |
|
64 |
1.1 |
|
66 |
1.1 2.2 |
|
68 |
1.1 2.2 3.3 |
|
69 |
1.1 |
|
81 |
1.1 |
|
85 |
1.1 2.2 |
|
96 |
1.1 |
|
108 |
1.1 |
|
131 |
1.1 |
|
135 |
1.1 |
|
141 |
1.1 |
|
142 |
1.1 |
|
153 |
1.1 |
|
159 |
1.1 |
|
165 |
1.1 |
|
176 |
1.1 2.2 |
|
178 |
1.1 2.2 |
|
180 |
1.1 2.2 |
|
182 |
1.1 |
|
184 |
1.1 |
|
188 |
1.1 |
|
198 |
1.1 |
|
216 |
1.1 |
|
228 |
1.1 |
|
229 |
1.1 |
|
238 |
1.1 |
|
247 |
1.1 |
|
250 |
1.1 |
|
254 |
1.1 |