1 /* 2 * Copyright 2002-2008 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.web.servlet.view.xslt; 18 19 import java.io.BufferedOutputStream; 20 import java.io.IOException; 21 import java.net.URL; 22 import java.util.Enumeration; 23 import java.util.Map; 24 import java.util.Properties; 25 import javax.servlet.http.HttpServletRequest; 26 import javax.servlet.http.HttpServletResponse; 27 import javax.xml.transform.ErrorListener; 28 import javax.xml.transform.OutputKeys; 29 import javax.xml.transform.Result; 30 import javax.xml.transform.Source; 31 import javax.xml.transform.Templates; 32 import javax.xml.transform.Transformer; 33 import javax.xml.transform.TransformerConfigurationException; 34 import javax.xml.transform.TransformerException; 35 import javax.xml.transform.TransformerFactory; 36 import javax.xml.transform.TransformerFactoryConfigurationError; 37 import javax.xml.transform.URIResolver; 38 import javax.xml.transform.dom.DOMSource; 39 import javax.xml.transform.stream.StreamResult; 40 import javax.xml.transform.stream.StreamSource; 41 42 import org.w3c.dom.Node; 43 44 import org.springframework.context.ApplicationContextException; 45 import org.springframework.core.io.Resource; 46 import org.springframework.util.Assert; 47 import org.springframework.util.xml.SimpleTransformErrorListener; 48 import org.springframework.util.xml.TransformerUtils; 49 import org.springframework.web.servlet.view.AbstractView; 50 import org.springframework.web.util.NestedServletException; 51 52 /** 53 * Convenient superclass for views rendered using an XSLT stylesheet. 54 * 55 * <p>Subclasses typically must provide the {@link Source} to transform 56 * by overriding {@link #createXsltSource}. Subclasses do not need to 57 * concern themselves with XSLT other than providing a valid stylesheet location. 58 * 59 * <p>Properties: 60 * <ul> 61 * <li>{@link #setStylesheetLocation(org.springframework.core.io.Resource) stylesheetLocation}: 62 * a {@link Resource} pointing to the XSLT stylesheet 63 * <li>{@link #setRoot(String) root}: the name of the root element; defaults to {@link #DEFAULT_ROOT "DocRoot"} 64 * <li>{@link #setUriResolver(javax.xml.transform.URIResolver) uriResolver}: 65 * the {@link URIResolver} to be used in the transform 66 * <li>{@link #setErrorListener(javax.xml.transform.ErrorListener) errorListener} (optional): 67 * the {@link ErrorListener} implementation instance for custom handling of warnings and errors during TransformerFactory operations 68 * <li>{@link #setIndent(boolean) indent} (optional): whether additional whitespace 69 * may be added when outputting the result; defaults to <code>true</code> 70 * <li>{@link #setCache(boolean) cache} (optional): are templates to be cached; debug setting only; defaults to <code>true</code> 71 * </ul> 72 * 73 * <p>Note that setting {@link #setCache(boolean) "cache"} to <code>false</code> 74 * will cause the template objects to be reloaded for each rendering. This is 75 * useful during development, but will seriously affect performance in production 76 * and is not thread-safe. 77 * 78 * @author Rod Johnson 79 * @author Juergen Hoeller 80 * @author Darren Davison 81 * @deprecated since Spring 2.5; superseded by {@link XsltView} and its 82 * more flexible {@link XsltView#locateSource} mechanism 83 */ 84 @Deprecated 85 public abstract class AbstractXsltView extends AbstractView { 86 87 /** The default content type if no stylesheet specified */ 88 public static final String XML_CONTENT_TYPE = "text/xml;charset=ISO-8859-1"; 89 90 /** The default document root name */ 91 public static final String DEFAULT_ROOT = "DocRoot"; 92 93 94 private boolean customContentTypeSet = false; 95 96 private Class transformerFactoryClass; 97 98 private Resource stylesheetLocation; 99 100 private String root = DEFAULT_ROOT; 101 102 private boolean useSingleModelNameAsRoot = true; 103 104 private URIResolver uriResolver; 105 106 private ErrorListener errorListener = new SimpleTransformErrorListener(logger); 107 108 private boolean indent = true; 109 110 private Properties outputProperties; 111 112 private boolean cache = true; 113 114 private TransformerFactory transformerFactory; 115 116 private volatile Templates cachedTemplates; 117 118 119 /** 120 * This constructor sets the content type to "text/xml;charset=ISO-8859-1" 121 * by default. This will be switched to the standard web view default 122 * "text/html;charset=ISO-8859-1" if a stylesheet location has been specified. 123 * <p>A specific content type can be configured via the 124 * {@link #setContentType "contentType"} bean property. 125 */ AbstractXsltView()126 protected AbstractXsltView() { 127 super.setContentType(XML_CONTENT_TYPE); 128 } 129 130 131 @Override setContentType(String contentType)132 public void setContentType(String contentType) { 133 super.setContentType(contentType); 134 this.customContentTypeSet = true; 135 } 136 137 /** 138 * Specify the XSLT TransformerFactory class to use. 139 * <p>The default constructor of the specified class will be called 140 * to build the TransformerFactory for this view. 141 */ setTransformerFactoryClass(Class transformerFactoryClass)142 public void setTransformerFactoryClass(Class transformerFactoryClass) { 143 Assert.isAssignable(TransformerFactory.class, transformerFactoryClass); 144 this.transformerFactoryClass = transformerFactoryClass; 145 } 146 147 /** 148 * Set the location of the XSLT stylesheet. 149 * <p>If the {@link TransformerFactory} used by this instance has already 150 * been initialized then invoking this setter will result in the 151 * {@link TransformerFactory#newTemplates(javax.xml.transform.Source) attendant templates} 152 * being re-cached. 153 * @param stylesheetLocation the location of the XSLT stylesheet 154 * @see org.springframework.context.ApplicationContext#getResource 155 */ setStylesheetLocation(Resource stylesheetLocation)156 public void setStylesheetLocation(Resource stylesheetLocation) { 157 this.stylesheetLocation = stylesheetLocation; 158 // Re-cache templates if transformer factory already initialized. 159 resetCachedTemplates(); 160 } 161 162 /** 163 * Return the location of the XSLT stylesheet, if any. 164 */ getStylesheetLocation()165 protected Resource getStylesheetLocation() { 166 return this.stylesheetLocation; 167 } 168 169 /** 170 * The document root element name. Default is {@link #DEFAULT_ROOT "DocRoot"}. 171 * <p>Only used if we're not passed a single {@link Node} as the model. 172 * @param root the document root element name 173 * @see #DEFAULT_ROOT 174 */ setRoot(String root)175 public void setRoot(String root) { 176 this.root = root; 177 } 178 179 /** 180 * Set whether to use the name of a given single model object as the 181 * document root element name. 182 * <p>Default is <code>true</code> : If you pass in a model with a single object 183 * named "myElement", then the document root will be named "myElement" 184 * as well. Set this flag to <code>false</code> if you want to pass in a single 185 * model object while still using the root element name configured 186 * through the {@link #setRoot(String) "root" property}. 187 * @param useSingleModelNameAsRoot <code>true</code> if the name of a given single 188 * model object is to be used as the document root element name 189 * @see #setRoot 190 */ setUseSingleModelNameAsRoot(boolean useSingleModelNameAsRoot)191 public void setUseSingleModelNameAsRoot(boolean useSingleModelNameAsRoot) { 192 this.useSingleModelNameAsRoot = useSingleModelNameAsRoot; 193 } 194 195 /** 196 * Set the URIResolver used in the transform. 197 * <p>The URIResolver handles calls to the XSLT <code>document()</code> function. 198 */ setUriResolver(URIResolver uriResolver)199 public void setUriResolver(URIResolver uriResolver) { 200 this.uriResolver = uriResolver; 201 } 202 203 /** 204 * Set an implementation of the {@link javax.xml.transform.ErrorListener} 205 * interface for custom handling of transformation errors and warnings. 206 * <p>If not set, a default 207 * {@link org.springframework.util.xml.SimpleTransformErrorListener} is 208 * used that simply logs warnings using the logger instance of the view class, 209 * and rethrows errors to discontinue the XML transformation. 210 * @see org.springframework.util.xml.SimpleTransformErrorListener 211 */ setErrorListener(ErrorListener errorListener)212 public void setErrorListener(ErrorListener errorListener) { 213 this.errorListener = errorListener; 214 } 215 216 /** 217 * Set whether the XSLT transformer may add additional whitespace when 218 * outputting the result tree. 219 * <p>Default is <code>true</code> (on); set this to <code>false</code> (off) 220 * to not specify an "indent" key, leaving the choice up to the stylesheet. 221 * @see javax.xml.transform.OutputKeys#INDENT 222 */ setIndent(boolean indent)223 public void setIndent(boolean indent) { 224 this.indent = indent; 225 } 226 227 /** 228 * Set arbitrary transformer output properties to be applied to the stylesheet. 229 * <p>Any values specified here will override defaults that this view sets 230 * programmatically. 231 * @see javax.xml.transform.Transformer#setOutputProperty 232 */ setOutputProperties(Properties outputProperties)233 public void setOutputProperties(Properties outputProperties) { 234 this.outputProperties = outputProperties; 235 } 236 237 /** 238 * Set whether to activate the template cache for this view. 239 * <p>Default is <code>true</code>. Turn this off to refresh 240 * the Templates object on every access, e.g. during development. 241 * @see #resetCachedTemplates() 242 */ setCache(boolean cache)243 public void setCache(boolean cache) { 244 this.cache = cache; 245 } 246 247 /** 248 * Reset the cached Templates object, if any. 249 * <p>The Templates object will subsequently be rebuilt on next 250 * {@link #getTemplates() access}, if caching is enabled. 251 * @see #setCache 252 */ resetCachedTemplates()253 public final void resetCachedTemplates() { 254 this.cachedTemplates = null; 255 } 256 257 258 /** 259 * Here we load our template, as we need the 260 * {@link org.springframework.context.ApplicationContext} to do it. 261 */ 262 @Override initApplicationContext()263 protected final void initApplicationContext() throws ApplicationContextException { 264 this.transformerFactory = newTransformerFactory(this.transformerFactoryClass); 265 this.transformerFactory.setErrorListener(this.errorListener); 266 if (this.uriResolver != null) { 267 this.transformerFactory.setURIResolver(this.uriResolver); 268 } 269 if (getStylesheetLocation() != null && !this.customContentTypeSet) { 270 // Use "text/html" as default (instead of "text/xml") if a stylesheet 271 // has been configured but no custom content type has been set. 272 super.setContentType(DEFAULT_CONTENT_TYPE); 273 } 274 try { 275 getTemplates(); 276 } 277 catch (TransformerConfigurationException ex) { 278 throw new ApplicationContextException("Cannot load stylesheet for XSLT view '" + getBeanName() + "'", ex); 279 } 280 } 281 282 /** 283 * Instantiate a new TransformerFactory for this view. 284 * <p>The default implementation simply calls 285 * {@link javax.xml.transform.TransformerFactory#newInstance()}. 286 * If a {@link #setTransformerFactoryClass "transformerFactoryClass"} 287 * has been specified explicitly, the default constructor of the 288 * specified class will be called instead. 289 * <p>Can be overridden in subclasses. 290 * @param transformerFactoryClass the specified factory class (if any) 291 * @return the new TransactionFactory instance 292 * @throws TransformerFactoryConfigurationError in case of instantiation failure 293 * @see #setTransformerFactoryClass 294 * @see #getTransformerFactory() 295 */ newTransformerFactory(Class transformerFactoryClass)296 protected TransformerFactory newTransformerFactory(Class transformerFactoryClass) { 297 if (transformerFactoryClass != null) { 298 try { 299 return (TransformerFactory) transformerFactoryClass.newInstance(); 300 } 301 catch (Exception ex) { 302 throw new TransformerFactoryConfigurationError(ex, "Could not instantiate TransformerFactory"); 303 } 304 } 305 else { 306 return TransformerFactory.newInstance(); 307 } 308 } 309 310 /** 311 * Return the TransformerFactory used by this view. 312 * Available once the View object has been fully initialized. 313 */ getTransformerFactory()314 protected final TransformerFactory getTransformerFactory() { 315 return this.transformerFactory; 316 } 317 318 319 @Override renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)320 protected final void renderMergedOutputModel( 321 Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 322 323 response.setContentType(getContentType()); 324 325 Source source = null; 326 String docRoot = null; 327 // Value of a single element in the map, if there is one. 328 Object singleModel = null; 329 330 if (this.useSingleModelNameAsRoot && model.size() == 1) { 331 docRoot = model.keySet().iterator().next(); 332 if (logger.isDebugEnabled()) { 333 logger.debug("Single model object received, key [" + docRoot + "] will be used as root tag"); 334 } 335 singleModel = model.get(docRoot); 336 } 337 338 // Handle special case when we have a single node. 339 if (singleModel instanceof Node || singleModel instanceof Source) { 340 // Don't domify if the model is already an XML node/source. 341 // We don't need to worry about model name, either: 342 // we leave the Node alone. 343 logger.debug("No need to domify: was passed an XML Node or Source"); 344 source = (singleModel instanceof Node ? new DOMSource((Node) singleModel) : (Source) singleModel); 345 } 346 else { 347 // docRoot local variable takes precedence 348 source = createXsltSource(model, (docRoot != null ? docRoot : this.root), request, response); 349 } 350 351 doTransform(model, source, request, response); 352 } 353 354 /** 355 * Return the XML {@link Source} to transform. 356 * @param model the model Map 357 * @param root name for root element. This can be supplied as a bean property 358 * to concrete subclasses within the view definition file, but will be overridden 359 * in the case of a single object in the model map to be the key for that object. 360 * If no root property is specified and multiple model objects exist, a default 361 * root tag name will be supplied. 362 * @param request HTTP request. Subclasses won't normally use this, as 363 * request processing should have been complete. However, we might want to 364 * create a RequestContext to expose as part of the model. 365 * @param response HTTP response. Subclasses won't normally use this, 366 * however there may sometimes be a need to set cookies. 367 * @return the XSLT Source to transform 368 * @throws Exception if an error occurs 369 */ createXsltSource( Map<String, Object> model, String root, HttpServletRequest request, HttpServletResponse response)370 protected Source createXsltSource( 371 Map<String, Object> model, String root, HttpServletRequest request, HttpServletResponse response) 372 throws Exception { 373 374 return null; 375 } 376 377 /** 378 * Perform the actual transformation, writing to the HTTP response. 379 * <p>The default implementation delegates to the 380 * {@link #doTransform(javax.xml.transform.Source, java.util.Map, javax.xml.transform.Result, String)} 381 * method, building a StreamResult for the ServletResponse OutputStream 382 * or for the ServletResponse Writer (according to {@link #useWriter()}). 383 * @param model the model Map 384 * @param source the Source to transform 385 * @param request current HTTP request 386 * @param response current HTTP response 387 * @throws Exception if an error occurs 388 * @see javax.xml.transform.stream.StreamResult 389 * @see javax.servlet.ServletResponse#getOutputStream() 390 * @see javax.servlet.ServletResponse#getWriter() 391 * @see #useWriter() 392 */ doTransform( Map<String, Object> model, Source source, HttpServletRequest request, HttpServletResponse response)393 protected void doTransform( 394 Map<String, Object> model, Source source, HttpServletRequest request, HttpServletResponse response) 395 throws Exception { 396 397 Map<String, Object> parameters = getParameters(model, request); 398 Result result = (useWriter() ? 399 new StreamResult(response.getWriter()) : 400 new StreamResult(new BufferedOutputStream(response.getOutputStream()))); 401 String encoding = response.getCharacterEncoding(); 402 doTransform(source, parameters, result, encoding); 403 } 404 405 /** 406 * Return a Map of transformer parameters to be applied to the stylesheet. 407 * <p>Subclasses can override this method in order to apply one or more 408 * parameters to the transformation process. 409 * <p>The default implementation delegates to the 410 * {@link #getParameters(HttpServletRequest)} variant. 411 * @param model the model Map 412 * @param request current HTTP request 413 * @return a Map of parameters to apply to the transformation process 414 * @see javax.xml.transform.Transformer#setParameter 415 */ getParameters(Map<String, Object> model, HttpServletRequest request)416 protected Map getParameters(Map<String, Object> model, HttpServletRequest request) { 417 return getParameters(request); 418 } 419 420 /** 421 * Return a Map of transformer parameters to be applied to the stylesheet. 422 * <p>Subclasses can override this method in order to apply one or more 423 * parameters to the transformation process. 424 * <p>The default implementation simply returns <code>null</code>. 425 * @param request current HTTP request 426 * @return a Map of parameters to apply to the transformation process 427 * @see #getParameters(Map, HttpServletRequest) 428 * @see javax.xml.transform.Transformer#setParameter 429 */ getParameters(HttpServletRequest request)430 protected Map getParameters(HttpServletRequest request) { 431 return null; 432 } 433 434 /** 435 * Return whether to use a <code>java.io.Writer</code> to write text content 436 * to the HTTP response. Else, a <code>java.io.OutputStream</code> will be used, 437 * to write binary content to the response. 438 * <p>The default implementation returns <code>false</code>, indicating a 439 * a <code>java.io.OutputStream</code>. 440 * @return whether to use a Writer (<code>true</code>) or an OutputStream 441 * (<code>false</code>) 442 * @see javax.servlet.ServletResponse#getWriter() 443 * @see javax.servlet.ServletResponse#getOutputStream() 444 */ useWriter()445 protected boolean useWriter() { 446 return false; 447 } 448 449 450 /** 451 * Perform the actual transformation, writing to the given result. 452 * @param source the Source to transform 453 * @param parameters a Map of parameters to be applied to the stylesheet 454 * (as determined by {@link #getParameters(Map, HttpServletRequest)}) 455 * @param result the result to write to 456 * @param encoding the preferred character encoding that the underlying Transformer should use 457 * @throws Exception if an error occurs 458 */ doTransform(Source source, Map<String, Object> parameters, Result result, String encoding)459 protected void doTransform(Source source, Map<String, Object> parameters, Result result, String encoding) 460 throws Exception { 461 462 try { 463 Transformer trans = buildTransformer(parameters); 464 465 // Explicitly apply URIResolver to every created Transformer. 466 if (this.uriResolver != null) { 467 trans.setURIResolver(this.uriResolver); 468 } 469 470 // Specify default output properties. 471 trans.setOutputProperty(OutputKeys.ENCODING, encoding); 472 if (this.indent) { 473 TransformerUtils.enableIndenting(trans); 474 } 475 476 // Apply any arbitrary output properties, if specified. 477 if (this.outputProperties != null) { 478 Enumeration propsEnum = this.outputProperties.propertyNames(); 479 while (propsEnum.hasMoreElements()) { 480 String propName = (String) propsEnum.nextElement(); 481 trans.setOutputProperty(propName, this.outputProperties.getProperty(propName)); 482 } 483 } 484 485 // Perform the actual XSLT transformation. 486 trans.transform(source, result); 487 } 488 catch (TransformerConfigurationException ex) { 489 throw new NestedServletException( 490 "Couldn't create XSLT transformer in XSLT view with name [" + getBeanName() + "]", ex); 491 } 492 catch (TransformerException ex) { 493 throw new NestedServletException( 494 "Couldn't perform transform in XSLT view with name [" + getBeanName() + "]", ex); 495 } 496 } 497 498 /** 499 * Build a Transformer object for immediate use, based on the 500 * given parameters. 501 * @param parameters a Map of parameters to be applied to the stylesheet 502 * (as determined by {@link #getParameters(Map, HttpServletRequest)}) 503 * @return the Transformer object (never <code>null</code>) 504 * @throws TransformerConfigurationException if the Transformer object 505 * could not be built 506 */ buildTransformer(Map<String, Object> parameters)507 protected Transformer buildTransformer(Map<String, Object> parameters) throws TransformerConfigurationException { 508 Templates templates = getTemplates(); 509 Transformer transformer = 510 (templates != null ? templates.newTransformer() : getTransformerFactory().newTransformer()); 511 applyTransformerParameters(parameters, transformer); 512 return transformer; 513 } 514 515 /** 516 * Obtain the Templates object to use, based on the configured 517 * stylesheet, either a cached one or a freshly built one. 518 * <p>Subclasses may override this method e.g. in order to refresh 519 * the Templates instance, calling {@link #resetCachedTemplates()} 520 * before delegating to this <code>getTemplates()</code> implementation. 521 * @return the Templates object (or <code>null</code> if there is 522 * no stylesheet specified) 523 * @throws TransformerConfigurationException if the Templates object 524 * could not be built 525 * @see #setStylesheetLocation 526 * @see #setCache 527 * @see #resetCachedTemplates 528 */ getTemplates()529 protected Templates getTemplates() throws TransformerConfigurationException { 530 if (this.cachedTemplates != null) { 531 return this.cachedTemplates; 532 } 533 Resource location = getStylesheetLocation(); 534 if (location != null) { 535 Templates templates = getTransformerFactory().newTemplates(getStylesheetSource(location)); 536 if (this.cache) { 537 this.cachedTemplates = templates; 538 } 539 return templates; 540 } 541 return null; 542 } 543 544 /** 545 * Apply the specified parameters to the given Transformer. 546 * @param parameters the transformer parameters 547 * (as determined by {@link #getParameters(Map, HttpServletRequest)}) 548 * @param transformer the Transformer to aply the parameters 549 */ applyTransformerParameters(Map<String, Object> parameters, Transformer transformer)550 protected void applyTransformerParameters(Map<String, Object> parameters, Transformer transformer) { 551 if (parameters != null) { 552 for (Map.Entry<String, Object> entry : parameters.entrySet()) { 553 transformer.setParameter(entry.getKey(), entry.getValue()); 554 } 555 } 556 } 557 558 /** 559 * Load the stylesheet from the specified location. 560 * @param stylesheetLocation the stylesheet resource to be loaded 561 * @return the stylesheet source 562 * @throws ApplicationContextException if the stylesheet resource could not be loaded 563 */ getStylesheetSource(Resource stylesheetLocation)564 protected Source getStylesheetSource(Resource stylesheetLocation) throws ApplicationContextException { 565 if (logger.isDebugEnabled()) { 566 logger.debug("Loading XSLT stylesheet from " + stylesheetLocation); 567 } 568 try { 569 URL url = stylesheetLocation.getURL(); 570 String urlPath = url.toString(); 571 String systemId = urlPath.substring(0, urlPath.lastIndexOf('/') + 1); 572 return new StreamSource(url.openStream(), systemId); 573 } 574 catch (IOException ex) { 575 throw new ApplicationContextException("Can't load XSLT stylesheet from " + stylesheetLocation, ex); 576 } 577 } 578 579 } 580