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