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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.ModelSource;
import org.apache.maven.api.services.model.LifecycleBindingsInjector;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.internal.impl.InternalMavenSession;
import org.apache.maven.model.v4.MavenModelVersion;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.SourceQueries;
Expand Down Expand Up @@ -161,14 +163,14 @@ public Model build(RepositorySystemSession session, MavenProject project, ModelS

protected Model buildPom(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
ModelBuilderResult result = buildModel(session, src);
ModelBuilderResult result = buildModel(session, project, src);
Model model = result.getRawModel();
return transformPom(model, project);
}

protected Model buildBomWithoutFlatten(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
ModelBuilderResult result = buildModel(session, src);
ModelBuilderResult result = buildModel(session, project, src);
Model model = result.getRawModel();
// For BOMs without flattening, we just need to transform the packaging from "bom" to "pom"
// but keep everything else from the raw model (including unresolved versions)
Expand All @@ -177,20 +179,21 @@ protected Model buildBomWithoutFlatten(RepositorySystemSession session, MavenPro

protected Model buildBom(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
ModelBuilderResult result = buildModel(session, src);
ModelBuilderResult result = buildModel(session, project, src);
Model model = result.getEffectiveModel();
return transformBom(model, project);
}

protected Model buildNonPom(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
Model model = buildEffectiveModel(session, src);
Model model = buildEffectiveModel(session, project, src);
return transformNonPom(model, project);
}

private Model buildEffectiveModel(RepositorySystemSession session, ModelSource src) throws ModelBuilderException {
private Model buildEffectiveModel(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
InternalSession iSession = InternalSession.from(session);
ModelBuilderResult result = buildModel(session, src);
ModelBuilderResult result = buildModel(session, project, src);
Model model = result.getEffectiveModel();

if (model.getDependencyManagement() != null
Expand Down Expand Up @@ -295,7 +298,7 @@ private static String getDependencyKey(Dependency dependency) {
+ (dependency.getClassifier() != null ? dependency.getClassifier() : "");
}

private ModelBuilderResult buildModel(RepositorySystemSession session, ModelSource src)
private ModelBuilderResult buildModel(RepositorySystemSession session, MavenProject project, ModelSource src)
throws ModelBuilderException {
InternalSession iSession = InternalSession.from(session);
ModelBuilderRequest.ModelBuilderRequestBuilder request = ModelBuilderRequest.builder();
Expand All @@ -306,6 +309,38 @@ private ModelBuilderResult buildModel(RepositorySystemSession session, ModelSour
request.systemProperties(session.getSystemProperties());
request.userProperties(session.getUserProperties());
request.lifecycleBindingsInjector(lifecycleBindingsInjector::injectLifecycleBindings);
// Pass remote repositories so that the model builder can resolve BOM imports
// from non-central repositories (e.g., repositories defined in settings.xml profiles).
// Prefer project repositories, but fall back to session repositories if the project's
// remote repository list is not populated (e.g., during install/deploy phases).
if (project != null
&& project.getRemoteProjectRepositories() != null
&& !project.getRemoteProjectRepositories().isEmpty()) {
request.repositories(project.getRemoteProjectRepositories().stream()
.map(iSession::getRemoteRepository)
.toList());
} else {
request.repositories(iSession.getRemoteRepositories());
}
// Pass profiles and active/inactive profile IDs from the execution request
// so that settings.xml profiles are applied during consumer POM model building.
if (iSession instanceof InternalMavenSession mavenSession) {
MavenExecutionRequest executionRequest =
mavenSession.getMavenSession().getRequest();
if (executionRequest.getProfiles() != null) {
request.profiles(executionRequest.getProfiles().stream()
.map(org.apache.maven.model.Profile::getDelegate)
.toList());
}
request.activeProfileIds(executionRequest.getActiveProfiles());
request.inactiveProfileIds(executionRequest.getInactiveProfiles());
} else {
LOGGER.debug(
"Session is not an InternalMavenSession ({}); settings.xml profiles will not be "
+ "passed to the consumer POM model builder. BOM imports from repositories "
+ "defined only in settings.xml profiles may fail to resolve.",
iSession.getClass().getName());
}
ModelBuilder.ModelBuilderSession mbSession =
iSession.getData().get(SessionData.key(ModelBuilder.ModelBuilderSession.class));
return mbSession.build(request.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
import org.apache.maven.internal.impl.InternalMavenSession;
import org.apache.maven.internal.transformation.AbstractRepositoryTestCase;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.repository.RemoteRepository;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertFalse;
Expand Down Expand Up @@ -198,4 +200,65 @@ void testScmInheritance() throws Exception {
assertNull(transformed.getScm().getChildScmUrlInheritAppendPath());
assertNull(transformed.getScm().getChildScmDeveloperConnectionInheritAppendPath());
}

/**
* Verifies that the consumer POM builder passes the project's remote repositories
* to the model builder request, so that BOM imports from non-central repositories
* (e.g. repositories defined in settings.xml profiles) can be resolved.
* <p>
* Without the fix in {@code DefaultConsumerPomBuilder.buildModel()}, the
* {@code ModelBuilderRequest} is constructed without repositories, profiles, or
* active profile IDs. This causes the model builder to only see Maven Central
* when resolving BOM imports, leading to "Non-resolvable import POM" failures
* for artifacts hosted in private/corporate repositories.
*/
@Test
void testConsumerPomPassesProjectRepositoriesToModelBuilder() throws Exception {
setRootDirectory("trivial");
Path file = Paths.get("src/test/resources/consumer/trivial/child/pom.xml");

MavenProject project = getEffectiveModel(file);

// Add a custom remote repository to the project, simulating a repository
// injected from settings.xml profile (e.g. a corporate/private repository)
RemoteRepository customRepo =
new RemoteRepository.Builder("custom-repo", "default", "https://repo.example.com/maven2").build();
project.getRemoteProjectRepositories().add(customRepo);

// Spy on the ModelBuilderSession to capture the ModelBuilderRequest
ModelBuilder.ModelBuilderSession originalMbs = modelBuilder.newSession();
ModelBuilder.ModelBuilderSession spyMbs = Mockito.spy(originalMbs);
InternalSession.from(session).getData().set(SessionData.key(ModelBuilder.ModelBuilderSession.class), spyMbs);

// Build the consumer POM
builder.build(session, project, Sources.buildSource(file));

// Capture the ModelBuilderRequest passed to the ModelBuilderSession
ArgumentCaptor<ModelBuilderRequest> requestCaptor = ArgumentCaptor.forClass(ModelBuilderRequest.class);
Mockito.verify(spyMbs, Mockito.atLeastOnce()).build(requestCaptor.capture());

// Find the BUILD_CONSUMER request (there may be multiple calls)
ModelBuilderRequest consumerRequest = requestCaptor.getAllValues().stream()
.filter(r -> r.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER)
.findFirst()
.orElse(null);

assertNotNull(consumerRequest, "Expected a BUILD_CONSUMER request to be made");

// Verify that repositories were passed to the request.
// Without the fix, getRepositories() returns null because buildModel() never sets them.
assertNotNull(
consumerRequest.getRepositories(),
"Consumer POM model builder request should include repositories from the project. "
+ "Without this, BOM imports from non-central repositories (e.g. settings.xml profiles) "
+ "cannot be resolved, causing 'Non-resolvable import POM' errors.");
assertFalse(
consumerRequest.getRepositories().isEmpty(),
"Consumer POM model builder request should have at least one repository");

// Verify the custom repository is included
boolean hasCustomRepo =
consumerRequest.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId()));
assertTrue(hasCustomRepo, "Consumer POM model builder request should include the project's custom repository");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ protected class ModelBuilderSessionState implements ModelProblemCollector {
List<RemoteRepository> externalRepositories;
List<RemoteRepository> repositories;

List<RemoteRepository> getRepositories() {
return repositories;
}

List<RemoteRepository> getExternalRepositories() {
return externalRepositories;
}

// Cycle detection chain shared across all derived sessions
// Contains both GAV coordinates (groupId:artifactId:version) and file paths
final Set<String> parentChain;
Expand Down Expand Up @@ -345,15 +353,32 @@ ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilder
}
// 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).
// This is scoped to BUILD_CONSUMER to avoid unintended side effects on other
// derived sessions (e.g., parent POM resolution during project builds).
List<RemoteRepository> derivedExtRepos = externalRepositories;
List<RemoteRepository> derivedRepos = repositories;
if (request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER
&& request.getRepositories() != null
&& !request.getRepositories().isEmpty()) {
derivedExtRepos = List.copyOf(request.getRepositories());
if (pomRepositories.isEmpty()) {
derivedRepos = derivedExtRepos;
} else {
RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class);
derivedRepos = repositoryFactory.aggregate(session, pomRepositories, derivedExtRepos, false);
}
}
return new ModelBuilderSessionState(
session,
request,
result,
dag,
mappedSources,
pomRepositories,
externalRepositories,
repositories,
derivedExtRepos,
derivedRepos,
new LinkedHashSet<>());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
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 @@ -254,6 +255,56 @@ public void testMissingDependencyGroupIdInference() throws Exception {
}
}

/**
* 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}.
* <p>
* 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");
}

private Path getPom(String name) {
return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.1.0">
<groupId>org.apache.maven.tests</groupId>
<artifactId>simple-standalone</artifactId>
<version>1.0</version>
</project>
Loading