001/* 002 * Copyright 2002-2013 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.velocity; 018 019import java.util.Locale; 020import java.util.Map; 021import java.util.TimeZone; 022import javax.servlet.http.HttpServletRequest; 023import javax.servlet.http.HttpServletResponse; 024 025import org.apache.velocity.Template; 026import org.apache.velocity.VelocityContext; 027import org.apache.velocity.app.VelocityEngine; 028import org.apache.velocity.context.Context; 029import org.apache.velocity.exception.MethodInvocationException; 030import org.apache.velocity.exception.ResourceNotFoundException; 031import org.apache.velocity.tools.generic.DateTool; 032import org.apache.velocity.tools.generic.NumberTool; 033 034import org.springframework.beans.BeansException; 035import org.springframework.beans.factory.BeanFactoryUtils; 036import org.springframework.beans.factory.NoSuchBeanDefinitionException; 037import org.springframework.context.ApplicationContextException; 038import org.springframework.core.NestedIOException; 039import org.springframework.web.servlet.support.RequestContextUtils; 040import org.springframework.web.servlet.view.AbstractTemplateView; 041import org.springframework.web.util.NestedServletException; 042 043/** 044 * View using the Velocity template engine. 045 * 046 * <p>Exposes the following JavaBean properties: 047 * <ul> 048 * <li><b>url</b>: the location of the Velocity template to be wrapped, 049 * relative to the Velocity resource loader path (see VelocityConfigurer). 050 * <li><b>encoding</b> (optional, default is determined by Velocity configuration): 051 * the encoding of the Velocity template file 052 * <li><b>velocityFormatterAttribute</b> (optional, default=null): the name of 053 * the VelocityFormatter helper object to expose in the Velocity context of this 054 * view, or {@code null} if not needed. VelocityFormatter is part of standard Velocity. 055 * <li><b>dateToolAttribute</b> (optional, default=null): the name of the 056 * DateTool helper object to expose in the Velocity context of this view, 057 * or {@code null} if not needed. DateTool is part of Velocity Tools. 058 * <li><b>numberToolAttribute</b> (optional, default=null): the name of the 059 * NumberTool helper object to expose in the Velocity context of this view, 060 * or {@code null} if not needed. NumberTool is part of Velocity Tools. 061 * <li><b>cacheTemplate</b> (optional, default=false): whether or not the Velocity 062 * template should be cached. It should normally be true in production, but setting 063 * this to false enables us to modify Velocity templates without restarting the 064 * application (similar to JSPs). Note that this is a minor optimization only, 065 * as Velocity itself caches templates in a modification-aware fashion. 066 * </ul> 067 * 068 * <p>Depends on a VelocityConfig object such as VelocityConfigurer being 069 * accessible in the current web application context, with any bean name. 070 * Alternatively, you can set the VelocityEngine object as bean property. 071 * 072 * <p>Note: Spring 3.0's VelocityView requires Velocity 1.4 or higher, and optionally 073 * Velocity Tools 1.1 or higher (depending on the use of DateTool and/or NumberTool). 074 * 075 * @author Rod Johnson 076 * @author Juergen Hoeller 077 * @author Dave Syer 078 * @see VelocityConfig 079 * @see VelocityConfigurer 080 * @see #setUrl 081 * @see #setExposeSpringMacroHelpers 082 * @see #setEncoding 083 * @see #setVelocityEngine 084 * @see VelocityConfig 085 * @see VelocityConfigurer 086 * @deprecated as of Spring 4.3, in favor of FreeMarker 087 */ 088@Deprecated 089public class VelocityView extends AbstractTemplateView { 090 091 private Map<String, Class<?>> toolAttributes; 092 093 private String dateToolAttribute; 094 095 private String numberToolAttribute; 096 097 private String encoding; 098 099 private boolean cacheTemplate = false; 100 101 private VelocityEngine velocityEngine; 102 103 private Template template; 104 105 106 /** 107 * Set tool attributes to expose to the view, as attribute name / class name pairs. 108 * An instance of the given class will be added to the Velocity context for each 109 * rendering operation, under the given attribute name. 110 * <p>For example, an instance of MathTool, which is part of the generic package 111 * of Velocity Tools, can be bound under the attribute name "math", specifying the 112 * fully qualified class name "org.apache.velocity.tools.generic.MathTool" as value. 113 * <p>Note that VelocityView can only create simple generic tools or values, that is, 114 * classes with a public default constructor and no further initialization needs. 115 * This class does not do any further checks, to not introduce a required dependency 116 * on a specific tools package. 117 * <p>For tools that are part of the view package of Velocity Tools, a special 118 * Velocity context and a special init callback are needed. Use VelocityToolboxView 119 * in such a case, or override {@code createVelocityContext} and 120 * {@code initTool} accordingly. 121 * <p>For a simple VelocityFormatter instance or special locale-aware instances 122 * of DateTool/NumberTool, which are part of the generic package of Velocity Tools, 123 * specify the "velocityFormatterAttribute", "dateToolAttribute" or 124 * "numberToolAttribute" properties, respectively. 125 * @param toolAttributes attribute names as keys, and tool class names as values 126 * @see org.apache.velocity.tools.generic.MathTool 127 * @see VelocityToolboxView 128 * @see #createVelocityContext 129 * @see #initTool 130 * @see #setDateToolAttribute 131 * @see #setNumberToolAttribute 132 */ 133 public void setToolAttributes(Map<String, Class<?>> toolAttributes) { 134 this.toolAttributes = toolAttributes; 135 } 136 137 /** 138 * Set the name of the DateTool helper object to expose in the Velocity context 139 * of this view, or {@code null} if not needed. The exposed DateTool will be aware of 140 * the current locale, as determined by Spring's LocaleResolver. 141 * <p>DateTool is part of the generic package of Velocity Tools 1.0. 142 * Spring uses a special locale-aware subclass of DateTool. 143 * @see org.apache.velocity.tools.generic.DateTool 144 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale 145 * @see org.springframework.web.servlet.LocaleResolver 146 */ 147 public void setDateToolAttribute(String dateToolAttribute) { 148 this.dateToolAttribute = dateToolAttribute; 149 } 150 151 /** 152 * Set the name of the NumberTool helper object to expose in the Velocity context 153 * of this view, or {@code null} if not needed. The exposed NumberTool will be aware of 154 * the current locale, as determined by Spring's LocaleResolver. 155 * <p>NumberTool is part of the generic package of Velocity Tools 1.1. 156 * Spring uses a special locale-aware subclass of NumberTool. 157 * @see org.apache.velocity.tools.generic.NumberTool 158 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale 159 * @see org.springframework.web.servlet.LocaleResolver 160 */ 161 public void setNumberToolAttribute(String numberToolAttribute) { 162 this.numberToolAttribute = numberToolAttribute; 163 } 164 165 /** 166 * Set the encoding of the Velocity template file. Default is determined 167 * by the VelocityEngine: "ISO-8859-1" if not specified otherwise. 168 * <p>Specify the encoding in the VelocityEngine rather than per template 169 * if all your templates share a common encoding. 170 */ 171 public void setEncoding(String encoding) { 172 this.encoding = encoding; 173 } 174 175 /** 176 * Return the encoding for the Velocity template. 177 */ 178 protected String getEncoding() { 179 return this.encoding; 180 } 181 182 /** 183 * Set whether the Velocity template should be cached. Default is "false". 184 * It should normally be true in production, but setting this to false enables us to 185 * modify Velocity templates without restarting the application (similar to JSPs). 186 * <p>Note that this is a minor optimization only, as Velocity itself caches 187 * templates in a modification-aware fashion. 188 */ 189 public void setCacheTemplate(boolean cacheTemplate) { 190 this.cacheTemplate = cacheTemplate; 191 } 192 193 /** 194 * Return whether the Velocity template should be cached. 195 */ 196 protected boolean isCacheTemplate() { 197 return this.cacheTemplate; 198 } 199 200 /** 201 * Set the VelocityEngine to be used by this view. 202 * <p>If this is not set, the default lookup will occur: A single VelocityConfig 203 * is expected in the current web application context, with any bean name. 204 * @see VelocityConfig 205 */ 206 public void setVelocityEngine(VelocityEngine velocityEngine) { 207 this.velocityEngine = velocityEngine; 208 } 209 210 /** 211 * Return the VelocityEngine used by this view. 212 */ 213 protected VelocityEngine getVelocityEngine() { 214 return this.velocityEngine; 215 } 216 217 218 /** 219 * Invoked on startup. Looks for a single VelocityConfig bean to 220 * find the relevant VelocityEngine for this factory. 221 */ 222 @Override 223 protected void initApplicationContext() throws BeansException { 224 super.initApplicationContext(); 225 226 if (getVelocityEngine() == null) { 227 // No explicit VelocityEngine: try to autodetect one. 228 setVelocityEngine(autodetectVelocityEngine()); 229 } 230 } 231 232 /** 233 * Autodetect a VelocityEngine via the ApplicationContext. 234 * Called if no explicit VelocityEngine has been specified. 235 * @return the VelocityEngine to use for VelocityViews 236 * @throws BeansException if no VelocityEngine could be found 237 * @see #getApplicationContext 238 * @see #setVelocityEngine 239 */ 240 protected VelocityEngine autodetectVelocityEngine() throws BeansException { 241 try { 242 VelocityConfig velocityConfig = BeanFactoryUtils.beanOfTypeIncludingAncestors( 243 getApplicationContext(), VelocityConfig.class, true, false); 244 return velocityConfig.getVelocityEngine(); 245 } 246 catch (NoSuchBeanDefinitionException ex) { 247 throw new ApplicationContextException( 248 "Must define a single VelocityConfig bean in this web application context " + 249 "(may be inherited): VelocityConfigurer is the usual implementation. " + 250 "This bean may be given any name.", ex); 251 } 252 } 253 254 /** 255 * Check that the Velocity template used for this view exists and is valid. 256 * <p>Can be overridden to customize the behavior, for example in case of 257 * multiple templates to be rendered into a single view. 258 */ 259 @Override 260 public boolean checkResource(Locale locale) throws Exception { 261 try { 262 // Check that we can get the template, even if we might subsequently get it again. 263 this.template = getTemplate(getUrl()); 264 return true; 265 } 266 catch (ResourceNotFoundException ex) { 267 if (logger.isDebugEnabled()) { 268 logger.debug("No Velocity view found for URL: " + getUrl()); 269 } 270 return false; 271 } 272 catch (Exception ex) { 273 throw new NestedIOException( 274 "Could not load Velocity template for URL [" + getUrl() + "]", ex); 275 } 276 } 277 278 279 /** 280 * Process the model map by merging it with the Velocity template. 281 * Output is directed to the servlet response. 282 * <p>This method can be overridden if custom behavior is needed. 283 */ 284 @Override 285 protected void renderMergedTemplateModel( 286 Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 287 288 exposeHelpers(model, request); 289 290 Context velocityContext = createVelocityContext(model, request, response); 291 exposeHelpers(velocityContext, request, response); 292 exposeToolAttributes(velocityContext, request); 293 294 doRender(velocityContext, response); 295 } 296 297 /** 298 * Expose helpers unique to each rendering operation. This is necessary so that 299 * different rendering operations can't overwrite each other's formats etc. 300 * <p>Called by {@code renderMergedTemplateModel}. The default implementation 301 * is empty. This method can be overridden to add custom helpers to the model. 302 * @param model the model that will be passed to the template for merging 303 * @param request current HTTP request 304 * @throws Exception if there's a fatal error while we're adding model attributes 305 * @see #renderMergedTemplateModel 306 */ 307 protected void exposeHelpers(Map<String, Object> model, HttpServletRequest request) throws Exception { 308 } 309 310 /** 311 * Create a Velocity Context instance for the given model, 312 * to be passed to the template for merging. 313 * <p>The default implementation delegates to {@link #createVelocityContext(Map)}. 314 * Can be overridden for a special context class, for example ChainedContext which 315 * is part of the view package of Velocity Tools. ChainedContext is needed for 316 * initialization of ViewTool instances. 317 * <p>Have a look at {@link VelocityToolboxView}, which pre-implements 318 * ChainedContext support. This is not part of the standard VelocityView class 319 * in order to avoid a required dependency on the view package of Velocity Tools. 320 * @param model the model Map, containing the model attributes to be exposed to the view 321 * @param request current HTTP request 322 * @param response current HTTP response 323 * @return the Velocity Context 324 * @throws Exception if there's a fatal error while creating the context 325 * @see #createVelocityContext(Map) 326 * @see #initTool 327 * @see org.apache.velocity.tools.view.context.ChainedContext 328 * @see VelocityToolboxView 329 */ 330 protected Context createVelocityContext( 331 Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 332 333 return createVelocityContext(model); 334 } 335 336 /** 337 * Create a Velocity Context instance for the given model, 338 * to be passed to the template for merging. 339 * <p>Default implementation creates an instance of Velocity's 340 * VelocityContext implementation class. 341 * @param model the model Map, containing the model attributes 342 * to be exposed to the view 343 * @return the Velocity Context 344 * @throws Exception if there's a fatal error while creating the context 345 * @see org.apache.velocity.VelocityContext 346 */ 347 protected Context createVelocityContext(Map<String, Object> model) throws Exception { 348 return new VelocityContext(model); 349 } 350 351 /** 352 * Expose helpers unique to each rendering operation. This is necessary so that 353 * different rendering operations can't overwrite each other's formats etc. 354 * <p>Called by {@code renderMergedTemplateModel}. Default implementation 355 * delegates to {@code exposeHelpers(velocityContext, request)}. This method 356 * can be overridden to add special tools to the context, needing the servlet response 357 * to initialize (see Velocity Tools, for example LinkTool and ViewTool/ChainedContext). 358 * @param velocityContext Velocity context that will be passed to the template 359 * @param request current HTTP request 360 * @param response current HTTP response 361 * @throws Exception if there's a fatal error while we're adding model attributes 362 * @see #exposeHelpers(org.apache.velocity.context.Context, HttpServletRequest) 363 */ 364 protected void exposeHelpers( 365 Context velocityContext, HttpServletRequest request, HttpServletResponse response) throws Exception { 366 367 exposeHelpers(velocityContext, request); 368 } 369 370 /** 371 * Expose helpers unique to each rendering operation. This is necessary so that 372 * different rendering operations can't overwrite each other's formats etc. 373 * <p>Default implementation is empty. This method can be overridden to add 374 * custom helpers to the Velocity context. 375 * @param velocityContext Velocity context that will be passed to the template 376 * @param request current HTTP request 377 * @throws Exception if there's a fatal error while we're adding model attributes 378 * @see #exposeHelpers(Map, HttpServletRequest) 379 */ 380 protected void exposeHelpers(Context velocityContext, HttpServletRequest request) throws Exception { 381 } 382 383 /** 384 * Expose the tool attributes, according to corresponding bean property settings. 385 * <p>Do not override this method unless for further tools driven by bean properties. 386 * Override one of the {@code exposeHelpers} methods to add custom helpers. 387 * @param velocityContext Velocity context that will be passed to the template 388 * @param request current HTTP request 389 * @throws Exception if there's a fatal error while we're adding model attributes 390 * @see #setDateToolAttribute 391 * @see #setNumberToolAttribute 392 * @see #exposeHelpers(Map, HttpServletRequest) 393 * @see #exposeHelpers(org.apache.velocity.context.Context, HttpServletRequest, HttpServletResponse) 394 */ 395 protected void exposeToolAttributes(Context velocityContext, HttpServletRequest request) throws Exception { 396 // Expose generic attributes. 397 if (this.toolAttributes != null) { 398 for (Map.Entry<String, Class<?>> entry : this.toolAttributes.entrySet()) { 399 String attributeName = entry.getKey(); 400 Class<?> toolClass = entry.getValue(); 401 try { 402 Object tool = toolClass.newInstance(); 403 initTool(tool, velocityContext); 404 velocityContext.put(attributeName, tool); 405 } 406 catch (Exception ex) { 407 throw new NestedServletException("Could not instantiate Velocity tool '" + attributeName + "'", ex); 408 } 409 } 410 } 411 412 // Expose locale-aware DateTool/NumberTool attributes. 413 if (this.dateToolAttribute != null || this.numberToolAttribute != null) { 414 if (this.dateToolAttribute != null) { 415 velocityContext.put(this.dateToolAttribute, new LocaleAwareDateTool(request)); 416 } 417 if (this.numberToolAttribute != null) { 418 velocityContext.put(this.numberToolAttribute, new LocaleAwareNumberTool(request)); 419 } 420 } 421 } 422 423 /** 424 * Initialize the given tool instance. The default implementation is empty. 425 * <p>Can be overridden to check for special callback interfaces, for example 426 * the ViewContext interface which is part of the view package of Velocity Tools. 427 * In the particular case of ViewContext, you'll usually also need a special 428 * Velocity context, like ChainedContext which is part of Velocity Tools too. 429 * <p>Have a look at {@link VelocityToolboxView}, which pre-implements such a 430 * ViewTool check. This is not part of the standard VelocityView class in order 431 * to avoid a required dependency on the view package of Velocity Tools. 432 * @param tool the tool instance to initialize 433 * @param velocityContext the Velocity context 434 * @throws Exception if initializion of the tool failed 435 * @see #createVelocityContext 436 * @see org.apache.velocity.tools.view.context.ViewContext 437 * @see org.apache.velocity.tools.view.context.ChainedContext 438 * @see VelocityToolboxView 439 */ 440 protected void initTool(Object tool, Context velocityContext) throws Exception { 441 } 442 443 444 /** 445 * Render the Velocity view to the given response, using the given Velocity 446 * context which contains the complete template model to use. 447 * <p>The default implementation renders the template specified by the "url" 448 * bean property, retrieved via {@code getTemplate}. It delegates to the 449 * {@code mergeTemplate} method to merge the template instance with the 450 * given Velocity context. 451 * <p>Can be overridden to customize the behavior, for example to render 452 * multiple templates into a single view. 453 * @param context the Velocity context to use for rendering 454 * @param response servlet response (use this to get the OutputStream or Writer) 455 * @throws Exception if thrown by Velocity 456 * @see #setUrl 457 * @see #getTemplate() 458 * @see #mergeTemplate 459 */ 460 protected void doRender(Context context, HttpServletResponse response) throws Exception { 461 if (logger.isDebugEnabled()) { 462 logger.debug("Rendering Velocity template [" + getUrl() + "] in VelocityView '" + getBeanName() + "'"); 463 } 464 mergeTemplate(getTemplate(), context, response); 465 } 466 467 /** 468 * Retrieve the Velocity template to be rendered by this view. 469 * <p>By default, the template specified by the "url" bean property will be 470 * retrieved: either returning a cached template instance or loading a fresh 471 * instance (according to the "cacheTemplate" bean property) 472 * @return the Velocity template to render 473 * @throws Exception if thrown by Velocity 474 * @see #setUrl 475 * @see #setCacheTemplate 476 * @see #getTemplate(String) 477 */ 478 protected Template getTemplate() throws Exception { 479 // We already hold a reference to the template, but we might want to load it 480 // if not caching. Velocity itself caches templates, so our ability to 481 // cache templates in this class is a minor optimization only. 482 if (isCacheTemplate() && this.template != null) { 483 return this.template; 484 } 485 else { 486 return getTemplate(getUrl()); 487 } 488 } 489 490 /** 491 * Retrieve the Velocity template specified by the given name, 492 * using the encoding specified by the "encoding" bean property. 493 * <p>Can be called by subclasses to retrieve a specific template, 494 * for example to render multiple templates into a single view. 495 * @param name the file name of the desired template 496 * @return the Velocity template 497 * @throws Exception if thrown by Velocity 498 * @see org.apache.velocity.app.VelocityEngine#getTemplate 499 */ 500 protected Template getTemplate(String name) throws Exception { 501 return (getEncoding() != null ? 502 getVelocityEngine().getTemplate(name, getEncoding()) : 503 getVelocityEngine().getTemplate(name)); 504 } 505 506 /** 507 * Merge the template with the context. 508 * Can be overridden to customize the behavior. 509 * @param template the template to merge 510 * @param context the Velocity context to use for rendering 511 * @param response servlet response (use this to get the OutputStream or Writer) 512 * @throws Exception if thrown by Velocity 513 * @see org.apache.velocity.Template#merge 514 */ 515 protected void mergeTemplate( 516 Template template, Context context, HttpServletResponse response) throws Exception { 517 518 try { 519 template.merge(context, response.getWriter()); 520 } 521 catch (MethodInvocationException ex) { 522 Throwable cause = ex.getWrappedThrowable(); 523 throw new NestedServletException( 524 "Method invocation failed during rendering of Velocity view with name '" + 525 getBeanName() + "': " + ex.getMessage() + "; reference [" + ex.getReferenceName() + 526 "], method '" + ex.getMethodName() + "'", 527 cause==null ? ex : cause); 528 } 529 } 530 531 532 /** 533 * Subclass of DateTool from Velocity Tools, using a Spring-resolved 534 * Locale and TimeZone instead of the default Locale. 535 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale 536 * @see org.springframework.web.servlet.support.RequestContextUtils#getTimeZone 537 */ 538 private static class LocaleAwareDateTool extends DateTool { 539 540 private final HttpServletRequest request; 541 542 public LocaleAwareDateTool(HttpServletRequest request) { 543 this.request = request; 544 } 545 546 @Override 547 public Locale getLocale() { 548 return RequestContextUtils.getLocale(this.request); 549 } 550 551 @Override 552 public TimeZone getTimeZone() { 553 TimeZone timeZone = RequestContextUtils.getTimeZone(this.request); 554 return (timeZone != null ? timeZone : super.getTimeZone()); 555 } 556 } 557 558 559 /** 560 * Subclass of NumberTool from Velocity Tools, using a Spring-resolved 561 * Locale instead of the default Locale. 562 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale 563 */ 564 private static class LocaleAwareNumberTool extends NumberTool { 565 566 private final HttpServletRequest request; 567 568 public LocaleAwareNumberTool(HttpServletRequest request) { 569 this.request = request; 570 } 571 572 @Override 573 public Locale getLocale() { 574 return RequestContextUtils.getLocale(this.request); 575 } 576 } 577 578}