blob: 4a312c3c6a6545caef98ce24551b470bd5996173 [file] [log] [blame]
/**
* Copyright (C) 2008 Google Inc.
*
* 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 com.google.inject.grapher.graphviz;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.grapher.AbstractInjectorGrapher;
import com.google.inject.grapher.BindingEdge;
import com.google.inject.grapher.DependencyEdge;
import com.google.inject.grapher.ImplementationNode;
import com.google.inject.grapher.InstanceNode;
import com.google.inject.grapher.InterfaceNode;
import com.google.inject.grapher.NameFactory;
import com.google.inject.grapher.NodeId;
import com.google.inject.spi.InjectionPoint;
import java.io.PrintWriter;
import java.lang.reflect.Member;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* {@link InjectorGrapher} implementation that writes out a Graphviz DOT file of the graph.
* Dependencies are bound in {@link GraphvizModule}.
* <p>
* Specify the {@link PrintWriter} to output to with {@link #setOut(PrintWriter)}.
*
* @author phopkins@gmail.com (Pete Hopkins)
*/
public class GraphvizGrapher extends AbstractInjectorGrapher {
private final Map<NodeId, GraphvizNode> nodes = Maps.newHashMap();
private final List<GraphvizEdge> edges = Lists.newArrayList();
private final NameFactory nameFactory;
private final PortIdFactory portIdFactory;
private PrintWriter out;
private String rankdir = "TB";
@Inject GraphvizGrapher(@Graphviz NameFactory nameFactory,
@Graphviz PortIdFactory portIdFactory) {
this.nameFactory = nameFactory;
this.portIdFactory = portIdFactory;
}
@Override protected void reset() {
nodes.clear();
edges.clear();
}
public void setOut(PrintWriter out) {
this.out = out;
}
public void setRankdir(String rankdir) {
this.rankdir = rankdir;
}
@Override protected void postProcess() {
start();
for (GraphvizNode node : nodes.values()) {
renderNode(node);
}
for (GraphvizEdge edge : edges) {
renderEdge(edge);
}
finish();
out.flush();
}
protected Map<String, String> getGraphAttributes() {
Map<String, String> attrs = Maps.newHashMap();
attrs.put("rankdir", rankdir);
return attrs;
}
protected void start() {
out.println("digraph injector {");
Map<String, String> attrs = getGraphAttributes();
out.println("graph " + getAttrString(attrs) + ";");
}
protected void finish() {
out.println("}");
}
protected void renderNode(GraphvizNode node) {
Map<String, String> attrs = getNodeAttributes(node);
out.println(node.getIdentifier() + " " + getAttrString(attrs));
}
protected Map<String, String> getNodeAttributes(GraphvizNode node) {
Map<String, String> attrs = Maps.newHashMap();
attrs.put("label", getNodeLabel(node));
// remove most of the margin because the table has internal padding
attrs.put("margin", "\"0.02,0\"");
attrs.put("shape", node.getShape().toString());
attrs.put("style", node.getStyle().toString());
return attrs;
}
/**
* Creates the "label" for a node. This is a string of HTML that defines a
* table with a heading at the top and (in the case of
* {@link ImplementationNode}s) rows for each of the member fields.
*/
protected String getNodeLabel(GraphvizNode node) {
String cellborder = node.getStyle() == NodeStyle.INVISIBLE ? "1" : "0";
StringBuilder html = new StringBuilder();
html.append("<");
html.append("<table cellspacing=\"0\" cellpadding=\"5\" cellborder=\"");
html.append(cellborder).append("\" border=\"0\">");
html.append("<tr>").append("<td align=\"left\" port=\"header\" ");
html.append("bgcolor=\"" + node.getHeaderBackgroundColor() + "\">");
String subtitle = Joiner.on("<br align=\"left\"/>").join(node.getSubtitles());
if (subtitle.length() != 0) {
html.append("<font color=\"").append(node.getHeaderTextColor());
html.append("\" point-size=\"10\">");
html.append(subtitle).append("<br align=\"left\"/>").append("</font>");
}
html.append("<font color=\"" + node.getHeaderTextColor() + "\">");
html.append(htmlEscape(node.getTitle())).append("<br align=\"left\"/>");
html.append("</font>").append("</td>").append("</tr>");
for (Map.Entry<String, String> field : node.getFields().entrySet()) {
html.append("<tr>");
html.append("<td align=\"left\" port=\"").append(htmlEscape(field.getKey())).append("\">");
html.append(htmlEscape(field.getValue()));
html.append("</td>").append("</tr>");
}
html.append("</table>");
html.append(">");
return html.toString();
}
protected void renderEdge(GraphvizEdge edge) {
Map<String, String> attrs = getEdgeAttributes(edge);
String tailId = getEdgeEndPoint(nodes.get(edge.getTailNodeId()).getIdentifier(),
edge.getTailPortId(), edge.getTailCompassPoint());
String headId = getEdgeEndPoint(nodes.get(edge.getHeadNodeId()).getIdentifier(),
edge.getHeadPortId(), edge.getHeadCompassPoint());
out.println(tailId + " -> " + headId + " " + getAttrString(attrs));
}
protected Map<String, String> getEdgeAttributes(GraphvizEdge edge) {
Map<String, String> attrs = Maps.newHashMap();
attrs.put("arrowhead", getArrowString(edge.getArrowHead()));
attrs.put("arrowtail", getArrowString(edge.getArrowTail()));
attrs.put("style", edge.getStyle().toString());
return attrs;
}
private String getAttrString(Map<String, String> attrs) {
List<String> attrList = Lists.newArrayList();
for (Entry<String, String> attr : attrs.entrySet()) {
String value = attr.getValue();
if (value != null) {
attrList.add(attr.getKey() + "=" + value);
}
}
return "[" + Joiner.on(", ").join(attrList) + "]";
}
/**
* Turns a {@link List} of {@link ArrowType}s into a {@link String} that
* represents combining them. With Graphviz, that just means concatenating
* them.
*/
protected String getArrowString(List<ArrowType> arrows) {
return Joiner.on("").join(arrows);
}
protected String getEdgeEndPoint(String nodeId, String portId, CompassPoint compassPoint) {
List<String> portStrings = Lists.newArrayList(nodeId);
if (portId != null) {
portStrings.add(portId);
}
if (compassPoint != null) {
portStrings.add(compassPoint.toString());
}
return Joiner.on(":").join(portStrings);
}
protected String htmlEscape(String str) {
return str.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
protected List<String> htmlEscape(List<String> elements) {
List<String> escaped = Lists.newArrayList();
for (String element : elements) {
escaped.add(htmlEscape(element));
}
return escaped;
}
@Override protected void newInterfaceNode(InterfaceNode node) {
// TODO(phopkins): Show the Module on the graph, which comes from the
// class name when source is a StackTraceElement.
NodeId nodeId = node.getId();
GraphvizNode gnode = new GraphvizNode(nodeId);
gnode.setStyle(NodeStyle.DASHED);
Key<?> key = nodeId.getKey();
gnode.setTitle(nameFactory.getClassName(key));
gnode.addSubtitle(0, nameFactory.getAnnotationName(key));
addNode(gnode);
}
@Override protected void newImplementationNode(ImplementationNode node) {
NodeId nodeId = node.getId();
GraphvizNode gnode = new GraphvizNode(nodeId);
gnode.setStyle(NodeStyle.SOLID);
gnode.setHeaderBackgroundColor("#000000");
gnode.setHeaderTextColor("#ffffff");
gnode.setTitle(nameFactory.getClassName(nodeId.getKey()));
for (Member member : node.getMembers()) {
gnode.addField(portIdFactory.getPortId(member), nameFactory.getMemberName(member));
}
addNode(gnode);
}
@Override protected void newInstanceNode(InstanceNode node) {
NodeId nodeId = node.getId();
GraphvizNode gnode = new GraphvizNode(nodeId);
gnode.setStyle(NodeStyle.SOLID);
gnode.setHeaderBackgroundColor("#000000");
gnode.setHeaderTextColor("#ffffff");
gnode.setTitle(nameFactory.getClassName(nodeId.getKey()));
gnode.addSubtitle(0, nameFactory.getSourceName(node.getSource()));
gnode.setHeaderBackgroundColor("#aaaaaa");
gnode.setHeaderTextColor("#ffffff");
gnode.setTitle(nameFactory.getInstanceName(node.getInstance()));
for (Member member : node.getMembers()) {
gnode.addField(portIdFactory.getPortId(member), nameFactory.getMemberName(member));
}
addNode(gnode);
}
@Override protected void newDependencyEdge(DependencyEdge edge) {
GraphvizEdge gedge = new GraphvizEdge(edge.getFromId(), edge.getToId());
InjectionPoint fromPoint = edge.getInjectionPoint();
if (fromPoint == null) {
gedge.setTailPortId("header");
} else {
gedge.setTailPortId(portIdFactory.getPortId(fromPoint.getMember()));
}
gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL));
gedge.setTailCompassPoint(CompassPoint.EAST);
edges.add(gedge);
}
@Override protected void newBindingEdge(BindingEdge edge) {
GraphvizEdge gedge = new GraphvizEdge(edge.getFromId(), edge.getToId());
gedge.setStyle(EdgeStyle.DASHED);
switch (edge.getType()) {
case NORMAL:
gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL_OPEN));
break;
case PROVIDER:
gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL_OPEN, ArrowType.NORMAL_OPEN));
break;
case CONVERTED_CONSTANT:
gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL_OPEN, ArrowType.DOT_OPEN));
break;
}
edges.add(gedge);
}
private void addNode(GraphvizNode node) {
node.setIdentifier("x" + nodes.size());
nodes.put(node.getNodeId(), node);
}
}