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.dependency;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031
032import org.apache.maven.artifact.Artifact;
033import org.apache.maven.artifact.versioning.ArtifactVersion;
034import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
035import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
036import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
037import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
038import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
039import org.apache.maven.enforcer.rules.utils.ParentNodeProvider;
040import org.apache.maven.enforcer.rules.utils.ParentsVisitor;
041import org.eclipse.aether.graph.DependencyNode;
042import org.eclipse.aether.graph.DependencyVisitor;
043import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
044
045import static org.apache.maven.artifact.Artifact.SCOPE_PROVIDED;
046import static org.apache.maven.artifact.Artifact.SCOPE_TEST;
047
048/**
049 * Rule to enforce that the resolved dependency is also the most recent one of all transitive dependencies.
050 *
051 * @author Geoffrey De Smet
052 * @since 1.1
053 */
054@Named("requireUpperBoundDeps")
055public final class RequireUpperBoundDeps extends AbstractStandardEnforcerRule {
056
057    /**
058     * @since 1.3
059     */
060    private boolean uniqueVersions;
061
062    /**
063     * Dependencies to ignore.
064     *
065     * @since TBD
066     */
067    private List<String> excludes = null;
068
069    /**
070     * Dependencies to include.
071     *
072     * @since 3.0.0
073     */
074    private List<String> includes = null;
075
076    /**
077     * Scope to exclude.
078     */
079    private List<String> excludedScopes = Arrays.asList(SCOPE_TEST, SCOPE_PROVIDED);
080
081    private RequireUpperBoundDepsVisitor upperBoundDepsVisitor;
082
083    private final ResolverUtil resolverUtil;
084
085    @Inject
086    public RequireUpperBoundDeps(ResolverUtil resolverUtil) {
087        this.resolverUtil = Objects.requireNonNull(resolverUtil);
088    }
089
090    /**
091     * Sets dependencies to exclude.
092     * @param excludes a list of {@code groupId:artifactId} names
093     */
094    public void setExcludes(List<String> excludes) {
095        this.excludes = excludes;
096    }
097
098    /**
099     * Sets dependencies to include.
100     *
101     * @param includes a list of {@code groupId:artifactId} names
102     */
103    public void setIncludes(List<String> includes) {
104        this.includes = includes;
105    }
106
107    @Override
108    public void execute() throws EnforcerRuleException {
109        DependencyNode node = resolverUtil.resolveTransitiveDependenciesVerbose(excludedScopes);
110        upperBoundDepsVisitor = new RequireUpperBoundDepsVisitor()
111                .setUniqueVersions(uniqueVersions)
112                .setIncludes(includes);
113        getLog().debug(() -> resolverUtil.dumpTree(node));
114        node.accept(upperBoundDepsVisitor);
115        List<String> errorMessages = buildErrorMessages(upperBoundDepsVisitor.getConflicts());
116        if (!errorMessages.isEmpty()) {
117            throw new EnforcerRuleException(
118                    "Failed while enforcing RequireUpperBoundDeps. The error(s) are " + errorMessages);
119        }
120    }
121
122    private List<String> buildErrorMessages(List<List<DependencyNode>> conflicts) {
123        List<String> errorMessages = new ArrayList<>(conflicts.size());
124        for (List<DependencyNode> conflict : conflicts) {
125            org.eclipse.aether.artifact.Artifact artifact = conflict.get(0).getArtifact();
126            String groupArt = artifact.getGroupId() + ":" + artifact.getArtifactId();
127            if (excludes != null && excludes.contains(groupArt)) {
128                getLog().info("Ignoring requireUpperBoundDeps in " + groupArt);
129            } else {
130                errorMessages.add(buildErrorMessage(conflict));
131            }
132        }
133        return errorMessages;
134    }
135
136    private String buildErrorMessage(List<DependencyNode> conflict) {
137        StringBuilder errorMessage = new StringBuilder();
138        errorMessage
139                .append(System.lineSeparator())
140                .append("Require upper bound dependencies error for ")
141                .append(getFullArtifactName(conflict.get(0), false))
142                .append(" paths to dependency are:")
143                .append(System.lineSeparator());
144        if (conflict.size() > 0) {
145            errorMessage.append(buildTreeString(conflict.get(0)));
146        }
147        for (DependencyNode node : conflict.subList(1, conflict.size())) {
148            errorMessage.append("and").append(System.lineSeparator());
149            errorMessage.append(buildTreeString(node));
150        }
151        return errorMessage.toString();
152    }
153
154    private StringBuilder buildTreeString(DependencyNode node) {
155        List<String> loc = new ArrayList<>();
156        DependencyNode currentNode = node;
157        while (currentNode != null) {
158            StringBuilder line = new StringBuilder(getFullArtifactName(currentNode, false));
159
160            if (DependencyManagerUtils.getPremanagedVersion(currentNode) != null) {
161                line.append(" (managed) <-- ");
162                line.append(getFullArtifactName(currentNode, true));
163            }
164
165            loc.add(line.toString());
166            currentNode = upperBoundDepsVisitor.getParent(currentNode);
167        }
168        Collections.reverse(loc);
169        StringBuilder builder = new StringBuilder();
170        for (int i = 0; i < loc.size(); i++) {
171            for (int j = 0; j < i; j++) {
172                builder.append("  ");
173            }
174            builder.append("+-").append(loc.get(i));
175            builder.append(System.lineSeparator());
176        }
177        return builder;
178    }
179
180    private String getFullArtifactName(DependencyNode node, boolean usePremanaged) {
181        Artifact artifact = ArtifactUtils.toArtifact(node);
182
183        String version = DependencyManagerUtils.getPremanagedVersion(node);
184        if (!usePremanaged || version == null) {
185            version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
186        }
187        String result = artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + version;
188
189        String classifier = artifact.getClassifier();
190        if (classifier != null && !classifier.isEmpty()) {
191            result += ":" + classifier;
192        }
193
194        String scope = artifact.getScope();
195        if (scope != null && !"compile".equals(scope)) {
196            result += " [" + scope + ']';
197        }
198
199        return result;
200    }
201
202    private static class RequireUpperBoundDepsVisitor implements DependencyVisitor, ParentNodeProvider {
203
204        private final ParentsVisitor parentsVisitor = new ParentsVisitor();
205        private boolean uniqueVersions;
206        private List<String> includes = null;
207
208        public RequireUpperBoundDepsVisitor setUniqueVersions(boolean uniqueVersions) {
209            this.uniqueVersions = uniqueVersions;
210            return this;
211        }
212
213        public RequireUpperBoundDepsVisitor setIncludes(List<String> includes) {
214            this.includes = includes;
215            return this;
216        }
217
218        private final Map<String, List<DependencyNodeHopCountPair>> keyToPairsMap = new HashMap<>();
219
220        @Override
221        public boolean visitEnter(DependencyNode node) {
222            parentsVisitor.visitEnter(node);
223            DependencyNodeHopCountPair pair = new DependencyNodeHopCountPair(node, this);
224            String key = pair.constructKey();
225
226            if (includes != null && !includes.isEmpty() && !includes.contains(key)) {
227                return true;
228            }
229
230            keyToPairsMap.computeIfAbsent(key, k1 -> new ArrayList<>()).add(pair);
231            keyToPairsMap.get(key).sort(DependencyNodeHopCountPair::compareTo);
232            return true;
233        }
234
235        @Override
236        public boolean visitLeave(DependencyNode node) {
237            return parentsVisitor.visitLeave(node);
238        }
239
240        public List<List<DependencyNode>> getConflicts() {
241            List<List<DependencyNode>> output = new ArrayList<>();
242            for (List<DependencyNodeHopCountPair> pairs : keyToPairsMap.values()) {
243                if (containsConflicts(pairs)) {
244                    List<DependencyNode> outputSubList = new ArrayList<>(pairs.size());
245                    for (DependencyNodeHopCountPair pair : pairs) {
246                        outputSubList.add(pair.getNode());
247                    }
248                    output.add(outputSubList);
249                }
250            }
251            return output;
252        }
253
254        private boolean containsConflicts(List<DependencyNodeHopCountPair> pairs) {
255            DependencyNodeHopCountPair resolvedPair = pairs.get(0);
256            ArtifactVersion resolvedVersion = resolvedPair.extractArtifactVersion(uniqueVersions, false);
257
258            for (DependencyNodeHopCountPair pair : pairs) {
259                ArtifactVersion version = pair.extractArtifactVersion(uniqueVersions, true);
260                if (resolvedVersion.compareTo(version) < 0) {
261                    return true;
262                }
263            }
264            return false;
265        }
266
267        @Override
268        public DependencyNode getParent(DependencyNode node) {
269            return parentsVisitor.getParent(node);
270        }
271    }
272
273    private static class DependencyNodeHopCountPair implements Comparable<DependencyNodeHopCountPair> {
274        private final DependencyNode node;
275        private int hopCount;
276        private final ParentNodeProvider parentNodeProvider;
277
278        private DependencyNodeHopCountPair(DependencyNode node, ParentNodeProvider parentNodeProvider) {
279            this.parentNodeProvider = parentNodeProvider;
280            this.node = node;
281            countHops();
282        }
283
284        private void countHops() {
285            hopCount = 0;
286            DependencyNode parent = parentNodeProvider.getParent(node);
287            while (parent != null) {
288                hopCount++;
289                parent = parentNodeProvider.getParent(parent);
290            }
291        }
292
293        private String constructKey() {
294            Artifact artifact = ArtifactUtils.toArtifact(node);
295            return artifact.getGroupId() + ":" + artifact.getArtifactId();
296        }
297
298        public DependencyNode getNode() {
299            return node;
300        }
301
302        private ArtifactVersion extractArtifactVersion(boolean uniqueVersions, boolean usePremanagedVersion) {
303            if (usePremanagedVersion && DependencyManagerUtils.getPremanagedVersion(node) != null) {
304                return new DefaultArtifactVersion(DependencyManagerUtils.getPremanagedVersion(node));
305            }
306
307            Artifact artifact = ArtifactUtils.toArtifact(node);
308            String version = uniqueVersions ? artifact.getVersion() : artifact.getBaseVersion();
309            if (version != null) {
310                return new DefaultArtifactVersion(version);
311            }
312            try {
313                return artifact.getSelectedVersion();
314            } catch (OverConstrainedVersionException e) {
315                throw new RuntimeException("Version ranges problem with " + node.getArtifact(), e);
316            }
317        }
318
319        public int getHopCount() {
320            return hopCount;
321        }
322
323        public int compareTo(DependencyNodeHopCountPair other) {
324            return Integer.compare(hopCount, other.getHopCount());
325        }
326    }
327}