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