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