001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.web.servlet.view.xslt;
018:
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.io.Reader;
022: import java.util.Enumeration;
023: import java.util.Iterator;
024: import java.util.Map;
025: import java.util.Properties;
026:
027: import javax.servlet.http.HttpServletRequest;
028: import javax.servlet.http.HttpServletResponse;
029: import javax.xml.transform.ErrorListener;
030: import javax.xml.transform.OutputKeys;
031: import javax.xml.transform.Result;
032: import javax.xml.transform.Source;
033: import javax.xml.transform.Templates;
034: import javax.xml.transform.Transformer;
035: import javax.xml.transform.TransformerConfigurationException;
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.w3c.dom.Document;
043: import org.w3c.dom.Node;
044:
045: import org.springframework.beans.BeansException;
046: import org.springframework.context.ApplicationContextException;
047: import org.springframework.core.io.Resource;
048: import org.springframework.util.CollectionUtils;
049: import org.springframework.util.ObjectUtils;
050: import org.springframework.util.StringUtils;
051: import org.springframework.util.xml.SimpleTransformErrorListener;
052: import org.springframework.web.servlet.view.AbstractUrlBasedView;
053: import org.springframework.web.util.WebUtils;
054:
055: /**
056: * XSLT-driven View that allows for response context to be rendered as the
057: * result of an XSLT transformation.
058: *
059: * <p>The XSLT Source object is supplied as a parameter in the model and then
060: * {@link #locateSource detected} during response rendering. Users can either specify
061: * a specific entry in the model via the {@link #setSourceKey sourceKey} property or
062: * have Spring locate the Source object. This class also provides basic conversion
063: * of objects into Source implementations. See {@link #getSourceTypes() here}
064: * for more details.
065: *
066: * <p>All model parameters are passed to the XSLT Transformer as parameters.
067: * In addition the user can configure {@link #setOutputProperties output properties}
068: * to be passed to the Transformer.
069: *
070: * @author Rob Harrop
071: * @author Juergen Hoeller
072: * @since 2.0
073: */
074: public class XsltView extends AbstractUrlBasedView {
075:
076: private final TransformerFactory transformerFactory = TransformerFactory
077: .newInstance();
078:
079: private String sourceKey;
080:
081: private URIResolver uriResolver;
082:
083: private ErrorListener errorListener = new SimpleTransformErrorListener(
084: logger);
085:
086: private boolean indent = true;
087:
088: private Properties outputProperties;
089:
090: private boolean cacheTemplates = true;
091:
092: private Templates cachedTemplates;
093:
094: /**
095: * Set the name of the model attribute that represents the XSLT Source.
096: * If not specified, the model map will be searched for a matching value type.
097: * <p>The following source types are supported out of the box:
098: * {@link Source}, {@link Document}, {@link Node}, {@link Reader},
099: * {@link InputStream} and {@link Resource}.
100: * @see #getSourceTypes
101: * @see #convertSource
102: */
103: public void setSourceKey(String sourceKey) {
104: this .sourceKey = sourceKey;
105: }
106:
107: /**
108: * Set the URIResolver used in the transform.
109: * <p>The URIResolver handles calls to the XSLT <code>document()</code> function.
110: */
111: public void setUriResolver(URIResolver uriResolver) {
112: this .uriResolver = uriResolver;
113: }
114:
115: /**
116: * Set an implementation of the {@link javax.xml.transform.ErrorListener}
117: * interface for custom handling of transformation errors and warnings.
118: * <p>If not set, a default
119: * {@link org.springframework.util.xml.SimpleTransformErrorListener} is
120: * used that simply logs warnings using the logger instance of the view class,
121: * and rethrows errors to discontinue the XML transformation.
122: * @see org.springframework.util.xml.SimpleTransformErrorListener
123: */
124: public void setErrorListener(ErrorListener errorListener) {
125: this .errorListener = (errorListener != null ? errorListener
126: : new SimpleTransformErrorListener(logger));
127: }
128:
129: /**
130: * Set whether the XSLT transformer may add additional whitespace when
131: * outputting the result tree.
132: * <p>Default is <code>true</code> (on); set this to <code>false</code> (off)
133: * to not specify an "indent" key, leaving the choice up to the stylesheet.
134: * @see javax.xml.transform.OutputKeys#INDENT
135: */
136: public void setIndent(boolean indent) {
137: this .indent = indent;
138: }
139:
140: /**
141: * Set arbitrary transformer output properties to be applied to the stylesheet.
142: * <p>Any values specified here will override defaults that this view sets
143: * programmatically.
144: * @see javax.xml.transform.Transformer#setOutputProperty
145: */
146: public void setOutputProperties(Properties outputProperties) {
147: this .outputProperties = outputProperties;
148: }
149:
150: /**
151: * Turn on/off the caching of the XSLT {@link Templates} instance.
152: * <p>The default value is "true". Only set this to "false" in development,
153: * where caching does not seriously impact performance.
154: */
155: public void setCacheTemplates(boolean cacheTemplates) {
156: this .cacheTemplates = cacheTemplates;
157: }
158:
159: /**
160: * Initialize this XsltView's TransformerFactory.
161: */
162: protected void initApplicationContext() throws BeansException {
163: this .transformerFactory.setErrorListener(this .errorListener);
164:
165: if (this .uriResolver != null) {
166: if (logger.isInfoEnabled()) {
167: logger.info("Using custom URIResolver '"
168: + this .uriResolver
169: + "' in XSLT view with URL '" + getUrl() + "'");
170: }
171: this .transformerFactory.setURIResolver(this .uriResolver);
172: }
173:
174: if (logger.isDebugEnabled()) {
175: logger.debug("URL in view is '" + getUrl() + "'");
176: }
177:
178: if (this .cacheTemplates) {
179: this .cachedTemplates = loadTemplates();
180: }
181: }
182:
183: /**
184: * Return the TransformerFactory that this XsltView uses.
185: * @return the TransformerFactory (never <code>null</code>)
186: */
187: protected final TransformerFactory getTransformerFactory() {
188: return this .transformerFactory;
189: }
190:
191: protected void renderMergedOutputModel(Map model,
192: HttpServletRequest request, HttpServletResponse response)
193: throws Exception {
194:
195: Templates templates = this .cachedTemplates;
196: if (templates == null) {
197: templates = loadTemplates();
198: }
199:
200: Transformer transformer = createTransformer(templates);
201: configureTransformer(model, response, transformer);
202: configureResponse(model, response, transformer);
203: Source source = null;
204: try {
205: source = locateSource(model);
206: if (source == null) {
207: throw new IllegalArgumentException(
208: "Unable to locate Source object in model: "
209: + model);
210: }
211: transformer.transform(source, createResult(response));
212: } finally {
213: closeSourceIfNecessary(source);
214: }
215: }
216:
217: /**
218: * Create the XSLT {@link Result} used to render the result of the transformation.
219: * <p>The default implementation creates a {@link StreamResult} wrapping the supplied
220: * HttpServletResponse's {@link HttpServletResponse#getOutputStream() OutputStream}.
221: * @param response current HTTP response
222: * @return the XSLT Result to use
223: * @throws Exception if the Result cannot be built
224: */
225: protected Result createResult(HttpServletResponse response)
226: throws Exception {
227: return new StreamResult(response.getOutputStream());
228: }
229:
230: /**
231: * <p>Locate the {@link Source} object in the supplied model,
232: * converting objects as required.
233: * The default implementation first attempts to look under the configured
234: * {@link #setSourceKey source key}, if any, before attempting to locate
235: * an object of {@link #getSourceTypes() supported type}.
236: * @param model the merged model Map
237: * @return the XSLT Source object (or <code>null</code> if none found)
238: * @throws Exception if an error occured during locating the source
239: * @see #setSourceKey
240: * @see #convertSource
241: */
242: protected Source locateSource(Map model) throws Exception {
243: if (this .sourceKey != null) {
244: return convertSource(model.get(this .sourceKey));
245: }
246: Object source = CollectionUtils.findValueOfType(model.values(),
247: getSourceTypes());
248: return (source != null ? convertSource(source) : null);
249: }
250:
251: /**
252: * Return the array of {@link Class Classes} that are supported when converting to an
253: * XSLT {@link Source}.
254: * <p>Currently supports {@link Source}, {@link Document}, {@link Node},
255: * {@link Reader}, {@link InputStream} and {@link Resource}.
256: * @return the supported source types
257: */
258: protected Class[] getSourceTypes() {
259: return new Class[] { Source.class, Document.class, Node.class,
260: Reader.class, InputStream.class, Resource.class };
261: }
262:
263: /**
264: * Convert the supplied {@link Object} into an XSLT {@link Source} if the
265: * {@link Object} type is {@link #getSourceTypes() supported}.
266: * @param source the original source object
267: * @return the adapted XSLT Source
268: * @throws IllegalArgumentException if the given Object is not of a supported type
269: */
270: protected Source convertSource(Object source) throws Exception {
271: if (source instanceof Source) {
272: return (Source) source;
273: } else if (source instanceof Document) {
274: return new DOMSource(((Document) source)
275: .getDocumentElement());
276: } else if (source instanceof Node) {
277: return new DOMSource((Node) source);
278: } else if (source instanceof Reader) {
279: return new StreamSource((Reader) source);
280: } else if (source instanceof InputStream) {
281: return new StreamSource((InputStream) source);
282: } else if (source instanceof Resource) {
283: return new StreamSource(((Resource) source)
284: .getInputStream());
285: } else {
286: throw new IllegalArgumentException("Value '" + source
287: + "' cannot be converted to XSLT Source");
288: }
289: }
290:
291: /**
292: * Configure the supplied {@link Transformer} instance.
293: * <p>The default implementation copies parameters from the model into the
294: * Transformer's {@link Transformer#setParameter parameter set}.
295: * This implementation also copies the {@link #setOutputProperties output properties}
296: * into the {@link Transformer} {@link Transformer#setOutputProperty output properties}.
297: * Indentation properties are set as well.
298: * @param model merged output Map (never <code>null</code>)
299: * @param response current HTTP response
300: * @param transformer the target transformer
301: * @see #copyModelParameters(Map, Transformer)
302: * @see #copyOutputProperties(Transformer)
303: * @see #configureIndentation(Transformer)
304: */
305: protected void configureTransformer(Map model,
306: HttpServletResponse response, Transformer transformer) {
307: copyModelParameters(model, transformer);
308: copyOutputProperties(transformer);
309: configureIndentation(transformer);
310: }
311:
312: /**
313: * Configure the indentation settings for the supplied {@link Transformer}.
314: * @param transformer the target transformer
315: * @throws IllegalArgumentException if the supplied {@link Transformer} is <code>null</code>
316: * @see TransformerUtils#enableIndenting(javax.xml.transform.Transformer)
317: * @see TransformerUtils#disableIndenting(javax.xml.transform.Transformer)
318: */
319: protected final void configureIndentation(Transformer transformer) {
320: if (this .indent) {
321: TransformerUtils.enableIndenting(transformer);
322: } else {
323: TransformerUtils.disableIndenting(transformer);
324: }
325: }
326:
327: /**
328: * Copy the configured output {@link Properties}, if any, into the
329: * {@link Transformer#setOutputProperty output property set} of the supplied
330: * {@link Transformer}.
331: * @param transformer the target transformer
332: */
333: protected final void copyOutputProperties(Transformer transformer) {
334: if (this .outputProperties != null) {
335: Enumeration en = this .outputProperties.propertyNames();
336: while (en.hasMoreElements()) {
337: String name = (String) en.nextElement();
338: transformer.setOutputProperty(name,
339: this .outputProperties.getProperty(name));
340: }
341: }
342: }
343:
344: /**
345: * Copy all entries from the supplied Map into the
346: * {@link Transformer#setParameter(String, Object) parameter set}
347: * of the supplied {@link Transformer}.
348: * @param model merged output Map (never <code>null</code>)
349: * @param transformer the target transformer
350: */
351: protected final void copyModelParameters(Map model,
352: Transformer transformer) {
353: copyMapEntriesToTransformerParameters(model, transformer);
354: }
355:
356: /**
357: * Configure the supplied {@link HttpServletResponse}.
358: * <p>The default implementation of this method sets the
359: * {@link HttpServletResponse#setContentType content type} and
360: * {@link HttpServletResponse#setCharacterEncoding encoding}
361: * from the "media-type" and "encoding" output properties
362: * specified in the {@link Transformer}.
363: * @param model merged output Map (never <code>null</code>)
364: * @param response current HTTP response
365: * @param transformer the target transformer
366: */
367: protected void configureResponse(Map model,
368: HttpServletResponse response, Transformer transformer) {
369: String contentType = getContentType();
370: String mediaType = transformer
371: .getOutputProperty(OutputKeys.MEDIA_TYPE);
372: String encoding = transformer
373: .getOutputProperty(OutputKeys.ENCODING);
374: if (StringUtils.hasText(mediaType)) {
375: contentType = mediaType;
376: }
377: if (StringUtils.hasText(encoding)) {
378: // Only apply encoding if content type is specified but does not contain charset clause already.
379: if (contentType != null
380: && contentType.toLowerCase().indexOf(
381: WebUtils.CONTENT_TYPE_CHARSET_PREFIX) == -1) {
382: contentType = contentType
383: + WebUtils.CONTENT_TYPE_CHARSET_PREFIX
384: + encoding;
385: }
386: }
387: response.setContentType(contentType);
388: }
389:
390: /**
391: * Load the {@link Templates} instance for the stylesheet at the configured location.
392: */
393: private Templates loadTemplates()
394: throws ApplicationContextException {
395: Source stylesheetSource = getStylesheetSource();
396: try {
397: Templates templates = this .transformerFactory
398: .newTemplates(stylesheetSource);
399: if (logger.isDebugEnabled()) {
400: logger.debug("Loading templates '" + templates + "'");
401: }
402: return templates;
403: } catch (TransformerConfigurationException ex) {
404: throw new ApplicationContextException(
405: "Can't load stylesheet from '" + getUrl() + "'", ex);
406: } finally {
407: closeSourceIfNecessary(stylesheetSource);
408: }
409: }
410:
411: /**
412: * Create the {@link Transformer} instance used to prefer the XSLT transformation.
413: * <p>The default implementation simply calls {@link Templates#newTransformer()}, and
414: * configures the {@link Transformer} with the custom {@link URIResolver} if specified.
415: * @param templates the XSLT Templates instance to create a Transformer for
416: */
417: protected Transformer createTransformer(Templates templates)
418: throws TransformerConfigurationException {
419: Transformer transformer = templates.newTransformer();
420: if (this .uriResolver != null) {
421: transformer.setURIResolver(this .uriResolver);
422: }
423: return transformer;
424: }
425:
426: /**
427: * Get the XSLT {@link Source} for the XSLT template under the {@link #setUrl configured URL}.
428: */
429: protected Source getStylesheetSource() {
430: String url = getUrl();
431: if (logger.isDebugEnabled()) {
432: logger.debug("Loading XSLT stylesheet from '" + url + "'");
433: }
434: try {
435: Resource stylesheetResource = getApplicationContext()
436: .getResource(url);
437: String systemId = url
438: .substring(0, url.lastIndexOf('/') + 1);
439: return new StreamSource(
440: stylesheetResource.getInputStream(), systemId);
441: } catch (IOException ex) {
442: throw new ApplicationContextException(
443: "Can't load XSLT stylesheet from '" + url + "'", ex);
444: }
445: }
446:
447: /**
448: * Copy all {@link Map.Entry entries} from the supplied {@link Map} into the
449: * {@link Transformer#setParameter(String, Object) parameter set} of the supplied
450: * {@link Transformer}.
451: */
452: private void copyMapEntriesToTransformerParameters(Map map,
453: Transformer transformer) {
454: for (Iterator iterator = map.entrySet().iterator(); iterator
455: .hasNext();) {
456: Map.Entry entry = (Map.Entry) iterator.next();
457: transformer.setParameter(ObjectUtils.nullSafeToString(entry
458: .getKey()), entry.getValue());
459: }
460: }
461:
462: /**
463: * Close the underlying resource managed by the supplied {@link Source} if applicable.
464: * <p>Only works for {@link StreamSource StreamSources}.
465: * @param source the XSLT Source to close (may be <code>null</code>)
466: */
467: private void closeSourceIfNecessary(Source source) {
468: if (source instanceof StreamSource) {
469: StreamSource streamSource = (StreamSource) source;
470: if (streamSource.getReader() != null) {
471: try {
472: streamSource.getReader().close();
473: } catch (IOException ex) {
474: }
475: }
476: if (streamSource.getInputStream() != null) {
477: try {
478: streamSource.getInputStream().close();
479: } catch (IOException ex) {
480: }
481: }
482: }
483: }
484:
485: }
|