| /* |
| * Copyright 2005 The Apache Software Foundation |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| * implied. |
| * |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.components.xslt; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.xml.transform.Result; |
| import javax.xml.transform.Templates; |
| import javax.xml.transform.Transformer; |
| import javax.xml.transform.TransformerException; |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.URIResolver; |
| import javax.xml.transform.sax.SAXTransformerFactory; |
| import javax.xml.transform.sax.TemplatesHandler; |
| import javax.xml.transform.sax.TransformerHandler; |
| import javax.xml.transform.stream.StreamSource; |
| |
| import org.apache.avalon.excalibur.pool.Recyclable; |
| import org.apache.avalon.framework.activity.Disposable; |
| import org.apache.avalon.framework.activity.Initializable; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.parameters.ParameterException; |
| import org.apache.avalon.framework.parameters.Parameterizable; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceResolver; |
| import org.apache.excalibur.source.SourceValidity; |
| import org.apache.excalibur.source.impl.validity.AggregatedValidity; |
| import org.apache.excalibur.store.Store; |
| import org.apache.excalibur.xml.sax.XMLizable; |
| import org.apache.excalibur.xml.xslt.XSLTProcessor; |
| import org.apache.excalibur.xml.xslt.XSLTProcessorException; |
| import org.apache.excalibur.xmlizer.XMLizer; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.XMLFilter; |
| |
| /** |
| * Adaptation of Excalibur's XSLTProcessor implementation to allow for better |
| * error reporting. |
| * |
| * @version $Id$ |
| * @since 2.1.8 |
| */ |
| public class TraxProcessor extends AbstractLogEnabled implements XSLTProcessor, Serviceable, Initializable, Disposable, Parameterizable, |
| Recyclable, URIResolver { |
| /** The store service instance */ |
| protected Store m_store; |
| |
| /** The configured transformer factory to use */ |
| protected String m_transformerFactory; |
| |
| /** The trax TransformerFactory this component uses */ |
| protected SAXTransformerFactory m_factory; |
| |
| /** The default TransformerFactory used by this component */ |
| protected SAXTransformerFactory m_defaultFactory; |
| |
| /** Is the store turned on? (default is off) */ |
| protected boolean m_useStore; |
| |
| /** Is incremental processing turned on? (default for Xalan: no) */ |
| protected boolean m_incrementalProcessing; |
| |
| /** Resolver used to resolve XSLT document() calls, imports and includes */ |
| protected SourceResolver m_resolver; |
| |
| /** Check included stylesheets */ |
| protected boolean m_checkIncludes; |
| |
| /** Map of pairs of System ID's / validities of the included stylesheets */ |
| protected Map m_includesMap = new HashMap(); |
| |
| protected XMLizer m_xmlizer; |
| |
| /** The ServiceManager */ |
| protected ServiceManager m_manager; |
| |
| /** |
| * Compose. Try to get the store |
| * |
| * @avalon.service interface="XMLizer" |
| * @avalon.service interface="SourceResolver" |
| * @avalon.service interface="Store/TransientStore" optional="true" |
| */ |
| public void service(final ServiceManager manager) throws ServiceException { |
| m_manager = manager; |
| m_xmlizer = (XMLizer) m_manager.lookup(XMLizer.ROLE); |
| m_resolver = (SourceResolver) m_manager.lookup(SourceResolver.ROLE); |
| |
| if (m_manager.hasService(Store.TRANSIENT_STORE)) { |
| m_store = (Store) m_manager.lookup(Store.TRANSIENT_STORE); |
| } |
| } |
| |
| /** |
| * Initialize |
| */ |
| public void initialize() throws Exception { |
| m_factory = getTransformerFactory(m_transformerFactory); |
| m_defaultFactory = m_factory; |
| } |
| |
| /** |
| * Disposable |
| */ |
| public void dispose() { |
| if (null != m_manager) { |
| m_manager.release(m_store); |
| m_manager.release(m_resolver); |
| m_manager.release(m_xmlizer); |
| m_manager = null; |
| } |
| m_xmlizer = null; |
| m_store = null; |
| m_resolver = null; |
| } |
| |
| /** |
| * Configure the component |
| */ |
| public void parameterize(final Parameters params) throws ParameterException { |
| m_useStore = params.getParameterAsBoolean("use-store", this.m_useStore); |
| m_incrementalProcessing = params.getParameterAsBoolean("incremental-processing", this.m_incrementalProcessing); |
| m_transformerFactory = params.getParameter("transformer-factory", null); |
| m_checkIncludes = params.getParameterAsBoolean("check-includes", true); |
| if (!m_useStore) { |
| // release the store, if we don't need it anymore |
| m_manager.release(m_store); |
| m_store = null; |
| } else if (null == m_store) { |
| final String message = "XSLTProcessor: use-store is set to true, " + "but unable to aquire the Store."; |
| throw new ParameterException(message); |
| } |
| } |
| |
| /** |
| * Set the transformer factory used by this component |
| */ |
| public void setTransformerFactory(final String classname) { |
| m_factory = getTransformerFactory(classname); |
| } |
| |
| /** |
| * @see org.apache.excalibur.xml.xslt.XSLTProcessor#getTransformerHandler(org.apache.excalibur.source.Source) |
| */ |
| public TransformerHandler getTransformerHandler(final Source stylesheet) throws XSLTProcessorException { |
| return getTransformerHandler(stylesheet, null); |
| } |
| |
| /** |
| * @see org.apache.excalibur.xml.xslt.XSLTProcessor#getTransformerHandler(org.apache.excalibur.source.Source, |
| * org.xml.sax.XMLFilter) |
| */ |
| public TransformerHandler getTransformerHandler(final Source stylesheet, final XMLFilter filter) throws XSLTProcessorException { |
| final XSLTProcessor.TransformerHandlerAndValidity validity = getTransformerHandlerAndValidity(stylesheet, filter); |
| return validity.getTransfomerHandler(); |
| } |
| |
| public TransformerHandlerAndValidity getTransformerHandlerAndValidity(final Source stylesheet) throws XSLTProcessorException { |
| return getTransformerHandlerAndValidity(stylesheet, null); |
| } |
| |
| public TransformerHandlerAndValidity getTransformerHandlerAndValidity(Source stylesheet, XMLFilter filter) throws XSLTProcessorException { |
| |
| final String id = stylesheet.getURI(); |
| TransformerHandlerAndValidity handlerAndValidity; |
| |
| try { |
| handlerAndValidity = getTemplates(stylesheet, id); |
| if (handlerAndValidity != null) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Reusing Templates for " + id); |
| } |
| return handlerAndValidity; |
| } |
| } catch(Exception e) { |
| throw new XSLTProcessorException("Error retrieving template", e); |
| } |
| |
| TraxErrorListener errorListener = new TraxErrorListener(getLogger(), stylesheet.getURI()); |
| try{ |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Creating new Templates for " + id); |
| } |
| |
| m_factory.setErrorListener(errorListener); |
| |
| // Create a Templates ContentHandler to handle parsing of the |
| // stylesheet. |
| TemplatesHandler templatesHandler = m_factory.newTemplatesHandler(); |
| |
| // Set the system ID for the template handler since some |
| // TrAX implementations (XSLTC) rely on this in order to obtain |
| // a meaningful identifier for the Templates instances. |
| templatesHandler.setSystemId(id); |
| if (filter != null) { |
| filter.setContentHandler(templatesHandler); |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Source = " + stylesheet + ", templatesHandler = " + templatesHandler); |
| } |
| |
| // Initialize List for included validities |
| SourceValidity validity = stylesheet.getValidity(); |
| if (validity != null && m_checkIncludes) { |
| m_includesMap.put(id, new ArrayList()); |
| } |
| |
| try { |
| // Process the stylesheet. |
| sourceToSAX(stylesheet, filter != null ? (ContentHandler) filter : (ContentHandler) templatesHandler); |
| |
| // Get the Templates object (generated during the parsing of |
| // the stylesheet) from the TemplatesHandler. |
| final Templates template = templatesHandler.getTemplates(); |
| |
| if (null == template) { |
| throw new XSLTProcessorException("Unable to create templates for stylesheet: " + stylesheet.getURI()); |
| } |
| |
| putTemplates(template, stylesheet, id); |
| |
| // Create transformer handler |
| final TransformerHandler handler = m_factory.newTransformerHandler(template); |
| handler.getTransformer().setErrorListener(new TraxErrorListener(getLogger(), stylesheet.getURI())); |
| handler.getTransformer().setURIResolver(this); |
| |
| // Create aggregated validity |
| AggregatedValidity aggregated = null; |
| if (validity != null && m_checkIncludes) { |
| List includes = (List) m_includesMap.get(id); |
| if (includes != null) { |
| aggregated = new AggregatedValidity(); |
| aggregated.add(validity); |
| for (int i = includes.size() - 1; i >= 0; i--) { |
| aggregated.add((SourceValidity) ((Object[]) includes.get(i))[1]); |
| } |
| validity = aggregated; |
| } |
| } |
| |
| // Create result |
| handlerAndValidity = new MyTransformerHandlerAndValidity(handler, validity); |
| } finally { |
| if (m_checkIncludes) |
| m_includesMap.remove(id); |
| } |
| |
| return handlerAndValidity; |
| } catch (Exception e) { |
| Throwable realEx = errorListener.getThrowable(); |
| if (realEx == null) realEx = e; |
| |
| if (realEx instanceof RuntimeException) { |
| throw (RuntimeException)realEx; |
| } |
| |
| if (realEx instanceof XSLTProcessorException) { |
| throw (XSLTProcessorException)realEx; |
| } |
| |
| throw new XSLTProcessorException("Exception when creating Transformer from " + stylesheet.getURI(), realEx); |
| } |
| } |
| |
| private void sourceToSAX(Source source, ContentHandler handler) throws SAXException, IOException, SourceException { |
| if (source instanceof XMLizable) { |
| ((XMLizable) source).toSAX(handler); |
| } else { |
| final InputStream inputStream = source.getInputStream(); |
| final String mimeType = source.getMimeType(); |
| final String systemId = source.getURI(); |
| m_xmlizer.toSAX(inputStream, mimeType, systemId, handler); |
| } |
| } |
| |
| public void transform(final Source source, final Source stylesheet, final Parameters params, final Result result) throws XSLTProcessorException { |
| try { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug( |
| "Transform source = " + source + ", stylesheet = " + stylesheet + ", parameters = " + params + ", result = " + result); |
| } |
| final TransformerHandler handler = getTransformerHandler(stylesheet); |
| if (params != null) { |
| final Transformer transformer = handler.getTransformer(); |
| transformer.clearParameters(); |
| String[] names = params.getNames(); |
| for (int i = names.length - 1; i >= 0; i--) { |
| transformer.setParameter(names[i], params.getParameter(names[i])); |
| } |
| } |
| |
| handler.setResult(result); |
| sourceToSAX(source, handler); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Transform done"); |
| } |
| } catch (SAXException e) { |
| // Unwrapping the exception will "remove" the real cause with |
| // never Xalan versions and makes the exception message unusable |
| final String message = "Error in running Transformation"; |
| throw new XSLTProcessorException(message, e); |
| /* |
| * if( e.getException() == null ) { final String message = "Error in |
| * running Transformation"; throw new XSLTProcessorException( |
| * message, e ); } else { final String message = "Got SAXException. |
| * Rethrowing cause exception."; getLogger().debug( message, e ); |
| * throw new XSLTProcessorException( "Error in running |
| * Transformation", e.getException() ); } |
| */ |
| } catch (Exception e) { |
| final String message = "Error in running Transformation"; |
| throw new XSLTProcessorException(message, e); |
| } |
| } |
| |
| /** |
| * Get the TransformerFactory associated with the given classname. If the |
| * class can't be found or the given class doesn't implement the required |
| * interface, the default factory is returned. |
| */ |
| private SAXTransformerFactory getTransformerFactory(String factoryName) { |
| SAXTransformerFactory _factory; |
| |
| if (null == factoryName) { |
| _factory = (SAXTransformerFactory) TransformerFactory.newInstance(); |
| } else { |
| try { |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| if (loader == null) { |
| loader = getClass().getClassLoader(); |
| } |
| _factory = (SAXTransformerFactory) loader.loadClass(factoryName).newInstance(); |
| } catch (ClassNotFoundException cnfe) { |
| getLogger().error("Cannot find the requested TrAX factory '" + factoryName + "'. Using default TrAX Transformer Factory instead."); |
| if (m_factory != null) |
| return m_factory; |
| _factory = (SAXTransformerFactory) TransformerFactory.newInstance(); |
| } catch (ClassCastException cce) { |
| getLogger().error( |
| "The indicated class '" + factoryName |
| + "' is not a TrAX Transformer Factory. Using default TrAX Transformer Factory instead."); |
| if (m_factory != null) |
| return m_factory; |
| _factory = (SAXTransformerFactory) TransformerFactory.newInstance(); |
| } catch (Exception e) { |
| getLogger().error( |
| "Error found loading the requested TrAX Transformer Factory '" + factoryName |
| + "'. Using default TrAX Transformer Factory instead."); |
| if (m_factory != null) |
| return m_factory; |
| _factory = (SAXTransformerFactory) TransformerFactory.newInstance(); |
| } |
| } |
| |
| _factory.setErrorListener(new TraxErrorListener(getLogger(), null)); |
| _factory.setURIResolver(this); |
| |
| // FIXME (SM): implementation-specific parameter passing should be |
| // made more extensible. |
| if (_factory.getClass().getName().equals("org.apache.xalan.processor.TransformerFactoryImpl")) { |
| _factory.setAttribute("http://xml.apache.org/xalan/features/incremental", Boolean.valueOf(m_incrementalProcessing)); |
| } |
| |
| return _factory; |
| } |
| |
| private TransformerHandlerAndValidity getTemplates(Source stylesheet, String id) throws IOException, TransformerException { |
| if (!m_useStore) { |
| return null; |
| } |
| |
| // we must augment the template ID with the factory classname since one |
| // transformer implementation cannot handle the instances of a |
| // template created by another one. |
| String key = "XSLTTemplate: " + id + '(' + m_factory.getClass().getName() + ')'; |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("getTemplates: stylesheet " + id); |
| } |
| |
| SourceValidity newValidity = stylesheet.getValidity(); |
| |
| // Only stylesheets with validity are stored |
| if (newValidity == null) { |
| // Remove an old template |
| m_store.remove(key); |
| return null; |
| } |
| |
| // Stored is an array of the templates and the caching time and list of |
| // includes |
| Object[] templateAndValidityAndIncludes = (Object[]) m_store.get(key); |
| if (templateAndValidityAndIncludes == null) { |
| // Templates not found in cache |
| return null; |
| } |
| |
| // Check template modification time |
| SourceValidity storedValidity = (SourceValidity) templateAndValidityAndIncludes[1]; |
| int valid = storedValidity.isValid(); |
| boolean isValid; |
| if (valid == 0) { |
| valid = storedValidity.isValid(newValidity); |
| isValid = (valid == 1); |
| } else { |
| isValid = (valid == 1); |
| } |
| if (!isValid) { |
| m_store.remove(key); |
| return null; |
| } |
| |
| // Check includes |
| if (m_checkIncludes) { |
| AggregatedValidity aggregated = null; |
| List includes = (List) templateAndValidityAndIncludes[2]; |
| if (includes != null) { |
| aggregated = new AggregatedValidity(); |
| aggregated.add(storedValidity); |
| |
| for (int i = includes.size() - 1; i >= 0; i--) { |
| // Every include stored as pair of source ID and validity |
| Object[] pair = (Object[]) includes.get(i); |
| storedValidity = (SourceValidity) pair[1]; |
| aggregated.add(storedValidity); |
| |
| valid = storedValidity.isValid(); |
| isValid = false; |
| if (valid == 0) { |
| Source includedSource = null; |
| try { |
| includedSource = m_resolver.resolveURI((String) pair[0]); |
| SourceValidity included = includedSource.getValidity(); |
| if (included != null) { |
| valid = storedValidity.isValid(included); |
| isValid = (valid == 1); |
| } |
| } finally { |
| m_resolver.release(includedSource); |
| } |
| } else { |
| isValid = (valid == 1); |
| } |
| if (!isValid) { |
| m_store.remove(key); |
| return null; |
| } |
| } |
| storedValidity = aggregated; |
| } |
| } |
| |
| TransformerHandler handler = m_factory.newTransformerHandler((Templates) templateAndValidityAndIncludes[0]); |
| handler.getTransformer().setErrorListener(new TraxErrorListener(getLogger(), stylesheet.getURI())); |
| handler.getTransformer().setURIResolver(this); |
| return new MyTransformerHandlerAndValidity(handler, storedValidity); |
| } |
| |
| private void putTemplates(Templates templates, Source stylesheet, String id) throws IOException { |
| if (!m_useStore) |
| return; |
| |
| // we must augment the template ID with the factory classname since one |
| // transformer implementation cannot handle the instances of a |
| // template created by another one. |
| String key = "XSLTTemplate: " + id + '(' + m_factory.getClass().getName() + ')'; |
| |
| // only stylesheets with a last modification date are stored |
| SourceValidity validity = stylesheet.getValidity(); |
| if (null != validity) { |
| // Stored is an array of the template and the current time |
| Object[] templateAndValidityAndIncludes = new Object[3]; |
| templateAndValidityAndIncludes[0] = templates; |
| templateAndValidityAndIncludes[1] = validity; |
| if (m_checkIncludes) { |
| templateAndValidityAndIncludes[2] = m_includesMap.get(id); |
| } |
| m_store.store(key, templateAndValidityAndIncludes); |
| } |
| } |
| |
| /** |
| * Called by the processor when it encounters an xsl:include, xsl:import, or |
| * document() function. |
| * |
| * @param href |
| * An href attribute, which may be relative or absolute. |
| * @param base |
| * The base URI in effect when the href attribute was |
| * encountered. |
| * |
| * @return A Source object, or null if the href cannot be resolved, and the |
| * processor should try to resolve the URI itself. |
| * |
| * @throws TransformerException |
| * if an error occurs when trying to resolve the URI. |
| */ |
| public javax.xml.transform.Source resolve(String href, String base) throws TransformerException { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("resolve(href = " + href + ", base = " + base + "); resolver = " + m_resolver); |
| } |
| |
| Source xslSource = null; |
| try { |
| if (base == null || href.indexOf(":") > 1) { |
| // Null base - href must be an absolute URL |
| xslSource = m_resolver.resolveURI(href); |
| } else if (href.length() == 0) { |
| // Empty href resolves to base |
| xslSource = m_resolver.resolveURI(base); |
| } else { |
| // is the base a file or a real m_url |
| if (!base.startsWith("file:")) { |
| int lastPathElementPos = base.lastIndexOf('/'); |
| if (lastPathElementPos == -1) { |
| // this should never occur as the base should |
| // always be protocol:/.... |
| return null; // we can't resolve this |
| } else { |
| xslSource = m_resolver.resolveURI(base.substring(0, lastPathElementPos) + "/" + href); |
| } |
| } else { |
| File parent = new File(base.substring(5)); |
| File parent2 = new File(parent.getParentFile(), href); |
| xslSource = m_resolver.resolveURI(parent2.toURL().toExternalForm()); |
| } |
| } |
| |
| InputSource is = getInputSource(xslSource); |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("xslSource = " + xslSource + ", system id = " + xslSource.getURI()); |
| } |
| |
| if (m_checkIncludes) { |
| // Populate included validities |
| List includes = (List) m_includesMap.get(base); |
| if (includes != null) { |
| SourceValidity included = xslSource.getValidity(); |
| if (included != null) { |
| includes.add(new Object[] { xslSource.getURI(), xslSource.getValidity() }); |
| } else { |
| // One of the included stylesheets is not cacheable |
| m_includesMap.remove(base); |
| } |
| } |
| } |
| |
| return new StreamSource(is.getByteStream(), is.getSystemId()); |
| } catch (SourceException e) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", e); |
| } |
| |
| // CZ: To obtain the same behaviour as when the resource is |
| // transformed by the XSLT Transformer we should return null here. |
| return null; |
| } catch (java.net.MalformedURLException mue) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", mue); |
| } |
| |
| return null; |
| } catch (IOException ioe) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", ioe); |
| } |
| |
| return null; |
| } finally { |
| m_resolver.release(xslSource); |
| } |
| } |
| |
| /** |
| * Return a new <code>InputSource</code> object that uses the |
| * <code>InputStream</code> and the system ID of the <code>Source</code> |
| * object. |
| * |
| * @throws IOException |
| * if I/O error occured. |
| */ |
| private static InputSource getInputSource(final Source source) throws IOException, SourceException { |
| final InputSource newObject = new InputSource(source.getInputStream()); |
| newObject.setSystemId(source.getURI()); |
| return newObject; |
| } |
| |
| /** |
| * Recycle the component |
| */ |
| public void recycle() { |
| m_includesMap.clear(); |
| // restore default factory |
| if (m_factory != m_defaultFactory) { |
| m_factory = m_defaultFactory; |
| } |
| } |
| |
| /** |
| * Subclass to allow for instanciation, as for some unknown reason the |
| * constructor is protected.... |
| */ |
| public static class MyTransformerHandlerAndValidity extends TransformerHandlerAndValidity { |
| |
| protected MyTransformerHandlerAndValidity(TransformerHandler handler, SourceValidity validity) { |
| super(handler, validity); |
| } |
| } |
| } |