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.enforcer.rules;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.Objects;
30  
31  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
32  import org.apache.maven.execution.MavenSession;
33  import org.apache.maven.model.Dependency;
34  import org.apache.maven.project.MavenProject;
35  import org.codehaus.plexus.util.StringUtils;
36  
37  /**
38   * This rule will check if a multi module build will follow the best practices.
39   *
40   * @author Karl-Heinz Marbaise
41   * @since 1.4
42   */
43  @Named("reactorModuleConvergence")
44  public final class ReactorModuleConvergence extends AbstractStandardEnforcerRule {
45      private static final String MODULE_TEXT = " module: ";
46  
47      private boolean ignoreModuleDependencies = false;
48  
49      private final MavenSession session;
50  
51      @Inject
52      public ReactorModuleConvergence(MavenSession session) {
53          this.session = Objects.requireNonNull(session);
54      }
55  
56      @Override
57      public void execute() throws EnforcerRuleException {
58  
59          List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
60          if (sortedProjects != null && !sortedProjects.isEmpty()) {
61              checkReactor(sortedProjects);
62              checkParentsInReactor(sortedProjects);
63              checkMissingParentsInReactor(sortedProjects);
64              checkParentsPartOfTheReactor(sortedProjects);
65              if (!isIgnoreModuleDependencies()) {
66                  checkDependenciesWithinReactor(sortedProjects);
67              }
68          }
69      }
70  
71      private void checkParentsPartOfTheReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
72          List<MavenProject> parentsWhichAreNotPartOfTheReactor = existParentsWhichAreNotPartOfTheReactor(sortedProjects);
73          if (!parentsWhichAreNotPartOfTheReactor.isEmpty()) {
74              StringBuilder sb = new StringBuilder().append(System.lineSeparator());
75              addMessageIfExist(sb);
76              for (MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor) {
77                  sb.append(MODULE_TEXT);
78                  sb.append(mavenProject.getId());
79                  sb.append(System.lineSeparator());
80              }
81              throw new EnforcerRuleException(
82                      "Module parents have been found which could not be found in the reactor." + sb);
83          }
84      }
85  
86      /**
87       * Convenience method to create a user readable message.
88       *
89       * @param sortedProjects The list of reactor projects.
90       * @throws EnforcerRuleException In case of a violation.
91       */
92      private void checkMissingParentsInReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
93          List<MavenProject> modulesWithoutParentsInReactor = existModulesWithoutParentsInReactor(sortedProjects);
94          if (!modulesWithoutParentsInReactor.isEmpty()) {
95              StringBuilder sb = new StringBuilder().append(System.lineSeparator());
96              addMessageIfExist(sb);
97              for (MavenProject mavenProject : modulesWithoutParentsInReactor) {
98                  sb.append(MODULE_TEXT);
99                  sb.append(mavenProject.getId());
100                 sb.append(System.lineSeparator());
101             }
102             throw new EnforcerRuleException("Reactor contains modules without parents." + sb);
103         }
104     }
105 
106     private void checkDependenciesWithinReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
107         // After we are sure having consistent version we can simply use the first one?
108         String reactorVersion = sortedProjects.get(0).getVersion();
109 
110         Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor =
111                 areThereDependenciesWhichAreNotPartOfTheReactor(reactorVersion, sortedProjects);
112         if (!areThereDependenciesWhichAreNotPartOfTheReactor.isEmpty()) {
113             StringBuilder sb = new StringBuilder().append(System.lineSeparator());
114             addMessageIfExist(sb);
115             // CHECKSTYLE_OFF: LineLength
116             for (Entry<MavenProject, List<Dependency>> item :
117                     areThereDependenciesWhichAreNotPartOfTheReactor.entrySet()) {
118                 sb.append(MODULE_TEXT);
119                 sb.append(item.getKey().getId());
120                 sb.append(System.lineSeparator());
121                 for (Dependency dependency : item.getValue()) {
122                     String id =
123                             dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion();
124                     sb.append("    dependency: ");
125                     sb.append(id);
126                     sb.append(System.lineSeparator());
127                 }
128             }
129             throw new EnforcerRuleException(
130                     "Reactor modules contains dependencies which do not reference the reactor." + sb);
131             // CHECKSTYLE_ON: LineLength
132         }
133     }
134 
135     /**
136      * Convenience method to create a user readable message.
137      *
138      * @param sortedProjects The list of reactor projects.
139      * @throws EnforcerRuleException In case of a violation.
140      */
141     private void checkParentsInReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
142         // After we are sure having consistent version we can simply use the first one?
143         String reactorVersion = sortedProjects.get(0).getVersion();
144 
145         List<MavenProject> areParentsFromTheReactor = areParentsFromTheReactor(reactorVersion, sortedProjects);
146         if (!areParentsFromTheReactor.isEmpty()) {
147             StringBuilder sb = new StringBuilder().append(System.lineSeparator());
148             addMessageIfExist(sb);
149             for (MavenProject mavenProject : areParentsFromTheReactor) {
150                 sb.append(" --> ");
151                 sb.append(mavenProject.getId());
152                 sb.append(" parent:");
153                 sb.append(mavenProject.getParent().getId());
154                 sb.append(System.lineSeparator());
155             }
156             throw new EnforcerRuleException("Reactor modules have parents which contain a wrong version." + sb);
157         }
158     }
159 
160     /**
161      * Convenience method to create user readable message.
162      *
163      * @param sortedProjects The list of reactor projects.
164      * @throws EnforcerRuleException In case of a violation.
165      */
166     private void checkReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
167         List<MavenProject> consistenceCheckResult = isReactorVersionConsistent(sortedProjects);
168         if (!consistenceCheckResult.isEmpty()) {
169             StringBuilder sb = new StringBuilder().append(System.lineSeparator());
170             addMessageIfExist(sb);
171             for (MavenProject mavenProject : consistenceCheckResult) {
172                 sb.append(" --> ");
173                 sb.append(mavenProject.getId());
174                 sb.append(System.lineSeparator());
175             }
176             throw new EnforcerRuleException("The reactor contains different versions." + sb);
177         }
178     }
179 
180     private List<MavenProject> areParentsFromTheReactor(String reactorVersion, List<MavenProject> sortedProjects) {
181         List<MavenProject> result = new ArrayList<>();
182 
183         for (MavenProject mavenProject : sortedProjects) {
184             getLog().debug("Project: " + mavenProject.getId());
185             if (hasParent(mavenProject)) {
186                 if (!mavenProject.isExecutionRoot()) {
187                     MavenProject parent = mavenProject.getParent();
188                     if (!reactorVersion.equals(parent.getVersion())) {
189                         getLog().debug("The project: " + mavenProject.getId()
190                                 + " has a parent which version does not match the other elements in reactor");
191                         result.add(mavenProject);
192                     }
193                 }
194             } else {
195                 // This situation is currently ignored, cause it's handled by existModulesWithoutParentsInReactor()
196             }
197         }
198 
199         return result;
200     }
201 
202     private List<MavenProject> existParentsWhichAreNotPartOfTheReactor(List<MavenProject> sortedProjects) {
203         List<MavenProject> result = new ArrayList<>();
204 
205         for (MavenProject mavenProject : sortedProjects) {
206             getLog().debug("Project: " + mavenProject.getId());
207             if (hasParent(mavenProject)) {
208                 if (!mavenProject.isExecutionRoot()) {
209                     MavenProject parent = mavenProject.getParent();
210                     if (!isProjectPartOfTheReactor(parent, sortedProjects)) {
211                         result.add(mavenProject);
212                     }
213                 }
214             }
215         }
216 
217         return result;
218     }
219 
220     /**
221      * This will check of the groupId/artifactId can be found in any reactor project. The version will be ignored cause
222      * versions are checked before.
223      *
224      * @param project        The project which should be checked if it is contained in the sortedProjects.
225      * @param sortedProjects The list of existing projects.
226      * @return true if the project has been found within the list false otherwise.
227      */
228     private boolean isProjectPartOfTheReactor(MavenProject project, List<MavenProject> sortedProjects) {
229         return isGAPartOfTheReactor(project.getGroupId(), project.getArtifactId(), sortedProjects);
230     }
231 
232     private boolean isDependencyPartOfTheReactor(Dependency dependency, List<MavenProject> sortedProjects) {
233         return isGAPartOfTheReactor(dependency.getGroupId(), dependency.getArtifactId(), sortedProjects);
234     }
235 
236     /**
237      * This will check if the given <code>groupId/artifactId</code> is part of the current reactor.
238      *
239      * @param groupId        The groupId
240      * @param artifactId     The artifactId
241      * @param sortedProjects The list of projects within the reactor.
242      * @return true if the groupId/artifactId is part of the reactor false otherwise.
243      */
244     private boolean isGAPartOfTheReactor(String groupId, String artifactId, List<MavenProject> sortedProjects) {
245         boolean result = false;
246         for (MavenProject mavenProject : sortedProjects) {
247             String parentId = groupId + ":" + artifactId;
248             String projectId = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId();
249             if (parentId.equals(projectId)) {
250                 result = true;
251             }
252         }
253         return result;
254     }
255 
256     /**
257      * Assume we have a module which is a child of a multi module build but this child does not have a parent. This
258      * method will exactly search for such cases.
259      *
260      * @param sortedProjects The sorted list of the reactor modules.
261      * @return The resulting list will contain the modules in the reactor which do not have a parent. The list will
262      *         never null. If the list is empty no violation have happened.
263      */
264     private List<MavenProject> existModulesWithoutParentsInReactor(List<MavenProject> sortedProjects) {
265         List<MavenProject> result = new ArrayList<>();
266 
267         for (MavenProject mavenProject : sortedProjects) {
268             getLog().debug("Project: " + mavenProject.getId());
269             if (!hasParent(mavenProject)) {
270                 // TODO: Should add an option to force having a parent?
271                 if (mavenProject.isExecutionRoot()) {
272                     getLog().debug("The root does not need having a parent.");
273                 } else {
274                     getLog().debug("The module: " + mavenProject.getId() + " has no parent.");
275                     result.add(mavenProject);
276                 }
277             }
278         }
279 
280         return result;
281     }
282 
283     /**
284      * Convenience method to handle adding a dependency to the Map of List.
285      *
286      * @param result     The result List which should be handled.
287      * @param project    The MavenProject which will be added.
288      * @param dependency The dependency which will be added.
289      */
290     private void addDep(Map<MavenProject, List<Dependency>> result, MavenProject project, Dependency dependency) {
291         if (result.containsKey(project)) {
292             List<Dependency> list = result.get(project);
293             if (list == null) {
294                 list = new ArrayList<>();
295             }
296             list.add(dependency);
297             result.put(project, list);
298         } else {
299             List<Dependency> list = new ArrayList<>();
300             list.add(dependency);
301             result.put(project, list);
302         }
303     }
304 
305     /**
306      * Go through the list of modules in the builds and check if we have dependencies. If yes we will check every
307      * dependency based on groupId/artifactId if it belongs to the multi module build. In such a case it will be checked
308      * if the version does fit the version in the rest of build.
309      *
310      * @param reactorVersion The version of the reactor.
311      * @param sortedProjects The list of existing projects within this build.
312      * @return List of violations. Never null. If the list is empty than no violation has happened.
313      */
314     // CHECKSTYLE_OFF: LineLength
315     private Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor(
316             String reactorVersion, List<MavenProject> sortedProjects)
317                 // CHECKSTYLE_ON: LineLength
318             {
319         Map<MavenProject, List<Dependency>> result = new HashMap<>();
320         for (MavenProject mavenProject : sortedProjects) {
321             getLog().debug("Project: " + mavenProject.getId());
322 
323             List<Dependency> dependencies = mavenProject.getDependencies();
324             if (hasDependencies(dependencies)) {
325                 for (Dependency dependency : dependencies) {
326                     getLog().debug(" -> Dep:" + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
327                             + dependency.getVersion());
328                     if (isDependencyPartOfTheReactor(dependency, sortedProjects)) {
329                         if (!dependency.getVersion().equals(reactorVersion)) {
330                             addDep(result, mavenProject, dependency);
331                         }
332                     }
333                 }
334             }
335         }
336 
337         return result;
338     }
339 
340     /**
341      * This method will check the following situation within a multi-module build.
342      * <pre>
343      *  &lt;parent&gt;
344      *    &lt;groupId&gt;...&lt;/groupId&gt;
345      *    &lt;artifactId&gt;...&lt;/artifactId&gt;
346      *    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
347      *  &lt;/parent&gt;
348      *  &lt;version&gt;1.1-SNAPSHOT&lt;/version&gt;
349      * </pre>
350      *
351      * @param projectList The sorted list of the reactor modules.
352      * @return The resulting list will contain the modules in the reactor which do the thing in the example above. The
353      *         list will never null. If the list is empty no violation have happened.
354      */
355     private List<MavenProject> isReactorVersionConsistent(List<MavenProject> projectList) {
356         List<MavenProject> result = new ArrayList<>();
357 
358         if (projectList != null && !projectList.isEmpty()) {
359             String version = projectList.get(0).getVersion();
360             getLog().debug("First version:" + version);
361             for (MavenProject mavenProject : projectList) {
362                 getLog().debug(" -> checking " + mavenProject.getId());
363                 if (!version.equals(mavenProject.getVersion())) {
364                     result.add(mavenProject);
365                 }
366             }
367         }
368         return result;
369     }
370 
371     private boolean hasDependencies(List<Dependency> dependencies) {
372         return dependencies != null && !dependencies.isEmpty();
373     }
374 
375     private boolean hasParent(MavenProject mavenProject) {
376         return mavenProject.getParent() != null;
377     }
378 
379     public boolean isIgnoreModuleDependencies() {
380         return ignoreModuleDependencies;
381     }
382 
383     /**
384      * This will add the given user message to the output.
385      *
386      * @param sb The already initialized exception message part.
387      */
388     private void addMessageIfExist(StringBuilder sb) {
389         if (!StringUtils.isEmpty(getMessage())) {
390             sb.append(getMessage());
391             sb.append(System.lineSeparator());
392         }
393     }
394 
395     @Override
396     public String getCacheId() {
397         return String.valueOf(toString().hashCode());
398     }
399 
400     @Override
401     public String toString() {
402         return String.format(
403                 "ReactorModuleConvergence[message=%s, ignoreModuleDependencies=%b]",
404                 getMessage(), ignoreModuleDependencies);
405     }
406 }