diff --git a/src/main/java/info/textgrid/services/aggregator/html/HTML.java b/src/main/java/info/textgrid/services/aggregator/html/HTML.java index a7eaf595d87a8fe157f0af044dc2b3c22766c0e5..881204fb5b1058dae53ba140b000aa6a59030a7f 100644 --- a/src/main/java/info/textgrid/services/aggregator/html/HTML.java +++ b/src/main/java/info/textgrid/services/aggregator/html/HTML.java @@ -10,6 +10,7 @@ import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService; import info.textgrid.services.aggregator.GenericExceptionMapper; import info.textgrid.services.aggregator.ITextGridRep; +import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier; import info.textgrid.services.aggregator.TextGridRepProvider; import info.textgrid.services.aggregator.teicorpus.TEICorpusSerializer; @@ -46,9 +47,10 @@ import org.apache.cxf.jaxrs.model.wadl.Description; +import com.google.common.base.Stopwatch; +import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.io.FileBackedOutputStream; @@ -71,7 +73,7 @@ public class HTML { @Context private ServletContext servlet; - private LoadingCache<URI, XsltExecutable> stylesheets; + private Cache<URI, XsltExecutable> stylesheets; private XsltExecutable getToHtml() { if (toHtml == null) { @@ -93,7 +95,12 @@ private XsltExecutable getToHtml() { public HTML(final ITextGridRep repository) throws IOException { this.repository = repository; xsltProcessor = new Processor(false); - stylesheets = CacheBuilder.newBuilder().maximumSize(50).weakValues() + xsltProcessor.getUnderlyingConfiguration().setURIResolver( + new TGUriResolver(repository)); + // xsltProcessor.getUnderlyingConfiguration().setAllowExternalFunctions( + // false); // we run external stylesheets + xsltProcessor.getUnderlyingConfiguration().setCompileWithTracing(true); + stylesheets = CacheBuilder.newBuilder().maximumSize(50).softValues() .removalListener(new RemovalListener<URI, XsltExecutable>() { @Override @@ -127,6 +134,76 @@ public XsltExecutable load(final URI url) throws Exception { } + /** + * Returns an appropriate stylesheet for the given URI. + * + * Basically, we try the following options in order: + * <ol> + * <li>The stylesheet is cached -> return the cached version. + * <li>The stylesheet is public or external -> load & cache it. + * <li>The stylesheet is non-public TextGrid internal -> load & do not cache + * it. + * </ol> + * + * @param uri + * the URI of the stylesheet to load + * @param sid + * the session ID to use, or null. + * @param forceLoad + * TODO + * @throws IOException + * if an error occurs reading the stylesheet. + * @throws SaxonApiException + * if saxon fails to compile the stylesheet. + */ + protected XsltExecutable getStylesheet(final URI uri, final String sid, + final boolean forceLoad) throws SaxonApiException, IOException { + XsltExecutable executable = null; + + // (1) try cached version, if it exists + if (!forceLoad) { + executable = stylesheets.getIfPresent(uri); + } + + if (executable == null) { + + final XsltCompiler compiler = xsltProcessor.newXsltCompiler(); + if (TGUriResolver.isResolveable(uri)) { + + // (2/3) it's a TextGrid object, load it from TG-crud. + final TGOSupplier<InputStream> xsltSupplier = repository.read(uri, sid); + executable = compiler.compile(new StreamSource(xsltSupplier + .getInput(), uri.toString())); + + if (isPublic(xsltSupplier.getMetadata())) { + // (2) it's public -> we can cache it. + stylesheets.put(uri, executable); + logger.log(Level.INFO, "Cached public stylesheet {0}", uri); + } else { + logger.log(Level.INFO, "Loaded private stylesheet {0}", uri); + } + } else { + // (2) it's non-TextGrid -- load & cache it. + executable = compiler.compile(new StreamSource(uri.toString())); + stylesheets.put(uri, executable); + logger.log(Level.INFO, "Cached external stylesheet {0}", uri); + } + } else { + logger.log(Level.INFO, "Reusing cached stylesheed {0}", uri); + } + + return executable; + } + + private static boolean isPublic(final ObjectType metadata) { + try { + return metadata.getGeneric().getGenerated().getAvailability() + .contains("public"); + } catch (final NullPointerException e) { + return false; + } + } + @GET @Path(value = "/{object}") @Produces(value = "text/html") @@ -145,6 +222,9 @@ public StreamingOutput get( SaxonApiException, ExecutionException { logger.fine("HTML called for root object: " + uri); + final Stopwatch stopwatch = new Stopwatch(); + stopwatch.start(); + final TGCrudService crud = repository.getCRUDService(); final MetadataContainerType container = crud.readMetadata(sid, null, uri.toString()); @@ -170,16 +250,18 @@ public StreamingOutput get( } else { tei = repository.getContent(uri, sid); } + logger.info("we have an input document after " + stopwatch.toString()); final XsltTransformer transformer; if (xsluri == null || "".equals(xsluri)) { transformer = getToHtml().load(); } else { - if (refreshStylesheet) { - stylesheets.refresh(xsluri); - } - transformer = stylesheets.get(xsluri).load(); + transformer = getStylesheet(xsluri, sid, refreshStylesheet).load(); + if (sid != null) { + transformer.setURIResolver(new TGUriResolver(repository, sid)); + } // otherwise default public URI resolver } + transformer.setSource(new StreamSource(tei)); transformer.setParameter(new QName("graphicsURLPattern"), new XdmAtomicValue(repository.getCRUDRestEndpoint() @@ -190,6 +272,7 @@ public StreamingOutput get( transformer.setParameter(new QName("cssFile"), new XdmAtomicValue(css)); } + logger.info("we're ready to transform after " + stopwatch.toString()); return new StreamingOutput() { @Override @@ -198,8 +281,9 @@ public void write(final OutputStream output) throws IOException, transformer.setDestination(xsltProcessor.newSerializer(output)); try { transformer.transform(); - logger.info("Finished transformation to HTML for " - + uri); + logger.info(MessageFormat + .format("Finished transformation to HTML for {0} after {1}", + uri, stopwatch.toString())); } catch (final SaxonApiException e) { throw new WebApplicationException(e); } diff --git a/src/main/java/info/textgrid/services/aggregator/html/TGUriResolver.java b/src/main/java/info/textgrid/services/aggregator/html/TGUriResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..13f3630f3ec6e0d8b3b24c9cdedce65a7dc121c9 --- /dev/null +++ b/src/main/java/info/textgrid/services/aggregator/html/TGUriResolver.java @@ -0,0 +1,83 @@ +package info.textgrid.services.aggregator.html; + +import info.textgrid.services.aggregator.ITextGridRep; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +import com.google.common.base.Optional; +import com.ibm.icu.text.MessageFormat; + +public class TGUriResolver implements URIResolver { + + private final Optional<String> sid; + private final String crudRestEndpoint; + final static Logger logger = Logger + .getLogger("info.textgrid.services.aggregator.html.URIResolver"); + + public TGUriResolver(final ITextGridRep repository, + final Optional<String> sid) { + super(); + crudRestEndpoint = repository.getCRUDRestEndpoint(); + this.sid = sid; + } + + public TGUriResolver(final ITextGridRep repository) { + this(repository, Optional.<String> absent()); + } + + public TGUriResolver(final ITextGridRep repository, final String sid) { + this(repository, sid == null || "".equals(sid) ? Optional + .<String> absent() : Optional.of(sid)); + } + + public static boolean isResolveable(final URI uri) { + final String scheme = uri.getScheme(); + return "textgrid".equals(scheme) || "hdl".equals(scheme); + } + + @Override + public Source resolve(final String href, final String base) throws TransformerException { + logger.info(MessageFormat.format( + "Trying to resolve href={0}, base={1}", href, base)); + try { + final URI uri = new URI(href).resolve(base); + if (isResolveable(uri)) { + final StringBuilder resolved = new StringBuilder( + crudRestEndpoint); + resolved.append('/').append(uri.getScheme()).append(':') + .append(uri.getSchemeSpecificPart()).append("/data"); + if (sid.isPresent()) { + resolved.append("?sessionId=").append(sid.get()); + } + if (uri.getFragment() != null) { + resolved.append('#').append(uri.getFragment()); + } + final URL url = new URL(resolved.toString()); + logger.log(Level.INFO, + MessageFormat.format("Resolved {0} to {1}", uri, url)); + return new StreamSource(url.openStream(), href); + } else { + logger.log(Level.INFO, "Did not resolve {0}", uri); + return null; + } + } catch (final URISyntaxException e) { + throw new TransformerException(e); + } catch (final MalformedURLException e) { + throw new TransformerException(e); + } catch (final IOException e) { + throw new TransformerException(e); + } + } + +}