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.io.FileInputStream;
025import java.io.IOException;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.Set;
032
033import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
034import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
035import org.apache.maven.model.Dependency;
036import org.apache.maven.model.Model;
037import org.apache.maven.model.Profile;
038import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
039import org.apache.maven.project.MavenProject;
040import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
041
042/**
043 * Since Maven 3 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique. Early versions of Maven
044 * 3 already warn, this rule can force to break a build for this reason.
045 *
046 * @author Robert Scholte
047 * @since 1.3
048 */
049@Named("banDuplicatePomDependencyVersions")
050public final class BanDuplicatePomDependencyVersions extends AbstractStandardEnforcerRule {
051
052    private final MavenProject project;
053
054    @Inject
055    public BanDuplicatePomDependencyVersions(MavenProject project) {
056        this.project = Objects.requireNonNull(project);
057    }
058
059    @Override
060    public void execute() throws EnforcerRuleException {
061
062        // re-read model, because M3 uses optimized model
063        MavenXpp3Reader modelReader = new MavenXpp3Reader();
064
065        try (FileInputStream pomInputStream = new FileInputStream(project.getFile())) {
066            Model model = modelReader.read(pomInputStream, false);
067            maven2Validation(model);
068        } catch (IOException | XmlPullParserException e) {
069            throw new EnforcerRuleError("Unable to retrieve the MavenProject: ", e);
070        }
071    }
072
073    private void maven2Validation(Model model) throws EnforcerRuleException {
074        List<Dependency> dependencies = model.getDependencies();
075        Map<String, Integer> duplicateDependencies = validateDependencies(dependencies);
076
077        int duplicates = duplicateDependencies.size();
078
079        StringBuilder summary = new StringBuilder();
080        messageBuilder(duplicateDependencies, "dependencies.dependency", summary);
081
082        if (model.getDependencyManagement() != null) {
083            List<Dependency> managementDependencies =
084                    model.getDependencyManagement().getDependencies();
085            Map<String, Integer> duplicateManagementDependencies = validateDependencies(managementDependencies);
086            duplicates += duplicateManagementDependencies.size();
087
088            messageBuilder(duplicateManagementDependencies, "dependencyManagement.dependencies.dependency", summary);
089        }
090
091        List<Profile> profiles = model.getProfiles();
092        for (Profile profile : profiles) {
093            List<Dependency> profileDependencies = profile.getDependencies();
094
095            Map<String, Integer> duplicateProfileDependencies = validateDependencies(profileDependencies);
096
097            duplicates += duplicateProfileDependencies.size();
098
099            messageBuilder(
100                    duplicateProfileDependencies,
101                    "profiles.profile[" + profile.getId() + "].dependencies.dependency",
102                    summary);
103
104            if (profile.getDependencyManagement() != null) {
105                List<Dependency> profileManagementDependencies =
106                        profile.getDependencyManagement().getDependencies();
107
108                Map<String, Integer> duplicateProfileManagementDependencies =
109                        validateDependencies(profileManagementDependencies);
110
111                duplicates += duplicateProfileManagementDependencies.size();
112
113                messageBuilder(
114                        duplicateProfileManagementDependencies,
115                        "profiles.profile[" + profile.getId() + "].dependencyManagement.dependencies.dependency",
116                        summary);
117            }
118        }
119
120        if (summary.length() > 0) {
121            StringBuilder message = new StringBuilder();
122            message.append("Found ").append(duplicates).append(" duplicate dependency ");
123            message.append(duplicateDependencies.size() == 1 ? "declaration" : "declarations")
124                    .append(" in this project:" + System.lineSeparator());
125            message.append(summary);
126            throw new EnforcerRuleException(message.toString());
127        }
128    }
129
130    private void messageBuilder(Map<String, Integer> duplicateDependencies, String prefix, StringBuilder message) {
131        if (!duplicateDependencies.isEmpty()) {
132            for (Map.Entry<String, Integer> entry : duplicateDependencies.entrySet()) {
133                message.append(" - ")
134                        .append(prefix)
135                        .append('[')
136                        .append(entry.getKey())
137                        .append("] (")
138                        .append(entry.getValue())
139                        .append(" times)" + System.lineSeparator());
140            }
141        }
142    }
143
144    private Map<String, Integer> validateDependencies(List<Dependency> dependencies) {
145        Map<String, Integer> duplicateDeps = new HashMap<>();
146        Set<String> deps = new HashSet<>();
147        for (Dependency dependency : dependencies) {
148            String key = dependency.getManagementKey();
149
150            if (deps.contains(key)) {
151                int times = 1;
152                if (duplicateDeps.containsKey(key)) {
153                    times = duplicateDeps.get(key);
154                }
155                duplicateDeps.put(key, times + 1);
156            } else {
157                deps.add(key);
158            }
159        }
160        return duplicateDeps;
161    }
162}