View Javadoc

1   /*
2    * $Id: XSLTResult.java 651946 2008-04-27 13:41:38Z apetrelli $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.views.xslt;
23  
24  import java.io.IOException;
25  import java.io.PrintWriter;
26  import java.io.Writer;
27  import java.net.URL;
28  import java.util.HashMap;
29  import java.util.Map;
30  
31  import javax.servlet.http.HttpServletResponse;
32  import javax.xml.transform.ErrorListener;
33  import javax.xml.transform.OutputKeys;
34  import javax.xml.transform.Source;
35  import javax.xml.transform.Templates;
36  import javax.xml.transform.Transformer;
37  import javax.xml.transform.TransformerException;
38  import javax.xml.transform.TransformerFactory;
39  import javax.xml.transform.URIResolver;
40  import javax.xml.transform.dom.DOMSource;
41  import javax.xml.transform.stream.StreamResult;
42  import javax.xml.transform.stream.StreamSource;
43  
44  import org.apache.struts2.ServletActionContext;
45  import org.apache.struts2.StrutsConstants;
46  import org.apache.struts2.StrutsException;
47  
48  import com.opensymphony.xwork2.ActionContext;
49  import com.opensymphony.xwork2.ActionInvocation;
50  import com.opensymphony.xwork2.Result;
51  import com.opensymphony.xwork2.inject.Inject;
52  import com.opensymphony.xwork2.util.TextParseUtil;
53  import com.opensymphony.xwork2.util.ValueStack;
54  import com.opensymphony.xwork2.util.logging.Logger;
55  import com.opensymphony.xwork2.util.logging.LoggerFactory;
56  
57  
58  /***
59   * <!-- START SNIPPET: description -->
60   *
61   * XSLTResult uses XSLT to transform an action object to XML. The recent version
62   * has been specifically modified to deal with Xalan flaws. When using Xalan you
63   * may notice that even though you have a very minimal stylesheet like this one
64   * <pre>
65   * &lt;xsl:template match="/result"&gt;
66   *   &lt;result/&gt;
67   * &lt;/xsl:template&gt;</pre>
68   *
69   * <p>
70   * Xalan would still iterate through every property of your action and all
71   * its descendants.
72   * </p>
73   *
74   * <p>
75   * If you had double-linked objects, Xalan would work forever analysing an
76   * infinite object tree. Even if your stylesheet was not constructed to process
77   * them all. It's because the current Xalan eagerly and extensively converts
78   * everything to its internal DTM model before further processing.
79   * </p>
80   *
81   * <p>
82   * That's why there's a loop eliminator added that works by indexing every
83   * object-property combination during processing. If it notices that some
84   * object's property was already walked through, it doesn't go any deeper.
85   * Say you have two objects, x and y, with the following properties set
86   * (pseudocode):
87   * </p>
88   * <pre>
89   * x.y = y;
90   * and
91   * y.x = x;
92   * action.x=x;</pre>
93   *
94   * <p>
95   * Due to that modification, the resulting XML document based on x would be:
96   * </p>
97   *
98   * <pre>
99   * &lt;result&gt;
100  *   &lt;x&gt;
101  *     &lt;y/&gt;
102  *   &lt;/x&gt;
103  * &lt;/result&gt;</pre>
104  *
105  * <p>
106  * Without it there would be endless x/y/x/y/x/y/... elements.
107  * </p>
108  *
109  * <p>
110  * The XSLTResult code tries also to deal with the fact that DTM model is built
111  * in a manner that children are processed before siblings. The result is that if
112  * there is object x that is both set in action's x property, and very deeply
113  * under action's a property then it would only appear under a, not under x.
114  * That's not what we expect, and that's why XSLTResult allows objects to repeat
115  * in various places to some extent.
116  * </p>
117  *
118  * <p>
119  * Sometimes the object mesh is still very dense and you may notice that even
120  * though you have a relatively simple stylesheet, execution takes a tremendous
121  * amount of time. To help you to deal with that obstacle of Xalan, you may
122  * attach regexp filters to elements paths (xpath).
123  * </p>
124  *
125  * <p>
126  * <b>Note:</b> In your .xsl file the root match must be named <tt>result</tt>.
127  * <br/>This example will output the username by using <tt>getUsername</tt> on your
128  * action class:
129  * <pre>
130  * &lt;xsl:template match="result"&gt;
131  *   &lt;html&gt;
132  *   &lt;body&gt;
133  *   Hello &lt;xsl:value-of select="username"/&gt; how are you?
134  *   &lt;/body&gt;
135  *   &lt;/html&gt;
136  * &lt;/xsl:template&gt;
137  * </pre>
138  *
139  * <p>
140  * In the following example the XSLT result would only walk through action's
141  * properties without their childs. It would also skip every property that has
142  * "hugeCollection" in their name. Element's path is first compared to
143  * excludingPattern - if it matches it's no longer processed. Then it is
144  * compared to matchingPattern and processed only if there's a match.
145  * </p>
146  *
147  * <!-- END SNIPPET: description -->
148  *
149  * <pre><!-- START SNIPPET: description.example -->
150  * &lt;result name="success" type="xslt"&gt;
151  *   &lt;param name="location"&gt;foo.xslt&lt;/param&gt;
152  *   &lt;param name="matchingPattern"&gt;^/result/[^/*]$&lt;/param&gt;
153  *   &lt;param name="excludingPattern"&gt;.*(hugeCollection).*&lt;/param&gt;
154  * &lt;/result&gt;
155  * <!-- END SNIPPET: description.example --></pre>
156  *
157  * <p>
158  * In the following example the XSLT result would use the action's user property
159  * instead of the action as it's base document and walk through it's properties.
160  * The exposedValue uses an ognl expression to derive it's value.
161  * </p>
162  *
163  * <pre>
164  * &lt;result name="success" type="xslt"&gt;
165  *   &lt;param name="location"&gt;foo.xslt&lt;/param&gt;
166  *   &lt;param name="exposedValue"&gt;user$&lt;/param&gt;
167  * &lt;/result&gt;
168  * </pre>
169  * *
170  * <b>This result type takes the following parameters:</b>
171  *
172  * <!-- START SNIPPET: params -->
173  *
174  * <ul>
175  *
176  * <li><b>location (default)</b> - the location to go to after execution.</li>
177  *
178  * <li><b>parse</b> - true by default. If set to false, the location param will
179  * not be parsed for Ognl expressions.</li>
180  *
181  * <!--
182  * <li><b>matchingPattern</b> - Pattern that matches only desired elements, by
183  * default it matches everything.</li>
184  *
185  * <li><b>excludingPattern</b> - Pattern that eliminates unwanted elements, by
186  * default it matches none.</li>
187  * -->
188  *
189  * </ul>
190  *
191  * <p>
192  * <code>struts.properties</code> related configuration:
193  * </p>
194  * <ul>
195  *
196  * <li><b>struts.xslt.nocache</b> - Defaults to false. If set to true, disables
197  * stylesheet caching. Good for development, bad for production.</li>
198  *
199  * </ul>
200  *
201  * <!-- END SNIPPET: params -->
202  *
203  * <b>Example:</b>
204  *
205  * <pre><!-- START SNIPPET: example -->
206  * &lt;result name="success" type="xslt"&gt;foo.xslt&lt;/result&gt;
207  * <!-- END SNIPPET: example --></pre>
208  *
209  */
210 public class XSLTResult implements Result {
211 
212     private static final long serialVersionUID = 6424691441777176763L;
213 
214     /*** Log instance for this result. */
215     private static final Logger LOG = LoggerFactory.getLogger(XSLTResult.class);
216 
217     /*** 'stylesheetLocation' parameter.  Points to the xsl. */
218     public static final String DEFAULT_PARAM = "stylesheetLocation";
219 
220     /*** Cache of all tempaltes. */
221     private static final Map<String, Templates> templatesCache;
222 
223     static {
224         templatesCache = new HashMap<String, Templates>();
225     }
226 
227     // Configurable Parameters
228 
229     /*** Determines whether or not the result should allow caching. */
230     protected boolean noCache;
231 
232     /*** Indicates the location of the xsl template. */
233     private String stylesheetLocation;
234 
235     /*** Indicates the property name patterns which should be exposed to the xml. */
236     private String matchingPattern;
237 
238     /*** Indicates the property name patterns which should be excluded from the xml. */
239     private String excludingPattern;
240 
241     /*** Indicates the ognl expression respresenting the bean which is to be exposed as xml. */
242     private String exposedValue;
243 
244     private boolean parse;
245     private AdapterFactory adapterFactory;
246 
247     public XSLTResult() {
248     }
249 
250     public XSLTResult(String stylesheetLocation) {
251         this();
252         setStylesheetLocation(stylesheetLocation);
253     }
254     
255     @Inject(StrutsConstants.STRUTS_XSLT_NOCACHE)
256     public void setNoCache(String val) {
257         noCache = "true".equals(val);
258     }
259 
260     /***
261      * @deprecated Use #setStylesheetLocation(String)
262      */
263     public void setLocation(String location) {
264         setStylesheetLocation(location);
265     }
266 
267     public void setStylesheetLocation(String location) {
268         if (location == null)
269             throw new IllegalArgumentException("Null location");
270         this.stylesheetLocation = location;
271     }
272 
273     public String getStylesheetLocation() {
274         return stylesheetLocation;
275     }
276 
277     public String getExposedValue() {
278         return exposedValue;
279     }
280 
281     public void setExposedValue(String exposedValue) {
282         this.exposedValue = exposedValue;
283     }
284 
285     /***
286      * @deprecated Since 2.1.1
287      */
288     public String getMatchingPattern() {
289         return matchingPattern;
290     }
291 
292     /***
293      * @deprecated Since 2.1.1
294      */
295     public void setMatchingPattern(String matchingPattern) {
296         this.matchingPattern = matchingPattern;
297     }
298 
299     /***
300      * @deprecated Since 2.1.1
301      */
302     public String getExcludingPattern() {
303         return excludingPattern;
304     }
305 
306     /***
307      * @deprecated Since 2.1.1
308      */
309     public void setExcludingPattern(String excludingPattern) {
310         this.excludingPattern = excludingPattern;
311     }
312 
313     /***
314      * If true, parse the stylesheet location for OGNL expressions.
315      *
316      * @param parse
317      */
318     public void setParse(boolean parse) {
319         this.parse = parse;
320     }
321 
322     public void execute(ActionInvocation invocation) throws Exception {
323         long startTime = System.currentTimeMillis();
324         String location = getStylesheetLocation();
325 
326         if (parse) {
327             ValueStack stack = ActionContext.getContext().getValueStack();
328             location = TextParseUtil.translateVariables(location, stack);
329         }
330 
331 
332         try {
333             HttpServletResponse response = ServletActionContext.getResponse();
334 
335             PrintWriter writer = response.getWriter();
336 
337             // Create a transformer for the stylesheet.
338             Templates templates = null;
339             Transformer transformer;
340             if (location != null) {
341                 templates = getTemplates(location);
342                 transformer = templates.newTransformer();
343             } else
344                 transformer = TransformerFactory.newInstance().newTransformer();
345 
346             transformer.setURIResolver(getURIResolver());
347             transformer.setErrorListener(new ErrorListener() {
348 
349                 public void error(TransformerException exception)
350                         throws TransformerException {
351                     throw new StrutsException("Error transforming result", exception);
352                 }
353 
354                 public void fatalError(TransformerException exception)
355                         throws TransformerException {
356                     throw new StrutsException("Fatal error transforming result", exception);
357                 }
358 
359                 public void warning(TransformerException exception)
360                         throws TransformerException {
361                     LOG.warn(exception.getMessage(), exception);
362                 }
363                 
364             });
365 
366             String mimeType;
367             if (templates == null)
368                 mimeType = "text/xml"; // no stylesheet, raw xml
369             else
370                 mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
371             if (mimeType == null) {
372                 // guess (this is a servlet, so text/html might be the best guess)
373                 mimeType = "text/html";
374             }
375 
376             response.setContentType(mimeType);
377 
378             Object result = invocation.getAction();
379             if (exposedValue != null) {
380                 ValueStack stack = invocation.getStack();
381                 result = stack.findValue(exposedValue);
382             }
383 
384             Source xmlSource = getDOMSourceForStack(result);
385 
386             // Transform the source XML to System.out.
387             LOG.debug("xmlSource = " + xmlSource);
388             transformer.transform(xmlSource, new StreamResult(writer));
389 
390             writer.flush(); // ...and flush...
391 
392             if (LOG.isDebugEnabled()) {
393                 LOG.debug("Time:" + (System.currentTimeMillis() - startTime) + "ms");
394             }
395 
396         } catch (Exception e) {
397             LOG.error("Unable to render XSLT Template, '" + location + "'", e);
398             throw e;
399         }
400     }
401 
402     protected AdapterFactory getAdapterFactory() {
403         if (adapterFactory == null)
404             adapterFactory = new AdapterFactory();
405         return adapterFactory;
406     }
407 
408     protected void setAdapterFactory(AdapterFactory adapterFactory) {
409         this.adapterFactory = adapterFactory;
410     }
411 
412     /***
413      * Get the URI Resolver to be called by the processor when it encounters an xsl:include, xsl:import, or document()
414      * function. The default is an instance of ServletURIResolver, which operates relative to the servlet context.
415      */
416     protected URIResolver getURIResolver() {
417         return new ServletURIResolver(
418                 ServletActionContext.getServletContext());
419     }
420 
421     protected Templates getTemplates(String path) throws TransformerException, IOException {
422         String pathFromRequest = ServletActionContext.getRequest().getParameter("xslt.location");
423 
424         if (pathFromRequest != null)
425             path = pathFromRequest;
426 
427         if (path == null)
428             throw new TransformerException("Stylesheet path is null");
429 
430         Templates templates = templatesCache.get(path);
431 
432         if (noCache || (templates == null)) {
433             synchronized (templatesCache) {
434                 URL resource = ServletActionContext.getServletContext().getResource(path);
435 
436                 if (resource == null) {
437                     throw new TransformerException("Stylesheet " + path + " not found in resources.");
438                 }
439 
440                 LOG.debug("Preparing XSLT stylesheet templates: " + path);
441 
442                 TransformerFactory factory = TransformerFactory.newInstance();
443                 factory.setURIResolver(getURIResolver());
444                 templates = factory.newTemplates(new StreamSource(resource.openStream()));
445                 templatesCache.put(path, templates);
446             }
447         }
448 
449         return templates;
450     }
451 
452     protected Source getDOMSourceForStack(Object value)
453             throws IllegalAccessException, InstantiationException {
454         return new DOMSource(getAdapterFactory().adaptDocument("result", value) );
455     }
456 }