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}