[MRESOLVER-535][MRESOLVER-538] Add decorator ability to graph dumper; show ranges (#464)

Ability to pass in any function that is able to "decorate" the print out of the graph. This should have a follow up Pr that enable/disable (or simply move all into decorators). Also, verbose tree should show use of version ranges, for cases when someone wants to detect them in whole transitive hull.

---

https://issues.apache.org/jira/browse/MRESOLVER-535
https://issues.apache.org/jira/browse/MRESOLVER-538
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java
index 6b39fc3..28f6c67 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java
@@ -18,7 +18,7 @@
  */
 package org.apache.maven.resolver.examples;
 
-import java.util.Arrays;
+import java.util.Collections;
 
 import org.apache.maven.resolver.examples.util.Booter;
 import org.eclipse.aether.RepositorySystem;
@@ -34,6 +34,9 @@
 import org.eclipse.aether.util.graph.transformer.ConflictResolver;
 import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper;
 
+import static org.eclipse.aether.util.graph.visitor.DependencyGraphDumper.artifactProperties;
+import static org.eclipse.aether.util.graph.visitor.DependencyGraphDumper.defaultsWith;
+
 /**
  * Visualizes the transitive dependencies of an artifact similar to m2e's dependency hierarchy view.
  */
@@ -68,7 +71,12 @@
 
                 CollectResult collectResult = system.collectDependencies(session, collectRequest);
 
-                collectResult.getRoot().accept(new DependencyGraphDumper(System.out::println, Arrays.asList("color")));
+                collectResult
+                        .getRoot()
+                        .accept(new DependencyGraphDumper(
+                                System.out::println,
+                                defaultsWith(
+                                        Collections.singleton(artifactProperties(Collections.singleton("color"))))));
             }
         }
     }
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java
index 3d3d15b..2f37957 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java
@@ -20,13 +20,16 @@
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import org.eclipse.aether.artifact.Artifact;
@@ -37,6 +40,7 @@
 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
 import org.eclipse.aether.util.graph.transformer.ConflictResolver;
+import org.eclipse.aether.version.VersionConstraint;
 
 import static java.util.Objects.requireNonNull;
 
@@ -47,28 +51,216 @@
  * @since 1.9.8
  */
 public class DependencyGraphDumper implements DependencyVisitor {
+    /**
+     * Decorator of "effective dependency": shows effective scope and optionality.
+     */
+    public static Function<DependencyNode, String> effectiveDependency() {
+        return dependencyNode -> {
+            Dependency d = dependencyNode.getDependency();
+            if (d != null) {
+                if (!d.getScope().isEmpty()) {
+                    String result = d.getScope();
+                    if (d.isOptional()) {
+                        result += ", optional";
+                    }
+                    return "[" + result + "]";
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "managed version": explains on nodes what was managed.
+     */
+    public static Function<DependencyNode, String> premanagedVersion() {
+        return dependencyNode -> {
+            if (dependencyNode.getArtifact() != null) {
+                String premanagedVersion = DependencyManagerUtils.getPremanagedVersion(dependencyNode);
+                if (premanagedVersion != null
+                        && !premanagedVersion.equals(
+                                dependencyNode.getArtifact().getBaseVersion())) {
+                    return "(version managed from " + premanagedVersion + ")";
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "managed scope": explains on nodes what was managed.
+     */
+    public static Function<DependencyNode, String> premanagedScope() {
+        return dependencyNode -> {
+            Dependency d = dependencyNode.getDependency();
+            if (d != null) {
+                String premanagedScope = DependencyManagerUtils.getPremanagedScope(dependencyNode);
+                if (premanagedScope != null && !premanagedScope.equals(d.getScope())) {
+                    return "(scope managed from " + premanagedScope + ")";
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "managed optionality": explains on nodes what was managed.
+     */
+    public static Function<DependencyNode, String> premanagedOptional() {
+        return dependencyNode -> {
+            Dependency d = dependencyNode.getDependency();
+            if (d != null) {
+                Boolean premanagedOptional = DependencyManagerUtils.getPremanagedOptional(dependencyNode);
+                if (premanagedOptional != null && !premanagedOptional.equals(d.getOptional())) {
+                    return "(optionality managed from " + premanagedOptional + ")";
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "managed exclusions": explains on nodes what was managed.
+     */
+    public static Function<DependencyNode, String> premanagedExclusions() {
+        return dependencyNode -> {
+            Dependency d = dependencyNode.getDependency();
+            if (d != null) {
+                Collection<Exclusion> premanagedExclusions =
+                        DependencyManagerUtils.getPremanagedExclusions(dependencyNode);
+                if (premanagedExclusions != null && !equals(premanagedExclusions, d.getExclusions())) {
+                    return "(exclusions managed from " + premanagedExclusions + ")";
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "managed properties": explains on nodes what was managed.
+     */
+    public static Function<DependencyNode, String> premanagedProperties() {
+        return dependencyNode -> {
+            if (dependencyNode.getArtifact() != null) {
+                Map<String, String> premanagedProperties =
+                        DependencyManagerUtils.getPremanagedProperties(dependencyNode);
+                if (premanagedProperties != null
+                        && !equals(
+                                premanagedProperties,
+                                dependencyNode.getArtifact().getProperties())) {
+                    return "(properties managed from " + premanagedProperties + ")";
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "range member": explains on nodes what range it participates in.
+     */
+    public static Function<DependencyNode, String> rangeMember() {
+        return dependencyNode -> {
+            VersionConstraint constraint = dependencyNode.getVersionConstraint();
+            if (constraint != null && constraint.getRange() != null) {
+                return "(range '" + constraint.getRange() + "')";
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "winner node": explains on losers why lost.
+     */
+    public static Function<DependencyNode, String> winnerNode() {
+        return dependencyNode -> {
+            if (dependencyNode.getArtifact() != null) {
+                DependencyNode winner =
+                        (DependencyNode) dependencyNode.getData().get(ConflictResolver.NODE_DATA_WINNER);
+                if (winner != null) {
+                    if (ArtifactIdUtils.equalsId(dependencyNode.getArtifact(), winner.getArtifact())) {
+                        return "(nearer exists)";
+                    } else {
+                        Artifact w = winner.getArtifact();
+                        String result = "conflicts with ";
+                        if (ArtifactIdUtils.toVersionlessId(dependencyNode.getArtifact())
+                                .equals(ArtifactIdUtils.toVersionlessId(w))) {
+                            result += w.getVersion();
+                        } else {
+                            result += w;
+                        }
+                        return "(" + result + ")";
+                    }
+                }
+            }
+            return null;
+        };
+    }
+    /**
+     * Decorator of "artifact properties": prints out asked properties, if present.
+     */
+    public static Function<DependencyNode, String> artifactProperties(Collection<String> properties) {
+        requireNonNull(properties, "properties");
+        return dependencyNode -> {
+            if (!properties.isEmpty() && dependencyNode.getDependency() != null) {
+                String props = properties.stream()
+                        .map(p -> p + "="
+                                + dependencyNode.getDependency().getArtifact().getProperty(p, "n/a"))
+                        .collect(Collectors.joining(","));
+                if (!props.isEmpty()) {
+                    return "(" + props + ")";
+                }
+            }
+            return null;
+        };
+    }
+
+    /**
+     * The standard "default" decorators.
+     *
+     * @since 2.0.0
+     */
+    private static final List<Function<DependencyNode, String>> DEFAULT_DECORATORS =
+            Collections.unmodifiableList(Arrays.asList(
+                    effectiveDependency(),
+                    premanagedVersion(),
+                    premanagedScope(),
+                    premanagedOptional(),
+                    premanagedExclusions(),
+                    premanagedProperties(),
+                    rangeMember(),
+                    winnerNode()));
+
+    /**
+     * Extends {@link #DEFAULT_DECORATORS} decorators with passed in ones.
+     *
+     * @since 2.0.0
+     */
+    public static List<Function<DependencyNode, String>> defaultsWith(
+            Collection<Function<DependencyNode, String>> extras) {
+        requireNonNull(extras, "extras");
+        ArrayList<Function<DependencyNode, String>> result = new ArrayList<>(DEFAULT_DECORATORS);
+        result.addAll(extras);
+        return result;
+    }
 
     private final Consumer<String> consumer;
 
-    private final Collection<String> properties;
+    private final List<Function<DependencyNode, String>> decorators;
 
     private final Deque<DependencyNode> nodes = new ArrayDeque<>();
 
     /**
      * Creates instance with given consumer.
+     *
+     * @param consumer The string consumer, must not be {@code null}.
      */
     public DependencyGraphDumper(Consumer<String> consumer) {
-        this(consumer, Collections.emptyList());
+        this(consumer, DEFAULT_DECORATORS);
     }
 
     /**
-     * Creates instance with given consumer and properties (to print out).
+     * Creates instance with given consumer and decorators.
      *
+     * @param consumer The string consumer, must not be {@code null}.
+     * @param decorators The decorators to apply, must not be {@code null}.
      * @since 2.0.0
      */
-    public DependencyGraphDumper(Consumer<String> consumer, Collection<String> properties) {
+    public DependencyGraphDumper(Consumer<String> consumer, Collection<Function<DependencyNode, String>> decorators) {
         this.consumer = requireNonNull(consumer);
-        this.properties = new ArrayList<>(properties);
+        this.decorators = new ArrayList<>(decorators);
     }
 
     @Override
@@ -116,77 +308,20 @@
         StringBuilder buffer = new StringBuilder(128);
         Artifact a = node.getArtifact();
         buffer.append(a);
-        Dependency d = node.getDependency();
-        if (d != null && !d.getScope().isEmpty()) {
-            buffer.append(" [").append(d.getScope());
-            if (d.isOptional()) {
-                buffer.append(", optional");
-            }
-            buffer.append("]");
-        }
-        String premanaged = DependencyManagerUtils.getPremanagedVersion(node);
-        if (premanaged != null && !premanaged.equals(a.getBaseVersion())) {
-            buffer.append(" (version managed from ").append(premanaged).append(")");
-        }
-
-        premanaged = DependencyManagerUtils.getPremanagedScope(node);
-        if (premanaged != null && d != null && !premanaged.equals(d.getScope())) {
-            buffer.append(" (scope managed from ").append(premanaged).append(")");
-        }
-
-        Boolean premanagedOptional = DependencyManagerUtils.getPremanagedOptional(node);
-        if (premanagedOptional != null && d != null && !premanagedOptional.equals(d.getOptional())) {
-            buffer.append(" (optionality managed from ")
-                    .append(premanagedOptional)
-                    .append(")");
-        }
-
-        Collection<Exclusion> premanagedExclusions = DependencyManagerUtils.getPremanagedExclusions(node);
-        if (premanagedExclusions != null && d != null && !equals(premanagedExclusions, d.getExclusions())) {
-            buffer.append(" (exclusions managed from ")
-                    .append(premanagedExclusions)
-                    .append(")");
-        }
-
-        Map<String, String> premanagedProperties = DependencyManagerUtils.getPremanagedProperties(node);
-        if (premanagedProperties != null && !equals(premanagedProperties, a.getProperties())) {
-            buffer.append(" (properties managed from ")
-                    .append(premanagedProperties)
-                    .append(")");
-        }
-
-        DependencyNode winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER);
-        if (winner != null) {
-            if (ArtifactIdUtils.equalsId(a, winner.getArtifact())) {
-                buffer.append(" (nearer exists)");
-            } else {
-                Artifact w = winner.getArtifact();
-                buffer.append(" (conflicts with ");
-                if (ArtifactIdUtils.toVersionlessId(a).equals(ArtifactIdUtils.toVersionlessId(w))) {
-                    buffer.append(w.getVersion());
-                } else {
-                    buffer.append(w);
-                }
-                buffer.append(")");
-            }
-        }
-
-        if (!properties.isEmpty() && node.getDependency() != null) {
-            String props = properties.stream()
-                    .map(p -> p + "=" + node.getDependency().getArtifact().getProperty(p, "n/a"))
-                    .collect(Collectors.joining(","));
-            if (!props.isEmpty()) {
-                buffer.append(" (").append(props).append(")");
+        for (Function<DependencyNode, String> decorator : decorators) {
+            String decoration = decorator.apply(node);
+            if (decoration != null) {
+                buffer.append(" ").append(decoration);
             }
         }
         return buffer.toString();
     }
 
-    private boolean equals(Collection<Exclusion> c1, Collection<Exclusion> c2) {
+    private static boolean equals(Collection<Exclusion> c1, Collection<Exclusion> c2) {
         return c1 != null && c2 != null && c1.size() == c2.size() && c1.containsAll(c2);
     }
 
-    private boolean equals(Map<String, String> m1, Map<String, String> m2) {
+    private static boolean equals(Map<String, String> m1, Map<String, String> m2) {
         return m1 != null
                 && m2 != null
                 && m1.size() == m2.size()