| /* |
| * Copyright 1999-2004 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.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. |
| * |
| * @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(); |
| } |
| } |