1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package com.xpn.xwiki.plugin.graphviz

File GraphVizPlugin.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart0.png
83% of files have more coverage

Code metrics

26
97
21
2
457
228
41
0.42
4.62
10.5
1.95

Classes

Class Line # Actions
GraphVizPlugin 56 93 0% 38 138
0.00%
GraphVizPlugin.Hangcheck 401 4 0% 3 6
0.00%
 

Contributing tests

No tests hitting this source file were found.

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software 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 software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package com.xpn.xwiki.plugin.graphviz;
21   
22    import java.io.File;
23    import java.io.IOException;
24    import java.io.OutputStream;
25    import java.lang.reflect.Type;
26    import java.util.regex.Matcher;
27    import java.util.regex.Pattern;
28   
29    import org.apache.commons.io.FileUtils;
30    import org.apache.commons.io.IOUtils;
31    import org.apache.commons.lang3.StringUtils;
32    import org.slf4j.Logger;
33    import org.slf4j.LoggerFactory;
34    import org.xwiki.environment.Environment;
35   
36    import com.xpn.xwiki.XWiki;
37    import com.xpn.xwiki.XWikiContext;
38    import com.xpn.xwiki.api.Api;
39    import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
40    import com.xpn.xwiki.plugin.XWikiPluginInterface;
41    import com.xpn.xwiki.web.Utils;
42    import com.xpn.xwiki.web.XWikiResponse;
43   
44    /**
45    * Plugin which wraps the <a href="http://graphviz.org/">GraphViz</a> <tt>dot</tt> executable; transforming dot source
46    * files (representing graphs) into images, image maps, or other output formats supported by GraphViz.
47    * <p>
48    * See http://www.graphviz.org/doc/info/lang.html for the dot language specification. See
49    * http://www.graphviz.org/doc/info/output.html for the possible output formats
50    * </p>
51    *
52    * @deprecated the plugin technology is deprecated
53    * @version $Id: 7aa5720e2c6bf1a0c71b8be530a4f98699e49c3e $
54    */
55    @Deprecated
 
56    public class GraphVizPlugin extends XWikiDefaultPlugin
57    {
58    /** Detects HTML character references produced by the {@link com.xpn.xwiki.render.filter.EscapeFilter}. */
59    private static final Pattern HTML_ESCAPE_PATTERN = Pattern.compile("&#([0-9]++);");
60   
61    /** Logging helper object. */
62    private static final Logger LOGGER = LoggerFactory.getLogger(com.xpn.xwiki.plugin.graphviz.GraphVizPlugin.class);
63   
64    /** The default output format to use: PNG image. */
65    private static final String DEFAULT_FORMAT = "png";
66   
67    /** The default engine to use: dot. */
68    private static final String DOT_ENGINE = "dot";
69   
70    /** An alternative engine to use: neato. */
71    private static final String NEATO_ENGINE = "neato";
72   
73    /** Temporary directory where generated files are stored. */
74    private File tempDir;
75   
76    /** The path to the dot executable. */
77    private String dotPath;
78   
79    /** The path to the neato executable. */
80    private String neatoPath;
81   
82    /**
83    * Used to get the temporary directory.
84    */
85    private Environment environment = Utils.getComponent((Type) Environment.class);
86   
87    /**
88    * The mandatory plugin constructor, this is the method called (through reflection) by the plugin manager.
89    *
90    * @param name the plugin name
91    * @param className the name of this class, ignored
92    * @param context the current request context
93    */
 
94  0 toggle public GraphVizPlugin(String name, String className, XWikiContext context)
95    {
96  0 super(name, className, context);
97  0 init(context);
98    }
99   
 
100  0 toggle @Override
101    public String getName()
102    {
103  0 return "graphviz";
104    }
105   
 
106  0 toggle @Override
107    public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
108    {
109  0 return new GraphVizPluginApi((GraphVizPlugin) plugin, context);
110    }
111   
 
112  0 toggle @Override
113    public void flushCache()
114    {
115  0 try {
116  0 FileUtils.cleanDirectory(this.tempDir);
117    } catch (Exception e) {
118    // Public APIs shouldn't throw errors; this shouldn't happen anyway
119    }
120    }
121   
 
122  0 toggle @Override
123    public void init(XWikiContext context)
124    {
125  0 super.init(context);
126   
127  0 File dir = this.environment.getTemporaryDirectory();
128  0 this.tempDir = new File(dir, this.getName());
129  0 try {
130  0 this.tempDir.mkdirs();
131    } catch (Exception ex) {
132  0 LOGGER.warn("Failed to create temporary file", ex);
133    }
134   
135  0 this.dotPath = context.getWiki().Param("xwiki.plugin.graphviz.dotpath", DOT_ENGINE);
136  0 if (!this.dotPath.equals(DOT_ENGINE)) {
137  0 try {
138  0 File dfile = new File(this.dotPath);
139  0 if (!dfile.exists()) {
140  0 LOGGER.error("Cannot find graphiz dot program at " + this.dotPath);
141    }
142    } catch (Exception e) {
143    // Access restrictions, not important
144    }
145    }
146   
147  0 this.neatoPath = context.getWiki().Param("xwiki.plugin.graphviz.neatopath", NEATO_ENGINE);
148  0 if (!this.neatoPath.equals(NEATO_ENGINE)) {
149  0 try {
150  0 File dfile = new File(this.neatoPath);
151  0 if (!dfile.exists()) {
152  0 LOGGER.error("Cannot find graphiz neato program at " + this.neatoPath);
153    }
154    } catch (Exception e) {
155    // Access restrictions, not important
156    }
157    }
158   
159    }
160   
161    /**
162    * Executes GraphViz and returns the URL for the produced file, a PNG image.
163    *
164    * @param content the dot source
165    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
166    * @param context the current request context
167    * @return the URL which can be used to access the generated image
168    * @throws IOException if writing the input or output files to the disk fails
169    * @see #getDotResultURL(String, boolean, String, XWikiContext) allows to chose another output format instead of PNG
170    */
 
171  0 toggle public String getDotImageURL(String content, boolean dot, XWikiContext context) throws IOException
172    {
173  0 return getDotResultURL(content, dot, DEFAULT_FORMAT, context);
174    }
175   
176    /**
177    * Executes GraphViz and returns the URL for the produced file.
178    *
179    * @param content the dot source code
180    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
181    * @param outputFormat the output format to use
182    * @param context the current request context
183    * @return the URL which can be used to access the result
184    * @throws IOException if writing the input or output files to the disk fails
185    * @see #getDotImageURL(String, boolean, XWikiContext) if the output should be a simple PNG image
186    */
 
187  0 toggle public String getDotResultURL(String content, boolean dot, String outputFormat, XWikiContext context)
188    throws IOException
189    {
190  0 String filename = writeDotImage(content, outputFormat, dot);
191  0 return context.getDoc().getAttachmentURL(filename, DOT_ENGINE, context);
192    }
193   
194    /**
195    * Executes GraphViz and return the content of the resulting image (PNG format).
196    *
197    * @param content the dot source code
198    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
199    * @return the content of the generated image
200    * @throws IOException if writing the input or output files to the disk fails
201    */
 
202  0 toggle public byte[] getDotImage(String content, boolean dot) throws IOException
203    {
204  0 return getDotImage(content, DEFAULT_FORMAT, dot);
205    }
206   
207    /**
208    * Executes GraphViz and return the content of the resulting image (PNG format).
209    *
210    * @param content the dot source code
211    * @param extension the output file extension
212    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
213    * @return the content of the generated file
214    * @throws IOException if writing the input or output files to the disk fails
215    */
 
216  0 toggle public byte[] getDotImage(String content, String extension, boolean dot) throws IOException
217    {
218  0 int hashCode = Math.abs(content.hashCode());
219  0 return getDotImage(hashCode, content, extension, dot);
220    }
221   
222    /**
223    * Executes GraphViz, writes the resulting image (PNG format) in a temporary file on disk, and returns the filename
224    * which can be later used in {@link #outputDotImageFromFile(String, XWikiContext)}.
225    *
226    * @param content the dot source code
227    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
228    * @return the name of the file where the generated output is stored
229    * @throws IOException if writing the input or output files to the disk fails
230    */
 
231  0 toggle public String writeDotImage(String content, boolean dot) throws IOException
232    {
233  0 return writeDotImage(content, DEFAULT_FORMAT, dot);
234    }
235   
236    /**
237    * Executes GraphViz, writes the resulting image (in the requested format) in a temporary file on disk, and returns
238    * the filename which can be later used in {@link #outputDotImageFromFile(String, XWikiContext)}.
239    *
240    * @param content the dot source code
241    * @param extension the output file extension
242    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
243    * @return the name of the file where the generated output is stored
244    * @throws IOException if writing the input or output files to the disk fails
245    */
 
246  0 toggle public String writeDotImage(String content, String extension, boolean dot) throws IOException
247    {
248  0 int hashCode = Math.abs(content.hashCode());
249  0 getDotImage(hashCode, content, extension, dot);
250  0 String name = (dot ? DOT_ENGINE : NEATO_ENGINE) + '-';
251  0 return name + hashCode + "." + extension;
252    }
253   
254    /**
255    * Executes GraphViz and writes the resulting image (PNG format) into the response.
256    *
257    * @param content the dot source code
258    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
259    * @param context the current request context
260    * @throws IOException if writing the input or output files to the disk fails, or if writing the response body fails
261    */
 
262  0 toggle public void outputDotImage(String content, boolean dot, XWikiContext context) throws IOException
263    {
264  0 outputDotImage(content, DEFAULT_FORMAT, dot, context);
265    }
266   
267    /**
268    * Executes GraphViz and writes the resulting image (in the requested format) into the response.
269    *
270    * @param content the dot source code
271    * @param extension the output file extension
272    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
273    * @param context the current request context
274    * @throws IOException if writing the input or output files to the disk fails, or if writing the response body fails
275    */
 
276  0 toggle public void outputDotImage(String content, String extension, boolean dot, XWikiContext context) throws IOException
277    {
278  0 byte[] dotbytes = getDotImage(content, extension, dot);
279  0 XWikiResponse response = context.getResponse();
280  0 context.setFinished(true);
281  0 response.setContentLength(dotbytes.length);
282  0 response.setContentType(context.getEngineContext().getMimeType("toto." + extension));
283  0 OutputStream os = response.getOutputStream();
284  0 os.write(dotbytes);
285  0 os.flush();
286    }
287   
288    /**
289    * Writes an already generated result from the temporary file into the response.
290    *
291    * @param filename the name of the temporary file, previously returned by
292    * {@link #writeDotImage(String, String, boolean)}
293    * @param context the current request context
294    * @throws IOException if reading the file from the disk fails, or if writing the response body fails
295    */
 
296  0 toggle public void outputDotImageFromFile(String filename, XWikiContext context) throws IOException
297    {
298  0 File ofile = getTempFile(filename);
299  0 byte[] dotbytes = readDotImage(ofile);
300  0 XWikiResponse response = context.getResponse();
301  0 context.setFinished(true);
302  0 response.setDateHeader("Last-Modified", ofile.lastModified());
303  0 response.setContentLength(dotbytes.length);
304  0 response.setContentType(context.getEngineContext().getMimeType(filename));
305  0 OutputStream os = response.getOutputStream();
306  0 os.write(dotbytes);
307    }
308   
309    /**
310    * Executes GraphViz, writes the resulting image (in the requested format) in a temporary file on disk, and returns
311    * the generated content from that file.
312    *
313    * @param hashCode the hascode of the content, to be used as the temporary file name
314    * @param content the dot source code
315    * @param extension the output file extension
316    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
317    * @return the content of the generated file
318    * @throws IOException if writing the input or output files to the disk fails, or if writing the response body fails
319    */
 
320  0 toggle private byte[] getDotImage(int hashCode, String content, String extension, boolean dot) throws IOException
321    {
322  0 File dfile = getTempFile(hashCode, "input.dot", dot);
323  0 if (!dfile.exists()) {
324  0 FileUtils.write(dfile, undoEscapeFilter(content), XWiki.DEFAULT_ENCODING);
325    }
326   
327  0 File ofile = getTempFile(hashCode, extension, dot);
328  0 if (!ofile.exists()) {
329  0 Runtime rt = Runtime.getRuntime();
330  0 String[] command = new String[5];
331  0 command[0] = dot ? this.dotPath : this.neatoPath;
332  0 command[1] = "-T" + extension;
333  0 command[2] = dfile.getAbsolutePath();
334  0 command[3] = "-o";
335  0 command[4] = ofile.getAbsolutePath();
336  0 Process p = rt.exec(command);
337  0 int exitValue = -1;
338  0 final Thread thisThread = Thread.currentThread();
339  0 Thread t = new Thread(new Hangcheck(thisThread), "dot-hangcheck");
340  0 t.run();
341  0 try {
342  0 exitValue = p.waitFor();
343  0 t.interrupt();
344    } catch (InterruptedException ex) {
345  0 p.destroy();
346  0 LOGGER.error("Timeout while generating image from dot", ex);
347    }
348   
349  0 if (exitValue != 0) {
350  0 LOGGER.error("Error while generating image from dot: "
351    + IOUtils.toString(p.getErrorStream(), XWiki.DEFAULT_ENCODING));
352    }
353    }
354  0 return FileUtils.readFileToByteArray(ofile);
355    }
356   
357    /**
358    * Get the contents of a previously generated temporary file.
359    *
360    * @param ofile the file to read
361    * @return the content found inside the file, if any
362    * @throws IOException when reading the file fails
363    */
 
364  0 toggle private byte[] readDotImage(File ofile) throws IOException
365    {
366  0 return FileUtils.readFileToByteArray(ofile);
367    }
368   
369    /**
370    * Return the temporary disk file corresponding to the given parameters.
371    *
372    * @param hashcode the hashcode of the dot content, used as the main part for the filename
373    * @param extension the output file extension
374    * @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
375    * @return the corresponding File
376    */
 
377  0 toggle private File getTempFile(int hashcode, String extension, boolean dot)
378    {
379  0 String name = (dot ? DOT_ENGINE : NEATO_ENGINE) + '-';
380  0 return getTempFile(name + hashcode + '.' + extension);
381    }
382   
383    /**
384    * Return the temporary disk file corresponding to the given filename.
385    *
386    * @param filename the filename to look for
387    * @return the corresponding File
388    */
 
389  0 toggle private File getTempFile(String filename)
390    {
391  0 return new File(this.tempDir, filename);
392    }
393   
394    /**
395    * Hangcheck runnable, which interrupts the main thread after 10 seconds of waiting for the conversion to end. If
396    * the conversion ends normally before the 10 seconds timeout expires, then this runnable should be terminated by
397    * {@link Thread#interrupt() interrupting it}.
398    *
399    * @version $Id: 7aa5720e2c6bf1a0c71b8be530a4f98699e49c3e $
400    */
 
401    private static class Hangcheck implements Runnable
402    {
403    /** The main thread that should be interrupted if the timeout expires. */
404    private Thread converterThread;
405   
406    /**
407    * Simple constructor which specifies the thread to monitor.
408    *
409    * @param converterThread the thread to monitor
410    */
 
411  0 toggle Hangcheck(Thread converterThread)
412    {
413  0 this.converterThread = converterThread;
414    }
415   
 
416  0 toggle @Override
417    public void run()
418    {
419  0 try {
420  0 Thread.sleep(10000);
421  0 this.converterThread.interrupt();
422    } catch (InterruptedException ex) {
423    // Expected result if the dot process terminates on time
424    }
425    }
426    }
427   
428    /**
429    * When rendering using Radeox, {@link com.xpn.xwiki.render.filter.EscapeFilter} replaces all instances of
430    * {@code \\char} backslash escapes with a HTML character reference. Unfortunately this also happens for GraphViz
431    * content, which isn't right, since some backslash escapes are valid GraphViz syntax. This method undoes this kind
432    * of escaping to preserve node and edge label formatting, if the character reference is an ASCII character.
433    *
434    * @param escapedContent the macro content, already filtered by Radeox and possibly containing broken backslash
435    * escapes
436    * @return the content with HTML character references replaced with backslash escapes
437    */
 
438  0 toggle private String undoEscapeFilter(String escapedContent)
439    {
440  0 if (StringUtils.isNotEmpty(escapedContent)) {
441  0 Matcher matcher = HTML_ESCAPE_PATTERN.matcher(escapedContent);
442  0 StringBuffer result = new StringBuffer(escapedContent.length());
443  0 while (matcher.find()) {
444  0 int codepoint = Integer.valueOf(matcher.group(1));
445  0 if (codepoint >= 65 && codepoint <= 122) {
446  0 matcher.appendReplacement(result, new String(new int[] { 92, 92, codepoint }, 0, 3));
447    } else {
448  0 matcher.appendReplacement(result, "$0");
449    }
450    }
451  0 matcher.appendTail(result);
452  0 return result.toString();
453    } else {
454  0 return "";
455    }
456    }
457    }