/**
* This file Copyright (c) 2003-2012 Magnolia International
* Ltd. (http://www.magnolia-cms.com). All rights reserved.
*
*
* This file is dual-licensed under both the Magnolia
* Network Agreement and the GNU General Public License.
* You may elect to use one or the other of these licenses.
*
* This file is distributed in the hope that it will be
* useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
* Redistribution, except as permitted by whichever of the GPL
* or MNA you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or
* modify this file under the terms of the GNU General
* Public License, Version 3, as published by the Free Software
* Foundation. You should have received a copy of the GNU
* General Public License, Version 3 along with this program;
* if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 2. For the Magnolia Network Agreement (MNA), this file
* and the accompanying materials are made available under the
* terms of the MNA which accompanies this distribution, and
* is available at http://www.magnolia-cms.com/mna.html
*
* Any modifications to this file must keep this entire header
* intact.
*
*/
package info.magnolia.freemarker;
import freemarker.cache.TemplateLoader;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.ext.servlet.FreemarkerServlet;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.ext.servlet.ServletContextHashModel;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import info.magnolia.cms.beans.config.ServerConfiguration;
import info.magnolia.context.MgnlContext;
import info.magnolia.context.WebContext;
import info.magnolia.objectfactory.Components;
import javax.inject.Singleton;
import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* A generic helper to render Content instances with Freemarker templates.
*
* TODO : expose Configuration#clearTemplateCache()
*
* @author gjoseph
* @version $Revision: $ ($Author: $)
*/
@Singleton
public class FreemarkerHelper {
/**
* @deprecated since 4.5, use IoC !
*/
public static FreemarkerHelper getInstance() {
return Components.getSingleton(FreemarkerHelper.class);
}
private final Configuration cfg;
// taglib support stuff
private TaglibFactory taglibFactory;
private ServletContextHashModel servletContextHashModel;
/**
* @deprecated since 4.5, use IoC, i.e use {@link #FreemarkerHelper(FreemarkerConfig)}
*/
public FreemarkerHelper() {
this(Components.getSingleton(FreemarkerConfig.class));
}
public FreemarkerHelper(final FreemarkerConfig freemarkerConfig) {
// we subclass freemarker.Configuration to override some methods which must delegate to our observed FreemarkerConfig
this.cfg = new Configuration() {
@Override
public Set getSharedVariableNames() {
final Set names = super.getSharedVariableNames();
names.addAll(freemarkerConfig.getSharedVariables().keySet());
return names;
}
@Override
public TemplateModel getSharedVariable(String name) {
final TemplateModel value = super.getSharedVariable(name);
if (value==null) {
return freemarkerConfig.getSharedVariables().get(name);
}
return value;
}
};
cfg.setTemplateExceptionHandler(freemarkerConfig.getTemplateExceptionHandler());
// ... and here we essentially do the same by instantiate delegator implementations of FreeMarker components, which delegate to our observed FreemarkerConfig
// these setters do more than their equivalent getters, so we can't just override the getter instead.
// ultimately, we could probably have our own clean subclass of freemarker.Configuration to hide all these details off FreemarkerHelper
cfg.setTemplateLoader(new ConfigDelegatingTemplateLoader(freemarkerConfig));
cfg.setObjectWrapper(new ConfigDelegatingObjectWrapper(freemarkerConfig));
cfg.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX);
cfg.setDefaultEncoding("UTF8");
//cfg.setTemplateUpdateDelay(10);
}
/**
* @deprecated not needed anymore since 4.3
*/
public void resetObjectWrapper() {
// getConfiguration().setObjectWrapper(newObjectWrapper());
}
/**
* @see #render(String, Locale, String, Object, java.io.Writer)
*/
public void render(String templatePath, Object root, Writer out) throws TemplateException, IOException {
render(templatePath, null, null, root, out);
}
/**
* Renders the given template, using the given root object (can be a map, or any other type of object
* handled by MagnoliaContentWrapper) to the given Writer.
* If the root is an instance of a Map, the following elements are added to it:
* - ctx, the current Context instance retrieved from MgnlContext
* - contextPath, if we have an available WebContext (@deprecated)
* - defaultBaseUrl, as per Server.getDefaultBaseUrl()
*
* @see ServerConfiguration#getDefaultBaseUrl()
*/
public void render(String templatePath, Locale locale, String i18nBasename, Object root, Writer out) throws TemplateException, IOException {
final Locale localeToUse = checkLocale(locale);
prepareRendering(localeToUse, i18nBasename, root);
final Template template = cfg.getTemplate(templatePath, localeToUse);
template.process(root, out);
}
/**
* Renders the template read by the given Reader instance. It should be noted that this method completely bypasses
* Freemarker's caching mechanism. The template will be parsed everytime, which might have a performance impact.
*
* @see #render(Reader, Locale, String, Object, Writer)
*/
public void render(Reader template, Object root, Writer out) throws TemplateException, IOException {
render(template, null, null, root, out);
}
protected void render(Reader template, Locale locale, String i18nBasename, Object root, Writer out) throws TemplateException, IOException {
final Locale localeToUse = checkLocale(locale);
prepareRendering(localeToUse, i18nBasename, root);
final Template t = new Template("inlinetemplate", template, cfg);
t.setLocale(localeToUse);
t.process(root, out);
}
/**
* Returns the passed Locale if non-null, otherwise attempts to get the Locale from the current context.
*/
protected Locale checkLocale(Locale locale) {
if (locale != null) {
return locale;
} else if (MgnlContext.hasInstance()) {
return MgnlContext.getLocale();
} else {
return Locale.getDefault();
}
}
/**
* Call checkLocale() before calling this method, to ensure it is not null.
*/
protected void prepareRendering(Locale checkedLocale, String i18nBasename, Object root) {
if (root instanceof Map) {
final Map<String, Object> data = (Map<String, Object>) root;
addDefaultData(data, checkedLocale, i18nBasename);
}
}
protected void addDefaultData(Map<String, Object> data, Locale locale, String i18nBasename) {
if (MgnlContext.hasInstance()) {
data.put("ctx", MgnlContext.getInstance());
}
if (MgnlContext.isWebContext()) {
final WebContext webCtx = MgnlContext.getWebContext();
// @deprecated (-> update all templates) - TODO see MAGNOLIA-1789
data.put("contextPath", webCtx.getContextPath());
data.put("aggregationState", webCtx.getAggregationState());
addTaglibSupportData(data, webCtx);
}
data.put("defaultBaseUrl", ServerConfiguration.getInstance().getDefaultBaseUrl());
if (i18nBasename != null) {
data.put("i18n", new MessagesWrapper(i18nBasename, locale));
}
// TODO : this is currently still in FreemarkerUtil. If we add it here,
// the attribute "message" we put in the Freemarker context should have a less generic name
// (-> update all templates)
// if (AlertUtil.isMessageSet(mgnlCtx)) {
// data.put("message", AlertUtil.getMessage(mgnlCtx));
// }
}
protected void addTaglibSupportData(Map<String, Object> data, WebContext webCtx) {
final ServletContext servletContext = webCtx.getServletContext();
try {
data.put(FreemarkerServlet.KEY_JSP_TAGLIBS, checkTaglibFactory(servletContext));
data.put(FreemarkerServlet.KEY_APPLICATION_PRIVATE, checkServletContextModel(servletContext));
data.put(FreemarkerServlet.KEY_REQUEST_PRIVATE, new HttpRequestHashModel(webCtx.getRequest(), webCtx.getResponse(), cfg.getObjectWrapper()));
} catch (ServletException e) {
// 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
throw new RuntimeException("Can't initialize taglib support for FreeMarker: ", e);
}
}
protected TaglibFactory checkTaglibFactory(ServletContext servletContext) {
if (taglibFactory == null) {
taglibFactory = new TaglibFactory(new FreemarkerServletContextWrapper(servletContext));
}
return taglibFactory;
}
protected ServletContextHashModel checkServletContextModel(ServletContext servletContext) throws ServletException {
if (servletContextHashModel == null) {
// FreeMarker needs an instance of a GenericServlet, but it doesn't have to do anything other than provide references to the ServletContext
final GenericServlet fs = new DoNothingServlet(servletContext);
servletContextHashModel = new ServletContextHashModel(fs, cfg.getObjectWrapper());
}
return servletContextHashModel;
}
protected Configuration getConfiguration() {
return cfg;
}
private class ConfigDelegatingTemplateLoader implements TemplateLoader {
private final FreemarkerConfig freemarkerConfig;
public ConfigDelegatingTemplateLoader(FreemarkerConfig freemarkerConfig) {
this.freemarkerConfig = freemarkerConfig;
}
@Override
public Object findTemplateSource(String name) throws IOException {
return freemarkerConfig.getTemplateLoader().findTemplateSource(name);
}
@Override
public long getLastModified(Object templateSource) {
return freemarkerConfig.getTemplateLoader().getLastModified(templateSource);
}
@Override
public Reader getReader(Object templateSource, String encoding) throws IOException {
return freemarkerConfig.getTemplateLoader().getReader(templateSource, encoding);
}
@Override
public void closeTemplateSource(Object templateSource) throws IOException {
freemarkerConfig.getTemplateLoader().closeTemplateSource(templateSource);
}
}
private class ConfigDelegatingObjectWrapper implements ObjectWrapper {
private final FreemarkerConfig freemarkerConfig;
public ConfigDelegatingObjectWrapper(FreemarkerConfig freemarkerConfig) {
this.freemarkerConfig = freemarkerConfig;
}
@Override
public TemplateModel wrap(Object obj) throws TemplateModelException {
return freemarkerConfig.getObjectWrapper().wrap(obj);
}
}
}