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("&", "&").replace("<", "<").replace(">", ">"); 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