001: /*
002: * $Id: XSLTResult.java 557637 2007-07-19 14:20:13Z jholmes $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.views.xslt;
022:
023: import java.io.IOException;
024: import java.io.PrintWriter;
025: import java.io.Writer;
026: import java.net.URL;
027: import java.util.HashMap;
028: import java.util.Map;
029:
030: import javax.servlet.http.HttpServletResponse;
031: import javax.xml.transform.OutputKeys;
032: import javax.xml.transform.Source;
033: import javax.xml.transform.Templates;
034: import javax.xml.transform.Transformer;
035: import javax.xml.transform.TransformerException;
036: import javax.xml.transform.TransformerFactory;
037: import javax.xml.transform.URIResolver;
038: import javax.xml.transform.dom.DOMSource;
039: import javax.xml.transform.stream.StreamResult;
040: import javax.xml.transform.stream.StreamSource;
041:
042: import org.apache.commons.logging.Log;
043: import org.apache.commons.logging.LogFactory;
044: import org.apache.struts2.ServletActionContext;
045: import org.apache.struts2.StrutsConstants;
046:
047: import com.opensymphony.xwork2.ActionContext;
048: import com.opensymphony.xwork2.ActionInvocation;
049: import com.opensymphony.xwork2.Result;
050: import com.opensymphony.xwork2.inject.Inject;
051: import com.opensymphony.xwork2.util.TextParseUtil;
052: import com.opensymphony.xwork2.util.ValueStack;
053:
054: /**
055: * <!-- START SNIPPET: description -->
056: *
057: * XSLTResult uses XSLT to transform action object to XML. Recent version has
058: * been specifically modified to deal with Xalan flaws. When using Xalan you may
059: * notice that even though you have very minimal stylesheet like this one
060: * <pre>
061: * <xsl:template match="/result">
062: * <result />
063: * </xsl:template></pre>
064: *
065: * <p>
066: * then Xalan would still iterate through every property of your action and it's
067: * all descendants.
068: * </p>
069: *
070: * <p>
071: * If you had double-linked objects then Xalan would work forever analysing
072: * infinite object tree. Even if your stylesheet was not constructed to process
073: * them all. It's becouse current Xalan eagerly and extensively converts
074: * everything to it's internal DTM model before further processing.
075: * </p>
076: *
077: * <p>
078: * Thet's why there's a loop eliminator added that works by indexing every
079: * object-property combination during processing. If it notices that some
080: * object's property were already walked through, it doesn't get any deeper.
081: * Say, you have two objects x and y with the following properties set
082: * (pseudocode):
083: * </p>
084: * <pre>
085: * x.y = y;
086: * and
087: * y.x = x;
088: * action.x=x;</pre>
089: *
090: * <p>
091: * Due to that modification the resulting XML document based on x would be:
092: * </p>
093: *
094: * <pre>
095: * <result>
096: * <x>
097: * <y/>
098: * </x>
099: * </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: // Configurable Parameters
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()
310: .getValueStack();
311: location = TextParseUtil
312: .translateVariables(location, stack);
313: }
314:
315: try {
316: HttpServletResponse response = ServletActionContext
317: .getResponse();
318:
319: Writer writer = response.getWriter();
320:
321: // Create a transformer for the stylesheet.
322: Templates templates = null;
323: Transformer transformer;
324: if (location != null) {
325: templates = getTemplates(location);
326: transformer = templates.newTransformer();
327: } else
328: transformer = TransformerFactory.newInstance()
329: .newTransformer();
330:
331: transformer.setURIResolver(getURIResolver());
332:
333: String mimeType;
334: if (templates == null)
335: mimeType = "text/xml"; // no stylesheet, raw xml
336: else
337: mimeType = templates.getOutputProperties().getProperty(
338: OutputKeys.MEDIA_TYPE);
339: if (mimeType == null) {
340: // guess (this is a servlet, so text/html might be the best guess)
341: mimeType = "text/html";
342: }
343:
344: response.setContentType(mimeType);
345:
346: Object result = invocation.getAction();
347: if (exposedValue != null) {
348: ValueStack stack = invocation.getStack();
349: result = stack.findValue(exposedValue);
350: }
351:
352: Source xmlSource = getDOMSourceForStack(result);
353:
354: // Transform the source XML to System.out.
355: PrintWriter out = response.getWriter();
356:
357: LOG.debug("xmlSource = " + xmlSource);
358: transformer.transform(xmlSource, new StreamResult(out));
359:
360: out.close(); // ...and flush...
361:
362: if (LOG.isDebugEnabled()) {
363: LOG.debug("Time:"
364: + (System.currentTimeMillis() - startTime)
365: + "ms");
366: }
367:
368: writer.flush();
369: } catch (Exception e) {
370: LOG.error("Unable to render XSLT Template, '" + location
371: + "'", e);
372: throw e;
373: }
374: }
375:
376: protected AdapterFactory getAdapterFactory() {
377: if (adapterFactory == null)
378: adapterFactory = new AdapterFactory();
379: return adapterFactory;
380: }
381:
382: protected void setAdapterFactory(AdapterFactory adapterFactory) {
383: this .adapterFactory = adapterFactory;
384: }
385:
386: /**
387: * Get the URI Resolver to be called by the processor when it encounters an xsl:include, xsl:import, or document()
388: * function. The default is an instance of ServletURIResolver, which operates relative to the servlet context.
389: */
390: protected URIResolver getURIResolver() {
391: return new ServletURIResolver(ServletActionContext
392: .getServletContext());
393: }
394:
395: protected Templates getTemplates(String path)
396: throws TransformerException, IOException {
397: String pathFromRequest = ServletActionContext.getRequest()
398: .getParameter("xslt.location");
399:
400: if (pathFromRequest != null)
401: path = pathFromRequest;
402:
403: if (path == null)
404: throw new TransformerException("Stylesheet path is null");
405:
406: Templates templates = templatesCache.get(path);
407:
408: if (noCache || (templates == null)) {
409: synchronized (templatesCache) {
410: URL resource = ServletActionContext.getServletContext()
411: .getResource(path);
412:
413: if (resource == null) {
414: throw new TransformerException("Stylesheet " + path
415: + " not found in resources.");
416: }
417:
418: LOG.debug("Preparing XSLT stylesheet templates: "
419: + path);
420:
421: TransformerFactory factory = TransformerFactory
422: .newInstance();
423: templates = factory.newTemplates(new StreamSource(
424: resource.openStream()));
425: templatesCache.put(path, templates);
426: }
427: }
428:
429: return templates;
430: }
431:
432: protected Source getDOMSourceForStack(Object value)
433: throws IllegalAccessException, InstantiationException {
434: return new DOMSource(getAdapterFactory().adaptDocument(
435: "result", value));
436: }
437: }
|