package edu.brown.graphs;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.voltdb.catalog.CatalogType;
import org.voltdb.catalog.Column;
import org.voltdb.catalog.Table;
import edu.brown.catalog.CatalogUtil;
import edu.brown.catalog.DependencyUtil;
import edu.brown.designer.DependencyGraph;
import edu.brown.designer.DesignerEdge;
import edu.brown.designer.DesignerVertex;
import edu.brown.designer.generators.DependencyGraphGenerator;
import edu.brown.utils.ArgumentsParser;
import edu.brown.utils.CollectionUtil;
import edu.brown.utils.FileUtil;
import edu.brown.utils.StringUtil;
/**
* Dump an IGraph to a Graphviz dot file
* @author pavlo
*
* @param <V>
* @param <E>
*/
public class GraphvizExport<V extends AbstractVertex, E extends AbstractEdge> {
private static final Logger LOG = Logger.getLogger(GraphvizExport.class);
// http://www.graphviz.org/doc/info/attrs.html
public enum Attribute {
BGCOLOR,
COLOR,
STYLE,
FILLCOLOR,
FONTSIZE,
FONTCOLOR,
FONTNAME,
PACK,
PENWIDTH,
RATIO,
SHAPE,
SIZE,
LABEL,
NOJUSTIFY,
};
public static class AttributeValues extends HashMap<Attribute, String> {
private static final long serialVersionUID = 1L;
@Override
public String put(Attribute key, String value) {
LOG.debug(key + " => " + value);
return super.put(key, value);
}
public String toString(String delimiter, Attribute...exclude) {
Set<Attribute> exclude_set = new HashSet<Attribute>();
if (exclude.length > 0) CollectionUtil.addAll(exclude_set, exclude);
final String f = (delimiter.equals("\n") ? StringUtil.SPACER : "") + "%s=\"%s\"" + delimiter;
StringBuilder sb = new StringBuilder();
for (Attribute a : this.keySet()) {
if (exclude_set.contains(a) == false) {
sb.append(String.format(f, a.name().toLowerCase(), this.get(a)));
}
} // FOR
return sb.toString();
}
} // END CLASS
// The graph that we will export
private final IGraph<V, E> graph;
// Global Options
private boolean edge_labels = true;
private boolean allow_isolated = true;
private boolean collapse_edges = false;
private String graph_label = null;
private final AttributeValues global_graph_attrs = new AttributeValues() {
private static final long serialVersionUID = 1L;
{
this.put(Attribute.BGCOLOR, "white");
this.put(Attribute.PACK, "true");
this.put(Attribute.RATIO, "compress");
}
};
private final AttributeValues global_vertex_attrs = new AttributeValues() {
private static final long serialVersionUID = 1L;
{
this.put(Attribute.SHAPE, "rectangle");
this.put(Attribute.FILLCOLOR, "grey");
this.put(Attribute.COLOR, "black");
this.put(Attribute.STYLE, "filled");
this.put(Attribute.FONTSIZE, "11");
this.put(Attribute.NOJUSTIFY, "true");
}
};
private final AttributeValues global_edge_attrs = new AttributeValues() {
private static final long serialVersionUID = 1L;
{
this.put(Attribute.FONTSIZE, "10");
}
};
private final AttributeValues graph_label_attrs = new AttributeValues() {
private static final long serialVersionUID = 1L;
{
this.put(Attribute.SHAPE, "ellipse");
this.put(Attribute.FILLCOLOR, "black");
this.put(Attribute.FONTCOLOR, "white");
this.put(Attribute.STYLE, "filled");
this.put(Attribute.FONTSIZE, "12");
}
};
private final Map<V, AttributeValues> vertex_attrs = new HashMap<V, AttributeValues>();
private final Map<E, AttributeValues> edge_attrs = new HashMap<E, AttributeValues>();
private final Map<String, Set<V>> subgraphs = new HashMap<String, Set<V>>();
/**
* Constructor
* @param graph
*/
public GraphvizExport(IGraph<V, E> graph) {
this.graph = graph;
}
@SuppressWarnings("unchecked")
public void addSubgraph(String subgraph, V...vertices) {
Set<V> subVertices = this.subgraphs.get(subgraph);
if (subVertices == null) {
subVertices = new HashSet<V>();
this.subgraphs.put(subgraph, subVertices);
}
for (V v : vertices) subVertices.add(v);
}
public void setGlobalLabel(String contents) {
this.graph_label = contents;
}
/**
* If set to true, the Graphviz file will contain edge labels
* @param edge_labels
*/
public void setEdgeLabels(boolean edge_labels) {
this.edge_labels = edge_labels;
}
/**
* If set to true, the Graphviz file will include vertices that do not have any edges
* @param allowIsolated
*/
public void setAllowIsolated(boolean allowIsolated) {
this.allow_isolated = allowIsolated;
}
/**
* If set to true, multiple edges between the same pairs of vertices will be collapsed into a single edge
* @param value
*/
public void setCollapseEdges(boolean value) {
this.collapse_edges = value;
}
public AttributeValues getGlobalGraphAttributes() {
return this.global_graph_attrs;
}
public AttributeValues getGlobalVertexAttributes() {
return this.global_vertex_attrs;
}
public AttributeValues getGlobalEdgeAttributes() {
return this.global_edge_attrs;
}
// Custom Vertex Attributes
public AttributeValues getAttributes(V vertex) {
return (this.getAttributes(vertex, true));
}
private AttributeValues getAttributes(V vertex, boolean create_if_null) {
AttributeValues av = this.vertex_attrs.get(vertex);
if (av == null && create_if_null) {
av = new AttributeValues();
this.vertex_attrs.put(vertex, av);
}
return (av);
}
public boolean hasAttributes(V vertex) {
return (this.vertex_attrs.containsKey(vertex));
}
// Custom Edge Attributes
public AttributeValues getAttributes(E edge) {
if (!this.edge_attrs.containsKey(edge)) {
this.edge_attrs.put(edge, new AttributeValues());
}
return (this.edge_attrs.get(edge));
}
public boolean hasAttributes(E edge) {
return (this.edge_attrs.containsKey(edge));
}
/**
* Export a graph into the Graphviz Dotty format
* @param <V>
* @param <E>
* @param graph
* @param graphName
* @return
* @throws Exception
*/
public String export(String graphName) throws Exception {
LOG.debug("Exporting " + this.graph.getClass().getSimpleName() + " to Graphviz " +
"[vertices=" + this.graph.getVertexCount() + ",edges=" + this.graph.getEdgeCount() + "]");
StringBuilder b = new StringBuilder();
boolean digraph = (this.graph instanceof AbstractDirectedGraph<?, ?> || this.graph instanceof AbstractDirectedTree<?, ?>);
// Start Graph
String graph_type = (digraph ? "digraph" : "graph");
String edge_type = " " + (digraph ? "->" : "--") + " ";
b.append(graph_type + " " + graphName + " {\n");
// Subgraphs
Set<V> subgraph_vertices = new HashSet<V>();
for (String subgraph : this.subgraphs.keySet()) {
b.append(StringUtil.SPACER).append("subgraph ").append(subgraph).append(" {\n");
for (V v : this.subgraphs.get(subgraph)) {
AttributeValues av = this.getAttributes(v, false);
this.writeVertex(b, v.toString(), av, StringUtil.SPACER + StringUtil.SPACER);
} // FOR
b.append(StringUtil.SPACER).append("}\n");
} // FOR
// Global Graph Attributes
b.append(StringUtil.SPACER).append("graph [\n");
b.append(StringUtil.addSpacers(this.getGlobalGraphAttributes().toString("\n")));
b.append(StringUtil.SPACER).append("]\n");
// Global Vertex Attributes
b.append(StringUtil.SPACER).append("node [\n");
b.append(StringUtil.addSpacers(this.getGlobalVertexAttributes().toString("\n")));
b.append(StringUtil.SPACER).append("]\n");
// Global Edge Attributes
b.append(StringUtil.SPACER).append("edge [\n");
b.append(StringUtil.addSpacers(this.getGlobalEdgeAttributes().toString("\n")));
b.append(StringUtil.SPACER).append("]\n");
// Edges
Set<V> all_vertices = new HashSet<V>();
Set<List<V>> redundant_edges = (this.collapse_edges ? new HashSet<List<V>>() : null);
// FORMAT: <Vertex0> <edgetype> <Vertex1>
final String edge_f = StringUtil.SPACER + "\"%s\" %s \"%s\" ";
for (E edge : this.graph.getEdges()) {
List<V> edge_vertices = new ArrayList<V>(graph.getIncidentVertices(edge));
assert(edge_vertices.isEmpty() == false) : "No vertice for edge " + edge;
all_vertices.addAll(edge_vertices);
// Check whether we've seen an edge between these two guys before
if (this.collapse_edges) {
Collections.sort(edge_vertices);
if (redundant_edges.contains(edge_vertices)) {
LOG.debug("Skipping redundant edge for " + edge_vertices);
continue;
}
redundant_edges.add(edge_vertices);
}
V v0 = edge_vertices.get(0);
assert(v0 != null) : "Source vertex is null for edge " + edge;
V v1 = edge_vertices.get(1);
assert(v1 != null) : "Destination vertex is null for edge " + edge;
// Print Edge
b.append(String.format(edge_f, v0.toString(), edge_type, v1.toString()));
// Edge Attributes
if (this.edge_labels || this.hasAttributes(edge)) {
String label = null;
AttributeValues av = this.getAttributes(edge);
if (av.containsKey(Attribute.LABEL)) {
label = av.get(Attribute.LABEL);
} else if (this.edge_labels) {
label = edge.toString();
}
b.append("[");
if (this.hasAttributes(edge)) b.append(this.getAttributes(edge).toString(" ", Attribute.LABEL));
if (label != null) b.append("label=\"").append(this.escapeLabel(label)).append("\"");
b.append("] ");
}
b.append(";\n");
} // FOR
// Vertices
b.append("\n");
for (V v : this.graph.getVertices()) {
// If this vertex wasn't a part of an edge and we don't allow for disconnected. then skip
if (!all_vertices.contains(v) && !this.allow_isolated) continue;
// If this vertex was a part of an edge but it doesn't have any custom attributes, then skip
if (all_vertices.contains(v) && !this.hasAttributes(v)) continue;
// If it's already in a subgraph, then skip it
if (subgraph_vertices.contains(v)) continue;
AttributeValues av = this.getAttributes(v, false);
this.writeVertex(b, v.toString(), av, StringUtil.SPACER);
} // FOR
// Global Graph Label
if (this.graph_label != null) {
this.graph_label_attrs.put(Attribute.LABEL, this.graph_label);
this.writeVertex(b, "__global__", this.graph_label_attrs, StringUtil.SPACER);
}
// Close graph
b.append("}\n");
return (b.toString());
}
private void writeVertex(StringBuilder b, String id, AttributeValues av, String spacer) {
b.append(String.format("%s\"%s\"", spacer, id));
// Vertex Attributes
if (av != null) {
String label = null;
if (av.containsKey(Attribute.LABEL)) {
label = av.get(Attribute.LABEL);
}
b.append("[");
b.append(av.toString(" ", Attribute.LABEL));
if (label != null) b.append("label=\"").append(this.escapeLabel(label)).append("\"");
b.append("] ");
}
b.append(" ;\n");
}
private String escapeLabel(String label) {
return (label.replace("\n", "\\n"));
}
/**
* Highlights the given edge path (with vertices) using the given color
* @param path
* @param highlight_color
*/
public void highlightPath(List<E> path, String highlight_color) {
Integer highlight_width = 4;
for (E e : path) {
this.getAttributes(e).put(Attribute.COLOR, highlight_color);
this.getAttributes(e).put(Attribute.PENWIDTH, Integer.toString(highlight_width * 2));
this.getAttributes(e).put(Attribute.STYLE, "bold");
for (V v : this.graph.getIncidentVertices(e)) {
this.getAttributes(v).put(Attribute.COLOR, highlight_color);
this.getAttributes(v).put(Attribute.PENWIDTH, highlight_width.toString());
} // FOR
} // FOR
}
/**
* Convenience method to write the GraphvizExport handle to a file the temporary directory
* @param catalog_obj
* @return
* @throws Exception
*/
public File writeToTempFile(CatalogType catalog_obj) {
return (this.writeToTempFile(catalog_obj.fullName(), null));
}
public File writeToTempFile(CatalogType catalog_obj, int i) {
return (this.writeToTempFile(catalog_obj.fullName(), Integer.toString(i)));
}
public File writeToTempFile(CatalogType catalog_obj, String suffix) {
return (this.writeToTempFile(catalog_obj.fullName(), suffix));
}
public File writeToTempFile(String name) {
return (this.writeToTempFile(name, null));
}
public File writeToTempFile() {
File f = FileUtil.getTempFile("dot", false);
String name = f.getName().replace(".dot", "");
try {
FileUtil.writeStringToFile(f, this.export(name));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
return (f);
}
/**
* Export the graph to a temp file
* @param name
* @param suffix
* @return
* @throws Exception
*/
public File writeToTempFile(String name, String suffix) {
if (suffix != null && suffix.length() > 0) suffix = "-" + suffix;
else if (suffix == null) suffix = "";
String filename = String.format("/tmp/%s%s.dot", name, suffix);
try {
return (FileUtil.writeStringToFile(filename, this.export(name)));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Export a graph into the Graphviz Dotty format
* @param <V>
* @param <E>
* @param graph
* @param graphName
* @return
* @throws Exception
*/
public static <V extends AbstractVertex, E extends AbstractEdge> String export(IGraph<V, E> graph, String graphName) {
GraphvizExport<V, E> gv = new GraphvizExport<V, E>(graph);
try {
return gv.export(graphName);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public static void main(String[] vargs) throws Exception {
ArgumentsParser args = ArgumentsParser.load(vargs);
args.require(ArgumentsParser.PARAM_CATALOG);
DependencyGraph dgraph = DependencyGraphGenerator.generate(args.catalogContext);
GraphUtil.removeDuplicateEdges(dgraph);
// Any optional parameters are tables we should ignore
// To do that we need to just remove them from the DependencyGraph
for (String opt : args.getOptParams()) {
for (String tableName : opt.split(",")) {
Table catalog_tbl = args.catalog_db.getTables().getIgnoreCase(tableName);
if (catalog_tbl == null) {
LOG.warn("Unknown table '" + tableName + "'");
continue;
}
DesignerVertex v = dgraph.getVertex(catalog_tbl);
assert(v != null) : "Failed to get vertex for " + catalog_tbl;
dgraph.removeVertex(v);
} // FOR
} // FOR
GraphvizExport<DesignerVertex, DesignerEdge> gvx = new GraphvizExport<DesignerVertex, DesignerEdge>(dgraph);
// Enable full edge labels
if (args.getBooleanParam(ArgumentsParser.PARAM_CATALOG_LABELS, false)) {
gvx.setEdgeLabels(true);
DependencyUtil dependUtil = DependencyUtil.singleton(args.catalog_db);
for (DesignerEdge e : dgraph.getEdges()) {
Table tbl0 = dgraph.getSource(e).getCatalogItem();
Table tbl1 = dgraph.getDest(e).getCatalogItem();
String label = "";
for (Column col0 : CatalogUtil.getSortedCatalogItems(tbl0.getColumns(), "index")) {
for (Column col1 : dependUtil.getDescendants(col0)) {
if (col1.getParent().equals(tbl1) == false) continue;
if (label.isEmpty() == false) label += "\n";
label += col0.getName() +
StringUtil.UNICODE_RIGHT_ARROW +
col1.getName();
} // FOR
} // FOR
AttributeValues attrs = gvx.getAttributes(e);
attrs.put(Attribute.LABEL, label);
} // FOR
} else {
gvx.setEdgeLabels(false);
}
String graphviz = gvx.export(args.catalog_type.name());
if (!graphviz.isEmpty()) {
File path = new File(args.catalog_type.name().toLowerCase() + ".dot");
FileUtil.writeStringToFile(path, graphviz);
System.out.println("Wrote contents to '" + path.getAbsolutePath() + "'");
} else {
System.err.println("ERROR: Failed to generate graphviz data");
System.exit(1);
}
}
}