Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -19,9 +19,12 @@
package org.apache.maven.model.v4;

import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;

import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginExecution;
Expand Down Expand Up @@ -72,4 +75,73 @@ void testV4ModelPriority() {
PluginExecution.newInstance().withPriority(5))))));
assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m));
}

@Test
void testV4ModelWithNewMaven4Scopes() {
// Test compile-only scope
Dependency compileOnlyDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("compile-only-dep")
.version("1.0.0")
.scope("compile-only")
.build();

Model m1 = model.withDependencies(Arrays.asList(compileOnlyDep));
// Should return "4.1.0" because compile-only scope requires Maven 4.1.0+
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m1));

// Test test-only scope
Dependency testOnlyDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("test-only-dep")
.version("1.0.0")
.scope("test-only")
.build();

Model m2 = model.withDependencies(Arrays.asList(testOnlyDep));
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m2));

// Test test-runtime scope
Dependency testRuntimeDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("test-runtime-dep")
.version("1.0.0")
.scope("test-runtime")
.build();

Model m3 = model.withDependencies(Arrays.asList(testRuntimeDep));
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m3));

// Test new scopes in dependency management
DependencyManagement depMgmt = DependencyManagement.newBuilder()
.dependencies(Arrays.asList(compileOnlyDep))
.build();

Model m4 = model.withDependencyManagement(depMgmt);
assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m4));
}

@Test
void testV4ModelWithStandardScopes() {
// Test that standard scopes don't require 4.1.0
Dependency compileDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("compile-dep")
.version("1.0.0")
.scope("compile")
.build();

Model m1 = model.withDependencies(Arrays.asList(compileDep));
assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m1));

Dependency testDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("test-dep")
.version("1.0.0")
.scope("test")
.build();

Model m2 = model.withDependencies(Arrays.asList(testDep));
assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ private Model buildEffectiveModel(RepositorySystemSession session, Path src) thr
.collect(Collectors.toMap(n -> getDependencyKey(n.getDependency()), Function.identity()));
Map<String, Dependency> directDependencies = model.getDependencies().stream()
.filter(dependency -> !"import".equals(dependency.getScope()))
.map(this::transformDependencyForConsumerPom)
.filter(dependency -> dependency != null) // Filter out dependencies that should be omitted
.collect(Collectors.toMap(
DefaultConsumerPomBuilder::getDependencyKey,
Function.identity(),
Expand All @@ -128,6 +130,8 @@ private Model buildEffectiveModel(RepositorySystemSession session, Path src) thr
Map<String, Dependency> managedDependencies = model.getDependencyManagement().getDependencies().stream()
.filter(dependency ->
nodes.containsKey(getDependencyKey(dependency)) && !"import".equals(dependency.getScope()))
.map(this::transformDependencyForConsumerPom)
.filter(dependency -> dependency != null) // Filter out dependencies that should be omitted
.collect(Collectors.toMap(
DefaultConsumerPomBuilder::getDependencyKey,
Function.identity(),
Expand Down Expand Up @@ -175,6 +179,45 @@ private Dependency merge(Dependency dep1, Dependency dep2) {
throw new IllegalArgumentException("Duplicate dependency: " + dep1);
}

/**
* Transforms a dependency for inclusion in a consumer POM.
* Handles new Maven 4 scopes that are not compatible with Maven 3.x consumers.
*
* @param dependency the original dependency
* @return the transformed dependency, or null if the dependency should be omitted
*/
Dependency transformDependencyForConsumerPom(Dependency dependency) {
if (dependency == null) {
return null;
}

String scope = dependency.getScope();
if (scope == null) {
return dependency;
}

// Handle new Maven 4 scopes when creating consumer POM
switch (scope) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but one nit: could we do this programmatically? We do have mvn3 scope manager config and we do have mvn4 scope manager config as well.... I would really like to have all scopes managed at single place, unlike in mvn3 when they are smeared across many classes in many projects (maven, resolver, many plugins, etc)

Copy link
Contributor Author

@gnodet gnodet Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the logic into Maven4ScopeManagerConfiguration. @cstamas is that what you were asking for ?

case "compile-only":
// compile-only dependencies should be omitted from consumer POM
// as they are only needed at compile time and not for consumers
return null;

case "test-only":
// test-only dependencies should be omitted from consumer POM
// as they are only needed for testing and not for consumers
return null;

case "test-runtime":
// test-runtime dependencies should be mapped to classic 'test' for consumer POM compatibility
return dependency.withScope("test");

default:
// Keep all other scopes as-is
return dependency;
}
}

private static String getDependencyKey(org.apache.maven.api.Dependency dependency) {
return dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
+ dependency.getType().id() + ":" + dependency.getClassifier();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.maven.api.PathScope;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Scm;
import org.apache.maven.api.services.DependencyResolver;
Expand All @@ -51,6 +52,7 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -158,4 +160,85 @@ void testScmInheritance() throws Exception {
assertNull(transformed.getScm().getChildScmUrlInheritAppendPath());
assertNull(transformed.getScm().getChildScmDeveloperConnectionInheritAppendPath());
}

@Test
void testNewMaven4ScopesInConsumerPom() throws Exception {
// Create dependencies with new Maven 4 scopes
Dependency compileOnlyDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("compile-only-dep")
.version("1.0.0")
.scope("compile-only")
.build();

Dependency testOnlyDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("test-only-dep")
.version("1.0.0")
.scope("test-only")
.build();

Dependency testRuntimeDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("test-runtime-dep")
.version("1.0.0")
.scope("test-runtime")
.build();

Dependency compileDep = Dependency.newBuilder()
.groupId("org.example")
.artifactId("compile-dep")
.version("1.0.0")
.scope("compile")
.build();

// Transform using the consumer POM builder
DefaultConsumerPomBuilder builder = new DefaultConsumerPomBuilder(null);

// Test the transformation method directly
Dependency transformedCompileOnly = builder.transformDependencyForConsumerPom(compileOnlyDep);
Dependency transformedTestOnly = builder.transformDependencyForConsumerPom(testOnlyDep);
Dependency transformedTestRuntime = builder.transformDependencyForConsumerPom(testRuntimeDep);
Dependency transformedCompile = builder.transformDependencyForConsumerPom(compileDep);

// New Maven 4 scopes handling
assertNull(transformedCompileOnly, "compile-only dependencies should be omitted from consumer POM");
assertNull(transformedTestOnly, "test-only dependencies should be omitted from consumer POM");
assertNotNull(
transformedTestRuntime, "test-runtime dependencies should be preserved as 'test' in consumer POM");
assertEquals("test", transformedTestRuntime.getScope());

// Standard scopes should be preserved
assertNotNull(transformedCompile, "compile dependencies should be preserved in consumer POM");
assertEquals("compile", transformedCompile.getScope());
}

@Test
void testNewMaven4ScopesInDependencyManagement() throws Exception {
// Create managed dependencies with new Maven 4 scopes
Dependency compileOnlyManaged = Dependency.newBuilder()
.groupId("org.example")
.artifactId("compile-only-managed")
.version("1.0.0")
.scope("compile-only")
.build();

Dependency testOnlyManaged = Dependency.newBuilder()
.groupId("org.example")
.artifactId("test-only-managed")
.version("1.0.0")
.scope("test-only")
.build();

// Test the transformation method directly
DefaultConsumerPomBuilder builder = new DefaultConsumerPomBuilder(null);

Dependency transformedCompileOnlyManaged = builder.transformDependencyForConsumerPom(compileOnlyManaged);
Dependency transformedTestOnlyManaged = builder.transformDependencyForConsumerPom(testOnlyManaged);

// New Maven 4 scopes should be filtered out even in dependency management
assertNull(
transformedCompileOnlyManaged, "compile-only managed dependencies should be omitted from consumer POM");
assertNull(transformedTestOnlyManaged, "test-only managed dependencies should be omitted from consumer POM");
}
}
50 changes: 50 additions & 0 deletions src/mdo/model-version.vm
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,19 @@ import org.apache.maven.api.xml.XmlNode;
#foreach ( $class in $model.allClasses )
import ${packageModelV4}.${class.Name};
#end
import ${packageModelV4}.Profile;

@Generated
public class ${className} {

public String getModelVersion(${root.name} model) {
Objects.requireNonNull(model, "model cannot be null");

// Check for new Maven 4 dependency scopes first
if (is_4_1_0_custom(model)) {
return "4.1.0";
}

#set ( $String = $model.getClass().forName("java.lang.String") )
#set ( $Comparator = $model.getClass().forName("java.util.Comparator") )
#set ( $LinkedHashSet = $model.getClass().forName("java.util.LinkedHashSet") )
Expand Down Expand Up @@ -190,4 +196,48 @@ public class ${className} {
return node != null;
}

/**
* Checks if a dependency uses one of the new Maven 4 scopes that require model version 4.1.0+
*/
private boolean hasNewMaven4Scope(Dependency dependency) {
if (dependency == null || dependency.getScope() == null) {
return false;
}
String scope = dependency.getScope();
return "compile-only".equals(scope) || "test-only".equals(scope) || "test-runtime".equals(scope);
}

/**
* Override the generated is_4_1_0 method to add custom logic for new Maven 4 dependency scopes
*/
private boolean is_4_1_0_custom(Model model) {
if (model == null) {
return false;
}

// Check direct dependencies for new scopes
if (model.getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) {
return true;
}

// Check dependency management for new scopes
if (model.getDependencyManagement() != null &&
model.getDependencyManagement().getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) {
return true;
}

// Check profiles for new scopes
for (Profile profile : model.getProfiles()) {
if (profile.getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) {
return true;
}
if (profile.getDependencyManagement() != null &&
profile.getDependencyManagement().getDependencies().stream().anyMatch(this::hasNewMaven4Scope)) {
return true;
}
}

return false;
}

}
Loading