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.Path;
22 import java.util.HashMap;
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.Stream;
29
30 import eu.maveniverse.domtrip.Document;
31 import eu.maveniverse.domtrip.Element;
32 import eu.maveniverse.domtrip.maven.Coordinates;
33 import eu.maveniverse.domtrip.maven.MavenPomElements;
34 import org.apache.maven.api.cli.mvnup.UpgradeOptions;
35 import org.apache.maven.api.di.Named;
36 import org.apache.maven.api.di.Priority;
37 import org.apache.maven.api.di.Singleton;
38 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
39
40 import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_APPEND;
41 import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_CHILDREN;
42 import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_MERGE;
43 import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_OVERRIDE;
44 import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_SELF;
45 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
46 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCIES;
47 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY;
48 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY_MANAGEMENT;
49 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PARENT;
50 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
51 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
52 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
53 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORIES;
54 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORY;
55 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
56 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
57 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.RELATIVE_PATH;
58 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORIES;
59 import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORY;
60 import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.DEFAULT_PARENT_RELATIVE_PATH;
61 import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
62 import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.MAVEN_PLUGIN_PREFIX;
63
64
65
66
67
68 @Named
69 @Singleton
70 @Priority(20)
71 public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {
72
73 @Override
74 public boolean isApplicable(UpgradeContext context) {
75 UpgradeOptions options = getOptions(context);
76
77
78 boolean useAll = options.all().orElse(false);
79 if (useAll) {
80 return true;
81 }
82
83
84
85 boolean noOptionsSpecified = options.all().isEmpty()
86 && options.infer().isEmpty()
87 && options.model().isEmpty()
88 && options.plugins().isEmpty()
89 && options.modelVersion().isEmpty();
90
91 boolean allOptionsDisabled = options.all().map(v -> !v).orElse(false)
92 && options.infer().map(v -> !v).orElse(false)
93 && options.model().map(v -> !v).orElse(false)
94 && options.plugins().map(v -> !v).orElse(false)
95 && options.modelVersion().isEmpty();
96
97 if (noOptionsSpecified || allOptionsDisabled) {
98 return true;
99 }
100
101
102 if (options.model().isPresent()) {
103 return options.model().get();
104 }
105
106 return false;
107 }
108
109 @Override
110 public String getDescription() {
111 return "Applying Maven 4 compatibility fixes";
112 }
113
114 @Override
115 public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
116 Set<Path> processedPoms = new HashSet<>();
117 Set<Path> modifiedPoms = new HashSet<>();
118 Set<Path> errorPoms = new HashSet<>();
119
120 for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
121 Path pomPath = entry.getKey();
122 Document pomDocument = entry.getValue();
123 processedPoms.add(pomPath);
124
125 context.info(pomPath + " (checking for Maven 4 compatibility issues)");
126 context.indent();
127
128 try {
129 boolean hasIssues = false;
130
131
132 hasIssues |= fixUnsupportedCombineChildrenAttributes(pomDocument, context);
133 hasIssues |= fixUnsupportedCombineSelfAttributes(pomDocument, context);
134 hasIssues |= fixDuplicateDependencies(pomDocument, context);
135 hasIssues |= fixDuplicatePlugins(pomDocument, context);
136 hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, context);
137 hasIssues |= fixIncorrectParentRelativePaths(pomDocument, pomPath, pomMap, context);
138
139 if (hasIssues) {
140 context.success("Maven 4 compatibility issues fixed");
141 modifiedPoms.add(pomPath);
142 } else {
143 context.success("No Maven 4 compatibility issues found");
144 }
145 } catch (Exception e) {
146 context.failure("Failed to fix Maven 4 compatibility issues" + ": " + e.getMessage());
147 errorPoms.add(pomPath);
148 } finally {
149 context.unindent();
150 }
151 }
152
153 return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
154 }
155
156
157
158
159
160 private boolean fixUnsupportedCombineChildrenAttributes(Document pomDocument, UpgradeContext context) {
161 boolean fixed = false;
162 Element root = pomDocument.root();
163
164
165 long fixedCombineChildrenCount = findElementsWithAttribute(root, COMBINE_CHILDREN, COMBINE_OVERRIDE)
166 .peek(element -> {
167 element.attributeObject(COMBINE_CHILDREN).value(COMBINE_MERGE);
168 context.detail("Fixed: " + COMBINE_CHILDREN + "='" + COMBINE_OVERRIDE + "' → '" + COMBINE_MERGE
169 + "' in " + element.name());
170 })
171 .count();
172 fixed |= fixedCombineChildrenCount > 0;
173
174 return fixed;
175 }
176
177
178
179
180
181 private boolean fixUnsupportedCombineSelfAttributes(Document pomDocument, UpgradeContext context) {
182 boolean fixed = false;
183 Element root = pomDocument.root();
184
185
186 long fixedCombineSelfCount = findElementsWithAttribute(root, COMBINE_SELF, COMBINE_APPEND)
187 .peek(element -> {
188 element.attributeObject(COMBINE_SELF).value(COMBINE_MERGE);
189 context.detail("Fixed: " + COMBINE_SELF + "='" + COMBINE_APPEND + "' → '" + COMBINE_MERGE + "' in "
190 + element.name());
191 })
192 .count();
193 fixed |= fixedCombineSelfCount > 0;
194
195 return fixed;
196 }
197
198
199
200
201 private boolean fixDuplicateDependencies(Document pomDocument, UpgradeContext context) {
202 Element root = pomDocument.root();
203
204
205 Stream<DependencyContainer> dependencyContainers = Stream.concat(
206
207 Stream.of(
208 new DependencyContainer(root.child(DEPENDENCIES).orElse(null), DEPENDENCIES),
209 new DependencyContainer(
210 root.child(DEPENDENCY_MANAGEMENT)
211 .flatMap(dm -> dm.child(DEPENDENCIES))
212 .orElse(null),
213 DEPENDENCY_MANAGEMENT))
214 .filter(container -> container.element != null),
215
216 root.child(PROFILES).stream()
217 .flatMap(profiles -> profiles.children(PROFILE))
218 .flatMap(profile -> Stream.of(
219 new DependencyContainer(
220 profile.child(DEPENDENCIES).orElse(null), "profile dependencies"),
221 new DependencyContainer(
222 profile.child(DEPENDENCY_MANAGEMENT)
223 .flatMap(dm -> dm.child(DEPENDENCIES))
224 .orElse(null),
225 "profile dependencyManagement"))
226 .filter(container -> container.element != null)));
227
228 return dependencyContainers
229 .map(container -> fixDuplicateDependenciesInSection(container.element, context, container.sectionName))
230 .reduce(false, Boolean::logicalOr);
231 }
232
233 private static class DependencyContainer {
234 final Element element;
235 final String sectionName;
236
237 DependencyContainer(Element element, String sectionName) {
238 this.element = element;
239 this.sectionName = sectionName;
240 }
241 }
242
243
244
245
246 private boolean fixDuplicatePlugins(Document pomDocument, UpgradeContext context) {
247 Element root = pomDocument.root();
248
249
250 Stream<BuildContainer> buildContainers = Stream.concat(
251
252 Stream.of(new BuildContainer(root.child(BUILD).orElse(null), BUILD))
253 .filter(container -> container.element != null),
254
255 root.child(PROFILES).stream()
256 .flatMap(profiles -> profiles.children(PROFILE))
257 .map(profile -> new BuildContainer(profile.child(BUILD).orElse(null), "profile build"))
258 .filter(container -> container.element != null));
259
260 return buildContainers
261 .map(container -> fixPluginsInBuildElement(container.element, context, container.sectionName))
262 .reduce(false, Boolean::logicalOr);
263 }
264
265 private static class BuildContainer {
266 final Element element;
267 final String sectionName;
268
269 BuildContainer(Element element, String sectionName) {
270 this.element = element;
271 this.sectionName = sectionName;
272 }
273 }
274
275
276
277
278 private boolean fixUnsupportedRepositoryExpressions(Document pomDocument, UpgradeContext context) {
279 Element root = pomDocument.root();
280
281
282 Stream<Element> repositoryContainers = Stream.concat(
283
284 Stream.of(
285 root.child(REPOSITORIES).orElse(null),
286 root.child(PLUGIN_REPOSITORIES).orElse(null))
287 .filter(Objects::nonNull),
288
289 root.child(PROFILES).stream()
290 .flatMap(profiles -> profiles.children(PROFILE))
291 .flatMap(profile -> Stream.of(
292 profile.child(REPOSITORIES).orElse(null),
293 profile.child(PLUGIN_REPOSITORIES).orElse(null))
294 .filter(Objects::nonNull)));
295
296 return repositoryContainers
297 .map(container -> fixRepositoryExpressions(container, pomDocument, context))
298 .reduce(false, Boolean::logicalOr);
299 }
300
301
302
303
304 private boolean fixIncorrectParentRelativePaths(
305 Document pomDocument, Path pomPath, Map<Path, Document> pomMap, UpgradeContext context) {
306 Element root = pomDocument.root();
307
308 Element parentElement = root.child(PARENT).orElse(null);
309 if (parentElement == null) {
310 return false;
311 }
312
313 Element relativePathElement = parentElement.child(RELATIVE_PATH).orElse(null);
314 String currentRelativePath =
315 relativePathElement != null ? relativePathElement.textContent().trim() : DEFAULT_PARENT_RELATIVE_PATH;
316
317
318 String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
319 String parentArtifactId = parentElement.childText(MavenPomElements.Elements.ARTIFACT_ID);
320 String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);
321
322 Path correctParentPath = findParentPomInMap(context, parentGroupId, parentArtifactId, parentVersion, pomMap);
323 if (correctParentPath != null) {
324 try {
325 Path correctRelativePath = pomPath.getParent().relativize(correctParentPath);
326 String correctRelativePathStr = correctRelativePath.toString().replace('\\', '/');
327
328 if (!correctRelativePathStr.equals(currentRelativePath)) {
329
330 DomUtils.updateOrCreateChildElement(parentElement, RELATIVE_PATH, correctRelativePathStr);
331 context.detail("Fixed: " + "relativePath corrected from '" + currentRelativePath + "' to '"
332 + correctRelativePathStr + "'");
333 return true;
334 }
335 } catch (Exception e) {
336 context.failure("Failed to compute correct relativePath" + ": " + e.getMessage());
337 }
338 }
339
340 return false;
341 }
342
343
344
345
346 private Stream<Element> findElementsWithAttribute(Element element, String attributeName, String attributeValue) {
347 return Stream.concat(
348
349 Stream.of(element).filter(e -> {
350 String attr = e.attribute(attributeName);
351 return attr != null && attributeValue.equals(attr);
352 }),
353
354 element.children().flatMap(child -> findElementsWithAttribute(child, attributeName, attributeValue)));
355 }
356
357
358
359
360 private boolean fixDuplicateDependenciesInSection(
361 Element dependenciesElement, UpgradeContext context, String sectionName) {
362 List<Element> dependencies = dependenciesElement.children(DEPENDENCY).toList();
363 Map<String, Element> seenDependencies = new HashMap<>();
364
365 List<Element> duplicates = dependencies.stream()
366 .filter(dependency -> {
367 String key = createDependencyKey(dependency);
368 if (seenDependencies.containsKey(key)) {
369 context.detail("Fixed: Removed duplicate dependency: " + key + " in " + sectionName);
370 return true;
371 } else {
372 seenDependencies.put(key, dependency);
373 return false;
374 }
375 })
376 .toList();
377
378
379 duplicates.forEach(DomUtils::removeElement);
380
381 return !duplicates.isEmpty();
382 }
383
384 private String createDependencyKey(Element dependency) {
385 String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
386 String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
387 String type = dependency.childText(MavenPomElements.Elements.TYPE);
388 String classifier = dependency.childText(MavenPomElements.Elements.CLASSIFIER);
389
390 return groupId + ":" + artifactId + ":" + (type != null ? type : "jar") + ":"
391 + (classifier != null ? classifier : "");
392 }
393
394 private boolean fixPluginsInBuildElement(Element buildElement, UpgradeContext context, String sectionName) {
395 boolean fixed = false;
396
397 Element pluginsElement = buildElement.child(PLUGINS).orElse(null);
398 if (pluginsElement != null) {
399 fixed |= fixDuplicatePluginsInSection(pluginsElement, context, sectionName + "/" + PLUGINS);
400 }
401
402 Element pluginManagementElement = buildElement.child(PLUGIN_MANAGEMENT).orElse(null);
403 if (pluginManagementElement != null) {
404 Element managedPluginsElement =
405 pluginManagementElement.child(PLUGINS).orElse(null);
406 if (managedPluginsElement != null) {
407 fixed |= fixDuplicatePluginsInSection(
408 managedPluginsElement, context, sectionName + "/" + PLUGIN_MANAGEMENT + "/" + PLUGINS);
409 }
410 }
411
412 return fixed;
413 }
414
415
416
417
418 private boolean fixDuplicatePluginsInSection(Element pluginsElement, UpgradeContext context, String sectionName) {
419 List<Element> plugins = pluginsElement.children(PLUGIN).toList();
420 Map<String, Element> seenPlugins = new HashMap<>();
421
422 List<Element> duplicates = plugins.stream()
423 .filter(plugin -> {
424 String key = createPluginKey(plugin);
425 if (key != null) {
426 if (seenPlugins.containsKey(key)) {
427 context.detail("Fixed: Removed duplicate plugin: " + key + " in " + sectionName);
428 return true;
429 } else {
430 seenPlugins.put(key, plugin);
431 }
432 }
433 return false;
434 })
435 .toList();
436
437
438 duplicates.forEach(DomUtils::removeElement);
439
440 return !duplicates.isEmpty();
441 }
442
443 private String createPluginKey(Element plugin) {
444 String groupId = plugin.childText(MavenPomElements.Elements.GROUP_ID);
445 String artifactId = plugin.childText(MavenPomElements.Elements.ARTIFACT_ID);
446
447
448 if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
449 groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
450 }
451
452 return (groupId != null && artifactId != null) ? groupId + ":" + artifactId : null;
453 }
454
455 private boolean fixRepositoryExpressions(
456 Element repositoriesElement, Document pomDocument, UpgradeContext context) {
457 if (repositoriesElement == null) {
458 return false;
459 }
460
461 boolean fixed = false;
462 String elementType = repositoriesElement.name().equals(REPOSITORIES) ? REPOSITORY : PLUGIN_REPOSITORY;
463 List<Element> repositories = repositoriesElement.children(elementType).toList();
464
465 for (Element repository : repositories) {
466 Element urlElement = repository.child("url").orElse(null);
467 if (urlElement != null) {
468 String url = urlElement.textContent().trim();
469 if (url.contains("${")) {
470
471
472 String repositoryId = repository.childText("id");
473 context.info("Detected interpolated expression in " + elementType + " URL (id: " + repositoryId
474 + "): " + url);
475 }
476 }
477 }
478
479 return fixed;
480 }
481
482 private Path findParentPomInMap(
483 UpgradeContext context, String groupId, String artifactId, String version, Map<Path, Document> pomMap) {
484 return pomMap.entrySet().stream()
485 .filter(entry -> {
486 Coordinates gav = AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(
487 context, entry.getValue());
488 return gav != null
489 && Objects.equals(gav.groupId(), groupId)
490 && Objects.equals(gav.artifactId(), artifactId)
491 && (version == null || Objects.equals(gav.version(), version));
492 })
493 .findFirst()
494 .map(Map.Entry::getKey)
495 .orElse(null);
496 }
497 }