com.offerready.xslt.WeaklyCachedXsltTransformer Maven / Gradle / Ivy
package com.offerready.xslt;
import com.databasesandlife.util.ThreadPool;
import com.databasesandlife.util.Timer;
import com.databasesandlife.util.gwtsafe.ConfigurationException;
import lombok.SneakyThrows;
import net.sf.saxon.TransformerFactoryImpl;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
/**
* Wraps an XSLT {@link Templates} capable of performing an XSLT transformation.
*
* Get an object by using the static method {@link #getTransformerOrScheduleCompilation(XsltCompilationThreads, String, Xslt)}
* This method maintains a cache by the MD5 of the XSLT file, so that every time an object is requested
* for the same XSLT file (even if this XSLT file exists in different places in the filesystem), the same object will be returned.
*
* Clients should keep a persistent reference to the {@link WeaklyCachedXsltTransformer}
* as long as transformations might need to be applied using it;
* the cache caches only weak references.
*
* Compilation of an XSLT file can fail (e.g. if the XSLT file is invalid).
* In this case, the desired behaviour is that all other valid XSLTs can be applied, so no exception is thrown upon compilation.
* The method {@link #assertValid()} returns void if the template is OK and throws the DocumentTemplateInvalidException otherwise.
*/
public class WeaklyCachedXsltTransformer {
private static final Map> cache = new HashMap<>();
/** Thrown if an XSLT is applied which previously did not compile */
public static class DocumentTemplateInvalidException extends Exception {
public DocumentTemplateInvalidException(String msg) { super(msg); }
}
public interface Xslt {
/**
* @return For example md5 of xslt; globally unique across the world's XSLTs
*/
String calculateCacheKey();
/**
* @return The XSLT file
* @throws ConfigurationException if the XSLT file cannot be parsed
*/
Document parseDocument() throws ConfigurationException;
}
/**
* Can produce a {@link Transformer}.
*
* In principle a {@link Templates} can do this,
* however for the identity transformation there is a method from Saxon to produce an identity {@link Transformer}
* but not identity {@link Templates}.
*/
protected interface XsltTransformerFactory {
Transformer newTransformer();
}
protected @CheckForNull String error = null;
/** After object is initialized, this is never null */
protected XsltTransformerFactory xsltTransformerFactory;
protected class CompileJob implements Runnable {
protected @Nonnull String md5, nameForLogging;
protected @Nonnull Document xslt;
protected CompileJob(@Nonnull String m, @Nonnull String n, @Nonnull Document x) { md5 = m; nameForLogging = n; xslt = x; }
public void run() {
var errorString = new StringBuilder();
var errorListener = new ErrorListener() {
public void warning(TransformerException e) { errorString.append("\nERROR: ").append(e.getMessage()); }
public void error(TransformerException e) { errorString.append("\nWARN: ").append(e.getMessage()); }
public void fatalError(TransformerException e) { errorString.append("\nFATAL: ").append(e.getMessage()); }
};
try (var ignored = new Timer("Compiling XSLT '" + nameForLogging + "'")) {
var transformerFactory = (TransformerFactoryImpl) TransformerFactory.newInstance(
TransformerFactoryImpl.class.getName(), DocumentGenerator.class.getClassLoader());
transformerFactory.setErrorListener(errorListener);
var templates = transformerFactory.newTemplates(new DOMSource(xslt));
xsltTransformerFactory = new XsltTransformerFactory() {
@SneakyThrows(TransformerConfigurationException.class)
@Override public Transformer newTransformer() { return templates.newTransformer(); }
};
}
catch (Exception exception) {
if (!errorString.isEmpty()) error = nameForLogging + ": " + errorString;
else error = nameForLogging + ": " + exception.getMessage();
LoggerFactory.getLogger(getClass()).error(error, exception);
}
cache.put(md5, new WeakReference<>(WeaklyCachedXsltTransformer.this));
}
}
public static class XsltCompilationThreads extends ThreadPool {
final Map toCompileForXsltMd5 = new HashMap<>();
@Override public void execute() {
try (var ignored = new Timer(threadNamePrefix)) {
super.execute();
}
}
}
public synchronized static @Nonnull WeaklyCachedXsltTransformer getTransformerOrScheduleCompilation(
@Nonnull XsltCompilationThreads threads, @Nonnull String nameForLogging, @Nonnull Xslt xslt
) throws ConfigurationException {
var cacheKey = xslt.calculateCacheKey();
var ref = cache.get(cacheKey);
var result = (ref == null) ? null : ref.get();
if (result != null) return result;
result = threads.toCompileForXsltMd5.get(cacheKey);
if (result != null) return result;
result = new WeaklyCachedXsltTransformer();
threads.toCompileForXsltMd5.put(cacheKey, result);
threads.addTask(result.new CompileJob(cacheKey, nameForLogging, xslt.parseDocument()));
return result;
}
public static @Nonnull WeaklyCachedXsltTransformer getIdentityTransformer() {
var transformerFactory = (TransformerFactoryImpl) TransformerFactory.newInstance(
TransformerFactoryImpl.class.getName(), DocumentGenerator.class.getClassLoader());
var result = new WeaklyCachedXsltTransformer();
result.xsltTransformerFactory = new XsltTransformerFactory() {
@SneakyThrows(TransformerConfigurationException.class)
@Override public Transformer newTransformer() { return transformerFactory.newTransformer(); }
};
return result;
}
public void assertValid() throws DocumentTemplateInvalidException {
if (error != null) throw new DocumentTemplateInvalidException(error);
}
public static @Nonnull WeaklyCachedXsltTransformer newInvalidTransformer(@Nonnull String error) {
var result = new WeaklyCachedXsltTransformer();
result.error = error;
return result;
}
public Transformer newTransformer() throws DocumentTemplateInvalidException {
assertValid();
return xsltTransformerFactory.newTransformer();
}
}