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