001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.lib.commons.graph; 018 019import java.awt.image.BufferedImage; 020import java.io.BufferedReader; 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.InputStreamReader; 025import java.io.OutputStreamWriter; 026import java.io.Writer; 027 028import javax.imageio.ImageIO; 029 030import org.conqat.lib.commons.io.StreamReaderThread; 031 032/** 033 * Java interface to the Graphviz graph drawing toolkit. 034 * 035 * @author Florian Deissenboeck 036 * @author Benjamin Hummel 037 */ 038public class GraphvizGenerator { 039 040 /** Path to the layout engine executable. */ 041 private final String layoutEnginePath; 042 043 /** 044 * Create a new generator that uses <code>dot</code> and expects it to be on 045 * the path. 046 * 047 */ 048 public GraphvizGenerator() { 049 this("dot"); 050 } 051 052 /** 053 * Create a new generator by specifying the executable of the layout engine. 054 * This may be used if <code>dot</code> is not on the path or if another 055 * layout engine like <code>neato</code> should be used. 056 * 057 * @param layoutEnginePath 058 * path to layout engine excutable 059 */ 060 public GraphvizGenerator(String layoutEnginePath) { 061 this.layoutEnginePath = layoutEnginePath; 062 } 063 064 /** 065 * Export a graph to a file. 066 * 067 * @param description 068 * the graph description 069 * @param file 070 * the file to export to. 071 * @param format 072 * the export format. 073 * @throws IOException 074 * if an I/O problem occurrs. 075 * @throws GraphvizException 076 * if Graphviz produced an error (exit code != 0) 077 */ 078 public void generateFile(String description, File file, EGraphvizOutputFormat format) 079 throws IOException, GraphvizException { 080 runDot(description, null, "-T" + format.name().toLowerCase(), "-o" + file); 081 } 082 083 /** 084 * Export a graph to a file and return the HTML image map code. 085 * 086 * @param description 087 * the graph description 088 * @param file 089 * the file to export to. 090 * @param format 091 * the export format. 092 * @return the generated image map. These are only area-tags. The map tags 093 * including the name of the map must be created by the calling 094 * application. 095 * @throws IOException 096 * if an I/O problem occurrs. 097 * @throws GraphvizException 098 * if Graphviz produced an error (exit code != 0) 099 */ 100 public String generateFileAndImageMap(String description, File file, EGraphvizOutputFormat format) 101 throws IOException, GraphvizException { 102 TextReader reader = new TextReader(); 103 runDot(description, reader, "-T" + format.name().toLowerCase(), "-o" + file, "-Tcmap"); 104 return reader.contents.toString(); 105 } 106 107 /** 108 * Generate an image from a graph description. This uses Graphviz to 109 * generate a PNG image of the graph and javax.imageio to create the image 110 * object. All communication with Graphviz is handled via streams so no 111 * temporary files are used. 112 * 113 * @param description 114 * the graph description. 115 * @return the image 116 * @throws IOException 117 * if an I/O problem occurrs. 118 * @throws GraphvizException 119 * if Graphviz produced an error (exit code != 0) 120 */ 121 public BufferedImage generateImage(String description) throws IOException, GraphvizException { 122 ImageReader reader = new ImageReader(); 123 runDot(description, reader, "-Tpng"); 124 return reader.image; 125 } 126 127 /** 128 * Executes DOT, feeding in the provided graph description. The output of 129 * dot may be handled using an {@link IStreamReader}. DOT errorr are handled 130 * in this method. 131 * 132 * @param description 133 * the graph description. 134 * @param streamReader 135 * the reader used for the output stream of DOT. If this is null, 136 * a dummy reader is used to keep DOT from blocking. 137 * @param arguments 138 * the arguments passed to DOT. 139 * @throws IOException 140 * if an I/O problem occurrs. 141 * @throws GraphvizException 142 * if Graphviz produced an error (exit code != 0) 143 */ 144 private void runDot(String description, IStreamReader streamReader, String... arguments) 145 throws IOException, GraphvizException { 146 147 String[] completeArguments = new String[arguments.length + 1]; 148 completeArguments[0] = layoutEnginePath; 149 for (int i = 0; i < arguments.length; ++i) { 150 completeArguments[i + 1] = arguments[i]; 151 } 152 153 ProcessBuilder builder = new ProcessBuilder(completeArguments); 154 Process dotProcess = builder.start(); 155 156 // read error for later use 157 StreamReaderThread errReader = new StreamReaderThread(dotProcess.getErrorStream()); 158 159 // pipe graph into dot 160 Writer stdIn = new OutputStreamWriter(dotProcess.getOutputStream()); 161 stdIn.write(description); 162 stdIn.close(); 163 164 if (streamReader == null) { 165 // read dot standard output to drain the buffer, then throw away 166 new StreamReaderThread(dotProcess.getInputStream()); 167 } else { 168 // reading may happen in this thread, as stderr is read in a thread 169 // of its own 170 streamReader.performReading(dotProcess.getInputStream()); 171 } 172 173 // wait for dot 174 try { 175 dotProcess.waitFor(); 176 } catch (InterruptedException e) { 177 // ignore this one 178 } 179 180 String errorContent = errReader.getContent(); 181 if (dotProcess.exitValue() != 0 182 // recent versions do not exit with non-null on syntax errors 183 || errorContent.contains("syntax error")) { 184 throw new GraphvizException(errorContent); 185 } 186 } 187 188 /** 189 * Interface used from 190 * {@link GraphvizGenerator#runDot(String, org.conqat.lib.commons.graph.GraphvizGenerator.IStreamReader, String[])} 191 * . 192 */ 193 private static interface IStreamReader { 194 195 /** Perform the desired action on the given input stream. */ 196 void performReading(InputStream inputStream) throws IOException; 197 } 198 199 /** A stream reader for reading an image. */ 200 private static class ImageReader implements IStreamReader { 201 /** The image read. */ 202 BufferedImage image = null; 203 204 /** {@inheritDoc} */ 205 @Override 206 public void performReading(InputStream inputStream) throws IOException { 207 image = ImageIO.read(inputStream); 208 } 209 } 210 211 /** A stream reader for reading plain text. */ 212 private static class TextReader implements IStreamReader { 213 /** The contents read from the stream. */ 214 StringBuilder contents = new StringBuilder(); 215 216 /** {@inheritDoc} */ 217 @Override 218 public void performReading(InputStream inputStream) throws IOException { 219 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 220 char[] buffer = new char[1024]; 221 int read = 0; 222 while ((read = reader.read(buffer)) != -1) { 223 contents.append(buffer, 0, read); 224 } 225 } 226 } 227}