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