CompatibilityFixStrategy.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.cling.invoker.mvnup.goals;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Element;
import eu.maveniverse.domtrip.maven.Coordinates;
import eu.maveniverse.domtrip.maven.MavenPomElements;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_APPEND;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_CHILDREN;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_MERGE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_OVERRIDE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Attributes.COMBINE_SELF;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.DEPENDENCY_MANAGEMENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PARENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_REPOSITORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.RELATIVE_PATH;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORIES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.REPOSITORY;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.DEFAULT_PARENT_RELATIVE_PATH;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.MAVEN_PLUGIN_PREFIX;
/**
* Strategy for applying Maven 4 compatibility fixes to POM files.
* Fixes issues that prevent POMs from being processed by Maven 4.
*/
@Named
@Singleton
@Priority(20)
public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
// Handle --all option (overrides individual options)
boolean useAll = options.all().orElse(false);
if (useAll) {
return true;
}
// Apply default behavior: if no specific options are provided, enable --model
// OR if all options are explicitly disabled, still apply default behavior
boolean noOptionsSpecified = options.all().isEmpty()
&& options.infer().isEmpty()
&& options.model().isEmpty()
&& options.plugins().isEmpty()
&& options.modelVersion().isEmpty();
boolean allOptionsDisabled = options.all().map(v -> !v).orElse(false)
&& options.infer().map(v -> !v).orElse(false)
&& options.model().map(v -> !v).orElse(false)
&& options.plugins().map(v -> !v).orElse(false)
&& options.modelVersion().isEmpty();
if (noOptionsSpecified || allOptionsDisabled) {
return true;
}
// Check if --model is explicitly set (and not part of "all disabled" scenario)
if (options.model().isPresent()) {
return options.model().get();
}
return false;
}
@Override
public String getDescription() {
return "Applying Maven 4 compatibility fixes";
}
@Override
public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
Set<Path> processedPoms = new HashSet<>();
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
context.info(pomPath + " (checking for Maven 4 compatibility issues)");
context.indent();
try {
boolean hasIssues = false;
// Apply all compatibility fixes
hasIssues |= fixUnsupportedCombineChildrenAttributes(pomDocument, context);
hasIssues |= fixUnsupportedCombineSelfAttributes(pomDocument, context);
hasIssues |= fixDuplicateDependencies(pomDocument, context);
hasIssues |= fixDuplicatePlugins(pomDocument, context);
hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, context);
hasIssues |= fixIncorrectParentRelativePaths(pomDocument, pomPath, pomMap, context);
if (hasIssues) {
context.success("Maven 4 compatibility issues fixed");
modifiedPoms.add(pomPath);
} else {
context.success("No Maven 4 compatibility issues found");
}
} catch (Exception e) {
context.failure("Failed to fix Maven 4 compatibility issues" + ": " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Fixes unsupported combine.children attribute values.
* Maven 4 only supports 'append' and 'merge', not 'override'.
*/
private boolean fixUnsupportedCombineChildrenAttributes(Document pomDocument, UpgradeContext context) {
boolean fixed = false;
Element root = pomDocument.root();
// Find all elements with combine.children="override" and change to "merge"
long fixedCombineChildrenCount = findElementsWithAttribute(root, COMBINE_CHILDREN, COMBINE_OVERRIDE)
.peek(element -> {
element.attributeObject(COMBINE_CHILDREN).value(COMBINE_MERGE);
context.detail("Fixed: " + COMBINE_CHILDREN + "='" + COMBINE_OVERRIDE + "' ��� '" + COMBINE_MERGE
+ "' in " + element.name());
})
.count();
fixed |= fixedCombineChildrenCount > 0;
return fixed;
}
/**
* Fixes unsupported combine.self attribute values.
* Maven 4 only supports 'override', 'merge', and 'remove' (default is merge), not 'append'.
*/
private boolean fixUnsupportedCombineSelfAttributes(Document pomDocument, UpgradeContext context) {
boolean fixed = false;
Element root = pomDocument.root();
// Find all elements with combine.self="append" and change to "merge"
long fixedCombineSelfCount = findElementsWithAttribute(root, COMBINE_SELF, COMBINE_APPEND)
.peek(element -> {
element.attributeObject(COMBINE_SELF).value(COMBINE_MERGE);
context.detail("Fixed: " + COMBINE_SELF + "='" + COMBINE_APPEND + "' ��� '" + COMBINE_MERGE + "' in "
+ element.name());
})
.count();
fixed |= fixedCombineSelfCount > 0;
return fixed;
}
/**
* Fixes duplicate dependencies in dependencies and dependencyManagement sections.
*/
private boolean fixDuplicateDependencies(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
// Collect all dependency containers to process
Stream<DependencyContainer> dependencyContainers = Stream.concat(
// Root level dependencies
Stream.of(
new DependencyContainer(root.child(DEPENDENCIES).orElse(null), DEPENDENCIES),
new DependencyContainer(
root.child(DEPENDENCY_MANAGEMENT)
.flatMap(dm -> dm.child(DEPENDENCIES))
.orElse(null),
DEPENDENCY_MANAGEMENT))
.filter(container -> container.element != null),
// Profile dependencies
root.child(PROFILES).stream()
.flatMap(profiles -> profiles.children(PROFILE))
.flatMap(profile -> Stream.of(
new DependencyContainer(
profile.child(DEPENDENCIES).orElse(null), "profile dependencies"),
new DependencyContainer(
profile.child(DEPENDENCY_MANAGEMENT)
.flatMap(dm -> dm.child(DEPENDENCIES))
.orElse(null),
"profile dependencyManagement"))
.filter(container -> container.element != null)));
return dependencyContainers
.map(container -> fixDuplicateDependenciesInSection(container.element, context, container.sectionName))
.reduce(false, Boolean::logicalOr);
}
private static class DependencyContainer {
final Element element;
final String sectionName;
DependencyContainer(Element element, String sectionName) {
this.element = element;
this.sectionName = sectionName;
}
}
/**
* Fixes duplicate plugins in plugins and pluginManagement sections.
*/
private boolean fixDuplicatePlugins(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
// Collect all build elements to process
Stream<BuildContainer> buildContainers = Stream.concat(
// Root level build
Stream.of(new BuildContainer(root.child(BUILD).orElse(null), BUILD))
.filter(container -> container.element != null),
// Profile builds
root.child(PROFILES).stream()
.flatMap(profiles -> profiles.children(PROFILE))
.map(profile -> new BuildContainer(profile.child(BUILD).orElse(null), "profile build"))
.filter(container -> container.element != null));
return buildContainers
.map(container -> fixPluginsInBuildElement(container.element, context, container.sectionName))
.reduce(false, Boolean::logicalOr);
}
private static class BuildContainer {
final Element element;
final String sectionName;
BuildContainer(Element element, String sectionName) {
this.element = element;
this.sectionName = sectionName;
}
}
/**
* Fixes unsupported repository URL expressions.
*/
private boolean fixUnsupportedRepositoryExpressions(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
// Collect all repository containers to process
Stream<Element> repositoryContainers = Stream.concat(
// Root level repositories
Stream.of(
root.child(REPOSITORIES).orElse(null),
root.child(PLUGIN_REPOSITORIES).orElse(null))
.filter(Objects::nonNull),
// Profile repositories
root.child(PROFILES).stream()
.flatMap(profiles -> profiles.children(PROFILE))
.flatMap(profile -> Stream.of(
profile.child(REPOSITORIES).orElse(null),
profile.child(PLUGIN_REPOSITORIES).orElse(null))
.filter(Objects::nonNull)));
return repositoryContainers
.map(container -> fixRepositoryExpressions(container, pomDocument, context))
.reduce(false, Boolean::logicalOr);
}
/**
* Fixes incorrect parent relative paths.
*/
private boolean fixIncorrectParentRelativePaths(
Document pomDocument, Path pomPath, Map<Path, Document> pomMap, UpgradeContext context) {
Element root = pomDocument.root();
Element parentElement = root.child(PARENT).orElse(null);
if (parentElement == null) {
return false; // No parent to fix
}
Element relativePathElement = parentElement.child(RELATIVE_PATH).orElse(null);
String currentRelativePath =
relativePathElement != null ? relativePathElement.textContent().trim() : DEFAULT_PARENT_RELATIVE_PATH;
// Try to find the correct parent POM
String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
String parentArtifactId = parentElement.childText(MavenPomElements.Elements.ARTIFACT_ID);
String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);
Path correctParentPath = findParentPomInMap(context, parentGroupId, parentArtifactId, parentVersion, pomMap);
if (correctParentPath != null) {
try {
Path correctRelativePath = pomPath.getParent().relativize(correctParentPath);
String correctRelativePathStr = correctRelativePath.toString().replace('\\', '/');
if (!correctRelativePathStr.equals(currentRelativePath)) {
// Update or create relativePath element using DomUtils convenience method
DomUtils.updateOrCreateChildElement(parentElement, RELATIVE_PATH, correctRelativePathStr);
context.detail("Fixed: " + "relativePath corrected from '" + currentRelativePath + "' to '"
+ correctRelativePathStr + "'");
return true;
}
} catch (Exception e) {
context.failure("Failed to compute correct relativePath" + ": " + e.getMessage());
}
}
return false;
}
/**
* Recursively finds all elements with a specific attribute value.
*/
private Stream<Element> findElementsWithAttribute(Element element, String attributeName, String attributeValue) {
return Stream.concat(
// Check current element
Stream.of(element).filter(e -> {
String attr = e.attribute(attributeName);
return attr != null && attributeValue.equals(attr);
}),
// Recursively check children
element.children().flatMap(child -> findElementsWithAttribute(child, attributeName, attributeValue)));
}
/**
* Helper methods extracted from BaseUpgradeGoal for compatibility fixes.
*/
private boolean fixDuplicateDependenciesInSection(
Element dependenciesElement, UpgradeContext context, String sectionName) {
List<Element> dependencies = dependenciesElement.children(DEPENDENCY).toList();
Map<String, Element> seenDependencies = new HashMap<>();
List<Element> duplicates = dependencies.stream()
.filter(dependency -> {
String key = createDependencyKey(dependency);
if (seenDependencies.containsKey(key)) {
context.detail("Fixed: Removed duplicate dependency: " + key + " in " + sectionName);
return true; // This is a duplicate
} else {
seenDependencies.put(key, dependency);
return false; // This is the first occurrence
}
})
.toList();
// Remove duplicates while preserving formatting
duplicates.forEach(DomUtils::removeElement);
return !duplicates.isEmpty();
}
private String createDependencyKey(Element dependency) {
String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
String type = dependency.childText(MavenPomElements.Elements.TYPE);
String classifier = dependency.childText(MavenPomElements.Elements.CLASSIFIER);
return groupId + ":" + artifactId + ":" + (type != null ? type : "jar") + ":"
+ (classifier != null ? classifier : "");
}
private boolean fixPluginsInBuildElement(Element buildElement, UpgradeContext context, String sectionName) {
boolean fixed = false;
Element pluginsElement = buildElement.child(PLUGINS).orElse(null);
if (pluginsElement != null) {
fixed |= fixDuplicatePluginsInSection(pluginsElement, context, sectionName + "/" + PLUGINS);
}
Element pluginManagementElement = buildElement.child(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagementElement != null) {
Element managedPluginsElement =
pluginManagementElement.child(PLUGINS).orElse(null);
if (managedPluginsElement != null) {
fixed |= fixDuplicatePluginsInSection(
managedPluginsElement, context, sectionName + "/" + PLUGIN_MANAGEMENT + "/" + PLUGINS);
}
}
return fixed;
}
/**
* Fixes duplicate plugins within a specific plugins section.
*/
private boolean fixDuplicatePluginsInSection(Element pluginsElement, UpgradeContext context, String sectionName) {
List<Element> plugins = pluginsElement.children(PLUGIN).toList();
Map<String, Element> seenPlugins = new HashMap<>();
List<Element> duplicates = plugins.stream()
.filter(plugin -> {
String key = createPluginKey(plugin);
if (key != null) {
if (seenPlugins.containsKey(key)) {
context.detail("Fixed: Removed duplicate plugin: " + key + " in " + sectionName);
return true; // This is a duplicate
} else {
seenPlugins.put(key, plugin);
}
}
return false; // This is the first occurrence or invalid plugin
})
.toList();
// Remove duplicates while preserving formatting
duplicates.forEach(DomUtils::removeElement);
return !duplicates.isEmpty();
}
private String createPluginKey(Element plugin) {
String groupId = plugin.childText(MavenPomElements.Elements.GROUP_ID);
String artifactId = plugin.childText(MavenPomElements.Elements.ARTIFACT_ID);
// Default groupId for Maven plugins
if (groupId == null && artifactId != null && artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
}
return (groupId != null && artifactId != null) ? groupId + ":" + artifactId : null;
}
private boolean fixRepositoryExpressions(
Element repositoriesElement, Document pomDocument, UpgradeContext context) {
if (repositoriesElement == null) {
return false;
}
boolean fixed = false;
String elementType = repositoriesElement.name().equals(REPOSITORIES) ? REPOSITORY : PLUGIN_REPOSITORY;
List<Element> repositories = repositoriesElement.children(elementType).toList();
for (Element repository : repositories) {
Element urlElement = repository.child("url").orElse(null);
if (urlElement != null) {
String url = urlElement.textContent().trim();
if (url.contains("${")) {
// Allow repository URL interpolation; do not disable.
// Keep a gentle warning to help users notice unresolved placeholders at build time.
String repositoryId = repository.childText("id");
context.info("Detected interpolated expression in " + elementType + " URL (id: " + repositoryId
+ "): " + url);
}
}
}
return fixed;
}
private Path findParentPomInMap(
UpgradeContext context, String groupId, String artifactId, String version, Map<Path, Document> pomMap) {
return pomMap.entrySet().stream()
.filter(entry -> {
Coordinates gav = AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(
context, entry.getValue());
return gav != null
&& Objects.equals(gav.groupId(), groupId)
&& Objects.equals(gav.artifactId(), artifactId)
&& (version == null || Objects.equals(gav.version(), version));
})
.findFirst()
.map(Map.Entry::getKey)
.orElse(null);
}
}