1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 * <xsl:template match="/result">
59 * <result />
60 * </xsl:template></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 * <result>
93 * <x>
94 * <y/>
95 * </x>
96 * </result></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 * <xsl:template match="result">
124 * <html>
125 * <body>
126 * Hello <xsl:value-of select="username"/> how are you?
127 * </body>
128 * <html>
129 * <xsl:template/>
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 * <result name="success" type="xslt">
144 * <param name="location">foo.xslt</param>
145 * <param name="matchingPattern">^/result/[^/*]$</param>
146 * <param name="excludingPattern">.*(hugeCollection).*</param>
147 * </result>
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 * <result name="success" type="xslt">foo.xslt</result>
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
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";
264 else
265 mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
266 if (mimeType == null) {
267
268 mimeType = "text/html";
269 }
270
271 response.setContentType(mimeType);
272
273 Source xmlSource = getDOMSourceForStack(invocation.getAction());
274
275
276 PrintWriter out = response.getWriter();
277
278 log.debug("xmlSource = " + xmlSource);
279 transformer.transform(xmlSource, new StreamResult(out));
280
281 out.close();
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 }