1 /*
2  * Copyright (C) 2008 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.inject.grapher.graphviz;
18 
19 import com.google.common.base.Joiner;
20 import com.google.common.collect.ImmutableList;
21 import com.google.common.collect.Lists;
22 import com.google.common.collect.Maps;
23 import com.google.inject.Inject;
24 import com.google.inject.Key;
25 import com.google.inject.grapher.AbstractInjectorGrapher;
26 import com.google.inject.grapher.BindingEdge;
27 import com.google.inject.grapher.DependencyEdge;
28 import com.google.inject.grapher.ImplementationNode;
29 import com.google.inject.grapher.InstanceNode;
30 import com.google.inject.grapher.InterfaceNode;
31 import com.google.inject.grapher.NameFactory;
32 import com.google.inject.grapher.NodeId;
33 import com.google.inject.spi.InjectionPoint;
34 import java.io.PrintWriter;
35 import java.lang.reflect.Member;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 
40 /**
41  * {@link com.google.inject.grapher.InjectorGrapher} implementation that writes out a Graphviz DOT
42  * file of the graph. Dependencies are bound in {@link GraphvizModule}.
43  *
44  * <p>Specify the {@link PrintWriter} to output to with {@link #setOut(PrintWriter)}.
45  *
46  * @author phopkins@gmail.com (Pete Hopkins)
47  * @since 4.0
48  */
49 public class GraphvizGrapher extends AbstractInjectorGrapher {
50   private final Map<NodeId, GraphvizNode> nodes = Maps.newHashMap();
51   private final List<GraphvizEdge> edges = Lists.newArrayList();
52   private final NameFactory nameFactory;
53   private final PortIdFactory portIdFactory;
54 
55   private PrintWriter out;
56   private String rankdir = "TB";
57 
58   @Inject
GraphvizGrapher(@raphviz NameFactory nameFactory, @Graphviz PortIdFactory portIdFactory)59   GraphvizGrapher(@Graphviz NameFactory nameFactory, @Graphviz PortIdFactory portIdFactory) {
60     this.nameFactory = nameFactory;
61     this.portIdFactory = portIdFactory;
62   }
63 
64   @Override
reset()65   protected void reset() {
66     nodes.clear();
67     edges.clear();
68   }
69 
setOut(PrintWriter out)70   public void setOut(PrintWriter out) {
71     this.out = out;
72   }
73 
setRankdir(String rankdir)74   public void setRankdir(String rankdir) {
75     this.rankdir = rankdir;
76   }
77 
78   @Override
postProcess()79   protected void postProcess() {
80     start();
81 
82     for (GraphvizNode node : nodes.values()) {
83       renderNode(node);
84     }
85 
86     for (GraphvizEdge edge : edges) {
87       renderEdge(edge);
88     }
89 
90     finish();
91 
92     out.flush();
93   }
94 
getGraphAttributes()95   protected Map<String, String> getGraphAttributes() {
96     Map<String, String> attrs = Maps.newHashMap();
97     attrs.put("rankdir", rankdir);
98     return attrs;
99   }
100 
start()101   protected void start() {
102     out.println("digraph injector {");
103 
104     Map<String, String> attrs = getGraphAttributes();
105     out.println("graph " + getAttrString(attrs) + ";");
106   }
107 
finish()108   protected void finish() {
109     out.println("}");
110   }
111 
renderNode(GraphvizNode node)112   protected void renderNode(GraphvizNode node) {
113     Map<String, String> attrs = getNodeAttributes(node);
114     out.println(node.getIdentifier() + " " + getAttrString(attrs));
115   }
116 
getNodeAttributes(GraphvizNode node)117   protected Map<String, String> getNodeAttributes(GraphvizNode node) {
118     Map<String, String> attrs = Maps.newHashMap();
119 
120     attrs.put("label", getNodeLabel(node));
121     // remove most of the margin because the table has internal padding
122     attrs.put("margin", "\"0.02,0\"");
123     attrs.put("shape", node.getShape().toString());
124     attrs.put("style", node.getStyle().toString());
125 
126     return attrs;
127   }
128 
129   /**
130    * Creates the "label" for a node. This is a string of HTML that defines a table with a heading at
131    * the top and (in the case of {@link ImplementationNode}s) rows for each of the member fields.
132    */
getNodeLabel(GraphvizNode node)133   protected String getNodeLabel(GraphvizNode node) {
134     String cellborder = node.getStyle() == NodeStyle.INVISIBLE ? "1" : "0";
135 
136     StringBuilder html = new StringBuilder();
137     html.append("<");
138     html.append("<table cellspacing=\"0\" cellpadding=\"5\" cellborder=\"");
139     html.append(cellborder).append("\" border=\"0\">");
140 
141     html.append("<tr>").append("<td align=\"left\" port=\"header\" ");
142     html.append("bgcolor=\"" + node.getHeaderBackgroundColor() + "\">");
143 
144     String subtitle = Joiner.on("<br align=\"left\"/>").join(node.getSubtitles());
145     if (subtitle.length() != 0) {
146       html.append("<font color=\"").append(node.getHeaderTextColor());
147       html.append("\" point-size=\"10\">");
148       html.append(subtitle).append("<br align=\"left\"/>").append("</font>");
149     }
150 
151     html.append("<font color=\"" + node.getHeaderTextColor() + "\">");
152     html.append(htmlEscape(node.getTitle())).append("<br align=\"left\"/>");
153     html.append("</font>").append("</td>").append("</tr>");
154 
155     for (Map.Entry<String, String> field : node.getFields().entrySet()) {
156       html.append("<tr>");
157       html.append("<td align=\"left\" port=\"").append(htmlEscape(field.getKey())).append("\">");
158       html.append(htmlEscape(field.getValue()));
159       html.append("</td>").append("</tr>");
160     }
161 
162     html.append("</table>");
163     html.append(">");
164     return html.toString();
165   }
166 
renderEdge(GraphvizEdge edge)167   protected void renderEdge(GraphvizEdge edge) {
168     Map<String, String> attrs = getEdgeAttributes(edge);
169 
170     String tailId =
171         getEdgeEndPoint(
172             nodes.get(edge.getTailNodeId()).getIdentifier(),
173             edge.getTailPortId(),
174             edge.getTailCompassPoint());
175 
176     String headId =
177         getEdgeEndPoint(
178             nodes.get(edge.getHeadNodeId()).getIdentifier(),
179             edge.getHeadPortId(),
180             edge.getHeadCompassPoint());
181 
182     out.println(tailId + " -> " + headId + " " + getAttrString(attrs));
183   }
184 
getEdgeAttributes(GraphvizEdge edge)185   protected Map<String, String> getEdgeAttributes(GraphvizEdge edge) {
186     Map<String, String> attrs = Maps.newHashMap();
187 
188     attrs.put("arrowhead", getArrowString(edge.getArrowHead()));
189     attrs.put("arrowtail", getArrowString(edge.getArrowTail()));
190     attrs.put("style", edge.getStyle().toString());
191 
192     return attrs;
193   }
194 
getAttrString(Map<String, String> attrs)195   private String getAttrString(Map<String, String> attrs) {
196     List<String> attrList = Lists.newArrayList();
197 
198     for (Entry<String, String> attr : attrs.entrySet()) {
199       String value = attr.getValue();
200 
201       if (value != null) {
202         attrList.add(attr.getKey() + "=" + value);
203       }
204     }
205 
206     return "[" + Joiner.on(", ").join(attrList) + "]";
207   }
208 
209   /**
210    * Turns a {@link List} of {@link ArrowType}s into a {@link String} that represents combining
211    * them. With Graphviz, that just means concatenating them.
212    */
getArrowString(List<ArrowType> arrows)213   protected String getArrowString(List<ArrowType> arrows) {
214     return Joiner.on("").join(arrows);
215   }
216 
getEdgeEndPoint(String nodeId, String portId, CompassPoint compassPoint)217   protected String getEdgeEndPoint(String nodeId, String portId, CompassPoint compassPoint) {
218     List<String> portStrings = Lists.newArrayList(nodeId);
219 
220     if (portId != null) {
221       portStrings.add(portId);
222     }
223 
224     if (compassPoint != null) {
225       portStrings.add(compassPoint.toString());
226     }
227 
228     return Joiner.on(":").join(portStrings);
229   }
230 
htmlEscape(String str)231   protected String htmlEscape(String str) {
232     return str.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
233   }
234 
htmlEscape(List<String> elements)235   protected List<String> htmlEscape(List<String> elements) {
236     List<String> escaped = Lists.newArrayList();
237     for (String element : elements) {
238       escaped.add(htmlEscape(element));
239     }
240     return escaped;
241   }
242 
243   @Override
newInterfaceNode(InterfaceNode node)244   protected void newInterfaceNode(InterfaceNode node) {
245     // TODO(phopkins): Show the Module on the graph, which comes from the
246     // class name when source is a StackTraceElement.
247 
248     NodeId nodeId = node.getId();
249     GraphvizNode gnode = new GraphvizNode(nodeId);
250     gnode.setStyle(NodeStyle.DASHED);
251     Key<?> key = nodeId.getKey();
252     gnode.setTitle(nameFactory.getClassName(key));
253     gnode.addSubtitle(0, nameFactory.getAnnotationName(key));
254     addNode(gnode);
255   }
256 
257   @Override
newImplementationNode(ImplementationNode node)258   protected void newImplementationNode(ImplementationNode node) {
259     NodeId nodeId = node.getId();
260     GraphvizNode gnode = new GraphvizNode(nodeId);
261     gnode.setStyle(NodeStyle.SOLID);
262 
263     gnode.setHeaderBackgroundColor("#000000");
264     gnode.setHeaderTextColor("#ffffff");
265     gnode.setTitle(nameFactory.getClassName(nodeId.getKey()));
266 
267     for (Member member : node.getMembers()) {
268       gnode.addField(portIdFactory.getPortId(member), nameFactory.getMemberName(member));
269     }
270 
271     addNode(gnode);
272   }
273 
274   @Override
newInstanceNode(InstanceNode node)275   protected void newInstanceNode(InstanceNode node) {
276     NodeId nodeId = node.getId();
277     GraphvizNode gnode = new GraphvizNode(nodeId);
278     gnode.setStyle(NodeStyle.SOLID);
279 
280     gnode.setHeaderBackgroundColor("#000000");
281     gnode.setHeaderTextColor("#ffffff");
282     gnode.setTitle(nameFactory.getClassName(nodeId.getKey()));
283 
284     gnode.addSubtitle(0, nameFactory.getSourceName(node.getSource()));
285 
286     gnode.setHeaderBackgroundColor("#aaaaaa");
287     gnode.setHeaderTextColor("#ffffff");
288     gnode.setTitle(nameFactory.getInstanceName(node.getInstance()));
289 
290     for (Member member : node.getMembers()) {
291       gnode.addField(portIdFactory.getPortId(member), nameFactory.getMemberName(member));
292     }
293 
294     addNode(gnode);
295   }
296 
297   @Override
newDependencyEdge(DependencyEdge edge)298   protected void newDependencyEdge(DependencyEdge edge) {
299     GraphvizEdge gedge = new GraphvizEdge(edge.getFromId(), edge.getToId());
300     InjectionPoint fromPoint = edge.getInjectionPoint();
301     if (fromPoint == null) {
302       gedge.setTailPortId("header");
303     } else {
304       gedge.setTailPortId(portIdFactory.getPortId(fromPoint.getMember()));
305     }
306     gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL));
307     gedge.setTailCompassPoint(CompassPoint.EAST);
308 
309     edges.add(gedge);
310   }
311 
312   @Override
newBindingEdge(BindingEdge edge)313   protected void newBindingEdge(BindingEdge edge) {
314     GraphvizEdge gedge = new GraphvizEdge(edge.getFromId(), edge.getToId());
315     gedge.setStyle(EdgeStyle.DASHED);
316     switch (edge.getType()) {
317       case NORMAL:
318         gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL_OPEN));
319         break;
320 
321       case PROVIDER:
322         gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL_OPEN, ArrowType.NORMAL_OPEN));
323         break;
324 
325       case CONVERTED_CONSTANT:
326         gedge.setArrowHead(ImmutableList.of(ArrowType.NORMAL_OPEN, ArrowType.DOT_OPEN));
327         break;
328     }
329 
330     edges.add(gedge);
331   }
332 
addNode(GraphvizNode node)333   private void addNode(GraphvizNode node) {
334     node.setIdentifier("x" + nodes.size());
335     nodes.put(node.getNodeId(), node);
336   }
337 }
338