From ed4f0d746e8662ae31c7ed5ac07e492303e36c46 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Fri, 3 Jul 2026 19:00:24 +0530 Subject: [PATCH 1/6] Fix BOM version resolution for sibling modules in dependencyManagement --- .../maven/impl/model/DefaultModelBuilder.java | 74 ++++++++++++++----- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index 182d0f1ae811..ca596d23e190 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -611,28 +611,68 @@ public void mergeRepositories(Model model, boolean replace) { // Infer inner reactor dependencies version // Model transformFileToRaw(Model model) { - if (model.getDependencies().isEmpty()) { - return model; - } - List newDeps = new ArrayList<>(model.getDependencies().size()); boolean changed = false; - for (Dependency dep : model.getDependencies()) { - Dependency newDep = null; - if (dep.getVersion() == null) { - newDep = inferDependencyVersion(model, dep); - if (newDep != null) { - changed = true; + + List newDeps = null; + if (!model.getDependencies().isEmpty()) { + newDeps = new ArrayList<>(model.getDependencies().size()); + for (Dependency dep : model.getDependencies()) { + Dependency newDep = null; + if (dep.getVersion() == null) { + newDep = inferDependencyVersion(model, dep); + if (newDep != null) { + changed = true; + } + } else if (dep.getGroupId() == null) { + // Handle missing groupId when version is present + newDep = inferDependencyGroupId(model, dep); + if (newDep != null) { + changed = true; + } } - } else if (dep.getGroupId() == null) { - // Handle missing groupId when version is present - newDep = inferDependencyGroupId(model, dep); - if (newDep != null) { - changed = true; + newDeps.add(newDep == null ? dep : newDep); + } + } + + DependencyManagement newMgmt = null; + if (model.getDependencyManagement() != null + && !model.getDependencyManagement().getDependencies().isEmpty()) { + List newMgmtDeps = new ArrayList<>( + model.getDependencyManagement().getDependencies().size()); + boolean mgmtChanged = false; + for (Dependency dep : model.getDependencyManagement().getDependencies()) { + Dependency newDep = null; + if (dep.getVersion() == null) { + newDep = inferDependencyVersion(model, dep); + if (newDep != null) { + mgmtChanged = true; + } + } else if (dep.getGroupId() == null) { + // Handle missing groupId when version is present + newDep = inferDependencyGroupId(model, dep); + if (newDep != null) { + mgmtChanged = true; + } } + newMgmtDeps.add(newDep == null ? dep : newDep); + } + if (mgmtChanged) { + newMgmt = model.getDependencyManagement().withDependencies(newMgmtDeps); + changed = true; + } + } + + if (changed) { + Model.Builder builder = Model.newBuilder(model); + if (newDeps != null) { + builder.dependencies(newDeps); } - newDeps.add(newDep == null ? dep : newDep); + if (newMgmt != null) { + builder.dependencyManagement(newMgmt); + } + return builder.build(); } - return changed ? model.withDependencies(newDeps) : model; + return model; } private Dependency inferDependencyVersion(Model model, Dependency dep) { From 758249af6ae0fbbe92d416a34d106b5bdf0020c9 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Sun, 5 Jul 2026 20:52:05 +0530 Subject: [PATCH 2/6] Add unit test for BOM dependencyManagement version inference (MNG-11147) --- .../impl/model/DefaultModelBuilderTest.java | 48 +++++++++++++++++++ .../poms/factory/bom-dep-mgmt-bom.xml | 36 ++++++++++++++ .../poms/factory/bom-dep-mgmt-lib.xml | 25 ++++++++++ .../poms/factory/bom-dep-mgmt-parent.xml | 23 +++++++++ 4 files changed, 132 insertions(+) create mode 100644 impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-bom.xml create mode 100644 impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-lib.xml create mode 100644 impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-parent.xml diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index 0789ca89fa90..06d1eb6414f2 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -444,6 +444,54 @@ public void testBuildConsumerResolvesParentProfileProperties() { "Managed dependency version should be interpolated, not ${managed.version}"); } + /** + * Verifies that version inference works for dependencies declared in + * {@code }, not just in {@code }. + * This is the regression fixed by MNG-11147 / GH-11147. + */ + @Test + public void testBomDependencyManagementVersionInference() { + // Build the lib POM to create the mainSession and register the lib in mappedSources + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("bom-dep-mgmt-lib"))) + .build()); + + // Access the mainSession to call transformFileToRaw directly + DefaultModelBuilder.ModelBuilderSessionState mainState = + ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; + + // Create a synthetic model with a dependencyManagement dependency missing a version + Model bomModel = Model.newBuilder() + .modelVersion("4.1.0") + .groupId("org.apache.maven.tests") + .artifactId("bom-dep-mgmt-bom") + .version("1.0-SNAPSHOT") + .packaging("pom") + .pomFile(getPom("bom-dep-mgmt-bom")) + .dependencyManagement(org.apache.maven.api.model.DependencyManagement.newBuilder() + .dependencies(Arrays.asList(Dependency.newBuilder() + .groupId("org.apache.maven.tests") + .artifactId("bom-dep-mgmt-lib") + // version intentionally omitted + .build())) + .build()) + .build(); + + // Transform — this should infer the version from the reactor + Model transformed = mainState.transformFileToRaw(bomModel); + + assertNotNull(transformed.getDependencyManagement()); + Dependency managedDep = transformed.getDependencyManagement().getDependencies().stream() + .filter(d -> "bom-dep-mgmt-lib".equals(d.getArtifactId())) + .findFirst() + .orElse(null); + assertNotNull(managedDep, "Managed dependency for sibling lib should exist"); + assertNotNull(managedDep.getVersion(), "Version should be inferred from the reactor sibling module"); + } + private Path getPom(String name) { return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); } diff --git a/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-bom.xml b/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-bom.xml new file mode 100644 index 000000000000..8d90fef50283 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-bom.xml @@ -0,0 +1,36 @@ + + + + + org.apache.maven.tests + bom-dep-mgmt-parent + bom-dep-mgmt-parent.xml + + bom-dep-mgmt-bom + pom + + + + + org.apache.maven.tests + bom-dep-mgmt-lib + + + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-lib.xml b/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-lib.xml new file mode 100644 index 000000000000..6034f67bd422 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-lib.xml @@ -0,0 +1,25 @@ + + + + + org.apache.maven.tests + bom-dep-mgmt-parent + bom-dep-mgmt-parent.xml + + bom-dep-mgmt-lib + diff --git a/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-parent.xml b/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-parent.xml new file mode 100644 index 000000000000..2fb8117371a7 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/bom-dep-mgmt-parent.xml @@ -0,0 +1,23 @@ + + + + org.apache.maven.tests + bom-dep-mgmt-parent + 1.0-SNAPSHOT + pom + From e0ba2410dfecf69fbc4a7b491d54efca3368957b Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 6 Jul 2026 19:14:25 +0530 Subject: [PATCH 3/6] Address review comment: use URL for GitHub issue --- .../org/apache/maven/impl/model/DefaultModelBuilderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index 06d1eb6414f2..3ac6ffa3af95 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -447,7 +447,7 @@ public void testBuildConsumerResolvesParentProfileProperties() { /** * Verifies that version inference works for dependencies declared in * {@code }, not just in {@code }. - * This is the regression fixed by MNG-11147 / GH-11147. + * This is the regression fixed by https://github.com/apache/maven/issues/11147. */ @Test public void testBomDependencyManagementVersionInference() { From acbea96f0afdf936f50564c254c0eced2a8a1b87 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Fri, 10 Jul 2026 10:14:43 +0530 Subject: [PATCH 4/6] Address review: extract inferDependencies helper and track depsChanged separately - Extract inferDependencies() helper method to deduplicate the near-identical for-loop bodies for regular dependencies and dependencyManagement dependencies - Track separate depsChanged/mgmtChanged booleans to avoid unnecessary list copy when only dependencyManagement changes --- .../maven/impl/model/DefaultModelBuilder.java | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index ca596d23e190..3248c59cc379 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -611,27 +611,13 @@ public void mergeRepositories(Model model, boolean replace) { // Infer inner reactor dependencies version // Model transformFileToRaw(Model model) { - boolean changed = false; + boolean depsChanged = false; + boolean mgmtChanged = false; List newDeps = null; if (!model.getDependencies().isEmpty()) { newDeps = new ArrayList<>(model.getDependencies().size()); - for (Dependency dep : model.getDependencies()) { - Dependency newDep = null; - if (dep.getVersion() == null) { - newDep = inferDependencyVersion(model, dep); - if (newDep != null) { - changed = true; - } - } else if (dep.getGroupId() == null) { - // Handle missing groupId when version is present - newDep = inferDependencyGroupId(model, dep); - if (newDep != null) { - changed = true; - } - } - newDeps.add(newDep == null ? dep : newDep); - } + depsChanged = inferDependencies(model, model.getDependencies(), newDeps); } DependencyManagement newMgmt = null; @@ -639,35 +625,19 @@ Model transformFileToRaw(Model model) { && !model.getDependencyManagement().getDependencies().isEmpty()) { List newMgmtDeps = new ArrayList<>( model.getDependencyManagement().getDependencies().size()); - boolean mgmtChanged = false; - for (Dependency dep : model.getDependencyManagement().getDependencies()) { - Dependency newDep = null; - if (dep.getVersion() == null) { - newDep = inferDependencyVersion(model, dep); - if (newDep != null) { - mgmtChanged = true; - } - } else if (dep.getGroupId() == null) { - // Handle missing groupId when version is present - newDep = inferDependencyGroupId(model, dep); - if (newDep != null) { - mgmtChanged = true; - } - } - newMgmtDeps.add(newDep == null ? dep : newDep); - } + mgmtChanged = inferDependencies( + model, model.getDependencyManagement().getDependencies(), newMgmtDeps); if (mgmtChanged) { newMgmt = model.getDependencyManagement().withDependencies(newMgmtDeps); - changed = true; } } - if (changed) { + if (depsChanged || mgmtChanged) { Model.Builder builder = Model.newBuilder(model); - if (newDeps != null) { + if (depsChanged) { builder.dependencies(newDeps); } - if (newMgmt != null) { + if (mgmtChanged) { builder.dependencyManagement(newMgmt); } return builder.build(); @@ -675,6 +645,33 @@ Model transformFileToRaw(Model model) { return model; } + /** + * Infers missing version or groupId for dependencies in the given list by looking up + * sibling reactor modules. Returns {@code true} if any dependency was modified. + * + * @param model the model owning the dependencies + * @param dependencies the source dependency list to process + * @param result the output list to populate (original or inferred dependencies) + * @return {@code true} if at least one dependency was inferred/changed + */ + private boolean inferDependencies( + Model model, List dependencies, List result) { + boolean changed = false; + for (Dependency dep : dependencies) { + Dependency newDep = null; + if (dep.getVersion() == null) { + newDep = inferDependencyVersion(model, dep); + } else if (dep.getGroupId() == null) { + newDep = inferDependencyGroupId(model, dep); + } + if (newDep != null) { + changed = true; + } + result.add(newDep == null ? dep : newDep); + } + return changed; + } + private Dependency inferDependencyVersion(Model model, Dependency dep) { Model depModel = getRawModel(model.getPomFile(), dep.getGroupId(), dep.getArtifactId()); if (depModel == null) { From d93230020a350766931596f2ebca13da286e9421 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Fri, 10 Jul 2026 11:50:53 +0530 Subject: [PATCH 5/6] Address review: assert specific expected version value in test --- .../org/apache/maven/impl/model/DefaultModelBuilderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index 3ac6ffa3af95..95e73f49c694 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -489,7 +489,7 @@ public void testBomDependencyManagementVersionInference() { .findFirst() .orElse(null); assertNotNull(managedDep, "Managed dependency for sibling lib should exist"); - assertNotNull(managedDep.getVersion(), "Version should be inferred from the reactor sibling module"); + assertEquals("1.0-SNAPSHOT", managedDep.getVersion(), "Version should be inferred from the reactor sibling module"); } private Path getPom(String name) { From bf2c3e1aab527b0e5e11746d744e386e4368fb61 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Fri, 24 Jul 2026 23:42:43 +0530 Subject: [PATCH 6/6] Address review: wrap long assertions and format inferDependencies call per Spotless --- .../maven/impl/model/DefaultModelBuilder.java | 274 +++--- .../impl/model/DefaultModelBuilderTest.java | 908 +++++++++--------- 2 files changed, 625 insertions(+), 557 deletions(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index 3248c59cc379..d51067ecacb0 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -124,10 +124,13 @@ import org.slf4j.LoggerFactory; /** - * The model builder is responsible for building the {@link Model} from the POM file. - * There are two ways to main use cases: the first one is to build the model from a POM file - * on the file system in order to actually build the project. The second one is to build the - * model for a dependency or an external parent. + * The model builder is responsible for building the {@link Model} from the POM + * file. + * There are two ways to main use cases: the first one is to build the model + * from a POM file + * on the file system in order to actually build the project. The second one is + * to build the + * model for a dependency or an external parent. */ @Named @Singleton @@ -248,7 +251,8 @@ public ModelBuilderResult build(ModelBuilderRequest request) throws ModelBuilder return session.result; } finally { // Clean up REQUEST_SCOPED cache entries to prevent memory leaks - // This is especially important for BUILD_PROJECT requests which are top-level requests + // This is especially important for BUILD_PROJECT requests which are top-level + // requests if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_PROJECT) { try { clearRequestScopedCache(request); @@ -351,10 +355,12 @@ ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilder if (session != request.getSession()) { throw new IllegalArgumentException("Session mismatch"); } - // Create a new parentChain for each derived session to prevent cycle detection issues + // Create a new parentChain for each derived session to prevent cycle detection + // issues // The parentChain now contains both GAV coordinates and file paths // For BUILD_CONSUMER requests, use the request's explicit repositories so that - // BOM imports can be resolved from non-central repos (e.g., settings.xml profiles). + // BOM imports can be resolved from non-central repos (e.g., settings.xml + // profiles). // This is scoped to BUILD_CONSUMER to avoid unintended side effects on other // derived sessions (e.g., parent POM resolution during project builds). List derivedExtRepos = externalRepositories; @@ -468,7 +474,7 @@ public void putSource(String groupId, String artifactId, ModelSource source) { mappedSources .computeIfAbsent(new GAKey(groupId, artifactId), k -> new HashSet<>()) .add(source); - // Also register the source under the null groupId + // Also register the source under the null groupId if (groupId != null) { putSource(null, artifactId, source); } @@ -548,8 +554,8 @@ public void add( column = e.getColumnNumber(); } - ModelProblem problem = - new DefaultModelProblem(message, severity, version, source, line, column, modelId, exception); + ModelProblem problem = new DefaultModelProblem(message, severity, version, source, line, column, modelId, + exception); add(problem); } @@ -591,8 +597,7 @@ public void mergeRepositories(Model model, boolean replace) { .filter(r -> !ids.contains(r.getId())) .toList(); } else { - Set ids = - pomRepositories.stream().map(RemoteRepository::getId).collect(Collectors.toSet()); + Set ids = pomRepositories.stream().map(RemoteRepository::getId).collect(Collectors.toSet()); repos = repos.stream().filter(r -> !ids.contains(r.getId())).toList(); } @@ -626,7 +631,9 @@ Model transformFileToRaw(Model model) { List newMgmtDeps = new ArrayList<>( model.getDependencyManagement().getDependencies().size()); mgmtChanged = inferDependencies( - model, model.getDependencyManagement().getDependencies(), newMgmtDeps); + model, + model.getDependencyManagement().getDependencies(), + newMgmtDeps); if (mgmtChanged) { newMgmt = model.getDependencyManagement().withDependencies(newMgmtDeps); } @@ -646,16 +653,20 @@ Model transformFileToRaw(Model model) { } /** - * Infers missing version or groupId for dependencies in the given list by looking up + * Infers missing version or groupId for dependencies in the given list by + * looking up * sibling reactor modules. Returns {@code true} if any dependency was modified. * - * @param model the model owning the dependencies + * @param model the model owning the dependencies * @param dependencies the source dependency list to process - * @param result the output list to populate (original or inferred dependencies) + * @param result the output list to populate (original or inferred + * dependencies) * @return {@code true} if at least one dependency was inferred/changed */ private boolean inferDependencies( - Model model, List dependencies, List result) { + Model model, + List dependencies, + List result) { boolean changed = false; for (Dependency dep : dependencies) { Dependency newDep = null; @@ -720,13 +731,16 @@ String replaceCiFriendlyVersion(Map properties, String version) /** * Get enhanced properties that include profile-aware property resolution. * This method activates profiles to ensure that properties defined in profiles - * are available for CI-friendly version processing and repository URL interpolation. - * It also includes directory-related properties that may be needed during profile activation. + * are available for CI-friendly version processing and repository URL + * interpolation. + * It also includes directory-related properties that may be needed during + * profile activation. */ private Map getEnhancedProperties(Model model, Path rootDirectory, Set activeModelReads) { Map properties = new HashMap<>(); - // Add directory-specific properties first, as they may be needed for profile activation + // Add directory-specific properties first, as they may be needed for profile + // activation if (model.getProjectDirectory() != null) { String basedir = model.getProjectDirectory().toString(); String basedirUri = model.getProjectDirectory().toUri().toString(); @@ -747,16 +761,17 @@ private Map getEnhancedProperties(Model model, Path rootDirector if (!Objects.equals(rootDirectory, model.getProjectDirectory())) { Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory); if (rootModelPath != null) { - // Check if the root model path is within the root directory to prevent infinite loops - // This can happen when a .mvn directory exists in a subdirectory and parent inference + // Check if the root model path is within the root directory to prevent infinite + // loops + // This can happen when a .mvn directory exists in a subdirectory and parent + // inference // tries to read models above the discovered root directory. // Also skip if the root model is already being read in an outer call frame // to prevent StackOverflowError when a project has an internal parent in a // subdirectory with CI-friendly ${revision} and a .mvn/ root marker (GH-12301). if (isParentWithinRootDirectory(rootModelPath, rootDirectory) && !activeModelReads.contains(rootModelPath.normalize())) { - Model rootModel = - derive(Sources.buildSource(rootModelPath)).readFileModel(activeModelReads); + Model rootModel = derive(Sources.buildSource(rootModelPath)).readFileModel(activeModelReads); properties.putAll(getPropertiesWithProfiles(rootModel, properties)); } } @@ -771,8 +786,9 @@ private Map getEnhancedProperties(Model model, Path rootDirector * Get properties from a model including properties from activated profiles. * This performs lightweight profile activation to merge profile properties. * - * @param model the model to get properties from - * @param baseProperties base properties (including directory properties) to include in profile activation context + * @param model the model to get properties from + * @param baseProperties base properties (including directory properties) to + * include in profile activation context */ private Map getPropertiesWithProfiles(Model model, Map baseProperties) { Map properties = new HashMap<>(); @@ -784,7 +800,8 @@ private Map getPropertiesWithProfiles(Model model, Map getPropertiesWithProfiles(Model model, Map getPropertiesWithProfiles(Model model, Map getPropertiesWithProfiles(Model model) { return getPropertiesWithProfiles(model, new HashMap<>()); } private void buildBuildPom() throws ModelBuilderException { - // Retrieve and normalize the source path, ensuring it's non-null and in absolute form + // Retrieve and normalize the source path, ensuring it's non-null and in + // absolute form Path top = request.getSource().getPath(); if (top == null) { throw new IllegalStateException("Recursive build requested but source has no path"); @@ -892,11 +914,14 @@ private void buildBuildPom() throws ModelBuilderException { } /** - * Generates a stream of DefaultModelBuilderResult objects, starting with the provided + * Generates a stream of DefaultModelBuilderResult objects, starting with the + * provided * result and recursively including all its child results. * - * @param r The initial DefaultModelBuilderResult object from which to generate the stream. - * @return A Stream of DefaultModelBuilderResult objects, starting with the provided result + * @param r The initial DefaultModelBuilderResult object from which to generate + * the stream. + * @return A Stream of DefaultModelBuilderResult objects, starting with the + * provided result * and including all its child results. */ Stream results(DefaultModelBuilderResult r) { @@ -1014,16 +1039,16 @@ void buildEffectiveModel(Collection importIds) throws ModelBuilderExcept setRootModel(resultModel); // model path translation - resultModel = - modelPathTranslator.alignToBaseDirectory(resultModel, resultModel.getProjectDirectory(), request); + resultModel = modelPathTranslator.alignToBaseDirectory(resultModel, resultModel.getProjectDirectory(), + request); // plugin management injection resultModel = pluginManagementInjector.injectManagement(resultModel, request, this); // lifecycle bindings injection if (request.getRequestType() != ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY) { - org.apache.maven.api.services.ModelTransformer lifecycleBindingsInjector = - request.getLifecycleBindingsInjector(); + org.apache.maven.api.services.ModelTransformer lifecycleBindingsInjector = request + .getLifecycleBindingsInjector(); if (lifecycleBindingsInjector != null) { resultModel = lifecycleBindingsInjector.transform(resultModel, request, this); } @@ -1114,7 +1139,7 @@ Model readParent( String superModelVersion = childModel.getModelVersion(); if (superModelVersion == null || !KNOWN_MODEL_VERSIONS.contains(superModelVersion)) { // Maven 3.x is always using 4.0.0 version to load the supermodel, so - // do the same when loading a dependency. The model validator will also + // do the same when loading a dependency. The model validator will also // check that field later. superModelVersion = MODEL_VERSION_4_0_0; } @@ -1159,8 +1184,8 @@ private Model readParentLocally( return null; } } else if (isParentOrSimpleMixin) { - candidateSource = - resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion()); + candidateSource = resolveReactorModel(parent.getGroupId(), parent.getArtifactId(), + parent.getVersion()); if (candidateSource == null && parentPath == null) { candidateSource = request.getSource().resolve(modelProcessor::locateExistingPom, ".."); } @@ -1210,7 +1235,8 @@ private Model readParentLocally( : ModelBuilderRequest.build(request, candidateSource)); // Check GA match BEFORE readAsParentModel() which recursively resolves - // the candidate's parent chain and can trigger false cycle detection (GH-12074). + // the candidate's parent chain and can trigger false cycle detection + // (GH-12074). Model fileModel = derived.readFileModel(); String fileGroupId = getGroupId(fileModel); String fileArtifactId = fileModel.getArtifactId(); @@ -1223,8 +1249,7 @@ private Model readParentLocally( } Model candidateModel = derived.readAsParentModel(profileActivationContext, parentChain); // Add profiles from parent, preserving model ID tracking - for (Map.Entry> entry : - derived.result.getActivePomProfilesByModel().entrySet()) { + for (Map.Entry> entry : derived.result.getActivePomProfilesByModel().entrySet()) { addActivePomProfiles(entry.getKey(), entry.getValue()); } @@ -1238,7 +1263,8 @@ private Model readParentLocally( return null; } - // Validate versions aren't inherited when using parent ranges the same way as when read + // Validate versions aren't inherited when using parent ranges the same way as + // when read // externally. String rawChildModelVersion = childModel.getVersion(); @@ -1265,7 +1291,8 @@ private Model readParentLocally( } return candidateModel; } finally { - // Remove the source location from the chain when we're done processing this parent + // Remove the source location from the chain when we're done processing this + // parent parentChain.remove(sourceLocation); } } @@ -1316,7 +1343,8 @@ Model resolveAndReadParentExternally( String classifier = parent instanceof Mixin ? ((Mixin) parent).getClassifier() : null; String extension = parent instanceof Mixin ? ((Mixin) parent).getExtension() : null; - // add repositories specified by the current model so that we can resolve the parent + // add repositories specified by the current model so that we can resolve the + // parent if (!childModel.getRepositories().isEmpty()) { var previousRepositories = repositories; mergeRepositories(childModel, false); @@ -1378,8 +1406,7 @@ Model resolveAndReadParentExternally( ModelBuilderSessionState derived = derive(lenientRequest); Model parentModel = derived.readAsParentModel(profileActivationContext, parentChain); // Add profiles from parent, preserving model ID tracking - for (Map.Entry> entry : - derived.result.getActivePomProfilesByModel().entrySet()) { + for (Map.Entry> entry : derived.result.getActivePomProfilesByModel().entrySet()) { addActivePomProfiles(entry.getKey(), entry.getValue()); } @@ -1456,8 +1483,8 @@ private Model readEffectiveModel() throws ModelBuilderException { Model activatedFileModel = activateFileModel(inputModel); // profile activation - DefaultProfileActivationContext profileActivationContext = - getProfileActivationContext(request, activatedFileModel); + DefaultProfileActivationContext profileActivationContext = getProfileActivationContext(request, + activatedFileModel); List activeExternalProfiles = result.getActiveExternalProfiles(); @@ -1499,7 +1526,8 @@ private Model readEffectiveModel() throws ModelBuilderException { // Merge mixin into model model = inheritanceAssembler.assembleModelInheritance(model, parent, request, this); // Ensure mixin properties override any previously inherited properties - // This is necessary because normal inheritance gives child precedence, but for mixins + // This is necessary because normal inheritance gives child precedence, but for + // mixins // we want the mixin to take precedence over inherited parent properties Map mergedProperties = new java.util.HashMap<>(model.getProperties()); mergedProperties.putAll(parent.getProperties()); @@ -1512,10 +1540,11 @@ private Model readEffectiveModel() throws ModelBuilderException { // profile activation profileActivationContext.setModel(model); - // Activate profiles from the input model (before inheritance) to get only local profiles + // Activate profiles from the input model (before inheritance) to get only local + // profiles // Parent profiles are already added when the parent model is read - List localActivePomProfiles = - getActiveProfiles(inputModel.getProfiles(), profileActivationContext); + List localActivePomProfiles = getActiveProfiles(inputModel.getProfiles(), + profileActivationContext); // profile injection - inject all profiles (local + inherited) into the model List activePomProfiles = getActiveProfiles(model.getProfiles(), profileActivationContext); @@ -1523,7 +1552,8 @@ private Model readEffectiveModel() throws ModelBuilderException { model = profileInjector.injectProfiles(model, activeExternalProfiles, request, this); // Track only the local profiles for this model - // Use ModelProblemUtils.toId() to get groupId:artifactId:version format (without packaging) + // Use ModelProblemUtils.toId() to get groupId:artifactId:version format + // (without packaging) addActivePomProfiles(ModelProblemUtils.toId(model), localActivePomProfiles); // model interpolation @@ -1538,11 +1568,9 @@ private Model readEffectiveModel() throws ModelBuilderException { // Now the fully interpolated model is available: reconfigure the resolver if (!resultModel.getRepositories().isEmpty()) { - List oldRepos = - repositories.stream().map(Object::toString).toList(); + List oldRepos = repositories.stream().map(Object::toString).toList(); mergeRepositories(resultModel, true); - List newRepos = - repositories.stream().map(Object::toString).toList(); + List newRepos = repositories.stream().map(Object::toString).toList(); if (!Objects.equals(oldRepos, newRepos)) { logger.debug("Replacing repositories from " + resultModel.getId() + "\n" + newRepos.stream().map(s -> " " + s).collect(Collectors.joining("\n"))); @@ -1647,7 +1675,8 @@ Model doReadFileModel(Set activeModelReads) throws ModelBuilderException { } catch (IOException e) { String msg = e.getMessage(); if (msg == null || msg.isEmpty()) { - // NOTE: There's java.nio.charset.MalformedInputException and sun.io.MalformedInputException + // NOTE: There's java.nio.charset.MalformedInputException and + // sun.io.MalformedInputException if (e.getClass().getName().endsWith("MalformedInputException")) { msg = "Some input bytes do not match the file encoding."; } else { @@ -1695,8 +1724,8 @@ Model doReadFileModel(Set activeModelReads) throws ModelBuilderException { throw newModelBuilderException(); } - Model parentModel = - derive(Sources.buildSource(pomPath)).readFileModel(activeModelReads); + Model parentModel = derive(Sources.buildSource(pomPath)) + .readFileModel(activeModelReads); String parentGroupId = getGroupId(parentModel); String parentArtifactId = parentModel.getArtifactId(); String parentVersion = getVersion(parentModel); @@ -1746,7 +1775,8 @@ Model doReadFileModel(Set activeModelReads) throws ModelBuilderException { } } - // Enhanced property resolution with profile activation for CI-friendly versions and repository URLs + // Enhanced property resolution with profile activation for CI-friendly versions + // and repository URLs // This includes directory properties, profile properties, and user properties Map properties = getEnhancedProperties(model, rootDirectory, activeModelReads); @@ -1784,8 +1814,8 @@ Model doReadFileModel(Set activeModelReads) throws ModelBuilderException { } if (rootDirectory != null) { try { - Map properties = - getEnhancedProperties(model, rootDirectory, activeModelReads); + Map properties = getEnhancedProperties(model, rootDirectory, + activeModelReads); model = model.with() .version(replaceCiFriendlyVersion(properties, model.getVersion())) .parent( @@ -1839,10 +1869,10 @@ private DistributionManagement interpolateRepository( ? null : distributionManagement .with() - .repository((DeploymentRepository) - interpolateRepository(distributionManagement.getRepository(), callback)) - .snapshotRepository((DeploymentRepository) - interpolateRepository(distributionManagement.getSnapshotRepository(), callback)) + .repository((DeploymentRepository) interpolateRepository( + distributionManagement.getRepository(), callback)) + .snapshotRepository((DeploymentRepository) interpolateRepository( + distributionManagement.getSnapshotRepository(), callback)) .build(); } @@ -1871,12 +1901,15 @@ private Repository interpolateRepository(Repository repository, UnaryOperator merge(List profiles, Map userProperties) { @@ -1896,13 +1929,16 @@ List merge(List profiles, Map userProperties) } /** - * Merges model properties with user properties, giving precedence to user properties. - * For any property key present in both maps, the user property value will override + * Merges model properties with user properties, giving precedence to user + * properties. + * For any property key present in both maps, the user property value will + * override * the model property value when they differ. * - * @param properties properties defined in the model + * @param properties properties defined in the model * @param userProperties user-defined properties that override model properties - * @return a new map with model properties overridden by user properties if changes were needed, + * @return a new map with model properties overridden by user properties if + * changes were needed, * or null if no overrides were needed */ Map merge(Map properties, Map userProperties) { @@ -1956,18 +1992,19 @@ private Model doReadRawModel() throws ModelBuilderException { /** * Record to store both the parent model and its activated profiles for caching. */ - private record ParentModelWithProfiles(Model model, List activatedProfiles) {} + private record ParentModelWithProfiles(Model model, List activatedProfiles) { + } /** * Reads the request source's parent with cycle detection. */ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext, Set parentChain) throws ModelBuilderException { - Map parentsPerContext = - cache(request.getSource(), PARENT, ConcurrentHashMap::new); + Map parentsPerContext = cache( + request.getSource(), PARENT, ConcurrentHashMap::new); - for (Map.Entry e : - parentsPerContext.entrySet()) { + for (Map.Entry e : parentsPerContext + .entrySet()) { if (e.getKey().matches(profileActivationContext)) { ParentModelWithProfiles cached = e.getValue(); // CRITICAL: On cache hit, we need to replay the cached record's keys into the @@ -1978,15 +2015,18 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext replayRecordIntoContext(e.getKey(), profileActivationContext); } // Add the activated profiles from cache to the result - // Use ModelProblemUtils.toId() to get groupId:artifactId:version format (without packaging) + // Use ModelProblemUtils.toId() to get groupId:artifactId:version format + // (without packaging) addActivePomProfiles(ModelProblemUtils.toId(cached.model()), cached.activatedProfiles()); return cached.model(); } } // Cache miss: process the parent model - // CRITICAL: Use a separate recording context to avoid recording intermediate keys - // that aren't essential to the final result. Only replay the final essential keys + // CRITICAL: Use a separate recording context to avoid recording intermediate + // keys + // that aren't essential to the final result. Only replay the final essential + // keys // into the parent recording context to maintain clean cache keys and avoid // over-recording during parent model processing. DefaultProfileActivationContext ctx = profileActivationContext.start(); @@ -1995,7 +2035,8 @@ Model readAsParentModel(DefaultProfileActivationContext profileActivationContext replayRecordIntoContext(record, profileActivationContext); parentsPerContext.put(record, modelWithProfiles); - // Use ModelProblemUtils.toId() to get groupId:artifactId:version format (without packaging) + // Use ModelProblemUtils.toId() to get groupId:artifactId:version format + // (without packaging) addActivePomProfiles( ModelProblemUtils.toId(modelWithProfiles.model()), modelWithProfiles.activatedProfiles()); return modelWithProfiles.model(); @@ -2006,15 +2047,16 @@ private ParentModelWithProfiles doReadAsParentModel( throws ModelBuilderException { Model raw = readRawModel(); Model parentData = readParent(raw, raw.getParent(), childProfileActivationContext, parentChain); - DefaultInheritanceAssembler defaultInheritanceAssembler = - new DefaultInheritanceAssembler(new DefaultInheritanceAssembler.InheritanceModelMerger() { + DefaultInheritanceAssembler defaultInheritanceAssembler = new DefaultInheritanceAssembler( + new DefaultInheritanceAssembler.InheritanceModelMerger() { @Override protected void mergeModel_Modules( Model.Builder builder, Model target, Model source, boolean sourceDominant, - Map context) {} + Map context) { + } @Override protected void mergeModel_Subprojects( @@ -2022,7 +2064,8 @@ protected void mergeModel_Subprojects( Model target, Model source, boolean sourceDominant, - Map context) {} + Map context) { + } }); Model parent = defaultInheritanceAssembler.assembleModelInheritance(raw, parentData, request, this); for (Mixin mixin : parent.getMixins()) { @@ -2042,8 +2085,8 @@ protected void mergeModel_Subprojects( // // Use the child's activation context (passed as parameter) to determine // which parent profiles should be active, ensuring consistency. - List parentActivePomProfiles = - getActiveProfiles(parent.getProfiles(), childProfileActivationContext); + List parentActivePomProfiles = getActiveProfiles(parent.getProfiles(), + childProfileActivationContext); // Inject profiles into parent model Model injectedParentModel = profileInjector @@ -2068,7 +2111,7 @@ private Model importDependencyManagement(Model model, Collection importI List importMgmts = null; List deps = new ArrayList<>(depMgmt.getDependencies()); - for (Iterator it = deps.iterator(); it.hasNext(); ) { + for (Iterator it = deps.iterator(); it.hasNext();) { Dependency dependency = it.next(); if (!("pom".equals(dependency.getType()) && "import".equals(dependency.getScope())) @@ -2133,8 +2176,8 @@ private DependencyManagement loadDependencyManagement(Dependency dependency, Col String imported = groupId + ':' + artifactId + ':' + version; if (importIds.contains(imported)) { - StringBuilder message = - new StringBuilder("The dependencies of type=pom and with scope=import form a cycle: "); + StringBuilder message = new StringBuilder( + "The dependencies of type=pom and with scope=import form a cycle: "); for (String modelId : importIds) { message.append(modelId).append(" -> "); } @@ -2264,8 +2307,8 @@ private T cache( try { RgavCacheKey r = new RgavCacheKey( session, trace.mvnTrace(), repositories, groupId, artifactId, version, classifier, tag); - SourceResponse response = - InternalSession.from(session).request(r, tr -> new SourceResponse<>(tr, supplier.get())); + SourceResponse response = InternalSession.from(session).request(r, + tr -> new SourceResponse<>(tr, supplier.get())); return response.response; } finally { RequestTraceHelper.exit(trace); @@ -2276,8 +2319,8 @@ private T cache(Source source, String tag, Supplier supplier) throws Mode RequestTraceHelper.ResolverTrace trace = RequestTraceHelper.enter(session, request); try { SourceCacheKey r = new SourceCacheKey(session, trace.mvnTrace(), source, tag); - SourceResponse response = - InternalSession.from(session).request(r, tr -> new SourceResponse<>(tr, supplier.get())); + SourceResponse response = InternalSession.from(session).request(r, + tr -> new SourceResponse<>(tr, supplier.get())); return response.response; } finally { RequestTraceHelper.exit(trace); @@ -2305,7 +2348,8 @@ private void replayRecordIntoContext( return; // Target context is not recording } - // Replay all the recorded keys from the cached record into the target context's record + // Replay all the recorded keys from the cached record into the target context's + // record // We need to access the mutable maps in the target context's record DefaultProfileActivationContext.Record targetRecord = targetContext.record; @@ -2344,8 +2388,10 @@ private static List getSubprojects(Model activated) { /** * Checks if subprojects are explicitly defined in the main model. * This method distinguishes between: - * 1. No subprojects/modules element present - returns false (should auto-discover) - * 2. Empty subprojects/modules element present - returns true (should NOT auto-discover) + * 1. No subprojects/modules element present - returns false (should + * auto-discover) + * 2. Empty subprojects/modules element present - returns true (should NOT + * auto-discover) * 3. Non-empty subprojects/modules - returns true (should NOT auto-discover) */ @SuppressWarnings("deprecation") @@ -2427,16 +2473,15 @@ private Model injectProfileActivations(Model model, Map acti } private Model interpolateModel(Model model, ModelBuilderRequest request, ModelProblemCollector problems) { - Model interpolatedModel = - modelInterpolator.interpolateModel(model, model.getProjectDirectory(), request, problems); + Model interpolatedModel = modelInterpolator.interpolateModel(model, model.getProjectDirectory(), request, + problems); if (interpolatedModel.getParent() != null) { Map map1 = request.getSession().getUserProperties(); Map map2 = model.getProperties(); Map map3 = request.getSession().getSystemProperties(); UnaryOperator cb = Interpolator.chain(map1::get, map2::get, map3::get); try { - String interpolated = - interpolator.interpolate(interpolatedModel.getParent().getVersion(), cb); + String interpolated = interpolator.interpolate(interpolatedModel.getParent().getVersion(), cb); interpolatedModel = interpolatedModel.withParent( interpolatedModel.getParent().withVersion(interpolated)); } catch (Exception e) { @@ -2486,7 +2531,8 @@ private boolean containsCoordinates(String message, String groupId, String artif && (version == null || message.contains(version)); } - record GAKey(String groupId, String artifactId) {} + record GAKey(String groupId, String artifactId) { + } public record RgavCacheKey( Session session, @@ -2702,9 +2748,10 @@ Set getContexts() { /** * Clears REQUEST_SCOPED cache entries for a specific request. *

- * The method identifies the outer request and removes the corresponding cache entry from the session data. + * The method identifies the outer request and removes the corresponding cache + * entry from the session data. * - * @param req the request whose REQUEST_SCOPED cache should be cleared + * @param req the request whose REQUEST_SCOPED cache should be cleared * @param the request type */ private > void clearRequestScopedCache(REQ req) { @@ -2768,9 +2815,10 @@ private static List map(List resources, BiFunction mapper, /** * Checks if the parent POM path is within the root directory. - * This prevents invalid setups where a parent POM is located above the root directory. + * This prevents invalid setups where a parent POM is located above the root + * directory. * - * @param parentPath the path to the parent POM + * @param parentPath the path to the parent POM * @param rootDirectory the root directory * @return true if the parent is within the root directory, false otherwise */ diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index 95e73f49c694..07685eae0dcd 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -49,450 +49,470 @@ */ class DefaultModelBuilderTest { - Session session; - ModelBuilder builder; - - @BeforeEach - void setup() { - session = ApiRunner.createSession(); - builder = session.getService(ModelBuilder.class); - assertNotNull(builder); - } - - @Test - public void testPropertiesAndProfiles() { - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("props-and-profiles"))) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - assertEquals("21", result.getEffectiveModel().getProperties().get("maven.compiler.release")); - } - - @Test - public void testMergeRepositories() throws Exception { - // this is here only to trigger mainSession creation; unrelated - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .userProperties(Map.of("firstParentRepo", "https://some.repo")) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("props-and-profiles"))) - .build(); - ModelBuilder.ModelBuilderSession session = builder.newSession(); - session.build(request); // ignored result value; just to trigger mainSession creation - - Field mainSessionField = DefaultModelBuilder.ModelBuilderSessionImpl.class.getDeclaredField("mainSession"); - mainSessionField.setAccessible(true); - DefaultModelBuilder.ModelBuilderSessionState state = - (DefaultModelBuilder.ModelBuilderSessionState) mainSessionField.get(session); - Field repositoriesField = DefaultModelBuilder.ModelBuilderSessionState.class.getDeclaredField("repositories"); - repositoriesField.setAccessible(true); - - List repositories; - // before merge - repositories = (List) repositoriesField.get(state); - assertEquals(1, repositories.size()); // central - - Model model = Model.newBuilder() - .properties(Map.of("thirdParentRepo", "https://third.repo")) - .repositories(Arrays.asList( - Repository.newBuilder() - .id("first") - .url("${firstParentRepo}") - .build(), - Repository.newBuilder() - .id("second") - .url("${secondParentRepo}") - .build(), - Repository.newBuilder() - .id("third") - .url("${thirdParentRepo}") - .build(), - Repository.newBuilder() - .id("${uninterpolatedRepoId}") - .url("https://valid.url") - .build())) - .build(); - - state.mergeRepositories(model, false); - - // after merge: "second" filtered (uninterpolated URL), "${uninterpolatedRepoId}" filtered (uninterpolated ID) - repositories = (List) repositoriesField.get(state); - assertEquals(3, repositories.size()); - assertEquals("first", repositories.get(0).getId()); - assertEquals("https://some.repo", repositories.get(0).getUrl()); // interpolated (user properties) - assertEquals("third", repositories.get(1).getId()); - assertEquals("https://third.repo", repositories.get(1).getUrl()); // interpolated (own model properties) - assertEquals("central", repositories.get(2).getId()); // default - } - - @Test - public void testCiFriendlyVersionWithProfiles() { - // Test case 1: Default profile should set revision to baseVersion+dev - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("ci-friendly-profiles"))) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - assertEquals("0.2.0+dev", result.getEffectiveModel().getVersion()); - - // Test case 2: Release profile should set revision to baseVersion only - request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("ci-friendly-profiles"))) - .activeProfileIds(List.of("releaseBuild")) - .build(); - result = builder.newSession().build(request); - assertNotNull(result); - assertEquals("0.2.0", result.getEffectiveModel().getVersion()); - } - - @Test - public void testRepositoryUrlInterpolationWithProfiles() { - // Test case 1: Default properties should be used - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("repository-url-profiles"))) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - assertEquals( - "http://default.repo.com/repository/maven-public/", - result.getEffectiveModel().getRepositories().get(0).getUrl()); - - // Test case 2: Development profile should override repository URL - request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("repository-url-profiles"))) - .activeProfileIds(List.of("development")) - .build(); - result = builder.newSession().build(request); - assertNotNull(result); - assertEquals( - "http://dev.repo.com/repository/maven-public/", - result.getEffectiveModel().getRepositories().get(0).getUrl()); - - // Test case 3: Production profile should override repository URL - request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("repository-url-profiles"))) - .activeProfileIds(List.of("production")) - .build(); - result = builder.newSession().build(request); - assertNotNull(result); - assertEquals( - "http://prod.repo.com/repository/maven-public/", - result.getEffectiveModel().getRepositories().get(0).getUrl()); - } - - @Test - public void testDirectoryPropertiesInProfilesAndRepositories() { - // Test that directory properties (like ${project.basedir}) are available - // during profile activation and repository URL interpolation - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("directory-properties-profiles"))) - .activeProfileIds(List.of("local-repo")) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - - // Verify CI-friendly version was resolved with profile properties - assertEquals("1.0.0-LOCAL", result.getEffectiveModel().getVersion()); - - // Verify repository URL was interpolated with directory properties from profile - String expectedUrl = - "file://" + getPom("directory-properties-profiles").getParent().toString() + "/local-repo"; - assertEquals( - expectedUrl, result.getEffectiveModel().getRepositories().get(0).getUrl()); - } - - @Test - public void testCiFriendlyDependencyVersionInterpolation() { - // Test that ${revision} in dependency versions is interpolated using model properties - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("ci-friendly-deps"))) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - Model effective = result.getEffectiveModel(); - assertEquals("1.0.0-SNAPSHOT", effective.getVersion()); - assertEquals(1, effective.getDependencies().size()); - assertEquals( - "1.0.0-SNAPSHOT", - effective.getDependencies().get(0).getVersion(), - "${revision} in dependency version should be interpolated"); - assertNotNull(effective.getDistributionManagement()); - assertEquals( - "releases-1.0.0-SNAPSHOT", - effective.getDistributionManagement().getRepository().getId(), - "${revision} in distributionManagement repository id should be interpolated"); - } - - @Test - public void testCiFriendlyDependencyVersionWithUserProperties() { - // Test that ${revision} in dependency versions is interpolated using user properties override - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .userProperties(Map.of("revision", "2.0.0")) - .source(Sources.buildSource(getPom("ci-friendly-deps"))) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - Model effective = result.getEffectiveModel(); - assertEquals("2.0.0", effective.getVersion()); - assertEquals(1, effective.getDependencies().size()); - assertEquals( - "2.0.0", - effective.getDependencies().get(0).getVersion(), - "${revision} in dependency version should be interpolated with user property"); - } - - @Test - public void testCiFriendlyDependencyVersionWithUserPropertiesOnly() { - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .userProperties(Map.of("revision", "3.0.0")) - .source(Sources.buildSource(getPom("ci-friendly-deps-no-prop"))) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - Model effective = result.getEffectiveModel(); - assertEquals("3.0.0", effective.getVersion(), "project version should use user property"); - assertEquals(1, effective.getDependencies().size()); - assertEquals( - "3.0.0", - effective.getDependencies().get(0).getVersion(), - "${revision} in dependency version should be interpolated with user-only property"); - } - - @Test - public void testMissingDependencyGroupIdInference() throws Exception { - // Test that dependencies with missing groupId but present version are inferred correctly in model 4.1.0 - - // Create the main model with a dependency that has missing groupId but present version - Model model = Model.newBuilder() - .modelVersion("4.1.0") - .groupId("com.example.test") - .artifactId("app") - .version("1.0.0-SNAPSHOT") - .dependencies(Arrays.asList(Dependency.newBuilder() - .artifactId("service") - .version("${project.version}") - .build())) - .build(); - - // Build the model to trigger the transformation - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("missing-dependency-groupId-41-app"))) - .build(); - - try { - ModelBuilderResult result = builder.newSession().build(request); - // The dependency should have its groupId inferred from the project - assertEquals(1, result.getEffectiveModel().getDependencies().size()); - assertEquals( - "com.example.test", - result.getEffectiveModel().getDependencies().get(0).getGroupId()); - assertEquals( - "service", - result.getEffectiveModel().getDependencies().get(0).getArtifactId()); - } catch (Exception e) { - // If the build fails due to missing dependency, that's expected in this test environment - // The important thing is that our code change doesn't break compilation - // We'll verify the fix with a simpler unit test - assertEquals(1, model.getDependencies().size()); - assertNull(model.getDependencies().get(0).getGroupId()); - assertEquals("service", model.getDependencies().get(0).getArtifactId()); - assertEquals("${project.version}", model.getDependencies().get(0).getVersion()); + Session session; + ModelBuilder builder; + + @BeforeEach + void setup() { + session = ApiRunner.createSession(); + builder = session.getService(ModelBuilder.class); + assertNotNull(builder); } - } - - /** - * Verify that building a model from a resolved source (null pomFile) does not throw - * a NullPointerException. This simulates the scenario from GH-11919 where the - * cyclonedx-maven-plugin resolves a dependency POM from the repository, which - * produces a ModelSource whose {@code getPath()} returns {@code null}. - */ - @Test - public void testResolvedSourceWithNullPomFile() { - Path pomPath = getPom("resolved-dependency"); - // resolvedSource returns null for getPath(), simulating a dependency POM - // resolved from a remote repository (not a local project build) - ModelBuilderRequest request = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY) - .source(Sources.resolvedSource(pomPath, "org.example:resolved-dep:1.0.0")) - .build(); - ModelBuilderResult result = builder.newSession().build(request); - assertNotNull(result); - assertNotNull(result.getEffectiveModel()); - assertNull(result.getEffectiveModel().getPomFile(), "pomFile should be null for resolved sources"); - assertEquals("org.example", result.getEffectiveModel().getGroupId()); - assertEquals("resolved-dep", result.getEffectiveModel().getArtifactId()); - assertEquals("1.0.0", result.getEffectiveModel().getVersion()); - } - - /** - * Verifies that when a BUILD_CONSUMER derived session is created with explicit - * repositories, those repositories are propagated to the derived session's - * {@code repositories} and {@code externalRepositories}. - *

- * This is critical for consumer POM building: the consumer POM builder reuses the - * existing {@code ModelBuilderSession} and calls {@code build()} with a request - * containing the project's repositories (which may include non-central repos from - * settings.xml profiles). Without this, BOM imports from non-central repositories fail. - */ - @Test - public void testBuildConsumerWithExplicitRepositories() { - // First build to create the mainSession (simulates project build phase) - ModelBuilderRequest firstRequest = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("simple-standalone"))) - .build(); - ModelBuilder.ModelBuilderSession mbs = builder.newSession(); - mbs.build(firstRequest); - - // Access the mainSession (package-private) to call derive() and verify state - DefaultModelBuilder.ModelBuilderSessionState mainState = - ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; - - // Verify the main session only has central - assertEquals(1, mainState.getRepositories().size()); - assertEquals("central", mainState.getRepositories().get(0).getId()); - - // Derive a BUILD_CONSUMER session with explicit repositories - RemoteRepository customRepo = session.createRemoteRepository("custom-repo", "https://repo.example.com/maven2"); - ModelBuilderRequest consumerRequest = ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) - .source(Sources.buildSource(getPom("simple-standalone"))) - .repositories(List.of( - customRepo, session.createRemoteRepository("central", "https://repo.maven.apache.org/maven2"))) - .build(); - - DefaultModelBuilder.ModelBuilderSessionState derived = mainState.derive(consumerRequest); - - // Verify the derived session includes the custom repository - assertTrue( - derived.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), - "Derived session repositories should include the custom repo from the request"); - assertTrue( - derived.getExternalRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), - "Derived session externalRepositories should include the custom repo from the request"); - } - - /** - * Verifies that BUILD_CONSUMER resolves properties defined in parent POM profiles - * when the parent is found via reactor model resolution (mappedSources). - */ - @Test - public void testBuildConsumerResolvesParentProfileProperties() { - Path parentPom = getPom("consumer-profile-property-parent"); - Path childPom = getPom("consumer-profile-property-child"); - - ModelBuilder.ModelBuilderSession mbs = builder.newSession(); - - mbs.build(ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(parentPom)) - .build()); - - ModelBuilderResult consumerResult = assertDoesNotThrow( - () -> mbs.build(ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) - .source(Sources.buildSource(childPom)) - .build()), - "BUILD_CONSUMER should not fail when parent defines properties in profiles"); - - assertNotNull(consumerResult); - Model effectiveModel = consumerResult.getEffectiveModel(); - assertNotNull(effectiveModel); - - assertEquals( - "1.2.3", - effectiveModel.getProperties().get("managed.version"), - "Property from parent's profile should be resolved in BUILD_CONSUMER effective model"); - - assertNotNull(effectiveModel.getDependencyManagement()); - Dependency managedDep = effectiveModel.getDependencyManagement().getDependencies().stream() - .filter(d -> "managed-lib".equals(d.getArtifactId())) - .findFirst() - .orElse(null); - assertNotNull(managedDep, "Managed dependency from parent should be inherited"); - assertEquals( - "1.2.3", - managedDep.getVersion(), - "Managed dependency version should be interpolated, not ${managed.version}"); - } - - /** - * Verifies that version inference works for dependencies declared in - * {@code }, not just in {@code }. - * This is the regression fixed by https://github.com/apache/maven/issues/11147. - */ - @Test - public void testBomDependencyManagementVersionInference() { - // Build the lib POM to create the mainSession and register the lib in mappedSources - ModelBuilder.ModelBuilderSession mbs = builder.newSession(); - mbs.build(ModelBuilderRequest.builder() - .session(session) - .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) - .source(Sources.buildSource(getPom("bom-dep-mgmt-lib"))) - .build()); - - // Access the mainSession to call transformFileToRaw directly - DefaultModelBuilder.ModelBuilderSessionState mainState = - ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; - - // Create a synthetic model with a dependencyManagement dependency missing a version - Model bomModel = Model.newBuilder() - .modelVersion("4.1.0") - .groupId("org.apache.maven.tests") - .artifactId("bom-dep-mgmt-bom") - .version("1.0-SNAPSHOT") - .packaging("pom") - .pomFile(getPom("bom-dep-mgmt-bom")) - .dependencyManagement(org.apache.maven.api.model.DependencyManagement.newBuilder() - .dependencies(Arrays.asList(Dependency.newBuilder() + + @Test + public void testPropertiesAndProfiles() { + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("props-and-profiles"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + assertEquals("21", result.getEffectiveModel().getProperties().get("maven.compiler.release")); + } + + @Test + public void testMergeRepositories() throws Exception { + // this is here only to trigger mainSession creation; unrelated + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .userProperties(Map.of("firstParentRepo", "https://some.repo")) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("props-and-profiles"))) + .build(); + ModelBuilder.ModelBuilderSession session = builder.newSession(); + session.build(request); // ignored result value; just to trigger mainSession creation + + Field mainSessionField = DefaultModelBuilder.ModelBuilderSessionImpl.class + .getDeclaredField("mainSession"); + mainSessionField.setAccessible(true); + DefaultModelBuilder.ModelBuilderSessionState state = (DefaultModelBuilder.ModelBuilderSessionState) mainSessionField + .get(session); + Field repositoriesField = DefaultModelBuilder.ModelBuilderSessionState.class + .getDeclaredField("repositories"); + repositoriesField.setAccessible(true); + + List repositories; + // before merge + repositories = (List) repositoriesField.get(state); + assertEquals(1, repositories.size()); // central + + Model model = Model.newBuilder() + .properties(Map.of("thirdParentRepo", "https://third.repo")) + .repositories(Arrays.asList( + Repository.newBuilder() + .id("first") + .url("${firstParentRepo}") + .build(), + Repository.newBuilder() + .id("second") + .url("${secondParentRepo}") + .build(), + Repository.newBuilder() + .id("third") + .url("${thirdParentRepo}") + .build(), + Repository.newBuilder() + .id("${uninterpolatedRepoId}") + .url("https://valid.url") + .build())) + .build(); + + state.mergeRepositories(model, false); + + // after merge: "second" filtered (uninterpolated URL), + // "${uninterpolatedRepoId}" filtered (uninterpolated ID) + repositories = (List) repositoriesField.get(state); + assertEquals(3, repositories.size()); + assertEquals("first", repositories.get(0).getId()); + assertEquals("https://some.repo", repositories.get(0).getUrl()); // interpolated (user properties) + assertEquals("third", repositories.get(1).getId()); + assertEquals("https://third.repo", repositories.get(1).getUrl()); // interpolated (own model properties) + assertEquals("central", repositories.get(2).getId()); // default + } + + @Test + public void testCiFriendlyVersionWithProfiles() { + // Test case 1: Default profile should set revision to baseVersion+dev + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("ci-friendly-profiles"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + assertEquals("0.2.0+dev", result.getEffectiveModel().getVersion()); + + // Test case 2: Release profile should set revision to baseVersion only + request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("ci-friendly-profiles"))) + .activeProfileIds(List.of("releaseBuild")) + .build(); + result = builder.newSession().build(request); + assertNotNull(result); + assertEquals("0.2.0", result.getEffectiveModel().getVersion()); + } + + @Test + public void testRepositoryUrlInterpolationWithProfiles() { + // Test case 1: Default properties should be used + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("repository-url-profiles"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + assertEquals( + "http://default.repo.com/repository/maven-public/", + result.getEffectiveModel().getRepositories().get(0).getUrl()); + + // Test case 2: Development profile should override repository URL + request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("repository-url-profiles"))) + .activeProfileIds(List.of("development")) + .build(); + result = builder.newSession().build(request); + assertNotNull(result); + assertEquals( + "http://dev.repo.com/repository/maven-public/", + result.getEffectiveModel().getRepositories().get(0).getUrl()); + + // Test case 3: Production profile should override repository URL + request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("repository-url-profiles"))) + .activeProfileIds(List.of("production")) + .build(); + result = builder.newSession().build(request); + assertNotNull(result); + assertEquals( + "http://prod.repo.com/repository/maven-public/", + result.getEffectiveModel().getRepositories().get(0).getUrl()); + } + + @Test + public void testDirectoryPropertiesInProfilesAndRepositories() { + // Test that directory properties (like ${project.basedir}) are available + // during profile activation and repository URL interpolation + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("directory-properties-profiles"))) + .activeProfileIds(List.of("local-repo")) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + + // Verify CI-friendly version was resolved with profile properties + assertEquals("1.0.0-LOCAL", result.getEffectiveModel().getVersion()); + + // Verify repository URL was interpolated with directory properties from profile + String expectedUrl = "file://" + getPom("directory-properties-profiles").getParent().toString() + + "/local-repo"; + assertEquals( + expectedUrl, result.getEffectiveModel().getRepositories().get(0).getUrl()); + } + + @Test + public void testCiFriendlyDependencyVersionInterpolation() { + // Test that ${revision} in dependency versions is interpolated using model + // properties + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("ci-friendly-deps"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + Model effective = result.getEffectiveModel(); + assertEquals("1.0.0-SNAPSHOT", effective.getVersion()); + assertEquals(1, effective.getDependencies().size()); + assertEquals( + "1.0.0-SNAPSHOT", + effective.getDependencies().get(0).getVersion(), + "${revision} in dependency version should be interpolated"); + assertNotNull(effective.getDistributionManagement()); + assertEquals( + "releases-1.0.0-SNAPSHOT", + effective.getDistributionManagement().getRepository().getId(), + "${revision} in distributionManagement repository id should be interpolated"); + } + + @Test + public void testCiFriendlyDependencyVersionWithUserProperties() { + // Test that ${revision} in dependency versions is interpolated using user + // properties override + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .userProperties(Map.of("revision", "2.0.0")) + .source(Sources.buildSource(getPom("ci-friendly-deps"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + Model effective = result.getEffectiveModel(); + assertEquals("2.0.0", effective.getVersion()); + assertEquals(1, effective.getDependencies().size()); + assertEquals( + "2.0.0", + effective.getDependencies().get(0).getVersion(), + "${revision} in dependency version should be interpolated with user property"); + } + + @Test + public void testCiFriendlyDependencyVersionWithUserPropertiesOnly() { + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .userProperties(Map.of("revision", "3.0.0")) + .source(Sources.buildSource(getPom("ci-friendly-deps-no-prop"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + Model effective = result.getEffectiveModel(); + assertEquals("3.0.0", effective.getVersion(), "project version should use user property"); + assertEquals(1, effective.getDependencies().size()); + assertEquals( + "3.0.0", + effective.getDependencies().get(0).getVersion(), + "${revision} in dependency version should be interpolated with user-only property"); + } + + @Test + public void testMissingDependencyGroupIdInference() throws Exception { + // Test that dependencies with missing groupId but present version are inferred + // correctly in model 4.1.0 + + // Create the main model with a dependency that has missing groupId but present + // version + Model model = Model.newBuilder() + .modelVersion("4.1.0") + .groupId("com.example.test") + .artifactId("app") + .version("1.0.0-SNAPSHOT") + .dependencies(Arrays.asList(Dependency.newBuilder() + .artifactId("service") + .version("${project.version}") + .build())) + .build(); + + // Build the model to trigger the transformation + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("missing-dependency-groupId-41-app"))) + .build(); + + try { + ModelBuilderResult result = builder.newSession().build(request); + // The dependency should have its groupId inferred from the project + assertEquals(1, result.getEffectiveModel().getDependencies().size()); + assertEquals( + "com.example.test", + result.getEffectiveModel().getDependencies().get(0).getGroupId()); + assertEquals( + "service", + result.getEffectiveModel().getDependencies().get(0).getArtifactId()); + } catch (Exception e) { + // If the build fails due to missing dependency, that's expected in this test + // environment + // The important thing is that our code change doesn't break compilation + // We'll verify the fix with a simpler unit test + assertEquals(1, model.getDependencies().size()); + assertNull(model.getDependencies().get(0).getGroupId()); + assertEquals("service", model.getDependencies().get(0).getArtifactId()); + assertEquals("${project.version}", model.getDependencies().get(0).getVersion()); + } + } + + /** + * Verify that building a model from a resolved source (null pomFile) does not + * throw + * a NullPointerException. This simulates the scenario from GH-11919 where the + * cyclonedx-maven-plugin resolves a dependency POM from the repository, which + * produces a ModelSource whose {@code getPath()} returns {@code null}. + */ + @Test + public void testResolvedSourceWithNullPomFile() { + Path pomPath = getPom("resolved-dependency"); + // resolvedSource returns null for getPath(), simulating a dependency POM + // resolved from a remote repository (not a local project build) + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY) + .source(Sources.resolvedSource(pomPath, "org.example:resolved-dep:1.0.0")) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + assertNotNull(result.getEffectiveModel()); + assertNull(result.getEffectiveModel().getPomFile(), "pomFile should be null for resolved sources"); + assertEquals("org.example", result.getEffectiveModel().getGroupId()); + assertEquals("resolved-dep", result.getEffectiveModel().getArtifactId()); + assertEquals("1.0.0", result.getEffectiveModel().getVersion()); + } + + /** + * Verifies that when a BUILD_CONSUMER derived session is created with explicit + * repositories, those repositories are propagated to the derived session's + * {@code repositories} and {@code externalRepositories}. + *

+ * This is critical for consumer POM building: the consumer POM builder reuses + * the + * existing {@code ModelBuilderSession} and calls {@code build()} with a request + * containing the project's repositories (which may include non-central repos + * from + * settings.xml profiles). Without this, BOM imports from non-central + * repositories fail. + */ + @Test + public void testBuildConsumerWithExplicitRepositories() { + // First build to create the mainSession (simulates project build phase) + ModelBuilderRequest firstRequest = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("simple-standalone"))) + .build(); + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + mbs.build(firstRequest); + + // Access the mainSession (package-private) to call derive() and verify state + DefaultModelBuilder.ModelBuilderSessionState mainState = ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; + + // Verify the main session only has central + assertEquals(1, mainState.getRepositories().size()); + assertEquals("central", mainState.getRepositories().get(0).getId()); + + // Derive a BUILD_CONSUMER session with explicit repositories + RemoteRepository customRepo = session.createRemoteRepository("custom-repo", + "https://repo.example.com/maven2"); + ModelBuilderRequest consumerRequest = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(getPom("simple-standalone"))) + .repositories(List.of( + customRepo, + session.createRemoteRepository("central", + "https://repo.maven.apache.org/maven2"))) + .build(); + + DefaultModelBuilder.ModelBuilderSessionState derived = mainState.derive(consumerRequest); + + // Verify the derived session includes the custom repository + assertTrue( + derived.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), + "Derived session repositories should include the custom repo from the request"); + assertTrue( + derived.getExternalRepositories().stream() + .anyMatch(r -> "custom-repo".equals(r.getId())), + "Derived session externalRepositories should include the custom repo from the request"); + } + + /** + * Verifies that BUILD_CONSUMER resolves properties defined in parent POM + * profiles + * when the parent is found via reactor model resolution (mappedSources). + */ + @Test + public void testBuildConsumerResolvesParentProfileProperties() { + Path parentPom = getPom("consumer-profile-property-parent"); + Path childPom = getPom("consumer-profile-property-child"); + + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(parentPom)) + .build()); + + ModelBuilderResult consumerResult = assertDoesNotThrow( + () -> mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_CONSUMER) + .source(Sources.buildSource(childPom)) + .build()), + "BUILD_CONSUMER should not fail when parent defines properties in profiles"); + + assertNotNull(consumerResult); + Model effectiveModel = consumerResult.getEffectiveModel(); + assertNotNull(effectiveModel); + + assertEquals( + "1.2.3", + effectiveModel.getProperties().get("managed.version"), + "Property from parent's profile should be resolved in BUILD_CONSUMER effective model"); + + assertNotNull(effectiveModel.getDependencyManagement()); + Dependency managedDep = effectiveModel.getDependencyManagement().getDependencies().stream() + .filter(d -> "managed-lib".equals(d.getArtifactId())) + .findFirst() + .orElse(null); + assertNotNull(managedDep, "Managed dependency from parent should be inherited"); + assertEquals( + "1.2.3", + managedDep.getVersion(), + "Managed dependency version should be interpolated, not ${managed.version}"); + } + + /** + * Verifies that version inference works for dependencies declared in + * {@code }, not just in {@code }. + * This is the regression fixed by https://github.com/apache/maven/issues/11147. + */ + @Test + public void testBomDependencyManagementVersionInference() { + // Build the lib POM to create the mainSession and register the lib in + // mappedSources + ModelBuilder.ModelBuilderSession mbs = builder.newSession(); + mbs.build(ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("bom-dep-mgmt-lib"))) + .build()); + + // Access the mainSession to call transformFileToRaw directly + DefaultModelBuilder.ModelBuilderSessionState mainState = ((DefaultModelBuilder.ModelBuilderSessionImpl) mbs).mainSession; + + // Create a synthetic model with a dependencyManagement dependency missing a + // version + Model bomModel = Model.newBuilder() + .modelVersion("4.1.0") .groupId("org.apache.maven.tests") - .artifactId("bom-dep-mgmt-lib") - // version intentionally omitted - .build())) - .build()) - .build(); - - // Transform — this should infer the version from the reactor - Model transformed = mainState.transformFileToRaw(bomModel); - - assertNotNull(transformed.getDependencyManagement()); - Dependency managedDep = transformed.getDependencyManagement().getDependencies().stream() - .filter(d -> "bom-dep-mgmt-lib".equals(d.getArtifactId())) - .findFirst() - .orElse(null); - assertNotNull(managedDep, "Managed dependency for sibling lib should exist"); - assertEquals("1.0-SNAPSHOT", managedDep.getVersion(), "Version should be inferred from the reactor sibling module"); - } - - private Path getPom(String name) { - return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); - } + .artifactId("bom-dep-mgmt-bom") + .version("1.0-SNAPSHOT") + .packaging("pom") + .pomFile(getPom("bom-dep-mgmt-bom")) + .dependencyManagement(org.apache.maven.api.model.DependencyManagement.newBuilder() + .dependencies(Arrays.asList(Dependency.newBuilder() + .groupId("org.apache.maven.tests") + .artifactId("bom-dep-mgmt-lib") + // version intentionally omitted + .build())) + .build()) + .build(); + + // Transform — this should infer the version from the reactor + Model transformed = mainState.transformFileToRaw(bomModel); + + assertNotNull(transformed.getDependencyManagement()); + Dependency managedDep = transformed.getDependencyManagement().getDependencies().stream() + .filter(d -> "bom-dep-mgmt-lib".equals(d.getArtifactId())) + .findFirst() + .orElse(null); + assertNotNull(managedDep, "Managed dependency for sibling lib should exist"); + assertEquals( + "1.0-SNAPSHOT", + managedDep.getVersion(), + "Version should be inferred from the reactor sibling module"); + } + + private Path getPom(String name) { + return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); + } }