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