View Javadoc

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