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