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 hasChanges |= trimParentElementLimited(context, root, parentElement, namespace);
393
394
395 String childGroupId = getChildText(root, GROUP_ID, namespace);
396 String childVersion = getChildText(root, VERSION, namespace);
397
398
399 if (childGroupId == null) {
400 Element parentGroupIdElement = parentElement.getChild(GROUP_ID, namespace);
401 if (parentGroupIdElement != null) {
402 removeElementWithFormatting(parentGroupIdElement);
403 context.detail("Removed: parent groupId (child has no explicit groupId)");
404 hasChanges = true;
405 }
406 }
407
408
409 if (childVersion == null) {
410 Element parentVersionElement = parentElement.getChild(VERSION, namespace);
411 if (parentVersionElement != null) {
412 removeElementWithFormatting(parentVersionElement);
413 context.detail("Removed: parent version (child has no explicit version)");
414 hasChanges = true;
415 }
416 }
417
418
419 if (canInferParentArtifactId(parentElement, namespace, pomMap)) {
420 Element parentArtifactIdElement = parentElement.getChild(ARTIFACT_ID, namespace);
421 if (parentArtifactIdElement != null) {
422 removeElementWithFormatting(parentArtifactIdElement);
423 context.detail("Removed: parent artifactId (can be inferred from relativePath)");
424 hasChanges = true;
425 }
426 }
427
428 return hasChanges;
429 }
430
431
432
433
434 private boolean canInferParentArtifactId(Element parentElement, Namespace namespace, Map<Path, Document> pomMap) {
435
436 String relativePath = getChildText(parentElement, RELATIVE_PATH, namespace);
437 if (relativePath == null || relativePath.trim().isEmpty()) {
438 relativePath = DEFAULT_PARENT_RELATIVE_PATH;
439 }
440
441
442
443
444
445 return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && !pomMap.isEmpty();
446 }
447
448
449
450
451 private boolean isSubprojectsListRedundant(Element subprojectsElement, Namespace namespace, Path pomPath) {
452 List<Element> subprojectElements = subprojectsElement.getChildren(SUBPROJECT, namespace);
453 if (subprojectElements.isEmpty()) {
454 return true;
455 }
456
457
458 Path parentDir = pomPath.getParent();
459 if (parentDir == null) {
460 return false;
461 }
462
463
464 Set<String> declaredSubprojects = new HashSet<>();
465 for (Element subprojectElement : subprojectElements) {
466 String subprojectName = subprojectElement.getTextTrim();
467 if (subprojectName != null && !subprojectName.isEmpty()) {
468 declaredSubprojects.add(subprojectName);
469 }
470 }
471
472
473 Set<String> actualSubprojects = new HashSet<>();
474 try {
475 if (Files.exists(parentDir) && Files.isDirectory(parentDir)) {
476 try (Stream<Path> children = Files.list(parentDir)) {
477 children.filter(Files::isDirectory)
478 .filter(dir -> Files.exists(dir.resolve(POM_XML)))
479 .forEach(dir ->
480 actualSubprojects.add(dir.getFileName().toString()));
481 }
482 }
483 } catch (Exception e) {
484
485 return false;
486 }
487
488
489 return declaredSubprojects.equals(actualSubprojects);
490 }
491
492
493
494
495 private boolean removeManagedDependenciesFromSection(
496 UpgradeContext context, Element dependencies, Namespace namespace, Set<GAV> allGAVs, String sectionName) {
497 List<Element> dependencyElements = dependencies.getChildren(DEPENDENCY, namespace);
498 List<Element> toRemove = new ArrayList<>();
499
500 for (Element dependency : dependencyElements) {
501 String groupId = getChildText(dependency, GROUP_ID, namespace);
502 String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
503
504 if (groupId != null && artifactId != null) {
505
506 boolean isProjectArtifact = allGAVs.stream()
507 .anyMatch(gav ->
508 Objects.equals(gav.groupId(), groupId) && Objects.equals(gav.artifactId(), artifactId));
509
510 if (isProjectArtifact) {
511 toRemove.add(dependency);
512 context.detail("Removed: " + "managed dependency " + groupId + ":" + artifactId + " from "
513 + sectionName + " (project artifact)");
514 }
515 }
516 }
517
518
519 for (Element dependency : toRemove) {
520 removeElementWithFormatting(dependency);
521 }
522
523 return !toRemove.isEmpty();
524 }
525
526
527
528
529 private boolean removeDependencyInferenceFromSection(
530 UpgradeContext context,
531 Element dependencies,
532 Namespace namespace,
533 Map<Path, Document> pomMap,
534 String sectionName) {
535 List<Element> dependencyElements = dependencies.getChildren(DEPENDENCY, namespace);
536 boolean hasChanges = false;
537
538 for (Element dependency : dependencyElements) {
539 String groupId = getChildText(dependency, GROUP_ID, namespace);
540 String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
541 String version = getChildText(dependency, VERSION, namespace);
542
543 if (artifactId != null) {
544
545 Document dependencyPom = findDependencyPom(context, pomMap, groupId, artifactId);
546 if (dependencyPom != null) {
547
548 if (groupId != null && canInferDependencyGroupId(context, dependencyPom, groupId)) {
549 Element groupIdElement = dependency.getChild(GROUP_ID, namespace);
550 if (groupIdElement != null) {
551 removeElementWithFormatting(groupIdElement);
552 context.detail("Removed: " + "dependency groupId " + groupId + " from " + sectionName
553 + " (can be inferred from project)");
554 hasChanges = true;
555 }
556 }
557
558
559 if (version != null && canInferDependencyVersion(context, dependencyPom, version)) {
560 Element versionElement = dependency.getChild(VERSION, namespace);
561 if (versionElement != null) {
562 removeElementWithFormatting(versionElement);
563 context.detail("Removed: " + "dependency version " + version + " from " + sectionName
564 + " (can be inferred from project)");
565 hasChanges = true;
566 }
567 }
568 }
569 }
570 }
571
572 return hasChanges;
573 }
574
575
576
577
578 private Document findDependencyPom(
579 UpgradeContext context, Map<Path, Document> pomMap, String groupId, String artifactId) {
580 for (Document pomDocument : pomMap.values()) {
581 GAV gav = GAVUtils.extractGAVWithParentResolution(context, pomDocument);
582 if (gav != null && Objects.equals(gav.groupId(), groupId) && Objects.equals(gav.artifactId(), artifactId)) {
583 return pomDocument;
584 }
585 }
586 return null;
587 }
588
589
590
591
592 private boolean canInferDependencyVersion(UpgradeContext context, Document dependencyPom, String declaredVersion) {
593 GAV projectGav = GAVUtils.extractGAVWithParentResolution(context, dependencyPom);
594 if (projectGav == null || projectGav.version() == null) {
595 return false;
596 }
597
598
599 return Objects.equals(declaredVersion, projectGav.version());
600 }
601
602
603
604
605 private boolean canInferDependencyGroupId(UpgradeContext context, Document dependencyPom, String declaredGroupId) {
606 GAV projectGav = GAVUtils.extractGAVWithParentResolution(context, dependencyPom);
607 if (projectGav == null || projectGav.groupId() == null) {
608 return false;
609 }
610
611
612 return Objects.equals(declaredGroupId, projectGav.groupId());
613 }
614
615
616
617
618 private String getChildText(Element parent, String childName, Namespace namespace) {
619 Element child = parent.getChild(childName, namespace);
620 return child != null ? child.getTextTrim() : null;
621 }
622
623
624
625
626 private void removeElementWithFormatting(Element element) {
627 Element parent = element.getParentElement();
628 if (parent != null) {
629 int index = parent.indexOf(element);
630 parent.removeContent(element);
631
632
633 if (index > 0) {
634 Content prevContent = parent.getContent(index - 1);
635 if (prevContent instanceof Text text && text.getTextTrim().isEmpty()) {
636 parent.removeContent(prevContent);
637 }
638 }
639 }
640 }
641 }