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.maven.enforcer.rule.api.EnforcerRuleException;
029import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
030import org.apache.maven.execution.MavenSession;
031import org.apache.maven.model.Dependency;
032import org.apache.maven.plugin.logging.Log;
033import org.apache.maven.project.MavenProject;
034import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
035import org.codehaus.plexus.util.StringUtils;
036
037/**
038 * This rule will check if a multi module build will follow the best practices.
039 * 
040 * @author Karl-Heinz Marbaise
041 * @since 1.4
042 */
043public class ReactorModuleConvergence
044    extends AbstractNonCacheableEnforcerRule
045{
046    private static final String MODULE_TEXT = " module: ";
047
048    private boolean ignoreModuleDependencies = false;
049
050    private Log logger;
051
052    @Override
053    public void execute( EnforcerRuleHelper helper )
054        throws EnforcerRuleException
055    {
056        logger = helper.getLog();
057
058        MavenSession session;
059        try
060        {
061            session = (MavenSession) helper.evaluate( "${session}" );
062        }
063        catch ( ExpressionEvaluationException eee )
064        {
065            throw new EnforcerRuleException( "Unable to retrieve the MavenSession: ", eee );
066        }
067
068        List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
069        if ( sortedProjects != null && !sortedProjects.isEmpty() )
070        {
071            checkReactor( sortedProjects );
072            checkParentsInReactor( sortedProjects );
073            checkMissingParentsInReactor( sortedProjects );
074            checkParentsPartOfTheReactor( sortedProjects );
075            if ( !isIgnoreModuleDependencies() )
076            {
077                checkDependenciesWithinReactor( sortedProjects );
078            }
079        }
080
081    }
082
083    private void checkParentsPartOfTheReactor( List<MavenProject> sortedProjects )
084        throws EnforcerRuleException
085    {
086        List<MavenProject> parentsWhichAreNotPartOfTheReactor =
087            existParentsWhichAreNotPartOfTheReactor( sortedProjects );
088        if ( !parentsWhichAreNotPartOfTheReactor.isEmpty() )
089        {
090            StringBuilder sb = new StringBuilder().append( System.lineSeparator() );
091            addMessageIfExist( sb );
092            for ( MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor )
093            {
094                sb.append( MODULE_TEXT );
095                sb.append( mavenProject.getId() );
096                sb.append( System.lineSeparator() );
097            }
098            throw new EnforcerRuleException( "Module parents have been found which could not be found in the reactor."
099                + 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}