1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <xsl:template match="/result">
66 * <result/>
67 * </xsl:template></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 * <result>
100 * <x>
101 * <y/>
102 * </x>
103 * </result></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 * <xsl:template match="result">
131 * <html>
132 * <body>
133 * Hello <xsl:value-of select="username"/> how are you?
134 * </body>
135 * </html>
136 * </xsl:template>
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 * <result name="success" type="xslt">
151 * <param name="location">foo.xslt</param>
152 * <param name="matchingPattern">^/result/[^/*]$</param>
153 * <param name="excludingPattern">.*(hugeCollection).*</param>
154 * </result>
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 * <result name="success" type="xslt">
165 * <param name="location">foo.xslt</param>
166 * <param name="exposedValue">user$</param>
167 * </result>
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 * <result name="success" type="xslt">foo.xslt</result>
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
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
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";
369 else
370 mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
371 if (mimeType == null) {
372
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
387 LOG.debug("xmlSource = " + xmlSource);
388 transformer.transform(xmlSource, new StreamResult(writer));
389
390 writer.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 }