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 * <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 */ 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 GraphvizGrapher(@raphviz NameFactory nameFactory, @Graphviz PortIdFactory portIdFactory)58 @Inject GraphvizGrapher(@Graphviz NameFactory nameFactory, 59 @Graphviz PortIdFactory portIdFactory) { 60 this.nameFactory = nameFactory; 61 this.portIdFactory = portIdFactory; 62 } 63 reset()64 @Override protected void reset() { 65 nodes.clear(); 66 edges.clear(); 67 } 68 setOut(PrintWriter out)69 public void setOut(PrintWriter out) { 70 this.out = out; 71 } 72 setRankdir(String rankdir)73 public void setRankdir(String rankdir) { 74 this.rankdir = rankdir; 75 } 76 postProcess()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 getGraphAttributes()93 protected Map<String, String> getGraphAttributes() { 94 Map<String, String> attrs = Maps.newHashMap(); 95 attrs.put("rankdir", rankdir); 96 return attrs; 97 } 98 start()99 protected void start() { 100 out.println("digraph injector {"); 101 102 Map<String, String> attrs = getGraphAttributes(); 103 out.println("graph " + getAttrString(attrs) + ";"); 104 } 105 finish()106 protected void finish() { 107 out.println("}"); 108 } 109 renderNode(GraphvizNode node)110 protected void renderNode(GraphvizNode node) { 111 Map<String, String> attrs = getNodeAttributes(node); 112 out.println(node.getIdentifier() + " " + getAttrString(attrs)); 113 } 114 getNodeAttributes(GraphvizNode node)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 */ getNodeLabel(GraphvizNode node)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 renderEdge(GraphvizEdge edge)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 getEdgeAttributes(GraphvizEdge edge)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 getAttrString(Map<String, String> attrs)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 */ getArrowString(List<ArrowType> arrows)207 protected String getArrowString(List<ArrowType> arrows) { 208 return Joiner.on("").join(arrows); 209 } 210 getEdgeEndPoint(String nodeId, String portId, CompassPoint compassPoint)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 htmlEscape(String str)225 protected String htmlEscape(String str) { 226 return str.replace("&", "&").replace("<", "<").replace(">", ">"); 227 } 228 htmlEscape(List<String> elements)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 newInterfaceNode(InterfaceNode node)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 newImplementationNode(ImplementationNode node)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 newInstanceNode(InstanceNode node)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 newDependencyEdge(DependencyEdge edge)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 newBindingEdge(BindingEdge edge)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 addNode(GraphvizNode node)322 private void addNode(GraphvizNode node) { 323 node.setIdentifier("x" + nodes.size()); 324 nodes.put(node.getNodeId(), node); 325 } 326 } 327