/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.utils.incubator; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import de.rcenvironment.core.toolkitbridge.transitional.TextStreamWatcherFactory; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.textstream.TextOutputReceiver; import de.rcenvironment.core.utils.common.textstream.TextStreamWatcher; import de.rcenvironment.core.utils.common.textstream.receivers.PrefixingTextOutForwarder; import de.rcenvironment.core.utils.executor.CommandLineExecutor; import de.rcenvironment.core.utils.executor.LocalCommandLineExecutor; /** * Utility class for preparing data for the "Graphviz" tools (http://www.graphviz.org/) and invoking its commands. Note that Graphviz must * be installed and available via the PATH variable for the command invocations. * * @author Robert Mischke */ public final class GraphvizUtils { private GraphvizUtils() { // prevent instantiation } /** * Builder class for "dot" Graphviz files. * * @author Robert Mischke */ public static class DotFileBuilder { private List<String> vertexOrder = new ArrayList<String>(); private List<String> edgeScriptLines = new ArrayList<String>(); private Map<String, String> vertexProperties = new HashMap<String, String>(); // private Map<String, String> edgeProperties = new HashMap<String, String>(); private String graphName; public DotFileBuilder(String graphName) { this.graphName = graphName; } /** * Adds a vertex to the underlying graph. * * @param id the internal string id of the vertex; should be alphanumeric and start with a letter * @param label the label to annotate the node with; double quotes must be escaped (TODO test/review behaviour) */ public void addVertex(String id, String label) { vertexOrder.add(id); vertexProperties.put(id, StringUtils.format("label=\"%s\"", escapeValue(label))); } /** * Adds an additional property to an already-added vertex. Note that the new key-value pair is simply appended; previous properties * with the same key are preserved in the generated script file. * * @param id the id of the already-added vertex * @param key the key of the new property * @param value the value of the new property */ public void addVertexProperty(String id, String key, String value) { String oldValue = vertexProperties.get(id); // append new key-value pair String newValue = StringUtils.format("%s,%s=\"%s\"", oldValue, key, escapeValue(value)); vertexProperties.put(id, newValue); } /** * Adds an edge to the underlying graph. * * @param fromId the internal id of the source node * @param toId the internal id of the target node * @param label the label to annotate the edge with; double quotes must be escaped (TODO test/review behaviour) */ public void addEdge(String fromId, String toId, String label) { // simple, direct approach; change to a map to allow adding properties later edgeScriptLines.add(StringUtils.format(" v_%s->v_%s[label=\"%s\"];\n", escapeVertexId(fromId), escapeVertexId(toId), escapeValue(label))); } /** * Adds an edge to the underlying graph. * * @param fromId the internal id of the source node * @param toId the internal id of the target node * @param label the label to annotate the edge with; double quotes must be escaped (TODO test/review behaviour) * @param properties key/value properties to set for the edge */ public void addEdge(String fromId, String toId, String label, Map<String, String> properties) { String additionalProperties = ""; if (properties != null) { for (Map.Entry<String, String> entry : properties.entrySet()) { additionalProperties = StringUtils.format("%s,%s=\"%s\"", additionalProperties, entry.getKey(), escapeValue(entry.getValue())); } } edgeScriptLines.add(StringUtils.format(" v_%s->v_%s[label=\"%s\"%s];\n", escapeVertexId(fromId), escapeVertexId(toId), escapeVertexId(label), additionalProperties)); } /** * Returns the generated script content. It is typically written to a file and then processed by the Graphviz "dot" command. * * @return the script content */ public String getScriptContent() { StringBuilder buffer = new StringBuilder(); buffer.append("digraph "); buffer.append(graphName); buffer.append(" {\n"); for (String vertexId : vertexOrder) { buffer.append(StringUtils.format(" v_%s[%s];\n", escapeVertexId(vertexId), vertexProperties.get(vertexId))); } for (String edge : edgeScriptLines) { buffer.append(edge); } buffer.append("}\n"); return buffer.toString(); } private String escapeVertexId(String original) { // guard against "null" values if (original == null) { throw new NullPointerException(); } String result = original; result = result.replaceAll("-", "_"); return result; } private String escapeValue(String original) { // guard against "null" values if (original == null) { return "<null>"; } // TODO test if this covers all possible escapes String result = original; result = result.replaceAll("\"", "\\\\\""); // replacement: ["] -> [\"] return result; } } /** * Creates a builder (see "builder pattern") for "dot" Graphviz files. * * @param graphName the internal name of the graph * @return the created builder */ public static DotFileBuilder createDotFileBuilder(String graphName) { return new DotFileBuilder(graphName); } /** * Invokes the Graphviz "dot" command to render a Grapviz dot script file to a PNG file. * * @param gvFile the file containing the dot script * @param pngFile the output PNG file to be written * @param outputReceiver a receiver for text output during invocation * @return true on successful execution */ public static boolean renderDotFileToPng(File gvFile, File pngFile, TextOutputReceiver outputReceiver) { try { CommandLineExecutor executor = new LocalCommandLineExecutor(gvFile.getParentFile()); // inherit PATH variable executor.setEnv("PATH", System.getenv("PATH")); // invoke // TODO handle spaces/quotes/... internally executor.start("dot " + gvFile.getAbsolutePath() + " -Tpng -o " + pngFile.getAbsolutePath()); TextOutputReceiver stdoutCapture = new PrefixingTextOutForwarder("StdOut: ", outputReceiver); TextOutputReceiver stderrCapture = new PrefixingTextOutForwarder("StdErr: ", outputReceiver); TextStreamWatcher outWatcher = TextStreamWatcherFactory.create(executor.getStdout(), stdoutCapture); TextStreamWatcher errWatcher = TextStreamWatcherFactory.create(executor.getStderr(), stderrCapture); outWatcher.start(); errWatcher.start(); int exitCode = executor.waitForTermination(); if (exitCode == 0) { return true; } outputReceiver.addOutput("WARNING: Running graphviz returned an exit code of " + exitCode); } catch (IOException e) { outputReceiver.onFatalError(e); } catch (InterruptedException e) { outputReceiver.onFatalError(e); } return false; } }