001/* Copyright (C) 2013 TU Dortmund
002 * This file is part of AutomataLib, http://www.automatalib.net/.
003 * 
004 * AutomataLib is free software; you can redistribute it and/or
005 * modify it under the terms of the GNU Lesser General Public
006 * License version 3.0 as published by the Free Software Foundation.
007 * 
008 * AutomataLib is distributed in the hope that it will be useful,
009 * but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
011 * Lesser General Public License for more details.
012 * 
013 * You should have received a copy of the GNU Lesser General Public
014 * License along with AutomataLib; if not, see
015 * http://www.gnu.de/documents/lgpl.en.html.
016 */
017package net.automatalib.commons.dotutil;
018
019import java.awt.Desktop;
020import java.awt.Dialog;
021import java.awt.Dimension;
022import java.awt.event.ActionEvent;
023import java.awt.event.KeyAdapter;
024import java.awt.event.KeyEvent;
025import java.awt.image.BufferedImage;
026import java.io.BufferedReader;
027import java.io.File;
028import java.io.FileNotFoundException;
029import java.io.FileReader;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.OutputStream;
033import java.io.OutputStreamWriter;
034import java.io.Reader;
035import java.io.StringReader;
036import java.io.StringWriter;
037import java.io.Writer;
038import java.util.logging.Logger;
039
040import javax.imageio.ImageIO;
041import javax.swing.AbstractAction;
042import javax.swing.JDialog;
043import javax.swing.JFrame;
044import javax.swing.JMenu;
045import javax.swing.JMenuBar;
046import javax.swing.JOptionPane;
047import javax.swing.JScrollPane;
048
049import net.automatalib.commons.util.IOUtil;
050
051
052/**
053 * Utility class to simplify operating the GraphVIZ "dot" utility. Please note that all
054 * of the provided methods require GraphVIZ to be installed on the system, and that the
055 * "dot" binary resides in the execution path.
056 * 
057 * @author Malte Isberner <malte.isberner@gmail.com>
058 *
059 */
060public class DOT {
061        
062        private static final Logger LOGGER = Logger.getLogger("automatalib.dotutil");
063        
064        private static final int MAX_WIDTH = 800;
065        private static final int MAX_HEIGHT = 600;
066        
067        
068        public static Process executeDOT(String format, String ...additionalOpts) throws IOException {
069                String[] dotArgs = new String[2 + additionalOpts.length];
070                dotArgs[0] = "dot";
071                dotArgs[1] = "-T" + format;
072                System.arraycopy(additionalOpts, 0, dotArgs, 2, additionalOpts.length);
073                Process dot = Runtime.getRuntime().exec(dotArgs);
074                
075                return dot;
076        }
077        
078        
079        /**
080         * Invokes the GraphVIZ DOT utility for rendering graphs.
081         * @param r the reader from which the GraphVIZ description is obtained.
082         * @param format the output format, as understood by the dot utility, e.g.,
083         * png, ps, ...
084         * @return an input stream from which the image data can be read.
085         * @throws IOException if reading from the specified reader fails.
086         */
087        public static InputStream runDOT(Reader r, String format, String ...additionalOpts) throws IOException {
088                Process dot = executeDOT(format, additionalOpts);
089                
090                OutputStream dotIn = dot.getOutputStream();
091                Writer dotWriter = new OutputStreamWriter(dotIn);
092                
093                IOUtil.copy(r, dotWriter);
094        
095                try {
096                        IOUtil.skip(dot.getErrorStream());
097                }
098                catch(IOException e) {}
099                
100                return dot.getInputStream();
101        }
102        
103        /**
104         * Invokes the DOT utility on a string.
105         * Convenience method, see {@link #runDOT(Reader, String)}.
106         */
107        public static InputStream runDOT(String dotText, String format, String ...additionalOpts) throws IOException {
108                StringReader sr = new StringReader(dotText);
109                return runDOT(sr, format, additionalOpts);
110        }
111        
112        /**
113         * Invokes the DOT utility on a file.
114         * Convenience method, see {@link #runDOT(Reader, String)}.
115         */
116        public static InputStream runDOT(File dotFile, String format, String ...additionalOpts) throws IOException {
117                FileReader fr = new FileReader(dotFile);
118                return runDOT(fr, format, additionalOpts);
119        }
120        
121        /**
122         * Invokes the GraphVIZ DOT utility for rendering graphs, writing output
123         * to the specified file.
124         * @param r the reader from which the GraphVIZ description is read.
125         * @param format the output format to produce.
126         * @param out the file to which the output is written.
127         * @throws IOException if an I/O error occurs reading from the given input
128         * or writing to the output file.
129         */
130        public static void runDOT(Reader r, String format, File out) throws IOException {
131                Process dot = executeDOT(format, "-o" + out.getAbsolutePath());
132                
133                OutputStream dotIn = dot.getOutputStream();
134                Writer dotWriter = new OutputStreamWriter(dotIn);
135                
136                IOUtil.copy(r, dotWriter);
137                dot.getErrorStream().close();
138                dot.getInputStream().close();
139                try {
140                        dot.waitFor();
141                }
142                catch(InterruptedException ex) {
143                        LOGGER.warning("Interrupted while waiting for 'dot' process to exit." + ex);
144                }
145        }
146        
147        
148        /**
149         * Invokes the DOT utility on a string, producing an output file.
150         * Convenience method, see {@link #runDOT(Reader, String, File)}.
151         */
152        public static void runDOT(String dotText, String format, File out) throws IOException {
153                runDOT(new StringReader(dotText), format, out);
154        }
155        
156        /**
157         * Invokes the DOT utility on a file, producing an output file.
158         * Convenience method, see {@link #runDOT(Reader, String, File)}.
159         */
160        public static void runDOT(File dotFile, String format, File out) throws IOException, FileNotFoundException {
161                runDOT(new FileReader(dotFile), format, out);
162        }
163        
164        /**
165         * Renders a GraphVIZ description, using an external program for displaying.
166         * The program is determined by the system's file type associations,
167         * using the {@link Desktop#open(File)} method. 
168         * @param r the reader from which the GraphVIZ description is read.
169         * @param format the output format, as understood by the dot utility, e.g.,
170         * png, ps, ... 
171         */
172        public static void renderDOTExternal(Reader r, String format) {
173                try {
174                        File image = File.createTempFile("dot", format);
175                        runDOT(r, format, image);
176                        Desktop.getDesktop().open(image);
177                }
178                catch(Exception e) {
179                        JOptionPane.showMessageDialog(null, "Error rendering DOT: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
180                }
181        }
182        
183        /**
184         * Renders a GraphVIZ description from a string, using an external program
185         * for displaying.
186         * Convenience method, see {@link #renderDOTExternal(Reader, String)}.
187         */
188        public static void renderDOTExternal(String dotText, String format) {
189                renderDOTExternal(new StringReader(dotText), format);
190        }
191        
192        /**
193         * Renders a GraphVIZ description from a file, using an external program
194         * for displaying.
195         * Convenience method, see {@link #renderDOTExternal(Reader, String)}.
196         * @throws FileNotFoundException if the specified file was not found.
197         */
198        public static void renderDOTExternal(File dotFile, String format) throws FileNotFoundException {
199                renderDOTExternal(new FileReader(dotFile), format);
200        }
201        
202        /**
203         * Renders a GraphVIZ description and displays it in a Swing window.
204         * @param r the reader from which the description is obtained.
205         * @param modal whether or not the dialog should be modal.
206         */
207        public static void renderDOT(Reader r, boolean modal) {
208                final DOTComponent cmp = createDOTComponent(r);
209                if (cmp == null)
210                        return;
211
212                final JDialog frame = new JDialog((Dialog) null, modal);
213                JScrollPane scrollPane = new JScrollPane(cmp);
214                frame.setContentPane(scrollPane);
215                frame.setMaximumSize(new Dimension(MAX_WIDTH, MAX_HEIGHT));
216                frame.pack();
217                JMenu menu = new JMenu("File");
218                menu.add(cmp.getSavePngAction());
219                menu.add(cmp.getSaveDotAction());
220                menu.addSeparator();
221                menu.add(new AbstractAction("Close") {
222                        private static final long serialVersionUID = -1L;
223
224                        @Override
225                        public void actionPerformed(ActionEvent e) {
226                                frame.setVisible(false);
227                        }
228                });
229                JMenuBar menuBar = new JMenuBar();
230                menuBar.add(menu);
231                frame.setJMenuBar(menuBar);
232                frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
233                frame.setVisible(true);
234                frame.addKeyListener(new KeyAdapter() {
235                        @Override
236                        public void keyTyped(KeyEvent e) {
237                                if(e.getKeyCode() == KeyEvent.VK_ESCAPE)
238                                        frame.setVisible(false);
239                        }
240                });
241        }
242        
243        
244        /**
245         * Renders a GraphVIZ description from a string and displays it in a
246         * Swing window.
247         * Convenience method, see {@link #renderDOT(Reader, boolean)}.
248         */
249        public static void renderDOT(String dotText, boolean modal) {
250                renderDOT(new StringReader(dotText), modal);
251        }
252        
253        /**
254         * Renders a GraphVIZ description from a string and displays it in a
255         * Swing window.
256         * Convenience method, see {@link #renderDOT(Reader, boolean)}.
257         * @throws FileNotFoundException if the specified file was not found.
258         */
259        public static void renderDOT(File dotFile, boolean modal) throws FileNotFoundException {
260                renderDOT(new FileReader(dotFile), modal);
261        }       
262        
263        /**
264         * Reads a DOT description from a reader and returns the PNG rendering result as a
265         * {@link BufferedImage}.
266         * @param dotReader the reader from which to read the description
267         * @return the rendering result
268         * @throws IOException if reading from the reader fails, or the pipe to the
269         * DOT process breaks.
270         */
271        public static BufferedImage renderDOTImage(Reader dotReader) throws IOException {
272                InputStream pngIs = runDOT(dotReader, "png");
273                BufferedImage img = ImageIO.read(pngIs);
274                pngIs.close();
275                
276                return img;
277        }
278        
279        /**
280         * Reads a DOT description from a string and returns the PNG rendering result as a
281         * {@link BufferedImage}.
282         * @param dotText the DOT description
283         * @return the rendering result
284         * @throws IOException if the pipe to the DOT process breaks.
285         */
286        public static BufferedImage renderDOTImage(String dotText) throws IOException {
287                return renderDOTImage(new StringReader(dotText));
288        }
289        
290        /**
291         * Reads a DOT description from a file and returns the PNG rendering result as a
292         * {@link BufferedImage}.
293         * @param dotFile the file containing the DOT description
294         * @return the rendering result
295         * @throws IOException if reading from the file fails or the pipe to the DOT process breaks.
296         */
297        public static BufferedImage renderDOTImage(File dotFile) throws IOException {
298                return renderDOTImage(new BufferedReader(new FileReader(dotFile)));
299        }
300        
301        /**
302         * Creates a {@link DOTComponent} that displays the result of rendering a DOT description
303         * read from a {@link Reader}.
304         * @param r the reader to read from
305         * @return the DOT component
306         */
307        public static DOTComponent createDOTComponent(Reader r) {
308                try {
309                        DOTComponent dc = new DOTComponent(r);
310                        return dc;
311                }
312                catch(IOException e) {
313                        JOptionPane.showMessageDialog(null, "Could not run DOT: " + e.getMessage(), "Failed to run DOT", JOptionPane.ERROR_MESSAGE);
314                        return null;
315                }
316        }
317        
318        /**
319         * Creates a Writer that can be used to write a DOT description to. Upon closing the writer,
320         * a window with the rendering result appears.
321         * @param modal whether or not this window is modal (if set to <tt>true</tt>, calls to
322         * {@link Writer#close()} will block.
323         * @return the writer
324         */
325        public static Writer createDotWriter(final boolean modal) {
326                return new StringWriter() {
327                        @Override
328                        public void close() throws IOException {
329                                renderDOT(getBuffer().toString(), modal);
330                                super.close();
331                        }
332                };
333        }
334}