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                 enrichRequestFromResumptionData(projects, session.getRequest());
103                 result = reactorDependencyGraph(session, projects);
104             }
105 
106             return result;
107         } catch (final ProjectBuildingException | DuplicateProjectException | MavenExecutionException e) {
108             return Result.error(Collections.singletonList(new DefaultModelProblem(null, null, null, null, 0, 0, e)));
109         } catch (final CycleDetectedException e) {
110             String message = "The projects in the reactor contain a cyclic reference: " + e.getMessage();
111             ProjectCycleException error = new ProjectCycleException(message, e);
112             return Result.error(
113                     Collections.singletonList(new DefaultModelProblem(null, null, null, null, 0, 0, error)));
114         }
115     }
116 
117     private Result<ProjectDependencyGraph> sessionDependencyGraph(final MavenSession session)
118             throws CycleDetectedException, DuplicateProjectException {
119         Result<ProjectDependencyGraph> result = null;
120 
121         if (session.getProjectDependencyGraph() != null || session.getProjects() != null) {
122             final ProjectDependencyGraph graph =
123                     new DefaultProjectDependencyGraph(session.getAllProjects(), session.getProjects());
124 
125             result = Result.success(graph);
126         }
127 
128         return result;
129     }
130 
131     private Result<ProjectDependencyGraph> reactorDependencyGraph(MavenSession session, List<MavenProject> projects)
132             throws CycleDetectedException, DuplicateProjectException, MavenExecutionException {
133         ProjectDependencyGraph projectDependencyGraph = new DefaultProjectDependencyGraph(projects);
134         List<MavenProject> activeProjects = projectDependencyGraph.getSortedProjects();
135         List<MavenProject> allSortedProjects = projectDependencyGraph.getSortedProjects();
136         activeProjects = trimProjectsToRequest(activeProjects, projectDependencyGraph, session.getRequest());
137         activeProjects =
138                 trimSelectedProjects(activeProjects, allSortedProjects, projectDependencyGraph, session.getRequest());
139         activeProjects = trimResumedProjects(activeProjects, projectDependencyGraph, session.getRequest());
140         activeProjects = trimExcludedProjects(activeProjects, projectDependencyGraph, session.getRequest());
141 
142         if (activeProjects.size() != projectDependencyGraph.getSortedProjects().size()) {
143             projectDependencyGraph = new FilteredProjectDependencyGraph(projectDependencyGraph, activeProjects);
144         }
145 
146         return Result.success(projectDependencyGraph);
147     }
148 
149     private List<MavenProject> trimProjectsToRequest(
150             List<MavenProject> activeProjects, ProjectDependencyGraph graph, MavenExecutionRequest request)
151             throws MavenExecutionException {
152         List<MavenProject> result = activeProjects;
153 
154         if (request.getPom() != null) {
155             result = getProjectsInRequestScope(request, activeProjects);
156 
157             List<MavenProject> sortedProjects = graph.getSortedProjects();
158             result.sort(comparing(sortedProjects::indexOf));
159 
160             result = includeAlsoMakeTransitively(result, request, graph);
161         }
162 
163         return result;
164     }
165 
166     private List<MavenProject> trimSelectedProjects(
167             List<MavenProject> projects,
168             List<MavenProject> allSortedProjects,
169             ProjectDependencyGraph graph,
170             MavenExecutionRequest request)
171             throws MavenExecutionException {
172         List<MavenProject> result = projects;
173 
174         ProjectActivation projectActivation = request.getProjectActivation();
175         Set<String> requiredSelectors = projectActivation.getRequiredActiveProjectSelectors();
176         Set<String> optionalSelectors = projectActivation.getOptionalActiveProjectSelectors();
177         if (!requiredSelectors.isEmpty() || !optionalSelectors.isEmpty()) {
178             Set<MavenProject> selectedProjects = new HashSet<>(requiredSelectors.size() + optionalSelectors.size());
179             selectedProjects.addAll(
180                     projectSelector.getRequiredProjectsBySelectors(request, allSortedProjects, requiredSelectors));
181             selectedProjects.addAll(
182                     projectSelector.getOptionalProjectsBySelectors(request, allSortedProjects, optionalSelectors));
183 
184             // it can be empty when an optional project is missing from the reactor, fallback to returning all projects
185             if (!selectedProjects.isEmpty()) {
186                 result = new ArrayList<>(selectedProjects);
187 
188                 result = includeAlsoMakeTransitively(result, request, graph);
189 
190                 // Order the new list in the original order
191                 List<MavenProject> sortedProjects = graph.getSortedProjects();
192                 result.sort(comparing(sortedProjects::indexOf));
193             }
194         }
195 
196         return result;
197     }
198 
199     private List<MavenProject> trimResumedProjects(
200             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
201             throws MavenExecutionException {
202         List<MavenProject> result = projects;
203 
204         if (request.getResumeFrom() != null && !request.getResumeFrom().isEmpty()) {
205             File reactorDirectory = projectSelector.getBaseDirectoryFromRequest(request);
206 
207             String selector = request.getResumeFrom();
208 
209             MavenProject resumingFromProject = projects.stream()
210                     .filter(project -> projectSelector.isMatchingProject(project, selector, reactorDirectory))
211                     .findFirst()
212                     .orElseThrow(() -> new MavenExecutionException(
213                             "Could not find project to resume reactor build from: " + selector + " vs "
214                                     + formatProjects(projects),
215                             request.getPom()));
216             int resumeFromProjectIndex = projects.indexOf(resumingFromProject);
217             List<MavenProject> retainingProjects = result.subList(resumeFromProjectIndex, projects.size());
218 
219             result = includeAlsoMakeTransitively(retainingProjects, request, graph);
220         }
221 
222         return result;
223     }
224 
225     private List<MavenProject> trimExcludedProjects(
226             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
227             throws MavenExecutionException {
228         List<MavenProject> result = projects;
229 
230         ProjectActivation projectActivation = request.getProjectActivation();
231         Set<String> requiredSelectors = projectActivation.getRequiredInactiveProjectSelectors();
232         Set<String> optionalSelectors = projectActivation.getOptionalInactiveProjectSelectors();
233         if (!requiredSelectors.isEmpty() || !optionalSelectors.isEmpty()) {
234             Set<MavenProject> excludedProjects = new HashSet<>(requiredSelectors.size() + optionalSelectors.size());
235             List<MavenProject> allProjects = graph.getAllProjects();
236             excludedProjects.addAll(
237                     projectSelector.getRequiredProjectsBySelectors(request, allProjects, requiredSelectors));
238             excludedProjects.addAll(
239                     projectSelector.getOptionalProjectsBySelectors(request, allProjects, optionalSelectors));
240 
241             result = new ArrayList<>(projects);
242             result.removeAll(excludedProjects);
243 
244             if (result.isEmpty()) {
245                 boolean isPlural = excludedProjects.size() > 1;
246                 String message = String.format(
247                         "The project exclusion%s in --projects/-pl resulted in an "
248                                 + "empty reactor, please correct %s.",
249                         isPlural ? "s" : "", isPlural ? "them" : "it");
250                 throw new MavenExecutionException(message, request.getPom());
251             }
252         }
253 
254         return result;
255     }
256 
257     private List<MavenProject> includeAlsoMakeTransitively(
258             List<MavenProject> projects, MavenExecutionRequest request, ProjectDependencyGraph graph)
259             throws MavenExecutionException {
260         List<MavenProject> result = projects;
261 
262         String makeBehavior = request.getMakeBehavior();
263         boolean makeBoth = MavenExecutionRequest.REACTOR_MAKE_BOTH.equals(makeBehavior);
264 
265         boolean makeUpstream = makeBoth || MavenExecutionRequest.REACTOR_MAKE_UPSTREAM.equals(makeBehavior);
266         boolean makeDownstream = makeBoth || MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM.equals(makeBehavior);
267 
268         if ((makeBehavior != null && !makeBehavior.isEmpty()) && !makeUpstream && !makeDownstream) {
269             throw new MavenExecutionException("Invalid reactor make behavior: " + makeBehavior, request.getPom());
270         }
271 
272         if (makeUpstream || makeDownstream) {
273             Set<MavenProject> projectsSet = new HashSet<>(projects);
274 
275             for (MavenProject project : projects) {
276                 if (makeUpstream) {
277                     projectsSet.addAll(graph.getUpstreamProjects(project, true));
278                 }
279                 if (makeDownstream) {
280                     projectsSet.addAll(graph.getDownstreamProjects(project, true));
281                 }
282             }
283 
284             result = new ArrayList<>(projectsSet);
285 
286             // Order the new list in the original order
287             List<MavenProject> sortedProjects = graph.getSortedProjects();
288             result.sort(comparing(sortedProjects::indexOf));
289         }
290 
291         return result;
292     }
293 
294     private void enrichRequestFromResumptionData(List<MavenProject> projects, MavenExecutionRequest request) {
295         if (request.isResume()) {
296             projects.stream()
297                     .filter(MavenProject::isExecutionRoot)
298                     .findFirst()
299                     .ifPresent(rootProject -> buildResumptionDataRepository.applyResumptionData(request, rootProject));
300         }
301     }
302 
303     private List<MavenProject> getProjectsInRequestScope(MavenExecutionRequest request, List<MavenProject> projects)
304             throws MavenExecutionException {
305         if (request.getPom() == null) {
306             return projects;
307         }
308 
309         MavenProject requestPomProject = projects.stream()
310                 .filter(project -> request.getPom().equals(project.getFile()))
311                 .findFirst()
312                 .orElseThrow(() -> new MavenExecutionException(
313                         "Could not find a project in reactor matching the request POM", request.getPom()));
314 
315         List<MavenProject> modules = requestPomProject.getCollectedProjects() != null
316                 ? requestPomProject.getCollectedProjects()
317                 : Collections.emptyList();
318 
319         List<MavenProject> result = new ArrayList<>(modules);
320         result.add(requestPomProject);
321         return result;
322     }
323 
324     private String formatProjects(List<MavenProject> projects) {
325         StringBuilder projectNames = new StringBuilder();
326         Iterator<MavenProject> iterator = projects.iterator();
327         while (iterator.hasNext()) {
328             MavenProject project = iterator.next();
329             projectNames.append(project.getGroupId()).append(":").append(project.getArtifactId());
330             if (iterator.hasNext()) {
331                 projectNames.append(", ");
332             }
333         }
334         return projectNames.toString();
335     }
336 
337     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
338     //
339     // Project collection
340     //
341     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
342 
343     private List<MavenProject> getProjectsForMavenReactor(MavenSession session) throws ProjectBuildingException {
344         MavenExecutionRequest request = session.getRequest();
345         request.getProjectBuildingRequest().setRepositorySession(session.getRepositorySession());
346 
347         // 1. Collect project for invocation without a POM.
348         if (request.getPom() == null) {
349             return pomlessCollectionStrategy.collectProjects(request);
350         }
351 
352         // 2. Collect projects for all modules in the multi-module project.
353         if (request.getMakeBehavior() != null || !request.getProjectActivation().isEmpty()) {
354             List<MavenProject> projects = multiModuleCollectionStrategy.collectProjects(request);
355             if (!projects.isEmpty()) {
356                 return projects;
357             }
358         }
359 
360         // 3. Collect projects for explicitly requested POM.
361         return requestPomCollectionStrategy.collectProjects(request);
362     }
363 
364     private void validateProjects(List<MavenProject> projects, MavenExecutionRequest request)
365             throws MavenExecutionException {
366         Map<String, MavenProject> projectsMap = new HashMap<>();
367 
368         List<MavenProject> projectsInRequestScope = getProjectsInRequestScope(request, projects);
369         for (MavenProject p : projectsInRequestScope) {
370             String projectKey = ArtifactUtils.key(p.getGroupId(), p.getArtifactId(), p.getVersion());
371 
372             projectsMap.put(projectKey, p);
373         }
374 
375         for (MavenProject project : projects) {
376             // MNG-1911 / MNG-5572: Building plugins with extensions cannot be part of reactor
377             for (Plugin plugin : project.getBuildPlugins()) {
378                 if (plugin.isExtensions()) {
379                     String pluginKey =
380                             ArtifactUtils.key(plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
381 
382                     if (projectsMap.containsKey(pluginKey)) {
383                         LOGGER.warn(
384                                 "'{}' uses '{}' as extension which is not possible within the same reactor build. "
385                                         + "This plugin was pulled from the local repository!",
386                                 project.getName(),
387                                 plugin.getKey());
388                     }
389                 }
390             }
391         }
392     }
393 }