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.nio.file.Files;
22 import java.nio.file.Path;
23 import java.util.ArrayList;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Set;
29 import java.util.stream.Stream;
30
31 import org.apache.maven.api.cli.mvnup.UpgradeOptions;
32 import org.apache.maven.api.di.Named;
33 import org.apache.maven.api.di.Priority;
34 import org.apache.maven.api.di.Singleton;
35 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
36 import org.jdom2.Content;
37 import org.jdom2.Document;
38 import org.jdom2.Element;
39 import org.jdom2.Namespace;
40 import org.jdom2.Text;
41
42 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.DEFAULT_PARENT_RELATIVE_PATH;
43 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.POM_XML;
44 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.ModelVersions.MODEL_VERSION_4_1_0;
45 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
46 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
47 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY;
48 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
49 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
50 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
51 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
52 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.RELATIVE_PATH;
53 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECT;
54 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.SUBPROJECTS;
55 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
56
57
58
59
60
61
62
63 @Named
64 @Singleton
65 @Priority(30)
66 public class InferenceStrategy extends AbstractUpgradeStrategy {
67
68 @Override
69 public boolean isApplicable(UpgradeContext context) {
70 UpgradeOptions options = getOptions(context);
71
72
73 boolean useAll = options.all().orElse(false);
74 if (useAll) {
75 return true;
76 }
77
78
79 if (options.infer().isPresent()) {
80 return options.infer().get();
81 }
82
83
84 if (options.infer().isEmpty()
85 && options.model().isEmpty()
86 && options.plugins().isEmpty()
87 && options.modelVersion().isEmpty()) {
88 return true;
89 }
90
91 return false;
92 }
93
94 @Override
95 public String getDescription() {
96 return "Applying Maven inference optimizations";
97 }
98
99 @Override
100 public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
101 Set<Path> processedPoms = new HashSet<>();
102 Set<Path> modifiedPoms = new HashSet<>();
103 Set<Path> errorPoms = new HashSet<>();
104
105
106 Set<GAV> allGAVs = GAVUtils.computeAllGAVs(context, pomMap);
107
108 for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
109 Path pomPath = entry.getKey();
110 Document pomDocument = entry.getValue();
111 processedPoms.add(pomPath);
112
113 String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
114 context.info(pomPath + " (current: " + currentVersion + ")");
115 context.indent();
116
117 try {
118 if (!ModelVersionUtils.isEligibleForInference(currentVersion)) {
119 context.warning(
120 "Model version " + currentVersion + " not eligible for inference (requires >= 4.0.0)");
121 continue;
122 }
123
124 boolean hasInferences = false;
125
126
127 hasInferences |= applyLimitedParentInference(context, pomDocument);
128
129
130 if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
131 hasInferences |= applyFullParentInference(context, pomMap, pomDocument);
132 hasInferences |= applyDependencyInference(context, allGAVs, pomDocument);
133 hasInferences |= applyDependencyInferenceRedundancy(context, pomMap, pomDocument);
134 hasInferences |= applySubprojectsInference(context, pomDocument, pomPath);
135 hasInferences |= applyModelVersionInference(context, pomDocument);
136 }
137
138 if (hasInferences) {
139 modifiedPoms.add(pomPath);
140 if (MODEL_VERSION_4_1_0.equals(currentVersion)
141 || ModelVersionUtils.isNewerThan410(currentVersion)) {
142 context.success("Full inference optimizations applied");
143 } else {
144 context.success("Limited inference optimizations applied (parent-related only)");
145 }
146 } else {
147 context.success("No inference optimizations needed");
148 }
149 } catch (Exception e) {
150 context.failure("Failed to apply inference optimizations" + ": " + e.getMessage());
151 errorPoms.add(pomPath);
152 } finally {
153 context.unindent();
154 }
155 }
156
157 return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
158 }
159
160
161
162
163
164 private boolean applyLimitedParentInference(UpgradeContext context, Document pomDocument) {
165 Element root = pomDocument.getRootElement();
166 Namespace namespace = root.getNamespace();
167
168
169 Element parentElement = root.getChild(PARENT, namespace);
170 if (parentElement == null) {
171 return false;
172 }
173
174
175 return trimParentElementLimited(context, root, parentElement, namespace);
176 }
177
178
179
180
181
182 private boolean applyFullParentInference(UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
183 Element root = pomDocument.getRootElement();
184 Namespace namespace = root.getNamespace();
185
186
187 Element parentElement = root.getChild(PARENT, namespace);
188 if (parentElement == null) {
189 return false;
190 }
191
192
193 return trimParentElementFull(context, root, parentElement, namespace, pomMap);
194 }
195
196
197
198
199
200 private boolean applyDependencyInference(UpgradeContext context, Set<GAV> allGAVs, Document pomDocument) {
201 boolean hasChanges = false;
202 Element root = pomDocument.getRootElement();
203 Namespace namespace = root.getNamespace();
204
205
206 Element dependencyManagement = root.getChild("dependencyManagement", namespace);
207 if (dependencyManagement != null) {
208 Element dependencies = dependencyManagement.getChild("dependencies", namespace);
209 if (dependencies != null) {
210 hasChanges |= removeManagedDependenciesFromSection(
211 context, dependencies, namespace, allGAVs, "dependencyManagement");
212 }
213 }
214
215
216 Element profilesElement = root.getChild("profiles", namespace);
217 if (profilesElement != null) {
218 List<Element> profileElements = profilesElement.getChildren("profile", namespace);
219 for (Element profileElement : profileElements) {
220 Element profileDependencyManagement = profileElement.getChild("dependencyManagement", namespace);
221 if (profileDependencyManagement != null) {
222 Element profileDependencies = profileDependencyManagement.getChild("dependencies", namespace);
223 if (profileDependencies != null) {
224 hasChanges |= removeManagedDependenciesFromSection(
225 context, profileDependencies, namespace, allGAVs, "profile dependencyManagement");
226 }
227 }
228 }
229 }
230
231 return hasChanges;
232 }
233
234
235
236
237
238 private boolean applyDependencyInferenceRedundancy(
239 UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
240 Element root = pomDocument.getRootElement();
241 Namespace namespace = root.getNamespace();
242 boolean hasChanges = false;
243
244
245 Element dependenciesElement = root.getChild("dependencies", namespace);
246 if (dependenciesElement != null) {
247 hasChanges |= removeDependencyInferenceFromSection(
248 context, dependenciesElement, namespace, pomMap, "dependencies");
249 }
250
251
252 Element profilesElement = root.getChild("profiles", namespace);
253 if (profilesElement != null) {
254 List<Element> profileElements = profilesElement.getChildren("profile", namespace);
255 for (Element profileElement : profileElements) {
256 Element profileDependencies = profileElement.getChild("dependencies", namespace);
257 if (profileDependencies != null) {
258 hasChanges |= removeDependencyInferenceFromSection(
259 context, profileDependencies, namespace, pomMap, "profile dependencies");
260 }
261 }
262 }
263
264
265 Element buildElement = root.getChild(BUILD, namespace);
266 if (buildElement != null) {
267 Element pluginsElement = buildElement.getChild(PLUGINS, namespace);
268 if (pluginsElement != null) {
269 List<Element> pluginElements = pluginsElement.getChildren(PLUGIN, namespace);
270 for (Element pluginElement : pluginElements) {
271 Element pluginDependencies = pluginElement.getChild("dependencies", namespace);
272 if (pluginDependencies != null) {
273 hasChanges |= removeDependencyInferenceFromSection(
274 context, pluginDependencies, namespace, pomMap, "plugin dependencies");
275 }
276 }
277 }
278 }
279
280 return hasChanges;
281 }
282
283
284
285
286
287 private boolean applySubprojectsInference(UpgradeContext context, Document pomDocument, Path pomPath) {
288 boolean hasChanges = false;
289 Element root = pomDocument.getRootElement();
290 Namespace namespace = root.getNamespace();
291
292
293 Element subprojectsElement = root.getChild(SUBPROJECTS, namespace);
294 if (subprojectsElement != null) {
295 if (isSubprojectsListRedundant(subprojectsElement, namespace, pomPath)) {
296 removeElementWithFormatting(subprojectsElement);
297 context.detail("Removed: redundant subprojects list (matches direct children)");
298 hasChanges = true;
299 }
300 }
301
302
303 Element profilesElement = root.getChild("profiles", namespace);
304 if (profilesElement != null) {
305 List<Element> profileElements = profilesElement.getChildren("profile", namespace);
306 for (Element profileElement : profileElements) {
307 Element profileSubprojects = profileElement.getChild(SUBPROJECTS, namespace);
308 if (profileSubprojects != null) {
309 if (isSubprojectsListRedundant(profileSubprojects, namespace, pomPath)) {
310 removeElementWithFormatting(profileSubprojects);
311 context.detail("Removed: redundant subprojects list from profile (matches direct children)");
312 hasChanges = true;
313 }
314 }
315 }
316 }
317
318 return hasChanges;
319 }
320
321
322
323
324
325 private boolean applyModelVersionInference(UpgradeContext context, Document pomDocument) {
326 String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
327
328
329 if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
330
331 if (ModelVersionUtils.removeModelVersion(pomDocument)) {
332 context.detail("Removed: modelVersion element (can be inferred from namespace)");
333 return true;
334 }
335 }
336
337 return false;
338 }
339
340
341
342
343
344 private boolean trimParentElementLimited(
345 UpgradeContext context, Element root, Element parentElement, Namespace namespace) {
346 boolean hasChanges = false;
347
348
349 String parentGroupId = getChildText(parentElement, "groupId", namespace);
350 String parentVersion = getChildText(parentElement, "version", namespace);
351
352
353 String childGroupId = getChildText(root, "groupId", namespace);
354 String childVersion = getChildText(root, "version", namespace);
355
356
357 if (childGroupId != null && Objects.equals(childGroupId, parentGroupId)) {
358 Element childGroupIdElement = root.getChild("groupId", namespace);
359 if (childGroupIdElement != null) {
360 removeElementWithFormatting(childGroupIdElement);
361 context.detail("Removed: child groupId (matches parent)");
362 hasChanges = true;
363 }
364 }
365
366
367 if (childVersion != null && Objects.equals(childVersion, parentVersion)) {
368 Element childVersionElement = root.getChild("version", namespace);
369 if (childVersionElement != null) {
370 removeElementWithFormatting(childVersionElement);
371 context.detail("Removed: child version (matches parent)");
372 hasChanges = true;
373 }
374 }
375
376 return hasChanges;
377 }
378
379
380
381
382
383 private boolean trimParentElementFull(
384 UpgradeContext context,
385 Element root,
386 Element parentElement,
387 Namespace namespace,
388 Map<Path, Document> pomMap) {
389 boolean hasChanges = false;
390
391
392 String childGroupId = getChildText(root, GROUP_ID, namespace);
393 String childVersion = getChildText(root, VERSION, namespace);
394
395
396 hasChanges |= trimParentElementLimited(context, root, parentElement, namespace);
397
398
399 if (isParentInReactor(parentElement, namespace, pomMap, context)) {
400
401 if (childGroupId == null) {
402 Element parentGroupIdElement = parentElement.getChild(GROUP_ID, namespace);
403 if (parentGroupIdElement != null) {
404 removeElementWithFormatting(parentGroupIdElement);
405 context.detail("Removed: parent groupId (child has no explicit groupId)");
406 hasChanges = true;
407 }
408 }
409
410
411 if (childVersion == null) {
412 Element parentVersionElement = parentElement.getChild(VERSION, namespace);
413 if (parentVersionElement != null) {
414 removeElementWithFormatting(parentVersionElement);
415 context.detail("Removed: parent version (child has no explicit version)");
416 hasChanges = true;
417 }
418 }
419
420
421 if (canInferParentArtifactId(parentElement, namespace, pomMap)) {
422 Element parentArtifactIdElement = parentElement.getChild(ARTIFACT_ID, namespace);
423 if (parentArtifactIdElement != null) {
424 removeElementWithFormatting(parentArtifactIdElement);
425 context.detail("Removed: parent artifactId (can be inferred from relativePath)");
426 hasChanges = true;
427 }
428 }
429 }
430
431 return hasChanges;
432 }
433
434
435
436
437
438 private boolean isParentInReactor(
439 Element parentElement, Namespace namespace, Map<Path, Document> pomMap, UpgradeContext context) {
440
441 String relativePath = getChildText(parentElement, RELATIVE_PATH, namespace);
442 if (relativePath != null && relativePath.trim().isEmpty()) {
443 return false;
444 }
445
446
447 String parentGroupId = getChildText(parentElement, GROUP_ID, namespace);
448 String parentArtifactId = getChildText(parentElement, ARTIFACT_ID, namespace);
449 String parentVersion = getChildText(parentElement, VERSION, namespace);
450
451 if (parentGroupId == null || parentArtifactId == null || parentVersion == null) {
452
453 return false;
454 }
455
456 GAV parentGAV = new GAV(parentGroupId, parentArtifactId, parentVersion);
457
458
459 for (Document pomDocument : pomMap.values()) {
460 GAV pomGAV = GAVUtils.extractGAVWithParentResolution(context, pomDocument);
461 if (pomGAV != null && pomGAV.equals(parentGAV)) {
462 return true;
463 }
464 }
465
466
467 return false;
468 }
469
470
471
472
473 private boolean canInferParentArtifactId(Element parentElement, Namespace namespace, Map<Path, Document> pomMap) {
474
475 String relativePath = getChildText(parentElement, RELATIVE_PATH, namespace);
476 if (relativePath == null || relativePath.trim().isEmpty()) {
477 relativePath = DEFAULT_PARENT_RELATIVE_PATH;
478 }
479
480
481
482 return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && pomMap.size() > 1;
483 }
484
485
486
487
488 private boolean isSubprojectsListRedundant(Element subprojectsElement, Namespace namespace, Path pomPath) {
489 List<Element> subprojectElements = subprojectsElement.getChildren(SUBPROJECT, namespace);
490 if (subprojectElements.isEmpty()) {
491 return true;
492 }
493
494
495 Path parentDir = pomPath.getParent();
496 if (parentDir == null) {
497 return false;
498 }
499
500
501 Set<String> declaredSubprojects = new HashSet<>();
502 for (Element subprojectElement : subprojectElements) {
503 String subprojectName = subprojectElement.getTextTrim();
504 if (subprojectName != null && !subprojectName.isEmpty()) {
505 declaredSubprojects.add(subprojectName);
506 }
507 }
508
509
510 Set<String> actualSubprojects = new HashSet<>();
511 try {
512 if (Files.exists(parentDir) && Files.isDirectory(parentDir)) {
513 try (Stream<Path> children = Files.list(parentDir)) {
514 children.filter(Files::isDirectory)
515 .filter(dir -> Files.exists(dir.resolve(POM_XML)))
516 .forEach(dir ->
517 actualSubprojects.add(dir.getFileName().toString()));
518 }
519 }
520 } catch (Exception e) {
521
522 return false;
523 }
524
525
526 return declaredSubprojects.equals(actualSubprojects);
527 }
528
529
530
531
532 private boolean removeManagedDependenciesFromSection(
533 UpgradeContext context, Element dependencies, Namespace namespace, Set<GAV> allGAVs, String sectionName) {
534 List<Element> dependencyElements = dependencies.getChildren(DEPENDENCY, namespace);
535 List<Element> toRemove = new ArrayList<>();
536
537 for (Element dependency : dependencyElements) {
538 String groupId = getChildText(dependency, GROUP_ID, namespace);
539 String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
540
541 if (groupId != null && artifactId != null) {
542
543 boolean isProjectArtifact = allGAVs.stream()
544 .anyMatch(gav ->
545 Objects.equals(gav.groupId(), groupId) && Objects.equals(gav.artifactId(), artifactId));
546
547 if (isProjectArtifact) {
548 toRemove.add(dependency);
549 context.detail("Removed: " + "managed dependency " + groupId + ":" + artifactId + " from "
550 + sectionName + " (project artifact)");
551 }
552 }
553 }
554
555
556 for (Element dependency : toRemove) {
557 removeElementWithFormatting(dependency);
558 }
559
560 return !toRemove.isEmpty();
561 }
562
563
564
565
566 private boolean removeDependencyInferenceFromSection(
567 UpgradeContext context,
568 Element dependencies,
569 Namespace namespace,
570 Map<Path, Document> pomMap,
571 String sectionName) {
572 List<Element> dependencyElements = dependencies.getChildren(DEPENDENCY, namespace);
573 boolean hasChanges = false;
574
575 for (Element dependency : dependencyElements) {
576 String groupId = getChildText(dependency, GROUP_ID, namespace);
577 String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
578 String version = getChildText(dependency, VERSION, namespace);
579
580 if (artifactId != null) {
581
582 Document dependencyPom = findDependencyPom(context, pomMap, groupId, artifactId);
583 if (dependencyPom != null) {
584
585 if (groupId != null && canInferDependencyGroupId(context, dependencyPom, groupId)) {
586 Element groupIdElement = dependency.getChild(GROUP_ID, namespace);
587 if (groupIdElement != null) {
588 removeElementWithFormatting(groupIdElement);
589 context.detail("Removed: " + "dependency groupId " + groupId + " from " + sectionName
590 + " (can be inferred from project)");
591 hasChanges = true;
592 }
593 }
594
595
596 if (version != null && canInferDependencyVersion(context, dependencyPom, version)) {
597 Element versionElement = dependency.getChild(VERSION, namespace);
598 if (versionElement != null) {
599 removeElementWithFormatting(versionElement);
600 context.detail("Removed: " + "dependency version " + version + " from " + sectionName
601 + " (can be inferred from project)");
602 hasChanges = true;
603 }
604 }
605 }
606 }
607 }
608
609 return hasChanges;
610 }
611
612
613
614
615 private Document findDependencyPom(
616 UpgradeContext context, Map<Path, Document> pomMap, String groupId, String artifactId) {
617 for (Document pomDocument : pomMap.values()) {
618 GAV gav = GAVUtils.extractGAVWithParentResolution(context, pomDocument);
619 if (gav != null && Objects.equals(gav.groupId(), groupId) && Objects.equals(gav.artifactId(), artifactId)) {
620 return pomDocument;
621 }
622 }
623 return null;
624 }
625
626
627
628
629 private boolean canInferDependencyVersion(UpgradeContext context, Document dependencyPom, String declaredVersion) {
630 GAV projectGav = GAVUtils.extractGAVWithParentResolution(context, dependencyPom);
631 if (projectGav == null || projectGav.version() == null) {
632 return false;
633 }
634
635
636 return Objects.equals(declaredVersion, projectGav.version());
637 }
638
639
640
641
642 private boolean canInferDependencyGroupId(UpgradeContext context, Document dependencyPom, String declaredGroupId) {
643 GAV projectGav = GAVUtils.extractGAVWithParentResolution(context, dependencyPom);
644 if (projectGav == null || projectGav.groupId() == null) {
645 return false;
646 }
647
648
649 return Objects.equals(declaredGroupId, projectGav.groupId());
650 }
651
652
653
654
655 private String getChildText(Element parent, String childName, Namespace namespace) {
656 Element child = parent.getChild(childName, namespace);
657 return child != null ? child.getTextTrim() : null;
658 }
659
660
661
662
663 private void removeElementWithFormatting(Element element) {
664 Element parent = element.getParentElement();
665 if (parent != null) {
666 int index = parent.indexOf(element);
667 parent.removeContent(element);
668
669
670 if (index > 0) {
671 Content prevContent = parent.getContent(index - 1);
672 if (prevContent instanceof Text text && text.getTextTrim().isEmpty()) {
673 parent.removeContent(prevContent);
674 }
675 }
676 }
677 }
678 }