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