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 
179         Set<String> requiredSelectors = projectActivation.getRequiredActiveProjectSelectors();
180         Set<String> optionalSelectors = projectActivation.getOptionalActiveProjectSelectors();
181         if (!requiredSelectors.isEmpty() || !optionalSelectors.isEmpty()) {
182             Set<MavenProject> selectedProjects =
183                     projectSelector.getActiveProjects(request, allSortedProjects, projectActivation.getActivations());
184 
185             // it can be empty when an optional project is missing from the reactor, fallback to returning all projects
186             if (!selectedProjects.isEmpty()) {
187                 result = new ArrayList<>(selectedProjects);
188 
189                 result = includeAlsoMakeTransitively(result, request, graph);
190 
191                 // Order the new list in the original order
192                 List<MavenProject> sortedProjects = graph.getSortedProjects();
193                 result.sort(comparing(sortedProjects::indexOf));
194             }
195         }
196 
197         return result;
198     }
199 
200     private List<MavenProject> trimResumedProjects(
201             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
202             throws MavenExecutionException {
203         List<MavenProject> result = projects;
204 
205         if (request.getResumeFrom() != null && !request.getResumeFrom().isEmpty()) {
206             File reactorDirectory = projectSelector.getBaseDirectoryFromRequest(request);
207 
208             String selector = request.getResumeFrom();
209 
210             MavenProject resumingFromProject = projects.stream()
211                     .filter(project -> projectSelector.isMatchingProject(project, selector, reactorDirectory))
212                     .findFirst()
213                     .orElseThrow(() -> new MavenExecutionException(
214                             "Could not find project to resume reactor build from: " + selector + " vs "
215                                     + formatProjects(projects),
216                             request.getPom()));
217             int resumeFromProjectIndex = projects.indexOf(resumingFromProject);
218             List<MavenProject> retainingProjects = result.subList(resumeFromProjectIndex, projects.size());
219 
220             result = includeAlsoMakeTransitively(retainingProjects, request, graph);
221         }
222 
223         return result;
224     }
225 
226     private List<MavenProject> trimExcludedProjects(
227             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
228             throws MavenExecutionException {
229         List<MavenProject> result = projects;
230 
231         ProjectActivation projectActivation = request.getProjectActivation();
232 
233         projectActivation.getActivations().stream()
234                 .filter(pa -> pa.activationSettings().active())
235                 .forEach(pas -> {});
236 
237         Set<String> requiredSelectors = projectActivation.getRequiredInactiveProjectSelectors();
238         Set<String> optionalSelectors = projectActivation.getOptionalInactiveProjectSelectors();
239         if (!requiredSelectors.isEmpty() || !optionalSelectors.isEmpty()) {
240             Set<MavenProject> excludedProjects = new HashSet<>(requiredSelectors.size() + optionalSelectors.size());
241             List<MavenProject> allProjects = graph.getAllProjects();
242             excludedProjects.addAll(
243                     projectSelector.getRequiredProjectsBySelectors(request, allProjects, requiredSelectors));
244             excludedProjects.addAll(
245                     projectSelector.getOptionalProjectsBySelectors(request, allProjects, optionalSelectors));
246 
247             result = new ArrayList<>(projects);
248             result.removeAll(excludedProjects);
249 
250             if (result.isEmpty()) {
251                 boolean isPlural = excludedProjects.size() > 1;
252                 String message = String.format(
253                         "The project exclusion%s in --projects/-pl resulted in an "
254                                 + "empty reactor, please correct %s.",
255                         isPlural ? "s" : "", isPlural ? "them" : "it");
256                 throw new MavenExecutionException(message, request.getPom());
257             }
258         }
259 
260         return result;
261     }
262 
263     private List<MavenProject> includeAlsoMakeTransitively(
264             List<MavenProject> projects, MavenExecutionRequest request, ProjectDependencyGraph graph)
265             throws MavenExecutionException {
266         List<MavenProject> result = projects;
267 
268         String makeBehavior = request.getMakeBehavior();
269         boolean makeBoth = MavenExecutionRequest.REACTOR_MAKE_BOTH.equals(makeBehavior);
270 
271         boolean makeUpstream = makeBoth || MavenExecutionRequest.REACTOR_MAKE_UPSTREAM.equals(makeBehavior);
272         boolean makeDownstream = makeBoth || MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM.equals(makeBehavior);
273 
274         if ((makeBehavior != null && !makeBehavior.isEmpty()) && !makeUpstream && !makeDownstream) {
275             throw new MavenExecutionException("Invalid reactor make behavior: " + makeBehavior, request.getPom());
276         }
277 
278         if (makeUpstream || makeDownstream) {
279             Set<MavenProject> projectsSet = new HashSet<>(projects);
280 
281             for (MavenProject project : projects) {
282                 if (makeUpstream) {
283                     projectsSet.addAll(graph.getUpstreamProjects(project, true));
284                 }
285                 if (makeDownstream) {
286                     projectsSet.addAll(graph.getDownstreamProjects(project, true));
287                 }
288             }
289 
290             result = new ArrayList<>(projectsSet);
291 
292             // Order the new list in the original order
293             List<MavenProject> sortedProjects = graph.getSortedProjects();
294             result.sort(comparing(sortedProjects::indexOf));
295         }
296 
297         return result;
298     }
299 
300     private void enrichRequestFromResumptionData(List<MavenProject> projects, MavenExecutionRequest request) {
301         if (request.isResume()) {
302             projects.stream()
303                     .filter(MavenProject::isExecutionRoot)
304                     .findFirst()
305                     .ifPresent(rootProject -> buildResumptionDataRepository.applyResumptionData(request, rootProject));
306         }
307     }
308 
309     private List<MavenProject> getProjectsInRequestScope(MavenExecutionRequest request, List<MavenProject> projects)
310             throws MavenExecutionException {
311         if (request.getPom() == null) {
312             return projects;
313         }
314 
315         MavenProject requestPomProject = projects.stream()
316                 .filter(project -> request.getPom().equals(project.getFile()))
317                 .findFirst()
318                 .orElseThrow(() -> new MavenExecutionException(
319                         "Could not find a project in reactor matching the request POM", request.getPom()));
320 
321         List<MavenProject> modules = requestPomProject.getCollectedProjects() != null
322                 ? requestPomProject.getCollectedProjects()
323                 : Collections.emptyList();
324 
325         List<MavenProject> result = new ArrayList<>(modules);
326         result.add(requestPomProject);
327         return result;
328     }
329 
330     private String formatProjects(List<MavenProject> projects) {
331         StringBuilder projectNames = new StringBuilder();
332         Iterator<MavenProject> iterator = projects.iterator();
333         while (iterator.hasNext()) {
334             MavenProject project = iterator.next();
335             projectNames.append(project.getGroupId()).append(":").append(project.getArtifactId());
336             if (iterator.hasNext()) {
337                 projectNames.append(", ");
338             }
339         }
340         return projectNames.toString();
341     }
342 
343     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
344     //
345     // Project collection
346     //
347     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
348 
349     private List<MavenProject> getProjectsForMavenReactor(MavenSession session) throws ProjectBuildingException {
350         MavenExecutionRequest request = session.getRequest();
351         request.getProjectBuildingRequest().setRepositorySession(session.getRepositorySession());
352 
353         // 1. Collect project for invocation without a POM.
354         if (request.getPom() == null) {
355             return pomlessCollectionStrategy.collectProjects(request);
356         }
357 
358         // 2. Collect projects for all modules in the multi-module project.
359         if (request.getMakeBehavior() != null || !request.getProjectActivation().isEmpty()) {
360             List<MavenProject> projects = multiModuleCollectionStrategy.collectProjects(request);
361             if (!projects.isEmpty()) {
362                 return projects;
363             }
364         }
365 
366         // 3. Collect projects for explicitly requested POM.
367         return requestPomCollectionStrategy.collectProjects(request);
368     }
369 
370     private void validateProjects(List<MavenProject> projects, MavenExecutionRequest request)
371             throws MavenExecutionException {
372         Map<String, MavenProject> projectsMap = new HashMap<>();
373 
374         List<MavenProject> projectsInRequestScope = getProjectsInRequestScope(request, projects);
375         for (MavenProject p : projectsInRequestScope) {
376             String projectKey = ArtifactUtils.key(p.getGroupId(), p.getArtifactId(), p.getVersion());
377 
378             projectsMap.put(projectKey, p);
379         }
380 
381         for (MavenProject project : projects) {
382             // MNG-1911 / MNG-5572: Building plugins with extensions cannot be part of reactor
383             for (Plugin plugin : project.getBuildPlugins()) {
384                 if (plugin.isExtensions()) {
385                     String pluginKey =
386                             ArtifactUtils.key(plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
387 
388                     if (projectsMap.containsKey(pluginKey)) {
389                         LOGGER.warn(
390                                 "'{}' uses '{}' as extension which is not possible within the same reactor build. "
391                                         + "This plugin was pulled from the local repository!",
392                                 project.getName(),
393                                 plugin.getKey());
394                     }
395                 }
396             }
397         }
398     }
399 
400     private void processPackagingAttribute(List<MavenProject> projects, MavenExecutionRequest request)
401             throws MavenExecutionException {
402         List<MavenProject> projectsInRequestScope = getProjectsInRequestScope(request, projects);
403         for (MavenProject p : projectsInRequestScope) {
404             if ("bom".equals(p.getPackaging())) {
405                 LOGGER.info(
406                         "The packaging attribute of the '{}' project is configured as 'bom' and changed to 'pom'",
407                         p.getName());
408                 p.setPackaging("pom");
409             }
410         }
411     }
412 }