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.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.LinkedHashSet;
33  import java.util.List;
34  import java.util.Map;
35  
36  import org.apache.maven.DefaultMaven;
37  import org.apache.maven.MavenExecutionException;
38  import org.apache.maven.ProjectCycleException;
39  import org.apache.maven.artifact.ArtifactUtils;
40  import org.apache.maven.execution.MavenExecutionRequest;
41  import org.apache.maven.execution.MavenSession;
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.ModelProblem;
46  import org.apache.maven.model.building.ModelProblemUtils;
47  import org.apache.maven.model.building.ModelSource;
48  import org.apache.maven.model.building.Result;
49  import org.apache.maven.model.building.UrlModelSource;
50  import org.apache.maven.project.DuplicateProjectException;
51  import org.apache.maven.project.MavenProject;
52  import org.apache.maven.project.ProjectBuilder;
53  import org.apache.maven.project.ProjectBuildingException;
54  import org.apache.maven.project.ProjectBuildingRequest;
55  import org.apache.maven.project.ProjectBuildingResult;
56  import org.codehaus.plexus.util.StringUtils;
57  import org.codehaus.plexus.util.dag.CycleDetectedException;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Builds the {@link ProjectDependencyGraph inter-dependencies graph} between projects in the reactor.
63   */
64  @Singleton
65  @Named(GraphBuilder.HINT)
66  public class DefaultGraphBuilder implements GraphBuilder {
67  
68      private final Logger logger = LoggerFactory.getLogger(getClass());
69  
70      @Inject
71      protected ProjectBuilder projectBuilder;
72  
73      @Override
74      public Result<ProjectDependencyGraph> build(MavenSession session) {
75          try {
76              Result<ProjectDependencyGraph> result = sessionDependencyGraph(session);
77  
78              if (result == null) {
79                  final List<MavenProject> projects = getProjectsForMavenReactor(session);
80                  validateProjects(projects);
81                  result = reactorDependencyGraph(session, projects);
82              }
83  
84              return result;
85          } catch (final ProjectBuildingException | DuplicateProjectException | MavenExecutionException e) {
86              return Result.error(Collections.singletonList(new DefaultModelProblem(null, null, null, null, 0, 0, e)));
87          } catch (final CycleDetectedException e) {
88              String message = "The projects in the reactor contain a cyclic reference: " + e.getMessage();
89              ProjectCycleException error = new ProjectCycleException(message, e);
90              return Result.error(
91                      Collections.singletonList(new DefaultModelProblem(null, null, null, null, 0, 0, error)));
92          }
93      }
94  
95      private Result<ProjectDependencyGraph> sessionDependencyGraph(final MavenSession session)
96              throws CycleDetectedException, DuplicateProjectException {
97          Result<ProjectDependencyGraph> result = null;
98  
99          if (session.getProjectDependencyGraph() != null || session.getProjects() != null) {
100             ProjectDependencyGraph graph = new DefaultProjectDependencyGraph(session.getAllProjects());
101             if (session.getProjects() != null) {
102                 graph = new FilteredProjectDependencyGraph(graph, session.getProjects());
103             }
104 
105             result = Result.success(graph);
106         }
107 
108         return result;
109     }
110 
111     private Result<ProjectDependencyGraph> reactorDependencyGraph(MavenSession session, List<MavenProject> projects)
112             throws CycleDetectedException, DuplicateProjectException, MavenExecutionException {
113         ProjectDependencyGraph projectDependencyGraph = new DefaultProjectDependencyGraph(projects);
114         List<MavenProject> activeProjects = projectDependencyGraph.getSortedProjects();
115         activeProjects = trimSelectedProjects(activeProjects, projectDependencyGraph, session.getRequest());
116         activeProjects = trimExcludedProjects(activeProjects, session.getRequest());
117         activeProjects = trimResumedProjects(activeProjects, session.getRequest());
118 
119         if (activeProjects.size() != projectDependencyGraph.getSortedProjects().size()) {
120             projectDependencyGraph = new FilteredProjectDependencyGraph(projectDependencyGraph, activeProjects);
121         }
122 
123         return Result.success(projectDependencyGraph);
124     }
125 
126     private List<MavenProject> trimSelectedProjects(
127             List<MavenProject> projects, ProjectDependencyGraph graph, MavenExecutionRequest request)
128             throws MavenExecutionException {
129         List<MavenProject> result = projects;
130 
131         if (!request.getSelectedProjects().isEmpty()) {
132             File reactorDirectory = null;
133             if (request.getBaseDirectory() != null) {
134                 reactorDirectory = new File(request.getBaseDirectory());
135             }
136 
137             Collection<MavenProject> selectedProjects = new LinkedHashSet<>(projects.size());
138 
139             for (String selector : request.getSelectedProjects()) {
140                 MavenProject selectedProject = null;
141 
142                 for (MavenProject project : projects) {
143                     if (isMatchingProject(project, selector, reactorDirectory)) {
144                         selectedProject = project;
145                         break;
146                     }
147                 }
148 
149                 if (selectedProject != null) {
150                     selectedProjects.add(selectedProject);
151                 } else {
152                     throw new MavenExecutionException(
153                             "Could not find the selected project in the reactor: " + selector, request.getPom());
154                 }
155             }
156 
157             boolean makeUpstream = false;
158             boolean makeDownstream = false;
159 
160             if (MavenExecutionRequest.REACTOR_MAKE_UPSTREAM.equals(request.getMakeBehavior())) {
161                 makeUpstream = true;
162             } else if (MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM.equals(request.getMakeBehavior())) {
163                 makeDownstream = true;
164             } else if (MavenExecutionRequest.REACTOR_MAKE_BOTH.equals(request.getMakeBehavior())) {
165                 makeUpstream = true;
166                 makeDownstream = true;
167             } else if (StringUtils.isNotEmpty(request.getMakeBehavior())) {
168                 throw new MavenExecutionException(
169                         "Invalid reactor make behavior: " + request.getMakeBehavior(), request.getPom());
170             }
171 
172             if (makeUpstream || makeDownstream) {
173                 for (MavenProject selectedProject : new ArrayList<>(selectedProjects)) {
174                     if (makeUpstream) {
175                         selectedProjects.addAll(graph.getUpstreamProjects(selectedProject, true));
176                     }
177                     if (makeDownstream) {
178                         selectedProjects.addAll(graph.getDownstreamProjects(selectedProject, true));
179                     }
180                 }
181             }
182 
183             result = new ArrayList<>(selectedProjects.size());
184 
185             for (MavenProject project : projects) {
186                 if (selectedProjects.contains(project)) {
187                     result.add(project);
188                 }
189             }
190         }
191 
192         return result;
193     }
194 
195     private List<MavenProject> trimExcludedProjects(List<MavenProject> projects, MavenExecutionRequest request)
196             throws MavenExecutionException {
197         List<MavenProject> result = projects;
198 
199         if (!request.getExcludedProjects().isEmpty()) {
200             File reactorDirectory = null;
201 
202             if (request.getBaseDirectory() != null) {
203                 reactorDirectory = new File(request.getBaseDirectory());
204             }
205 
206             Collection<MavenProject> excludedProjects = new LinkedHashSet<>(projects.size());
207 
208             for (String selector : request.getExcludedProjects()) {
209                 MavenProject excludedProject = null;
210 
211                 for (MavenProject project : projects) {
212                     if (isMatchingProject(project, selector, reactorDirectory)) {
213                         excludedProject = project;
214                         break;
215                     }
216                 }
217 
218                 if (excludedProject != null) {
219                     excludedProjects.add(excludedProject);
220                 } else {
221                     throw new MavenExecutionException(
222                             "Could not find the selected project in the reactor: " + selector, request.getPom());
223                 }
224             }
225 
226             result = new ArrayList<>(projects.size());
227             for (MavenProject project : projects) {
228                 if (!excludedProjects.contains(project)) {
229                     result.add(project);
230                 }
231             }
232         }
233 
234         return result;
235     }
236 
237     private List<MavenProject> trimResumedProjects(List<MavenProject> projects, MavenExecutionRequest request)
238             throws MavenExecutionException {
239         List<MavenProject> result = projects;
240 
241         if (StringUtils.isNotEmpty(request.getResumeFrom())) {
242             File reactorDirectory = null;
243             if (request.getBaseDirectory() != null) {
244                 reactorDirectory = new File(request.getBaseDirectory());
245             }
246 
247             String selector = request.getResumeFrom();
248 
249             result = new ArrayList<>(projects.size());
250 
251             boolean resumed = false;
252 
253             for (MavenProject project : projects) {
254                 if (!resumed && isMatchingProject(project, selector, reactorDirectory)) {
255                     resumed = true;
256                 }
257 
258                 if (resumed) {
259                     result.add(project);
260                 }
261             }
262 
263             if (!resumed) {
264                 throw new MavenExecutionException(
265                         "Could not find project to resume reactor build from: " + selector + " vs "
266                                 + formatProjects(projects),
267                         request.getPom());
268             }
269         }
270 
271         return result;
272     }
273 
274     private String formatProjects(List<MavenProject> projects) {
275         StringBuilder projectNames = new StringBuilder();
276         Iterator<MavenProject> iterator = projects.iterator();
277         while (iterator.hasNext()) {
278             MavenProject project = iterator.next();
279             projectNames.append(project.getGroupId()).append(":").append(project.getArtifactId());
280             if (iterator.hasNext()) {
281                 projectNames.append(", ");
282             }
283         }
284         return projectNames.toString();
285     }
286 
287     private boolean isMatchingProject(MavenProject project, String selector, File reactorDirectory) {
288         // [groupId]:artifactId
289         if (selector.indexOf(':') >= 0) {
290             String id = ':' + project.getArtifactId();
291 
292             if (id.equals(selector)) {
293                 return true;
294             }
295 
296             id = project.getGroupId() + id;
297 
298             if (id.equals(selector)) {
299                 return true;
300             }
301         }
302 
303         // relative path, e.g. "sub", "../sub" or "."
304         else if (reactorDirectory != null) {
305             File selectedProject =
306                     new File(new File(reactorDirectory, selector).toURI().normalize());
307 
308             if (selectedProject.isFile()) {
309                 return selectedProject.equals(project.getFile());
310             } else if (selectedProject.isDirectory()) {
311                 return selectedProject.equals(project.getBasedir());
312             }
313         }
314 
315         return false;
316     }
317 
318     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
319     //
320     // Project collection
321     //
322     // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
323 
324     private List<MavenProject> getProjectsForMavenReactor(MavenSession session) throws ProjectBuildingException {
325         MavenExecutionRequest request = session.getRequest();
326 
327         request.getProjectBuildingRequest().setRepositorySession(session.getRepositorySession());
328 
329         List<MavenProject> projects = new ArrayList<>();
330 
331         // We have no POM file.
332         //
333         if (request.getPom() == null) {
334             ModelSource modelSource = new UrlModelSource(DefaultMaven.class.getResource("project/standalone.xml"));
335             MavenProject project = projectBuilder
336                     .build(modelSource, request.getProjectBuildingRequest())
337                     .getProject();
338             project.setExecutionRoot(true);
339             projects.add(project);
340             request.setProjectPresent(false);
341             return projects;
342         }
343 
344         List<File> files = Arrays.asList(request.getPom().getAbsoluteFile());
345         collectProjects(projects, files, request);
346         return projects;
347     }
348 
349     private void collectProjects(List<MavenProject> projects, List<File> files, MavenExecutionRequest request)
350             throws ProjectBuildingException {
351         ProjectBuildingRequest projectBuildingRequest = request.getProjectBuildingRequest();
352 
353         List<ProjectBuildingResult> results =
354                 projectBuilder.build(files, request.isRecursive(), projectBuildingRequest);
355 
356         boolean problems = false;
357 
358         for (ProjectBuildingResult result : results) {
359             projects.add(result.getProject());
360 
361             if (!result.getProblems().isEmpty() && logger.isWarnEnabled()) {
362                 logger.warn("");
363                 logger.warn("Some problems were encountered while building the effective model for "
364                         + result.getProject().getId());
365 
366                 for (ModelProblem problem : result.getProblems()) {
367                     String loc = ModelProblemUtils.formatLocation(problem, result.getProjectId());
368                     logger.warn(problem.getMessage() + (StringUtils.isNotEmpty(loc) ? " @ " + loc : ""));
369                 }
370 
371                 problems = true;
372             }
373         }
374 
375         if (problems) {
376             logger.warn("");
377             logger.warn("It is highly recommended to fix these problems"
378                     + " because they threaten the stability of your build.");
379             logger.warn("");
380             logger.warn("For this reason, future Maven versions might no"
381                     + " longer support building such malformed projects.");
382             logger.warn("");
383         }
384     }
385 
386     private void validateProjects(List<MavenProject> projects) {
387         Map<String, MavenProject> projectsMap = new HashMap<>();
388 
389         for (MavenProject p : projects) {
390             String projectKey = ArtifactUtils.key(p.getGroupId(), p.getArtifactId(), p.getVersion());
391 
392             projectsMap.put(projectKey, p);
393         }
394 
395         for (MavenProject project : projects) {
396             // MNG-1911 / MNG-5572: Building plugins with extensions cannot be part of reactor
397             for (Plugin plugin : project.getBuildPlugins()) {
398                 if (plugin.isExtensions()) {
399                     String pluginKey =
400                             ArtifactUtils.key(plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
401 
402                     if (projectsMap.containsKey(pluginKey)) {
403                         logger.warn(project.getName() + " uses " + plugin.getKey()
404                                 + " as extensions, which is not possible within the same reactor build. "
405                                 + "This plugin was pulled from the local repository!");
406                     }
407                 }
408             }
409         }
410     }
411 }