View Javadoc
1   package org.apache.maven.plugins.enforcer;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Map.Entry;
27  
28  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
29  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
30  import org.apache.maven.execution.MavenSession;
31  import org.apache.maven.model.Dependency;
32  import org.apache.maven.plugin.logging.Log;
33  import org.apache.maven.project.MavenProject;
34  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
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  public class ReactorModuleConvergence
44      extends AbstractNonCacheableEnforcerRule
45  {
46      private static final String MODULE_TEXT = " module: ";
47  
48      private boolean ignoreModuleDependencies = false;
49  
50      private Log logger;
51  
52      @Override
53      public void execute( EnforcerRuleHelper helper )
54          throws EnforcerRuleException
55      {
56          logger = helper.getLog();
57  
58          MavenSession session;
59          try
60          {
61              session = (MavenSession) helper.evaluate( "${session}" );
62          }
63          catch ( ExpressionEvaluationException eee )
64          {
65              throw new EnforcerRuleException( "Unable to retrieve the MavenSession: ", eee );
66          }
67  
68          List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
69          if ( sortedProjects != null && !sortedProjects.isEmpty() )
70          {
71              checkReactor( sortedProjects );
72              checkParentsInReactor( sortedProjects );
73              checkMissingParentsInReactor( sortedProjects );
74              checkParentsPartOfTheReactor( sortedProjects );
75              if ( !isIgnoreModuleDependencies() )
76              {
77                  checkDependenciesWithinReactor( sortedProjects );
78              }
79          }
80  
81      }
82  
83      private void checkParentsPartOfTheReactor( List<MavenProject> sortedProjects )
84          throws EnforcerRuleException
85      {
86          List<MavenProject> parentsWhichAreNotPartOfTheReactor =
87              existParentsWhichAreNotPartOfTheReactor( sortedProjects );
88          if ( !parentsWhichAreNotPartOfTheReactor.isEmpty() )
89          {
90              StringBuilder sb = new StringBuilder().append( System.lineSeparator() );
91              addMessageIfExist( sb );
92              for ( MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor )
93              {
94                  sb.append( MODULE_TEXT );
95                  sb.append( mavenProject.getId() );
96                  sb.append( System.lineSeparator() );
97              }
98              throw new EnforcerRuleException( "Module parents have been found which could not be found in the reactor."
99                  + sb.toString() );
100         }
101     }
102 
103     /**
104      * Convenience method to create a user readable message.
105      * 
106      * @param sortedProjects The list of reactor projects.
107      * @throws EnforcerRuleException In case of a violation.
108      */
109     private void checkMissingParentsInReactor( List<MavenProject> sortedProjects )
110         throws EnforcerRuleException
111     {
112         List<MavenProject> modulesWithoutParentsInReactor = existModulesWithoutParentsInReactor( sortedProjects );
113         if ( !modulesWithoutParentsInReactor.isEmpty() )
114         {
115             StringBuilder sb = new StringBuilder().append( System.lineSeparator() );
116             addMessageIfExist( sb );
117             for ( MavenProject mavenProject : modulesWithoutParentsInReactor )
118             {
119                 sb.append( MODULE_TEXT );
120                 sb.append( mavenProject.getId() );
121                 sb.append( System.lineSeparator() );
122             }
123             throw new EnforcerRuleException( "Reactor contains modules without parents." + sb.toString() );
124         }
125     }
126 
127     private void checkDependenciesWithinReactor( List<MavenProject> sortedProjects )
128         throws EnforcerRuleException
129     {
130         // After we are sure having consistent version we can simply use the first one?
131         String reactorVersion = sortedProjects.get( 0 ).getVersion();
132 
133         Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor =
134             areThereDependenciesWhichAreNotPartOfTheReactor( reactorVersion, sortedProjects );
135         if ( !areThereDependenciesWhichAreNotPartOfTheReactor.isEmpty() )
136         {
137             StringBuilder sb = new StringBuilder().append( System.lineSeparator() );
138             addMessageIfExist( sb );
139             // CHECKSTYLE_OFF: LineLength
140             for ( Entry<MavenProject, List<Dependency>> item : areThereDependenciesWhichAreNotPartOfTheReactor.entrySet() )
141             {
142                 sb.append( MODULE_TEXT );
143                 sb.append( item.getKey().getId() );
144                 sb.append( System.lineSeparator() );
145                 for ( Dependency dependency : item.getValue() )
146                 {
147                     String id =
148                         dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion();
149                     sb.append( "    dependency: " );
150                     sb.append( id );
151                     sb.append( System.lineSeparator() );
152                 }
153             }
154             throw new EnforcerRuleException(
155                                              "Reactor modules contains dependencies which do not reference the reactor."
156                                                  + sb.toString() );
157             // CHECKSTYLE_ON: LineLength
158         }
159     }
160 
161     /**
162      * Convenience method to create a user readable message.
163      * 
164      * @param sortedProjects The list of reactor projects.
165      * @throws EnforcerRuleException In case of a violation.
166      */
167     private void checkParentsInReactor( List<MavenProject> sortedProjects )
168         throws EnforcerRuleException
169     {
170         // After we are sure having consistent version we can simply use the first one?
171         String reactorVersion = sortedProjects.get( 0 ).getVersion();
172 
173         List<MavenProject> areParentsFromTheReactor = areParentsFromTheReactor( reactorVersion, sortedProjects );
174         if ( !areParentsFromTheReactor.isEmpty() )
175         {
176             StringBuilder sb = new StringBuilder().append( System.lineSeparator() );
177             addMessageIfExist( sb );
178             for ( MavenProject mavenProject : areParentsFromTheReactor )
179             {
180                 sb.append( " --> " );
181                 sb.append( mavenProject.getId() );
182                 sb.append( " parent:" );
183                 sb.append( mavenProject.getParent().getId() );
184                 sb.append( System.lineSeparator() );
185             }
186             throw new EnforcerRuleException( "Reactor modules have parents which contain a wrong version."
187                 + sb.toString() );
188         }
189     }
190 
191     /**
192      * Convenience method to create user readable message.
193      * 
194      * @param sortedProjects The list of reactor projects.
195      * @throws EnforcerRuleException In case of a violation.
196      */
197     private void checkReactor( List<MavenProject> sortedProjects )
198         throws EnforcerRuleException
199     {
200         List<MavenProject> consistenceCheckResult = isReactorVersionConsistent( sortedProjects );
201         if ( !consistenceCheckResult.isEmpty() )
202         {
203             StringBuilder sb = new StringBuilder().append( System.lineSeparator() );
204             addMessageIfExist( sb );
205             for ( MavenProject mavenProject : consistenceCheckResult )
206             {
207                 sb.append( " --> " );
208                 sb.append( mavenProject.getId() );
209                 sb.append( System.lineSeparator() );
210             }
211             throw new EnforcerRuleException( "The reactor contains different versions." + sb.toString() );
212         }
213     }
214 
215     private List<MavenProject> areParentsFromTheReactor( String reactorVersion, List<MavenProject> sortedProjects )
216     {
217         List<MavenProject> result = new ArrayList<>();
218 
219         for ( MavenProject mavenProject : sortedProjects )
220         {
221             logger.debug( "Project: " + mavenProject.getId() );
222             if ( hasParent( mavenProject ) )
223             {
224                 if ( !mavenProject.isExecutionRoot() )
225                 {
226                     MavenProject parent = mavenProject.getParent();
227                     if ( !reactorVersion.equals( parent.getVersion() ) )
228                     {
229                         logger.debug( "The project: " + mavenProject.getId()
230                             + " has a parent which version does not match the other elements in reactor" );
231                         result.add( mavenProject );
232                     }
233                 }
234             }
235             else
236             {
237                 // This situation is currently ignored, cause it's handled by existModulesWithoutParentsInReactor()
238             }
239         }
240 
241         return result;
242     }
243 
244     private List<MavenProject> existParentsWhichAreNotPartOfTheReactor( List<MavenProject> sortedProjects )
245     {
246         List<MavenProject> result = new ArrayList<>();
247 
248         for ( MavenProject mavenProject : sortedProjects )
249         {
250             logger.debug( "Project: " + mavenProject.getId() );
251             if ( hasParent( mavenProject ) )
252             {
253                 if ( !mavenProject.isExecutionRoot() )
254                 {
255                     MavenProject parent = mavenProject.getParent();
256                     if ( !isProjectPartOfTheReactor( parent, sortedProjects ) )
257                     {
258                         result.add( mavenProject );
259                     }
260                 }
261             }
262         }
263 
264         return result;
265     }
266 
267     /**
268      * This will check of the groupId/artifactId can be found in any reactor project. The version will be ignored cause
269      * versions are checked before.
270      * 
271      * @param project The project which should be checked if it is contained in the sortedProjects.
272      * @param sortedProjects The list of existing projects.
273      * @return true if the project has been found within the list false otherwise.
274      */
275     private boolean isProjectPartOfTheReactor( MavenProject project, List<MavenProject> sortedProjects )
276     {
277         return isGAPartOfTheReactor( project.getGroupId(), project.getArtifactId(), sortedProjects );
278     }
279 
280     private boolean isDependencyPartOfTheReactor( Dependency dependency, List<MavenProject> sortedProjects )
281     {
282         return isGAPartOfTheReactor( dependency.getGroupId(), dependency.getArtifactId(), sortedProjects );
283     }
284 
285     /**
286      * This will check if the given <code>groupId/artifactId</code> is part of the current reactor.
287      * 
288      * @param groupId The groupId
289      * @param artifactId The artifactId
290      * @param sortedProjects The list of projects within the reactor.
291      * @return true if the groupId/artifactId is part of the reactor false otherwise.
292      */
293     private boolean isGAPartOfTheReactor( String groupId, String artifactId, List<MavenProject> sortedProjects )
294     {
295         boolean result = false;
296         for ( MavenProject mavenProject : sortedProjects )
297         {
298             String parentId = groupId + ":" + artifactId;
299             String projectId = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId();
300             if ( parentId.equals( projectId ) )
301             {
302                 result = true;
303             }
304         }
305         return result;
306     }
307 
308     /**
309      * Assume we have a module which is a child of a multi module build but this child does not have a parent. This
310      * method will exactly search for such cases.
311      * 
312      * @param sortedProjects The sorted list of the reactor modules.
313      * @return The resulting list will contain the modules in the reactor which do not have a parent. The list will
314      *         never null. If the list is empty no violation have happened.
315      */
316     private List<MavenProject> existModulesWithoutParentsInReactor( List<MavenProject> sortedProjects )
317     {
318         List<MavenProject> result = new ArrayList<>();
319 
320         for ( MavenProject mavenProject : sortedProjects )
321         {
322             logger.debug( "Project: " + mavenProject.getId() );
323             if ( !hasParent( mavenProject ) )
324             {
325                 // TODO: Should add an option to force having a parent?
326                 if ( mavenProject.isExecutionRoot() )
327                 {
328                     logger.debug( "The root does not need having a parent." );
329                 }
330                 else
331                 {
332                     logger.debug( "The module: " + mavenProject.getId() + " has no parent." );
333                     result.add( mavenProject );
334                 }
335             }
336         }
337 
338         return result;
339     }
340 
341     /**
342      * Convenience method to handle adding a dependency to the Map of List.
343      * 
344      * @param result The result List which should be handled.
345      * @param project The MavenProject which will be added.
346      * @param dependency The dependency which will be added.
347      */
348     private void addDep( Map<MavenProject, List<Dependency>> result, MavenProject project, Dependency dependency )
349     {
350         if ( result.containsKey( project ) )
351         {
352             List<Dependency> list = result.get( project );
353             if ( list == null )
354             {
355                 list = new ArrayList<>();
356             }
357             list.add( dependency );
358             result.put( project, list );
359         }
360         else
361         {
362             List<Dependency> list = new ArrayList<>();
363             list.add( dependency );
364             result.put( project, list );
365         }
366     }
367 
368     /**
369      * Go through the list of modules in the builds and check if we have dependencies. If yes we will check every
370      * dependency based on groupId/artifactId if it belongs to the multi module build. In such a case it will be checked
371      * if the version does fit the version in the rest of build.
372      * 
373      * @param reactorVersion The version of the reactor.
374      * @param sortedProjects The list of existing projects within this build.
375      * @return List of violations. Never null. If the list is empty than no violation has happened.
376      */
377     // CHECKSTYLE_OFF: LineLength
378     private Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor( String reactorVersion,
379                                                                                                  List<MavenProject> sortedProjects )
380     // CHECKSTYLE_ON: LineLength
381     {
382         Map<MavenProject, List<Dependency>> result = new HashMap<>();
383         for ( MavenProject mavenProject : sortedProjects )
384         {
385             logger.debug( "Project: " + mavenProject.getId() );
386 
387             List<Dependency> dependencies = mavenProject.getDependencies();
388             if ( hasDependencies( dependencies ) )
389             {
390                 for ( Dependency dependency : dependencies )
391                 {
392                     logger.debug( " -> Dep:" + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
393                         + dependency.getVersion() );
394                     if ( isDependencyPartOfTheReactor( dependency, sortedProjects ) )
395                     {
396                         if ( !dependency.getVersion().equals( reactorVersion ) )
397                         {
398                             addDep( result, mavenProject, dependency );
399                         }
400                     }
401                 }
402             }
403         }
404 
405         return result;
406     }
407 
408     /**
409      * This method will check the following situation within a multi-module build.
410      * 
411      * <pre>
412      *  &lt;parent&gt;
413      *    &lt;groupId&gt;...&lt;/groupId&gt;
414      *    &lt;artifactId&gt;...&lt;/artifactId&gt;
415      *    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
416      *  &lt;/parent&gt;
417      *  
418      *  &lt;version&gt;1.1-SNAPSHOT&lt;/version&gt;
419      * </pre>
420      * 
421      * @param projectList The sorted list of the reactor modules.
422      * @return The resulting list will contain the modules in the reactor which do the thing in the example above. The
423      *         list will never null. If the list is empty no violation have happened.
424      */
425     private List<MavenProject> isReactorVersionConsistent( List<MavenProject> projectList )
426     {
427         List<MavenProject> result = new ArrayList<>();
428 
429         if ( projectList != null && !projectList.isEmpty() )
430         {
431             String version = projectList.get( 0 ).getVersion();
432             logger.debug( "First version:" + version );
433             for ( MavenProject mavenProject : projectList )
434             {
435                 logger.debug( " -> checking " + mavenProject.getId() );
436                 if ( !version.equals( mavenProject.getVersion() ) )
437                 {
438                     result.add( mavenProject );
439                 }
440             }
441         }
442         return result;
443     }
444 
445     private boolean hasDependencies( List<Dependency> dependencies )
446     {
447         return dependencies != null && !dependencies.isEmpty();
448     }
449 
450     private boolean hasParent( MavenProject mavenProject )
451     {
452         return mavenProject.getParent() != null;
453     }
454 
455     public boolean isIgnoreModuleDependencies()
456     {
457         return ignoreModuleDependencies;
458     }
459 
460     public void setIgnoreModuleDependencies( boolean ignoreModuleDependencies )
461     {
462         this.ignoreModuleDependencies = ignoreModuleDependencies;
463     }
464 
465     /**
466      * This will add the given user message to the output.
467      * 
468      * @param sb The already initialized exception message part.
469      */
470     private void addMessageIfExist( StringBuilder sb )
471     {
472         if ( !StringUtils.isEmpty( getMessage() ) )
473         {
474             sb.append( getMessage() );
475             sb.append( System.lineSeparator() );
476         }
477     }
478 
479 }