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