InferenceStrategy.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.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
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.Elements.ARTIFACT_ID;
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.GROUP_ID;
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.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SUBPROJECTS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.VERSION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.DEFAULT_PARENT_RELATIVE_PATH;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Files.POM_XML;
import static eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_1_0;
/**
* Strategy for applying Maven inference optimizations.
* For 4.0.0 models: applies limited inference (parent-related only).
* For 4.1.0+ models: applies full inference optimizations.
* Removes redundant information that can be inferred by Maven during model building.
*/
@Named
@Singleton
@Priority(30)
public class InferenceStrategy 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;
}
// Check if --infer is explicitly set
if (options.infer().isPresent()) {
return options.infer().get();
}
// Apply default behavior: if no specific options are provided, enable --infer
if (options.infer().isEmpty()
&& options.model().isEmpty()
&& options.plugins().isEmpty()
&& options.modelVersion().isEmpty()) {
return true;
}
return false;
}
@Override
public String getDescription() {
return "Applying Maven inference optimizations";
}
@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<>();
// Compute all GAVs for inference
Set<Coordinates> allGAVs = computeAllArtifactCoordinates(context, pomMap);
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
context.info(pomPath + " (current: " + currentVersion + ")");
context.indent();
try {
if (!ModelVersionUtils.isEligibleForInference(currentVersion)) {
context.warning(
"Model version " + currentVersion + " not eligible for inference (requires >= 4.0.0)");
continue;
}
boolean hasInferences = false;
// Apply limited parent inference for all eligible models (4.0.0+)
hasInferences |= applyLimitedParentInference(context, pomDocument);
// Apply full inference optimizations only for 4.1.0+ models
if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
hasInferences |= applyFullParentInference(context, pomMap, pomDocument);
hasInferences |= applyDependencyInference(context, allGAVs, pomDocument);
hasInferences |= applyDependencyInferenceRedundancy(context, pomMap, pomDocument);
hasInferences |= applySubprojectsInference(context, pomDocument, pomPath);
hasInferences |= applyModelVersionInference(context, pomDocument);
}
if (hasInferences) {
modifiedPoms.add(pomPath);
if (MODEL_VERSION_4_1_0.equals(currentVersion)
|| ModelVersionUtils.isNewerThan410(currentVersion)) {
context.success("Full inference optimizations applied");
} else {
context.success("Limited inference optimizations applied (parent-related only)");
}
} else {
context.success("No inference optimizations needed");
}
} catch (Exception e) {
context.failure("Failed to apply inference optimizations" + ": " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Applies limited parent-related inference optimizations for Maven 4.0.0+ models.
* Removes redundant child groupId/version that can be inferred from parent.
*/
private boolean applyLimitedParentInference(UpgradeContext context, Document pomDocument) {
Element root = pomDocument.root();
// Check if this POM has a parent
Element parentElement = root.child(PARENT).orElse(null);
if (parentElement == null) {
return false;
}
// Apply limited inference (child groupId/version removal only)
return trimParentElementLimited(context, root, parentElement);
}
/**
* Applies full parent-related inference optimizations for Maven 4.1.0+ models.
* Removes redundant parent elements that can be inferred from relativePath.
*/
private boolean applyFullParentInference(UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
Element root = pomDocument.root();
// Check if this POM has a parent
Element parentElement = root.child(PARENT).orElse(null);
if (parentElement == null) {
return false;
}
// Apply full inference (parent element trimming based on relativePath)
return trimParentElementFull(context, root, parentElement, pomMap);
}
/**
* Applies dependency-related inference optimizations.
* Removes managed dependencies that point to project artifacts.
*/
private boolean applyDependencyInference(UpgradeContext context, Set<Coordinates> allGAVs, Document pomDocument) {
boolean hasChanges = false;
Element root = pomDocument.root();
// Check dependencyManagement section
Element dependencyManagement = root.child(DEPENDENCY_MANAGEMENT).orElse(null);
if (dependencyManagement != null) {
Element dependencies = dependencyManagement.child(DEPENDENCIES).orElse(null);
if (dependencies != null) {
hasChanges |=
removeManagedDependenciesFromSection(context, dependencies, allGAVs, DEPENDENCY_MANAGEMENT);
}
}
// Check profiles for dependencyManagement
boolean profileChanges = root.child(PROFILES).stream()
.flatMap(profiles -> profiles.children(PROFILE))
.map(profile -> profile.child(DEPENDENCY_MANAGEMENT)
.flatMap(dm -> dm.child(DEPENDENCIES))
.map(deps -> removeManagedDependenciesFromSection(
context, deps, allGAVs, "profile dependencyManagement"))
.orElse(false))
.reduce(false, Boolean::logicalOr);
hasChanges |= profileChanges;
return hasChanges;
}
/**
* Applies dependency inference redundancy optimizations.
* Removes redundant groupId/version from regular dependencies that can be inferred from project artifacts.
*/
private boolean applyDependencyInferenceRedundancy(
UpgradeContext context, Map<Path, Document> pomMap, Document pomDocument) {
Element root = pomDocument.root();
boolean hasChanges = false;
// Process main dependencies
Element dependenciesElement = root.child(DEPENDENCIES).orElse(null);
if (dependenciesElement != null) {
hasChanges |= removeDependencyInferenceFromSection(context, dependenciesElement, pomMap, DEPENDENCIES);
}
// Process profile dependencies
boolean profileDependencyChanges = root.child(PROFILES).stream()
.flatMap(profiles -> profiles.children(PROFILE))
.map(profile -> profile.child(DEPENDENCIES)
.map(deps ->
removeDependencyInferenceFromSection(context, deps, pomMap, "profile dependencies"))
.orElse(false))
.reduce(false, Boolean::logicalOr);
hasChanges |= profileDependencyChanges;
// Process build plugin dependencies
boolean pluginDependencyChanges = root.child(BUILD).flatMap(build -> build.child(PLUGINS)).stream()
.flatMap(plugins -> plugins.children(PLUGIN))
.map(plugin -> plugin.child(DEPENDENCIES)
.map(deps -> removeDependencyInferenceFromSection(context, deps, pomMap, "plugin dependencies"))
.orElse(false))
.reduce(false, Boolean::logicalOr);
hasChanges |= pluginDependencyChanges;
return hasChanges;
}
/**
* Applies subprojects-related inference optimizations.
* Removes redundant subprojects lists that match direct children.
*/
private boolean applySubprojectsInference(UpgradeContext context, Document pomDocument, Path pomPath) {
boolean hasChanges = false;
Element root = pomDocument.root();
// Check main subprojects
Element subprojectsElement = root.child(SUBPROJECTS).orElse(null);
if (subprojectsElement != null) {
if (isSubprojectsListRedundant(subprojectsElement, pomPath)) {
DomUtils.removeElement(subprojectsElement);
context.detail("Removed: redundant subprojects list (matches direct children)");
hasChanges = true;
}
}
// Check profiles for subprojects
boolean profileSubprojectsChanges = root.child(PROFILES).stream()
.flatMap(profiles -> profiles.children(PROFILE))
.map(profile -> profile.child(SUBPROJECTS)
.filter(subprojects -> isSubprojectsListRedundant(subprojects, pomPath))
.map(subprojects -> {
DomUtils.removeElement(subprojects);
context.detail(
"Removed: redundant subprojects list from profile (matches direct children)");
return true;
})
.orElse(false))
.reduce(false, Boolean::logicalOr);
hasChanges |= profileSubprojectsChanges;
return hasChanges;
}
/**
* Applies model version inference optimization.
* Removes modelVersion element when it can be inferred from namespace.
*/
private boolean applyModelVersionInference(UpgradeContext context, Document pomDocument) {
String currentVersion = ModelVersionUtils.detectModelVersion(pomDocument);
// Only remove modelVersion for 4.1.0+ models where it can be inferred from namespace
if (MODEL_VERSION_4_1_0.equals(currentVersion) || ModelVersionUtils.isNewerThan410(currentVersion)) {
if (ModelVersionUtils.removeModelVersion(pomDocument)) {
context.detail("Removed: modelVersion element (can be inferred from namespace)");
return true;
}
}
return false;
}
/**
* Applies limited parent inference for 4.0.0 models.
* Only removes child groupId/version when they match parent.
*/
private boolean trimParentElementLimited(UpgradeContext context, Element root, Element parentElement) {
boolean hasChanges = false;
// Get parent GAV
String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);
// Get child GAV
String childGroupId = root.childText(MavenPomElements.Elements.GROUP_ID);
String childVersion = root.childText(MavenPomElements.Elements.VERSION);
// Remove child groupId if it matches parent groupId
if (childGroupId != null && Objects.equals(childGroupId, parentGroupId)) {
Element childGroupIdElement = root.child(GROUP_ID).orElse(null);
if (childGroupIdElement != null) {
DomUtils.removeElement(childGroupIdElement);
context.detail("Removed: child groupId (matches parent)");
hasChanges = true;
}
}
// Remove child version if it matches parent version
if (childVersion != null && Objects.equals(childVersion, parentVersion)) {
Element childVersionElement = root.child("version").orElse(null);
if (childVersionElement != null) {
DomUtils.removeElement(childVersionElement);
context.detail("Removed: child version (matches parent)");
hasChanges = true;
}
}
return hasChanges;
}
/**
* Applies full parent inference for 4.1.0+ models.
* Removes parent groupId/version/artifactId when they can be inferred.
*/
private boolean trimParentElementFull(
UpgradeContext context, Element root, Element parentElement, Map<Path, Document> pomMap) {
boolean hasChanges = false;
// Get child GAV before applying any changes
String childGroupId = root.childText(MavenPomElements.Elements.GROUP_ID);
String childVersion = root.childText(MavenPomElements.Elements.VERSION);
// First apply limited inference (child elements) - this removes matching child groupId/version
hasChanges |= trimParentElementLimited(context, root, parentElement);
// Only remove parent elements if the parent is in the same reactor (not external)
if (isParentInReactor(parentElement, pomMap, context)) {
// Remove parent groupId if child has no explicit groupId
if (childGroupId == null) {
Element parentGroupIdElement = parentElement.child(GROUP_ID).orElse(null);
if (parentGroupIdElement != null) {
DomUtils.removeElement(parentGroupIdElement);
context.detail("Removed: parent groupId (child has no explicit groupId)");
hasChanges = true;
}
}
// Remove parent version if child has no explicit version
if (childVersion == null) {
Element parentVersionElement = parentElement.child(VERSION).orElse(null);
if (parentVersionElement != null) {
DomUtils.removeElement(parentVersionElement);
context.detail("Removed: parent version (child has no explicit version)");
hasChanges = true;
}
}
// Remove parent artifactId if it can be inferred from relativePath
if (canInferParentArtifactId(parentElement, pomMap)) {
Element parentArtifactIdElement =
parentElement.child(ARTIFACT_ID).orElse(null);
if (parentArtifactIdElement != null) {
DomUtils.removeElement(parentArtifactIdElement);
context.detail("Removed: parent artifactId (can be inferred from relativePath)");
hasChanges = true;
}
}
}
return hasChanges;
}
/**
* Determines if the parent is part of the same reactor (multi-module project)
* vs. an external parent POM by checking if the parent exists in the pomMap.
*/
private boolean isParentInReactor(Element parentElement, Map<Path, Document> pomMap, UpgradeContext context) {
// If relativePath is explicitly set to empty, parent is definitely external
String relativePath = parentElement.childText(MavenPomElements.Elements.RELATIVE_PATH);
if (relativePath != null && relativePath.trim().isEmpty()) {
return false;
}
// Extract parent GAV
String parentGroupId = parentElement.childText(MavenPomElements.Elements.GROUP_ID);
String parentArtifactId = parentElement.childText(MavenPomElements.Elements.ARTIFACT_ID);
String parentVersion = parentElement.childText(MavenPomElements.Elements.VERSION);
if (parentGroupId == null || parentArtifactId == null || parentVersion == null) {
// Cannot determine parent GAV, assume external
return false;
}
Coordinates parentGAV = Coordinates.of(parentGroupId, parentArtifactId, parentVersion);
// Check if any POM in our reactor matches the parent GAV using GAVUtils
for (Document pomDocument : pomMap.values()) {
Coordinates pomGAV =
AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(context, pomDocument);
if (pomGAV != null && pomGAV.equals(parentGAV)) {
return true;
}
}
// Parent not found in reactor, must be external
return false;
}
/**
* Determines if parent artifactId can be inferred from relativePath.
*/
private boolean canInferParentArtifactId(Element parentElement, Map<Path, Document> pomMap) {
// Get relativePath (default is "../pom.xml" if not specified)
String relativePath = parentElement.childText(MavenPomElements.Elements.RELATIVE_PATH);
if (relativePath == null || relativePath.trim().isEmpty()) {
relativePath = DEFAULT_PARENT_RELATIVE_PATH; // Maven default
}
// Only infer artifactId if relativePath is the default and we have multiple POMs
// indicating this is likely a multi-module project
return DEFAULT_PARENT_RELATIVE_PATH.equals(relativePath) && pomMap.size() > 1;
}
/**
* Checks if a subprojects list is redundant (matches direct child directories with pom.xml).
*/
private boolean isSubprojectsListRedundant(Element subprojectsElement, Path pomPath) {
List<Element> subprojectElements =
subprojectsElement.children(SUBPROJECT).toList();
if (subprojectElements.isEmpty()) {
return true; // Empty list is redundant
}
// Get the directory containing this POM
Path parentDir = pomPath.getParent();
if (parentDir == null) {
return false;
}
// Get declared subprojects
Set<String> declaredSubprojects = subprojectElements.stream()
.map(Element::textContentTrimmed)
.filter(name -> !name.isEmpty())
.collect(Collectors.toSet());
// Get list of actual direct child directories with pom.xml
Set<String> actualSubprojects = new HashSet<>();
try {
if (Files.exists(parentDir) && Files.isDirectory(parentDir)) {
try (Stream<Path> children = Files.list(parentDir)) {
children.filter(Files::isDirectory)
.filter(dir -> Files.exists(dir.resolve(POM_XML)))
.forEach(dir ->
actualSubprojects.add(dir.getFileName().toString()));
}
}
} catch (Exception e) {
// If we can't read the directory, assume not redundant
return false;
}
// Lists are redundant if they match exactly
return declaredSubprojects.equals(actualSubprojects);
}
/**
* Helper method to remove managed dependencies from a specific dependencies section.
*/
private boolean removeManagedDependenciesFromSection(
UpgradeContext context, Element dependencies, Set<Coordinates> allGAVs, String sectionName) {
List<Element> dependencyElements = dependencies.children(DEPENDENCY).toList();
List<Element> projectArtifacts = dependencyElements.stream()
.filter(dependency -> {
String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
if (groupId != null && artifactId != null) {
boolean isProjectArtifact = allGAVs.stream()
.anyMatch(gav -> Objects.equals(gav.groupId(), groupId)
&& Objects.equals(gav.artifactId(), artifactId));
if (isProjectArtifact) {
context.detail("Removed: managed dependency " + groupId + ":" + artifactId + " from "
+ sectionName + " (project artifact)");
return true;
}
}
return false;
})
.toList();
// Remove project artifacts while preserving formatting
projectArtifacts.forEach(DomUtils::removeElement);
return !projectArtifacts.isEmpty();
}
/**
* Helper method to remove dependency inference redundancy from a specific dependencies section.
*/
private boolean removeDependencyInferenceFromSection(
UpgradeContext context, Element dependencies, Map<Path, Document> pomMap, String sectionName) {
List<Element> dependencyElements = dependencies.children(DEPENDENCY).toList();
boolean hasChanges = false;
for (Element dependency : dependencyElements) {
String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
String version = dependency.childText(MavenPomElements.Elements.VERSION);
if (artifactId != null) {
// Try to find the dependency POM in our pomMap
Document dependencyPom = findDependencyPom(context, pomMap, groupId, artifactId);
if (dependencyPom != null) {
// Check if we can infer groupId
if (groupId != null && canInferDependencyGroupId(context, dependencyPom, groupId)) {
Element groupIdElement = dependency.child(GROUP_ID).orElse(null);
if (groupIdElement != null) {
DomUtils.removeElement(groupIdElement);
context.detail("Removed: " + "dependency groupId " + groupId + " from " + sectionName
+ " (can be inferred from project)");
hasChanges = true;
}
}
// Check if we can infer version
if (version != null && canInferDependencyVersion(context, dependencyPom, version)) {
Element versionElement = dependency.child(VERSION).orElse(null);
if (versionElement != null) {
DomUtils.removeElement(versionElement);
context.detail("Removed: " + "dependency version " + version + " from " + sectionName
+ " (can be inferred from project)");
hasChanges = true;
}
}
}
}
}
return hasChanges;
}
/**
* Finds a dependency POM in the pomMap by groupId and artifactId.
*/
private Document findDependencyPom(
UpgradeContext context, Map<Path, Document> pomMap, String groupId, String artifactId) {
return pomMap.values().stream()
.filter(pomDocument -> {
Coordinates gav = AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(
context, pomDocument);
return gav != null
&& Objects.equals(gav.groupId(), groupId)
&& Objects.equals(gav.artifactId(), artifactId);
})
.findFirst()
.orElse(null);
}
/**
* Determines if a dependency version can be inferred from the project artifact.
*/
private boolean canInferDependencyVersion(UpgradeContext context, Document dependencyPom, String declaredVersion) {
Coordinates projectGav =
AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(context, dependencyPom);
if (projectGav == null || projectGav.version() == null) {
return false;
}
// We can infer the version if the declared version matches the project version
return Objects.equals(declaredVersion, projectGav.version());
}
/**
* Determines if a dependency groupId can be inferred from the project artifact.
*/
private boolean canInferDependencyGroupId(UpgradeContext context, Document dependencyPom, String declaredGroupId) {
Coordinates projectGav =
AbstractUpgradeStrategy.extractArtifactCoordinatesWithParentResolution(context, dependencyPom);
if (projectGav == null || projectGav.groupId() == null) {
return false;
}
// We can infer the groupId if the declared groupId matches the project groupId
return Objects.equals(declaredGroupId, projectGav.groupId());
}
}