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. Maven 3.10.0 and Maven 4 reject such POMs
045 * themselves, before this rule is executed.
046 *
047 * @author Robert Scholte
048 * @since 1.3
049 */
050@Named("banDuplicatePomDependencyVersions")
051public final class BanDuplicatePomDependencyVersions extends AbstractStandardEnforcerRule {
052
053    private final MavenProject project;
054
055    @Inject
056    public BanDuplicatePomDependencyVersions(MavenProject project) {
057        this.project = Objects.requireNonNull(project);
058    }
059
060    @Override
061    public void execute() throws EnforcerRuleException {
062
063        // re-read model, because M3 uses optimized model
064        MavenXpp3Reader modelReader = new MavenXpp3Reader();
065
066        try (FileInputStream pomInputStream = new FileInputStream(project.getFile())) {
067            Model model = modelReader.read(pomInputStream, false);
068            maven2Validation(model);
069        } catch (IOException | XmlPullParserException e) {
070            throw new EnforcerRuleError("Unable to retrieve the MavenProject: ", e);
071        }
072    }
073
074    private void maven2Validation(Model model) throws EnforcerRuleException {
075        List<Dependency> dependencies = model.getDependencies();
076        Map<String, Integer> duplicateDependencies = validateDependencies(dependencies);
077
078        int duplicates = duplicateDependencies.size();
079
080        StringBuilder summary = new StringBuilder();
081        messageBuilder(duplicateDependencies, "dependencies.dependency", summary);
082
083        if (model.getDependencyManagement() != null) {
084            List<Dependency> managementDependencies =
085                    model.getDependencyManagement().getDependencies();
086            Map<String, Integer> duplicateManagementDependencies = validateDependencies(managementDependencies);
087            duplicates += duplicateManagementDependencies.size();
088
089            messageBuilder(duplicateManagementDependencies, "dependencyManagement.dependencies.dependency", summary);
090        }
091
092        List<Profile> profiles = model.getProfiles();
093        for (Profile profile : profiles) {
094            List<Dependency> profileDependencies = profile.getDependencies();
095
096            Map<String, Integer> duplicateProfileDependencies = validateDependencies(profileDependencies);
097
098            duplicates += duplicateProfileDependencies.size();
099
100            messageBuilder(
101                    duplicateProfileDependencies,
102                    "profiles.profile[" + profile.getId() + "].dependencies.dependency",
103                    summary);
104
105            if (profile.getDependencyManagement() != null) {
106                List<Dependency> profileManagementDependencies =
107                        profile.getDependencyManagement().getDependencies();
108
109                Map<String, Integer> duplicateProfileManagementDependencies =
110                        validateDependencies(profileManagementDependencies);
111
112                duplicates += duplicateProfileManagementDependencies.size();
113
114                messageBuilder(
115                        duplicateProfileManagementDependencies,
116                        "profiles.profile[" + profile.getId() + "].dependencyManagement.dependencies.dependency",
117                        summary);
118            }
119        }
120
121        if (summary.length() > 0) {
122            StringBuilder message = new StringBuilder();
123            if (getMessage() != null) {
124                message.append(getMessage()).append("\n");
125            }
126            message.append("Found ").append(duplicates).append(" duplicate dependency ");
127            message.append(duplicateDependencies.size() == 1 ? "declaration" : "declarations")
128                    .append(" in this project:" + System.lineSeparator());
129            message.append(summary);
130            throw new EnforcerRuleException(message.toString());
131        }
132    }
133
134    private void messageBuilder(Map<String, Integer> duplicateDependencies, String prefix, StringBuilder message) {
135        if (!duplicateDependencies.isEmpty()) {
136            for (Map.Entry<String, Integer> entry : duplicateDependencies.entrySet()) {
137                message.append(" - ")
138                        .append(prefix)
139                        .append('[')
140                        .append(entry.getKey())
141                        .append("] (")
142                        .append(entry.getValue())
143                        .append(" times)" + System.lineSeparator());
144            }
145        }
146    }
147
148    private Map<String, Integer> validateDependencies(List<Dependency> dependencies) {
149        Map<String, Integer> duplicateDeps = new HashMap<>();
150        Set<String> deps = new HashSet<>();
151        for (Dependency dependency : dependencies) {
152            String key = dependency.getManagementKey();
153
154            if (deps.contains(key)) {
155                int times = 1;
156                if (duplicateDeps.containsKey(key)) {
157                    times = duplicateDeps.get(key);
158                }
159                duplicateDeps.put(key, times + 1);
160            } else {
161                deps.add(key);
162            }
163        }
164        return duplicateDeps;
165    }
166}