1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <xsl:template match="/result">
63 * <result />
64 * </xsl:template></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 * <result>
97 * <x>
98 * <y/>
99 * </x>
100 * </result></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 * <xsl:template match="result">
128 * <html>
129 * <body>
130 * Hello <xsl:value-of select="username"/> how are you?
131 * </body>
132 * <html>
133 * <xsl:template/>
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 * <result name="success" type="xslt">
148 * <param name="location">foo.xslt</param>
149 * <param name="matchingPattern">^/result/[^/*]$</param>
150 * <param name="excludingPattern">.*(hugeCollection).*</param>
151 * </result>
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 * <result name="success" type="xslt">
162 * <param name="location">foo.xslt</param>
163 * <param name="exposedValue">user$</param>
164 * </result>
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 * <result name="success" type="xslt">foo.xslt</result>
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
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
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";
334 else
335 mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
336 if (mimeType == null) {
337
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
352 PrintWriter out = response.getWriter();
353
354 LOG.debug("xmlSource = " + xmlSource);
355 transformer.transform(xmlSource, new StreamResult(out));
356
357 out.close();
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 }