View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.util.graph.visitor;
20  
21  import java.util.ArrayDeque;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.Deque;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.function.Consumer;
32  import java.util.function.Function;
33  import java.util.stream.Collectors;
34  
35  import org.eclipse.aether.artifact.Artifact;
36  import org.eclipse.aether.graph.Dependency;
37  import org.eclipse.aether.graph.DependencyNode;
38  import org.eclipse.aether.graph.DependencyVisitor;
39  import org.eclipse.aether.graph.Exclusion;
40  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
41  import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
42  import org.eclipse.aether.util.graph.transformer.ConflictResolver;
43  import org.eclipse.aether.version.VersionConstraint;
44  
45  import static java.util.Objects.requireNonNull;
46  
47  /**
48   * A dependency visitor that dumps the graph to any {@link Consumer}{@code <String>}. Meant for diagnostic and testing, as
49   * it may output the graph to standard output, error or even some logging interface.
50   *
51   * @since 1.9.8
52   */
53  public class DependencyGraphDumper implements DependencyVisitor {
54      /**
55       * Decorator of "effective dependency": shows effective scope and optionality.
56       */
57      public static Function<DependencyNode, String> effectiveDependency() {
58          return dependencyNode -> {
59              Dependency d = dependencyNode.getDependency();
60              if (d != null) {
61                  if (!d.getScope().isEmpty()) {
62                      String result = d.getScope();
63                      if (d.isOptional()) {
64                          result += ", optional";
65                      }
66                      return "[" + result + "]";
67                  }
68              }
69              return null;
70          };
71      }
72      /**
73       * Decorator of "managed version": explains on nodes what was managed.
74       */
75      public static Function<DependencyNode, String> premanagedVersion() {
76          return dependencyNode -> {
77              if (dependencyNode.getArtifact() != null) {
78                  String premanagedVersion = DependencyManagerUtils.getPremanagedVersion(dependencyNode);
79                  if (premanagedVersion != null
80                          && !premanagedVersion.equals(
81                                  dependencyNode.getArtifact().getBaseVersion())) {
82                      return "(version managed from " + premanagedVersion + ")";
83                  }
84              }
85              return null;
86          };
87      }
88      /**
89       * Decorator of "managed scope": explains on nodes what was managed.
90       */
91      public static Function<DependencyNode, String> premanagedScope() {
92          return dependencyNode -> {
93              Dependency d = dependencyNode.getDependency();
94              if (d != null) {
95                  String premanagedScope = DependencyManagerUtils.getPremanagedScope(dependencyNode);
96                  if (premanagedScope != null && !premanagedScope.equals(d.getScope())) {
97                      return "(scope managed from " + premanagedScope + ")";
98                  }
99              }
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 }