001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.enforcer.rules;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Objects;
030
031import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
032import org.apache.maven.execution.MavenSession;
033import org.apache.maven.model.Dependency;
034import org.apache.maven.project.MavenProject;
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 */
043@Named("reactorModuleConvergence")
044public final class ReactorModuleConvergence extends AbstractStandardEnforcerRule {
045    private static final String MODULE_TEXT = " module: ";
046
047    private boolean ignoreModuleDependencies = false;
048
049    private final MavenSession session;
050
051    @Inject
052    public ReactorModuleConvergence(MavenSession session) {
053        this.session = Objects.requireNonNull(session);
054    }
055
056    @Override
057    public void execute() throws EnforcerRuleException {
058
059        List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
060        if (sortedProjects != null && !sortedProjects.isEmpty()) {
061            checkReactor(sortedProjects);
062            checkParentsInReactor(sortedProjects);
063            checkMissingParentsInReactor(sortedProjects);
064            checkParentsPartOfTheReactor(sortedProjects);
065            if (!isIgnoreModuleDependencies()) {
066                checkDependenciesWithinReactor(sortedProjects);
067            }
068        }
069    }
070
071    private void checkParentsPartOfTheReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
072        List<MavenProject> parentsWhichAreNotPartOfTheReactor = existParentsWhichAreNotPartOfTheReactor(sortedProjects);
073        if (!parentsWhichAreNotPartOfTheReactor.isEmpty()) {
074            StringBuilder sb = new StringBuilder().append(System.lineSeparator());
075            addMessageIfExist(sb);
076            for (MavenProject mavenProject : parentsWhichAreNotPartOfTheReactor) {
077                sb.append(MODULE_TEXT);
078                sb.append(mavenProject.getId());
079                sb.append(System.lineSeparator());
080            }
081            throw new EnforcerRuleException(
082                    "Module parents have been found which could not be found in the reactor." + sb);
083        }
084    }
085
086    /**
087     * Convenience method to create a user readable message.
088     *
089     * @param sortedProjects The list of reactor projects.
090     * @throws EnforcerRuleException In case of a violation.
091     */
092    private void checkMissingParentsInReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
093        List<MavenProject> modulesWithoutParentsInReactor = existModulesWithoutParentsInReactor(sortedProjects);
094        if (!modulesWithoutParentsInReactor.isEmpty()) {
095            StringBuilder sb = new StringBuilder().append(System.lineSeparator());
096            addMessageIfExist(sb);
097            for (MavenProject mavenProject : modulesWithoutParentsInReactor) {
098                sb.append(MODULE_TEXT);
099                sb.append(mavenProject.getId());
100                sb.append(System.lineSeparator());
101            }
102            throw new EnforcerRuleException("Reactor contains modules without parents." + sb);
103        }
104    }
105
106    private void checkDependenciesWithinReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
107        // After we are sure having consistent version we can simply use the first one?
108        String reactorVersion = sortedProjects.get(0).getVersion();
109
110        Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor =
111                areThereDependenciesWhichAreNotPartOfTheReactor(reactorVersion, sortedProjects);
112        if (!areThereDependenciesWhichAreNotPartOfTheReactor.isEmpty()) {
113            StringBuilder sb = new StringBuilder().append(System.lineSeparator());
114            addMessageIfExist(sb);
115            // CHECKSTYLE_OFF: LineLength
116            for (Entry<MavenProject, List<Dependency>> item :
117                    areThereDependenciesWhichAreNotPartOfTheReactor.entrySet()) {
118                sb.append(MODULE_TEXT);
119                sb.append(item.getKey().getId());
120                sb.append(System.lineSeparator());
121                for (Dependency dependency : item.getValue()) {
122                    String id =
123                            dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion();
124                    sb.append("    dependency: ");
125                    sb.append(id);
126                    sb.append(System.lineSeparator());
127                }
128            }
129            throw new EnforcerRuleException(
130                    "Reactor modules contains dependencies which do not reference the reactor." + sb);
131            // CHECKSTYLE_ON: LineLength
132        }
133    }
134
135    /**
136     * Convenience method to create a user readable message.
137     *
138     * @param sortedProjects The list of reactor projects.
139     * @throws EnforcerRuleException In case of a violation.
140     */
141    private void checkParentsInReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
142        // After we are sure having consistent version we can simply use the first one?
143        String reactorVersion = sortedProjects.get(0).getVersion();
144
145        List<MavenProject> areParentsFromTheReactor = areParentsFromTheReactor(reactorVersion, sortedProjects);
146        if (!areParentsFromTheReactor.isEmpty()) {
147            StringBuilder sb = new StringBuilder().append(System.lineSeparator());
148            addMessageIfExist(sb);
149            for (MavenProject mavenProject : areParentsFromTheReactor) {
150                sb.append(" --> ");
151                sb.append(mavenProject.getId());
152                sb.append(" parent:");
153                sb.append(mavenProject.getParent().getId());
154                sb.append(System.lineSeparator());
155            }
156            throw new EnforcerRuleException("Reactor modules have parents which contain a wrong version." + sb);
157        }
158    }
159
160    /**
161     * Convenience method to create user readable message.
162     *
163     * @param sortedProjects The list of reactor projects.
164     * @throws EnforcerRuleException In case of a violation.
165     */
166    private void checkReactor(List<MavenProject> sortedProjects) throws EnforcerRuleException {
167        List<MavenProject> consistenceCheckResult = isReactorVersionConsistent(sortedProjects);
168        if (!consistenceCheckResult.isEmpty()) {
169            StringBuilder sb = new StringBuilder().append(System.lineSeparator());
170            addMessageIfExist(sb);
171            for (MavenProject mavenProject : consistenceCheckResult) {
172                sb.append(" --> ");
173                sb.append(mavenProject.getId());
174                sb.append(System.lineSeparator());
175            }
176            throw new EnforcerRuleException("The reactor contains different versions." + sb);
177        }
178    }
179
180    private List<MavenProject> areParentsFromTheReactor(String reactorVersion, List<MavenProject> sortedProjects) {
181        List<MavenProject> result = new ArrayList<>();
182
183        for (MavenProject mavenProject : sortedProjects) {
184            getLog().debug("Project: " + mavenProject.getId());
185            if (hasParent(mavenProject)) {
186                if (!mavenProject.isExecutionRoot()) {
187                    MavenProject parent = mavenProject.getParent();
188                    if (!reactorVersion.equals(parent.getVersion())) {
189                        getLog().debug("The project: " + mavenProject.getId()
190                                + " has a parent which version does not match the other elements in reactor");
191                        result.add(mavenProject);
192                    }
193                }
194            } else {
195                // This situation is currently ignored, cause it's handled by existModulesWithoutParentsInReactor()
196            }
197        }
198
199        return result;
200    }
201
202    private List<MavenProject> existParentsWhichAreNotPartOfTheReactor(List<MavenProject> sortedProjects) {
203        List<MavenProject> result = new ArrayList<>();
204
205        for (MavenProject mavenProject : sortedProjects) {
206            getLog().debug("Project: " + mavenProject.getId());
207            if (hasParent(mavenProject)) {
208                if (!mavenProject.isExecutionRoot()) {
209                    MavenProject parent = mavenProject.getParent();
210                    if (!isProjectPartOfTheReactor(parent, sortedProjects)) {
211                        result.add(mavenProject);
212                    }
213                }
214            }
215        }
216
217        return result;
218    }
219
220    /**
221     * This will check of the groupId/artifactId can be found in any reactor project. The version will be ignored cause
222     * versions are checked before.
223     *
224     * @param project        The project which should be checked if it is contained in the sortedProjects.
225     * @param sortedProjects The list of existing projects.
226     * @return true if the project has been found within the list false otherwise.
227     */
228    private boolean isProjectPartOfTheReactor(MavenProject project, List<MavenProject> sortedProjects) {
229        return isGAPartOfTheReactor(project.getGroupId(), project.getArtifactId(), sortedProjects);
230    }
231
232    private boolean isDependencyPartOfTheReactor(Dependency dependency, List<MavenProject> sortedProjects) {
233        return isGAPartOfTheReactor(dependency.getGroupId(), dependency.getArtifactId(), sortedProjects);
234    }
235
236    /**
237     * This will check if the given <code>groupId/artifactId</code> is part of the current reactor.
238     *
239     * @param groupId        The groupId
240     * @param artifactId     The artifactId
241     * @param sortedProjects The list of projects within the reactor.
242     * @return true if the groupId/artifactId is part of the reactor false otherwise.
243     */
244    private boolean isGAPartOfTheReactor(String groupId, String artifactId, List<MavenProject> sortedProjects) {
245        boolean result = false;
246        for (MavenProject mavenProject : sortedProjects) {
247            String parentId = groupId + ":" + artifactId;
248            String projectId = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId();
249            if (parentId.equals(projectId)) {
250                result = true;
251            }
252        }
253        return result;
254    }
255
256    /**
257     * Assume we have a module which is a child of a multi module build but this child does not have a parent. This
258     * method will exactly search for such cases.
259     *
260     * @param sortedProjects The sorted list of the reactor modules.
261     * @return The resulting list will contain the modules in the reactor which do not have a parent. The list will
262     *         never null. If the list is empty no violation have happened.
263     */
264    private List<MavenProject> existModulesWithoutParentsInReactor(List<MavenProject> sortedProjects) {
265        List<MavenProject> result = new ArrayList<>();
266
267        for (MavenProject mavenProject : sortedProjects) {
268            getLog().debug("Project: " + mavenProject.getId());
269            if (!hasParent(mavenProject)) {
270                // TODO: Should add an option to force having a parent?
271                if (mavenProject.isExecutionRoot()) {
272                    getLog().debug("The root does not need having a parent.");
273                } else {
274                    getLog().debug("The module: " + mavenProject.getId() + " has no parent.");
275                    result.add(mavenProject);
276                }
277            }
278        }
279
280        return result;
281    }
282
283    /**
284     * Convenience method to handle adding a dependency to the Map of List.
285     *
286     * @param result     The result List which should be handled.
287     * @param project    The MavenProject which will be added.
288     * @param dependency The dependency which will be added.
289     */
290    private void addDep(Map<MavenProject, List<Dependency>> result, MavenProject project, Dependency dependency) {
291        if (result.containsKey(project)) {
292            List<Dependency> list = result.get(project);
293            if (list == null) {
294                list = new ArrayList<>();
295            }
296            list.add(dependency);
297            result.put(project, list);
298        } else {
299            List<Dependency> list = new ArrayList<>();
300            list.add(dependency);
301            result.put(project, list);
302        }
303    }
304
305    /**
306     * Go through the list of modules in the builds and check if we have dependencies. If yes we will check every
307     * dependency based on groupId/artifactId if it belongs to the multi module build. In such a case it will be checked
308     * if the version does fit the version in the rest of build.
309     *
310     * @param reactorVersion The version of the reactor.
311     * @param sortedProjects The list of existing projects within this build.
312     * @return List of violations. Never null. If the list is empty than no violation has happened.
313     */
314    // CHECKSTYLE_OFF: LineLength
315    private Map<MavenProject, List<Dependency>> areThereDependenciesWhichAreNotPartOfTheReactor(
316            String reactorVersion, List<MavenProject> sortedProjects)
317                // CHECKSTYLE_ON: LineLength
318            {
319        Map<MavenProject, List<Dependency>> result = new HashMap<>();
320        for (MavenProject mavenProject : sortedProjects) {
321            getLog().debug("Project: " + mavenProject.getId());
322
323            List<Dependency> dependencies = mavenProject.getDependencies();
324            if (hasDependencies(dependencies)) {
325                for (Dependency dependency : dependencies) {
326                    getLog().debug(" -> Dep:" + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
327                            + dependency.getVersion());
328                    if (isDependencyPartOfTheReactor(dependency, sortedProjects)) {
329                        if (!dependency.getVersion().equals(reactorVersion)) {
330                            addDep(result, mavenProject, dependency);
331                        }
332                    }
333                }
334            }
335        }
336
337        return result;
338    }
339
340    /**
341     * This method will check the following situation within a multi-module build.
342     * <pre>
343     *  &lt;parent&gt;
344     *    &lt;groupId&gt;...&lt;/groupId&gt;
345     *    &lt;artifactId&gt;...&lt;/artifactId&gt;
346     *    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
347     *  &lt;/parent&gt;
348     *  &lt;version&gt;1.1-SNAPSHOT&lt;/version&gt;
349     * </pre>
350     *
351     * @param projectList The sorted list of the reactor modules.
352     * @return The resulting list will contain the modules in the reactor which do the thing in the example above. The
353     *         list will never null. If the list is empty no violation have happened.
354     */
355    private List<MavenProject> isReactorVersionConsistent(List<MavenProject> projectList) {
356        List<MavenProject> result = new ArrayList<>();
357
358        if (projectList != null && !projectList.isEmpty()) {
359            String version = projectList.get(0).getVersion();
360            getLog().debug("First version:" + version);
361            for (MavenProject mavenProject : projectList) {
362                getLog().debug(" -> checking " + mavenProject.getId());
363                if (!version.equals(mavenProject.getVersion())) {
364                    result.add(mavenProject);
365                }
366            }
367        }
368        return result;
369    }
370
371    private boolean hasDependencies(List<Dependency> dependencies) {
372        return dependencies != null && !dependencies.isEmpty();
373    }
374
375    private boolean hasParent(MavenProject mavenProject) {
376        return mavenProject.getParent() != null;
377    }
378
379    public boolean isIgnoreModuleDependencies() {
380        return ignoreModuleDependencies;
381    }
382
383    /**
384     * This will add the given user message to the output.
385     *
386     * @param sb The already initialized exception message part.
387     */
388    private void addMessageIfExist(StringBuilder sb) {
389        if (!StringUtils.isEmpty(getMessage())) {
390            sb.append(getMessage());
391            sb.append(System.lineSeparator());
392        }
393    }
394
395    @Override
396    public String getCacheId() {
397        return String.valueOf(toString().hashCode());
398    }
399
400    @Override
401    public String toString() {
402        return String.format(
403                "ReactorModuleConvergence[message=%s, ignoreModuleDependencies=%b]",
404                getMessage(), ignoreModuleDependencies);
405    }
406}