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