Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import eu.maveniverse.domtrip.Comment;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import eu.maveniverse.domtrip.maven.Coordinates;
import eu.maveniverse.domtrip.maven.MavenPomElements;
Expand All @@ -54,6 +58,7 @@
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.PROPERTIES;
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;
Expand All @@ -70,6 +75,8 @@
@Priority(20)
public class CompatibilityFixStrategy extends AbstractUpgradeStrategy {

private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\{([^}]+)}");

@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
Expand Down Expand Up @@ -117,6 +124,9 @@ public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap)
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();

// Collect all properties defined across all project POMs for cross-POM analysis
Set<String> allDefinedProperties = collectAllDefinedProperties(pomMap);

for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
Expand All @@ -135,6 +145,7 @@ public UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap)
hasIssues |= fixDuplicatePlugins(pomDocument, context);
hasIssues |= fixUnsupportedRepositoryExpressions(pomDocument, context);
hasIssues |= fixIncorrectParentRelativePaths(pomDocument, pomPath, pomMap, context);
hasIssues |= fixUndefinedPropertyExpressions(pomDocument, allDefinedProperties, context);

if (hasIssues) {
context.success("Maven 4 compatibility issues fixed");
Expand Down Expand Up @@ -345,6 +356,147 @@ private boolean fixIncorrectParentRelativePaths(
return false;
}

/**
* Collects all property names defined in properties sections across all project POMs.
*/
private Set<String> collectAllDefinedProperties(Map<Path, Document> pomMap) {
Set<String> properties = new HashSet<>();

for (Document document : pomMap.values()) {
Element root = document.root();

root.child(PROPERTIES)
.ifPresent(propsElement -> propsElement.children().forEach(child -> properties.add(child.name())));

root.child(PROFILES)
.ifPresent(profiles -> profiles.children(PROFILE).forEach(profile -> profile.child(PROPERTIES)
.ifPresent(propsElement ->
propsElement.children().forEach(child -> properties.add(child.name())))));
}

return properties;
}

/**
* Fixes dependencies with undefined property expressions by commenting them out.
*/
private boolean fixUndefinedPropertyExpressions(
Document pomDocument, Set<String> allDefinedProperties, UpgradeContext context) {
Element root = pomDocument.root();

Stream<DependencyContainer> dependencyContainers = Stream.concat(
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),
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 -> fixUndefinedPropertyExpressionsInSection(
container.element, allDefinedProperties, pomDocument, context, container.sectionName))
.reduce(false, Boolean::logicalOr);
}

/**
* Fixes undefined property expressions in a specific dependencies section.
*/
private boolean fixUndefinedPropertyExpressionsInSection(
Element dependenciesElement,
Set<String> allDefinedProperties,
Document pomDocument,
UpgradeContext context,
String sectionName) {
boolean fixed = false;
List<Element> dependencies = dependenciesElement.children(DEPENDENCY).toList();
Editor editor = new Editor(pomDocument);

for (Element dependency : dependencies) {
Set<String> undefinedProps = findUndefinedProperties(dependency, allDefinedProperties);
if (!undefinedProps.isEmpty()) {
String propLabel = undefinedProps.size() > 1 ? "properties" : "property";
String propsStr = "'" + String.join("', '", undefinedProps) + "'";

Comment comment = editor.commentOutElement(dependency);
String elementXml = comment.content().trim();
comment.content(
" mvnup: commented out - undefined " + propLabel + " " + propsStr + "\n" + elementXml + " ");

context.detail("Fixed: Commented out dependency with undefined " + propLabel + " " + propsStr + " in "
+ sectionName);
fixed = true;
}
}

return fixed;
}

/**
* Finds undefined property expressions in a dependency's coordinate fields.
*/
private Set<String> findUndefinedProperties(Element dependency, Set<String> allDefinedProperties) {
Set<String> undefinedProperties = new HashSet<>();

String groupId = dependency.childText(MavenPomElements.Elements.GROUP_ID);
String artifactId = dependency.childText(MavenPomElements.Elements.ARTIFACT_ID);
String version = dependency.childText(MavenPomElements.Elements.VERSION);

collectUndefinedExpressions(groupId, allDefinedProperties, undefinedProperties);
collectUndefinedExpressions(artifactId, allDefinedProperties, undefinedProperties);
collectUndefinedExpressions(version, allDefinedProperties, undefinedProperties);

return undefinedProperties;
}

private void collectUndefinedExpressions(String value, Set<String> allDefinedProperties, Set<String> result) {
if (value == null) {
return;
}
Matcher matcher = EXPRESSION_PATTERN.matcher(value);
while (matcher.find()) {
String propertyName = matcher.group(1);
if (!isWellKnownProperty(propertyName) && !allDefinedProperties.contains(propertyName)) {
result.add(propertyName);
}
}
}

private static boolean isWellKnownProperty(String propertyName) {
if (propertyName.startsWith("project.")
|| propertyName.startsWith("pom.")
|| propertyName.startsWith("env.")
|| propertyName.startsWith("settings.")
|| propertyName.startsWith("maven.")) {
return true;
}
if (propertyName.startsWith("java.")
|| propertyName.startsWith("os.")
|| propertyName.startsWith("user.")
|| propertyName.startsWith("file.")
|| propertyName.startsWith("line.")
|| propertyName.startsWith("path.")
|| propertyName.startsWith("sun.")) {
return true;
}
return "basedir".equals(propertyName)
|| "revision".equals(propertyName)
|| "sha1".equals(propertyName)
|| "changelist".equals(propertyName);
}

/**
* Recursively finds all elements with a specific attribute value.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,176 @@ void shouldNotModifyUrlsWithoutDeprecatedExpressions() throws Exception {
}
}

@Nested
@DisplayName("Undefined Property Expression Fixes")
class UndefinedPropertyExpressionFixesTests {

@Test
@DisplayName("should comment out dependency with undefined property expression")
void shouldCommentOutDependencyWithUndefinedProperty() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""";

Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);

assertTrue(result.success(), "Compatibility fix should succeed");
assertTrue(result.modifiedCount() > 0, "Should have commented out dependency");

String xml = DomUtils.toXml(document);
assertTrue(xml.contains("mvnup: commented out"), "Should contain comment-out marker");
assertTrue(xml.contains("guava-version"), "Should mention the undefined property");

Element root = document.root();
Element depMgmt = DomUtils.findChildElement(root, "dependencyManagement");
Element deps = DomUtils.findChildElement(depMgmt, "dependencies");
assertEquals(0, deps.childElements("dependency").count(), "Should have no dependency elements");
}

@Test
@DisplayName("should not comment out dependency with defined property")
void shouldNotCommentOutDependencyWithDefinedProperty() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<properties>
<guava-version>30.0-jre</guava-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
""";

Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);

Element root = document.root();
Element depMgmt = DomUtils.findChildElement(root, "dependencyManagement");
Element deps = DomUtils.findChildElement(depMgmt, "dependencies");
assertEquals(1, deps.childElements("dependency").count(), "Dependency should still be present");
}

@Test
@DisplayName("should not comment out dependency with well-known built-in property")
void shouldNotCommentOutDependencyWithBuiltinProperty() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>test-dep</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
""";

Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);

Element root = document.root();
Element deps = DomUtils.findChildElement(root, "dependencies");
assertEquals(
1,
deps.childElements("dependency").count(),
"Dependency with built-in property should still be present");
}

@Test
@DisplayName("should recognize property defined in another module POM")
void shouldRecognizePropertyFromOtherPom() throws Exception {
String parentPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<properties>
<guava-version>30.0-jre</guava-version>
</properties>
</project>
""";

String childPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava-version}</version>
</dependency>
</dependencies>
</project>
""";

Document parentDoc = Document.of(parentPom);
Document childDoc = Document.of(childPom);
Map<Path, Document> pomMap = Map.of(
Paths.get("pom.xml"), parentDoc,
Paths.get("child/pom.xml"), childDoc);

UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);

Element root = childDoc.root();
Element deps = DomUtils.findChildElement(root, "dependencies");
assertEquals(
1,
deps.childElements("dependency").count(),
"Dependency should not be commented out when property is defined in another POM");
}
}

@Nested
@DisplayName("Strategy Description")
class StrategyDescriptionTests {
Expand Down