/*
============================================================================
The Apache Software License, Version 1.1
============================================================================
Copyright (C) 1999-2002 The Apache Software Foundation. All rights reserved.
Redistribution and use in source and binary forms, with or without modifica-
tion, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. The end-user documentation included with the redistribution, if any, must
include the following acknowledgment: "This product includes software
developed by the Apache Software Foundation (http://www.apache.org/)."
Alternately, this acknowledgment may appear in the software itself, if
and wherever such third-party acknowledgments normally appear.
4. The names "Apache Cocoon" and "Apache Software Foundation" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
apache@apache.org.
5. Products derived from this software may not be called "Apache", nor may
"Apache" appear in their name, without prior written permission of the
Apache Software Foundation.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU-
DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This software consists of voluntary contributions made by many individuals
on behalf of the Apache Software Foundation and was originally created by
Stefano Mazzocchi <stefano@apache.org>. For more information on the Apache
Software Foundation, please see <http://www.apache.org/>.
*/
package org.apache.cocoon.transformation;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.component.ComponentException;
import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.component.Composable;
import org.apache.avalon.framework.component.ComponentException;
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.Loggable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.caching.CacheValidity;
import org.apache.cocoon.caching.Cacheable;
import org.apache.cocoon.components.browser.Browser;
import org.apache.cocoon.components.xslt.XSLTProcessor;
import org.apache.cocoon.components.deli.Deli;
import org.apache.cocoon.environment.Session;
import org.apache.cocoon.environment.Cookie;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Source;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.util.HashUtil;
import org.apache.cocoon.xml.XMLConsumer;
import org.xml.sax.SAXException;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.TransformerHandler;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.Iterator;
import java.util.Enumeration;
/**
* This Transformer is used to transform this incomming SAX stream using
* a XSLT stylesheet. Use the following sitemap declarations to define, configure
* and parameterize it:
*
* <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>org.apache.cocoon.components.xslt.XSLTProcessor</xslt-processor-role>
* </map:transformer>
* </pre>
*
* The <use-request-parameter> configuration forces the transformer to make all
* request parameters available in the XSLT stylesheet. Note that this might have issues
* concerning cachability of the generated output of this transformer.<br>
* This property is false by default.
* <p>
* The <use-browser-capabilities-db> configuration forces the transformer to make all
* properties from the browser capability database available in the XSLT stylesheetas.
* Note that this might have issues concerning cachability 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 stylesheetas.
* Note that this might have issues concerning cachability 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> The <use-deli> configuration forces the transformer to
* make all the properties from the CC/PP profile resolved from the
* request available in the XSLT stylesheets. CC/PP support is
* provided via the DELI library. If the request does not provide
* CC/PP information, then CC/PP information can added via the DELI
* legacy device database. This property is false by default.
*
* <p>Note that these properties might introduces issues concerning
* cacheability of the generated output of this transformer.<br>
*
*
* The <xslt-processor> configuration allows to specify the XSLT processor that will be
* used by its role name. This allows to have several XSLT processors in the configuration
* (e.g. Xalan and Saxon) and choose one or the other depending on the needs of stylesheet
* specificities.<br>
* This property defaults to "org.apache.cocoon.components.xslt.XSLTProcessor" which is the
* standard role name for an XSLTProcessor.
* <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:fumagalli@exoffice.com">Pierpaolo Fumagalli</a>
* (Apache Software Foundation, Exoffice Technologies)
* @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>
* @version CVS $Id: TraxTransformer.java,v 1.21.2.2 2002/06/16 22:45:49 vgritsenko Exp $
*/
public class TraxTransformer extends AbstractTransformer
implements Transformer, Composable, Configurable, Cacheable, Disposable {
/** The Browser service instance */
private Browser browser = null;
/** The DELI service instance */
private Deli deli = null;
/** 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 browser capability properties available in the stylesheet? (default is off) */
private boolean useBrowserCap = false;
private boolean _useBrowserCap = false;
/** Should we make the cookies availalbe in the stylesheet? (default is off) */
private boolean useCookies = false;
private boolean _useCookies = false;
/** Should we info about the session availalbe in the stylesheet? (default is off) */
private boolean useSessionInfo = false;
private boolean _useSessionInfo = false;
private ComponentManager manager;
/** The trax TransformerHandler */
TransformerHandler transformerHandler;
/** The Source */
private Source inputSource;
/** The parameters */
private Parameters par;
/** The object model */
private Map objectModel;
/** The XSLTProcessor */
private XSLTProcessor xsltProcessor;
/** Did we finish the processing (is endDocument() called) */
private boolean finishedDocument = false;
/**
* Configure this transformer.
*/
public void configure(Configuration conf)
throws ConfigurationException {
if (conf != null) {
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-browser-capabilities-db");
this.useBrowserCap = child.getValueAsBoolean(false);
this._useBrowserCap = this.useBrowserCap;
child = conf.getChild("use-session-info");
this.useSessionInfo = child.getValueAsBoolean(false);
this._useSessionInfo = this.useSessionInfo;
child = conf.getChild("xslt-processor-role");
String xsltRole = child.getValue(XSLTProcessor.ROLE);
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Use parameters is " + this.useParameters + " for " + this);
this.getLogger().debug("Use cookies is " + this.useCookies + " for " + this);
this.getLogger().debug("Use browser capabilities is " + this.useBrowserCap + " for " + this);
this.getLogger().debug("Use session info is " + this.useSessionInfo + " for " + this);
this.getLogger().debug("Use XSLTProcessor of role " + xsltRole);
}
try {
this.xsltProcessor = (XSLTProcessor)this.manager.lookup(xsltRole);
} catch (ComponentException e) {
throw new ConfigurationException("Cannot load XSLT processor", e);
}
}
}
/**
* Set the current <code>ComponentManager</code> instance used by this
* <code>Composable</code>.
*/
public void compose(ComponentManager manager) throws ComponentException {
this.manager = manager;
if (this.getLogger().isDebugEnabled()) {
getLogger().debug("Looking up " + Browser.ROLE);
}
this.browser = (Browser) manager.lookup(Browser.ROLE);
if (this.manager.hasComponent(Deli.ROLE)) {
try {
if (this.getLogger().isDebugEnabled()) {
getLogger().debug("Looking up " + Deli.ROLE);
}
this.deli = (Deli) this.manager.lookup(Deli.ROLE);
} catch (ComponentException e) {
getLogger().debug("Deli is not available");
} catch (NoClassDefFoundError e) {
getLogger().debug("Deli is not available");
}
}
}
/**
* 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 {
// Check the stylesheet uri
if (src == null) {
throw new ProcessingException("Stylesheet URI can't be null");
}
this.par = par;
this.objectModel = objectModel;
this.inputSource = resolver.resolve(src);
this.xsltProcessor.setSourceResolver(resolver);
_useParameters = par.getParameterAsBoolean("use-request-parameters", this.useParameters);
_useBrowserCap = par.getParameterAsBoolean("use-browser-capabilities-db", this.useBrowserCap);
_useCookies = par.getParameterAsBoolean("use-cookies", this.useCookies);
_useSessionInfo = par.getParameterAsBoolean("use-session-info", this.useSessionInfo);
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Using stylesheet: '"+this.inputSource.getSystemId()+"' in " + this + ", last modified: " + this.inputSource.getLastModified());
}
/** Get a Transformer Handler */
this.transformerHandler = this.xsltProcessor.getTransformerHandler(inputSource);
}
/**
* Generate the unique key.
* This key must be unique inside the space of this component.
*
* @return The generated key hashes the src
*/
public long generateKey() {
if (this.inputSource.getLastModified() != 0) {
HashMap map = getLogicSheetParameters();
if (map == null) {
return HashUtil.hash(this.inputSource.getSystemId());
}
StringBuffer sb = new StringBuffer(this.inputSource.getSystemId());
for(Iterator i = map.entrySet().iterator(); i.hasNext();){
sb.append(';');
Map.Entry entry = (Map.Entry)i.next();
sb.append(entry.getKey());
sb.append('=');
sb.append(entry.getValue());
}
return HashUtil.hash(sb.toString());
}
return 0;
}
/**
* Generate the validity object.
*
* @return The generated validity object or <code>null</code> if the
* component is currently not cacheable.
*/
public CacheValidity generateValidity() {
/*
* 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 modified time is included.
*/
return xsltProcessor.getTransformerValidity(this.inputSource);
}
/**
* Set the <code>XMLConsumer</code> that will receive XML data.
*/
public void setConsumer(XMLConsumer consumer) {
HashMap map = getLogicSheetParameters();
if (map != null) {
Iterator iterator = map.keySet().iterator();
while(iterator.hasNext()) {
String name = (String)iterator.next();
transformerHandler.getTransformer().setParameter(name,map.get(name));
}
}
super.setContentHandler(transformerHandler);
super.setLexicalHandler(transformerHandler);
if(transformerHandler instanceof Loggable) {
((Loggable)transformerHandler).setLogger(getLogger());
}
// According to TrAX specs, all TransformerHandlers are LexicalHandlers
SAXResult result = new SAXResult(consumer);
result.setLexicalHandler(consumer);
transformerHandler.setResult(result);
}
private HashMap getLogicSheetParameters() {
HashMap map = null;
if (par != null) {
String[] params = par.getNames();
if (params != null) {
for(int i = 0; i < params.length; i++) {
String name = (String) params[i];
if (isValidXSLTParameterName(name)) {
String value = par.getParameter(name,null);
if (value != null) {
if (map == null) {
map = new HashMap();
}
map.put(name,value);
}
}
}
}
}
if (this._useParameters) {
/** The Request object */
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) {
/** The Request object */
Request request = ObjectModelHelper.getRequest(objectModel);
if (map == null) map = new HashMap(5);
Session session = request.getSession(false);
if (session != null) {
map.put("session-available","true");
map.put("session-is-new",session.isNew()?"true":"false");
map.put("session-id-from-cookie",request.isRequestedSessionIdFromCookie()?"true":"false");
map.put("session-id-from-url",request.isRequestedSessionIdFromURL()?"true":"false");
map.put("session-valid",request.isRequestedSessionIdValid()?"true":"false");
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();
}
map.put(name,value);
}
}
}
}
if (this._useBrowserCap) try {
Request request = ObjectModelHelper.getRequest(objectModel);
if (map == null) {
map = new HashMap();
}
/* Get the accept header; it's needed to get the browser type. */
String accept = request.getParameter("accept");
if (accept == null)
accept = request.getHeader("accept");
/* Get the user agent; it's needed to get the browser type. */
String agent = request.getParameter("user-agent");
if (agent == null)
agent = request.getHeader("user-agent");
/* add the accept param */
map.put("accept", accept);
/* add the user agent param */
map.put("user-agent", java.net.URLEncoder.encode(agent));
/* add the map param */
HashMap agmap = browser.getBrowser(agent, accept);
map.put("browser", agmap);
/* add the media param */
String browserMedia = browser.getMedia(agmap);
map.put("browser-media", browserMedia);
/* add the uaCapabilities param */
org.w3c.dom.Document uaCapabilities = browser.getUACapabilities(agmap);
map.put("ua-capabilities", uaCapabilities);
} catch (Exception e) {
getLogger().error("Error setting Browser info", e);
}
if (this.deli != null) {
try {
Request request = ObjectModelHelper.getRequest(objectModel);
if (map == null) {
map = new HashMap();
}
org.w3c.dom.Document deliCapabilities = this.deli.getUACapabilities(request);
map.put("deli-capabilities", deliCapabilities);
String accept = request.getParameter("accept");
if (accept == null)
accept = request.getHeader("accept");
/* add the accept param */
map.put("accept", accept);
} catch (Exception e) {
getLogger().error("Error setting DELI info", e);
}
}
return map;
}
// FIXME (SM): this method may be a hotspot for requests with many
// parameters we should try to optimize it further
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;
}
public void dispose() {
this.manager.release(this.browser);
this.manager.release(this.xsltProcessor);
this.manager.release(this.deli);
this.browser = null;
this.xsltProcessor = null;
this.deli = null;
this.manager = null;
}
public void recycle() {
// Remove per-request resolver from the XSLT processor
this.xsltProcessor.setSourceResolver(null);
this.transformerHandler = null;
this.objectModel = null;
if (this.inputSource != null) {
this.inputSource.recycle();
this.inputSource = null;
}
this.par = null;
if (this.finishedDocument == false) {
try {
super.endDocument();
} catch (Exception ignore) {}
}
this.finishedDocument = false;
super.recycle();
}
/**
* SAX Event handling
* FIXME (CZ) This fixes a problem with either the XSLTProcessor
* or the byte stream compiler and comment events send before
* the startDocument event.
*/
public void comment(char ary[], int start, int length)
throws SAXException {
try {
super.comment(ary, start, length);
} catch (Exception any) {
this.getLogger().error("Error in TraxTransformer.comment.", any);
}
}
/**
* Fix for stopping hanging threads of Xalan
*/
public void endDocument()
throws SAXException {
super.endDocument();
this.finishedDocument = true;
}
}