001/* 002 * Copyright 2002-2018 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.freemarker; 018 019import java.io.FileNotFoundException; 020import java.io.IOException; 021import java.util.Collections; 022import java.util.Enumeration; 023import java.util.Locale; 024import java.util.Map; 025 026import javax.servlet.GenericServlet; 027import javax.servlet.ServletConfig; 028import javax.servlet.ServletContext; 029import javax.servlet.ServletException; 030import javax.servlet.ServletRequest; 031import javax.servlet.ServletResponse; 032import javax.servlet.http.HttpServletRequest; 033import javax.servlet.http.HttpServletResponse; 034import javax.servlet.http.HttpSession; 035 036import freemarker.core.ParseException; 037import freemarker.ext.jsp.TaglibFactory; 038import freemarker.ext.servlet.AllHttpScopesHashModel; 039import freemarker.ext.servlet.FreemarkerServlet; 040import freemarker.ext.servlet.HttpRequestHashModel; 041import freemarker.ext.servlet.HttpRequestParametersHashModel; 042import freemarker.ext.servlet.HttpSessionHashModel; 043import freemarker.ext.servlet.ServletContextHashModel; 044import freemarker.template.Configuration; 045import freemarker.template.DefaultObjectWrapperBuilder; 046import freemarker.template.ObjectWrapper; 047import freemarker.template.SimpleHash; 048import freemarker.template.Template; 049import freemarker.template.TemplateException; 050 051import org.springframework.beans.BeansException; 052import org.springframework.beans.factory.BeanFactoryUtils; 053import org.springframework.beans.factory.BeanInitializationException; 054import org.springframework.beans.factory.NoSuchBeanDefinitionException; 055import org.springframework.context.ApplicationContextException; 056import org.springframework.lang.Nullable; 057import org.springframework.util.Assert; 058import org.springframework.web.servlet.support.RequestContextUtils; 059import org.springframework.web.servlet.view.AbstractTemplateView; 060 061/** 062 * View using the FreeMarker template engine. 063 * 064 * <p>Exposes the following JavaBean properties: 065 * <ul> 066 * <li><b>url</b>: the location of the FreeMarker template to be wrapped, 067 * relative to the FreeMarker template context (directory). 068 * <li><b>encoding</b> (optional, default is determined by FreeMarker configuration): 069 * the encoding of the FreeMarker template file 070 * </ul> 071 * 072 * <p>Depends on a single {@link FreeMarkerConfig} object such as {@link FreeMarkerConfigurer} 073 * being accessible in the current web application context, with any bean name. 074 * Alternatively, you can set the FreeMarker {@link Configuration} object as a 075 * bean property. See {@link #setConfiguration} for more details on the impacts 076 * of this approach. 077 * 078 * <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. 079 * 080 * @author Darren Davison 081 * @author Juergen Hoeller 082 * @since 03.03.2004 083 * @see #setUrl 084 * @see #setExposeSpringMacroHelpers 085 * @see #setEncoding 086 * @see #setConfiguration 087 * @see FreeMarkerConfig 088 * @see FreeMarkerConfigurer 089 */ 090public class FreeMarkerView extends AbstractTemplateView { 091 092 @Nullable 093 private String encoding; 094 095 @Nullable 096 private Configuration configuration; 097 098 @Nullable 099 private TaglibFactory taglibFactory; 100 101 @Nullable 102 private ServletContextHashModel servletContextHashModel; 103 104 105 /** 106 * Set the encoding of the FreeMarker template file. Default is determined 107 * by the FreeMarker Configuration: "ISO-8859-1" if not specified otherwise. 108 * <p>Specify the encoding in the FreeMarker Configuration rather than per 109 * template if all your templates share a common encoding. 110 */ 111 public void setEncoding(@Nullable String encoding) { 112 this.encoding = encoding; 113 } 114 115 /** 116 * Return the encoding for the FreeMarker template. 117 */ 118 @Nullable 119 protected String getEncoding() { 120 return this.encoding; 121 } 122 123 /** 124 * Set the FreeMarker Configuration to be used by this view. 125 * <p>If this is not set, the default lookup will occur: a single {@link FreeMarkerConfig} 126 * is expected in the current web application context, with any bean name. 127 * <strong>Note:</strong> using this method will cause a new instance of {@link TaglibFactory} 128 * to created for every single {@link FreeMarkerView} instance. This can be quite expensive 129 * in terms of memory and initial CPU usage. In production it is recommended that you use 130 * a {@link FreeMarkerConfig} which exposes a single shared {@link TaglibFactory}. 131 */ 132 public void setConfiguration(@Nullable Configuration configuration) { 133 this.configuration = configuration; 134 } 135 136 /** 137 * Return the FreeMarker configuration used by this view. 138 */ 139 @Nullable 140 protected Configuration getConfiguration() { 141 return this.configuration; 142 } 143 144 /** 145 * Obtain the FreeMarker configuration for actual use. 146 * @return the FreeMarker configuration (never {@code null}) 147 * @throws IllegalStateException in case of no Configuration object set 148 * @since 5.0 149 */ 150 protected Configuration obtainConfiguration() { 151 Configuration configuration = getConfiguration(); 152 Assert.state(configuration != null, "No Configuration set"); 153 return configuration; 154 } 155 156 157 /** 158 * Invoked on startup. Looks for a single FreeMarkerConfig bean to 159 * find the relevant Configuration for this factory. 160 * <p>Checks that the template for the default Locale can be found: 161 * FreeMarker will check non-Locale-specific templates if a 162 * locale-specific one is not found. 163 * @see freemarker.cache.TemplateCache#getTemplate 164 */ 165 @Override 166 protected void initServletContext(ServletContext servletContext) throws BeansException { 167 if (getConfiguration() != null) { 168 this.taglibFactory = new TaglibFactory(servletContext); 169 } 170 else { 171 FreeMarkerConfig config = autodetectConfiguration(); 172 setConfiguration(config.getConfiguration()); 173 this.taglibFactory = config.getTaglibFactory(); 174 } 175 176 GenericServlet servlet = new GenericServletAdapter(); 177 try { 178 servlet.init(new DelegatingServletConfig()); 179 } 180 catch (ServletException ex) { 181 throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex); 182 } 183 this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper()); 184 } 185 186 /** 187 * Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext. 188 * @return the Configuration instance to use for FreeMarkerViews 189 * @throws BeansException if no Configuration instance could be found 190 * @see #getApplicationContext 191 * @see #setConfiguration 192 */ 193 protected FreeMarkerConfig autodetectConfiguration() throws BeansException { 194 try { 195 return BeanFactoryUtils.beanOfTypeIncludingAncestors( 196 obtainApplicationContext(), FreeMarkerConfig.class, true, false); 197 } 198 catch (NoSuchBeanDefinitionException ex) { 199 throw new ApplicationContextException( 200 "Must define a single FreeMarkerConfig bean in this web application context " + 201 "(may be inherited): FreeMarkerConfigurer is the usual implementation. " + 202 "This bean may be given any name.", ex); 203 } 204 } 205 206 /** 207 * Return the configured FreeMarker {@link ObjectWrapper}, or the 208 * {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified. 209 * @see freemarker.template.Configuration#getObjectWrapper() 210 */ 211 protected ObjectWrapper getObjectWrapper() { 212 ObjectWrapper ow = obtainConfiguration().getObjectWrapper(); 213 return (ow != null ? ow : 214 new DefaultObjectWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build()); 215 } 216 217 /** 218 * Check that the FreeMarker template used for this view exists and is valid. 219 * <p>Can be overridden to customize the behavior, for example in case of 220 * multiple templates to be rendered into a single view. 221 */ 222 @Override 223 public boolean checkResource(Locale locale) throws Exception { 224 String url = getUrl(); 225 Assert.state(url != null, "'url' not set"); 226 227 try { 228 // Check that we can get the template, even if we might subsequently get it again. 229 getTemplate(url, locale); 230 return true; 231 } 232 catch (FileNotFoundException ex) { 233 // Allow for ViewResolver chaining... 234 return false; 235 } 236 catch (ParseException ex) { 237 throw new ApplicationContextException("Failed to parse [" + url + "]", ex); 238 } 239 catch (IOException ex) { 240 throw new ApplicationContextException("Failed to load [" + url + "]", ex); 241 } 242 } 243 244 245 /** 246 * Process the model map by merging it with the FreeMarker template. 247 * Output is directed to the servlet response. 248 * <p>This method can be overridden if custom behavior is needed. 249 */ 250 @Override 251 protected void renderMergedTemplateModel( 252 Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 253 254 exposeHelpers(model, request); 255 doRender(model, request, response); 256 } 257 258 /** 259 * Expose helpers unique to each rendering operation. This is necessary so that 260 * different rendering operations can't overwrite each other's formats etc. 261 * <p>Called by {@code renderMergedTemplateModel}. The default implementation 262 * is empty. This method can be overridden to add custom helpers to the model. 263 * @param model the model that will be passed to the template at merge time 264 * @param request current HTTP request 265 * @throws Exception if there's a fatal error while we're adding information to the context 266 * @see #renderMergedTemplateModel 267 */ 268 protected void exposeHelpers(Map<String, Object> model, HttpServletRequest request) throws Exception { 269 } 270 271 /** 272 * Render the FreeMarker view to the given response, using the given model 273 * map which contains the complete template model to use. 274 * <p>The default implementation renders the template specified by the "url" 275 * bean property, retrieved via {@code getTemplate}. It delegates to the 276 * {@code processTemplate} method to merge the template instance with 277 * the given template model. 278 * <p>Adds the standard Freemarker hash models to the model: request parameters, 279 * request, session and application (ServletContext), as well as the JSP tag 280 * library hash model. 281 * <p>Can be overridden to customize the behavior, for example to render 282 * multiple templates into a single view. 283 * @param model the model to use for rendering 284 * @param request current HTTP request 285 * @param response current servlet response 286 * @throws IOException if the template file could not be retrieved 287 * @throws Exception if rendering failed 288 * @see #setUrl 289 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale 290 * @see #getTemplate(java.util.Locale) 291 * @see #processTemplate 292 * @see freemarker.ext.servlet.FreemarkerServlet 293 */ 294 protected void doRender(Map<String, Object> model, HttpServletRequest request, 295 HttpServletResponse response) throws Exception { 296 297 // Expose model to JSP tags (as request attributes). 298 exposeModelAsRequestAttributes(model, request); 299 // Expose all standard FreeMarker hash models. 300 SimpleHash fmModel = buildTemplateModel(model, request, response); 301 302 // Grab the locale-specific version of the template. 303 Locale locale = RequestContextUtils.getLocale(request); 304 processTemplate(getTemplate(locale), fmModel, response); 305 } 306 307 /** 308 * Build a FreeMarker template model for the given model Map. 309 * <p>The default implementation builds a {@link AllHttpScopesHashModel}. 310 * @param model the model to use for rendering 311 * @param request current HTTP request 312 * @param response current servlet response 313 * @return the FreeMarker template model, as a {@link SimpleHash} or subclass thereof 314 */ 315 protected SimpleHash buildTemplateModel(Map<String, Object> model, HttpServletRequest request, 316 HttpServletResponse response) { 317 318 AllHttpScopesHashModel fmModel = new AllHttpScopesHashModel(getObjectWrapper(), getServletContext(), request); 319 fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory); 320 fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel); 321 fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response)); 322 fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper())); 323 fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request)); 324 fmModel.putAll(model); 325 return fmModel; 326 } 327 328 /** 329 * Build a FreeMarker {@link HttpSessionHashModel} for the given request, 330 * detecting whether a session already exists and reacting accordingly. 331 * @param request current HTTP request 332 * @param response current servlet response 333 * @return the FreeMarker HttpSessionHashModel 334 */ 335 private HttpSessionHashModel buildSessionModel(HttpServletRequest request, HttpServletResponse response) { 336 HttpSession session = request.getSession(false); 337 if (session != null) { 338 return new HttpSessionHashModel(session, getObjectWrapper()); 339 } 340 else { 341 return new HttpSessionHashModel(null, request, response, getObjectWrapper()); 342 } 343 } 344 345 /** 346 * Retrieve the FreeMarker template for the given locale, 347 * to be rendering by this view. 348 * <p>By default, the template specified by the "url" bean property 349 * will be retrieved. 350 * @param locale the current locale 351 * @return the FreeMarker template to render 352 * @throws IOException if the template file could not be retrieved 353 * @see #setUrl 354 * @see #getTemplate(String, java.util.Locale) 355 */ 356 protected Template getTemplate(Locale locale) throws IOException { 357 String url = getUrl(); 358 Assert.state(url != null, "'url' not set"); 359 return getTemplate(url, locale); 360 } 361 362 /** 363 * Retrieve the FreeMarker template specified by the given name, 364 * using the encoding specified by the "encoding" bean property. 365 * <p>Can be called by subclasses to retrieve a specific template, 366 * for example to render multiple templates into a single view. 367 * @param name the file name of the desired template 368 * @param locale the current locale 369 * @return the FreeMarker template 370 * @throws IOException if the template file could not be retrieved 371 */ 372 protected Template getTemplate(String name, Locale locale) throws IOException { 373 return (getEncoding() != null ? 374 obtainConfiguration().getTemplate(name, locale, getEncoding()) : 375 obtainConfiguration().getTemplate(name, locale)); 376 } 377 378 /** 379 * Process the FreeMarker template to the servlet response. 380 * <p>Can be overridden to customize the behavior. 381 * @param template the template to process 382 * @param model the model for the template 383 * @param response servlet response (use this to get the OutputStream or Writer) 384 * @throws IOException if the template file could not be retrieved 385 * @throws TemplateException if thrown by FreeMarker 386 * @see freemarker.template.Template#process(Object, java.io.Writer) 387 */ 388 protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) 389 throws IOException, TemplateException { 390 391 template.process(model, response.getWriter()); 392 } 393 394 395 /** 396 * Simple adapter class that extends {@link GenericServlet}. 397 * Needed for JSP access in FreeMarker. 398 */ 399 @SuppressWarnings("serial") 400 private static class GenericServletAdapter extends GenericServlet { 401 402 @Override 403 public void service(ServletRequest servletRequest, ServletResponse servletResponse) { 404 // no-op 405 } 406 } 407 408 409 /** 410 * Internal implementation of the {@link ServletConfig} interface, 411 * to be passed to the servlet adapter. 412 */ 413 private class DelegatingServletConfig implements ServletConfig { 414 415 @Override 416 @Nullable 417 public String getServletName() { 418 return FreeMarkerView.this.getBeanName(); 419 } 420 421 @Override 422 @Nullable 423 public ServletContext getServletContext() { 424 return FreeMarkerView.this.getServletContext(); 425 } 426 427 @Override 428 @Nullable 429 public String getInitParameter(String paramName) { 430 return null; 431 } 432 433 @Override 434 public Enumeration<String> getInitParameterNames() { 435 return Collections.enumeration(Collections.emptySet()); 436 } 437 } 438 439}