View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * Strategy for applying Maven 4 compatibility fixes to POM files.
73   * Fixes issues that prevent POMs from being processed by Maven 4.
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          // Handle --all option (overrides individual options)
85          boolean useAll = options.all().orElse(false);
86          if (useAll) {
87              return true;
88          }
89  
90          // Apply default behavior: if no specific options are provided, enable --model
91          // OR if all options are explicitly disabled, still apply default behavior
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         // Check if --model is explicitly set (and not part of "all disabled" scenario)
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                 // Apply all compatibility fixes
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      * Fixes unsupported combine.children attribute values.
165      * Maven 4 only supports 'append' and 'merge', not 'override'.
166      */
167     private boolean fixUnsupportedCombineChildrenAttributes(Document pomDocument, UpgradeContext context) {
168         boolean fixed = false;
169         Element root = pomDocument.getRootElement();
170 
171         // Find all elements with combine.children="override" and change to "merge"
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      * Fixes unsupported combine.self attribute values.
185      * Maven 4 only supports 'override', 'merge', and 'remove' (default is merge), not 'append'.
186      */
187     private boolean fixUnsupportedCombineSelfAttributes(Document pomDocument, UpgradeContext context) {
188         boolean fixed = false;
189         Element root = pomDocument.getRootElement();
190 
191         // Find all elements with combine.self="append" and change to "merge"
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      * Fixes duplicate dependencies in dependencies and dependencyManagement sections.
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         // Fix main dependencies
212         Element dependenciesElement = root.getChild(DEPENDENCIES, namespace);
213         if (dependenciesElement != null) {
214             fixed |= fixDuplicateDependenciesInSection(dependenciesElement, namespace, context, DEPENDENCIES);
215         }
216 
217         // Fix dependencyManagement
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         // Fix profile dependencies
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      * Fixes duplicate plugins in plugins and pluginManagement sections.
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         // Fix build plugins
261         Element buildElement = root.getChild(BUILD, namespace);
262         if (buildElement != null) {
263             fixed |= fixPluginsInBuildElement(buildElement, namespace, context, BUILD);
264         }
265 
266         // Fix profile plugins
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      * Fixes unsupported repository URL expressions.
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         // Fix repositories
289         fixed |= fixRepositoryExpressions(root.getChild(REPOSITORIES, namespace), namespace, context);
290 
291         // Fix pluginRepositories
292         fixed |= fixRepositoryExpressions(root.getChild(PLUGIN_REPOSITORIES, namespace), namespace, context);
293 
294         // Fix repositories and pluginRepositories in profiles
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      * Fixes incorrect parent relative paths.
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; // No parent to fix
319         }
320 
321         Element relativePathElement = parentElement.getChild(RELATIVE_PATH, namespace);
322         String currentRelativePath =
323                 relativePathElement != null ? relativePathElement.getTextTrim() : DEFAULT_PARENT_RELATIVE_PATH;
324 
325         // Try to find the correct parent POM
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                     // Update relativePath element
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      * Recursively finds all elements with a specific attribute value.
365      */
366     private List<Element> findElementsWithAttribute(Element element, String attributeName, String attributeValue) {
367         List<Element> result = new ArrayList<>();
368 
369         // Check current element
370         Attribute attr = element.getAttribute(attributeName);
371         if (attr != null && attributeValue.equals(attr.getValue())) {
372             result.add(element);
373         }
374 
375         // Recursively check children
376         for (Element child : element.getChildren()) {
377             result.addAll(findElementsWithAttribute(child, attributeName, attributeValue));
378         }
379 
380         return result;
381     }
382 
383     /**
384      * Helper methods extracted from BaseUpgradeGoal for compatibility fixes.
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             // Create a key for uniqueness check
400             String key = groupId + ":" + artifactId + ":" + (type != null ? type : "jar") + ":"
401                     + (classifier != null ? classifier : "");
402 
403             if (seenDependencies.containsKey(key)) {
404                 // Found duplicate - remove it
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         // Remove duplicates while preserving formatting
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      * Fixes duplicate plugins within a specific plugins section.
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             // Default groupId for Maven plugins
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                 // Create a key for uniqueness check (groupId:artifactId)
466                 String key = groupId + ":" + artifactId;
467 
468                 if (seenPlugins.containsKey(key)) {
469                     // Found duplicate - remove it
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         // Remove duplicates while preserving formatting
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                     // Allow repository URL interpolation; do not disable.
502                     // Keep a gentle warning to help users notice unresolved placeholders at build time.
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      * Removes an element while preserving formatting by also removing preceding whitespace.
535      */
536     private void removeElementWithFormatting(Element element) {
537         Element parent = element.getParentElement();
538         if (parent != null) {
539             int index = parent.indexOf(element);
540 
541             // Remove the element
542             parent.removeContent(element);
543 
544             // Try to remove preceding whitespace/newline
545             if (index > 0) {
546                 Content prevContent = parent.getContent(index - 1);
547                 if (prevContent instanceof Text textContent) {
548                     String text = textContent.getText();
549                     // If it's just whitespace and newlines, remove it
550                     if (text.trim().isEmpty() && text.contains("\n")) {
551                         parent.removeContent(prevContent);
552                     }
553                 }
554             }
555         }
556     }
557 }