/*
* $Id: XSLTResult.java 651946 2008-04-27 13:41:38Z apetrelli $
*
* 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.struts2.views.xslt;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
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.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
/**
* <!-- START SNIPPET: description -->
*
* XSLTResult uses XSLT to transform an action object to XML. The recent version
* has been specifically modified to deal with Xalan flaws. When using Xalan you
* may notice that even though you have a very minimal stylesheet like this one
* <pre>
* <xsl:template match="/result">
* <result/>
* </xsl:template></pre>
*
* <p>
* Xalan would still iterate through every property of your action and all
* its descendants.
* </p>
*
* <p>
* If you had double-linked objects, Xalan would work forever analysing an
* infinite object tree. Even if your stylesheet was not constructed to process
* them all. It's because the current Xalan eagerly and extensively converts
* everything to its internal DTM model before further processing.
* </p>
*
* <p>
* That's why there's a loop eliminator added that works by indexing every
* object-property combination during processing. If it notices that some
* object's property was already walked through, it doesn't go any deeper.
* Say you have two objects, x and y, with the following properties set
* (pseudocode):
* </p>
* <pre>
* x.y = y;
* and
* y.x = x;
* action.x=x;</pre>
*
* <p>
* Due to that modification, the resulting XML document based on x would be:
* </p>
*
* <pre>
* <result>
* <x>
* <y/>
* </x>
* </result></pre>
*
* <p>
* Without it there would be endless x/y/x/y/x/y/... elements.
* </p>
*
* <p>
* The XSLTResult code tries also to deal with the fact that DTM model is built
* in a manner that children are processed before siblings. The result is that if
* there is object x that is both set in action's x property, and very deeply
* under action's a property then it would only appear under a, not under x.
* That's not what we expect, and that's why XSLTResult allows objects to repeat
* in various places to some extent.
* </p>
*
* <p>
* Sometimes the object mesh is still very dense and you may notice that even
* though you have a relatively simple stylesheet, execution takes a tremendous
* amount of time. To help you to deal with that obstacle of Xalan, you may
* attach regexp filters to elements paths (xpath).
* </p>
*
* <p>
* <b>Note:</b> In your .xsl file the root match must be named <tt>result</tt>.
* <br/>This example will output the username by using <tt>getUsername</tt> on your
* action class:
* <pre>
* <xsl:template match="result">
* <html>
* <body>
* Hello <xsl:value-of select="username"/> how are you?
* </body>
* </html>
* </xsl:template>
* </pre>
*
* <p>
* In the following example the XSLT result would only walk through action's
* properties without their childs. It would also skip every property that has
* "hugeCollection" in their name. Element's path is first compared to
* excludingPattern - if it matches it's no longer processed. Then it is
* compared to matchingPattern and processed only if there's a match.
* </p>
*
* <!-- END SNIPPET: description -->
*
* <pre><!-- START SNIPPET: description.example -->
* <result name="success" type="xslt">
* <param name="location">foo.xslt</param>
* <param name="matchingPattern">^/result/[^/*]$</param>
* <param name="excludingPattern">.*(hugeCollection).*</param>
* </result>
* <!-- END SNIPPET: description.example --></pre>
*
* <p>
* In the following example the XSLT result would use the action's user property
* instead of the action as it's base document and walk through it's properties.
* The exposedValue uses an ognl expression to derive it's value.
* </p>
*
* <pre>
* <result name="success" type="xslt">
* <param name="location">foo.xslt</param>
* <param name="exposedValue">user$</param>
* </result>
* </pre>
* *
* <b>This result type takes the following parameters:</b>
*
* <!-- START SNIPPET: params -->
*
* <ul>
*
* <li><b>location (default)</b> - the location to go to after execution.</li>
*
* <li><b>parse</b> - true by default. If set to false, the location param will
* not be parsed for Ognl expressions.</li>
*
* <!--
* <li><b>matchingPattern</b> - Pattern that matches only desired elements, by
* default it matches everything.</li>
*
* <li><b>excludingPattern</b> - Pattern that eliminates unwanted elements, by
* default it matches none.</li>
* -->
*
* </ul>
*
* <p>
* <code>struts.properties</code> related configuration:
* </p>
* <ul>
*
* <li><b>struts.xslt.nocache</b> - Defaults to false. If set to true, disables
* stylesheet caching. Good for development, bad for production.</li>
*
* </ul>
*
* <!-- END SNIPPET: params -->
*
* <b>Example:</b>
*
* <pre><!-- START SNIPPET: example -->
* <result name="success" type="xslt">foo.xslt</result>
* <!-- END SNIPPET: example --></pre>
*
*/
public class XSLTResult implements Result {
private static final long serialVersionUID = 6424691441777176763L;
/** Log instance for this result. */
private static final Logger LOG = LoggerFactory.getLogger(XSLTResult.class);
/** 'stylesheetLocation' parameter. Points to the xsl. */
public static final String DEFAULT_PARAM = "stylesheetLocation";
/** Cache of all tempaltes. */
private static final Map<String, Templates> templatesCache;
static {
templatesCache = new HashMap<String, Templates>();
}
// Configurable Parameters
/** Determines whether or not the result should allow caching. */
protected boolean noCache;
/** Indicates the location of the xsl template. */
private String stylesheetLocation;
/** Indicates the property name patterns which should be exposed to the xml. */
private String matchingPattern;
/** Indicates the property name patterns which should be excluded from the xml. */
private String excludingPattern;
/** Indicates the ognl expression respresenting the bean which is to be exposed as xml. */
private String exposedValue;
private boolean parse;
private AdapterFactory adapterFactory;
public XSLTResult() {
}
public XSLTResult(String stylesheetLocation) {
this();
setStylesheetLocation(stylesheetLocation);
}
@Inject(StrutsConstants.STRUTS_XSLT_NOCACHE)
public void setNoCache(String val) {
noCache = "true".equals(val);
}
/**
* @deprecated Use #setStylesheetLocation(String)
*/
public void setLocation(String location) {
setStylesheetLocation(location);
}
public void setStylesheetLocation(String location) {
if (location == null)
throw new IllegalArgumentException("Null location");
this.stylesheetLocation = location;
}
public String getStylesheetLocation() {
return stylesheetLocation;
}
public String getExposedValue() {
return exposedValue;
}
public void setExposedValue(String exposedValue) {
this.exposedValue = exposedValue;
}
/**
* @deprecated Since 2.1.1
*/
public String getMatchingPattern() {
return matchingPattern;
}
/**
* @deprecated Since 2.1.1
*/
public void setMatchingPattern(String matchingPattern) {
this.matchingPattern = matchingPattern;
}
/**
* @deprecated Since 2.1.1
*/
public String getExcludingPattern() {
return excludingPattern;
}
/**
* @deprecated Since 2.1.1
*/
public void setExcludingPattern(String excludingPattern) {
this.excludingPattern = excludingPattern;
}
/**
* If true, parse the stylesheet location for OGNL expressions.
*
* @param parse
*/
public void setParse(boolean parse) {
this.parse = parse;
}
public void execute(ActionInvocation invocation) throws Exception {
long startTime = System.currentTimeMillis();
String location = getStylesheetLocation();
if (parse) {
ValueStack stack = ActionContext.getContext().getValueStack();
location = TextParseUtil.translateVariables(location, stack);
}
try {
HttpServletResponse response = ServletActionContext.getResponse();
PrintWriter writer = response.getWriter();
// Create a transformer for the stylesheet.
Templates templates = null;
Transformer transformer;
if (location != null) {
templates = getTemplates(location);
transformer = templates.newTransformer();
} else
transformer = TransformerFactory.newInstance().newTransformer();
transformer.setURIResolver(getURIResolver());
transformer.setErrorListener(new ErrorListener() {
public void error(TransformerException exception)
throws TransformerException {
throw new StrutsException("Error transforming result", exception);
}
public void fatalError(TransformerException exception)
throws TransformerException {
throw new StrutsException("Fatal error transforming result", exception);
}
public void warning(TransformerException exception)
throws TransformerException {
LOG.warn(exception.getMessage(), exception);
}
});
String mimeType;
if (templates == null)
mimeType = "text/xml"; // no stylesheet, raw xml
else
mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
if (mimeType == null) {
// guess (this is a servlet, so text/html might be the best guess)
mimeType = "text/html";
}
response.setContentType(mimeType);
Object result = invocation.getAction();
if (exposedValue != null) {
ValueStack stack = invocation.getStack();
result = stack.findValue(exposedValue);
}
Source xmlSource = getDOMSourceForStack(result);
// Transform the source XML to System.out.
LOG.debug("xmlSource = " + xmlSource);
transformer.transform(xmlSource, new StreamResult(writer));
writer.flush(); // ...and flush...
if (LOG.isDebugEnabled()) {
LOG.debug("Time:" + (System.currentTimeMillis() - startTime) + "ms");
}
} catch (Exception e) {
LOG.error("Unable to render XSLT Template, '" + location + "'", e);
throw e;
}
}
protected AdapterFactory getAdapterFactory() {
if (adapterFactory == null)
adapterFactory = new AdapterFactory();
return adapterFactory;
}
protected void setAdapterFactory(AdapterFactory adapterFactory) {
this.adapterFactory = adapterFactory;
}
/**
* Get the URI Resolver to be called by the processor when it encounters an xsl:include, xsl:import, or document()
* function. The default is an instance of ServletURIResolver, which operates relative to the servlet context.
*/
protected URIResolver getURIResolver() {
return new ServletURIResolver(
ServletActionContext.getServletContext());
}
protected Templates getTemplates(String path) throws TransformerException, IOException {
String pathFromRequest = ServletActionContext.getRequest().getParameter("xslt.location");
if (pathFromRequest != null)
path = pathFromRequest;
if (path == null)
throw new TransformerException("Stylesheet path is null");
Templates templates = templatesCache.get(path);
if (noCache || (templates == null)) {
synchronized (templatesCache) {
URL resource = ServletActionContext.getServletContext().getResource(path);
if (resource == null) {
throw new TransformerException("Stylesheet " + path + " not found in resources.");
}
LOG.debug("Preparing XSLT stylesheet templates: " + path);
TransformerFactory factory = TransformerFactory.newInstance();
factory.setURIResolver(getURIResolver());
templates = factory.newTemplates(new StreamSource(resource.openStream()));
templatesCache.put(path, templates);
}
}
return templates;
}
protected Source getDOMSourceForStack(Object value)
throws IllegalAccessException, InstantiationException {
return new DOMSource(getAdapterFactory().adaptDocument("result", value) );
}
}
|