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