View Javadoc

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