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.eclipse.aether.util.graph.visitor;
020
021import java.util.ArrayDeque;
022import java.util.Collection;
023import java.util.Deque;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.Objects;
027import java.util.function.Consumer;
028
029import org.eclipse.aether.artifact.Artifact;
030import org.eclipse.aether.graph.Dependency;
031import org.eclipse.aether.graph.DependencyNode;
032import org.eclipse.aether.graph.DependencyVisitor;
033import org.eclipse.aether.graph.Exclusion;
034import org.eclipse.aether.util.artifact.ArtifactIdUtils;
035import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
036import org.eclipse.aether.util.graph.transformer.ConflictResolver;
037
038import static java.util.Objects.requireNonNull;
039
040/**
041 * A dependency visitor that dumps the graph to any {@link Consumer}{@code <String>}. Meant for diagnostic and testing, as
042 * it may output the graph to standard output, error or even some logging interface.
043 *
044 * @since 1.9.8
045 */
046public class DependencyGraphDumper implements DependencyVisitor {
047
048    private final Consumer<String> consumer;
049
050    private final Deque<DependencyNode> nodes = new ArrayDeque<>();
051
052    public DependencyGraphDumper(Consumer<String> consumer) {
053        this.consumer = requireNonNull(consumer);
054    }
055
056    @Override
057    public boolean visitEnter(DependencyNode node) {
058        nodes.push(node);
059        consumer.accept(formatLine(nodes));
060        return true;
061    }
062
063    @Override
064    public boolean visitLeave(DependencyNode node) {
065        if (!nodes.isEmpty()) {
066            nodes.pop();
067        }
068        return true;
069    }
070
071    protected String formatLine(Deque<DependencyNode> nodes) {
072        return formatIndentation(nodes) + formatNode(nodes);
073    }
074
075    protected String formatIndentation(Deque<DependencyNode> nodes) {
076        StringBuilder buffer = new StringBuilder(128);
077        Iterator<DependencyNode> iter = nodes.descendingIterator();
078        DependencyNode parent = iter.hasNext() ? iter.next() : null;
079        DependencyNode child = iter.hasNext() ? iter.next() : null;
080        while (parent != null && child != null) {
081            boolean lastChild = parent.getChildren().get(parent.getChildren().size() - 1) == child;
082            boolean end = child == nodes.peekFirst();
083            String indent;
084            if (end) {
085                indent = lastChild ? "\\- " : "+- ";
086            } else {
087                indent = lastChild ? "   " : "|  ";
088            }
089            buffer.append(indent);
090            parent = child;
091            child = iter.hasNext() ? iter.next() : null;
092        }
093        return buffer.toString();
094    }
095
096    protected String formatNode(Deque<DependencyNode> nodes) {
097        DependencyNode node = requireNonNull(nodes.peek(), "bug: should not happen");
098        StringBuilder buffer = new StringBuilder(128);
099        Artifact a = node.getArtifact();
100        buffer.append(a);
101        Dependency d = node.getDependency();
102        if (d != null && !d.getScope().isEmpty()) {
103            buffer.append(" [").append(d.getScope());
104            if (d.isOptional()) {
105                buffer.append(", optional");
106            }
107            buffer.append("]");
108        }
109        String premanaged = DependencyManagerUtils.getPremanagedVersion(node);
110        if (premanaged != null && !premanaged.equals(a.getBaseVersion())) {
111            buffer.append(" (version managed from ").append(premanaged).append(")");
112        }
113
114        premanaged = DependencyManagerUtils.getPremanagedScope(node);
115        if (premanaged != null && d != null && !premanaged.equals(d.getScope())) {
116            buffer.append(" (scope managed from ").append(premanaged).append(")");
117        }
118
119        Boolean premanagedOptional = DependencyManagerUtils.getPremanagedOptional(node);
120        if (premanagedOptional != null && d != null && !premanagedOptional.equals(d.getOptional())) {
121            buffer.append(" (optionality managed from ")
122                    .append(premanagedOptional)
123                    .append(")");
124        }
125
126        Collection<Exclusion> premanagedExclusions = DependencyManagerUtils.getPremanagedExclusions(node);
127        if (premanagedExclusions != null && d != null && !equals(premanagedExclusions, d.getExclusions())) {
128            buffer.append(" (exclusions managed from ")
129                    .append(premanagedExclusions)
130                    .append(")");
131        }
132
133        Map<String, String> premanagedProperties = DependencyManagerUtils.getPremanagedProperties(node);
134        if (premanagedProperties != null && !equals(premanagedProperties, a.getProperties())) {
135            buffer.append(" (properties managed from ")
136                    .append(premanagedProperties)
137                    .append(")");
138        }
139
140        DependencyNode winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER);
141        if (winner != null) {
142            if (ArtifactIdUtils.equalsId(a, winner.getArtifact())) {
143                buffer.append(" (nearer exists)");
144            } else {
145                Artifact w = winner.getArtifact();
146                buffer.append(" (conflicts with ");
147                if (ArtifactIdUtils.toVersionlessId(a).equals(ArtifactIdUtils.toVersionlessId(w))) {
148                    buffer.append(w.getVersion());
149                } else {
150                    buffer.append(w);
151                }
152                buffer.append(")");
153            }
154        }
155        return buffer.toString();
156    }
157
158    private boolean equals(Collection<Exclusion> c1, Collection<Exclusion> c2) {
159        return c1 != null && c2 != null && c1.size() == c2.size() && c1.containsAll(c2);
160    }
161
162    private boolean equals(Map<String, String> m1, Map<String, String> m2) {
163        return m1 != null
164                && m2 != null
165                && m1.size() == m2.size()
166                && m1.entrySet().stream().allMatch(entry -> Objects.equals(m2.get(entry.getKey()), entry.getValue()));
167    }
168}