From c824726a40d6fcbe91946d241b5d6bcf8da48019 Mon Sep 17 00:00:00 2001 From: Ronald Haentjens Dekker Date: Wed, 6 May 2015 15:39:44 +0200 Subject: [PATCH] Added VariantGraphSVGMessageBodyWriter. --- .../collatex/http/CollateApplication.java | 32 ++++++ .../io/VariantGraphSVGMessageBodyWriter.java | 103 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 collatex-servlet/src/main/java/eu/interedition/collatex/io/VariantGraphSVGMessageBodyWriter.java diff --git a/collatex-servlet/src/main/java/eu/interedition/collatex/http/CollateApplication.java b/collatex-servlet/src/main/java/eu/interedition/collatex/http/CollateApplication.java index df07eae7c..b8ae7c322 100644 --- a/collatex-servlet/src/main/java/eu/interedition/collatex/http/CollateApplication.java +++ b/collatex-servlet/src/main/java/eu/interedition/collatex/http/CollateApplication.java @@ -3,15 +3,46 @@ import eu.interedition.collatex.io.IOExceptionMapper; import eu.interedition.collatex.io.SimpleCollationJSONMessageBodyReader; import eu.interedition.collatex.io.VariantGraphJSONMessageBodyWriter; +import eu.interedition.collatex.io.VariantGraphSVGMessageBodyWriter; import javax.ws.rs.core.Application; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.Charset; import java.util.HashSet; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Created by ronald on 5/3/15. */ public class CollateApplication extends Application { + private static final Logger LOG = Logger.getLogger(CollateApplication.class.getName()); + + private static String detectDotPath() { + for (String detectionCommand : new String[] { "which dot", "where dot.exe" }) { + try { + + final Process process = Runtime.getRuntime().exec(detectionCommand); + try (BufferedReader processReader = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { + final CompletableFuture> path = CompletableFuture.supplyAsync(() -> processReader.lines() + .map(String::trim) + .filter(l -> l.toLowerCase().contains("dot")) + .findFirst()); + process.waitFor(); + final String dotPath = path.get().get(); + LOG.info(() -> "Detected GraphViz' dot at '" + dotPath + "'"); + return dotPath; + } + } catch (Throwable t) { + LOG.log(Level.FINE, detectionCommand, t); + } + } + return null; + } @Override public Set> getClasses() { @@ -26,6 +57,7 @@ public Set> getClasses() { public Set getSingletons() { Set singletons = new HashSet<>(); singletons.add(new CollateResource("", 10, 0)); + singletons.add(new VariantGraphSVGMessageBodyWriter(detectDotPath())); return singletons; } } diff --git a/collatex-servlet/src/main/java/eu/interedition/collatex/io/VariantGraphSVGMessageBodyWriter.java b/collatex-servlet/src/main/java/eu/interedition/collatex/io/VariantGraphSVGMessageBodyWriter.java new file mode 100644 index 000000000..e1c12976f --- /dev/null +++ b/collatex-servlet/src/main/java/eu/interedition/collatex/io/VariantGraphSVGMessageBodyWriter.java @@ -0,0 +1,103 @@ +package eu.interedition.collatex.io; + +import eu.interedition.collatex.VariantGraph; +import eu.interedition.collatex.simple.SimpleVariantGraphSerializer; + +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.concurrent.*; + +/** + * Created by ronald on 5/6/15. + * Based on code written by Gregor Middell. + * Class is intended to be used as a singleton + */ +@Provider +@Produces("image/svg+xml") +public class VariantGraphSVGMessageBodyWriter implements MessageBodyWriter { + private String dotPath; + private final ExecutorService processThreads = Executors.newCachedThreadPool(); + + public VariantGraphSVGMessageBodyWriter(String dotPath) { + this.dotPath = dotPath; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return VariantGraph.class.isAssignableFrom(type); + } + + @Override + public long getSize(VariantGraph variantGraph, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return 0; + } + + @Override + public void writeTo(VariantGraph graph, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + if (dotPath == null) { + throw new WebApplicationException(Response.Status.NO_CONTENT); + } else { + final StringWriter dot = new StringWriter(); + new SimpleVariantGraphSerializer(graph).toDot(dot); + + final Process dotProc = new ProcessBuilder(dotPath, "-Grankdir=LR", "-Gid=VariantGraph", "-Tsvg").start(); + final StringWriter errors = new StringWriter(); + try { + CompletableFuture.allOf( + CompletableFuture.runAsync(() -> { + final char[] buf = new char[8192]; + try (final Reader errorStream = new InputStreamReader(dotProc.getErrorStream())) { + int len; + while ((len = errorStream.read(buf)) >= 0) { + errors.write(buf, 0, len); + } + } catch (IOException e) { + throw new CompletionException(e); + } + }, processThreads), + CompletableFuture.runAsync(() -> { + try (final Writer dotProcStream = new OutputStreamWriter(dotProc.getOutputStream(), "UTF-8")) { + dotProcStream.write(dot.toString()); + } catch (IOException e) { + throw new CompletionException(e); + } + }, processThreads), + CompletableFuture.runAsync(() -> { + httpHeaders.add(HttpHeaders.CONTENT_TYPE, "image/svg+xml"); + final byte[] buf = new byte[8192]; + try (final InputStream in = dotProc.getInputStream(); final OutputStream out = entityStream) { + int len; + while ((len = in.read(buf)) >= 0) { + out.write(buf, 0, len); + } + } catch (IOException e) { + throw new CompletionException(e); + } + }, processThreads), + CompletableFuture.runAsync(() -> { + try { + if (dotProc.waitFor() != 0) { + throw new CompletionException(new IllegalStateException(errors.toString())); + } + } catch (InterruptedException e) { + throw new CompletionException(e); + } + }, processThreads) + ).exceptionally(t -> { + throw new WebApplicationException(t); + }).get(); + } catch(InterruptedException|ExecutionException e) { + throw new WebApplicationException(e); + } + } + } +}