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