View Javadoc
1   /*
2    * Copyright 2002-2015 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.view.freemarker;
18  
19  import java.io.FileNotFoundException;
20  import java.io.IOException;
21  import java.util.Collections;
22  import java.util.Enumeration;
23  import java.util.HashSet;
24  import java.util.Locale;
25  import java.util.Map;
26  import javax.servlet.GenericServlet;
27  import javax.servlet.ServletConfig;
28  import javax.servlet.ServletContext;
29  import javax.servlet.ServletException;
30  import javax.servlet.ServletRequest;
31  import javax.servlet.ServletResponse;
32  import javax.servlet.http.HttpServletRequest;
33  import javax.servlet.http.HttpServletResponse;
34  import javax.servlet.http.HttpSession;
35  
36  import freemarker.core.ParseException;
37  import freemarker.ext.jsp.TaglibFactory;
38  import freemarker.ext.servlet.AllHttpScopesHashModel;
39  import freemarker.ext.servlet.FreemarkerServlet;
40  import freemarker.ext.servlet.HttpRequestHashModel;
41  import freemarker.ext.servlet.HttpRequestParametersHashModel;
42  import freemarker.ext.servlet.HttpSessionHashModel;
43  import freemarker.ext.servlet.ServletContextHashModel;
44  import freemarker.template.Configuration;
45  import freemarker.template.ObjectWrapper;
46  import freemarker.template.SimpleHash;
47  import freemarker.template.Template;
48  import freemarker.template.TemplateException;
49  
50  import org.springframework.beans.BeansException;
51  import org.springframework.beans.factory.BeanFactoryUtils;
52  import org.springframework.beans.factory.BeanInitializationException;
53  import org.springframework.beans.factory.NoSuchBeanDefinitionException;
54  import org.springframework.context.ApplicationContextException;
55  import org.springframework.web.servlet.support.RequestContextUtils;
56  import org.springframework.web.servlet.view.AbstractTemplateView;
57  
58  /**
59   * View using the FreeMarker template engine.
60   *
61   * <p>Exposes the following JavaBean properties:
62   * <ul>
63   * <li><b>url</b>: the location of the FreeMarker template to be wrapped,
64   * relative to the FreeMarker template context (directory).
65   * <li><b>encoding</b> (optional, default is determined by FreeMarker configuration):
66   * the encoding of the FreeMarker template file
67   * </ul>
68   *
69   * <p>Depends on a single {@link FreeMarkerConfig} object such as {@link FreeMarkerConfigurer}
70   * being accessible in the current web application context, with any bean name.
71   * Alternatively, you can set the FreeMarker {@link Configuration} object as bean property.
72   * See {@link #setConfiguration} for more details on the impacts of this approach.
73   *
74   * <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
75   *
76   * @author Darren Davison
77   * @author Juergen Hoeller
78   * @since 03.03.2004
79   * @see #setUrl
80   * @see #setExposeSpringMacroHelpers
81   * @see #setEncoding
82   * @see #setConfiguration
83   * @see FreeMarkerConfig
84   * @see FreeMarkerConfigurer
85   */
86  public class FreeMarkerView extends AbstractTemplateView {
87  
88  	private String encoding;
89  
90  	private Configuration configuration;
91  
92  	private TaglibFactory taglibFactory;
93  
94  	private ServletContextHashModel servletContextHashModel;
95  
96  
97  	/**
98  	 * Set the encoding of the FreeMarker template file. Default is determined
99  	 * 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 	@SuppressWarnings("deprecation")
190 	protected ObjectWrapper getObjectWrapper() {
191 		ObjectWrapper ow = getConfiguration().getObjectWrapper();
192 		return (ow != null ? ow : ObjectWrapper.DEFAULT_WRAPPER);
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 		try {
203 			// Check that we can get the template, even if we might subsequently get it again.
204 			getTemplate(getUrl(), locale);
205 			return true;
206 		}
207 		catch (FileNotFoundException ex) {
208 			if (logger.isDebugEnabled()) {
209 				logger.debug("No FreeMarker view found for URL: " + getUrl());
210 			}
211 			return false;
212 		}
213 		catch (ParseException ex) {
214 			throw new ApplicationContextException(
215 					"Failed to parse FreeMarker template for URL [" +  getUrl() + "]", ex);
216 		}
217 		catch (IOException ex) {
218 			throw new ApplicationContextException(
219 					"Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
220 		}
221 	}
222 
223 
224 	/**
225 	 * Process the model map by merging it with the FreeMarker template.
226 	 * Output is directed to the servlet response.
227 	 * <p>This method can be overridden if custom behavior is needed.
228 	 */
229 	@Override
230 	protected void renderMergedTemplateModel(
231 			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
232 
233 		exposeHelpers(model, request);
234 		doRender(model, request, response);
235 	}
236 
237 	/**
238 	 * Expose helpers unique to each rendering operation. This is necessary so that
239 	 * different rendering operations can't overwrite each other's formats etc.
240 	 * <p>Called by {@code renderMergedTemplateModel}. The default implementation
241 	 * is empty. This method can be overridden to add custom helpers to the model.
242 	 * @param model The model that will be passed to the template at merge time
243 	 * @param request current HTTP request
244 	 * @throws Exception if there's a fatal error while we're adding information to the context
245 	 * @see #renderMergedTemplateModel
246 	 */
247 	protected void exposeHelpers(Map<String, Object> model, HttpServletRequest request) throws Exception {
248 	}
249 
250 	/**
251 	 * Render the FreeMarker view to the given response, using the given model
252 	 * map which contains the complete template model to use.
253 	 * <p>The default implementation renders the template specified by the "url"
254 	 * bean property, retrieved via {@code getTemplate}. It delegates to the
255 	 * {@code processTemplate} method to merge the template instance with
256 	 * the given template model.
257 	 * <p>Adds the standard Freemarker hash models to the model: request parameters,
258 	 * request, session and application (ServletContext), as well as the JSP tag
259 	 * library hash model.
260 	 * <p>Can be overridden to customize the behavior, for example to render
261 	 * multiple templates into a single view.
262 	 * @param model the model to use for rendering
263 	 * @param request current HTTP request
264 	 * @param response current servlet response
265 	 * @throws IOException if the template file could not be retrieved
266 	 * @throws Exception if rendering failed
267 	 * @see #setUrl
268 	 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
269 	 * @see #getTemplate(java.util.Locale)
270 	 * @see #processTemplate
271 	 * @see freemarker.ext.servlet.FreemarkerServlet
272 	 */
273 	protected void doRender(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
274 		// Expose model to JSP tags (as request attributes).
275 		exposeModelAsRequestAttributes(model, request);
276 		// Expose all standard FreeMarker hash models.
277 		SimpleHash fmModel = buildTemplateModel(model, request, response);
278 
279 		if (logger.isDebugEnabled()) {
280 			logger.debug("Rendering FreeMarker template [" + getUrl() + "] in FreeMarkerView '" + getBeanName() + "'");
281 		}
282 		// Grab the locale-specific version of the template.
283 		Locale locale = RequestContextUtils.getLocale(request);
284 		processTemplate(getTemplate(locale), fmModel, response);
285 	}
286 
287 	/**
288 	 * Build a FreeMarker template model for the given model Map.
289 	 * <p>The default implementation builds a {@link AllHttpScopesHashModel}.
290 	 * @param model the model to use for rendering
291 	 * @param request current HTTP request
292 	 * @param response current servlet response
293 	 * @return the FreeMarker template model, as a {@link SimpleHash} or subclass thereof
294 	 */
295 	protected SimpleHash buildTemplateModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
296 		AllHttpScopesHashModel fmModel = new AllHttpScopesHashModel(getObjectWrapper(), getServletContext(), request);
297 		fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory);
298 		fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel);
299 		fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response));
300 		fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper()));
301 		fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request));
302 		fmModel.putAll(model);
303 		return fmModel;
304 	}
305 
306 	/**
307 	 * Build a FreeMarker {@link HttpSessionHashModel} for the given request,
308 	 * detecting whether a session already exists and reacting accordingly.
309 	 * @param request current HTTP request
310 	 * @param response current servlet response
311 	 * @return the FreeMarker HttpSessionHashModel
312 	 */
313 	private HttpSessionHashModel buildSessionModel(HttpServletRequest request, HttpServletResponse response) {
314 		HttpSession session = request.getSession(false);
315 		if (session != null) {
316 			return new HttpSessionHashModel(session, getObjectWrapper());
317 		}
318 		else {
319 			return new HttpSessionHashModel(null, request, response, getObjectWrapper());
320 		}
321 	}
322 
323 	/**
324 	 * Retrieve the FreeMarker template for the given locale,
325 	 * to be rendering by this view.
326 	 * <p>By default, the template specified by the "url" bean property
327 	 * will be retrieved.
328 	 * @param locale the current locale
329 	 * @return the FreeMarker template to render
330 	 * @throws IOException if the template file could not be retrieved
331 	 * @see #setUrl
332 	 * @see #getTemplate(String, java.util.Locale)
333 	 */
334 	protected Template getTemplate(Locale locale) throws IOException {
335 		return getTemplate(getUrl(), locale);
336 	}
337 
338 	/**
339 	 * Retrieve the FreeMarker template specified by the given name,
340 	 * using the encoding specified by the "encoding" bean property.
341 	 * <p>Can be called by subclasses to retrieve a specific template,
342 	 * for example to render multiple templates into a single view.
343 	 * @param name the file name of the desired template
344 	 * @param locale the current locale
345 	 * @return the FreeMarker template
346 	 * @throws IOException if the template file could not be retrieved
347 	 */
348 	protected Template getTemplate(String name, Locale locale) throws IOException {
349 		return (getEncoding() != null ?
350 				getConfiguration().getTemplate(name, locale, getEncoding()) :
351 				getConfiguration().getTemplate(name, locale));
352 	}
353 
354 	/**
355 	 * Process the FreeMarker template to the servlet response.
356 	 * <p>Can be overridden to customize the behavior.
357 	 * @param template the template to process
358 	 * @param model the model for the template
359 	 * @param response servlet response (use this to get the OutputStream or Writer)
360 	 * @throws IOException if the template file could not be retrieved
361 	 * @throws TemplateException if thrown by FreeMarker
362 	 * @see freemarker.template.Template#process(Object, java.io.Writer)
363 	 */
364 	protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response)
365 			throws IOException, TemplateException {
366 
367 		template.process(model, response.getWriter());
368 	}
369 
370 
371 	/**
372 	 * Simple adapter class that extends {@link GenericServlet}.
373 	 * Needed for JSP access in FreeMarker.
374 	 */
375 	@SuppressWarnings("serial")
376 	private static class GenericServletAdapter extends GenericServlet {
377 
378 		@Override
379 		public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
380 			// no-op
381 		}
382 	}
383 
384 
385 	/**
386 	 * Internal implementation of the {@link ServletConfig} interface,
387 	 * to be passed to the servlet adapter.
388 	 */
389 	private class DelegatingServletConfig implements ServletConfig {
390 
391 		@Override
392 		public String getServletName() {
393 			return FreeMarkerView.this.getBeanName();
394 		}
395 
396 		@Override
397 		public ServletContext getServletContext() {
398 			return FreeMarkerView.this.getServletContext();
399 		}
400 
401 		@Override
402 		public String getInitParameter(String paramName) {
403 			return null;
404 		}
405 
406 		@Override
407 		public Enumeration<String> getInitParameterNames() {
408 			return Collections.enumeration(new HashSet<String>());
409 		}
410 	}
411 
412 }