/*
* 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}.
*
*
For each component, writes a DOT file
* 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.
*
*
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 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 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> extends Indented {
private final String base;
private final Map 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(Object nodeName) {
super(quote(nodeName.toString()));
}
}
static class DotEdge extends DotStatement {
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 nodeIds = new HashMap<>();
NodesGraph(BindingGraph bindingGraph) {
this.bindingGraph = bindingGraph;
}
DotGraph graph() {
if (nodeIds.isEmpty()) {
Iterator 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(Edge edge) {
EndpointPair 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;
}
}
}