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.BufferedOutputStream;
020: import java.io.IOException;
021: import java.net.URL;
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.TransformerException;
037: import javax.xml.transform.TransformerFactory;
038: import javax.xml.transform.URIResolver;
039: import javax.xml.transform.dom.DOMSource;
040: import javax.xml.transform.stream.StreamResult;
041: import javax.xml.transform.stream.StreamSource;
042:
043: import org.w3c.dom.Node;
044:
045: import org.springframework.context.ApplicationContextException;
046: import org.springframework.core.io.Resource;
047: import org.springframework.util.xml.SimpleTransformErrorListener;
048: import org.springframework.web.servlet.view.AbstractView;
049: import org.springframework.web.util.NestedServletException;
050:
051: /**
052: * Convenient superclass for views rendered using an XSLT stylesheet.
053: *
054: * <p>Subclasses typically must provide the {@link Source} to transform
055: * by overriding {@link #createXsltSource}. Subclasses do not need to
056: * concern themselves with XSLT other than providing a valid stylesheet location.
057: *
058: * <p>Properties:
059: * <ul>
060: * <li>{@link #setStylesheetLocation(org.springframework.core.io.Resource) stylesheetLocation}:
061: * a {@link Resource} pointing to the XSLT stylesheet
062: * <li>{@link #setRoot(String) root}: the name of the root element; defaults to {@link #DEFAULT_ROOT "DocRoot"}
063: * <li>{@link #setUriResolver(javax.xml.transform.URIResolver) uriResolver}:
064: * the {@link URIResolver} to be used in the transform
065: * <li>{@link #setErrorListener(javax.xml.transform.ErrorListener) errorListener} (optional):
066: * the {@link ErrorListener} implementation instance for custom handling of warnings and errors during TransformerFactory operations
067: * <li>{@link #setIndent(boolean) indent} (optional): whether additional whitespace
068: * may be added when outputting the result; defaults to <code>true</code>
069: * <li>{@link #setCache(boolean) cache} (optional): are templates to be cached; debug setting only; defaults to <code>true</code>
070: * </ul>
071: *
072: * <p>Note that setting {@link #setCache(boolean) "cache"} to <code>false</code>
073: * will cause the template objects to be reloaded for each rendering. This is
074: * useful during development, but will seriously affect performance in production
075: * and is not thread-safe.
076: *
077: * @author Rod Johnson
078: * @author Juergen Hoeller
079: * @author Darren Davison
080: */
081: public abstract class AbstractXsltView extends AbstractView {
082:
083: /** The default content type if no stylesheet specified */
084: public static final String XML_CONTENT_TYPE = "text/xml;charset=ISO-8859-1";
085:
086: /** The default document root name */
087: public static final String DEFAULT_ROOT = "DocRoot";
088:
089: private boolean customContentTypeSet = false;
090:
091: private Resource stylesheetLocation;
092:
093: private String root = DEFAULT_ROOT;
094:
095: private boolean useSingleModelNameAsRoot = true;
096:
097: private URIResolver uriResolver;
098:
099: private ErrorListener errorListener = new SimpleTransformErrorListener(
100: logger);
101:
102: private boolean indent = true;
103:
104: private Properties outputProperties;
105:
106: private boolean cache = true;
107:
108: private TransformerFactory transformerFactory;
109:
110: private volatile Templates cachedTemplates;
111:
112: /**
113: * This constructor sets the content type to "text/xml;charset=ISO-8859-1"
114: * by default. This will be switched to the standard web view default
115: * "text/html;charset=ISO-8859-1" if a stylesheet location has been specified.
116: * <p>A specific content type can be configured via the
117: * {@link #setContentType "contentType"} bean property.
118: */
119: protected AbstractXsltView() {
120: super .setContentType(XML_CONTENT_TYPE);
121: }
122:
123: public void setContentType(String contentType) {
124: super .setContentType(contentType);
125: this .customContentTypeSet = true;
126: }
127:
128: /**
129: * Set the location of the XSLT stylesheet.
130: * <p>If the {@link TransformerFactory} used by this instance has already
131: * been initialized then invoking this setter will result in the
132: * {@link TransformerFactory#newTemplates(javax.xml.transform.Source) attendant templates}
133: * being re-cached.
134: * @param stylesheetLocation the location of the XSLT stylesheet
135: * @see org.springframework.context.ApplicationContext#getResource
136: */
137: public void setStylesheetLocation(Resource stylesheetLocation) {
138: this .stylesheetLocation = stylesheetLocation;
139: // Re-cache templates if transformer factory already initialized.
140: resetCachedTemplates();
141: }
142:
143: /**
144: * Return the location of the XSLT stylesheet, if any.
145: */
146: protected Resource getStylesheetLocation() {
147: return this .stylesheetLocation;
148: }
149:
150: /**
151: * The document root element name. Default is {@link #DEFAULT_ROOT "DocRoot"}.
152: * <p>Only used if we're not passed a single {@link Node} as the model.
153: * @param root the document root element name
154: * @see #DEFAULT_ROOT
155: */
156: public void setRoot(String root) {
157: this .root = root;
158: }
159:
160: /**
161: * Set whether to use the name of a given single model object as the
162: * document root element name.
163: * <p>Default is <code>true</code> : If you pass in a model with a single object
164: * named "myElement", then the document root will be named "myElement"
165: * as well. Set this flag to <code>false</code> if you want to pass in a single
166: * model object while still using the root element name configured
167: * through the {@link #setRoot(String) "root" property}.
168: * @param useSingleModelNameAsRoot <code>true</code> if the name of a given single
169: * model object is to be used as the document root element name
170: * @see #setRoot
171: */
172: public void setUseSingleModelNameAsRoot(
173: boolean useSingleModelNameAsRoot) {
174: this .useSingleModelNameAsRoot = useSingleModelNameAsRoot;
175: }
176:
177: /**
178: * Set the URIResolver used in the transform.
179: * <p>The URIResolver handles calls to the XSLT <code>document()</code> function.
180: */
181: public void setUriResolver(URIResolver uriResolver) {
182: this .uriResolver = uriResolver;
183: }
184:
185: /**
186: * Set an implementation of the {@link javax.xml.transform.ErrorListener}
187: * interface for custom handling of transformation errors and warnings.
188: * <p>If not set, a default
189: * {@link org.springframework.util.xml.SimpleTransformErrorListener} is
190: * used that simply logs warnings using the logger instance of the view class,
191: * and rethrows errors to discontinue the XML transformation.
192: * @see org.springframework.util.xml.SimpleTransformErrorListener
193: */
194: public void setErrorListener(ErrorListener errorListener) {
195: this .errorListener = errorListener;
196: }
197:
198: /**
199: * Set whether the XSLT transformer may add additional whitespace when
200: * outputting the result tree.
201: * <p>Default is <code>true</code> (on); set this to <code>false</code> (off)
202: * to not specify an "indent" key, leaving the choice up to the stylesheet.
203: * @see javax.xml.transform.OutputKeys#INDENT
204: */
205: public void setIndent(boolean indent) {
206: this .indent = indent;
207: }
208:
209: /**
210: * Set arbitrary transformer output properties to be applied to the stylesheet.
211: * <p>Any values specified here will override defaults that this view sets
212: * programmatically.
213: * @see javax.xml.transform.Transformer#setOutputProperty
214: */
215: public void setOutputProperties(Properties outputProperties) {
216: this .outputProperties = outputProperties;
217: }
218:
219: /**
220: * Set whether to activate the template cache for this view.
221: * <p>Default is <code>true</code>. Turn this off to refresh
222: * the Templates object on every access, e.g. during development.
223: * @see #resetCachedTemplates()
224: */
225: public void setCache(boolean cache) {
226: this .cache = cache;
227: }
228:
229: /**
230: * Reset the cached Templates object, if any.
231: * <p>The Templates object will subsequently be rebuilt on next
232: * {@link #getTemplates() access}, if caching is enabled.
233: * @see #setCache
234: */
235: public final void resetCachedTemplates() {
236: this .cachedTemplates = null;
237: }
238:
239: /**
240: * Here we load our template, as we need the
241: * {@link org.springframework.context.ApplicationContext} to do it.
242: */
243: protected final void initApplicationContext()
244: throws ApplicationContextException {
245: this .transformerFactory = TransformerFactory.newInstance();
246: this .transformerFactory.setErrorListener(this .errorListener);
247: if (this .uriResolver != null) {
248: this .transformerFactory.setURIResolver(this .uriResolver);
249: }
250: if (getStylesheetLocation() != null
251: && !this .customContentTypeSet) {
252: // Use "text/html" as default (instead of "text/xml") if a stylesheet
253: // has been configured but no custom content type has been set.
254: super .setContentType(DEFAULT_CONTENT_TYPE);
255: }
256: try {
257: getTemplates();
258: } catch (TransformerConfigurationException ex) {
259: throw new ApplicationContextException(
260: "Cannot load stylesheet for XSLT view '"
261: + getBeanName() + "'", ex);
262: }
263: }
264:
265: /**
266: * Return the TransformerFactory used by this view.
267: * Available once the View object has been fully initialized.
268: */
269: protected final TransformerFactory getTransformerFactory() {
270: return this .transformerFactory;
271: }
272:
273: protected final void renderMergedOutputModel(Map model,
274: HttpServletRequest request, HttpServletResponse response)
275: throws Exception {
276:
277: response.setContentType(getContentType());
278:
279: Source source = null;
280: String docRoot = null;
281: // Value of a single element in the map, if there is one.
282: Object singleModel = null;
283:
284: if (this .useSingleModelNameAsRoot && model.size() == 1) {
285: docRoot = (String) model.keySet().iterator().next();
286: if (logger.isDebugEnabled()) {
287: logger.debug("Single model object received, key ["
288: + docRoot + "] will be used as root tag");
289: }
290: singleModel = model.get(docRoot);
291: }
292:
293: // Handle special case when we have a single node.
294: if (singleModel instanceof Node
295: || singleModel instanceof Source) {
296: // Don't domify if the model is already an XML node/source.
297: // We don't need to worry about model name, either:
298: // we leave the Node alone.
299: logger
300: .debug("No need to domify: was passed an XML Node or Source");
301: source = (singleModel instanceof Node ? new DOMSource(
302: (Node) singleModel) : (Source) singleModel);
303: } else {
304: // docRoot local variable takes precedence
305: source = createXsltSource(model, (docRoot != null ? docRoot
306: : this .root), request, response);
307: }
308:
309: doTransform(model, source, request, response);
310: }
311:
312: /**
313: * Return the XML {@link Source} to transform.
314: * @param model the model Map
315: * @param root name for root element. This can be supplied as a bean property
316: * to concrete subclasses within the view definition file, but will be overridden
317: * in the case of a single object in the model map to be the key for that object.
318: * If no root property is specified and multiple model objects exist, a default
319: * root tag name will be supplied.
320: * @param request HTTP request. Subclasses won't normally use this, as
321: * request processing should have been complete. However, we might want to
322: * create a RequestContext to expose as part of the model.
323: * @param response HTTP response. Subclasses won't normally use this,
324: * however there may sometimes be a need to set cookies.
325: * @return the XSLT Source to transform
326: * @throws Exception if an error occurs
327: */
328: protected Source createXsltSource(Map model, String root,
329: HttpServletRequest request, HttpServletResponse response)
330: throws Exception {
331:
332: return null;
333: }
334:
335: /**
336: * Perform the actual transformation, writing to the HTTP response.
337: * <p>The default implementation delegates to the
338: * {@link #doTransform(javax.xml.transform.Source, java.util.Map, javax.xml.transform.Result, String)}
339: * method, building a StreamResult for the ServletResponse OutputStream
340: * or for the ServletResponse Writer (according to {@link #useWriter()}).
341: * @param model the model Map
342: * @param source the Source to transform
343: * @param request current HTTP request
344: * @param response current HTTP response
345: * @throws Exception if an error occurs
346: * @see javax.xml.transform.stream.StreamResult
347: * @see javax.servlet.ServletResponse#getOutputStream()
348: * @see javax.servlet.ServletResponse#getWriter()
349: * @see #useWriter()
350: */
351: protected void doTransform(Map model, Source source,
352: HttpServletRequest request, HttpServletResponse response)
353: throws Exception {
354:
355: Map parameters = getParameters(model, request);
356: Result result = (useWriter() ? new StreamResult(response
357: .getWriter()) : new StreamResult(
358: new BufferedOutputStream(response.getOutputStream())));
359: String encoding = response.getCharacterEncoding();
360: doTransform(source, parameters, result, encoding);
361: }
362:
363: /**
364: * Return a Map of transformer parameters to be applied to the stylesheet.
365: * <p>Subclasses can override this method in order to apply one or more
366: * parameters to the transformation process.
367: * <p>The default implementation delegates to the
368: * {@link #getParameters(HttpServletRequest)} variant.
369: * @param model the model Map
370: * @param request current HTTP request
371: * @return a Map of parameters to apply to the transformation process
372: * @see #getParameters()
373: * @see javax.xml.transform.Transformer#setParameter
374: */
375: protected Map getParameters(Map model, HttpServletRequest request) {
376: return getParameters(request);
377: }
378:
379: /**
380: * Return a Map of transformer parameters to be applied to the stylesheet.
381: * <p>Subclasses can override this method in order to apply one or more
382: * parameters to the transformation process.
383: * <p>The default implementation delegates to the simple
384: * {@link #getParameters()} variant.
385: * @param request current HTTP request
386: * @return a Map of parameters to apply to the transformation process
387: * @see #getParameters(Map, HttpServletRequest)
388: * @see javax.xml.transform.Transformer#setParameter
389: */
390: protected Map getParameters(HttpServletRequest request) {
391: return getParameters();
392: }
393:
394: /**
395: * Return a Map of transformer parameters to be applied to the stylesheet.
396: * @return a Map of parameters to apply to the transformation process
397: * @deprecated as of Spring 2.0.4, in favor of the
398: * {@link #getParameters(HttpServletRequest)} variant
399: */
400: protected Map getParameters() {
401: return null;
402: }
403:
404: /**
405: * Return whether to use a <code>java.io.Writer</code> to write text content
406: * to the HTTP response. Else, a <code>java.io.OutputStream</code> will be used,
407: * to write binary content to the response.
408: * <p>The default implementation returns <code>false</code>, indicating a
409: * a <code>java.io.OutputStream</code>.
410: * @return whether to use a Writer (<code>true</code>) or an OutputStream
411: * (<code>false</code>)
412: * @see javax.servlet.ServletResponse#getWriter()
413: * @see javax.servlet.ServletResponse#getOutputStream()
414: */
415: protected boolean useWriter() {
416: return false;
417: }
418:
419: /**
420: * Perform the actual transformation, writing to the given result.
421: * @param source the Source to transform
422: * @param parameters a Map of parameters to be applied to the stylesheet
423: * (as determined by {@link #getParameters(Map, HttpServletRequest)})
424: * @param result the result to write to
425: * @param encoding the preferred character encoding that the underlying Transformer should use
426: * @throws Exception if an error occurs
427: */
428: protected void doTransform(Source source, Map parameters,
429: Result result, String encoding) throws Exception {
430:
431: try {
432: Transformer trans = buildTransformer(parameters);
433:
434: // Explicitly apply URIResolver to every created Transformer.
435: if (this .uriResolver != null) {
436: trans.setURIResolver(this .uriResolver);
437: }
438:
439: // Specify default output properties.
440: trans.setOutputProperty(OutputKeys.ENCODING, encoding);
441: if (this .indent) {
442: TransformerUtils.enableIndenting(trans);
443: }
444:
445: // Apply any arbitrary output properties, if specified.
446: if (this .outputProperties != null) {
447: Enumeration propsEnum = this .outputProperties
448: .propertyNames();
449: while (propsEnum.hasMoreElements()) {
450: String propName = (String) propsEnum.nextElement();
451: trans
452: .setOutputProperty(propName,
453: this .outputProperties
454: .getProperty(propName));
455: }
456: }
457:
458: // Perform the actual XSLT transformation.
459: trans.transform(source, result);
460: } catch (TransformerConfigurationException ex) {
461: throw new NestedServletException(
462: "Couldn't create XSLT transformer in XSLT view with name ["
463: + getBeanName() + "]", ex);
464: } catch (TransformerException ex) {
465: throw new NestedServletException(
466: "Couldn't perform transform in XSLT view with name ["
467: + getBeanName() + "]", ex);
468: }
469: }
470:
471: /**
472: * Build a Transformer object for immediate use, based on the
473: * given parameters.
474: * @param parameters a Map of parameters to be applied to the stylesheet
475: * (as determined by {@link #getParameters(Map, HttpServletRequest)})
476: * @return the Transformer object (never <code>null</code>)
477: * @throws TransformerConfigurationException if the Transformer object
478: * could not be built
479: */
480: protected Transformer buildTransformer(Map parameters)
481: throws TransformerConfigurationException {
482: Templates templates = getTemplates();
483: Transformer transformer = (templates != null ? templates
484: .newTransformer() : getTransformerFactory()
485: .newTransformer());
486: applyTransformerParameters(parameters, transformer);
487: return transformer;
488: }
489:
490: /**
491: * Obtain the Templates object to use, based on the configured
492: * stylesheet, either a cached one or a freshly built one.
493: * <p>Subclasses may override this method e.g. in order to refresh
494: * the Templates instance, calling {@link #resetCachedTemplates()}
495: * before delegating to this <code>getTemplates()</code> implementation.
496: * @return the Templates object (or <code>null</code> if there is
497: * no stylesheet specified)
498: * @throws TransformerConfigurationException if the Templates object
499: * could not be built
500: * @see #setStylesheetLocation
501: * @see #setCache
502: * @see #resetCachedTemplates
503: */
504: protected Templates getTemplates()
505: throws TransformerConfigurationException {
506: if (this .cachedTemplates != null) {
507: return this .cachedTemplates;
508: }
509: Resource location = getStylesheetLocation();
510: if (location != null) {
511: Templates templates = getTransformerFactory().newTemplates(
512: getStylesheetSource(location));
513: if (this .cache) {
514: this .cachedTemplates = templates;
515: }
516: return templates;
517: }
518: return null;
519: }
520:
521: /**
522: * Apply the specified parameters to the given Transformer.
523: * @param parameters the transformer parameters
524: * (as determined by {@link #getParameters(Map, HttpServletRequest)})
525: * @param transformer the Transformer to aply the parameters
526: */
527: protected void applyTransformerParameters(Map parameters,
528: Transformer transformer) {
529: if (parameters != null) {
530: for (Iterator it = parameters.entrySet().iterator(); it
531: .hasNext();) {
532: Map.Entry entry = (Map.Entry) it.next();
533: transformer.setParameter(entry.getKey().toString(),
534: entry.getValue());
535: }
536: }
537: }
538:
539: /**
540: * Load the stylesheet from the specified location.
541: * @param stylesheetLocation the stylesheet resource to be loaded
542: * @return the stylesheet source
543: * @throws ApplicationContextException if the stylesheet resource could not be loaded
544: */
545: protected Source getStylesheetSource(Resource stylesheetLocation)
546: throws ApplicationContextException {
547: if (logger.isDebugEnabled()) {
548: logger.debug("Loading XSLT stylesheet from "
549: + stylesheetLocation);
550: }
551: try {
552: URL url = stylesheetLocation.getURL();
553: String urlPath = url.toString();
554: String systemId = urlPath.substring(0, urlPath
555: .lastIndexOf('/') + 1);
556: return new StreamSource(url.openStream(), systemId);
557: } catch (IOException ex) {
558: throw new ApplicationContextException(
559: "Can't load XSLT stylesheet from "
560: + stylesheetLocation, ex);
561: }
562: }
563:
564: }
|