View Javadoc

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