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.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Deque;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.function.Consumer;
032import java.util.function.Function;
033import java.util.stream.Collectors;
034
035import org.eclipse.aether.artifact.Artifact;
036import org.eclipse.aether.graph.Dependency;
037import org.eclipse.aether.graph.DependencyNode;
038import org.eclipse.aether.graph.DependencyVisitor;
039import org.eclipse.aether.graph.Exclusion;
040import org.eclipse.aether.util.artifact.ArtifactIdUtils;
041import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
042import org.eclipse.aether.util.graph.transformer.ConflictResolver;
043import org.eclipse.aether.version.VersionConstraint;
044
045import static java.util.Objects.requireNonNull;
046
047/**
048 * A dependency visitor that dumps the graph to any {@link Consumer}{@code <String>}. Meant for diagnostic and testing, as
049 * it may output the graph to standard output, error or even some logging interface.
050 *
051 * @since 1.9.8
052 */
053public class DependencyGraphDumper implements DependencyVisitor {
054    /**
055     * Decorator of "effective dependency": shows effective scope and optionality.
056     */
057    public static Function<DependencyNode, String> effectiveDependency() {
058        return dependencyNode -> {
059            Dependency d = dependencyNode.getDependency();
060            if (d != null) {
061                if (!d.getScope().isEmpty()) {
062                    String result = d.getScope();
063                    if (d.isOptional()) {
064                        result += ", optional";
065                    }
066                    return "[" + result + "]";
067                }
068            }
069            return null;
070        };
071    }
072    /**
073     * Decorator of "managed version": explains on nodes what was managed.
074     */
075    public static Function<DependencyNode, String> premanagedVersion() {
076        return dependencyNode -> {
077            if (dependencyNode.getArtifact() != null) {
078                String premanagedVersion = DependencyManagerUtils.getPremanagedVersion(dependencyNode);
079                if (premanagedVersion != null
080                        && !premanagedVersion.equals(
081                                dependencyNode.getArtifact().getBaseVersion())) {
082                    return "(version managed from " + premanagedVersion + ")";
083                }
084            }
085            return null;
086        };
087    }
088    /**
089     * Decorator of "managed scope": explains on nodes what was managed.
090     */
091    public static Function<DependencyNode, String> premanagedScope() {
092        return dependencyNode -> {
093            Dependency d = dependencyNode.getDependency();
094            if (d != null) {
095                String premanagedScope = DependencyManagerUtils.getPremanagedScope(dependencyNode);
096                if (premanagedScope != null && !premanagedScope.equals(d.getScope())) {
097                    return "(scope managed from " + premanagedScope + ")";
098                }
099            }
100            return null;
101        };
102    }
103    /**
104     * Decorator of "managed optionality": explains on nodes what was managed.
105     */
106    public static Function<DependencyNode, String> premanagedOptional() {
107        return dependencyNode -> {
108            Dependency d = dependencyNode.getDependency();
109            if (d != null) {
110                Boolean premanagedOptional = DependencyManagerUtils.getPremanagedOptional(dependencyNode);
111                if (premanagedOptional != null && !premanagedOptional.equals(d.getOptional())) {
112                    return "(optionality managed from " + premanagedOptional + ")";
113                }
114            }
115            return null;
116        };
117    }
118    /**
119     * Decorator of "managed exclusions": explains on nodes what was managed.
120     */
121    public static Function<DependencyNode, String> premanagedExclusions() {
122        return dependencyNode -> {
123            Dependency d = dependencyNode.getDependency();
124            if (d != null) {
125                Collection<Exclusion> premanagedExclusions =
126                        DependencyManagerUtils.getPremanagedExclusions(dependencyNode);
127                if (premanagedExclusions != null && !equals(premanagedExclusions, d.getExclusions())) {
128                    return "(exclusions managed from " + premanagedExclusions + ")";
129                }
130            }
131            return null;
132        };
133    }
134    /**
135     * Decorator of "managed properties": explains on nodes what was managed.
136     */
137    public static Function<DependencyNode, String> premanagedProperties() {
138        return dependencyNode -> {
139            if (dependencyNode.getArtifact() != null) {
140                Map<String, String> premanagedProperties =
141                        DependencyManagerUtils.getPremanagedProperties(dependencyNode);
142                if (premanagedProperties != null
143                        && !equals(
144                                premanagedProperties,
145                                dependencyNode.getArtifact().getProperties())) {
146                    return "(properties managed from " + premanagedProperties + ")";
147                }
148            }
149            return null;
150        };
151    }
152    /**
153     * Decorator of "range member": explains on nodes what range it participates in.
154     */
155    public static Function<DependencyNode, String> rangeMember() {
156        return dependencyNode -> {
157            VersionConstraint constraint = dependencyNode.getVersionConstraint();
158            if (constraint != null && constraint.getRange() != null) {
159                return "(range '" + constraint.getRange() + "')";
160            }
161            return null;
162        };
163    }
164    /**
165     * Decorator of "winner node": explains on losers why lost.
166     */
167    public static Function<DependencyNode, String> winnerNode() {
168        return dependencyNode -> {
169            if (dependencyNode.getArtifact() != null) {
170                DependencyNode winner =
171                        (DependencyNode) dependencyNode.getData().get(ConflictResolver.NODE_DATA_WINNER);
172                if (winner != null) {
173                    if (ArtifactIdUtils.equalsId(dependencyNode.getArtifact(), winner.getArtifact())) {
174                        return "(nearer exists)";
175                    } else {
176                        Artifact w = winner.getArtifact();
177                        String result = "conflicts with ";
178                        if (ArtifactIdUtils.toVersionlessId(dependencyNode.getArtifact())
179                                .equals(ArtifactIdUtils.toVersionlessId(w))) {
180                            result += w.getVersion();
181                        } else {
182                            result += w;
183                        }
184                        return "(" + result + ")";
185                    }
186                }
187            }
188            return null;
189        };
190    }
191    /**
192     * Decorator of "artifact properties": prints out asked properties, if present.
193     */
194    public static Function<DependencyNode, String> artifactProperties(Collection<String> properties) {
195        requireNonNull(properties, "properties");
196        return dependencyNode -> {
197            if (!properties.isEmpty() && dependencyNode.getDependency() != null) {
198                String props = properties.stream()
199                        .map(p -> p + "="
200                                + dependencyNode.getDependency().getArtifact().getProperty(p, "n/a"))
201                        .collect(Collectors.joining(","));
202                if (!props.isEmpty()) {
203                    return "(" + props + ")";
204                }
205            }
206            return null;
207        };
208    }
209
210    /**
211     * The standard "default" decorators.
212     *
213     * @since 2.0.0
214     */
215    private static final List<Function<DependencyNode, String>> DEFAULT_DECORATORS =
216            Collections.unmodifiableList(Arrays.asList(
217                    effectiveDependency(),
218                    premanagedVersion(),
219                    premanagedScope(),
220                    premanagedOptional(),
221                    premanagedExclusions(),
222                    premanagedProperties(),
223                    rangeMember(),
224                    winnerNode()));
225
226    /**
227     * Extends {@link #DEFAULT_DECORATORS} decorators with passed in ones.
228     *
229     * @since 2.0.0
230     */
231    public static List<Function<DependencyNode, String>> defaultsWith(
232            Collection<Function<DependencyNode, String>> extras) {
233        requireNonNull(extras, "extras");
234        ArrayList<Function<DependencyNode, String>> result = new ArrayList<>(DEFAULT_DECORATORS);
235        result.addAll(extras);
236        return result;
237    }
238
239    private final Consumer<String> consumer;
240
241    private final List<Function<DependencyNode, String>> decorators;
242
243    private final Deque<DependencyNode> nodes = new ArrayDeque<>();
244
245    /**
246     * Creates instance with given consumer.
247     *
248     * @param consumer The string consumer, must not be {@code null}.
249     */
250    public DependencyGraphDumper(Consumer<String> consumer) {
251        this(consumer, DEFAULT_DECORATORS);
252    }
253
254    /**
255     * Creates instance with given consumer and decorators.
256     *
257     * @param consumer The string consumer, must not be {@code null}.
258     * @param decorators The decorators to apply, must not be {@code null}.
259     * @since 2.0.0
260     */
261    public DependencyGraphDumper(Consumer<String> consumer, Collection<Function<DependencyNode, String>> decorators) {
262        this.consumer = requireNonNull(consumer);
263        this.decorators = new ArrayList<>(decorators);
264    }
265
266    @Override
267    public boolean visitEnter(DependencyNode node) {
268        nodes.push(node);
269        consumer.accept(formatLine(nodes));
270        return true;
271    }
272
273    @Override
274    public boolean visitLeave(DependencyNode node) {
275        if (!nodes.isEmpty()) {
276            nodes.pop();
277        }
278        return true;
279    }
280
281    protected String formatLine(Deque<DependencyNode> nodes) {
282        return formatIndentation(nodes) + formatNode(nodes);
283    }
284
285    protected String formatIndentation(Deque<DependencyNode> nodes) {
286        StringBuilder buffer = new StringBuilder(128);
287        Iterator<DependencyNode> iter = nodes.descendingIterator();
288        DependencyNode parent = iter.hasNext() ? iter.next() : null;
289        DependencyNode child = iter.hasNext() ? iter.next() : null;
290        while (parent != null && child != null) {
291            boolean lastChild = parent.getChildren().get(parent.getChildren().size() - 1) == child;
292            boolean end = child == nodes.peekFirst();
293            String indent;
294            if (end) {
295                indent = lastChild ? "\\- " : "+- ";
296            } else {
297                indent = lastChild ? "   " : "|  ";
298            }
299            buffer.append(indent);
300            parent = child;
301            child = iter.hasNext() ? iter.next() : null;
302        }
303        return buffer.toString();
304    }
305
306    protected String formatNode(Deque<DependencyNode> nodes) {
307        DependencyNode node = requireNonNull(nodes.peek(), "bug: should not happen");
308        StringBuilder buffer = new StringBuilder(128);
309        Artifact a = node.getArtifact();
310        buffer.append(a);
311        for (Function<DependencyNode, String> decorator : decorators) {
312            String decoration = decorator.apply(node);
313            if (decoration != null) {
314                buffer.append(" ").append(decoration);
315            }
316        }
317        return buffer.toString();
318    }
319
320    private static boolean equals(Collection<Exclusion> c1, Collection<Exclusion> c2) {
321        return c1 != null && c2 != null && c1.size() == c2.size() && c1.containsAll(c2);
322    }
323
324    private static boolean equals(Map<String, String> m1, Map<String, String> m2) {
325        return m1 != null
326                && m2 != null
327                && m1.size() == m2.size()
328                && m1.entrySet().stream().allMatch(entry -> Objects.equals(m2.get(entry.getKey()), entry.getValue()));
329    }
330}