View Javadoc
1   /*
2    * This file is a part of the SchemaSpy project (http://schemaspy.sourceforge.net).
3    * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 John Currier
4    *
5    * SchemaSpy is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU Lesser General Public
7    * License as published by the Free Software Foundation; either
8    * version 2.1 of the License, or (at your option) any later version.
9    *
10   * SchemaSpy is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with this library; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18   */
19  package net.sourceforge.schemaspy.util;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.Set;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  import net.sourceforge.schemaspy.Config;
32  
33  public class Dot {
34      private static Dot instance = new Dot();
35      private final Version version;
36      private final Version supportedVersion = new Version("2.2.1");
37      private final Version badVersion = new Version("2.4");
38      private final String lineSeparator = System.getProperty("line.separator");
39      private String dotExe;
40      private String format = "png";
41      private String renderer;
42      private final Set<String> validatedRenderers = Collections.synchronizedSet(new HashSet<String>());
43      private final Set<String> invalidatedRenderers = Collections.synchronizedSet(new HashSet<String>());
44  
45      private Dot() {
46          String versionText = null;
47          // dot -V should return something similar to:
48          //  dot version 2.8 (Fri Feb  3 22:38:53 UTC 2006)
49          // or sometimes something like:
50          //  dot - Graphviz version 2.9.20061004.0440 (Wed Oct 4 21:01:52 GMT 2006)
51          String[] dotCommand = new String[] { getExe(), "-V" };
52  
53          try {
54              Process process = Runtime.getRuntime().exec(dotCommand);
55              BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
56              String versionLine = reader.readLine();
57  
58              // look for a number followed numbers or dots
59              Matcher matcher = Pattern.compile("[0-9][0-9.]+").matcher(versionLine);
60              if (matcher.find()) {
61                  versionText = matcher.group();
62              } else {
63                  if (Config.getInstance().isHtmlGenerationEnabled()) {
64                      System.err.println();
65                      System.err.println("Invalid dot configuration detected.  '" +
66                                          getDisplayableCommand(dotCommand) + "' returned:");
67                      System.err.println("   " + versionLine);
68                  }
69              }
70          } catch (Exception validDotDoesntExist) {
71              if (Config.getInstance().isHtmlGenerationEnabled()) {
72                  System.err.println("Failed to query Graphviz version information");
73                  System.err.println("  with: " + getDisplayableCommand(dotCommand));
74                  System.err.println("  " + validDotDoesntExist);
75              }
76          }
77  
78          version = new Version(versionText);
79      }
80  
81      public static Dot getInstance() {
82          return instance;
83      }
84  
85      public boolean exists() {
86          return version.toString() != null;
87      }
88  
89      public Version getVersion() {
90          return version;
91      }
92  
93      public boolean isValid() {
94          return exists() && (getVersion().equals(supportedVersion) || getVersion().compareTo(badVersion) > 0);
95      }
96  
97      public String getSupportedVersions() {
98          return "dot version " + supportedVersion + " or versions greater than " + badVersion;
99      }
100 
101     public boolean supportsCenteredEastWestEdges() {
102         return getVersion().compareTo(new Version("2.6")) >= 0;
103     }
104 
105     /**
106      * Set the image format to generate.  Defaults to <code>png</code>.
107      * See <a href='http://www.graphviz.org/doc/info/output.html'>http://www.graphviz.org/doc/info/output.html</a>
108      * for valid formats.
109      *
110      * @param format image format to generate
111      */
112     public void setFormat(String format) {
113         this.format = format;
114     }
115 
116     /**
117      * @see #setFormat(String)
118      * @return
119      */
120     public String getFormat() {
121         return format;
122     }
123 
124     /**
125      * Returns true if the installed dot requires specifying :gd as a renderer.
126      * This was added when Win 2.15 came out because it defaulted to Cairo, which produces
127      * better quality output, but at a significant speed and size penalty.<p>
128      *
129      * The intent of this property is to determine if it's ok to tack ":gd" to
130      * the format specifier.  Earlier versions didn't require it and didn't know
131      * about the option.
132      *
133      * @return
134      */
135     public boolean requiresGdRenderer() {
136         return getVersion().compareTo(new Version("2.12")) >= 0 && supportsRenderer(":gd");
137     }
138 
139     /**
140      * Set the renderer to use for the -Tformat[:renderer[:formatter]] dot option as specified
141      * at <a href='http://www.graphviz.org/doc/info/command.html'>
142      * http://www.graphviz.org/doc/info/command.html</a> where "format" is specified by
143      * {@link #setFormat(String)}<p>
144      * Note that the leading ":" is required while :formatter is optional.
145      *
146      * @param renderer
147      */
148     public void setRenderer(String renderer) {
149         this.renderer = renderer;
150     }
151 
152     public String getRenderer() {
153         return renderer != null && supportsRenderer(renderer) ? renderer
154             : (requiresGdRenderer() ? ":gd" : "");
155     }
156 
157     /**
158      * If <code>true</code> then generate output of "higher quality"
159      * than the default ("lower quality").
160      * Note that the default is intended to be "lower quality",
161      * but various installations of Graphviz may have have different abilities.
162      * That is, some might not have the "lower quality" libraries and others might
163      * not have the "higher quality" libraries.
164      */
165     public void setHighQuality(boolean highQuality) {
166         if (highQuality && supportsRenderer(":cairo")) {
167             setRenderer(":cairo");
168         } else if (supportsRenderer(":gd")) {
169             setRenderer(":gd");
170         }
171     }
172 
173     /**
174      * @see #setHighQuality(boolean)
175      */
176     public boolean isHighQuality() {
177         return getRenderer().indexOf(":cairo") != -1;
178     }
179 
180     /**
181      * Returns <code>true</code> if the specified renderer is supported.
182      * See {@link #setRenderer(String)} for renderer details.
183      *
184      * @param renderer
185      * @return
186      */
187     public boolean supportsRenderer(@SuppressWarnings("hiding") String renderer) {
188         if (!exists())
189             return false;
190 
191         if (validatedRenderers.contains(renderer))
192             return true;
193 
194         if (invalidatedRenderers.contains(renderer))
195             return false;
196 
197         try {
198             String[] dotCommand = new String[] {
199                 getExe(),
200                 "-T" + getFormat() + ':'
201             };
202             Process process = Runtime.getRuntime().exec(dotCommand);
203             BufferedReader errors = new BufferedReader(new InputStreamReader(process.getErrorStream()));
204             String line;
205             while ((line = errors.readLine()) != null) {
206                 if (line.contains(getFormat() + renderer)) {
207                     validatedRenderers.add(renderer);
208                 }
209             }
210             process.waitFor();
211         } catch (Exception exc) {
212             exc.printStackTrace();
213         }
214 
215         if (!validatedRenderers.contains(renderer)) {
216             //System.err.println("\nFailed to validate " + getFormat() + " renderer '" + renderer + "'.  Reverting to detault renderer for " + getFormat() + '.');
217             invalidatedRenderers.add(renderer);
218             return false;
219         }
220 
221         return true;
222     }
223 
224     /**
225      * Returns the executable to use to run dot
226      *
227      * @return
228      */
229     private String getExe() {
230         if (dotExe == null)
231         {
232             File gv = Config.getInstance().getGraphvizDir();
233 
234             if (gv == null) {
235                 // default to finding dot in the PATH
236                 dotExe = "dot";
237             } else {
238                 // pull dot from the Graphviz bin directory specified
239                 dotExe = new File(new File(gv, "bin"), "dot").toString();
240             }
241         }
242 
243         return dotExe;
244     }
245 
246     /**
247      * Using the specified .dot file generates an image returning the image's image map.
248      */
249     public String generateDiagram(File dotFile, File diagramFile) throws DotFailure {
250         StringBuilder mapBuffer = new StringBuilder(1024);
251 
252         BufferedReader mapReader = null;
253         // this one is for executing.  it can (hopefully) deal with funky things in filenames.
254         String[] dotCommand = new String[] {
255             getExe(),
256             "-T" + getFormat() + getRenderer(),
257             dotFile.toString(),
258             "-o" + diagramFile,
259             "-Tcmapx"
260         };
261         // this one is for display purposes ONLY.
262         String commandLine = getDisplayableCommand(dotCommand);
263 
264         try {
265             Process process = Runtime.getRuntime().exec(dotCommand);
266             new ProcessOutputReader(commandLine, process.getErrorStream()).start();
267             mapReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
268             String line;
269             while ((line = mapReader.readLine()) != null) {
270                 mapBuffer.append(line);
271                 mapBuffer.append(lineSeparator);
272             }
273             int rc = process.waitFor();
274             if (rc != 0)
275                 throw new DotFailure("'" + commandLine + "' failed with return code " + rc);
276             if (!diagramFile.exists())
277                 throw new DotFailure("'" + commandLine + "' failed to create output file");
278 
279             // dot generates post-HTML 4.0.1 output...convert trailing />'s to >'s
280             return mapBuffer.toString().replace("/>", ">");
281         } catch (InterruptedException interrupted) {
282             throw new RuntimeException(interrupted);
283         } catch (DotFailure failed) {
284             diagramFile.delete();
285             throw failed;
286         } catch (IOException failed) {
287             diagramFile.delete();
288             throw new DotFailure("'" + commandLine + "' failed with exception " + failed);
289         } finally {
290             if (mapReader != null) {
291                 try {
292                     mapReader.close();
293                 } catch (IOException ignore) {}
294             }
295         }
296     }
297 
298     public class DotFailure extends IOException {
299         private static final long serialVersionUID = 3833743270181351987L;
300 
301         public DotFailure(String msg) {
302             super(msg);
303         }
304     }
305 
306     private static String getDisplayableCommand(String[] command) {
307         StringBuilder displayable = new StringBuilder();
308         for (int i = 0; i < command.length; ++i) {
309             displayable.append(command[i]);
310             if (i + 1 < command.length)
311                 displayable.append(' ');
312         }
313         return displayable.toString();
314     }
315 
316     private static class ProcessOutputReader extends Thread {
317         private final BufferedReader processReader;
318         private final String command;
319 
320         ProcessOutputReader(String command, InputStream processStream) {
321             processReader = new BufferedReader(new InputStreamReader(processStream));
322             this.command = command;
323             setDaemon(true);
324         }
325 
326         @Override
327         public void run() {
328             try {
329                 String line;
330                 while ((line = processReader.readLine()) != null) {
331                     // don't report port id unrecognized or unrecognized port
332                     if (line.indexOf("unrecognized") == -1 && line.indexOf("port") == -1)
333                         System.err.println(command + ": " + line);
334                 }
335             } catch (IOException ioException) {
336                 ioException.printStackTrace();
337             } finally {
338                 try {
339                     processReader.close();
340                 } catch (Exception exc) {
341                     exc.printStackTrace(); // shouldn't ever get here...but...
342                 }
343             }
344         }
345     }
346 }