diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java index 8ef3802062ea..963d29acccdb 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java @@ -32,6 +32,8 @@ import org.apache.maven.api.model.Repository; import org.apache.maven.api.services.ArtifactCoordinatesFactory; import org.apache.maven.api.services.DependencyCoordinatesFactory; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.ProblemCollector; import org.apache.maven.api.services.VersionResolverException; import org.apache.maven.api.settings.Settings; import org.apache.maven.api.toolchain.ToolchainModel; @@ -93,6 +95,25 @@ public interface Session extends ProtoSession { @Nonnull SessionData getData(); + /** + * Returns the model problems encountered while discovering the projects in this session's reactor. + * + * @return the session model problem collector, never {@code null} + */ + @Nonnull + default ProblemCollector getModelProblemCollector() { + return SessionModelProblems.getProblemCollector(this); + } + + /** + * Returns whether model problems were encountered while discovering the projects in this session's reactor. + * + * @return {@code true} if at least one model problem was encountered + */ + default boolean hasModelProblems() { + return getModelProblemCollector().hasWarningProblems(); + } + /** * Default implementation at {@link ProtoSession} level, as the notion of project * does not exist there. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/SessionModelProblems.java b/api/maven-api-core/src/main/java/org/apache/maven/api/SessionModelProblems.java new file mode 100644 index 000000000000..f5df598bedba --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/SessionModelProblems.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api; + +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.ProblemCollector; + +final class SessionModelProblems { + + private static final SessionData.Key KEY = SessionData.key(State.class, SessionModelProblems.class); + + private SessionModelProblems() {} + + static ProblemCollector getProblemCollector(Session session) { + return session.getData() + .computeIfAbsent(KEY, () -> new State(ProblemCollector.create(session))) + .problemCollector; + } + + private static final class State { + + private final ProblemCollector problemCollector; + + private State(ProblemCollector problemCollector) { + this.problemCollector = problemCollector; + } + } +} diff --git a/impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java b/impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java index 400e1653cc31..d3f49c9547d5 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java +++ b/impl/maven-core/src/main/java/org/apache/maven/execution/MavenSession.java @@ -34,6 +34,7 @@ import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.repository.RepositoryCache; import org.apache.maven.impl.SettingsUtilsV4; +import org.apache.maven.internal.impl.SessionModelProblemsBridge; import org.apache.maven.model.Profile; import org.apache.maven.monitor.event.EventDispatcher; import org.apache.maven.plugin.descriptor.PluginDescriptor; @@ -295,6 +296,8 @@ public void setAllProjects(List allProjects) { private Session session; + private boolean modelProblems; + @Deprecated /** @deprecated This appears not to be used anywhere within Maven itself. */ public Map getProjectMap() { @@ -496,8 +499,34 @@ public Session getSession() { return session; } + /** + * Returns whether the reactor projects had model problems. + * + * @return {@code true} if model problems were encountered + * @since 3.10.0 + */ + public boolean hasModelProblems() { + return modelProblems || (session != null && session.hasModelProblems()); + } + + /** + * Sets whether the reactor projects had model problems. + * + * @param modelProblems whether model problems were encountered + * @since 3.10.0 + */ + public void setModelProblems(boolean modelProblems) { + this.modelProblems = modelProblems; + if (session != null) { + SessionModelProblemsBridge.setLegacyFlag(session, modelProblems); + } + } + public void setSession(Session session) { this.session = session; + if (session != null) { + SessionModelProblemsBridge.setLegacyFlag(session, modelProblems); + } } /*end[MAVEN4]*/ } diff --git a/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java index 5f20f38e594c..c912263bd55b 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/graph/DefaultGraphBuilder.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import org.apache.maven.MavenExecutionException; import org.apache.maven.ProjectCycleException; @@ -349,22 +350,48 @@ private String formatProjects(List projects) { private List getProjectsForMavenReactor(MavenSession session) throws ProjectBuildingException { MavenExecutionRequest request = session.getRequest(); request.getProjectBuildingRequest().setRepositorySession(session.getRepositorySession()); + Consumer modelProblemConsumer = getModelProblemConsumer(session); // 1. Collect project for invocation without a POM. if (request.getPom() == null) { - return pomlessCollectionStrategy.collectProjects(request); + return pomlessCollectionStrategy.collectProjects(request, modelProblemConsumer); } // 2. Collect projects for all modules in the multi-module project. if (request.getMakeBehavior() != null || !request.getProjectActivation().isEmpty()) { - List projects = multiModuleCollectionStrategy.collectProjects(request); + List projects = multiModuleCollectionStrategy.collectProjects(request, modelProblemConsumer); if (!projects.isEmpty()) { return projects; } } // 3. Collect projects for explicitly requested POM. - return requestPomCollectionStrategy.collectProjects(request); + return requestPomCollectionStrategy.collectProjects(request, modelProblemConsumer); + } + + private Consumer getModelProblemConsumer(MavenSession session) { + org.apache.maven.api.Session apiSession = session.getSession(); + if (apiSession == null) { + return problem -> session.setModelProblems(true); + } + return problem -> apiSession.getModelProblemCollector().reportProblem(toApiModelProblem(problem)); + } + + private org.apache.maven.api.services.ModelProblem toApiModelProblem( + org.apache.maven.model.building.ModelProblem problem) { + return new org.apache.maven.impl.model.DefaultModelProblem( + problem.getMessage(), + org.apache.maven.api.services.BuilderProblem.Severity.valueOf( + problem.getSeverity().name()), + problem.getVersion() != null + ? org.apache.maven.api.services.ModelProblem.Version.valueOf( + problem.getVersion().name()) + : org.apache.maven.api.services.ModelProblem.Version.BASE, + problem.getSource(), + problem.getLineNumber(), + problem.getColumnNumber(), + problem.getModelId(), + problem.getException()); } private void validateProjects(List projects, MavenExecutionRequest request) diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java index 4e054d3a57da..276ddc97671c 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultSession.java @@ -180,6 +180,11 @@ public List getProjects() { return getProjects(getMavenSession().getProjects()); } + @Override + public boolean hasModelProblems() { + return SessionModelProblemsBridge.hasModelProblems(this); + } + @Nonnull @Override public Map getPluginContext(Project project) { diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SessionModelProblemsBridge.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SessionModelProblemsBridge.java new file mode 100644 index 000000000000..9ce4d2dd8151 --- /dev/null +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/SessionModelProblemsBridge.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.maven.api.Session; +import org.apache.maven.api.SessionData; + +import static java.util.Objects.requireNonNull; + +/** + * Stores the legacy model-problem flag in session data so that derived sessions see the same state. + */ +public final class SessionModelProblemsBridge { + + private static final SessionData.Key KEY = SessionData.key(State.class, SessionModelProblemsBridge.class); + + private SessionModelProblemsBridge() {} + + public static boolean hasModelProblems(Session session) { + return getState(session).legacyFlag.get() + || session.getModelProblemCollector().hasWarningProblems(); + } + + public static void setLegacyFlag(Session session, boolean value) { + getState(session).legacyFlag.set(value); + } + + private static State getState(Session session) { + requireNonNull(session, "session"); + return session.getData().computeIfAbsent(KEY, State::new); + } + + private static final class State { + + private final AtomicBoolean legacyFlag = new AtomicBoolean(); + } +} diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java b/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java index 0be344384adf..c721012b284e 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java @@ -25,6 +25,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.model.building.ModelProblem; @@ -54,6 +55,13 @@ public DefaultProjectsSelector(ProjectBuilder projectBuilder) { @Override public List selectProjects(List files, MavenExecutionRequest request) throws ProjectBuildingException { + return selectProjects(files, request, problem -> {}); + } + + @Override + public List selectProjects( + List files, MavenExecutionRequest request, Consumer problemConsumer) + throws ProjectBuildingException { ProjectBuildingRequest projectBuildingRequest = request.getProjectBuildingRequest(); boolean hasProjectSelection = !request.getProjectActivation().isEmpty(); @@ -66,6 +74,7 @@ public List selectProjects(List files, MavenExecutionRequest for (ProjectBuildingResult result : results) { projects.add(result.getProject()); + result.getProblems().forEach(problemConsumer); int problemsCount = result.getProblems().size(); totalProblemsCount += problemsCount; diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/collector/MultiModuleCollectionStrategy.java b/impl/maven-core/src/main/java/org/apache/maven/project/collector/MultiModuleCollectionStrategy.java index 3e6616a80cd4..6e86750aa312 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/collector/MultiModuleCollectionStrategy.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/collector/MultiModuleCollectionStrategy.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Predicate; import org.apache.maven.api.services.model.ModelProcessor; @@ -62,12 +63,19 @@ public MultiModuleCollectionStrategy(ModelProcessor modelProcessor, ProjectsSele @Override public List collectProjects(MavenExecutionRequest request) throws ProjectBuildingException { + return collectProjects(request, problem -> {}); + } + + public List collectProjects(MavenExecutionRequest request, Consumer problemConsumer) + throws ProjectBuildingException { File moduleProjectPomFile = getRootProject(request); List files = Collections.singletonList(moduleProjectPomFile.getAbsoluteFile()); try { - List projects = projectsSelector.selectProjects(files, request); + List problems = new ArrayList<>(); + List projects = projectsSelector.selectProjects(files, request, problems::add); boolean isRequestedProjectCollected = isRequestedProjectCollected(request, projects); if (isRequestedProjectCollected) { + problems.forEach(problemConsumer); return projects; } else { LOGGER.debug( diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/collector/PomlessCollectionStrategy.java b/impl/maven-core/src/main/java/org/apache/maven/project/collector/PomlessCollectionStrategy.java index bae54f3a5c95..f4331ab5b963 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/collector/PomlessCollectionStrategy.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/collector/PomlessCollectionStrategy.java @@ -24,15 +24,18 @@ import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; import org.apache.maven.DefaultMaven; import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.model.building.ModelProblem; import org.apache.maven.model.building.ModelSource; import org.apache.maven.model.building.UrlModelSource; import org.apache.maven.project.MavenProject; import org.apache.maven.project.ProjectBuilder; import org.apache.maven.project.ProjectBuildingException; import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.project.ProjectBuildingResult; /** * Strategy to collect projects for building when the Maven invocation is not in a directory that contains a pom.xml. @@ -49,10 +52,16 @@ public PomlessCollectionStrategy(ProjectBuilder projectBuilder) { @Override public List collectProjects(final MavenExecutionRequest request) throws ProjectBuildingException { + return collectProjects(request, problem -> {}); + } + + public List collectProjects(MavenExecutionRequest request, Consumer problemConsumer) + throws ProjectBuildingException { ProjectBuildingRequest buildingRequest = request.getProjectBuildingRequest(); ModelSource modelSource = new UrlModelSource(DefaultMaven.class.getResource("project/standalone.xml")); - MavenProject project = - projectBuilder.build(modelSource, buildingRequest).getProject(); + ProjectBuildingResult result = projectBuilder.build(modelSource, buildingRequest); + result.getProblems().forEach(problemConsumer); + MavenProject project = result.getProject(); project.setExecutionRoot(true); request.setProjectPresent(false); diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/collector/ProjectsSelector.java b/impl/maven-core/src/main/java/org/apache/maven/project/collector/ProjectsSelector.java index 74425f9149ae..20f8f5efb494 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/collector/ProjectsSelector.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/collector/ProjectsSelector.java @@ -20,8 +20,10 @@ import java.io.File; import java.util.List; +import java.util.function.Consumer; import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.model.building.ModelProblem; import org.apache.maven.project.MavenProject; import org.apache.maven.project.ProjectBuildingException; @@ -37,4 +39,19 @@ public interface ProjectsSelector { * @throws ProjectBuildingException In case the POMs are not used. */ List selectProjects(List files, MavenExecutionRequest request) throws ProjectBuildingException; + + /** + * Select Maven projects from a list of POM files and report model problems encountered while building them. + * + * @param files List of POM files. + * @param request The {@link MavenExecutionRequest} + * @param problemConsumer Consumer for model problems encountered while building the selected projects. + * @return A list of projects that have been found in the specified POM files. + * @throws ProjectBuildingException In case the POMs are not used. + */ + default List selectProjects( + List files, MavenExecutionRequest request, Consumer problemConsumer) + throws ProjectBuildingException { + return selectProjects(files, request); + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/collector/RequestPomCollectionStrategy.java b/impl/maven-core/src/main/java/org/apache/maven/project/collector/RequestPomCollectionStrategy.java index 8ba724e8f123..6fd50956fe68 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/collector/RequestPomCollectionStrategy.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/collector/RequestPomCollectionStrategy.java @@ -25,8 +25,10 @@ import java.io.File; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.model.building.ModelProblem; import org.apache.maven.project.MavenProject; import org.apache.maven.project.ProjectBuildingException; @@ -45,7 +47,12 @@ public RequestPomCollectionStrategy(ProjectsSelector projectsSelector) { @Override public List collectProjects(MavenExecutionRequest request) throws ProjectBuildingException { + return collectProjects(request, problem -> {}); + } + + public List collectProjects(MavenExecutionRequest request, Consumer problemConsumer) + throws ProjectBuildingException { List files = Collections.singletonList(request.getPom().getAbsoluteFile()); - return projectsSelector.selectProjects(files, request); + return projectsSelector.selectProjects(files, request, problemConsumer); } } diff --git a/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java index 72dad0b244d5..aedb88c912ce 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java @@ -29,6 +29,10 @@ import java.util.stream.Stream; import org.apache.maven.MavenExecutionException; +import org.apache.maven.api.Session; +import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.ModelProblem; +import org.apache.maven.api.services.ProblemCollector; import org.apache.maven.api.services.model.ModelProcessor; import org.apache.maven.execution.BuildResumptionDataRepository; import org.apache.maven.execution.MavenExecutionRequest; @@ -65,6 +69,7 @@ import static org.apache.maven.graph.DefaultGraphBuilderTest.ScenarioBuilder.scenario; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -99,7 +104,9 @@ class DefaultGraphBuilderTest { private final ProjectBuilder projectBuilder = mock(ProjectBuilder.class); private final MavenSession session = mock(MavenSession.class); + private final Session apiSession = mock(Session.class); private final MavenExecutionRequest mavenExecutionRequest = mock(MavenExecutionRequest.class); + private final ProblemCollector modelProblems = ProblemCollector.create(100); private final ProjectsSelector projectsSelector = new DefaultProjectsSelector(projectBuilder); @@ -372,6 +379,123 @@ void testProcessPackagingAttribute() throws ProjectBuildingException { assertEquals("pom", actualReactorProjects.get(1).getPackaging()); } + @Test + void selectedReactorModelProblemsAreRetainedInSession() throws ProjectBuildingException { + Exception cause = new IllegalStateException("model problem cause"); + org.apache.maven.model.building.ModelProblem legacyProblem = + new org.apache.maven.model.building.DefaultModelProblem( + "model warning", + org.apache.maven.model.building.ModelProblem.Severity.WARNING, + org.apache.maven.model.building.ModelProblem.Version.V40, + "module-parent/pom.xml", + 12, + 4, + "unittest:module-parent:1.0", + cause); + List projectBuildingResults = + createProjectBuildingResultMocks(artifactIdProjectMap.values()); + when(projectBuildingResults.get(0).getProblems()).thenReturn(singletonList(legacyProblem)); + when(projectBuilder.build(anyList(), anyBoolean(), any(ProjectBuildingRequest.class))) + .thenReturn(projectBuildingResults); + configureFullReactorRequest(); + + Result result = graphBuilder.build(session); + + assertFalse(result.hasErrors(), "Expected result not to have errors"); + assertTrue(apiSession.hasModelProblems()); + assertTrue(session.hasModelProblems()); + assertEquals(1, modelProblems.totalProblemsReported()); + ModelProblem problem = modelProblems.problems().findFirst().orElseThrow(); + assertEquals("model warning", problem.getMessage()); + assertEquals(BuilderProblem.Severity.WARNING, problem.getSeverity()); + assertEquals(ModelProblem.Version.V40, problem.getVersion()); + assertEquals("module-parent/pom.xml", problem.getSource()); + assertEquals(12, problem.getLineNumber()); + assertEquals(4, problem.getColumnNumber()); + assertEquals("unittest:module-parent:1.0", problem.getModelId()); + assertSame(cause, problem.getException()); + } + + @Test + void discardedMultiModuleCollectionProblemsAreNotRetained() throws ProjectBuildingException { + MavenProject requestedProject = artifactIdProjectMap.get(MODULE_A); + MavenProject unrelatedProject = artifactIdProjectMap.get(PARENT_MODULE); + ProjectBuildingResult discardedResult = createProjectBuildingResultMocks(singletonList(unrelatedProject)) + .get(0); + when(discardedResult.getProblems()) + .thenReturn(singletonList(new org.apache.maven.model.building.DefaultModelProblem( + "discarded warning", + org.apache.maven.model.building.ModelProblem.Severity.WARNING, + org.apache.maven.model.building.ModelProblem.Version.V40, + "pom.xml", + 1, + 1, + "unittest:discarded:1.0", + null))); + List selectedResults = createProjectBuildingResultMocks(singletonList(requestedProject)); + when(projectBuilder.build(anyList(), anyBoolean(), any(ProjectBuildingRequest.class))) + .thenReturn(singletonList(discardedResult), selectedResults); + ModelProcessor testModelProcessor = mock(ModelProcessor.class); + when(testModelProcessor.locateExistingPom(Paths.get("reactor-root"))) + .thenReturn(Paths.get("reactor-root/pom.xml")); + MultiModuleCollectionStrategy testMultiModuleCollectionStrategy = + new MultiModuleCollectionStrategy(testModelProcessor, projectsSelector); + graphBuilder = new DefaultGraphBuilder( + mock(BuildResumptionDataRepository.class), + pomlessCollectionStrategy, + testMultiModuleCollectionStrategy, + requestPomCollectionStrategy); + when(mavenExecutionRequest.getPom()).thenReturn(requestedProject.getFile()); + when(mavenExecutionRequest.getRootDirectory()).thenReturn(Paths.get("reactor-root")); + when(mavenExecutionRequest.getMakeBehavior()).thenReturn(REACTOR_MAKE_UPSTREAM); + when(mavenExecutionRequest.getProjectActivation()).thenReturn(new ProjectActivation()); + when(mavenExecutionRequest.isRecursive()).thenReturn(true); + + Result result = graphBuilder.build(session); + + assertFalse(result.hasErrors(), "Expected result not to have errors"); + assertFalse(apiSession.hasModelProblems()); + assertFalse(session.hasModelProblems()); + assertEquals(0, modelProblems.totalProblemsReported()); + } + + @Test + void problemsFromProjectsRemovedAfterDiscoveryAreRetained() throws ProjectBuildingException { + List projectBuildingResults = + createProjectBuildingResultMocks(artifactIdProjectMap.values()); + ProjectBuildingResult removedProjectResult = projectBuildingResults.stream() + .filter(projectResult -> + MODULE_B.equals(projectResult.getProject().getArtifactId())) + .findFirst() + .orElseThrow(); + when(removedProjectResult.getProblems()) + .thenReturn(singletonList(new org.apache.maven.model.building.DefaultModelProblem( + "warning from project removed after discovery", + org.apache.maven.model.building.ModelProblem.Severity.WARNING, + org.apache.maven.model.building.ModelProblem.Version.V40, + "module-b/pom.xml", + 1, + 1, + "unittest:module-b:1.0", + null))); + when(projectBuilder.build(anyList(), anyBoolean(), any(ProjectBuildingRequest.class))) + .thenReturn(projectBuildingResults); + ProjectActivation projectActivation = new ProjectActivation(); + projectActivation.activateRequiredProject(":" + MODULE_A); + when(mavenExecutionRequest.getPom()).thenReturn(new File(PARENT_MODULE, "pom.xml")); + when(mavenExecutionRequest.getRootDirectory()).thenReturn(Paths.get(PARENT_MODULE)); + when(mavenExecutionRequest.getProjectActivation()).thenReturn(projectActivation); + when(mavenExecutionRequest.isRecursive()).thenReturn(true); + + Result result = graphBuilder.build(session); + + assertFalse(result.hasErrors(), "Expected result not to have errors"); + assertEquals( + singletonList(artifactIdProjectMap.get(MODULE_A)), result.get().getSortedProjects()); + assertTrue(apiSession.hasModelProblems()); + assertEquals(1, modelProblems.totalProblemsReported()); + } + @BeforeEach void before() throws Exception { graphBuilder = new DefaultGraphBuilder( @@ -413,6 +537,10 @@ void before() throws Exception { // Set up needed mocks when(session.getRequest()).thenReturn(mavenExecutionRequest); + when(session.getSession()).thenReturn(apiSession); + when(apiSession.getModelProblemCollector()).thenReturn(modelProblems); + when(apiSession.hasModelProblems()).thenAnswer(invocation -> modelProblems.hasWarningProblems()); + when(session.hasModelProblems()).thenAnswer(invocation -> apiSession.hasModelProblems()); when(session.getProjects()).thenReturn(null); // needed, otherwise it will be an empty list by default when(mavenExecutionRequest.getProjectBuildingRequest()).thenReturn(mock(ProjectBuildingRequest.class)); List projectBuildingResults = @@ -422,6 +550,12 @@ void before() throws Exception { when(mavenExecutionRequest.getRootDirectory()).thenReturn(null); } + private void configureFullReactorRequest() { + when(mavenExecutionRequest.getPom()).thenReturn(new File(PARENT_MODULE, "pom.xml")); + when(mavenExecutionRequest.getProjectActivation()).thenReturn(new ProjectActivation()); + when(mavenExecutionRequest.isRecursive()).thenReturn(true); + } + private MavenProject getMavenProject(String artifactId, MavenProject parentProject) { MavenProject project = getMavenProject(artifactId); Parent parent = new Parent(); diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java index a25276b22ec3..4e7b4228b76a 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultSessionTest.java @@ -21,8 +21,12 @@ import java.nio.file.Paths; import java.util.Collections; +import org.apache.maven.api.Session; +import org.apache.maven.api.services.BuilderProblem; +import org.apache.maven.api.services.ModelProblem; import org.apache.maven.execution.DefaultMavenExecutionRequest; import org.apache.maven.execution.MavenSession; +import org.apache.maven.impl.model.DefaultModelProblem; import org.apache.maven.model.root.RootLocator; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; @@ -30,7 +34,10 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; public class DefaultSessionTest { @@ -60,4 +67,56 @@ void testRootDirectory() { assertEquals(Paths.get("myRootDirectory"), session.getRootDirectory()); } + + @Test + void modelProblemsAreSharedWithDerivedAndLegacySessions() { + RepositorySystemSession rss = new DefaultRepositorySystemSession(h -> false); + MavenSession legacySession = new MavenSession(null, rss, new DefaultMavenExecutionRequest(), null); + DefaultSession session = new DefaultSession( + legacySession, mock(RepositorySystem.class), Collections.emptyList(), null, null, null); + legacySession.setSession(session); + + ModelProblem problem = new DefaultModelProblem( + "model warning", + BuilderProblem.Severity.WARNING, + ModelProblem.Version.BASE, + "pom.xml", + 12, + 4, + "org.example:project:1", + null); + session.getModelProblemCollector().reportProblem(problem); + + Session derivedSession = session.withRemoteRepositories(Collections.emptyList()); + + assertTrue(session.hasModelProblems()); + assertTrue(derivedSession.hasModelProblems()); + assertTrue(legacySession.hasModelProblems()); + assertSame(session.getModelProblemCollector(), derivedSession.getModelProblemCollector()); + assertSame( + problem, + session.getModelProblemCollector().problems().findFirst().orElseThrow()); + } + + @Test + void legacyModelProblemSetterIsVisibleThroughNativeSession() { + RepositorySystemSession rss = new DefaultRepositorySystemSession(h -> false); + MavenSession legacySession = new MavenSession(null, rss, new DefaultMavenExecutionRequest(), null); + DefaultSession session = new DefaultSession( + legacySession, mock(RepositorySystem.class), Collections.emptyList(), null, null, null); + legacySession.setSession(session); + + assertFalse(session.hasModelProblems()); + assertFalse(legacySession.hasModelProblems()); + + legacySession.setModelProblems(true); + + assertTrue(session.hasModelProblems()); + assertTrue(legacySession.hasModelProblems()); + + legacySession.setModelProblems(false); + + assertFalse(session.hasModelProblems()); + assertFalse(legacySession.hasModelProblems()); + } } 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 496eb81ad156..2fd3766c701c 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 @@ -347,6 +347,10 @@ ModelBuilderSessionState derive(ModelBuilderRequest request) { return derive(request, new DefaultModelBuilderResult(request, ProblemCollector.create(session))); } + ModelBuilderSessionState deriveWithProblemCollector(ModelBuilderRequest request) { + return derive(request, new DefaultModelBuilderResult(request, getProblemCollector())); + } + ModelBuilderSessionState derive(ModelBuilderRequest request, DefaultModelBuilderResult result) { if (session != request.getSession()) { throw new IllegalArgumentException("Session mismatch"); @@ -1195,7 +1199,7 @@ private Model readParentLocally( } try { - ModelBuilderSessionState derived = derive( + ModelBuilderSessionState derived = deriveWithProblemCollector( request.getRequestType() == ModelBuilderRequest.RequestType.BUILD_CONSUMER ? ModelBuilderRequest.builder(request) .requestType(ModelBuilderRequest.RequestType.CONSUMER_PARENT) @@ -1369,7 +1373,7 @@ Model resolveAndReadParentExternally( .source(modelSource) .build(); - ModelBuilderSessionState derived = derive(lenientRequest); + ModelBuilderSessionState derived = deriveWithProblemCollector(lenientRequest); Model parentModel = derived.readAsParentModel(profileActivationContext, parentChain); // Add profiles from parent, preserving model ID tracking for (Map.Entry> entry : @@ -2218,7 +2222,7 @@ private Model doLoadDependencyManagement( .source(importSource) .repositories(repositories) .build(); - ModelBuilderSessionState modelBuilderSession = derive(importRequest); + ModelBuilderSessionState modelBuilderSession = deriveWithProblemCollector(importRequest); // build the effective model modelBuilderSession.buildEffectiveModel(importIds); importResult = modelBuilderSession.result; 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 29689184c8d4..f93af8bc09e2 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 @@ -27,6 +27,8 @@ import org.apache.maven.api.RemoteRepository; import org.apache.maven.api.Session; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Provides; import org.apache.maven.api.model.Dependency; import org.apache.maven.api.model.DependencyManagement; import org.apache.maven.api.model.Model; @@ -35,8 +37,10 @@ import org.apache.maven.api.services.ModelBuilder; import org.apache.maven.api.services.ModelBuilderRequest; import org.apache.maven.api.services.ModelBuilderResult; +import org.apache.maven.api.services.ModelProblem; import org.apache.maven.api.services.Sources; import org.apache.maven.impl.standalone.ApiRunner; +import org.eclipse.aether.transport.file.FileTransporterFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,6 +77,48 @@ public void testPropertiesAndProfiles() { assertEquals("21", result.getEffectiveModel().getProperties().get("maven.compiler.release")); } + @Test + void defaultSessionModelProblemCollectorIsWritable() { + ModelProblem problem = new DefaultModelProblem( + "model warning", + org.apache.maven.api.services.BuilderProblem.Severity.WARNING, + ModelProblem.Version.BASE, + "pom.xml", + -1, + -1, + "org.apache.maven.tests:project:1.0", + null); + + session.getModelProblemCollector().reportProblem(problem); + + assertTrue(session.hasModelProblems()); + assertEquals(1, session.getModelProblemCollector().totalProblemsReported()); + } + + @Test + void externalParentProblemsAreRetainedInProjectResult() { + ModelBuilderResult result = buildWithTestRepository("model-problems-external-parent-child"); + + assertTrue( + result.getProblemCollector() + .problems() + .anyMatch( + problem -> problem.getMessage().contains("Duplicate activation for profile duplicate")), + "The project result should contain problems encountered while building its external parent"); + } + + @Test + void importedBomProblemsAreRetainedInProjectResult() { + ModelBuilderResult result = buildWithTestRepository("model-problems-imported-bom"); + + assertTrue( + result.getProblemCollector() + .problems() + .anyMatch( + problem -> problem.getMessage().contains("Duplicate activation for profile duplicate")), + "The project result should contain problems encountered while building an imported BOM"); + } + @Test public void testMergeRepositories() throws Exception { // this is here only to trigger mainSession creation; unrelated @@ -521,4 +567,28 @@ public void testBomDependencyManagementVersionInference() { private Path getPom(String name) { return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); } + + private ModelBuilderResult buildWithTestRepository(String pom) { + Path basedir = Paths.get(System.getProperty("basedir", "")).toAbsolutePath(); + Session repositorySession = ApiRunner.createSession( + injector -> injector.bindInstance(DefaultModelBuilderTest.class, this), + basedir.resolve("target/model-problems-test-repository")); + RemoteRepository testRepository = repositorySession.createRemoteRepository( + RemoteRepository.CENTRAL_ID, + basedir.resolve("src/test/remote-repo").toUri().toString()); + repositorySession = repositorySession.withRemoteRepositories(List.of(testRepository)); + ModelBuilder repositoryBuilder = repositorySession.getService(ModelBuilder.class); + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(repositorySession) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom(pom))) + .build(); + return repositoryBuilder.newSession().build(request); + } + + @Provides + @Named(FileTransporterFactory.NAME) + static FileTransporterFactory newFileTransporterFactory() { + return new FileTransporterFactory(); + } } diff --git a/impl/maven-impl/src/test/remote-repo/org/apache/maven/tests/model-problems-bom/1.0/model-problems-bom-1.0.pom b/impl/maven-impl/src/test/remote-repo/org/apache/maven/tests/model-problems-bom/1.0/model-problems-bom-1.0.pom new file mode 100644 index 000000000000..6db26fe67e1f --- /dev/null +++ b/impl/maven-impl/src/test/remote-repo/org/apache/maven/tests/model-problems-bom/1.0/model-problems-bom-1.0.pom @@ -0,0 +1,41 @@ + + + + 4.0.0 + org.apache.maven.tests + model-problems-bom + 1.0 + pom + + + duplicate + + + duplicate + + + + + + org.apache.maven.tests + managed-dependency + 1.0 + + + + diff --git a/impl/maven-impl/src/test/remote-repo/org/apache/maven/tests/model-problems-parent/1.0/model-problems-parent-1.0.pom b/impl/maven-impl/src/test/remote-repo/org/apache/maven/tests/model-problems-parent/1.0/model-problems-parent-1.0.pom new file mode 100644 index 000000000000..5e61f83361d1 --- /dev/null +++ b/impl/maven-impl/src/test/remote-repo/org/apache/maven/tests/model-problems-parent/1.0/model-problems-parent-1.0.pom @@ -0,0 +1,32 @@ + + + + 4.0.0 + org.apache.maven.tests + model-problems-parent + 1.0 + pom + + + duplicate + + + duplicate + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/model-problems-external-parent-child.xml b/impl/maven-impl/src/test/resources/poms/factory/model-problems-external-parent-child.xml new file mode 100644 index 000000000000..6bb8e8a58bea --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/model-problems-external-parent-child.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + org.apache.maven.tests + model-problems-parent + 1.0 + + + model-problems-external-parent-child + diff --git a/impl/maven-impl/src/test/resources/poms/factory/model-problems-imported-bom.xml b/impl/maven-impl/src/test/resources/poms/factory/model-problems-imported-bom.xml new file mode 100644 index 000000000000..89c81f2fc10d --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/model-problems-imported-bom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.maven.tests + model-problems-imported-bom + 1.0 + pom + + + + org.apache.maven.tests + model-problems-bom + 1.0 + pom + import + + + + diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITRememberModelProblemsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITRememberModelProblemsTest.java new file mode 100644 index 000000000000..8f2695f0a6ab --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITRememberModelProblemsTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.it; + +import java.nio.file.Path; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that model problems encountered during reactor discovery remain available from the build session. + */ +public class MavenITRememberModelProblemsTest extends AbstractMavenIntegrationTestCase { + + @Test + public void testModelProblemStateInNativeAndLegacySessions() throws Exception { + Path testDir = extractResources("remember-model-problems"); + + Properties warningProperties = execute(testDir.resolve("warning")); + + assertEquals("true", warningProperties.getProperty("session.hasModelProblems")); + assertEquals("true", warningProperties.getProperty("session.session.hasModelProblems")); + assertTrue( + Integer.parseInt(warningProperties.getProperty( + "session.session.modelProblemCollector.totalProblemsReported")) + > 0, + "Expected at least one retained model problem"); + + Properties cleanProperties = execute(testDir.resolve("clean")); + + assertEquals("false", cleanProperties.getProperty("session.hasModelProblems")); + assertEquals("false", cleanProperties.getProperty("session.session.hasModelProblems")); + assertEquals( + "0", + cleanProperties.getProperty("session.session.modelProblemCollector.totalProblemsReported")); + } + + private Properties execute(Path testDir) throws Exception { + Verifier verifier = newVerifier(testDir); + verifier.setAutoclean(false); + verifier.deleteDirectory("target"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + return verifier.loadProperties("target/session.properties"); + } +} diff --git a/its/core-it-suite/src/test/resources/remember-model-problems/clean/pom.xml b/its/core-it-suite/src/test/resources/remember-model-problems/clean/pom.xml new file mode 100644 index 000000000000..88a4afbbd029 --- /dev/null +++ b/its/core-it-suite/src/test/resources/remember-model-problems/clean/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + org.apache.maven.its + remember-model-problems-clean + 1.0 + pom + + + + + org.apache.maven.its.plugins + maven-it-plugin-expression + 2.1-SNAPSHOT + + target/session.properties + + session/hasModelProblems + session/session/hasModelProblems + session/session/modelProblemCollector/totalProblemsReported + + + + + inspect-session + validate + + eval + + + + + + + diff --git a/its/core-it-suite/src/test/resources/remember-model-problems/warning/pom.xml b/its/core-it-suite/src/test/resources/remember-model-problems/warning/pom.xml new file mode 100644 index 000000000000..39ec2fca5b16 --- /dev/null +++ b/its/core-it-suite/src/test/resources/remember-model-problems/warning/pom.xml @@ -0,0 +1,64 @@ + + + + 4.0.0 + + org.apache.maven.its + remember-model-problems-warning + 1.0 + pom + + + + org.apache.maven.its + system-dependency + 1.0 + system + ${project.basedir}/system-dependency.jar + + + + + + + org.apache.maven.its.plugins + maven-it-plugin-expression + 2.1-SNAPSHOT + + target/session.properties + + session/hasModelProblems + session/session/hasModelProblems + session/session/modelProblemCollector/totalProblemsReported + + + + + inspect-session + validate + + eval + + + + + + +