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