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.ArrayList;
23 import java.util.HashMap;
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
30 import org.apache.maven.api.cli.mvnup.UpgradeOptions;
31 import org.apache.maven.api.di.Named;
32 import org.apache.maven.api.di.Priority;
33 import org.apache.maven.api.di.Singleton;
34 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
35 import org.jdom2.Attribute;
36 import org.jdom2.Comment;
37 import org.jdom2.Content;
38 import org.jdom2.Document;
39 import org.jdom2.Element;
40 import org.jdom2.Namespace;
41 import org.jdom2.Text;
42
43 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Files.DEFAULT_PARENT_RELATIVE_PATH;
44 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
45 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.Plugins.MAVEN_PLUGIN_PREFIX;
46 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_APPEND;
47 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_CHILDREN;
48 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_MERGE;
49 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_OVERRIDE;
50 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlAttributes.COMBINE_SELF;
51 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.ARTIFACT_ID;
52 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.BUILD;
53 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.CLASSIFIER;
54 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCIES;
55 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY;
56 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.DEPENDENCY_MANAGEMENT;
57 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.GROUP_ID;
58 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PARENT;
59 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN;
60 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGINS;
61 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_MANAGEMENT;
62 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_REPOSITORIES;
63 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PLUGIN_REPOSITORY;
64 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILE;
65 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.PROFILES;
66 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.RELATIVE_PATH;
67 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPOSITORIES;
68 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.REPOSITORY;
69 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.TYPE;
70 import static org.apache.maven.cling.invoker.mvnup.goals.UpgradeConstants.XmlElements.VERSION;
71
72
73
74
75
76 @Named
77 @Singleton
78 @Priority(20)
79 public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {
80
81 @Override
82 public boolean isApplicable(UpgradeContext context) {
83 UpgradeOptions options = getOptions(context);
84
85
86 boolean useAll = options.all().orElse(false);
87 if (useAll) {
88 return true;
89 }
90
91
92
93 boolean noOptionsSpecified = options.all().isEmpty()
94 && options.infer().isEmpty()
95 && options.model().isEmpty()
96 && options.plugins().isEmpty()
97 && options.modelVersion().isEmpty();
98
99 boolean allOptionsDisabled = options.all().map(v -> !v).orElse(false)
100 && options.infer().map(v -> !v).orElse(false)
101 && options.model().map(v -> !v).orElse(false)
102 && options.plugins().map(v -> !v).orElse(false)
103 && options.modelVersion().isEmpty();
104
105 if (noOptionsSpecified || allOptionsDisabled) {
106 return true;
107 }
108
109
110 if (options.model().isPresent()) {
111 return options.model().get();
112 }
113
114 return false;
115 }
116
117 @Override
118 public String getDescription() {
119 return "Applying Maven 4 compatibility fixes";
120 }
121
122 @Override
123 public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
124 Set<Path> processedPoms = new HashSet<>();
125 Set<Path> modifiedPoms = new HashSet<>();
126 Set<Path> errorPoms = new HashSet<>();
127
128 for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
129 Path pomPath = entry.getKey();
130 Document pomDocument = entry.getValue();
131 processedPoms.add(pomPath);
132
133 context.info(pomPath + " (checking for Maven 4 compatibility issues)");
134 context.indent();
135
136 try {
137 boolean hasIssues = false;
138
139
140 hasIssues |= fixUnsupportedCombineChildrenAttributes(pomDocument, context);
141 hasIssues |= fixUnsupportedCombineSelfAttributes(pomDocument, context);
142 hasIssues |= fixDuplicateDependencies(pomDocument, context);
143 hasIssues |= fixDuplicatePlugins(pomDocument, context);
144 hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, context);
145 hasIssues |= fixIncorrectParentRelativePaths(pomDocument, pomPath, pomMap, context);
146
147 if (hasIssues) {
148 context.success("Maven 4 compatibility issues fixed");
149 modifiedPoms.add(pomPath);
150 } else {
151 context.success("No Maven 4 compatibility issues found");
152 }
153 } catch (Exception e) {
154 context.failure("Failed to fix Maven 4 compatibility issues" + ": " + e.getMessage());
155 errorPoms.add(pomPath);
156 } finally {
157 context.unindent();
158 }
159 }
160
161 return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
162 }
163
164
165
166
167
168 private boolean fixUnsupportedCombineChildrenAttributes(Document pomDocument, UpgradeContext context) {
169 boolean fixed = false;
170 Element root = pomDocument.getRootElement();
171
172
173 List<Element> elementsWithCombineChildren = findElementsWithAttribute(root, COMBINE_CHILDREN, COMBINE_OVERRIDE);
174 for (Element element : elementsWithCombineChildren) {
175 element.getAttribute(COMBINE_CHILDREN).setValue(COMBINE_MERGE);
176 context.detail("Fixed: " + COMBINE_CHILDREN + "='" + COMBINE_OVERRIDE + "' → '" + COMBINE_MERGE + "' in "
177 + element.getName());
178 fixed = true;
179 }
180
181 return fixed;
182 }
183
184
185
186
187
188 private boolean fixUnsupportedCombineSelfAttributes(Document pomDocument, UpgradeContext context) {
189 boolean fixed = false;
190 Element root = pomDocument.getRootElement();
191
192
193 List<Element> elementsWithCombineSelf = findElementsWithAttribute(root, COMBINE_SELF, COMBINE_APPEND);
194 for (Element element : elementsWithCombineSelf) {
195 element.getAttribute(COMBINE_SELF).setValue(COMBINE_MERGE);
196 context.detail("Fixed: " + COMBINE_SELF + "='" + COMBINE_APPEND + "' → '" + COMBINE_MERGE + "' in "
197 + element.getName());
198 fixed = true;
199 }
200
201 return fixed;
202 }
203
204
205
206
207 private boolean fixDuplicateDependencies(Document pomDocument, UpgradeContext context) {
208 Element root = pomDocument.getRootElement();
209 Namespace namespace = root.getNamespace();
210 boolean fixed = false;
211
212
213 Element dependenciesElement = root.getChild(DEPENDENCIES, namespace);
214 if (dependenciesElement != null) {
215 fixed |= fixDuplicateDependenciesInSection(dependenciesElement, namespace, context, DEPENDENCIES);
216 }
217
218
219 Element dependencyManagementElement = root.getChild(DEPENDENCY_MANAGEMENT, namespace);
220 if (dependencyManagementElement != null) {
221 Element managedDependenciesElement = dependencyManagementElement.getChild(DEPENDENCIES, namespace);
222 if (managedDependenciesElement != null) {
223 fixed |= fixDuplicateDependenciesInSection(
224 managedDependenciesElement, namespace, context, DEPENDENCY_MANAGEMENT);
225 }
226 }
227
228
229 Element profilesElement = root.getChild(PROFILES, namespace);
230 if (profilesElement != null) {
231 List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
232 for (Element profileElement : profileElements) {
233 Element profileDependencies = profileElement.getChild(DEPENDENCIES, namespace);
234 if (profileDependencies != null) {
235 fixed |= fixDuplicateDependenciesInSection(
236 profileDependencies, namespace, context, "profile dependencies");
237 }
238
239 Element profileDepMgmt = profileElement.getChild(DEPENDENCY_MANAGEMENT, namespace);
240 if (profileDepMgmt != null) {
241 Element profileManagedDeps = profileDepMgmt.getChild(DEPENDENCIES, namespace);
242 if (profileManagedDeps != null) {
243 fixed |= fixDuplicateDependenciesInSection(
244 profileManagedDeps, namespace, context, "profile dependencyManagement");
245 }
246 }
247 }
248 }
249
250 return fixed;
251 }
252
253
254
255
256 private boolean fixDuplicatePlugins(Document pomDocument, UpgradeContext context) {
257 Element root = pomDocument.getRootElement();
258 Namespace namespace = root.getNamespace();
259 boolean fixed = false;
260
261
262 Element buildElement = root.getChild(BUILD, namespace);
263 if (buildElement != null) {
264 fixed |= fixPluginsInBuildElement(buildElement, namespace, context, BUILD);
265 }
266
267
268 Element profilesElement = root.getChild(PROFILES, namespace);
269 if (profilesElement != null) {
270 for (Element profileElement : profilesElement.getChildren(PROFILE, namespace)) {
271 Element profileBuildElement = profileElement.getChild(BUILD, namespace);
272 if (profileBuildElement != null) {
273 fixed |= fixPluginsInBuildElement(profileBuildElement, namespace, context, "profile build");
274 }
275 }
276 }
277
278 return fixed;
279 }
280
281
282
283
284 private boolean fixUnsupportedRepositoryExpressions(Document pomDocument, UpgradeContext context) {
285 Element root = pomDocument.getRootElement();
286 Namespace namespace = root.getNamespace();
287 boolean fixed = false;
288
289
290 fixed |= fixRepositoryExpressions(root.getChild(REPOSITORIES, namespace), namespace, context);
291
292
293 fixed |= fixRepositoryExpressions(root.getChild(PLUGIN_REPOSITORIES, namespace), namespace, context);
294
295
296 Element profilesElement = root.getChild(PROFILES, namespace);
297 if (profilesElement != null) {
298 List<Element> profileElements = profilesElement.getChildren(PROFILE, namespace);
299 for (Element profileElement : profileElements) {
300 fixed |= fixRepositoryExpressions(profileElement.getChild(REPOSITORIES, namespace), namespace, context);
301 fixed |= fixRepositoryExpressions(
302 profileElement.getChild(PLUGIN_REPOSITORIES, namespace), namespace, context);
303 }
304 }
305
306 return fixed;
307 }
308
309
310
311
312 private boolean fixIncorrectParentRelativePaths(
313 Document pomDocument, Path pomPath, Map<Path, Document> pomMap, UpgradeContext context) {
314 Element root = pomDocument.getRootElement();
315 Namespace namespace = root.getNamespace();
316
317 Element parentElement = root.getChild(PARENT, namespace);
318 if (parentElement == null) {
319 return false;
320 }
321
322 Element relativePathElement = parentElement.getChild(RELATIVE_PATH, namespace);
323 String currentRelativePath =
324 relativePathElement != null ? relativePathElement.getTextTrim() : DEFAULT_PARENT_RELATIVE_PATH;
325
326
327 String parentGroupId = getChildText(parentElement, GROUP_ID, namespace);
328 String parentArtifactId = getChildText(parentElement, ARTIFACT_ID, namespace);
329 String parentVersion = getChildText(parentElement, VERSION, namespace);
330
331 Path correctParentPath = findParentPomInMap(context, parentGroupId, parentArtifactId, parentVersion, pomMap);
332 if (correctParentPath != null) {
333 try {
334 Path correctRelativePath = pomPath.getParent().relativize(correctParentPath);
335 String correctRelativePathStr = correctRelativePath.toString().replace('\\', '/');
336
337 if (!correctRelativePathStr.equals(currentRelativePath)) {
338
339 if (relativePathElement == null) {
340 relativePathElement = new Element(RELATIVE_PATH, namespace);
341 Element insertAfter = parentElement.getChild(VERSION, namespace);
342 if (insertAfter == null) {
343 insertAfter = parentElement.getChild(ARTIFACT_ID, namespace);
344 }
345 if (insertAfter != null) {
346 parentElement.addContent(parentElement.indexOf(insertAfter) + 1, relativePathElement);
347 } else {
348 parentElement.addContent(relativePathElement);
349 }
350 }
351 relativePathElement.setText(correctRelativePathStr);
352 context.detail("Fixed: " + "relativePath corrected from '" + currentRelativePath + "' to '"
353 + correctRelativePathStr + "'");
354 return true;
355 }
356 } catch (Exception e) {
357 context.failure("Failed to compute correct relativePath" + ": " + e.getMessage());
358 }
359 }
360
361 return false;
362 }
363
364
365
366
367 private List<Element> findElementsWithAttribute(Element element, String attributeName, String attributeValue) {
368 List<Element> result = new ArrayList<>();
369
370
371 Attribute attr = element.getAttribute(attributeName);
372 if (attr != null && attributeValue.equals(attr.getValue())) {
373 result.add(element);
374 }
375
376
377 for (Element child : element.getChildren()) {
378 result.addAll(findElementsWithAttribute(child, attributeName, attributeValue));
379 }
380
381 return result;
382 }
383
384
385
386
387 private boolean fixDuplicateDependenciesInSection(
388 Element dependenciesElement, Namespace namespace, UpgradeContext context, String sectionName) {
389 boolean fixed = false;
390 List<Element> dependencies = dependenciesElement.getChildren(DEPENDENCY, namespace);
391 Map<String, Element> seenDependencies = new HashMap<>();
392 List<Element> toRemove = new ArrayList<>();
393
394 for (Element dependency : dependencies) {
395 String groupId = getChildText(dependency, GROUP_ID, namespace);
396 String artifactId = getChildText(dependency, ARTIFACT_ID, namespace);
397 String type = getChildText(dependency, TYPE, namespace);
398 String classifier = getChildText(dependency, CLASSIFIER, namespace);
399
400
401 String key = groupId + ":" + artifactId + ":" + (type != null ? type : "jar") + ":"
402 + (classifier != null ? classifier : "");
403
404 if (seenDependencies.containsKey(key)) {
405
406 toRemove.add(dependency);
407 context.detail("Fixed: " + "Removed duplicate dependency: " + key + " in " + sectionName);
408 fixed = true;
409 } else {
410 seenDependencies.put(key, dependency);
411 }
412 }
413
414
415 for (Element duplicate : toRemove) {
416 removeElementWithFormatting(duplicate);
417 }
418
419 return fixed;
420 }
421
422 private boolean fixPluginsInBuildElement(
423 Element buildElement, Namespace namespace, UpgradeContext context, String sectionName) {
424 boolean fixed = false;
425
426 Element pluginsElement = buildElement.getChild(PLUGINS, namespace);
427 if (pluginsElement != null) {
428 fixed |= fixDuplicatePluginsInSection(pluginsElement, namespace, context, sectionName + "/" + PLUGINS);
429 }
430
431 Element pluginManagementElement = buildElement.getChild(PLUGIN_MANAGEMENT, namespace);
432 if (pluginManagementElement != null) {
433 Element managedPluginsElement = pluginManagementElement.getChild(PLUGINS, namespace);
434 if (managedPluginsElement != null) {
435 fixed |= fixDuplicatePluginsInSection(
436 managedPluginsElement,
437 namespace,
438 context,
439 sectionName + "/" + PLUGIN_MANAGEMENT + "/" + PLUGINS);
440 }
441 }
442
443 return fixed;
444 }
445
446
447
448
449 private boolean fixDuplicatePluginsInSection(
450 Element pluginsElement, Namespace namespace, UpgradeContext context, String sectionName) {
451 boolean fixed = false;
452 List<Element> plugins = pluginsElement.getChildren(PLUGIN, namespace);
453 Map<String, Element> seenPlugins = new HashMap<>();
454 List<Element> toRemove = new ArrayList<>();
455
456 for (Element plugin : plugins) {
457 String groupId = getChildText(plugin, GROUP_ID, namespace);
458 String artifactId = getChildText(plugin, ARTIFACT_ID, namespace);
459
460
461 if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
462 groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
463 }
464
465 if (groupId != null && artifactId != null) {
466
467 String key = groupId + ":" + artifactId;
468
469 if (seenPlugins.containsKey(key)) {
470
471 toRemove.add(plugin);
472 context.detail("Fixed: " + "Removed duplicate plugin: " + key + " in " + sectionName);
473 fixed = true;
474 } else {
475 seenPlugins.put(key, plugin);
476 }
477 }
478 }
479
480
481 for (Element duplicate : toRemove) {
482 removeElementWithFormatting(duplicate);
483 }
484
485 return fixed;
486 }
487
488 private boolean fixRepositoryExpressions(Element repositoriesElement, Namespace namespace, UpgradeContext context) {
489 if (repositoriesElement == null) {
490 return false;
491 }
492
493 boolean fixed = false;
494 String elementType = repositoriesElement.getName().equals(REPOSITORIES) ? REPOSITORY : PLUGIN_REPOSITORY;
495 List<Element> repositories = repositoriesElement.getChildren(elementType, namespace);
496
497 for (Element repository : repositories) {
498 Element urlElement = repository.getChild("url", namespace);
499 if (urlElement != null) {
500 String url = urlElement.getTextTrim();
501 if (url.contains("${")
502 && !url.contains("${project.basedir}")
503 && !url.contains("${project.rootDirectory}")) {
504 String repositoryId = getChildText(repository, "id", namespace);
505 context.warning("Found unsupported expression in " + elementType + " URL (id: " + repositoryId
506 + "): " + url);
507 context.warning(
508 "Maven 4 only supports ${project.basedir} and ${project.rootDirectory} expressions in repository URLs");
509
510
511 Comment comment =
512 new Comment(" Repository disabled due to unsupported expression in URL: " + url + " ");
513 Element parent = repository.getParentElement();
514 parent.addContent(parent.indexOf(repository), comment);
515 removeElementWithFormatting(repository);
516
517 context.detail("Fixed: " + "Commented out " + elementType + " with unsupported URL expression (id: "
518 + repositoryId + ")");
519 fixed = true;
520 }
521 }
522 }
523
524 return fixed;
525 }
526
527 private Path findParentPomInMap(
528 UpgradeContext context, String groupId, String artifactId, String version, Map<Path, Document> pomMap) {
529 return pomMap.entrySet().stream()
530 .filter(entry -> {
531 GAV gav = GAVUtils.extractGAVWithParentResolution(context, entry.getValue());
532 return gav != null
533 && Objects.equals(gav.groupId(), groupId)
534 && Objects.equals(gav.artifactId(), artifactId)
535 && (version == null || Objects.equals(gav.version(), version));
536 })
537 .map(Map.Entry::getKey)
538 .findFirst()
539 .orElse(null);
540 }
541
542 private String getChildText(Element parent, String elementName, Namespace namespace) {
543 Element element = parent.getChild(elementName, namespace);
544 return element != null ? element.getTextTrim() : null;
545 }
546
547
548
549
550 private void removeElementWithFormatting(Element element) {
551 Element parent = element.getParentElement();
552 if (parent != null) {
553 int index = parent.indexOf(element);
554
555
556 parent.removeContent(element);
557
558
559 if (index > 0) {
560 Content prevContent = parent.getContent(index - 1);
561 if (prevContent instanceof Text textContent) {
562 String text = textContent.getText();
563
564 if (text.trim().isEmpty() && text.contains("\n")) {
565 parent.removeContent(prevContent);
566 }
567 }
568 }
569 }
570 }
571 }