View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 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.cms.core.HTMLEscapingAggregationState;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.context.WebContext;
40  import info.magnolia.context.HTMLEscapingWebContextWrapper;
41  import info.magnolia.i18nsystem.TranslationService;
42  import info.magnolia.init.MagnoliaConfigurationProperties;
43  import info.magnolia.objectfactory.Components;
44  
45  import java.io.IOException;
46  import java.io.Reader;
47  import java.io.Writer;
48  import java.util.Locale;
49  import java.util.Map;
50  import java.util.Set;
51  
52  import javax.inject.Inject;
53  import javax.inject.Singleton;
54  import javax.servlet.GenericServlet;
55  import javax.servlet.ServletContext;
56  import javax.servlet.ServletException;
57  
58  import freemarker.cache.TemplateLoader;
59  import freemarker.ext.jsp.TaglibFactory;
60  import freemarker.ext.servlet.FreemarkerServlet;
61  import freemarker.ext.servlet.HttpRequestHashModel;
62  import freemarker.ext.servlet.ServletContextHashModel;
63  import freemarker.template.Configuration;
64  import freemarker.template.ObjectWrapper;
65  import freemarker.template.Template;
66  import freemarker.template.TemplateException;
67  import freemarker.template.TemplateModel;
68  import freemarker.template.TemplateModelException;
69  
70  /**
71   * A generic helper to render Content instances with Freemarker templates.
72   *
73   * TODO : expose Configuration#clearTemplateCache()
74   */
75  @Singleton
76  public class FreemarkerHelper {
77  
78      /**
79       * @deprecated since 4.5, use IoC !
80       */
81      @Deprecated
82      public static FreemarkerHelper getInstance() {
83          return Components.getSingleton(FreemarkerHelper.class);
84      }
85  
86      private final Configuration cfg;
87      private final TranslationService translationService;
88  
89      // taglib support stuff
90      private TaglibFactory taglibFactory;
91      private ServletContextHashModel servletContextHashModel;
92  
93      private boolean escapeHtml = false;
94  
95      @Inject
96      public FreemarkerHelper(final FreemarkerConfig freemarkerConfig, TranslationService translationService, MagnoliaConfigurationProperties properties) {
97          boolean devMode = properties.getBooleanProperty("magnolia.develop");
98          this.translationService = translationService;
99          // we subclass freemarker.Configuration to override some methods which must delegate to our observed FreemarkerConfig
100         // TODO use new constructor
101         this.cfg = new Configuration() {
102             @Override
103             public Set getSharedVariableNames() {
104                 final Set names = super.getSharedVariableNames();
105                 names.addAll(freemarkerConfig.getSharedVariables().keySet());
106                 return names;
107             }
108 
109             @Override
110             public TemplateModel getSharedVariable(String name) {
111                 final TemplateModel value = super.getSharedVariable(name);
112                 if (value == null) {
113                     return freemarkerConfig.getSharedVariables().get(name);
114                 }
115                 return value;
116             }
117         };
118         cfg.setTemplateExceptionHandler(freemarkerConfig.getTemplateExceptionHandler());
119 
120         // ... and here we essentially do the same by instantiate delegator implementations of FreeMarker components, which delegate to our observed FreemarkerConfig
121         // these setters do more than their equivalent getters, so we can't just override the getter instead.
122         // ultimately, we could probably have our own clean subclass of freemarker.Configuration to hide all these details off FreemarkerHelper
123         cfg.setTemplateLoader(new ConfigDelegatingTemplateLoader(freemarkerConfig));
124         cfg.setObjectWrapper(new ConfigDelegatingObjectWrapper(freemarkerConfig));
125 
126         cfg.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX);
127         cfg.setDefaultEncoding("UTF-8");
128         cfg.setURLEscapingCharset("UTF-8");
129         if (devMode) {
130             cfg.setTemplateUpdateDelay(0);
131         }
132     }
133 
134     /**
135      * @deprecated since 5.4.8. Please use {@link #FreemarkerHelper(FreemarkerConfig, TranslationService, MagnoliaConfigurationProperties)} instead.
136      */
137     @Deprecated
138     public FreemarkerHelper(final FreemarkerConfig freemarkerConfig, TranslationService transactionService) {
139         this(freemarkerConfig, transactionService, Components.getComponent(MagnoliaConfigurationProperties.class));
140     }
141 
142     /**
143      * @deprecated since 5.4.4. Use {@link #FreemarkerHelper(FreemarkerConfig, TranslationService, MagnoliaConfigurationProperties)} instead.
144      */
145     @Deprecated
146     public FreemarkerHelper(final FreemarkerConfig freemarkerConfig) {
147         this(freemarkerConfig, Components.getComponent(TranslationService.class), Components.getComponent(MagnoliaConfigurationProperties.class));
148     }
149 
150     /**
151      * @see #render(String, Locale, String, Object, java.io.Writer)
152      */
153     public void render(String templatePath, Object root, Writer out) throws TemplateException, IOException {
154         render(templatePath, null, null, root, out);
155     }
156 
157     /**
158      * Renders the given template, using the given root object (can be a map, or any other type of object
159      * handled by MagnoliaContentWrapper) to the given Writer.
160      * If the root is an instance of a Map, the following elements are added to it:
161      * - ctx, the current Context instance retrieved from MgnlContext
162      * - contextPath, if we have an available WebContext (@deprecated)
163      * - defaultBaseUrl, as per Server.getDefaultBaseUrl()
164      *
165      * @see ServerConfiguration#getDefaultBaseUrl()
166      */
167     public void render(String templatePath, Locale locale, String i18nBasename, Object root, Writer out) throws TemplateException, IOException {
168         final Locale localeToUse = checkLocale(locale);
169         prepareRendering(localeToUse, i18nBasename, root);
170 
171         final Template template = cfg.getTemplate(templatePath, localeToUse);
172         template.process(root, out);
173     }
174 
175     /**
176      * Renders the template read by the given Reader instance. It should be noted that this method completely bypasses
177      * Freemarker's caching mechanism. The template will be parsed everytime, which might have a performance impact.
178      *
179      * @see #render(Reader, Locale, String, Object, Writer)
180      */
181     public void render(Reader template, Object root, Writer out) throws TemplateException, IOException {
182         render(template, null, null, root, out);
183     }
184 
185     protected void render(Reader template, Locale locale, String i18nBasename, Object root, Writer out) throws TemplateException, IOException {
186         final Locale localeToUse = checkLocale(locale);
187         prepareRendering(localeToUse, i18nBasename, root);
188 
189         final Template t = new Template("inlinetemplate", template, cfg);
190         t.setLocale(localeToUse);
191         t.process(root, out);
192     }
193 
194     /**
195      * Returns the passed Locale if non-null, otherwise attempts to get the Locale from the current context.
196      */
197     protected Locale checkLocale(Locale locale) {
198         if (locale != null) {
199             return locale;
200         } else if (MgnlContext.hasInstance()) {
201             return MgnlContext.getLocale();
202         } else {
203             return Locale.getDefault();
204         }
205     }
206 
207     /**
208      * Call checkLocale() before calling this method, to ensure it is not null.
209      */
210     protected void prepareRendering(Locale checkedLocale, String i18nBasename, Object root) {
211         if (root instanceof Map) {
212             final Map<String, Object> data = (Map<String, Object>) root;
213             addDefaultData(data, checkedLocale, i18nBasename);
214         }
215     }
216 
217     protected void addDefaultData(Map<String, Object> data, Locale locale, String i18nBasename) {
218         if (MgnlContext.hasInstance()) {
219             data.put("ctx", MgnlContext.getInstance());
220         }
221         if (MgnlContext.isWebContext()) {
222             final WebContext webCtx = MgnlContext.getWebContext();
223 
224             if (isEscapeHtml()) {
225                 data.put("ctx", new HTMLEscapingWebContextWrapper(webCtx));
226             }
227             // @deprecated (-> update all templates) - TODO see MAGNOLIA-1789
228             data.put("contextPath", webCtx.getContextPath());
229             data.put("aggregationState", isEscapeHtml() ?
230                     Components.getComponent(HTMLEscapingAggregationState.class) : webCtx.getAggregationState());
231 
232             addTaglibSupportData(data, webCtx);
233         }
234 
235         data.put("defaultBaseUrl", ServerConfiguration.getInstance().getDefaultBaseUrl());
236         //we still use the deprecated constructor of MessagesWrapper till we drop support of deprecated i18nBasename
237         data.put("i18n", new MessagesWrapper(i18nBasename, locale, translationService));
238         data.put("i18nAuthoring", new MessagesWrapper(i18nBasename, MgnlContext.getLocale(), translationService));
239 
240         // TODO : this is currently still in FreemarkerUtil. If we add it here,
241         // the attribute "message" we put in the Freemarker context should have a less generic name
242         // (-> update all templates)
243 //            if (AlertUtil.isMessageSet(mgnlCtx)) {
244 //                data.put("message", AlertUtil.getMessage(mgnlCtx));
245 //            }
246     }
247 
248     protected void addTaglibSupportData(Map<String, Object> data, WebContext webCtx) {
249         final ServletContext servletContext = webCtx.getServletContext();
250         try {
251             data.put(FreemarkerServlet.KEY_JSP_TAGLIBS, checkTaglibFactory(servletContext));
252             data.put(FreemarkerServlet.KEY_APPLICATION_PRIVATE, checkServletContextModel(servletContext));
253             data.put(FreemarkerServlet.KEY_REQUEST_PRIVATE, new HttpRequestHashModel(webCtx.getRequest(), webCtx.getResponse(), cfg.getObjectWrapper()));
254         } catch (ServletException e) {
255             // 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
256             throw new RuntimeException("Can't initialize taglib support for FreeMarker: ", e);
257         }
258     }
259 
260     protected TaglibFactory checkTaglibFactory(ServletContext servletContext) {
261         if (taglibFactory == null) {
262             taglibFactory = new TaglibFactory(new FreemarkerServletContextWrapper(servletContext));
263         }
264         return taglibFactory;
265     }
266 
267     protected ServletContextHashModel checkServletContextModel(ServletContext servletContext) throws ServletException {
268         if (servletContextHashModel == null) {
269             // FreeMarker needs an instance of a GenericServlet, but it doesn't have to do anything other than provide references to the ServletContext
270             final GenericServlet fs = new DoNothingServlet(servletContext);
271             servletContextHashModel = new ServletContextHashModel(fs, cfg.getObjectWrapper());
272         }
273         return servletContextHashModel;
274     }
275 
276     protected Configuration getConfiguration() {
277         return cfg;
278     }
279 
280     public void setEscapeHtml(boolean escapeHtml) {
281         this.escapeHtml = escapeHtml;
282     }
283 
284     public boolean isEscapeHtml() {
285         return escapeHtml;
286     }
287 
288     private static class ConfigDelegatingTemplateLoader implements TemplateLoader {
289         private final FreemarkerConfig freemarkerConfig;
290 
291         public ConfigDelegatingTemplateLoader(FreemarkerConfig freemarkerConfig) {
292             this.freemarkerConfig = freemarkerConfig;
293         }
294 
295         @Override
296         public Object findTemplateSource(String name) throws IOException {
297             return freemarkerConfig.getTemplateLoader().findTemplateSource(name);
298         }
299 
300         @Override
301         public long getLastModified(Object templateSource) {
302             return freemarkerConfig.getTemplateLoader().getLastModified(templateSource);
303         }
304 
305         @Override
306         public Reader getReader(Object templateSource, String encoding) throws IOException {
307             return freemarkerConfig.getTemplateLoader().getReader(templateSource, encoding);
308         }
309 
310         @Override
311         public void closeTemplateSource(Object templateSource) throws IOException {
312             freemarkerConfig.getTemplateLoader().closeTemplateSource(templateSource);
313         }
314     }
315 
316     private static class ConfigDelegatingObjectWrapper implements ObjectWrapper {
317         private final FreemarkerConfig freemarkerConfig;
318 
319         public ConfigDelegatingObjectWrapper(FreemarkerConfig freemarkerConfig) {
320             this.freemarkerConfig = freemarkerConfig;
321         }
322 
323         @Override
324         public TemplateModel wrap(Object obj) throws TemplateModelException {
325             return freemarkerConfig.getObjectWrapper().wrap(obj);
326         }
327     }
328 }