| /* |
| * Copyright (C) 2017 The Dagger Authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package dagger.example.spi; |
| |
| import static java.util.UUID.randomUUID; |
| import static java.util.regex.Matcher.quoteReplacement; |
| import static java.util.stream.Collectors.groupingBy; |
| |
| import com.google.auto.service.AutoService; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterators; |
| import com.google.common.graph.EndpointPair; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import com.squareup.javapoet.ClassName; |
| import dagger.model.Binding; |
| import dagger.model.BindingGraph; |
| import dagger.model.BindingGraph.ChildFactoryMethodEdge; |
| import dagger.model.BindingGraph.DependencyEdge; |
| import dagger.model.BindingGraph.Edge; |
| import dagger.model.BindingGraph.MaybeBinding; |
| import dagger.model.BindingGraph.MissingBinding; |
| import dagger.model.BindingGraph.Node; |
| import dagger.model.BindingGraph.SubcomponentCreatorBindingEdge; |
| import dagger.model.BindingKind; |
| import dagger.model.ComponentPath; |
| import dagger.spi.BindingGraphPlugin; |
| import dagger.spi.DiagnosticReporter; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.UUID; |
| import java.util.stream.Collectors; |
| import javax.annotation.processing.Filer; |
| import javax.lang.model.element.TypeElement; |
| import javax.tools.FileObject; |
| import javax.tools.StandardLocation; |
| |
| /** |
| * Experimental visualizer used as a proof-of-concept for {@link BindingGraphPlugin}. |
| * |
| * <p>For each component, writes a <a href=http://www.graphviz.org/content/dot-language>DOT file</a> |
| * in the same package. The file name is the name of the component type (with enclosing type names, |
| * joined by underscores, preceding it), with a {@code .dot} extension. |
| * |
| * <p>For example, for a nested component type {@code Foo.Bar} this will generate a file {@code |
| * Foo_Bar.dot}. |
| */ |
| @AutoService(BindingGraphPlugin.class) |
| public final class BindingGraphVisualizer implements BindingGraphPlugin { |
| private Filer filer; |
| |
| @Override |
| public void initFiler(Filer filer) { |
| this.filer = filer; |
| } |
| |
| /** Graphviz color names to use for binding nodes within each component. */ |
| private static final ImmutableList<String> COMPONENT_COLORS = |
| ImmutableList.of( |
| "/set312/1", |
| "/set312/2", |
| "/set312/3", |
| "/set312/4", |
| "/set312/5", |
| "/set312/6", |
| "/set312/7", |
| "/set312/8", |
| "/set312/9", |
| "/set312/10", |
| "/set312/11", |
| "/set312/12"); |
| |
| @Override |
| public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { |
| TypeElement componentElement = |
| bindingGraph.rootComponentNode().componentPath().currentComponent(); |
| DotGraph graph = new NodesGraph(bindingGraph).graph(); |
| ClassName componentName = ClassName.get(componentElement); |
| try { |
| FileObject file = |
| filer |
| .createResource( |
| StandardLocation.CLASS_OUTPUT, |
| componentName.packageName(), |
| Joiner.on('_').join(componentName.simpleNames()) + ".dot", |
| componentElement); |
| try (PrintWriter writer = new PrintWriter(file.openWriter())) { |
| graph.write(0, writer); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private abstract static class Indented { |
| |
| abstract void write(int level, PrintWriter writer); |
| |
| @CanIgnoreReturnValue |
| PrintWriter indent(int level, PrintWriter writer) { |
| writer.print(Strings.repeat(" ", level * 2)); |
| return writer; |
| } |
| } |
| |
| static class DotGraph extends Indented { |
| private final String header; |
| private final List<Indented> elements = new ArrayList<>(); |
| |
| DotGraph(String header) { |
| this.header = header; |
| } |
| |
| @CanIgnoreReturnValue |
| DotGraph add(Indented element) { |
| elements.add(element); |
| return this; |
| } |
| |
| @Override |
| void write(int level, PrintWriter writer) { |
| indent(level, writer); |
| writer.println(header + " {"); |
| for (Indented element : elements) { |
| element.write(level + 1, writer); |
| } |
| indent(level, writer); |
| writer.println("}"); |
| } |
| } |
| |
| static class DotStatement<S extends DotStatement<S>> extends Indented { |
| private final String base; |
| private final Map<String, Object> attributes = new LinkedHashMap<>(); |
| |
| DotStatement(String base) { |
| this.base = base; |
| } |
| |
| @SuppressWarnings("unchecked") |
| @CanIgnoreReturnValue |
| S addAttribute(String name, Object value) { |
| attributes.put(name, value); |
| return (S) this; |
| } |
| |
| @CanIgnoreReturnValue |
| S addAttributeFormat(String name, String format, Object... args) { |
| return addAttribute(name, String.format(format, args)); |
| } |
| |
| @Override |
| void write(int level, PrintWriter writer) { |
| indent(level, writer); |
| writer.print(base); |
| if (!attributes.isEmpty()) { |
| writer.print( |
| attributes |
| .entrySet() |
| .stream() |
| .map( |
| entry -> |
| String.format("%s=%s", entry.getKey(), quote(entry.getValue().toString()))) |
| .collect(Collectors.joining(", ", " [", "]"))); |
| } |
| writer.println(); |
| } |
| } |
| |
| private static String quote(String string) { |
| return '"' + string.replaceAll("\"", quoteReplacement("\\\"")) + '"'; |
| } |
| |
| static class DotNode extends DotStatement<DotNode> { |
| DotNode(Object nodeName) { |
| super(quote(nodeName.toString())); |
| } |
| } |
| |
| static class DotEdge extends DotStatement<DotEdge> { |
| DotEdge(Object leftNode, Object rightNode) { |
| super(quote(leftNode.toString()) + " -> " + quote(rightNode.toString())); |
| } |
| } |
| |
| static class NodesGraph { |
| private final DotGraph graph = |
| new DotGraph("digraph") |
| .add( |
| new DotStatement<>("graph") |
| .addAttribute("rankdir", "LR") |
| .addAttribute("labeljust", "l") |
| .addAttribute("compound", true)); |
| |
| private final BindingGraph bindingGraph; |
| private final Map<Node, UUID> nodeIds = new HashMap<>(); |
| |
| NodesGraph(BindingGraph bindingGraph) { |
| this.bindingGraph = bindingGraph; |
| } |
| |
| DotGraph graph() { |
| if (nodeIds.isEmpty()) { |
| Iterator<String> colors = Iterators.cycle(COMPONENT_COLORS); |
| bindingGraph.network().nodes().stream() |
| .collect(groupingBy(Node::componentPath)) |
| .forEach( |
| (component, networkNodes) -> { |
| DotGraph subgraph = subgraph(component); |
| subgraph.add( |
| new DotStatement<>("node") |
| .addAttribute("style", "filled") |
| .addAttribute("shape", "box") |
| .addAttribute("fillcolor", colors.next())); |
| subgraph.add(new DotStatement<>("graph").addAttribute("label", component)); |
| for (Node node : networkNodes) { |
| subgraph.add(dotNode(node)); |
| } |
| }); |
| for (Edge edge : bindingGraph.network().edges()) { |
| dotEdge(edge).ifPresent(graph::add); |
| } |
| } |
| return graph; |
| } |
| |
| DotGraph subgraph(ComponentPath component) { |
| DotGraph subgraph = new DotGraph("subgraph " + quote(clusterName(component))); |
| graph.add(subgraph); |
| return subgraph; |
| } |
| |
| UUID nodeId(Node node) { |
| return nodeIds.computeIfAbsent(node, n -> randomUUID()); |
| } |
| |
| Optional<DotEdge> dotEdge(Edge edge) { |
| EndpointPair<Node> incidentNodes = bindingGraph.network().incidentNodes(edge); |
| DotEdge dotEdge = new DotEdge(nodeId(incidentNodes.source()), nodeId(incidentNodes.target())); |
| if (edge instanceof DependencyEdge) { |
| if (((DependencyEdge) edge).isEntryPoint()) { |
| return Optional.empty(); |
| } |
| } else if (edge instanceof ChildFactoryMethodEdge) { |
| dotEdge.addAttribute("style", "dashed"); |
| dotEdge.addAttribute("lhead", clusterName(incidentNodes.target().componentPath())); |
| dotEdge.addAttribute("ltail", clusterName(incidentNodes.source().componentPath())); |
| dotEdge.addAttribute("taillabel", ((ChildFactoryMethodEdge) edge).factoryMethod()); |
| } else if (edge instanceof SubcomponentCreatorBindingEdge) { |
| dotEdge.addAttribute("style", "dashed"); |
| dotEdge.addAttribute("lhead", clusterName(incidentNodes.target().componentPath())); |
| dotEdge.addAttribute("taillabel", "subcomponent"); |
| } |
| return Optional.of(dotEdge); |
| } |
| |
| DotNode dotNode(Node node) { |
| DotNode dotNode = new DotNode(nodeId(node)); |
| if (node instanceof MaybeBinding) { |
| dotNode.addAttribute("tooltip", ""); |
| if (bindingGraph.entryPointBindings().contains(node)) { |
| dotNode.addAttribute("penwidth", 3); |
| } |
| if (node instanceof Binding) { |
| dotNode.addAttribute("label", label((Binding) node)); |
| } |
| if (node instanceof MissingBinding) { |
| dotNode.addAttributeFormat( |
| "label", "missing binding for %s", ((MissingBinding) node).key()); |
| } |
| } else { |
| dotNode.addAttribute("style", "invis").addAttribute("shape", "point"); |
| } |
| return dotNode; |
| } |
| |
| private String label(Binding binding) { |
| if (binding.kind().equals(BindingKind.MEMBERS_INJECTION)) { |
| return String.format("inject(%s)", binding.key()); |
| } else if (binding.isProduction()) { |
| return String.format("@Produces %s", binding.key()); |
| } else { |
| return binding.key().toString(); |
| } |
| } |
| |
| private static String clusterName(ComponentPath owningComponentPath) { |
| return "cluster" + owningComponentPath; |
| } |
| } |
| } |