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}