View Javadoc

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