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}