/* * This file is part of JOP, the Java Optimized Processor * see <http://www.jopdesign.com/> * * Copyright (C) 2008, Benedikt Huber (benedikt.huber@gmail.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.jopdesign.common.graphutils; import org.jgrapht.DirectedGraph; import org.jgrapht.Graph; import java.io.IOException; import java.io.Writer; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; /** * Export graphs to .DOT, with custom attributes. Supports JGraphT graphs via {@code JGraphTAdapter} * * @author Benedikt Huber <benedikt.huber@gmail.com> * @param <V> node type * @param <E> edge type */ public class AdvancedDOTExporter<V, E> { public interface GraphAdapter<V,E> { public V getEdgeSource(E e); public V getEdgeTarget(E e); } public static class JGraphTAdapter<V,E> implements GraphAdapter<V,E> { private Graph<V, E> backing; public JGraphTAdapter(Graph<V,E> g) { this.backing = g; } @Override public V getEdgeSource(E e) { return backing.getEdgeSource(e); } @Override public V getEdgeTarget(E e) { return backing.getEdgeTarget(e); } } public enum MultiLineAlignment { ML_ALIGN_LEFT, ML_ALIGN_CENTER, ML_ALIGN_RIGHT } /** * Escape a string, s.t. it can be used as a DOT attribute value. * * @param s input string * @param mla alignment type * @return the string, with special characters replaced, and quoted if neccessary */ public static String escapedToString(String s, MultiLineAlignment mla) { StringBuffer sb = new StringBuffer(); boolean quote = false; boolean hasBreak = false; String lineBreak = null; switch (mla) { case ML_ALIGN_LEFT: lineBreak = "\\l"; break; case ML_ALIGN_CENTER: lineBreak = "\\n"; break; case ML_ALIGN_RIGHT: lineBreak = "\\r"; break; } char c = 0; for (int i = 0; i < s.length(); i++) { c = s.charAt(i); switch (c) { case '\n': sb.append(lineBreak); hasBreak = true; break; case '\r': break; case '\t': sb.append("\\t"); break; case '"': sb.append("\\\""); break; default: sb.append(c); break; } if (!Character.isLetterOrDigit(c)) quote = true; } // needed to justify the last line if (hasBreak && c != '\n') sb.append(lineBreak); if (quote || sb.length() == 0) { sb.insert(0, '"'); sb.append('"'); } return sb.toString(); } /** * Sets label and other attributes of a <code>DOT</code> node or edge based on the provided * object. * * @param <T> The type of the node/edge. */ public interface DOTLabeller<T> { /** * get the label for the object * * @param object tje object to the the label from * @return the label, or null if no label should be used */ String getLabel(T object); /** * Set <code>DOT</code> attributes depending on the given object * * @param object the object to set the label to * @param ht the map to write attributes into * @return true if <code>ht</code> has been changed */ boolean setAttributes(T object, Map<String, String> ht); } /** * Provides the ID and sets label and other attributes of a <code>DOT</code> node based * on the provided node. * * @param <T> node type */ public interface DOTNodeLabeller<T> extends DOTLabeller<T> { /** * get the ID of a graph's node * * @param node the node to get the ID from * @return the (unique) integer ID */ int getID(T node); } /** * Doesn't set a label. * * @param <T> the type of the objects getting labelled */ public static class NullLabeller<T> implements DOTLabeller<T> { public String getLabel(T obj) { return null; } public boolean setAttributes(T object, Map<String, String> ht) { return false; } } /** * Uses {@link T#toString()} to provide a label * * @param <T> the type of the objects getting labelled */ public static class DefaultDOTLabeller<T> implements DOTLabeller<T> { public String getLabel(T node) { return node.toString(); } public boolean setAttributes(T node, Map<String, String> ht) { String label = getLabel(node); if (label != null) ht.put("label", label); return (node != null); } } /** * Uses a <code>Map</code>(dictionary) to provide labels * * @param <T> the type of the objects getting labelled */ public static class MapLabeller<T> extends DefaultDOTLabeller<T> { protected Map<T, ?> annots; public MapLabeller(Map<T, ?> annots) { this.annots = annots; } public String getLabel(T obj) { if (annots.containsKey(obj)) { return annots.get(obj).toString(); } else { return obj.toString(); } } } /** * Additionally provides node IDs using a hash-based <code>Map</code> * * @param <T> The node type. Needs to provide correct <code>hashCode()</code> */ public static class DefaultNodeLabeller<T> extends DefaultDOTLabeller<T> implements DOTNodeLabeller<T> { private Map<T, Integer> nodeId = new HashMap<T, Integer>(); public int getID(T node) { Integer id = nodeId.get(node); if (id == null) { id = nodeId.size(); nodeId.put(node, id); } return id; } } /** * Default DOT attributes used for nodes, using box shape and * a 10pt font. * * @return the attribute dictionary */ public static Map<String, String> defaultNodeAttributes() { Map<String, String> attrs = new HashMap<String, String>(); attrs.put("shape", "box"); attrs.put("fontname", "Courier"); attrs.put("fontsize", "10"); return attrs; } /** * Default DOT attributes used for edges, using a 10pt font. * * @return the attribute dictionary */ public static Map<String, String> defaultEdgeAttributes() { Map<String, String> attrs = new HashMap<String, String>(); attrs.put("fontname", "Courier"); attrs.put("fontsize", "10"); return attrs; } private DOTNodeLabeller<V> nodeLabeller; private DOTLabeller<E> edgeLabeller; private Map<String, String> nodeAttributes; private Map<String, String> edgeAttributes; private Map<String, String> graphAttributes; private MultiLineAlignment multiLineAlignment; /** * Create a DOT exporter with {@link DefaultNodeLabeller} for nodes, * and {@link NullLabeller} for edges. */ public AdvancedDOTExporter() { this(null, null); } /** * Create a DOT exporter using the provided labellers * * @param nodeLabeller if null, use {@link DefaultNodeLabeller} * @param edgeLabeller if null, use {@link NullLabeller} */ public AdvancedDOTExporter(DOTNodeLabeller<V> nodeLabeller, DOTLabeller<E> edgeLabeller) { this.nodeLabeller = nodeLabeller == null ? new DefaultNodeLabeller<V>() : nodeLabeller; this.edgeLabeller = edgeLabeller == null ? new NullLabeller<E>() : edgeLabeller; this.nodeAttributes = defaultNodeAttributes(); this.edgeAttributes = defaultEdgeAttributes(); this.graphAttributes = new HashMap<String, String>(); this.multiLineAlignment = MultiLineAlignment.ML_ALIGN_LEFT; } /** * Create a DOT file for the given directed graph * * @param writer target for the export * @param graph the directed graph to export * @throws IOException if {@code writer} raises an IO exception */ public void exportDOT(Writer writer, DirectedGraph<V, E> graph) throws IOException { exportDOTDiGraph(writer, graph); writer.flush(); } /** * Create a DOT digraph for the given graph * * @param writer target for the export * @param graph the graph to export * @throws IOException if {@code writer} raises an IO exception */ public void exportDOTDiGraph(Writer writer, Graph<V, E> graph) throws IOException { exportDOTDiGraph(writer, graph.vertexSet(), graph.edgeSet(), new JGraphTAdapter<V,E>(graph)); } /** * Create a DOT digraph for the given graph * * @param writer target for the export * @param graph the graph to export * @throws IOException if {@code writer} raises an IO exception */ public void exportDOTDiGraph(Writer writer, Iterable<V> vertexSet, Iterable<E> edgeSet, GraphAdapter<V,E> topo) throws IOException { writer.append("digraph cfg\n{\n"); if (!this.graphAttributes.isEmpty()) { writer.append("graph "); appendAttributes(writer, this.graphAttributes); writer.append(";\n"); } for (V n : vertexSet) { int id = nodeLabeller.getID(n); Map<String, String> attrs = new HashMap<String, String>(this.nodeAttributes); nodeLabeller.setAttributes(n, attrs); writer.append("" + id + " "); appendAttributes(writer, attrs); writer.append(";\n"); } for (E e : edgeSet) { int idSrc = nodeLabeller.getID(topo.getEdgeSource(e)); int idTarget = nodeLabeller.getID(topo.getEdgeTarget(e)); Map<String, String> attrs = new HashMap<String, String>(this.edgeAttributes); edgeLabeller.setAttributes(e, attrs); writer.append("" + idSrc + " -> " + idTarget); appendAttributes(writer, attrs); writer.append(";\n"); } writer.append("}\n"); } private void appendAttributes(Writer w, Map<String, String> attrs) throws IOException { if (attrs.isEmpty()) return; w.append('['); boolean first = true; for (Entry<String, String> e : attrs.entrySet()) { if (first) first = false; else w.append(','); w.append(e.getKey()); w.append('='); w.append(escapedToString(e.getValue(), this.multiLineAlignment)); } w.append(']'); } public void setGraphAttribute(String key, String val) { this.graphAttributes.put(key, val); } }