View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.graph;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  
35  import org.apache.maven.MavenExecutionException;
36  import org.apache.maven.ProjectCycleException;
37  import org.apache.maven.artifact.ArtifactUtils;
38  import org.apache.maven.execution.BuildResumptionDataRepository;
39  import org.apache.maven.execution.MavenExecutionRequest;
40  import org.apache.maven.execution.MavenSession;
41  import org.apache.maven.execution.ProjectActivation;
42  import org.apache.maven.execution.ProjectDependencyGraph;
43  import org.apache.maven.model.Plugin;
44  import org.apache.maven.model.building.DefaultModelProblem;
45  import org.apache.maven.model.building.Result;
46  import org.apache.maven.project.CycleDetectedException;
47  import org.apache.maven.project.DuplicateProjectException;
48  import org.apache.maven.project.MavenProject;
49  import org.apache.maven.project.ProjectBuildingException;
50  import org.apache.maven.project.collector.MultiModuleCollectionStrategy;
51  import org.apache.maven.project.collector.PomlessCollectionStrategy;
52  import org.apache.maven.project.collector.RequestPomCollectionStrategy;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import static java.util.Comparator.comparing;
57  
58  /**
59   * Builds the {@link ProjectDependencyGraph inter-dependencies graph} between projects in the reactor.
60   */
61  @Named(GraphBuilder.HINT)
62  @Singleton
63  public class DefaultGraphBuilder implements GraphBuilder {
64      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultGraphBuilder.class);
65  
66      private final BuildResumptionDataRepository buildResumptionDataRepository;
67      private final PomlessCollectionStrategy pomlessCollectionStrategy;
68      private final MultiModuleCollectionStrategy multiModuleCollectionStrategy;
69      private final RequestPomCollectionStrategy requestPomCollectionStrategy;
70      private final ProjectSelector projectSelector;
71  
72      /**
73       * @deprecated Use {@link #DefaultGraphBuilder(BuildResumptionDataRepository, PomlessCollectionStrategy,
74       * MultiModuleCollectionStrategy, RequestPomCollectionStrategy)} instead or rely on JSR 330
75       */
76      @Deprecated
77      public DefaultGraphBuilder() {
78          this(null, null, null, null);
79      }
80  
81      @Inject
82      public DefaultGraphBuilder(
83              BuildResumptionDataRepository buildResumptionDataRepository,
84              PomlessCollectionStrategy pomlessCollectionStrategy,
85              MultiModuleCollectionStrategy multiModuleCollectionStrategy,
86              RequestPomCollectionStrategy requestPomCollectionStrategy) {
87          this.buildResumptionDataRepository = buildResumptionDataRepository;
88          this.pomlessCollectionStrategy = pomlessCollectionStrategy;
89          this.multiModuleCollectionStrategy = multiModuleCollectionStrategy;
90          this.requestPomCollectionStrategy = requestPomCollectionStrategy;
91          this.projectSelector = new ProjectSelector(); // if necessary switch to DI
92      }
93  
94      @Override
95      public Result<ProjectDependencyGraph> build(MavenSession session) {
96          try {
97              Result<ProjectDependencyGraph> result = sessionDependencyGraph(session);
98  
99              if (result == null) {
100                 final List<MavenProject> projects = getProjectsForMavenReactor(session);
101                 validateProjects(projects, session.getRequest());
102                 processPackagingAttribute(projects, session.getRequest());
103                 enrichRequestFromResumptionData(projects, session.getRequest());
104                 result = reactorDependencyGraph(session, projects);
105             }
106 
107             return result;
108         } catch (final ProjectBuildingException | DuplicateProjectException | MavenExecutionException e) {
109             return Result.error(Collections.singletonList(new DefaultModelProblem(null, null, null, null, 0, 0, e)));
110         } catch (final CycleDetectedException e) {
111             String message = "The projects in the reactor contain a cyclic reference: " + e.getMessage();
112             ProjectCycleException error = new ProjectCycleException(message, e);
113             return Result.error(
114                     Collections.singletonList(new DefaultModelProblem(null, null, null, null, 0, 0, error)));
115         }
116     }
117 
118     private Result<ProjectDependencyGraph> sessionDependencyGraph(final MavenSession session)
119             throws CycleDetectedException, DuplicateProjectException {
120         Result<ProjectDependencyGraph> result = null;
121 
122         if (session.getProjectDependencyGraph() != null || session.getProjects() != null) {
123             ProjectDependencyGraph graph = new DefaultProjectDependencyGraph(session.getAllProjects());
124             if (session.getProjects() != null) {
125                 graph = new FilteredProjectDependencyGraph(graph, session.getProjects());
126             }
127 
128             result = Result.success(graph);
129         }
130 
131         return result;
132     }
133 
134     private Result<ProjectDependencyGraph> reactorDependencyGraph(MavenSession session, List<MavenProject> projects)
135             throws CycleDetectedException, DuplicateProjectException, MavenExecutionException {
136         ProjectDependencyGraph projectDependencyGraph = new DefaultProjectDependencyGraph(projects);
137         List<MavenProject> activeProjects = projectDependencyGraph.getSortedProjects();
138         List<MavenProject> allSortedProjects = projectDependencyGraph.getSortedProjects();
139         activeProjects = trimProjectsToRequest(activeProjects, projectDependencyGraph, session.getRequest());
140         activeProjects =
141                 trimSelectedProjects(activeProjects, allSortedProjects, projectDependencyGraph, session.getRequest());
142         activeProjects = trimResumedProjects(activeProjects, projectDependencyGraph, session.getRequest());
143         activeProjects = trimExcludedProjects(activeProjects, projectDependencyGraph, session.getRequest());
144 
145         if (activeProjects.size() != projectDependencyGraph.getSortedProjects().size()) {
146             projectDependencyGraph = new FilteredProjectDependencyGraph(projectDependencyGraph, activeProjects);
147         }
148 
149         return Result.success(projectDependencyGraph);
150     }
151 
152     private List<MavenProject> trimProjectsToRequest(
153             List<MavenProject> activeProjects, ProjectDependencyGraph graph, MavenExecutionRequest request)
154             throws MavenExecutionException {
155         List<MavenProject> result = activeProjects;
156 
157         if (request.getPom() != null) {
158             result = getProjectsInRequestScope(request, activeProjects);
159 
160             List<MavenProject> sortedProjects = graph.getSortedProjects();
161             result.sort(comparing(sortedProjects::indexOf));
162 
163             result = includeAlsoMakeTransitively(result, request, graph);
164         }
165 
166         return result;
167     }
168 
169     private List<MavenProject> trimSelectedProjects(
170             List<MavenProject> projects,
171             List<MavenProject> allSortedProjects,
172             ProjectDependencyGraph graph,
173             MavenExecutionRequest request)
174             throws MavenExecutionException {
175         List<MavenProject> result = projects;
176 
177         ProjectActivation projectActivation = request.getProjectActivation();
178         Set<String> requiredSelectors = projectActivation.getRequiredActiveProjectSelectors();
179         Set<String> optionalSelectors = projectActivation.getOptionalActiveProjectSelectors();
180         if (!requiredSelectors.isEmpty() || !optionalSelectors.isEmpty()) {
181             Set<MavenProject> selectedProjects = new HashSet<>(requiredSelectors.size() + optionalSelectors.size());
182             selectedProjects.addAll(
183                     projectSelector.getRequiredProjectsBySelectors(request, allSortedProjects, requiredSelectors));
184             selectedProjects.addAll(
185                     projectSelector.getOptionalProjectsBySelectors(request, allSortedProjects, optionalSelectors));
186 
187             // it can be empty when an optional project is missing from the reactor, fallback to returning all projects
188             if (!selectedProjects.isEmpty()) {
189                 result = new ArrayList<>(selectedProjects);
190 
191                 result = includeAlsoMakeTransitively(result, request, graph);
192 
193                 // Order the new list in the original order
194                 List<MavenProject> sortedProjects = graph.getSortedProjects();
195                 result.sort(comparing(sortedProjects::indexOf));
196             }
197         }
198 
199         return result;
200     }
201 
202     private List<MavenProject> trimResumedProjects(
203             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
204             throws MavenExecutionException {
205         List<MavenProject> result = projects;
206 
207         if (request.getResumeFrom() != null && !request.getResumeFrom().isEmpty()) {
208             File reactorDirectory = projectSelector.getBaseDirectoryFromRequest(request);
209 
210             String selector = request.getResumeFrom();
211 
212             MavenProject resumingFromProject = projects.stream()
213                     .filter(project -> projectSelector.isMatchingProject(project, selector, reactorDirectory))
214                     .findFirst()
215                     .orElseThrow(() -> new MavenExecutionException(
216                             "Could not find project to resume reactor build from: " + selector + " vs "
217                                     + formatProjects(projects),
218                             request.getPom()));
219             int resumeFromProjectIndex = projects.indexOf(resumingFromProject);
220             List<MavenProject> retainingProjects = result.subList(resumeFromProjectIndex, projects.size());
221 
222             result = includeAlsoMakeTransitively(retainingProjects, request, graph);
223         }
224 
225         return result;
226     }
227 
228     private List<MavenProject> trimExcludedProjects(
229             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
230             throws MavenExecutionException {
231         List<MavenProject> result = projects;
232 
233         ProjectActivation projectActivation = request.getProjectActivation();
234         Set<String> requiredSelectors = projectActivation.getRequiredInactiveProjectSelectors();
235         Set<String> optionalSelectors = projectActivation.getOptionalInactiveProjectSelectors();
236         if (!requiredSelectors.isEmpty() || !optionalSelectors.isEmpty()) {
237             Set<MavenProject> excludedProjects = new HashSet<>(requiredSelectors.size() + optionalSelectors.size());
238             List<MavenProject> allProjects = graph.getAllProjects();
239             excludedProjects.addAll(
240                     projectSelector.getRequiredProjectsBySelectors(request, allProjects, requiredSelectors));
241             excludedProjects.addAll(
242                     projectSelector.getOptionalProjectsBySelectors(request, allProjects, optionalSelectors));
243 
244             result = new ArrayList<>(projects);
245             result.removeAll(excludedProjects);
246 
247             if (result.isEmpty()) {
248                 boolean isPlural = excludedProjects.size() > 1;
249                 String message = String.format(
250                         "The project exclusion%s in --projects/-pl resulted in an "
251                                 + "empty reactor, please correct %s.",
252                         isPlural ? "s" : "", isPlural ? "them" : "it");
253                 throw new MavenExecutionException(message, request.getPom());
254             }
255         }
256 
257         return result;
258     }
259 
260     private List<MavenProject> includeAlsoMakeTransitively(
261             List<MavenProject> projects, MavenExecutionRequest request, ProjectDependencyGraph graph)
262             throws MavenExecutionException {
263         List<MavenProject> result = projects;
264 
265         String makeBehavior = request.getMakeBehavior();
266         boolean makeBoth = MavenExecutionRequest.REACTOR_MAKE_BOTH.equals(makeBehavior);
267 
268         boolean makeUpstream = makeBoth || MavenExecutionRequest.REACTOR_MAKE_UPSTREAM.equals(makeBehavior);
269         boolean makeDownstream = makeBoth || MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM.equals(makeBehavior);
270 
271         if ((makeBehavior != null && !makeBehavior.isEmpty()) && !makeUpstream && !makeDownstream) {
272             throw new MavenExecutionException("Invalid reactor make behavior: " + makeBehavior, request.getPom());
273         }
274 
275         if (makeUpstream || makeDownstream) {
276             Set<MavenProject> projectsSet = new HashSet<>(projects);
277 
278             for (MavenProject project : projects) {
279                 if (makeUpstream) {
280                     projectsSet.addAll(graph.getUpstreamProjects(project, true));
281                 }
282                 if (makeDownstream) {
283                     projectsSet.addAll(graph.getDownstreamProjects(project, true));
284                 }
285             }
286 
287             result = new ArrayList<>(projectsSet);
288 
289             // Order the new list in the original order
290             List<MavenProject> sortedProjects = graph.getSortedProjects();
291             result.sort(comparing(sortedProjects::indexOf));
292         }
293 
294         return result;
295     }
296 
297     private void enrichRequestFromResumptionData(List<MavenProject> projects, MavenExecutionRequest request) {
298         if (request.isResume()) {
299             projects.stream()
300                     .filter(MavenProject::isExecutionRoot)
301                     .findFirst()
302                     .ifPresent(rootProject -> buildResumptionDataRepository.applyResumptionData(request, rootProject));
303         }
304     }
305 
306     private List<MavenProject> getProjectsInRequestScope(MavenExecutionRequest request, List<MavenProject> projects)
307             throws MavenExecutionException {
308         if (request.getPom() == null) {
309             return projects;
310         }
311 
312         MavenProject requestPomProject = projects.stream()
313                 .filter(project -> request.getPom().equals(project.getFile()))
314                 .findFirst()
315                 .orElseThrow(() -> new MavenExecutionException(
316                         "Could not find a project in reactor matching the request POM", request.getPom()));
317 
318         List<MavenProject> modules = requestPomProject.getCollectedProjects() != null
319                 ? requestPomProject.getCollectedProjects()
320                 : Collections.emptyList();
321 
322         List<MavenProject> result = new ArrayList<>(modules);
323         result.add(requestPomProject);
324         return result;
325     }
326 
327     private String formatProjects(List<MavenProject> projects) {
328         StringBuilder projectNames = new StringBuilder();
329         Iterator<MavenProject> iterator = projects.iterator();
330         while (iterator.hasNext()) {
331             MavenProject project = iterator.next();
332             projectNames.append(project.getGroupId()).append(":").append(project.getArtifactId());
333             if (iterator.hasNext()) {
334                 projectNames.append(", ");
335             }
336         }
337         return projectNames.toString();
338     }
339 
340     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
341     //
342     // Project collection
343     //
344     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
345 
346     private List<MavenProject> getProjectsForMavenReactor(MavenSession session) throws ProjectBuildingException {
347         MavenExecutionRequest request = session.getRequest();
348         request.getProjectBuildingRequest().setRepositorySession(session.getRepositorySession());
349 
350         // 1. Collect project for invocation without a POM.
351         if (request.getPom() == null) {
352             return pomlessCollectionStrategy.collectProjects(request);
353         }
354 
355         // 2. Collect projects for all modules in the multi-module project.
356         if (request.getMakeBehavior() != null || !request.getProjectActivation().isEmpty()) {
357             List<MavenProject> projects = multiModuleCollectionStrategy.collectProjects(request);
358             if (!projects.isEmpty()) {
359                 return projects;
360             }
361         }
362 
363         // 3. Collect projects for explicitly requested POM.
364         return requestPomCollectionStrategy.collectProjects(request);
365     }
366 
367     private void validateProjects(List<MavenProject> projects, MavenExecutionRequest request)
368             throws MavenExecutionException {
369         Map<String, MavenProject> projectsMap = new HashMap<>();
370 
371         List<MavenProject> projectsInRequestScope = getProjectsInRequestScope(request, projects);
372         for (MavenProject p : projectsInRequestScope) {
373             String projectKey = ArtifactUtils.key(p.getGroupId(), p.getArtifactId(), p.getVersion());
374 
375             projectsMap.put(projectKey, p);
376         }
377 
378         for (MavenProject project : projects) {
379             // MNG-1911 / MNG-5572: Building plugins with extensions cannot be part of reactor
380             for (Plugin plugin : project.getBuildPlugins()) {
381                 if (plugin.isExtensions()) {
382                     String pluginKey =
383                             ArtifactUtils.key(plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
384 
385                     if (projectsMap.containsKey(pluginKey)) {
386                         LOGGER.warn(
387                                 "'{}' uses '{}' as extension which is not possible within the same reactor build. "
388                                         + "This plugin was pulled from the local repository!",
389                                 project.getName(),
390                                 plugin.getKey());
391                     }
392                 }
393             }
394         }
395     }
396 
397     private void processPackagingAttribute(List<MavenProject> projects, MavenExecutionRequest request)
398             throws MavenExecutionException {
399         List<MavenProject> projectsInRequestScope = getProjectsInRequestScope(request, projects);
400         for (MavenProject p : projectsInRequestScope) {
401             if ("bom".equals(p.getPackaging())) {
402                 LOGGER.info(
403                         "The packaging attribute of the '{}' project is configured as 'bom' and changed to 'pom'",
404                         p.getName());
405                 p.setPackaging("pom");
406             }
407         }
408     }
409 }