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