1 | package pro.verron.officestamper.core; | |
2 | ||
3 | import org.docx4j.XmlUtils; | |
4 | import org.docx4j.jaxb.Context; | |
5 | import org.docx4j.openpackaging.exceptions.Docx4JException; | |
6 | import org.docx4j.openpackaging.exceptions.InvalidFormatException; | |
7 | import org.docx4j.openpackaging.packages.WordprocessingMLPackage; | |
8 | import org.docx4j.openpackaging.parts.PartName; | |
9 | import org.docx4j.openpackaging.parts.Parts; | |
10 | import org.docx4j.openpackaging.parts.WordprocessingML.CommentsPart; | |
11 | import org.docx4j.wml.*; | |
12 | import org.jvnet.jaxb2_commons.ppp.Child; | |
13 | import org.slf4j.Logger; | |
14 | import org.slf4j.LoggerFactory; | |
15 | import pro.verron.officestamper.api.Comment; | |
16 | import pro.verron.officestamper.api.OfficeStamperException; | |
17 | import pro.verron.officestamper.api.Placeholder; | |
18 | ||
19 | import java.math.BigInteger; | |
20 | import java.util.*; | |
21 | import java.util.stream.Collectors; | |
22 | ||
23 | import static org.docx4j.XmlUtils.unwrap; | |
24 | ||
25 | /** | |
26 | * Utility class for working with comments in a DOCX document. | |
27 | * | |
28 | * @author Joseph Verron | |
29 | * @author Tom Hombergs | |
30 | * @version ${version} | |
31 | * @since 1.0.0 | |
32 | */ | |
33 | public class CommentUtil { | |
34 | private static final PartName WORD_COMMENTS_PART_NAME; | |
35 | private static final Logger logger = LoggerFactory.getLogger(CommentUtil.class); | |
36 | ||
37 | static { | |
38 | try { | |
39 | WORD_COMMENTS_PART_NAME = new PartName("/word/comments.xml"); | |
40 | } catch (InvalidFormatException e) { | |
41 | throw new OfficeStamperException(e); | |
42 | } | |
43 | } | |
44 | ||
45 | private CommentUtil() { | |
46 | throw new OfficeStamperException("Utility class shouldn't be instantiated"); | |
47 | } | |
48 | ||
49 | /** | |
50 | * Returns the comment the given DOCX4J object is commented with. | |
51 | * | |
52 | * @param run the DOCX4J object whose comment to retrieve. | |
53 | * @param document the document that contains the object. | |
54 | * | |
55 | * @return Optional of the comment, if found, Optional.empty() otherwise. | |
56 | */ | |
57 | public static Optional<Comments.Comment> getCommentAround(R run, WordprocessingMLPackage document) { | |
58 | ContentAccessor parent = (ContentAccessor) ((Child) run).getParent(); | |
59 |
1
1. getCommentAround : negated conditional → KILLED |
if (parent == null) return Optional.empty(); |
60 | ||
61 | try { | |
62 |
1
1. getCommentAround : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::getCommentAround → KILLED |
return getComment(run, document, parent); |
63 | } catch (Docx4JException e) { | |
64 | throw new OfficeStamperException("error accessing the comments of the document!", e); | |
65 | } | |
66 | } | |
67 | ||
68 | private static Optional<Comments.Comment> getComment( | |
69 | R run, WordprocessingMLPackage document, ContentAccessor parent | |
70 | ) | |
71 | throws Docx4JException { | |
72 | CommentRangeStart possibleComment = null; | |
73 | boolean foundChild = false; | |
74 | for (Object contentElement : parent.getContent()) { | |
75 | // so first we look for the start of the comment | |
76 |
1
1. getComment : negated conditional → KILLED |
if (unwrap(contentElement) instanceof CommentRangeStart crs) possibleComment = crs; |
77 | // then we check if the child we are looking for is ours | |
78 |
2
1. getComment : negated conditional → KILLED 2. getComment : negated conditional → KILLED |
else if (possibleComment != null && run.equals(contentElement)) foundChild = true; |
79 | // and then, if we have an end of a comment, we are good! | |
80 |
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) { |
81 | try { | |
82 | var id = possibleComment.getId(); | |
83 |
1
1. getComment : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::getComment → KILLED |
return findComment(document, id); |
84 | } catch (InvalidFormatException e) { | |
85 | var format = "Error while searching comment. Skipping run %s."; | |
86 | var message = String.format(format, run); | |
87 | logger.warn(message, e); | |
88 | } | |
89 | } | |
90 | // else restart | |
91 | else { | |
92 | possibleComment = null; | |
93 | foundChild = false; | |
94 | } | |
95 | } | |
96 | return Optional.empty(); | |
97 | } | |
98 | ||
99 | /** | |
100 | * Finds a comment with the given ID in the specified WordprocessingMLPackage document. | |
101 | * | |
102 | * @param document the WordprocessingMLPackage document to search for the comment | |
103 | * @param id the ID of the comment to find | |
104 | * | |
105 | * @return an Optional containing the Comment if found, or an empty Optional if not found | |
106 | * | |
107 | * @throws Docx4JException if an error occurs while searching for the comment | |
108 | */ | |
109 | private static Optional<Comments.Comment> findComment(WordprocessingMLPackage document, BigInteger id) | |
110 | throws Docx4JException { | |
111 | var wordComments = getCommentsPart(document.getParts()); | |
112 | var comments = wordComments.getContents(); | |
113 |
1
1. findComment : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::findComment → KILLED |
return comments.getComment() |
114 | .stream() | |
115 |
2
1. lambda$findComment$0 : replaced boolean return with true for pro/verron/officestamper/core/CommentUtil::lambda$findComment$0 → KILLED 2. lambda$findComment$0 : replaced boolean return with false for pro/verron/officestamper/core/CommentUtil::lambda$findComment$0 → KILLED |
.filter(comment -> comment.getId() |
116 | .equals(id)) | |
117 | .findFirst(); | |
118 | } | |
119 | ||
120 | static CommentsPart getCommentsPart(Parts parts) { | |
121 |
1
1. getCommentsPart : replaced return value with null for pro/verron/officestamper/core/CommentUtil::getCommentsPart → KILLED |
return (CommentsPart) parts.get(WORD_COMMENTS_PART_NAME); |
122 | } | |
123 | ||
124 | /** | |
125 | * Returns the first comment found for the given docx object. Note that an object is | |
126 | * only considered commented if the comment STARTS within the object. Comments | |
127 | * spanning several objects are not supported by this method. | |
128 | * | |
129 | * @param object the object whose comment to load. | |
130 | * @param document the document in which the object is embedded (needed to load the | |
131 | * comment from the comments.xml part). | |
132 | * | |
133 | * @return the concatenated string of all text paragraphs within the | |
134 | * comment or null if the specified object is not commented. | |
135 | */ | |
136 | public static Optional<Comments.Comment> getCommentFor(ContentAccessor object, WordprocessingMLPackage document) { | |
137 | for (Object contentObject : object.getContent()) { | |
138 |
1
1. getCommentFor : negated conditional → KILLED |
if (!(contentObject instanceof CommentRangeStart crs)) continue; |
139 | BigInteger id = crs.getId(); | |
140 | CommentsPart commentsPart = getCommentsPart(document.getParts()); | |
141 | var comments = getComments(commentsPart); | |
142 | ||
143 | for (Comments.Comment comment : comments) { | |
144 | var commentId = comment.getId(); | |
145 |
1
1. getCommentFor : negated conditional → KILLED |
if (commentId.equals(id)) { |
146 |
1
1. getCommentFor : replaced return value with Optional.empty for pro/verron/officestamper/core/CommentUtil::getCommentFor → KILLED |
return Optional.of(comment); |
147 | } | |
148 | } | |
149 | } | |
150 | return Optional.empty(); | |
151 | } | |
152 | ||
153 | public static List<Comments.Comment> getComments(CommentsPart commentsPart) { | |
154 | try { | |
155 |
1
1. getComments : replaced return value with Collections.emptyList for pro/verron/officestamper/core/CommentUtil::getComments → KILLED |
return commentsPart.getContents() |
156 | .getComment(); | |
157 | } catch (Docx4JException e) { | |
158 | throw new OfficeStamperException("error accessing the comments of the document!", e); | |
159 | } | |
160 | } | |
161 | ||
162 | /** | |
163 | * Returns the string value of the specified comment object. | |
164 | * | |
165 | * @param comment a {@link Comments.Comment} object | |
166 | * | |
167 | * @return a {@link String} object | |
168 | */ | |
169 | public static Placeholder getCommentString(Comments.Comment comment) { | |
170 | StringBuilder builder = new StringBuilder(); | |
171 | for (Object commentChildObject : comment.getContent()) { | |
172 |
1
1. getCommentString : negated conditional → KILLED |
if (commentChildObject instanceof P p) { |
173 | builder.append(new StandardParagraph(p).asString()); | |
174 | } | |
175 | } | |
176 | String string = builder.toString(); | |
177 |
1
1. getCommentString : replaced return value with null for pro/verron/officestamper/core/CommentUtil::getCommentString → KILLED |
return Placeholders.raw(string); |
178 | } | |
179 | ||
180 | /** | |
181 | * Returns the string value of the specified comment object. | |
182 | * | |
183 | * @param comment a {@link Comment} object | |
184 | */ | |
185 | public static void deleteComment(Comment comment) { | |
186 | CommentRangeEnd end = comment.getCommentRangeEnd(); | |
187 |
1
1. deleteComment : negated conditional → KILLED |
if (end != null) { |
188 | ContentAccessor endParent = (ContentAccessor) end.getParent(); | |
189 | endParent.getContent() | |
190 | .remove(end); | |
191 | } | |
192 | CommentRangeStart start = comment.getCommentRangeStart(); | |
193 |
1
1. deleteComment : negated conditional → SURVIVED |
if (start != null) { |
194 | ContentAccessor startParent = (ContentAccessor) start.getParent(); | |
195 | startParent.getContent() | |
196 | .remove(start); | |
197 | } | |
198 | R.CommentReference reference = comment.getCommentReference(); | |
199 |
1
1. deleteComment : negated conditional → KILLED |
if (reference != null) { |
200 | ContentAccessor referenceParent = (ContentAccessor) reference.getParent(); | |
201 | referenceParent.getContent() | |
202 | .remove(reference); | |
203 | } | |
204 | } | |
205 | ||
206 | /** | |
207 | * Returns the string value of the specified comment object. | |
208 | * | |
209 | * @param items a {@link List} object | |
210 | * @param commentId a {@link BigInteger} object | |
211 | */ | |
212 | public static void deleteCommentFromElements(List<Object> items, BigInteger commentId) { | |
213 | List<Object> elementsToRemove = new ArrayList<>(); | |
214 | for (Object item : items) { | |
215 | Object unwrapped = unwrap(item); | |
216 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
if (unwrapped instanceof CommentRangeStart crs) { |
217 | var id = crs.getId(); | |
218 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
if (id.equals(commentId)) { |
219 | elementsToRemove.add(item); | |
220 | } | |
221 | } | |
222 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
else if (unwrapped instanceof CommentRangeEnd cre) { |
223 | var id = cre.getId(); | |
224 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
if (id.equals(commentId)) { |
225 | elementsToRemove.add(item); | |
226 | } | |
227 | } | |
228 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
else if (unwrapped instanceof R.CommentReference rcr) { |
229 | var id = rcr.getId(); | |
230 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
if (id.equals(commentId)) { |
231 | elementsToRemove.add(item); | |
232 | } | |
233 | } | |
234 |
1
1. deleteCommentFromElements : negated conditional → KILLED |
else if (unwrapped instanceof ContentAccessor ca) { |
235 |
1
1. deleteCommentFromElements : removed call to pro/verron/officestamper/core/CommentUtil::deleteCommentFromElements → KILLED |
deleteCommentFromElements(ca.getContent(), commentId); |
236 | } | |
237 | } | |
238 | items.removeAll(elementsToRemove); | |
239 | } | |
240 | ||
241 | private static void deleteCommentFromElements( | |
242 | Comment comment, List<Object> elements | |
243 | ) { | |
244 | var docx4jComment = comment.getComment(); | |
245 | var commentId = docx4jComment.getId(); | |
246 |
1
1. deleteCommentFromElements : removed call to pro/verron/officestamper/core/CommentUtil::deleteCommentFromElements → KILLED |
deleteCommentFromElements(elements, commentId); |
247 | } | |
248 | ||
249 | /** | |
250 | * Creates a sub Word document | |
251 | * by extracting a specified comment and its associated content from the original document. | |
252 | * | |
253 | * @param comment The comment to be extracted from the original document. | |
254 | * | |
255 | * @return The sub Word document containing the content of the specified comment. | |
256 | */ | |
257 | public static WordprocessingMLPackage createSubWordDocument(Comment comment) { | |
258 | var elements = comment.getElements(); | |
259 | ||
260 | var target = createWordPackageWithCommentsPart(); | |
261 | ||
262 | // copy the elements without comment range anchors | |
263 | var finalElements = elements.stream() | |
264 | .map(XmlUtils::deepCopy) | |
265 | .collect(Collectors.toCollection(ArrayList::new)); | |
266 |
1
1. createSubWordDocument : removed call to pro/verron/officestamper/core/CommentUtil::deleteCommentFromElements → KILLED |
deleteCommentFromElements(comment, finalElements); |
267 | target.getMainDocumentPart() | |
268 | .getContent() | |
269 | .addAll(finalElements); | |
270 | ||
271 | // copy the images from parent document using the original repeat elements | |
272 | var wmlObjectFactory = Context.getWmlObjectFactory(); | |
273 | var fakeBody = wmlObjectFactory.createBody(); | |
274 | fakeBody.getContent() | |
275 | .addAll(elements); | |
276 | DocumentUtil.walkObjectsAndImportImages(fakeBody, comment.getDocument(), target); | |
277 | ||
278 | var comments = extractComments(comment.getChildren()); | |
279 | target.getMainDocumentPart() | |
280 | .getCommentsPart() | |
281 |
1
1. createSubWordDocument : removed call to org/docx4j/openpackaging/parts/WordprocessingML/CommentsPart::setContents → KILLED |
.setContents(comments); |
282 |
1
1. createSubWordDocument : replaced return value with null for pro/verron/officestamper/core/CommentUtil::createSubWordDocument → KILLED |
return target; |
283 | } | |
284 | ||
285 | private static WordprocessingMLPackage createWordPackageWithCommentsPart() { | |
286 | try { | |
287 | CommentsPart targetCommentsPart = new CommentsPart(); | |
288 | var target = WordprocessingMLPackage.createPackage(); | |
289 | var mainDocumentPart = target.getMainDocumentPart(); | |
290 | mainDocumentPart.addTargetPart(targetCommentsPart); | |
291 |
1
1. createWordPackageWithCommentsPart : replaced return value with null for pro/verron/officestamper/core/CommentUtil::createWordPackageWithCommentsPart → KILLED |
return target; |
292 | } catch (InvalidFormatException e) { | |
293 | throw new OfficeStamperException("Failed to create a Word package with comment Part", e); | |
294 | } | |
295 | } | |
296 | ||
297 | private static Comments extractComments(Set<Comment> commentChildren) { | |
298 | var wmlObjectFactory = Context.getWmlObjectFactory(); | |
299 | var comments = wmlObjectFactory.createComments(); | |
300 | var commentList = comments.getComment(); | |
301 | ||
302 | var queue = new ArrayDeque<>(commentChildren); | |
303 |
1
1. extractComments : negated conditional → KILLED |
while (!queue.isEmpty()) { |
304 | var comment = queue.remove(); | |
305 | commentList.add(comment.getComment()); | |
306 |
1
1. extractComments : negated conditional → KILLED |
if (comment.getChildren() != null) { |
307 | queue.addAll(comment.getChildren()); | |
308 | } | |
309 | } | |
310 |
1
1. extractComments : replaced return value with null for pro/verron/officestamper/core/CommentUtil::extractComments → KILLED |
return comments; |
311 | } | |
312 | } | |
Mutations | ||
59 |
1.1 |
|
62 |
1.1 |
|
76 |
1.1 |
|
78 |
1.1 2.2 |
|
80 |
1.1 2.2 3.3 |
|
83 |
1.1 |
|
113 |
1.1 |
|
115 |
1.1 2.2 |
|
121 |
1.1 |
|
138 |
1.1 |
|
145 |
1.1 |
|
146 |
1.1 |
|
155 |
1.1 |
|
172 |
1.1 |
|
177 |
1.1 |
|
187 |
1.1 |
|
193 |
1.1 |
|
199 |
1.1 |
|
216 |
1.1 |
|
218 |
1.1 |
|
222 |
1.1 |
|
224 |
1.1 |
|
228 |
1.1 |
|
230 |
1.1 |
|
234 |
1.1 |
|
235 |
1.1 |
|
246 |
1.1 |
|
266 |
1.1 |
|
281 |
1.1 |
|
282 |
1.1 |
|
291 |
1.1 |
|
303 |
1.1 |
|
306 |
1.1 |
|
310 |
1.1 |