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.List;
028import java.util.Objects;
029
030import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
031import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
032import org.apache.maven.enforcer.rules.utils.ArtifactUtils;
033import org.eclipse.aether.graph.DependencyNode;
034
035import static org.apache.maven.artifact.Artifact.SCOPE_PROVIDED;
036import static org.apache.maven.artifact.Artifact.SCOPE_TEST;
037
038/**
039 * @author <a href="mailto:rex@e-hoffman.org">Rex Hoffman</a>
040 */
041@Named("dependencyConvergence")
042public final class DependencyConvergence extends AbstractStandardEnforcerRule {
043
044    // parameters
045
046    private boolean uniqueVersions;
047
048    private List<String> includes;
049
050    private List<String> excludes;
051
052    private List<String> excludedScopes = Arrays.asList(SCOPE_TEST, SCOPE_PROVIDED);
053
054    // parameters - end
055
056    private DependencyVersionMap dependencyVersionMap;
057
058    private final ResolverUtil resolverUtil;
059
060    @Inject
061    public DependencyConvergence(ResolverUtil resolverUtil) {
062        this.resolverUtil = Objects.requireNonNull(resolverUtil);
063    }
064
065    @Override
066    public void execute() throws EnforcerRuleException {
067
068        DependencyNode node = resolverUtil.resolveTransitiveDependenciesVerbose(excludedScopes);
069        dependencyVersionMap = new DependencyVersionMap().setUniqueVersions(uniqueVersions);
070        node.accept(dependencyVersionMap);
071
072        List<String> errorMsgs =
073                getConvergenceErrorMsgs(dependencyVersionMap.getConflictedVersionNumbers(includes, excludes));
074
075        if (!errorMsgs.isEmpty()) {
076            throw new EnforcerRuleException("Failed while enforcing releasability." + System.lineSeparator()
077                    + String.join(System.lineSeparator(), errorMsgs));
078        }
079    }
080
081    private StringBuilder buildTreeString(DependencyNode node) {
082        List<String> loc = new ArrayList<>();
083        DependencyNode currentNode = node;
084        while (currentNode != null) {
085            // ArtifactUtils.toArtifact(node) adds scope and optional information, if present
086            loc.add(ArtifactUtils.toArtifact(currentNode).toString());
087            currentNode = dependencyVersionMap.getParent(currentNode);
088        }
089        Collections.reverse(loc);
090        StringBuilder builder = new StringBuilder();
091        for (int i = 0; i < loc.size(); i++) {
092            for (int j = 0; j < i; j++) {
093                builder.append("  ");
094            }
095            builder.append("+-").append(loc.get(i)).append(System.lineSeparator());
096        }
097        return builder;
098    }
099
100    private List<String> getConvergenceErrorMsgs(List<List<DependencyNode>> errors) {
101        List<String> errorMsgs = new ArrayList<>();
102        for (List<DependencyNode> nodeList : errors) {
103            errorMsgs.add(buildConvergenceErrorMsg(nodeList));
104        }
105        return errorMsgs;
106    }
107
108    private String buildConvergenceErrorMsg(List<DependencyNode> nodeList) {
109        StringBuilder builder = new StringBuilder();
110        builder.append(System.lineSeparator())
111                .append("Dependency convergence error for ")
112                .append(nodeList.get(0).getArtifact().toString())
113                .append(" paths to dependency are:")
114                .append(System.lineSeparator());
115        if (nodeList.size() > 0) {
116            builder.append(buildTreeString(nodeList.get(0)));
117        }
118        for (DependencyNode node : nodeList.subList(1, nodeList.size())) {
119            builder.append("and").append(System.lineSeparator()).append(buildTreeString(node));
120        }
121        return builder.toString();
122    }
123
124    @Override
125    public String toString() {
126        return String.format(
127                "DependencyConvergence[includes=%s, excludes=%s, uniqueVersions=%b]",
128                includes, excludes, uniqueVersions);
129    }
130}