/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xpn.xwiki.plugin.graphviz;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.environment.Environment;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.api.Api;
import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
import com.xpn.xwiki.plugin.XWikiPluginInterface;
import com.xpn.xwiki.web.Utils;
import com.xpn.xwiki.web.XWikiResponse;
/**
* Plugin which wraps the <a href="http://graphviz.org/">GraphViz</a> <tt>dot</tt> executable; transforming dot source
* files (representing graphs) into images, image maps, or other output formats supported by GraphViz.
* <p>
* See http://www.graphviz.org/doc/info/lang.html for the dot language specification. See
* http://www.graphviz.org/doc/info/output.html for the possible output formats
* </p>
*
* @deprecated the plugin technology is deprecated
* @version $Id: 7aa5720e2c6bf1a0c71b8be530a4f98699e49c3e $
*/
@Deprecated
public class GraphVizPlugin extends XWikiDefaultPlugin
{
/** Detects HTML character references produced by the {@link com.xpn.xwiki.render.filter.EscapeFilter}. */
private static final Pattern HTML_ESCAPE_PATTERN = Pattern.compile("([0-9]++);");
/** Logging helper object. */
private static final Logger LOGGER = LoggerFactory.getLogger(com.xpn.xwiki.plugin.graphviz.GraphVizPlugin.class);
/** The default output format to use: PNG image. */
private static final String DEFAULT_FORMAT = "png";
/** The default engine to use: dot. */
private static final String DOT_ENGINE = "dot";
/** An alternative engine to use: neato. */
private static final String NEATO_ENGINE = "neato";
/** Temporary directory where generated files are stored. */
private File tempDir;
/** The path to the dot executable. */
private String dotPath;
/** The path to the neato executable. */
private String neatoPath;
/**
* Used to get the temporary directory.
*/
private Environment environment = Utils.getComponent((Type) Environment.class);
/**
* The mandatory plugin constructor, this is the method called (through reflection) by the plugin manager.
*
* @param name the plugin name
* @param className the name of this class, ignored
* @param context the current request context
*/
public GraphVizPlugin(String name, String className, XWikiContext context)
{
super(name, className, context);
init(context);
}
@Override
public String getName()
{
return "graphviz";
}
@Override
public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
{
return new GraphVizPluginApi((GraphVizPlugin) plugin, context);
}
@Override
public void flushCache()
{
try {
FileUtils.cleanDirectory(this.tempDir);
} catch (Exception e) {
// Public APIs shouldn't throw errors; this shouldn't happen anyway
}
}
@Override
public void init(XWikiContext context)
{
super.init(context);
File dir = this.environment.getTemporaryDirectory();
this.tempDir = new File(dir, this.getName());
try {
this.tempDir.mkdirs();
} catch (Exception ex) {
LOGGER.warn("Failed to create temporary file", ex);
}
this.dotPath = context.getWiki().Param("xwiki.plugin.graphviz.dotpath", DOT_ENGINE);
if (!this.dotPath.equals(DOT_ENGINE)) {
try {
File dfile = new File(this.dotPath);
if (!dfile.exists()) {
LOGGER.error("Cannot find graphiz dot program at " + this.dotPath);
}
} catch (Exception e) {
// Access restrictions, not important
}
}
this.neatoPath = context.getWiki().Param("xwiki.plugin.graphviz.neatopath", NEATO_ENGINE);
if (!this.neatoPath.equals(NEATO_ENGINE)) {
try {
File dfile = new File(this.neatoPath);
if (!dfile.exists()) {
LOGGER.error("Cannot find graphiz neato program at " + this.neatoPath);
}
} catch (Exception e) {
// Access restrictions, not important
}
}
}
/**
* Executes GraphViz and returns the URL for the produced file, a PNG image.
*
* @param content the dot source
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @param context the current request context
* @return the URL which can be used to access the generated image
* @throws IOException if writing the input or output files to the disk fails
* @see #getDotResultURL(String, boolean, String, XWikiContext) allows to chose another output format instead of PNG
*/
public String getDotImageURL(String content, boolean dot, XWikiContext context) throws IOException
{
return getDotResultURL(content, dot, DEFAULT_FORMAT, context);
}
/**
* Executes GraphViz and returns the URL for the produced file.
*
* @param content the dot source code
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @param outputFormat the output format to use
* @param context the current request context
* @return the URL which can be used to access the result
* @throws IOException if writing the input or output files to the disk fails
* @see #getDotImageURL(String, boolean, XWikiContext) if the output should be a simple PNG image
*/
public String getDotResultURL(String content, boolean dot, String outputFormat, XWikiContext context)
throws IOException
{
String filename = writeDotImage(content, outputFormat, dot);
return context.getDoc().getAttachmentURL(filename, DOT_ENGINE, context);
}
/**
* Executes GraphViz and return the content of the resulting image (PNG format).
*
* @param content the dot source code
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @return the content of the generated image
* @throws IOException if writing the input or output files to the disk fails
*/
public byte[] getDotImage(String content, boolean dot) throws IOException
{
return getDotImage(content, DEFAULT_FORMAT, dot);
}
/**
* Executes GraphViz and return the content of the resulting image (PNG format).
*
* @param content the dot source code
* @param extension the output file extension
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @return the content of the generated file
* @throws IOException if writing the input or output files to the disk fails
*/
public byte[] getDotImage(String content, String extension, boolean dot) throws IOException
{
int hashCode = Math.abs(content.hashCode());
return getDotImage(hashCode, content, extension, dot);
}
/**
* Executes GraphViz, writes the resulting image (PNG format) in a temporary file on disk, and returns the filename
* which can be later used in {@link #outputDotImageFromFile(String, XWikiContext)}.
*
* @param content the dot source code
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @return the name of the file where the generated output is stored
* @throws IOException if writing the input or output files to the disk fails
*/
public String writeDotImage(String content, boolean dot) throws IOException
{
return writeDotImage(content, DEFAULT_FORMAT, dot);
}
/**
* Executes GraphViz, writes the resulting image (in the requested format) in a temporary file on disk, and returns
* the filename which can be later used in {@link #outputDotImageFromFile(String, XWikiContext)}.
*
* @param content the dot source code
* @param extension the output file extension
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @return the name of the file where the generated output is stored
* @throws IOException if writing the input or output files to the disk fails
*/
public String writeDotImage(String content, String extension, boolean dot) throws IOException
{
int hashCode = Math.abs(content.hashCode());
getDotImage(hashCode, content, extension, dot);
String name = (dot ? DOT_ENGINE : NEATO_ENGINE) + '-';
return name + hashCode + "." + extension;
}
/**
* Executes GraphViz and writes the resulting image (PNG format) into the response.
*
* @param content the dot source code
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @param context the current request context
* @throws IOException if writing the input or output files to the disk fails, or if writing the response body fails
*/
public void outputDotImage(String content, boolean dot, XWikiContext context) throws IOException
{
outputDotImage(content, DEFAULT_FORMAT, dot, context);
}
/**
* Executes GraphViz and writes the resulting image (in the requested format) into the response.
*
* @param content the dot source code
* @param extension the output file extension
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @param context the current request context
* @throws IOException if writing the input or output files to the disk fails, or if writing the response body fails
*/
public void outputDotImage(String content, String extension, boolean dot, XWikiContext context) throws IOException
{
byte[] dotbytes = getDotImage(content, extension, dot);
XWikiResponse response = context.getResponse();
context.setFinished(true);
response.setContentLength(dotbytes.length);
response.setContentType(context.getEngineContext().getMimeType("toto." + extension));
OutputStream os = response.getOutputStream();
os.write(dotbytes);
os.flush();
}
/**
* Writes an already generated result from the temporary file into the response.
*
* @param filename the name of the temporary file, previously returned by
* {@link #writeDotImage(String, String, boolean)}
* @param context the current request context
* @throws IOException if reading the file from the disk fails, or if writing the response body fails
*/
public void outputDotImageFromFile(String filename, XWikiContext context) throws IOException
{
File ofile = getTempFile(filename);
byte[] dotbytes = readDotImage(ofile);
XWikiResponse response = context.getResponse();
context.setFinished(true);
response.setDateHeader("Last-Modified", ofile.lastModified());
response.setContentLength(dotbytes.length);
response.setContentType(context.getEngineContext().getMimeType(filename));
OutputStream os = response.getOutputStream();
os.write(dotbytes);
}
/**
* Executes GraphViz, writes the resulting image (in the requested format) in a temporary file on disk, and returns
* the generated content from that file.
*
* @param hashCode the hascode of the content, to be used as the temporary file name
* @param content the dot source code
* @param extension the output file extension
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @return the content of the generated file
* @throws IOException if writing the input or output files to the disk fails, or if writing the response body fails
*/
private byte[] getDotImage(int hashCode, String content, String extension, boolean dot) throws IOException
{
File dfile = getTempFile(hashCode, "input.dot", dot);
if (!dfile.exists()) {
FileUtils.write(dfile, undoEscapeFilter(content), XWiki.DEFAULT_ENCODING);
}
File ofile = getTempFile(hashCode, extension, dot);
if (!ofile.exists()) {
Runtime rt = Runtime.getRuntime();
String[] command = new String[5];
command[0] = dot ? this.dotPath : this.neatoPath;
command[1] = "-T" + extension;
command[2] = dfile.getAbsolutePath();
command[3] = "-o";
command[4] = ofile.getAbsolutePath();
Process p = rt.exec(command);
int exitValue = -1;
final Thread thisThread = Thread.currentThread();
Thread t = new Thread(new Hangcheck(thisThread), "dot-hangcheck");
t.run();
try {
exitValue = p.waitFor();
t.interrupt();
} catch (InterruptedException ex) {
p.destroy();
LOGGER.error("Timeout while generating image from dot", ex);
}
if (exitValue != 0) {
LOGGER.error("Error while generating image from dot: "
+ IOUtils.toString(p.getErrorStream(), XWiki.DEFAULT_ENCODING));
}
}
return FileUtils.readFileToByteArray(ofile);
}
/**
* Get the contents of a previously generated temporary file.
*
* @param ofile the file to read
* @return the content found inside the file, if any
* @throws IOException when reading the file fails
*/
private byte[] readDotImage(File ofile) throws IOException
{
return FileUtils.readFileToByteArray(ofile);
}
/**
* Return the temporary disk file corresponding to the given parameters.
*
* @param hashcode the hashcode of the dot content, used as the main part for the filename
* @param extension the output file extension
* @param dot which engine to execute: {@code dot} if {@code true}, {@code neato} if {@code false}
* @return the corresponding File
*/
private File getTempFile(int hashcode, String extension, boolean dot)
{
String name = (dot ? DOT_ENGINE : NEATO_ENGINE) + '-';
return getTempFile(name + hashcode + '.' + extension);
}
/**
* Return the temporary disk file corresponding to the given filename.
*
* @param filename the filename to look for
* @return the corresponding File
*/
private File getTempFile(String filename)
{
return new File(this.tempDir, filename);
}
/**
* Hangcheck runnable, which interrupts the main thread after 10 seconds of waiting for the conversion to end. If
* the conversion ends normally before the 10 seconds timeout expires, then this runnable should be terminated by
* {@link Thread#interrupt() interrupting it}.
*
* @version $Id: 7aa5720e2c6bf1a0c71b8be530a4f98699e49c3e $
*/
private static class Hangcheck implements Runnable
{
/** The main thread that should be interrupted if the timeout expires. */
private Thread converterThread;
/**
* Simple constructor which specifies the thread to monitor.
*
* @param converterThread the thread to monitor
*/
Hangcheck(Thread converterThread)
{
this.converterThread = converterThread;
}
@Override
public void run()
{
try {
Thread.sleep(10000);
this.converterThread.interrupt();
} catch (InterruptedException ex) {
// Expected result if the dot process terminates on time
}
}
}
/**
* When rendering using Radeox, {@link com.xpn.xwiki.render.filter.EscapeFilter} replaces all instances of
* {@code \\char} backslash escapes with a HTML character reference. Unfortunately this also happens for GraphViz
* content, which isn't right, since some backslash escapes are valid GraphViz syntax. This method undoes this kind
* of escaping to preserve node and edge label formatting, if the character reference is an ASCII character.
*
* @param escapedContent the macro content, already filtered by Radeox and possibly containing broken backslash
* escapes
* @return the content with HTML character references replaced with backslash escapes
*/
private String undoEscapeFilter(String escapedContent)
{
if (StringUtils.isNotEmpty(escapedContent)) {
Matcher matcher = HTML_ESCAPE_PATTERN.matcher(escapedContent);
StringBuffer result = new StringBuffer(escapedContent.length());
while (matcher.find()) {
int codepoint = Integer.valueOf(matcher.group(1));
if (codepoint >= 65 && codepoint <= 122) {
matcher.appendReplacement(result, new String(new int[] { 92, 92, codepoint }, 0, 3));
} else {
matcher.appendReplacement(result, "$0");
}
}
matcher.appendTail(result);
return result.toString();
} else {
return "";
}
}
}