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.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 * <xsl:template match="/result">
62 * <result />
63 * </xsl:template></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 * <result>
96 * <x>
97 * <y/>
98 * </x>
99 * </result></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 * <xsl:template match="result">
127 * <html>
128 * <body>
129 * Hello <xsl:value-of select="username"/> how are you?
130 * </body>
131 * <html>
132 * <xsl:template/>
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 * <result name="success" type="xslt">
147 * <param name="location">foo.xslt</param>
148 * <param name="matchingPattern">^/result/[^/*]$</param>
149 * <param name="excludingPattern">.*(hugeCollection).*</param>
150 * </result>
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 * <result name="success" type="xslt">
161 * <param name="location">foo.xslt</param>
162 * <param name="exposedValue">user$</param>
163 * </result>
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 * <result name="success" type="xslt">foo.xslt</result>
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
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
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";
331 else
332 mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
333 if (mimeType == null) {
334
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
349 PrintWriter out = response.getWriter();
350
351 LOG.debug("xmlSource = " + xmlSource);
352 transformer.transform(xmlSource, new StreamResult(out));
353
354 out.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 }