1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.cling.invoker.mvnup.goals;
20
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.codehaus.plexus.util.StringUtils;
28 import org.jdom2.Content;
29 import org.jdom2.Element;
30 import org.jdom2.Namespace;
31 import org.jdom2.Parent;
32 import org.jdom2.Text;
33
34 import static java.util.Arrays.asList;
35 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Indentation;
36 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
37 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
38 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CI_MANAGEMENT;
39 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CLASSIFIER;
40 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CONFIGURATION;
41 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CONTRIBUTORS;
42 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEFAULT_GOAL;
43 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCIES;
44 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY;
45 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY_MANAGEMENT;
46 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DESCRIPTION;
47 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEVELOPERS;
48 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DIRECTORY;
49 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DISTRIBUTION_MANAGEMENT;
50 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXCLUSIONS;
51 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXECUTIONS;
52 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.EXTENSIONS;
53 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.FINAL_NAME;
54 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GOALS;
55 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
56 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.INCEPTION_YEAR;
57 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.INHERITED;
58 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ISSUE_MANAGEMENT;
59 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.LICENSES;
60 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MAILING_LISTS;
61 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODEL_VERSION;
62 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.MODULES;
63 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.NAME;
64 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.OPTIONAL;
65 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ORGANIZATION;
66 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.OUTPUT_DIRECTORY;
67 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PACKAGING;
68 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
69 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
70 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
71 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_MANAGEMENT;
72 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_REPOSITORIES;
73 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PREREQUISITES;
74 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
75 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROPERTIES;
76 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPORTING;
77 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPOSITORIES;
78 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SCM;
79 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SCOPE;
80 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SCRIPT_SOURCE_DIRECTORY;
81 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SOURCE_DIRECTORY;
82 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SYSTEM_PATH;
83 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TEST_OUTPUT_DIRECTORY;
84 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TEST_SOURCE_DIRECTORY;
85 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TYPE;
86 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.URL;
87 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
88 import static org.jdom2.filter.Filters.textOnly;
89
90
91
92
93 public class JDomUtils {
94
95
96 private static final Map<String, List<String>> ELEMENT_ORDER = new HashMap<>();
97
98 static {
99
100 ELEMENT_ORDER.put(
101 "project",
102 asList(
103 MODEL_VERSION,
104 "",
105 PARENT,
106 "",
107 GROUP_ID,
108 ARTIFACT_ID,
109 VERSION,
110 PACKAGING,
111 "",
112 NAME,
113 DESCRIPTION,
114 URL,
115 INCEPTION_YEAR,
116 ORGANIZATION,
117 LICENSES,
118 "",
119 DEVELOPERS,
120 CONTRIBUTORS,
121 "",
122 MAILING_LISTS,
123 "",
124 PREREQUISITES,
125 "",
126 MODULES,
127 "",
128 SCM,
129 ISSUE_MANAGEMENT,
130 CI_MANAGEMENT,
131 DISTRIBUTION_MANAGEMENT,
132 "",
133 PROPERTIES,
134 "",
135 DEPENDENCY_MANAGEMENT,
136 DEPENDENCIES,
137 "",
138 REPOSITORIES,
139 PLUGIN_REPOSITORIES,
140 "",
141 BUILD,
142 "",
143 REPORTING,
144 "",
145 PROFILES));
146
147
148 ELEMENT_ORDER.put(
149 BUILD,
150 asList(
151 DEFAULT_GOAL,
152 DIRECTORY,
153 FINAL_NAME,
154 SOURCE_DIRECTORY,
155 SCRIPT_SOURCE_DIRECTORY,
156 TEST_SOURCE_DIRECTORY,
157 OUTPUT_DIRECTORY,
158 TEST_OUTPUT_DIRECTORY,
159 EXTENSIONS,
160 "",
161 PLUGIN_MANAGEMENT,
162 PLUGINS));
163
164
165 ELEMENT_ORDER.put(
166 PLUGIN,
167 asList(
168 GROUP_ID,
169 ARTIFACT_ID,
170 VERSION,
171 EXTENSIONS,
172 EXECUTIONS,
173 DEPENDENCIES,
174 GOALS,
175 INHERITED,
176 CONFIGURATION));
177
178
179 ELEMENT_ORDER.put(
180 DEPENDENCY,
181 asList(GROUP_ID, ARTIFACT_ID, VERSION, CLASSIFIER, TYPE, SCOPE, SYSTEM_PATH, OPTIONAL, EXCLUSIONS));
182 }
183
184 private JDomUtils() {
185
186 }
187
188
189
190
191
192
193
194
195
196
197
198
199 public static Element insertNewElement(String name, Element root) {
200 return insertNewElement(name, root, calcNewElementIndex(name, root));
201 }
202
203
204
205
206
207
208
209
210
211
212 public static Element insertNewElement(String name, Element root, int index) {
213 String indent = detectIndentation(root);
214 Element newElement = createElement(name, root.getNamespace());
215
216
217
218 boolean parentHasMinimalContent = root.getContentSize() == 1
219 && root.getContent(0) instanceof Text
220 && ((Text) root.getContent(0)).getText().trim().isEmpty();
221
222 if (parentHasMinimalContent) {
223
224 root.removeContent();
225 index = 0;
226 }
227
228 root.addContent(index, newElement);
229 addAppropriateSpacing(root, index, name, indent);
230
231
232 ensureProperClosingTagFormatting(root);
233 ensureProperClosingTagFormatting(newElement);
234
235 return newElement;
236 }
237
238
239
240
241
242 private static Element createElement(String name, Namespace namespace) {
243 Element newElement = new Element(name, namespace);
244
245
246
247 newElement.addContent(new Text(""));
248
249 return newElement;
250 }
251
252
253
254
255 private static void addAppropriateSpacing(Element root, int index, String elementName, String indent) {
256
257 String prependingElementName = "";
258 if (index > 0) {
259 Content prevContent = root.getContent(index - 1);
260 if (prevContent instanceof Element) {
261 prependingElementName = ((Element) prevContent).getName();
262 }
263 }
264
265 if (isBlankLineBetweenElements(prependingElementName, elementName, root)) {
266
267
268 root.addContent(index, new Text("\n"));
269 root.addContent(index + 1, new Text("\n" + indent));
270 } else {
271 root.addContent(index, new Text("\n" + indent));
272 }
273 }
274
275
276
277
278
279
280 private static void ensureProperClosingTagFormatting(Element parent) {
281 List<Content> contents = parent.getContent();
282
283
284 String parentIndent = detectParentIndentation(parent);
285
286
287 if (contents.isEmpty()
288 || (contents.size() == 1
289 && contents.get(0) instanceof Text
290 && ((Text) contents.get(0)).getText().trim().isEmpty())) {
291
292
293 parent.removeContent();
294 parent.addContent(new Text("\n" + parentIndent));
295 return;
296 }
297
298
299 Content lastContent = contents.get(contents.size() - 1);
300 if (lastContent instanceof Text) {
301 String text = ((Text) lastContent).getText();
302
303 if (!text.endsWith("\n" + parentIndent)) {
304
305 if (text.trim().isEmpty()) {
306 parent.removeContent(lastContent);
307 parent.addContent(new Text("\n" + parentIndent));
308 } else {
309
310 parent.addContent(new Text("\n" + parentIndent));
311 }
312 }
313 } else {
314
315 parent.addContent(new Text("\n" + parentIndent));
316 }
317 }
318
319
320
321
322 private static String detectParentIndentation(Element element) {
323 Parent parent = element.getParent();
324 if (parent instanceof Element) {
325 return detectIndentation((Element) parent);
326 }
327 return "";
328 }
329
330
331
332
333
334
335
336
337
338 public static Element insertContentElement(Element parent, String name, String content) {
339 Element element = insertNewElement(name, parent);
340 element.setText(content);
341 return element;
342 }
343
344
345
346
347
348
349
350
351
352 public static String detectIndentation(Element element) {
353
354 for (Iterator<Text> iterator = element.getContent(textOnly()).iterator(); iterator.hasNext(); ) {
355 String text = iterator.next().getText();
356 int lastLsIndex = StringUtils.lastIndexOfAny(text, new String[] {"\n", "\r"});
357 if (lastLsIndex > -1) {
358 String indent = text.substring(lastLsIndex + 1);
359 if (iterator.hasNext()) {
360
361 return indent;
362 } else {
363
364 String baseIndent = detectBaseIndentationUnit(element);
365 return indent + baseIndent;
366 }
367 }
368 }
369
370 Parent parent = element.getParent();
371 if (parent instanceof Element) {
372 String baseIndent = detectBaseIndentationUnit(element);
373 return detectIndentation((Element) parent) + baseIndent;
374 }
375
376 return "";
377 }
378
379
380
381
382
383
384
385
386 public static String detectBaseIndentationUnit(Element element) {
387
388 Element root = element;
389 while (root.getParent() instanceof Element) {
390 root = (Element) root.getParent();
391 }
392
393
394 Map<String, Integer> indentationCounts = new HashMap<>();
395 collectIndentationSamples(root, indentationCounts, "");
396
397
398 return analyzeIndentationPattern(indentationCounts);
399 }
400
401
402
403
404 private static void collectIndentationSamples(
405 Element element, Map<String, Integer> indentationCounts, String parentIndent) {
406 for (Iterator<Text> iterator = element.getContent(textOnly()).iterator(); iterator.hasNext(); ) {
407 String text = iterator.next().getText();
408 int lastLsIndex = StringUtils.lastIndexOfAny(text, new String[] {"\n", "\r"});
409 if (lastLsIndex > -1) {
410 String indent = text.substring(lastLsIndex + 1);
411 if (iterator.hasNext() && !indent.isEmpty()) {
412
413 if (indent.length() > parentIndent.length()) {
414 String indentDiff = indent.substring(parentIndent.length());
415 indentationCounts.merge(indentDiff, 1, Integer::sum);
416 }
417 }
418 }
419 }
420
421
422 for (Element child : element.getChildren()) {
423 String childIndent = detectIndentationForElement(element, child);
424 if (childIndent != null && childIndent.length() > parentIndent.length()) {
425 String indentDiff = childIndent.substring(parentIndent.length());
426 indentationCounts.merge(indentDiff, 1, Integer::sum);
427 collectIndentationSamples(child, indentationCounts, childIndent);
428 }
429 }
430 }
431
432
433
434
435 private static String detectIndentationForElement(Element parent, Element child) {
436 int childIndex = parent.indexOf(child);
437 if (childIndex > 0) {
438 Content prevContent = parent.getContent(childIndex - 1);
439 if (prevContent instanceof Text) {
440 String text = ((Text) prevContent).getText();
441 int lastLsIndex = StringUtils.lastIndexOfAny(text, new String[] {"\n", "\r"});
442 if (lastLsIndex > -1) {
443 return text.substring(lastLsIndex + 1);
444 }
445 }
446 }
447 return null;
448 }
449
450
451
452
453 private static String analyzeIndentationPattern(Map<String, Integer> indentationCounts) {
454 if (indentationCounts.isEmpty()) {
455 return Indentation.TWO_SPACES;
456 }
457
458
459 String mostCommon = indentationCounts.entrySet().stream()
460 .max(Map.Entry.comparingByValue())
461 .map(Map.Entry::getKey)
462 .orElse(Indentation.TWO_SPACES);
463
464
465 if (mostCommon.matches("^\\s+$")) {
466 return mostCommon;
467 }
468
469
470 Set<String> patterns = indentationCounts.keySet();
471
472
473 if (patterns.stream().anyMatch(p -> p.equals(Indentation.FOUR_SPACES))) {
474 return Indentation.FOUR_SPACES;
475 }
476 if (patterns.stream().anyMatch(p -> p.equals(Indentation.TAB))) {
477 return Indentation.TAB;
478 }
479 if (patterns.stream().anyMatch(p -> p.equals(Indentation.TWO_SPACES))) {
480 return Indentation.TWO_SPACES;
481 }
482
483
484 return mostCommon.isEmpty() ? Indentation.TWO_SPACES : mostCommon;
485 }
486
487
488
489
490 private static int calcNewElementIndex(String elementName, Element parent) {
491 List<String> elementOrder = ELEMENT_ORDER.get(parent.getName());
492 if (elementOrder == null || elementOrder.isEmpty()) {
493 return parent.getContentSize();
494 }
495
496 int targetIndex = elementOrder.indexOf(elementName);
497 if (targetIndex == -1) {
498 return parent.getContentSize();
499 }
500
501
502 List<Content> contents = parent.getContent();
503 for (int i = contents.size() - 1; i >= 0; i--) {
504 Content content = contents.get(i);
505 if (content instanceof Element element) {
506 int currentIndex = elementOrder.indexOf(element.getName());
507 if (currentIndex != -1 && currentIndex <= targetIndex) {
508 return i + 1;
509 }
510 }
511 }
512
513 return 0;
514 }
515
516
517
518
519
520
521 private static boolean isBlankLineBetweenElements(
522 String prependingElementName, String elementName, Element parent) {
523 List<String> elementOrder = ELEMENT_ORDER.get(parent.getName());
524 if (elementOrder == null || elementOrder.isEmpty()) {
525 return false;
526 }
527
528 int prependingIndex = elementOrder.indexOf(prependingElementName);
529 int currentIndex = elementOrder.indexOf(elementName);
530
531 if (prependingIndex == -1 || currentIndex == -1) {
532 return false;
533 }
534
535
536 for (int i = prependingIndex + 1; i < currentIndex; i++) {
537 if (elementOrder.get(i).isEmpty()) {
538 return true;
539 }
540 }
541
542 return false;
543 }
544 }