cdc.gv.GvWriter Maven / Gradle / Ivy
package cdc.gv;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import cdc.gv.GvWriterContext.Status;
import cdc.gv.atts.GvAttributeName;
import cdc.gv.atts.GvAttributes;
import cdc.gv.atts.GvClusterAttributes;
import cdc.gv.atts.GvEdgeAttributes;
import cdc.gv.atts.GvGraphAttributes;
import cdc.gv.atts.GvNodeAttributes;
import cdc.gv.atts.GvSubgraphAttributes;
import cdc.gv.support.GvSupport;
import cdc.io.utils.NonCloseableOutputStream;
import cdc.util.lang.Checks;
import cdc.util.lang.InvalidStateException;
/**
* Class facilitating creation of GraphViz files.
*
* @author Damien Carbonne
*
*/
public class GvWriter implements Flushable, Closeable {
private GvWriterContext context = new GvWriterContext();
private String indentString = " ";
private final Writer writer;
private static final String EOL = System.lineSeparator();
/**
* String used to represent an edge. This may be "--" for undirected graphs
* or {@literal "->"} for directed graphs.
*/
private String edge = null;
/**
* Creates a GvWriter that will print to passed writer.
*
* @param writer The writer.
*/
public GvWriter(Writer writer) {
Checks.isNotNull(writer, "writer");
this.writer = writer;
}
/**
* Creates a GvWriter that will print to System.out.
*/
public GvWriter() {
this(new BufferedWriter(new OutputStreamWriter(new NonCloseableOutputStream(System.out))));
}
/**
* Creates a GvWriter that will print to a file using default encoding.
*
* @param filename Name of the file to create.
* @throws IOException If the named file exists but is a directory rather than
* a regular file, does not exist but cannot be created, or cannot
* be opened for any other reason
*/
public GvWriter(String filename) throws IOException {
this(new BufferedWriter(new FileWriter(filename)));
}
/**
* Creates a GvWriter that will print to a file using default encoding.
*
* @param file File to create.
* @throws IOException If the file exists but is a directory rather than
* a regular file, does not exist but cannot be created, or
* cannot be opened for any other reason
*/
public GvWriter(File file) throws IOException {
this(new BufferedWriter(new FileWriter(file)));
}
public GvWriter(String filename,
String encoding)
throws IOException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename), encoding)));
}
public GvWriter(File file,
String encoding)
throws IOException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), encoding)));
}
public void indent(int delta) throws IOException {
for (int i = context.getLevel() + delta; i > 0; i--) {
writer.append(indentString);
}
}
public void indent() throws IOException {
indent(0);
}
/**
* Prints a set of attributes, one per line.
*
* Only non null values are printed.
*
*
* att1=?value1?;
* att2=?value2?;
* ...
* where ? is an appropriate delimiter.
*
* @param atts The attributes.
* @throws IOException If an I/O error occurs
*/
private void printSplit(GvAttributes> atts) throws IOException {
if (atts != null) {
for (final GvAttributeName name : GvAttributeName.values()) {
final String value = atts.getValue(name);
if (value != null) {
indent(1);
writer.append(name.encode());
writer.append("=");
writer.append(name.getDelimiter(value));
writer.append(value);
writer.append(name.getDelimiter(value));
writer.append(";");
writer.append(EOL);
}
}
for (final String name : atts.getExtensionNames()) {
final String value = atts.getExtensionValue(name);
if (value != null) {
indent(1);
writer.append(name);
writer.append("=\"");
writer.append(value);
writer.append("\";");
writer.append(EOL);
}
}
}
}
/**
* Prints a set of attributes on one line.
*
* Only non null values are printed.
* '[' att1=?value1? att2=?value2? ...']'
* where ? is an appropriate delimiter.
*
* @param atts The attributes.
* @throws IOException If an I/O error occurs.
*/
private void printSep(GvAttributes> atts) throws IOException {
if (atts != null) {
boolean first = true;
for (final GvAttributeName name : GvAttributeName.values()) {
final String value = atts.getValue(name);
if (value != null) {
if (first) {
writer.append(" [");
first = false;
} else {
writer.append(" ");
}
writer.append(name.encode());
writer.append("=");
writer.append(name.getDelimiter(value));
writer.append(value);
writer.append(name.getDelimiter(value));
}
}
for (final String name : atts.getExtensionNames()) {
final String value = atts.getExtensionValue(name);
if (value != null) {
if (first) {
writer.append(" [");
first = false;
} else {
writer.append(" ");
}
writer.append(name);
writer.append("=\"");
writer.append(value);
writer.append("\"");
}
}
if (!first) {
writer.append("]");
}
}
}
private void stateException(String message) {
throw new InvalidStateException("Invalid state: " + context.getStatus() + ", when calling: " + message);
}
/**
* @return The current tab size used for indentation.
*/
public final String getIndentString() {
return indentString;
}
public final GvWriter setIndentString(String s) {
this.indentString = s;
return this;
}
/**
* Writes a comment.
*
* This can be done at any place when the writer is open.
*
* @param comment The comment to write
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter addComment(String comment) throws IOException {
indent(1);
writer.append("// ");
writer.append(comment);
writer.append(EOL);
return this;
}
public final GvWriter print(String s) throws IOException {
writer.append(s);
return this;
}
public final GvWriter println(String s) throws IOException {
print(s);
return println();
}
/**
* Terminates the current line by writing a line separator string.
*
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter println() throws IOException {
writer.append(EOL);
return this;
}
/**
* Begins a directed or undirected graph. A call to endGraph should match
* this call.
*
* @param name Name of the graph.
* @param directed If true, the graph is directed. Otherwise it is
* undirected.
* @param atts Graph attributes.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter beginGraph(String name,
boolean directed,
GvGraphAttributes atts) throws IOException {
if (context.getStatus() == Status.IN_OPEN_STREAM) {
context = context.pushContext(Status.IN_GRAPH);
context.setName(name);
} else {
stateException("beginGraph()");
}
if (directed) {
writer.append("digraph ");
this.edge = " -> ";
} else {
writer.append("graph ");
this.edge = " -- ";
}
writer.append(GvSupport.toValidId(name));
writer.append(" {");
writer.append(EOL);
printSplit(atts);
return this;
}
/**
* Defines default node attributes for the current graph or subgraph.
*
* @param atts Default node attributes.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter setDefaultNodeAttributes(GvNodeAttributes atts) throws IOException {
switch (context.getStatus()) {
case IN_GRAPH:
case IN_SUBGRAPH:
break;
default:
stateException("setDefaultNodeAttributes()");
break;
}
indent(1);
writer.append("node");
printSep(atts);
writer.append(EOL);
return this;
}
/**
* Defines default edge attributes for the current graph or subgraph.
*
* @param atts Default edge attributes.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter setDefaultEdgeAttributes(GvEdgeAttributes atts) throws IOException {
switch (context.getStatus()) {
case IN_GRAPH:
case IN_SUBGRAPH:
break;
default:
stateException("setDefaultEdgeAttributes()");
break;
}
indent(1);
writer.append("edge");
printSep(atts);
writer.append(EOL);
return this;
}
/**
* End the definition of the current graph. After that, comments and new
* lines can be added, or the writer can be closed.
*
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter endGraph() throws IOException {
if (context.getStatus() == Status.IN_GRAPH) {
context = context.popContext();
context.setStatus(Status.IN_CLOSE_STREAM);
} else {
stateException("endGraph()");
}
writer.append("}");
writer.append(EOL);
writer.flush();
return this;
}
/**
* Begins the creation of a new subgraph, in the current graph, subgraph or
* cluster.
*
* @param name Name of the subgraph.
* @param atts Attributes of the subgraph.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter beginSubgraph(String name,
GvSubgraphAttributes atts) throws IOException {
switch (context.getStatus()) {
case IN_GRAPH:
case IN_SUBGRAPH:
case IN_CLUSTER:
context = context.pushContext(Status.IN_SUBGRAPH);
context.setName(name);
break;
default:
stateException("beginSubgraph()");
break;
}
indent(0);
if (name != null) {
writer.append("subgraph ");
writer.append(GvSupport.toValidId(name));
writer.append(" {");
} else {
writer.append('{');
}
writer.append(EOL);
printSplit(atts);
return this;
}
/**
* Ends the definition of the current subgraph.
*
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter endSubgraph() throws IOException {
if (context.getStatus() == Status.IN_SUBGRAPH) {
context = context.popContext();
} else {
stateException("endSubgraph()");
}
indent(1);
writer.append("}");
writer.append(EOL);
return this;
}
/**
* Begins the creation of a cluster, in the current graph, subgraph or
* cluster.
*
* @param name Name of the cluster.
* @param atts Attributes of the cluster.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter beginCluster(String name,
GvClusterAttributes atts) throws IOException {
switch (context.getStatus()) {
case IN_GRAPH:
case IN_SUBGRAPH:
case IN_CLUSTER:
context = context.pushContext(Status.IN_CLUSTER);
context.setName(name);
break;
default:
stateException("beginCluster()");
break;
}
indent(0);
writer.append("subgraph ");
writer.append(GvSupport.toClusterId(name));
writer.append(" {");
writer.append(EOL);
printSplit(atts);
return this;
}
public final GvWriter endCluster() throws IOException {
if (context.getStatus() == Status.IN_CLUSTER) {
context = context.popContext();
} else {
stateException("endCluster()");
}
indent(1);
writer.append("}");
writer.append(EOL);
return this;
}
/**
* Adds a node to the current graph, subgraph or cluster.
*
* @param id Node identifier.
* @param atts Node attributes.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter addNode(String id,
GvNodeAttributes atts) throws IOException {
switch (context.getStatus()) {
case IN_GRAPH:
case IN_SUBGRAPH:
case IN_CLUSTER:
break;
default:
stateException("addNode()");
break;
}
indent(1);
writer.append(GvSupport.toValidId(id));
printSep(atts);
writer.append(EOL);
return this;
}
public final GvWriter addNode(String id) throws IOException {
return addNode(id, null);
}
/**
* Adds an edge to the current graph, subgraph or cluster.
*
* @param sourceId Source node identifier.
* @param targetId Target node identifier.
* @param atts Edge attributes.
* @return This GvWriter.
* @throws IOException If an I/O error occurs
*/
public final GvWriter addEdge(String sourceId,
String targetId,
GvEdgeAttributes atts) throws IOException {
switch (context.getStatus()) {
case IN_GRAPH:
case IN_SUBGRAPH:
case IN_CLUSTER:
break;
default:
stateException("addEdge()");
break;
}
indent(1);
writer.append(GvSupport.toValidId(sourceId));
writer.append(edge);
writer.append(GvSupport.toValidId(targetId));
printSep(atts);
writer.append(EOL);
return this;
}
public final GvWriter addEdge(String sourceId,
String targetId) throws IOException {
return addEdge(sourceId, targetId, null);
}
public final GvWriter addEdge(String sourceId,
String sourceClusterId,
String targetId,
String targetClusterId,
GvEdgeAttributes atts) throws IOException {
final String pltail = atts.getValue(GvAttributeName.LTAIL);
final String plhead = atts.getValue(GvAttributeName.LHEAD);
if (sourceClusterId != null) {
atts.setLogicalTail(sourceClusterId);
}
if (targetClusterId != null) {
atts.setLogicalHead(targetClusterId);
}
addEdge(sourceId, targetId, atts);
// Reset atts to previous values.
atts.setLogicalTail(pltail);
atts.setLogicalHead(plhead);
return this;
}
public final GvWriter addEdge(String sourceId,
String sourceClusterId,
String targetId,
String targetClusterId) throws IOException {
return addEdge(sourceId, sourceClusterId, targetId, targetClusterId, new GvEdgeAttributes());
}
@Override
public void flush() throws IOException {
writer.flush();
}
@Override
public void close() throws IOException {
writer.close();
}
}