/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program 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 program 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.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.processor.transformer.xslt;
import org.orbeon.errorified.Exceptions;
import org.apache.log4j.Logger;
import org.orbeon.dom.Document;
import org.orbeon.dom.Node;
import org.orbeon.oxf.cache.CacheKey;
import org.orbeon.oxf.cache.InternalCacheKey;
import org.orbeon.oxf.cache.ObjectCache;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.OrbeonLocationException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.util.XPath;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.processor.generator.URLGenerator;
import org.orbeon.oxf.processor.impl.CacheableTransformerOutputImpl;
import org.orbeon.oxf.processor.transformer.TransformerURIResolver;
import org.orbeon.oxf.processor.transformer.URIResolverListener;
import org.orbeon.oxf.properties.PropertySet;
import org.orbeon.oxf.properties.PropertyStore;
import org.orbeon.oxf.resources.URLFactory;
import org.orbeon.oxf.util.StringBuilderWriter;
import org.orbeon.oxf.xml.*;
import org.orbeon.oxf.xml.XMLParsing;
import org.orbeon.oxf.xml.dom4j.ConstantLocator;
import org.orbeon.oxf.xml.dom4j.ExtendedLocationData;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.saxon.*;
import org.orbeon.saxon.event.ContentHandlerProxyLocator;
import org.orbeon.saxon.event.MessageEmitter;
import org.orbeon.saxon.event.SaxonOutputKeys;
import org.orbeon.saxon.expr.*;
import org.orbeon.saxon.functions.FunctionLibrary;
import org.orbeon.saxon.functions.FunctionLibraryList;
import org.orbeon.saxon.instruct.TerminationException;
import org.orbeon.saxon.om.Item;
import org.orbeon.saxon.om.NodeInfo;
import org.orbeon.saxon.om.StructuredQName;
import org.orbeon.saxon.sxpath.IndependentContext;
import org.orbeon.saxon.trans.XPathException;
import org.xml.sax.*;
import javax.xml.transform.Result;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
/**
* NOTE: This class requires a re-rooted Saxon to be present. Saxon is used to detect stylesheet dependencies, and the
* processor also has support for outputting Saxon line numbers. But the processor must remain able to run other
* transformers like Xalan.
*/
public abstract class XSLTTransformer extends ProcessorImpl {
private static Logger logger = Logger.getLogger(XSLTTransformer.class);
public static final String XSLT_URI = "http://www.w3.org/1999/XSL/Transform";
public static final String XSLT_TRANSFORMER_CONFIG_NAMESPACE_URI = "http://orbeon.org/oxf/xml/xslt-transformer-config";
public static final String XSLT_PREFERENCES_CONFIG_NAMESPACE_URI = "http://orbeon.org/oxf/xml/xslt-preferences-config";
private static final String OUTPUT_LOCATION_MODE_PROPERTY = "location-mode";
private static final String OUTPUT_LOCATION_NONE = "none";
private static final String OUTPUT_LOCATION_DUMB = "dumb";
private static final String OUTPUT_LOCATION_SMART = "smart";
private static final String OUTPUT_LOCATION_MODE_DEFAULT = OUTPUT_LOCATION_NONE;
// This input determines the JAXP transformer factory class to use
private static final String INPUT_TRANSFORMER = "transformer";
// This input determines attributes to set on the TransformerFactory
private static final String INPUT_ATTRIBUTES = "attributes";
public static final String XSLT_STYLESHEET_URI_LISTENER = "xslt-stylesheet-uri-listener"; // used by XSLTTransformer
public XSLTTransformer(String schemaURI) {
addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, schemaURI));
addInputInfo(new ProcessorInputOutputInfo(INPUT_TRANSFORMER, XSLT_TRANSFORMER_CONFIG_NAMESPACE_URI));
addInputInfo(new ProcessorInputOutputInfo(INPUT_ATTRIBUTES, XSLT_PREFERENCES_CONFIG_NAMESPACE_URI));
addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA));
addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA));
}
@Override
public ProcessorOutput createOutput(final String name) {
final ProcessorOutput output = new CacheableTransformerOutputImpl(XSLTTransformer.this, name) {
public void readImpl(PipelineContext pipelineContext, XMLReceiver xmlReceiver) {
makeSureStateIsSet(pipelineContext);
final XSLTTransformerState state = (XSLTTransformerState) getState(pipelineContext);
if (!state.hasTransformationRun) {
state.hasTransformationRun = true;
// Get URI references from cache
final KeyValidity configKeyValidity = getInputKeyValidity(pipelineContext, INPUT_CONFIG);
final URIReferences uriReferences = getURIReferences(pipelineContext, configKeyValidity);
// Get transformer from cache
TemplatesInfo templatesInfo = null;
if (uriReferences != null) {
// FIXME: this won't depend on the transformer input.
final KeyValidity stylesheetKeyValidity = createStyleSheetKeyValidity(pipelineContext, configKeyValidity, uriReferences);
if (stylesheetKeyValidity != null)
templatesInfo = (TemplatesInfo) ObjectCache.instance()
.findValid(stylesheetKeyValidity.key, stylesheetKeyValidity.validity);
}
// Get transformer attributes if any
final Map<String, Boolean> attributesFromProperties;
// Read optional attributes input only if connected
if (getConnectedInputs().get(INPUT_ATTRIBUTES) != null) {
// Read input as an attribute Map and cache it
attributesFromProperties = readCacheInputAsObject(pipelineContext, getInputByName(INPUT_ATTRIBUTES), new CacheableInputReader<Map<String, Boolean>>() {
public Map<String, Boolean> read(PipelineContext context, ProcessorInput input) {
final Document preferencesDocument = readInputAsOrbeonDom(context, input);
final PropertyStore propertyStore = new PropertyStore(preferencesDocument);
final PropertySet propertySet = propertyStore.getGlobalPropertySet();
return propertySet.getBooleanProperties();
}
});
} else
attributesFromProperties = Collections.emptyMap();
// Output location mode
final String outputLocationMode = getPropertySet().getString(OUTPUT_LOCATION_MODE_PROPERTY, OUTPUT_LOCATION_MODE_DEFAULT);
final boolean isDumbOutputLocation = OUTPUT_LOCATION_DUMB.equals(outputLocationMode);
final boolean isSmartOutputLocation = OUTPUT_LOCATION_SMART.equals(outputLocationMode);
final Map<String, Boolean> attributes;
if (isSmartOutputLocation) {
// Create new HashMap as we don't want to change the one in cache
attributes = new HashMap<String, Boolean>(attributesFromProperties);
// Set attributes for Saxon source location
attributes.put(FeatureKeys.LINE_NUMBERING, Boolean.TRUE);
attributes.put(FeatureKeys.COMPILE_WITH_TRACING, Boolean.TRUE);
} else
attributes = attributesFromProperties;
// Create transformer if we did not find one in cache
if (templatesInfo == null) {
// Get transformer configuration
final Node config = readCacheInputAsDOM4J(pipelineContext, INPUT_TRANSFORMER);
final String transformerClass = XPathUtils.selectStringValueNormalize(config, "/config/class");
// Create transformer
// NOTE: createTransformer() handles its own exceptions
templatesInfo = createTransformer(pipelineContext, transformerClass, attributes);
}
// At this point, we have a templatesInfo, so run the transformation
// Find which receiver to use
final XMLReceiver outputReceiver;
if (name.equals(OUTPUT_DATA)) {
// The first output called is the main output: output directly
outputReceiver = xmlReceiver;
} else {
// The first output called is not the main output: store main result
final SAXStore store = new SAXStore();
state.addOutputDocument(OUTPUT_DATA, store);
outputReceiver = store;
// First output will stream out
state.setFirstXMLReceiver(name, xmlReceiver);
}
runTransformer(pipelineContext, state, outputReceiver, templatesInfo, attributes, isDumbOutputLocation, isSmartOutputLocation);
} else {
// Transformation has run already, replay output
if (state.outputDocuments == null)
throw new OXFException("Attempt to read non-existing output: " + name);
try {
final SAXStore outputStore = state.outputDocuments.get(name);
if (outputStore == null)
throw new OXFException("Attempt to read non-existing output: " + name);
outputStore.replay(xmlReceiver);
state.outputDocuments.remove(name);
} catch (SAXException e) {
throw new OXFException(e);
}
}
}
private void runTransformer(final PipelineContext pipelineContext, final XSLTTransformerState state, final XMLReceiver xmlReceiver, TemplatesInfo templatesInfo,
Map<String, Boolean> attributes, final boolean dumbOutputLocation, final boolean smartOutputLocation) {
StringBuilderWriter saxonStringBuilderWriter = null;
try {
// Create transformer handler and set output writer for Saxon
final StringErrorListener errorListener = new StringErrorListener(logger);
final TransformerHandler transformerHandler = TransformerUtils.getTransformerHandler(templatesInfo.templates, templatesInfo.transformerClass, attributes, createXSLTConfiguration());
// Set handler for xsl:result-document
if (transformerHandler instanceof TransformerHandlerImpl) {
final TransformerHandlerImpl saxonTransformerHandler = (TransformerHandlerImpl) transformerHandler;
((Controller) saxonTransformerHandler.getTransformer()).setOutputURIResolver(new OutputURIResolver() {
public Result resolve(String href, String base) throws TransformerException {
final String outputName = getProcessorOutputSchemeInputName(href);
if (outputName == null) {
// Regular URL
try {
final URL url = URLFactory.createURL(base, href);
final StreamResult result;
if (url.getProtocol().equals("file")) {
// Special handling of file as URLConnection does not support writing to a file
result = new StreamResult(new FileOutputStream(new File(url.toURI())));
} else {
// Other protocols
final URLConnection urlConnection = url.openConnection();
urlConnection.setDoOutput(true);
result = new StreamResult(urlConnection.getOutputStream());
}
result.setSystemId(url.toExternalForm());
return result;
} catch (Exception e) {
throw new OXFException(e);
}
} else {
// output:*
final XMLReceiver outputReceiver;
if (outputName.equals(state.firstOutputName)) {
// Stream through first receiver
outputReceiver = state.firstXMLReceiver;
} else {
// Store
final SAXStore store = new SAXStore();
state.addOutputDocument(outputName, store);
outputReceiver = store;
}
final SAXResult result = new SAXResult(outputReceiver);
result.setLexicalHandler(outputReceiver);
result.setSystemId(href);
return result;
}
}
public void close(Result result) throws TransformerException {
// Free information from the state
final String outputName = getProcessorOutputSchemeInputName(result.getSystemId());
if (outputName == null) {
// Regular URL
if (result instanceof StreamResult) {
final OutputStream os = ((StreamResult) result).getOutputStream();
if (os != null)
try {
os.close();
} catch (IOException e) {
throw new OXFException(e);
}
}
} else {
// output:*
if (outputName.equals(state.firstOutputName)) {
state.firstOutputName = null;
state.firstXMLReceiver = null;
}
}
}
});
}
final Transformer transformer = transformerHandler.getTransformer();
final TransformerURIResolver transformerURIResolver = new TransformerURIResolver(XSLTTransformer.this, pipelineContext, INPUT_DATA, XMLParsing.ParserConfiguration.PLAIN);
transformer.setURIResolver(transformerURIResolver);
transformer.setErrorListener(errorListener);
if (smartOutputLocation)
transformer.setOutputProperty(SaxonOutputKeys.SUPPLY_SOURCE_LOCATOR, "yes");
// Create writer for transformation errors
saxonStringBuilderWriter = createErrorStringBuilderWriter(transformerHandler);
// Fallback location data
final LocationData processorLocationData = getLocationData();
// Output filter to fix-up SAX stream and handle location data if needed
final XMLReceiver outputReceiver = new SimpleForwardingXMLReceiver(xmlReceiver) {
private Locator inputLocator;
private OutputLocator outputLocator;
private Stack<LocationData> startElementLocationStack;
class OutputLocator implements Locator {
private LocationData currentLocationData;
public String getPublicId() {
return null;
}
public String getSystemId() {
return (currentLocationData != null) ? currentLocationData.file() : inputLocator.getSystemId();
}
public int getLineNumber() {
return (currentLocationData != null) ? currentLocationData.line() : inputLocator.getLineNumber();
}
public int getColumnNumber() {
return (currentLocationData != null) ? currentLocationData.col() : inputLocator.getColumnNumber();
}
public void setLocationData(LocationData locationData) {
this.currentLocationData = locationData;
}
}
@Override
public void setDocumentLocator(final Locator locator) {
this.inputLocator = locator;
if (smartOutputLocation) {
this.outputLocator = new OutputLocator();
this.startElementLocationStack = new Stack<LocationData>();
super.setDocumentLocator(this.outputLocator);
} else if (dumbOutputLocation) {
super.setDocumentLocator(this.inputLocator);
} else {
// NOP: don't set a locator
}
}
@Override
public void startDocument() throws SAXException {
// Try to set fallback Locator
if (((outputLocator != null && outputLocator.getSystemId() == null) || (inputLocator != null && inputLocator.getSystemId() == null))
&& processorLocationData != null && dumbOutputLocation) {
final Locator locator = new ConstantLocator(processorLocationData);
super.setDocumentLocator(locator);
}
super.startDocument();
}
@Override
public void endDocument() throws SAXException {
if (endDocumentCalled()) {
// Hack to test if Saxon outputs more than one endDocument() event
logger.warn("XSLT transformer attempted to call endDocument() more than once.");
return;
}
super.endDocument();
}
@Override
public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException {
if (outputLocator != null) {
final LocationData locationData = findSourceElementLocationData(uri, localname);
outputLocator.setLocationData(locationData);
startElementLocationStack.push(locationData);
super.startElement(uri, localname, qName, attributes);
outputLocator.setLocationData(null);
} else {
super.startElement(uri, localname, qName, attributes);
}
}
@Override
public void endElement(String uri, String localname, String qName) throws SAXException {
if (outputLocator != null) {
// Here we do a funny thing: since Saxon does not provide location data on endElement(), we use that of startElement()
final LocationData locationData = startElementLocationStack.peek();
outputLocator.setLocationData(locationData);
super.endElement(uri, localname, qName);
outputLocator.setLocationData(null);
startElementLocationStack.pop();
} else {
super.endElement(uri, localname, qName);
}
}
@Override
public void characters(char[] chars, int start, int length) throws SAXException {
if (outputLocator != null) {
final LocationData locationData = findSourceCharacterLocationData();
outputLocator.setLocationData(locationData);
super.characters(chars, start, length);
outputLocator.setLocationData(null);
} else {
super.characters(chars, start, length);
}
}
private LocationData findSourceElementLocationData(String uri, String localname) {
if (inputLocator instanceof ContentHandlerProxyLocator) {
final Stack stack = ((ContentHandlerProxyLocator) inputLocator).getContextItemStack();
for (int i = stack.size() - 1; i >= 0; i--) {
final Item currentItem = (Item) stack.get(i);
if (currentItem instanceof NodeInfo) {
final NodeInfo currentNodeInfo = (NodeInfo) currentItem;
if (currentNodeInfo.getNodeKind() == org.w3c.dom.Node.ELEMENT_NODE
&& currentNodeInfo.getLocalPart().equals(localname)
&& currentNodeInfo.getURI().equals(uri)) {
// Very probable match...
return new LocationData(currentNodeInfo.getSystemId(), currentNodeInfo.getLineNumber(), -1);
}
}
}
}
return null;
}
private LocationData findSourceCharacterLocationData() {
if (inputLocator instanceof ContentHandlerProxyLocator) {
final Stack stack = ((ContentHandlerProxyLocator) inputLocator).getContextItemStack();
if (stack != null) {
for (int i = stack.size() - 1; i >= 0; i--) {
final Item currentItem = (Item) stack.get(i);
if (currentItem instanceof NodeInfo) {
final NodeInfo currentNodeInfo = (NodeInfo) currentItem;
// if (currentNodeInfo.getNodeKind() == org.w3c.dom.Node.TEXT_NODE) {
// Possible match
return new LocationData(currentNodeInfo.getSystemId(), currentNodeInfo.getLineNumber(), -1);
// }
}
}
}
}
return null;
}
};
// Create result, also setting LexicalHandler to handle comments
final SAXResult saxResult = new SAXResult(outputReceiver);
saxResult.setLexicalHandler(outputReceiver);
if (processorLocationData != null) {
final String processorSystemId = processorLocationData.file();
//saxResult.setSystemId(sysID); // NOT SURE WHY WE DID THIS
// TODO: use source document system ID, not stylesheet system ID
transformerHandler.setSystemId(processorSystemId);
}
transformerHandler.setResult(saxResult);
// Execute transformation
try {
if (XSLTTransformer.this.getConnectedInputs().size() > 4) {
// The default inputs are data, config, transformer, and attributes. When other inputs
// (i.e. more than 4) are connected, they can be read with the doc() function in XSLT.
// Reading those documents might happen before the whole input document is read, which
// is not compatible with our processing model. So in this case, we first read the
// data in a SAX store.
final SAXStore dataSaxStore = new SAXStore();
readInputAsSAX(pipelineContext, INPUT_DATA, dataSaxStore);
dataSaxStore.replay(new ForwardingXMLReceiver(transformerHandler, transformerHandler));
} else {
readInputAsSAX(pipelineContext, INPUT_DATA, new ForwardingXMLReceiver(transformerHandler, transformerHandler));
}
} finally {
// Log message from Saxon
if (saxonStringBuilderWriter != null) {
String message = saxonStringBuilderWriter.toString();
if (message.length() > 0)
logger.info(message);
}
// Make sure we don't keep stale references to URI resolver objects
transformer.setURIResolver(null);
transformerURIResolver.destroy();
}
// Check whether some errors were added
if (errorListener.hasErrors()) {
final List errors = errorListener.getErrors();
if (errors != null) {
ValidationException ve = null;
for (Iterator i = errors.iterator(); i.hasNext();) {
final LocationData currentLocationData = (LocationData) i.next();
if (ve == null)
ve = new ValidationException("Errors while executing transformation", currentLocationData);
else
ve.addLocationData(currentLocationData);
}
}
}
} catch (Exception e) {
final Throwable rootCause = Exceptions.getRootThrowable(e);
if (rootCause instanceof TransformerException) {
final TransformerException transformerException = (TransformerException) rootCause;
// Add location data of TransformerException if possible
final LocationData locationData =
(transformerException.getLocator() != null && transformerException.getLocator().getSystemId() != null)
? new LocationData(transformerException.getLocator())
: (templatesInfo.systemId != null)
? new LocationData(templatesInfo.systemId, -1, -1)
: null;
if (rootCause instanceof TerminationException) {
// Saxon-specific exception thrown by xsl:message terminate="yes"
final ValidationException customException = new ValidationException("Processing terminated by xsl:message: " + saxonStringBuilderWriter.toString(), locationData);
throw new ValidationException(customException, new ExtendedLocationData(locationData, "executing XSLT transformation"));
} else {
// Other transformation error
throw new ValidationException(rootCause, new ExtendedLocationData(locationData, "executing XSLT transformation"));
}
} else {
// Add template location data if possible
final LocationData templatesLocationData = (templatesInfo.systemId != null) ? new LocationData(templatesInfo.systemId, -1, -1) : null;
throw OrbeonLocationException.wrapException(rootCause, new ExtendedLocationData(templatesLocationData, "executing XSLT transformation"));
}
}
}
@Override
protected boolean supportsLocalKeyValidity() {
return true;
}
@Override
protected CacheKey getLocalKey(PipelineContext context) {
makeSureStateIsSet(context);
final KeyValidity configKeyValidity = getInputKeyValidity(context, INPUT_CONFIG);
URIReferences uriReferences = getURIReferences(context, configKeyValidity);
if (uriReferences == null || uriReferences.hasDynamicDocumentReferences)
return null;
final List<CacheKey> keys = new ArrayList<CacheKey>();
keys.add(configKeyValidity.key);
final List<URIReference> allURIReferences = new ArrayList<URIReference>();
allURIReferences.addAll(uriReferences.stylesheetReferences);
allURIReferences.addAll(uriReferences.documentReferences);
for (Iterator<URIReference> i = allURIReferences.iterator(); i.hasNext();) {
final URIReference uriReference = i.next();
keys.add(new InternalCacheKey(XSLTTransformer.this, "xsltURLReference", URLFactory.createURL(uriReference.context, uriReference.spec).toExternalForm()));
}
return new InternalCacheKey(XSLTTransformer.this, keys);
}
@Override
protected Object getLocalValidity(PipelineContext context) {
makeSureStateIsSet(context);
final KeyValidity configKeyValidity = getInputKeyValidity(context, INPUT_CONFIG);
final URIReferences uriReferences = getURIReferences(context, configKeyValidity);
if (uriReferences == null || uriReferences.hasDynamicDocumentReferences)
return null;
final List validities = new ArrayList();
validities.add(configKeyValidity.validity);
final List<URIReference> allURIReferences = new ArrayList<URIReference>();
allURIReferences.addAll(uriReferences.stylesheetReferences);
allURIReferences.addAll(uriReferences.documentReferences);
for (Iterator<URIReference> i = allURIReferences.iterator(); i.hasNext();) {
final URIReference uriReference = i.next();
final Processor urlGenerator = new URLGenerator(URLFactory.createURL(uriReference.context, uriReference.spec));
validities.add(((ProcessorOutputImpl) urlGenerator.createOutput(OUTPUT_DATA)).getValidity(context));
}
return validities;
}
private URIReferences getURIReferences(PipelineContext context, KeyValidity configKeyValidity) {
if (configKeyValidity == null)
return null;
return (URIReferences) ObjectCache.instance().findValid(configKeyValidity.key, configKeyValidity.validity);
}
private KeyValidity createStyleSheetKeyValidity(PipelineContext context, KeyValidity configKeyValidity, URIReferences uriReferences) {
if (configKeyValidity == null)
return null;
final List<CacheKey> keys = new ArrayList<CacheKey>();
final List<Object> validities = new ArrayList<Object>();
keys.add(configKeyValidity.key);
validities.add(configKeyValidity.validity);
for (Iterator<URIReference> i = uriReferences.stylesheetReferences.iterator(); i.hasNext();) {
final URIReference uriReference = i.next();
final URL url = URLFactory.createURL(uriReference.context, uriReference.spec);
keys.add(new InternalCacheKey(XSLTTransformer.this, "xsltURLReference", url.toExternalForm()));
final Processor urlGenerator = new URLGenerator(url);
validities.add(((ProcessorOutputImpl) urlGenerator.createOutput(OUTPUT_DATA)).getValidity(context));//FIXME: can we do better? See URL generator.
}
return new KeyValidity(new InternalCacheKey(XSLTTransformer.this, keys), validities);
}
// Create a Saxon Configuration which adds the Orbeon pipeline function library
private Configuration createXSLTConfiguration() {
final Configuration newConfiguration = XPath.newConfiguration();
final FunctionLibrary javaFunctionLibrary = newConfiguration.getExtensionBinder("java");
final FunctionLibraryList functionLibraryList = new FunctionLibraryList();
functionLibraryList.addFunctionLibrary(javaFunctionLibrary);
functionLibraryList.addFunctionLibrary(org.orbeon.oxf.pipeline.api.FunctionLibrary.instance());
newConfiguration.setExtensionBinder("java", functionLibraryList);
return newConfiguration;
}
/**
* Reads the input and creates the JAXP Templates object (wrapped in a Transformer object). While reading
* the input, figures out the direct dependencies on other files (URIReferences object), and stores these
* two mappings in cache:
*
* configKey -> uriReferences
* uriReferencesKey -> transformer
*/
private TemplatesInfo createTransformer(PipelineContext pipelineContext, String transformerClass, Map<String, Boolean> attributes) {
StringErrorListener errorListener = new StringErrorListener(logger);
final StylesheetForwardingXMLReceiver topStylesheetXMLReceiver = new StylesheetForwardingXMLReceiver();
try {
// Create transformer
final TemplatesInfo templatesInfo = new TemplatesInfo();
final List<StylesheetForwardingXMLReceiver> xsltXMLReceivers = new ArrayList<StylesheetForwardingXMLReceiver>();
{
// Create SAXSource adding our forwarding receiver
final SAXSource stylesheetSAXSource;
{
xsltXMLReceivers.add(topStylesheetXMLReceiver);
final XMLReader xmlReader = new ProcessorOutputXMLReader(pipelineContext, getInputByName(INPUT_CONFIG).getOutput()) {
@Override
public void setContentHandler(ContentHandler handler) {
super.setContentHandler(new TeeXMLReceiver(Arrays.asList(topStylesheetXMLReceiver, new SimpleForwardingXMLReceiver(handler))));
}
};
stylesheetSAXSource = new SAXSource(xmlReader, new InputSource());
}
// Put listener in context that will be called by URI resolved
pipelineContext.setAttribute(XSLT_STYLESHEET_URI_LISTENER, new URIResolverListener() {
public XMLReceiver getXMLReceiver() {
StylesheetForwardingXMLReceiver xmlReceiver = new StylesheetForwardingXMLReceiver();
xsltXMLReceivers.add(xmlReceiver);
return xmlReceiver;
}
});
final TransformerURIResolver uriResolver
= new TransformerURIResolver(XSLTTransformer.this, pipelineContext, INPUT_DATA, XMLParsing.ParserConfiguration.PLAIN);
templatesInfo.templates = TransformerUtils.getTemplates(stylesheetSAXSource, transformerClass, attributes, createXSLTConfiguration(), errorListener, uriResolver);
uriResolver.destroy();
templatesInfo.transformerClass = transformerClass;
templatesInfo.systemId = topStylesheetXMLReceiver.getSystemId();
}
// Update cache
{
// Create uriReferences
URIReferences uriReferences = new URIReferences();
for (final StylesheetForwardingXMLReceiver xsltXMLReceiver : xsltXMLReceivers) {
uriReferences.hasDynamicDocumentReferences = uriReferences.hasDynamicDocumentReferences
|| xsltXMLReceiver.getURIReferences().hasDynamicDocumentReferences;
uriReferences.stylesheetReferences.addAll
(xsltXMLReceiver.getURIReferences().stylesheetReferences);
uriReferences.documentReferences.addAll
(xsltXMLReceiver.getURIReferences().documentReferences);
}
// Put in cache: configKey -> uriReferences
final KeyValidity configKeyValidity = getInputKeyValidity(pipelineContext, INPUT_CONFIG);
if (configKeyValidity != null)
ObjectCache.instance().add(configKeyValidity.key, configKeyValidity.validity, uriReferences);
// Put in cache: (configKey, uriReferences.stylesheetReferences) -> transformer
final KeyValidity stylesheetKeyValidity = createStyleSheetKeyValidity(pipelineContext, configKeyValidity, uriReferences);
if (stylesheetKeyValidity != null)
ObjectCache.instance().add(stylesheetKeyValidity.key, stylesheetKeyValidity.validity, templatesInfo);
}
return templatesInfo;
} catch (TransformerException e) {
if (errorListener.hasErrors()) {
// Use error messages information and provide location data of first error
final ValidationException validationException = new ValidationException(errorListener.getMessages(), errorListener.getErrors().get(0));
// If possible add location of top-level stylesheet
if (topStylesheetXMLReceiver.getSystemId() != null)
validationException.addLocationData(new ExtendedLocationData(new LocationData(topStylesheetXMLReceiver.getSystemId(), -1, -1), "creating XSLT transformer"));
throw validationException;
} else {
// No XSLT errors are available
final LocationData transformerExceptionLocationData
= StringErrorListener.getTransformerExceptionLocationData(e, topStylesheetXMLReceiver.getSystemId());
if (transformerExceptionLocationData.file() != null)
throw OrbeonLocationException.wrapException(e, new ExtendedLocationData(transformerExceptionLocationData, "creating XSLT transformer"));
else
throw new OXFException(e);
}
// final ExtendedLocationData extendedLocationData
// = StringErrorListener.getTransformerExceptionLocationData(e, topStylesheetContentHandler.getSystemId());
//
// final ValidationException ve = new ValidationException(e.getMessage() + " " + errorListener.getMessages(), e, extendedLocationData);
//
// // Append location data gathered from error listener
// if (errorListener.hasErrors()) {
// final List errors = errorListener.getErrors();
// if (errors != null) {
// for (Iterator i = errors.iterator(); i.hasNext();) {
// final LocationData currentLocationData = (LocationData) i.next();
// ve.addLocationData(currentLocationData);
// }
// }
// }
// throw ve;
} catch (Exception e) {
if (topStylesheetXMLReceiver.getSystemId() != null) {
throw OrbeonLocationException.wrapException(e, new ExtendedLocationData(topStylesheetXMLReceiver.getSystemId(), -1, -1, "creating XSLT transformer"));
} else {
throw new OXFException(e);
}
}
}
};
addOutput(name, output);
return output;
}
private StringBuilderWriter createErrorStringBuilderWriter(TransformerHandler transformerHandler) throws Exception {
final String transformerClassName = transformerHandler.getTransformer().getClass().getName();
// NOTE: 2007-07-05 MK suggests that since we depend on Saxon anyway, we shouldn't use reflection
// here but directly the Saxon classes to avoid the cost of reflection.
StringBuilderWriter saxonStringBuilderWriter = null;
if (transformerClassName.equals("org.orbeon.saxon.Controller")) {
// Built-in Saxon transformer
saxonStringBuilderWriter = new StringBuilderWriter();
final Controller saxonController = (Controller) transformerHandler.getTransformer();
final MessageEmitter emitter = new MessageEmitter();
emitter.setStreamResult(new StreamResult(saxonStringBuilderWriter));
saxonController.setMessageEmitter(emitter);
} else if (transformerClassName.equals("net.sf.saxon.Controller")) {
// A Saxon transformer, we don't know which version
saxonStringBuilderWriter = new StringBuilderWriter();
final Transformer saxonController = transformerHandler.getTransformer();
final Method getMessageEmitter = saxonController.getClass().getMethod("getMessageEmitter");
Object messageEmitter = getMessageEmitter.invoke(saxonController);
if (messageEmitter == null) {
// Try to set a Saxon MessageEmitter
final String messageEmitterClassName = "net.sf.saxon.event.MessageEmitter";
final Class messageEmitterClass = Class.forName(messageEmitterClassName);
messageEmitter = messageEmitterClass.newInstance();
final Class receiverClass = Class.forName("net.sf.saxon.event.Receiver");
final Method setMessageEmitter = saxonController.getClass().getMethod("setMessageEmitter", receiverClass);
setMessageEmitter.invoke(saxonController, messageEmitter);
}
final Method setWriter = messageEmitter.getClass().getMethod("setWriter", new Class[]{Writer.class});
setWriter.invoke(messageEmitter, saxonStringBuilderWriter);
}
return saxonStringBuilderWriter;
}
/**
* This forwarding content handler intercepts all the references to external
* resources from the XSLT stylesheet. There can be external references in
* an XSLT stylesheet when the <xsl:include> or <xsl:import>
* elements are used, or when there is an occurrence of the
* <code>document()</code> function in an XPath expression.
*
* @see #getURIReferences()
*/
private static class StylesheetForwardingXMLReceiver extends ForwardingXMLReceiver {
/**
* This is context that will resolve any prefix, function, and variable.
* It is just used to parse XPath expression and get an AST.
*/
private IndependentContext dummySaxonXPathContext;
private void initDummySaxonXPathContext() {
final Configuration config = XPath.newConfiguration();
config.setHostLanguage(Configuration.XSLT);
dummySaxonXPathContext = new IndependentContext(config) {
{
// Dummy Function lib that accepts any name
setFunctionLibrary(new FunctionLibrary() {
public Expression bind(StructuredQName functionName, Expression[] staticArgs, StaticContext env) throws XPathException {
if ((XMLConstants.XPATH_FUNCTIONS_NAMESPACE_URI.equals(functionName.getNamespaceURI()) || "".equals(functionName.getNamespaceURI()))
&& ("doc".equals(functionName.getLocalName()) || "document".equals(functionName.getLocalName()))
&& (staticArgs != null && staticArgs.length > 0)) {
if (staticArgs[0] instanceof StringLiteral) {
// Found doc() or document() function which contains a static string
final String literalURI = ((StringLiteral) staticArgs[0]).getStringValue();
// We don't need to worry here about reference to the processor inputs
if (!isProcessorInputScheme(literalURI)) {
final URIReference uriReference = new URIReference();
uriReference.context = systemId;
uriReference.spec = literalURI;
uriReferences.documentReferences.add(uriReference);
}
} else {
// Found doc() or document() function which contains something more complex
uriReferences.hasDynamicDocumentReferences = true;
}
}
// NOTE: We used to return new FunctionCall() here, but MK says EmptySequence.getInstance() will work.
// TODO: Check if this works in Saxon 9.0. It doesn't work in 8.8, so for now we keep return new ContextItemExpression().
// return EmptySequence.getInstance();
return new ContextItemExpression();
}
public boolean isAvailable(StructuredQName functionName, int arity) {
return true;
}
public FunctionLibrary copy() {
return this;
}
});
}
@Override
public String getURIForPrefix(String prefix) {
return namespaceContext.getURI(prefix);
}
@Override
public boolean isImportedSchema(String namespace) { return true; }
@Override
// Dummy var declaration to allow any name
public VariableReference bindVariable(StructuredQName qName) throws XPathException {
return new VariableReference();
// return new VariableReference(XPathVariable.make());
// return new VariableReference(new VariableReference(); {
// public void registerReference(BindingReference bindingReference) {
// }
//
// public int getNameCode() {
// return fingerprint;
// }
//
// public String getVariableName() {
// return "dummy";
// }
// });
}
};
}
private Locator locator;
private URIReferences uriReferences = new URIReferences();
private String systemId;
private final NamespaceContext namespaceContext = new NamespaceContext();
public StylesheetForwardingXMLReceiver() {
super();
initDummySaxonXPathContext();
}
public URIReferences getURIReferences() {
return uriReferences;
}
public String getSystemId() {
return systemId;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
super.setDocumentLocator(locator);
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
namespaceContext.startPrefixMapping(prefix, uri);
super.startPrefixMapping(prefix, uri);
}
@Override
public void startElement(String uri, String localname, String qName, Attributes attributes) throws SAXException {
namespaceContext.startElement();
// Save system id
if (systemId == null && locator != null)
systemId = locator.getSystemId();
// Handle possible include
if (XSLT_URI.equals(uri)) {
// <xsl:include> or <xsl:import>
if ("include".equals(localname) || "import".equals(localname)) {
final String href = attributes.getValue("href");
final URIReference uriReference = new URIReference();
uriReference.context = systemId;
uriReference.spec = href;
uriReferences.stylesheetReferences.add(uriReference);
} else if ("import-schema".equals(localname)) {
final String schemaLocation = attributes.getValue("schema-location");// NOTE: We ignore the @namespace attribute for now
final URIReference uriReference = new URIReference();
uriReference.context = systemId;
uriReference.spec = schemaLocation;
uriReferences.stylesheetReferences.add(uriReference);
}
// Find XPath expression on current element
String xpathString;
{
xpathString = attributes.getValue("test");
if (xpathString == null)
xpathString = attributes.getValue("select");
}
// Analyze XPath expression to find dependencies on URIs
if (xpathString != null) {
try {
// First, test that one of the strings is present so we don't have to parse unnecessarily
// NOTE: 2007-07-05 MK says that there can be spaces and comments between the "doc" and the
// "(". Suggestion: "One possibility here if you want to avoid parsing the expression
// unnecessarily is to run it through the lexer (net.sf.saxon.expr.Tokenizer). You can just look
// at the stream of tokens and look for doc (or a lexical QName whose local part is doc)
// followed by "("."
// For now, we will probably have many false positive but we just test on "doc". The exact match
// is done by parsing the expression below anyway.
final boolean containsDocString = xpathString.contains("doc");
if (containsDocString) {
// The following will call our FunctionLibrary.bind() method, which we use to test for the
// presence of the functions.
ExpressionTool.make(xpathString, dummySaxonXPathContext, 0, -1, 0, false);
// NOTE: *If* we wanted to use Saxon to parse the whole Stylesheet:
// MK: "In Saxon 9.0 there's a method explain() on PreparedStylesheet that writes an XML
// representation of the compiled stylesheet to a user-supplied Receiver as a sequence of
// events. You could call this with your own Receiver and just watch for the events
// representing <functionCall name="doc"><literal>...</literal></functionCall>. But this
// depends on compiling the stylesheet first.
}
} catch (XPathException e) {
logger.error("Original exception", e);
throw new ValidationException("XPath syntax exception (" + e.getMessage() + ") for expression: "
+ xpathString, new LocationData(locator));
}
}
}
super.startElement(uri, localname, qName, attributes);
}
@Override
public void endElement(String uri, String localname, String qName) throws SAXException {
super.endElement(uri, localname, qName);
namespaceContext.endElement();
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
@Override
public void startDocument() throws SAXException {
super.startDocument();
}
}
private static class URIReference {
public String context;
public String spec;
}
private static class URIReferences {
public List<URIReference> stylesheetReferences = new ArrayList<URIReference>();
public List<URIReference> documentReferences = new ArrayList<URIReference>();
/**
* Is true if and only if an XPath expression with a call to the
* <code>document()</code> function was found and the value of the
* attribute to the <code>document()</code> function call cannot be
* determined without executing the stylesheet. When this happens, the
* result of the stylesheet execution cannot be cached.
*/
public boolean hasDynamicDocumentReferences = false;
}
private static class TemplatesInfo {
public Templates templates;
public String transformerClass;
public String systemId;
}
private static class XSLTTransformerState {
public boolean hasTransformationRun;
public Map<String, SAXStore> outputDocuments;
public String firstOutputName;
public XMLReceiver firstXMLReceiver;
public void addOutputDocument(String uri, SAXStore store) {
if (outputDocuments == null)
outputDocuments = new HashMap<String, SAXStore>();
outputDocuments.put(uri, store);
}
public void setFirstXMLReceiver(String firstOutputName, XMLReceiver firstXMLReceiver) {
this.firstOutputName = firstOutputName;
this.firstXMLReceiver = firstXMLReceiver;
}
}
private void makeSureStateIsSet(PipelineContext pipelineContext) {
if (!hasState(pipelineContext))
setState(pipelineContext, new XSLTTransformerState());
}
@Override
public void reset(PipelineContext pipelineContext) {
setState(pipelineContext, new XSLTTransformerState());
}
}