001package org.apache.maven.plugins.enforcer;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027
028import org.apache.commons.lang3.SystemUtils;
029import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
030import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
031import org.apache.maven.execution.MavenSession;
032import org.apache.maven.model.Dependency;
033import org.apache.maven.plugin.logging.Log;
034import org.apache.maven.project.MavenProject;
035import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
036import org.codehaus.plexus.util.StringUtils;
037
038/**
039 * This rule will check if a multi module build will follow the best practices.
040 * 
041 * @author Karl-Heinz Marbaise
042 * @since 1.4
043 */
044public class ReactorModuleConvergence
045    extends AbstractNonCacheableEnforcerRule
046{
047    private boolean ignoreModuleDependencies = false;
048
049    private Log logger;
050
051    @Override
052    public void execute( EnforcerRuleHelper helper )
053        throws EnforcerRuleException
054    {
055        logger = helper.getLog();
056
057        MavenSession session;
058        try
059        {
060            session = (MavenSession) helper.evaluate( "${session}" );
061        }
062        catch ( ExpressionEvaluationException eee )
063        {
064            throw new EnforcerRuleException( "Unable to retrieve the MavenSession: ", eee );
065        }
066
067        List<MavenProject> sortedProjects = session.getSortedProjects();
068        if ( sortedProjects != null && !sortedProjects.isEmpty() )
069        {
070            checkReactor( sortedProjects );
071            checkParentsInReactor( sortedProjects );
072            checkMissingParentsInReactor( sortedProjects );
073            checkParentsPartOfTheReactor( sortedProjects );
074            if ( !isIgnoreModuleDependencies() )
075            {
076                checkDependenciesWithinReactor( sortedProjects );
077            }
078        }
079
080    }
081
082    private void checkParentsPartOfTheReactor( List<MavenProject> sortedProjects )
083        throws EnforcerRuleException
084    {
085        List<MavenProject> parentsWhichAreNotPartOfTheReactor =
086            existParentsWhichAreNotPartOfTheReactor( sortedProjects );
087        if ( !parentsWhichAreNotPartOfTheReactor.isEmpty() )
088        {
089            StringBuilder sb = new StringBuilder().append( SystemUtils.LINE_SEPARATOR );
090            addMessageIfExist( sb );
091            for ( MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor )
092            {
093                sb.append( " module: " );
094                sb.append( mavenProject.getId() );
095                sb.append( SystemUtils.LINE_SEPARATOR );
096            }
097            throw new EnforcerRuleException( "Module parents have been found which could not be found in the reactor."
098                + sb.toString() );
099        }
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}