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