/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.transformation; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Method; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.TransformerHandler; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.logger.LogEnabled; 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.cocoon.ProcessingException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.components.source.SourceUtil; import org.apache.cocoon.components.xslt.TraxErrorListener; import org.apache.cocoon.environment.Cookie; import org.apache.cocoon.environment.ObjectModelHelper; import org.apache.cocoon.environment.Request; import org.apache.cocoon.environment.Session; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.xml.XMLConsumer; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.exception.NestableRuntimeException; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.xml.xslt.XSLTProcessor; import org.apache.excalibur.xml.xslt.XSLTProcessorException; import org.xml.sax.SAXException; /** * @cocoon.sitemap.component.documentation * The stylesheet processor * * @cocoon.sitemap.component.name xslt * @cocoon.sitemap.component.logger sitemap.transformer.xslt * @cocoon.sitemap.component.documentation.caching * Uses the last modification date of the xslt document for validation * * @cocoon.sitemap.component.pooling.max 32 * <p> * This Transformer is used to transform the incoming SAX stream using * a TrAXProcessor. Use the following sitemap declarations to define, configure * and parameterize it: * </p> * <b>In the map:sitemap/map:components/map:transformers:</b><br> * <pre> * <map:transformer name="xslt" src="org.apache.cocoon.transformation.TraxTransformer"><br> * <use-request-parameters>false</use-request-parameters> * <use-browser-capabilities-db>false</use-browser-capabilities-db> * <use-session-info>false</use-session-info> * <xslt-processor-role>xslt</xslt-processor-role> * <transformer-factory>org.apache.xalan.processor.TransformerFactoryImpl</transformer-factory> * <check-includes>true</check-includes> * </map:transformer> * </pre> * * The <use-request-parameter> configuration forces the transformer to make all * request parameters available in the XSLT stylesheet. Note that this has * implications for caching of the generated output of this transformer.<br> * This property is false by default. * <p> * The <use-cookies> configuration forces the transformer to make all * cookies from the request available in the XSLT stylesheets. * Note that this has implications for caching of the generated output of this * transformer.<br> * This property is false by default. * <p> * The <use-session-info> configuration forces the transformer to make all * of the session information available in the XSLT stylesheetas.<br> * These infos are (boolean values are "true" or "false" strings: session-is-new, * session-id-from-cookie, session-id-from-url, session-valid, session-id.<br> * This property is false by default. * * <p>Note that this has implications for caching of the generated output of * this transformer.<br> * * * The <xslt-processor-role> configuration allows to specify the TrAX processor (defined in * the cocoon.xconf) that will be used to obtain the XSLT processor. This allows to have * several XSLT processors in the configuration (e.g. Xalan, XSLTC, Saxon, ...) and choose * one or the other depending on the needs of stylesheet specificities.<br> * If no processor is specified, this transformer will use the XSLT implementation * that Cocoon uses internally. * * The <transformer-factory> configuration allows to specify the TrAX transformer factory * implementation that will be used to obtain the XSLT processor. This is only useful for * compatibility reasons. Please configure the XSLT processor in the cocoon.xconf properly * and use the xslt-processor-role configuration mentioned above. * * The <check-includes> configuration specifies if the included stylesheets are * also checked for changes during caching. If this is set to true (default), the * included stylesheets are also checked for changes; if this is set to false, only * the main stylesheet is checked. Setting this to false improves the performance, * and should be used whenever no includes are in the stylesheet. However, if * you have includes, you have to be careful when changing included stylesheets * as the changes might not take effect immediately. You should touch the main * stylesheet as well. * * <p> * <b>In a map:sitemap/map:pipelines/map:pipeline:</b><br> * <pre> * <map:transform type="xslt" src="stylesheets/yours.xsl"><br> * <parameter name="myparam" value="myvalue"/> * </map:transform> * </pre> * All <parameter> declarations will be made available in the XSLT stylesheet as * xsl:variables. * * @author <a href="mailto:pier@apache.org">Pierpaolo Fumagalli</a> * @author <a href="mailto:dims@yahoo.com">Davanum Srinivas</a> * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a> * @author <a href="mailto:giacomo@apache.org">Giacomo Pati</a> * @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu</a> * @author <a href="mailto:marbut@hplb.hpl.hp.com">Mark H. Butler</a> * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a> * * @version SVN $Id$ */ public class TraxTransformer extends AbstractTransformer implements Serviceable, Configurable, CacheableProcessingComponent, Disposable { /** The service manager instance (protected because used by subclasses) */ protected ServiceManager manager; /** The object model (protected because used by subclasses) */ protected Map objectModel; /** Logicsheet parameters (protected because used by subclasses) */ protected Map logicSheetParameters; /** Should we make the request parameters available in the stylesheet? (default is off) */ private boolean useParameters = false; private boolean _useParameters = false; /** Should we make the cookies available in the stylesheet? (default is off) */ private boolean useCookies = false; private boolean _useCookies = false; /** Should we info about the session available in the stylesheet? (default is off) */ private boolean useSessionInfo = false; private boolean _useSessionInfo = false; /** Do we check included stylesheets for changes? */ private boolean checkIncludes = true; /** The trax TransformerHandler */ protected TransformerHandler transformerHandler; /** The validity of the Transformer */ protected SourceValidity transformerValidity; /** The Source */ private Source inputSource; /** The parameters */ private Parameters par; /** The source resolver */ private SourceResolver resolver; /** Default source, used to create specialized transformers by configuration */ private String defaultSrc; /** The XSLTProcessor */ private XSLTProcessor xsltProcessor; /** Did we finish the processing (is endDocument() called) */ private boolean finishedDocument = false; /** Xalan's DTMManager.getIncremental() method. See recycle() method to see what we need this for. */ private Method xalanDtmManagerGetIncrementalMethod; /** Exception that might occur during setConsumer */ private SAXException exceptionDuringSetConsumer; /** The error listener used by the stylesheet */ private TraxErrorListener errorListener; /** * Configure this transformer. */ public void configure(Configuration conf) throws ConfigurationException { Configuration child; child = conf.getChild("use-request-parameters"); this.useParameters = child.getValueAsBoolean(false); this._useParameters = this.useParameters; child = conf.getChild("use-cookies"); this.useCookies = child.getValueAsBoolean(false); this._useCookies = this.useCookies; child = conf.getChild("use-session-info"); this.useSessionInfo = child.getValueAsBoolean(false); this._useSessionInfo = this.useSessionInfo; child = conf.getChild("transformer-factory"); // traxFactory is null, if transformer-factory config is unspecified final String traxFactory = child.getValue(null); child = conf.getChild("xslt-processor-role"); String xsltProcessorRole = child.getValue(XSLTProcessor.ROLE); if (!xsltProcessorRole.startsWith(XSLTProcessor.ROLE)) { xsltProcessorRole = XSLTProcessor.ROLE + '/' + xsltProcessorRole; } child = conf.getChild("check-includes"); this.checkIncludes = child.getValueAsBoolean(this.checkIncludes); child = conf.getChild("default-src",false); if(child!=null) { this.defaultSrc = child.getValue(); } if (getLogger().isDebugEnabled()) { getLogger().debug("Use parameters is " + this.useParameters); getLogger().debug("Use cookies is " + this.useCookies); getLogger().debug("Use session info is " + this.useSessionInfo); getLogger().debug("Use TrAX Processor " + xsltProcessorRole); getLogger().debug("Check for included stylesheets is " + this.checkIncludes); if (traxFactory != null) { getLogger().debug("Use TrAX Transformer Factory " + traxFactory); } else { getLogger().debug("Use default TrAX Transformer Factory."); } getLogger().debug("Default source = " + this.defaultSrc); } try { this.xsltProcessor = (XSLTProcessor) this.manager.lookup(xsltProcessorRole); if (traxFactory != null) { this.xsltProcessor.setTransformerFactory(traxFactory); } } catch (ServiceException e) { throw new ConfigurationException("Cannot load XSLT processor", e); } try { // see the recyle() method to see what we need this for Class dtmManagerClass = Class.forName("org.apache.xml.dtm.DTMManager"); xalanDtmManagerGetIncrementalMethod = dtmManagerClass.getMethod("getIncremental", null); } catch (ClassNotFoundException e) { // do nothing -- user does not use xalan, so we don't need the dtm manager } catch (NoSuchMethodException e) { throw new ConfigurationException("Was not able to get getIncremental method from Xalan's DTMManager.", e); } } /** * Set the current <code>ServiceManager</code> instance used by this * <code>Serviceable</code>. */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; } /** * Set the <code>SourceResolver</code>, the <code>Map</code> with * the object model, the source and sitemap * <code>Parameters</code> used to process the request. */ public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws SAXException, ProcessingException, IOException { if(src==null && defaultSrc!=null) { if(getLogger().isDebugEnabled()) { getLogger().debug("src is null, using default source " + defaultSrc); } src = defaultSrc; } if (src == null) { throw new ProcessingException("Stylesheet URI can't be null"); } this.par = par; this.objectModel = objectModel; this.resolver = resolver; try { this.inputSource = resolver.resolveURI(src); } catch (SourceException se) { throw SourceUtil.handle("Unable to resolve " + src, se); } _useParameters = par.getParameterAsBoolean("use-request-parameters", this.useParameters); _useCookies = par.getParameterAsBoolean("use-cookies", this.useCookies); _useSessionInfo = par.getParameterAsBoolean("use-session-info", this.useSessionInfo); final boolean _checkIncludes = par.getParameterAsBoolean("check-includes", this.checkIncludes); if (getLogger().isDebugEnabled()) { getLogger().debug("Using stylesheet: '" + this.inputSource.getURI() + "' in " + this); getLogger().debug("Use parameters is " + this._useParameters); getLogger().debug("Use cookies is " + this._useCookies); getLogger().debug("Use session info is " + this._useSessionInfo); getLogger().debug("Check for included stylesheets is " + _checkIncludes); } // Get a Transformer Handler if we check for includes // If we don't check the handler is get during setConsumer() try { if ( _checkIncludes ) { XSLTProcessor.TransformerHandlerAndValidity handlerAndValidity = this.xsltProcessor.getTransformerHandlerAndValidity(this.inputSource, null); this.transformerHandler = handlerAndValidity.getTransfomerHandler(); this.transformerValidity = handlerAndValidity.getTransfomerValidity(); } else { this.transformerValidity = this.inputSource.getValidity(); } } catch (XSLTProcessorException se) { throw new ProcessingException("Unable to get transformer handler for " + this.inputSource.getURI(), se); } } /** * Generate the unique key. * This key must be unique inside the space of this component. * * @return The generated key hashes the src */ public Serializable getKey() { Map map = getLogicSheetParameters(); if (map == null) { return this.inputSource.getURI(); } StringBuffer sb = new StringBuffer(); sb.append(this.inputSource.getURI()); Set entries = map.entrySet(); for(Iterator i=entries.iterator(); i.hasNext();){ sb.append(';'); Map.Entry entry = (Map.Entry)i.next(); sb.append(entry.getKey()); sb.append('='); sb.append(entry.getValue()); } return sb.toString(); } /** * Generate the validity object. * * @return The generated validity object or <code>null</code> if the * component is currently not cacheable. */ public SourceValidity getValidity() { // // VG: Key is generated using parameter/value pairs, // so this information does not need to be verified again // (if parameter added/removed or value changed, key should // change also), only stylesheet's validity is included. // return this.transformerValidity; } /** * Set the <code>XMLConsumer</code> that will receive XML data. */ public void setConsumer(XMLConsumer consumer) { if ( this.transformerHandler == null ) { try { this.transformerHandler = this.xsltProcessor.getTransformerHandler(this.inputSource); } catch (XSLTProcessorException se) { // the exception will be thrown during startDocument() this.exceptionDuringSetConsumer = new SAXException("Unable to get transformer handler for " + this.inputSource.getURI(), se); return; } } final Map map = getLogicSheetParameters(); if (map != null) { final javax.xml.transform.Transformer transformer = this.transformerHandler.getTransformer(); final Iterator iterator = map.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry entry = (Entry) iterator.next(); transformer.setParameter((String)entry.getKey(), entry.getValue()); } } super.setContentHandler(this.transformerHandler); super.setLexicalHandler(this.transformerHandler); if (this.transformerHandler instanceof LogEnabled) { ((LogEnabled)this.transformerHandler).enableLogging(getLogger()); } // According to TrAX specs, all TransformerHandlers are LexicalHandlers final SAXResult result = new SAXResult(consumer); result.setLexicalHandler(consumer); this.transformerHandler.setResult(result); this.errorListener = new TraxErrorListener(getLogger(), this.inputSource.getURI()); this.transformerHandler.getTransformer().setErrorListener(this.errorListener); } /** * Get the parameters for the logicsheet */ protected Map getLogicSheetParameters() { if (this.logicSheetParameters != null) { return this.logicSheetParameters; } HashMap map = null; if (par != null) { String[] params = par.getNames(); if (params != null) { for(int i = 0; i < params.length; i++) { String name = params[i]; if (isValidXSLTParameterName(name)) { String value = par.getParameter(name,null); if (value != null) { if (map == null) { map = new HashMap(params.length); } map.put(name,value); } } } } } if (this._useParameters) { Request request = ObjectModelHelper.getRequest(objectModel); Enumeration parameters = request.getParameterNames(); if (parameters != null) { while (parameters.hasMoreElements()) { String name = (String) parameters.nextElement(); if (isValidXSLTParameterName(name)) { String value = request.getParameter(name); if (map == null) { map = new HashMap(); } map.put(name,value); } } } } if (this._useSessionInfo) { final Request request = ObjectModelHelper.getRequest(objectModel); if (map == null) { map = new HashMap(6); } final Session session = request.getSession(false); if (session != null) { map.put("session-available", "true"); map.put("session-is-new", BooleanUtils.toStringTrueFalse(session.isNew())); map.put("session-id-from-cookie", BooleanUtils.toStringTrueFalse(request.isRequestedSessionIdFromCookie())); map.put("session-id-from-url", BooleanUtils.toStringTrueFalse(request.isRequestedSessionIdFromURL())); map.put("session-valid", BooleanUtils.toStringTrueFalse(request.isRequestedSessionIdValid())); map.put("session-id", session.getId()); } else { map.put("session-available", "false"); } } if (this._useCookies) { Request request = ObjectModelHelper.getRequest(objectModel); Cookie cookies[] = request.getCookies(); if (cookies != null) { for (int i = 0; i < cookies.length; i++) { String name = cookies[i].getName(); if (isValidXSLTParameterName(name)) { String value = cookies[i].getValue(); if (map == null) { map = new HashMap(cookies.length); } map.put(name,value); } } } } this.logicSheetParameters = map; return this.logicSheetParameters; } /** * Test if the name is a valid parameter name for XSLT */ static boolean isValidXSLTParameterName(String name) { if (name.length() == 0) { return false; } char c = name.charAt(0); if (!(Character.isLetter(c) || c == '_')) { return false; } for (int i = name.length()-1; i > 1; i--) { c = name.charAt(i); if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == '.')) { return false; } } return true; } /** * Disposable */ public void dispose() { if ( this.manager != null ) { this.manager.release(this.xsltProcessor); this.xsltProcessor = null; this.manager = null; } } /** * Recyclable */ public void recycle() { this.objectModel = null; if (this.inputSource != null) { this.resolver.release(this.inputSource); this.inputSource = null; } this.resolver = null; this.par = null; if (!this.finishedDocument && transformerHandler != null) { // This situation will only occur if an exception occured during pipeline execution. // If Xalan is used in incremental mode, it is important that endDocument is called, otherwise // the thread on which it runs the transformation will keep waiting. // However, calling endDocument will cause the pipeline to continue executing, and thus the // serializer will write output to the outputstream after what's already there (the error page), // see also bug 13186. if (xalanDtmManagerGetIncrementalMethod != null && transformerHandler.getClass().getName().equals("org.apache.xalan.transformer.TransformerHandlerImpl")) { try { final boolean incremental = ((Boolean)xalanDtmManagerGetIncrementalMethod.invoke(null, null)).booleanValue(); if (incremental) { super.endDocument(); } } catch (Exception ignore) {} } } this.finishedDocument = true; this.logicSheetParameters = null; this.transformerHandler = null; this.transformerValidity = null; this.exceptionDuringSetConsumer = null; this.errorListener = null; super.recycle(); } /** * Fix for stopping hanging threads of Xalan */ public void endDocument() throws SAXException { try { super.endDocument(); } catch(Exception e) { Throwable realEx = this.errorListener.getThrowable(); if (realEx == null) realEx = e; if (realEx instanceof RuntimeException) { throw (RuntimeException)realEx; } if (realEx instanceof SAXException) { throw (SAXException)realEx; } if (realEx instanceof Error) { throw (Error)realEx; } throw new NestableRuntimeException(realEx); } this.finishedDocument = true; } /* (non-Javadoc) * @see org.xml.sax.ContentHandler#startDocument() */ public void startDocument() throws SAXException { // did an exception occur during setConsumer? // if so, throw it here if ( this.exceptionDuringSetConsumer != null ) { throw this.exceptionDuringSetConsumer; } this.finishedDocument = false; super.startDocument(); } }