View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.freemarker;
35  
36  import info.magnolia.cms.beans.config.ServerConfiguration;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.context.WebContext;
39  import info.magnolia.i18nsystem.TranslationService;
40  import info.magnolia.objectfactory.Components;
41  
42  import java.io.IOException;
43  import java.io.Reader;
44  import java.io.Writer;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Set;
48  
49  import javax.inject.Inject;
50  import javax.inject.Singleton;
51  import javax.servlet.GenericServlet;
52  import javax.servlet.ServletContext;
53  import javax.servlet.ServletException;
54  
55  import freemarker.cache.TemplateLoader;
56  import freemarker.ext.jsp.TaglibFactory;
57  import freemarker.ext.servlet.FreemarkerServlet;
58  import freemarker.ext.servlet.HttpRequestHashModel;
59  import freemarker.ext.servlet.ServletContextHashModel;
60  import freemarker.template.Configuration;
61  import freemarker.template.ObjectWrapper;
62  import freemarker.template.Template;
63  import freemarker.template.TemplateException;
64  import freemarker.template.TemplateModel;
65  import freemarker.template.TemplateModelException;
66  
67  /**
68   * A generic helper to render Content instances with Freemarker templates.
69   *
70   * TODO : expose Configuration#clearTemplateCache()
71   */
72  @Singleton
73  public class FreemarkerHelper {
74  
75      /**
76       * @deprecated since 4.5, use IoC !
77       */
78      @Deprecated
79      public static FreemarkerHelper getInstance() {
80          return Components.getSingleton(FreemarkerHelper.class);
81      }
82  
83      private final Configuration cfg;
84      private final TranslationService translationService;
85  
86      // taglib support stuff
87      private TaglibFactory taglibFactory;
88      private ServletContextHashModel servletContextHashModel;
89  
90      @Inject
91      public FreemarkerHelper(final FreemarkerConfig freemarkerConfig, TranslationService transactionService) {
92          this.translationService = transactionService;
93          // we subclass freemarker.Configuration to override some methods which must delegate to our observed FreemarkerConfig
94          // TODO use new constructor
95          this.cfg = new Configuration() {
96              @Override
97              public Set getSharedVariableNames() {
98                  final Set names = super.getSharedVariableNames();
99                  names.addAll(freemarkerConfig.getSharedVariables().keySet());
100                 return names;
101             }
102 
103             @Override
104             public TemplateModel getSharedVariable(String name) {
105                 final TemplateModel value = super.getSharedVariable(name);
106                 if (value == null) {
107                     return freemarkerConfig.getSharedVariables().get(name);
108                 }
109                 return value;
110             }
111         };
112         cfg.setTemplateExceptionHandler(freemarkerConfig.getTemplateExceptionHandler());
113 
114         // ... and here we essentially do the same by instantiate delegator implementations of FreeMarker components, which delegate to our observed FreemarkerConfig
115         // these setters do more than their equivalent getters, so we can't just override the getter instead.
116         // ultimately, we could probably have our own clean subclass of freemarker.Configuration to hide all these details off FreemarkerHelper
117         cfg.setTemplateLoader(new ConfigDelegatingTemplateLoader(freemarkerConfig));
118         cfg.setObjectWrapper(new ConfigDelegatingObjectWrapper(freemarkerConfig));
119 
120         cfg.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX);
121         cfg.setDefaultEncoding("UTF-8");
122         cfg.setURLEscapingCharset("UTF-8");
123         //cfg.setTemplateUpdateDelay(10);
124     }
125 
126     /**
127      * @deprecated since 5.4.4. Use {@link #FreemarkerHelper(FreemarkerConfig, info.magnolia.i18nsystem.TranslationService)} instead.
128      */
129     @Deprecated
130     public FreemarkerHelper(final FreemarkerConfig freemarkerConfig) {
131         this(freemarkerConfig, Components.getComponent(TranslationService.class));
132     }
133 
134     /**
135      * @see #render(String, Locale, String, Object, java.io.Writer)
136      */
137     public void render(String templatePath, Object root, Writer out) throws TemplateException, IOException {
138         render(templatePath, null, null, root, out);
139     }
140 
141     /**
142      * Renders the given template, using the given root object (can be a map, or any other type of object
143      * handled by MagnoliaContentWrapper) to the given Writer.
144      * If the root is an instance of a Map, the following elements are added to it:
145      * - ctx, the current Context instance retrieved from MgnlContext
146      * - contextPath, if we have an available WebContext (@deprecated)
147      * - defaultBaseUrl, as per Server.getDefaultBaseUrl()
148      *
149      * @see ServerConfiguration#getDefaultBaseUrl()
150      */
151     public void render(String templatePath, Locale locale, String i18nBasename, Object root, Writer out) throws TemplateException, IOException {
152         final Locale localeToUse = checkLocale(locale);
153         prepareRendering(localeToUse, i18nBasename, root);
154 
155         final Template template = cfg.getTemplate(templatePath, localeToUse);
156         template.process(root, out);
157     }
158 
159     /**
160      * Renders the template read by the given Reader instance. It should be noted that this method completely bypasses
161      * Freemarker's caching mechanism. The template will be parsed everytime, which might have a performance impact.
162      *
163      * @see #render(Reader, Locale, String, Object, Writer)
164      */
165     public void render(Reader template, Object root, Writer out) throws TemplateException, IOException {
166         render(template, null, null, root, out);
167     }
168 
169     protected void render(Reader template, Locale locale, String i18nBasename, Object root, Writer out) throws TemplateException, IOException {
170         final Locale localeToUse = checkLocale(locale);
171         prepareRendering(localeToUse, i18nBasename, root);
172 
173         final Template t = new Template("inlinetemplate", template, cfg);
174         t.setLocale(localeToUse);
175         t.process(root, out);
176     }
177 
178     /**
179      * Returns the passed Locale if non-null, otherwise attempts to get the Locale from the current context.
180      */
181     protected Locale checkLocale(Locale locale) {
182         if (locale != null) {
183             return locale;
184         } else if (MgnlContext.hasInstance()) {
185             return MgnlContext.getLocale();
186         } else {
187             return Locale.getDefault();
188         }
189     }
190 
191     /**
192      * Call checkLocale() before calling this method, to ensure it is not null.
193      */
194     protected void prepareRendering(Locale checkedLocale, String i18nBasename, Object root) {
195         if (root instanceof Map) {
196             final Map<String, Object> data = (Map<String, Object>) root;
197             addDefaultData(data, checkedLocale, i18nBasename);
198         }
199     }
200 
201     protected void addDefaultData(Map<String, Object> data, Locale locale, String i18nBasename) {
202         if (MgnlContext.hasInstance()) {
203             data.put("ctx", MgnlContext.getInstance());
204         }
205         if (MgnlContext.isWebContext()) {
206             final WebContext webCtx = MgnlContext.getWebContext();
207             // @deprecated (-> update all templates) - TODO see MAGNOLIA-1789
208             data.put("contextPath", webCtx.getContextPath());
209             data.put("aggregationState", webCtx.getAggregationState());
210 
211             addTaglibSupportData(data, webCtx);
212         }
213 
214         data.put("defaultBaseUrl", ServerConfiguration.getInstance().getDefaultBaseUrl());
215         //we still use the deprecated constructor of MessagesWrapper till we drop support of deprecated i18nBasename
216         data.put("i18n", new MessagesWrapper(i18nBasename, locale, translationService));
217         data.put("i18nAuthoring", new MessagesWrapper(i18nBasename, MgnlContext.getLocale(), translationService));
218 
219         // TODO : this is currently still in FreemarkerUtil. If we add it here,
220         // the attribute "message" we put in the Freemarker context should have a less generic name
221         // (-> update all templates)
222 //            if (AlertUtil.isMessageSet(mgnlCtx)) {
223 //                data.put("message", AlertUtil.getMessage(mgnlCtx));
224 //            }
225     }
226 
227     protected void addTaglibSupportData(Map<String, Object> data, WebContext webCtx) {
228         final ServletContext servletContext = webCtx.getServletContext();
229         try {
230             data.put(FreemarkerServlet.KEY_JSP_TAGLIBS, checkTaglibFactory(servletContext));
231             data.put(FreemarkerServlet.KEY_APPLICATION_PRIVATE, checkServletContextModel(servletContext));
232             data.put(FreemarkerServlet.KEY_REQUEST_PRIVATE, new HttpRequestHashModel(webCtx.getRequest(), webCtx.getResponse(), cfg.getObjectWrapper()));
233         } catch (ServletException e) {
234             // this should be an IllegalStateException (i.e there's no reason we should end up here) but this constructor isn't available in 1.4
235             throw new RuntimeException("Can't initialize taglib support for FreeMarker: ", e);
236         }
237     }
238 
239     protected TaglibFactory checkTaglibFactory(ServletContext servletContext) {
240         if (taglibFactory == null) {
241             taglibFactory = new TaglibFactory(new FreemarkerServletContextWrapper(servletContext));
242         }
243         return taglibFactory;
244     }
245 
246     protected ServletContextHashModel checkServletContextModel(ServletContext servletContext) throws ServletException {
247         if (servletContextHashModel == null) {
248             // FreeMarker needs an instance of a GenericServlet, but it doesn't have to do anything other than provide references to the ServletContext
249             final GenericServlet fs = new DoNothingServlet(servletContext);
250             servletContextHashModel = new ServletContextHashModel(fs, cfg.getObjectWrapper());
251         }
252         return servletContextHashModel;
253     }
254 
255     protected Configuration getConfiguration() {
256         return cfg;
257     }
258 
259     private static class ConfigDelegatingTemplateLoader implements TemplateLoader {
260         private final FreemarkerConfig freemarkerConfig;
261 
262         public ConfigDelegatingTemplateLoader(FreemarkerConfig freemarkerConfig) {
263             this.freemarkerConfig = freemarkerConfig;
264         }
265 
266         @Override
267         public Object findTemplateSource(String name) throws IOException {
268             return freemarkerConfig.getTemplateLoader().findTemplateSource(name);
269         }
270 
271         @Override
272         public long getLastModified(Object templateSource) {
273             return freemarkerConfig.getTemplateLoader().getLastModified(templateSource);
274         }
275 
276         @Override
277         public Reader getReader(Object templateSource, String encoding) throws IOException {
278             return freemarkerConfig.getTemplateLoader().getReader(templateSource, encoding);
279         }
280 
281         @Override
282         public void closeTemplateSource(Object templateSource) throws IOException {
283             freemarkerConfig.getTemplateLoader().closeTemplateSource(templateSource);
284         }
285     }
286 
287     private static class ConfigDelegatingObjectWrapper implements ObjectWrapper {
288         private final FreemarkerConfig freemarkerConfig;
289 
290         public ConfigDelegatingObjectWrapper(FreemarkerConfig freemarkerConfig) {
291             this.freemarkerConfig = freemarkerConfig;
292         }
293 
294         @Override
295         public TemplateModel wrap(Object obj) throws TemplateModelException {
296             return freemarkerConfig.getObjectWrapper().wrap(obj);
297         }
298     }
299 }