View Javadoc

1   /*
2    * $Id: FreemarkerManager.java 719672 2008-11-21 18:57:55Z musachy $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.views.freemarker;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.Properties;
31  import java.util.Set;
32  
33  import javax.servlet.GenericServlet;
34  import javax.servlet.ServletContext;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  import javax.servlet.http.HttpSession;
38  
39  import org.apache.struts2.StrutsConstants;
40  import org.apache.struts2.views.JspSupportServlet;
41  import org.apache.struts2.views.TagLibrary;
42  import org.apache.struts2.views.util.ContextUtil;
43  
44  import com.opensymphony.xwork2.inject.Container;
45  import com.opensymphony.xwork2.inject.Inject;
46  import com.opensymphony.xwork2.util.FileManager;
47  import com.opensymphony.xwork2.util.ValueStack;
48  import com.opensymphony.xwork2.util.logging.Logger;
49  import com.opensymphony.xwork2.util.logging.LoggerFactory;
50  
51  import freemarker.cache.FileTemplateLoader;
52  import freemarker.cache.MultiTemplateLoader;
53  import freemarker.cache.TemplateLoader;
54  import freemarker.cache.WebappTemplateLoader;
55  import freemarker.ext.beans.BeansWrapper;
56  import freemarker.ext.jsp.TaglibFactory;
57  import freemarker.ext.servlet.HttpRequestHashModel;
58  import freemarker.ext.servlet.HttpRequestParametersHashModel;
59  import freemarker.ext.servlet.HttpSessionHashModel;
60  import freemarker.ext.servlet.ServletContextHashModel;
61  import freemarker.template.ObjectWrapper;
62  import freemarker.template.SimpleHash;
63  import freemarker.template.TemplateException;
64  import freemarker.template.TemplateExceptionHandler;
65  import freemarker.template.TemplateModel;
66  
67  
68  /***
69   * Static Configuration Manager for the FreemarkerResult's configuration
70   *
71   * <p/>
72   *
73   * Possible extension points are :-
74   * <ul>
75   *   <li>createConfiguration method</li>
76   *   <li>loadSettings method</li>
77   *   <li>getTemplateLoader method</li>
78   *   <li>populateContext method</li>
79   * </ul>
80   *
81   * <p/>
82   * <b> createConfiguration method </b><br/>
83   * Create a freemarker Configuration.
84   * <p/>
85   *
86   * <b> loadSettings method </b><br/>
87   * Load freemarker settings, default to freemarker.properties (if found in classpath)
88   * <p/>
89   *
90   * <b> getTemplateLoader method</b><br/>
91   * create a freemarker TemplateLoader that loads freemarker template in the following order :-
92   * <ol>
93   *   <li>path defined in ServletContext init parameter named 'templatePath' or 'TemplatePath' (must be an absolute path)</li>
94   *   <li>webapp classpath</li>
95   *   <li>struts's static folder (under [STRUT2_SOURCE]/org/apache/struts2/static/</li>
96   * </ol>
97   * <p/>
98   *
99   * <b> populateContext method</b><br/>
100  * populate the created model.
101  *
102  */
103 public class FreemarkerManager {
104 
105     private static final Logger LOG = LoggerFactory.getLogger(FreemarkerManager.class);
106     public static final String CONFIG_SERVLET_CONTEXT_KEY = "freemarker.Configuration";
107     public static final String KEY_EXCEPTION = "exception";
108 
109     // coppied from freemarker servlet - since they are private
110     protected static final String ATTR_APPLICATION_MODEL = ".freemarker.Application";
111     protected static final String ATTR_JSP_TAGLIBS_MODEL = ".freemarker.JspTaglibs";
112     protected static final String ATTR_REQUEST_MODEL = ".freemarker.Request";
113     protected static final String ATTR_REQUEST_PARAMETERS_MODEL = ".freemarker.RequestParameters";
114 
115     // coppied from freemarker servlet - so that there is no dependency on it
116     public static final String KEY_APPLICATION = "Application";
117     public static final String KEY_REQUEST_MODEL = "Request";
118     public static final String KEY_SESSION_MODEL = "Session";
119     public static final String KEY_JSP_TAGLIBS = "JspTaglibs";
120     public static final String KEY_REQUEST_PARAMETER_MODEL = "Parameters";
121     
122     protected String encoding;
123     protected boolean altMapWrapper;
124     protected boolean cacheBeanWrapper;
125     protected int mruMaxStrongSize;
126     protected Map<String,TagLibrary> tagLibraries;
127     
128     @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
129     public void setEncoding(String encoding) {
130         this.encoding = encoding;
131     }
132     
133     @Inject(StrutsConstants.STRUTS_FREEMARKER_WRAPPER_ALT_MAP)
134     public void setWrapperAltMap(String val) {
135         altMapWrapper = "true".equals(val);
136     }
137     
138     @Inject(StrutsConstants.STRUTS_FREEMARKER_BEANWRAPPER_CACHE)
139     public void setCacheBeanWrapper(String val) {
140         cacheBeanWrapper = "true".equals(val);
141     }
142     
143     @Inject(StrutsConstants.STRUTS_FREEMARKER_MRU_MAX_STRONG_SIZE)
144     public void setMruMaxStrongSize(String size) {
145         mruMaxStrongSize = Integer.parseInt(size);
146     }
147     
148     @Inject
149     public void setContainer(Container container) {
150         Map<String,TagLibrary> map = new HashMap<String,TagLibrary>();
151         Set<String> prefixes = container.getInstanceNames(TagLibrary.class);
152         for (String prefix : prefixes) {
153             map.put(prefix, container.getInstance(TagLibrary.class, prefix));
154         }
155         this.tagLibraries = Collections.unmodifiableMap(map);
156     }
157 
158     public synchronized freemarker.template.Configuration getConfiguration(ServletContext servletContext) throws TemplateException {
159         freemarker.template.Configuration config = (freemarker.template.Configuration) servletContext.getAttribute(CONFIG_SERVLET_CONTEXT_KEY);
160 
161         if (config == null) {
162             config = createConfiguration(servletContext);
163 
164             // store this configuration in the servlet context
165             servletContext.setAttribute(CONFIG_SERVLET_CONTEXT_KEY, config);
166         }
167         
168         config.setWhitespaceStripping(true);
169 
170         return config;
171     }
172 
173     protected ScopesHashModel buildScopesHashModel(ServletContext servletContext, HttpServletRequest request, HttpServletResponse response, ObjectWrapper wrapper, ValueStack stack) {
174         ScopesHashModel model = new ScopesHashModel(wrapper, servletContext, request, stack);
175 
176         // Create hash model wrapper for servlet context (the application)
177         // only need one thread to do this once, per servlet context
178         synchronized (servletContext) {
179             ServletContextHashModel servletContextModel = (ServletContextHashModel) servletContext.getAttribute(ATTR_APPLICATION_MODEL);
180 
181             if (servletContextModel == null) {
182 
183                 GenericServlet servlet = JspSupportServlet.jspSupportServlet;
184                 // TODO if the jsp support  servlet isn't load-on-startup then it won't exist
185                 // if it hasn't been accessed, and a JSP page is accessed
186                 if (servlet != null) {
187                     servletContextModel = new ServletContextHashModel(servlet, wrapper);
188                     servletContext.setAttribute(ATTR_APPLICATION_MODEL, servletContextModel);
189                     TaglibFactory taglibs = new TaglibFactory(servletContext);
190                     servletContext.setAttribute(ATTR_JSP_TAGLIBS_MODEL, taglibs);
191                 }
192 
193             }
194 
195             model.put(KEY_APPLICATION, servletContextModel);
196             model.put(KEY_JSP_TAGLIBS, (TemplateModel) servletContext.getAttribute(ATTR_JSP_TAGLIBS_MODEL));
197         }
198 
199         // Create hash model wrapper for session
200         HttpSession session = request.getSession(false);
201         if (session != null) {
202             model.put(KEY_SESSION_MODEL, new HttpSessionHashModel(session, wrapper));
203         } else {
204             // no session means no attributes ???
205             //            model.put(KEY_SESSION_MODEL, new SimpleHash());
206         }
207 
208         // Create hash model wrapper for the request attributes
209         HttpRequestHashModel requestModel = (HttpRequestHashModel) request.getAttribute(ATTR_REQUEST_MODEL);
210 
211         if ((requestModel == null) || (requestModel.getRequest() != request)) {
212             requestModel = new HttpRequestHashModel(request, response, wrapper);
213             request.setAttribute(ATTR_REQUEST_MODEL, requestModel);
214         }
215 
216         model.put(KEY_REQUEST_MODEL, requestModel);
217 
218 
219         // Create hash model wrapper for request parameters
220         HttpRequestParametersHashModel reqParametersModel = (HttpRequestParametersHashModel) request.getAttribute(ATTR_REQUEST_PARAMETERS_MODEL);
221         if (reqParametersModel == null || requestModel.getRequest() != request) {
222             reqParametersModel = new HttpRequestParametersHashModel(request);
223             request.setAttribute(ATTR_REQUEST_PARAMETERS_MODEL, reqParametersModel);
224         }
225         model.put(KEY_REQUEST_PARAMETER_MODEL, reqParametersModel);
226 
227         return model;
228     }
229 
230     protected void populateContext(ScopesHashModel model, ValueStack stack, Object action, HttpServletRequest request, HttpServletResponse response) {
231         // put the same objects into the context that the velocity result uses
232         Map standard = ContextUtil.getStandardContext(stack, request, response);
233         model.putAll(standard);
234 
235         // support for JSP exception pages, exposing the servlet or JSP exception
236         Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
237 
238         if (exception == null) {
239             exception = (Throwable) request.getAttribute("javax.servlet.error.JspException");
240         }
241 
242         if (exception != null) {
243             model.put(KEY_EXCEPTION, exception);
244         }
245     }
246 
247     protected BeansWrapper getObjectWrapper() {
248         StrutsBeanWrapper wrapper = new StrutsBeanWrapper(altMapWrapper);
249         wrapper.setUseCache(cacheBeanWrapper);
250         return wrapper;
251     }
252 
253     /***
254      * The default template loader is a MultiTemplateLoader which includes
255      * a ClassTemplateLoader and a WebappTemplateLoader (and a FileTemplateLoader depending on
256      * the init-parameter 'TemplatePath').
257      * <p/>
258      * The ClassTemplateLoader will resolve fully qualified template includes
259      * that begin with a slash. for example /com/company/template/common.ftl
260      * <p/>
261      * The WebappTemplateLoader attempts to resolve templates relative to the web root folder
262      */
263     protected TemplateLoader getTemplateLoader(ServletContext servletContext) {
264         // construct a FileTemplateLoader for the init-param 'TemplatePath'
265         FileTemplateLoader templatePathLoader = null;
266 
267         String templatePath = servletContext.getInitParameter("TemplatePath");
268         if (templatePath == null) {
269             templatePath = servletContext.getInitParameter("templatePath");
270         }
271 
272         if (templatePath != null) {
273             try {
274                 templatePathLoader = new FileTemplateLoader(new File(templatePath));
275             } catch (IOException e) {
276                 LOG.error("Invalid template path specified: " + e.getMessage(), e);
277             }
278         }
279 
280         // presume that most apps will require the class and webapp template loader
281         // if people wish to
282         return templatePathLoader != null ?
283                 new MultiTemplateLoader(new TemplateLoader[]{
284                         templatePathLoader,
285                         new WebappTemplateLoader(servletContext),
286                         new StrutsClassTemplateLoader()
287                 })
288                 : new MultiTemplateLoader(new TemplateLoader[]{
289                 new WebappTemplateLoader(servletContext),
290                 new StrutsClassTemplateLoader()
291         });
292     }
293 
294     /***
295      * Create the instance of the freemarker Configuration object.
296      * <p/>
297      * this implementation
298      * <ul>
299      * <li>obtains the default configuration from Configuration.getDefaultConfiguration()
300      * <li>sets up template loading from a ClassTemplateLoader and a WebappTemplateLoader
301      * <li>sets up the object wrapper to be the BeansWrapper
302      * <li>loads settings from the classpath file /freemarker.properties
303      * </ul>
304      *
305      * @param servletContext
306      */
307     protected freemarker.template.Configuration createConfiguration(ServletContext servletContext) throws TemplateException {
308         freemarker.template.Configuration configuration = new freemarker.template.Configuration();
309 
310         configuration.setTemplateLoader(getTemplateLoader(servletContext));
311 
312         configuration.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
313 
314         configuration.setObjectWrapper(getObjectWrapper());
315         
316         if( mruMaxStrongSize > 0 ) {
317             configuration.setSetting(freemarker.template.Configuration.CACHE_STORAGE_KEY, "strong:" + mruMaxStrongSize);
318         }
319 
320         if (encoding != null) {
321             configuration.setDefaultEncoding(encoding);
322         }
323 
324         loadSettings(servletContext, configuration);
325 
326         return configuration;
327     }
328 
329     /***
330      * Load the settings from the /freemarker.properties file on the classpath
331      *
332      * @see freemarker.template.Configuration#setSettings for the definition of valid settings
333      */
334     protected void loadSettings(ServletContext servletContext, freemarker.template.Configuration configuration) {
335         InputStream in = null;
336 
337         try {
338             in = FileManager.loadFile("freemarker.properties", FreemarkerManager.class);
339 
340             if (in != null) {
341                 Properties p = new Properties();
342                 p.load(in);
343                 configuration.setSettings(p);
344             }
345         } catch (IOException e) {
346             LOG.error("Error while loading freemarker settings from /freemarker.properties", e);
347         } catch (TemplateException e) {
348             LOG.error("Error while loading freemarker settings from /freemarker.properties", e);
349         } finally {
350             if (in != null) {
351                 try {
352                     in.close();
353                 } catch(IOException io) {
354                     LOG.warn("Unable to close input stream", io);
355                 }
356             }
357         }
358     }
359 
360     public SimpleHash buildTemplateModel(ValueStack stack, Object action, ServletContext servletContext, HttpServletRequest request, HttpServletResponse response, ObjectWrapper wrapper) {
361         ScopesHashModel model = buildScopesHashModel(servletContext, request, response, wrapper, stack);
362         populateContext(model, stack, action, request, response);
363         if (tagLibraries != null) {
364             for (String prefix : tagLibraries.keySet()) {
365                 model.put(prefix, tagLibraries.get(prefix).getFreemarkerModels(stack, request, response));
366             }
367         }
368         return model;
369     }
370 }