FreemarkerConfigurationBuilder.java

package fr.sii.ogham.template.freemarker.builder;

import static freemarker.template.Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;

import java.io.Writer;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilderHelper;
import fr.sii.ogham.core.builder.configurer.Configurer;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.env.EnvironmentBuilder;
import fr.sii.ogham.core.exception.builder.BuildException;
import fr.sii.ogham.core.fluent.AbstractParent;
import freemarker.core.Configurable;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.Version;

/**
 * Fluent builder to configure Freemarker configuration object.
 * 
 * @author Aurélien Baudet
 *
 * @param <P>
 *            the type of the parent builder (when calling {@link #and()}
 *            method)
 */
public class FreemarkerConfigurationBuilder<P> extends AbstractParent<P> implements Builder<Configuration> {
	private final ConfigurationValueBuilderHelper<FreemarkerConfigurationBuilder<P>, String> defaultEncodingValueBuilder;
	private final ConfigurationValueBuilderHelper<FreemarkerConfigurationBuilder<P>, Boolean> enableStaticMethodAccessValueBuilder;
	private final ConfigurationValueBuilderHelper<FreemarkerConfigurationBuilder<P>, String> staticMethodAccessVariableNameValueBuilder;
	private final Map<String, Object> sharedVariables;
	private Configuration base;
	private Version version;
	private TemplateExceptionHandler templateExceptionHandler;
	private TemplateHashModelEx variablesHash;
	// TODO: handle all other options

	/**
	 * Initializes the builder with a parent builder. The parent builder is used
	 * when calling {@link #and()} method. The {@link EnvironmentBuilder} is
	 * used to evaluate properties when {@link #build()} method is called.
	 * 
	 * @param parent
	 *            the parent builder
	 * @param buildContext
	 *            for registering instances and property evaluation
	 */
	public FreemarkerConfigurationBuilder(P parent, BuildContext buildContext) {
		super(parent);
		defaultEncodingValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
		enableStaticMethodAccessValueBuilder = buildContext.newConfigurationValueBuilder(this, Boolean.class);
		staticMethodAccessVariableNameValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
		sharedVariables = new HashMap<>();
	}

	/**
	 * Sets the base configuration that will be configured.
	 * 
	 * If none is provided, a new configuration instance is created.
	 * 
	 * @param base
	 *            the configuration to use and to configure
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> base(Configuration base) {
		this.base = base;
		return this;
	}

	/**
	 * Sets which of the non-backward-compatible bugfixes/improvements should be
	 * enabled.
	 * 
	 * See {@link Configuration#Configuration(Version)} for more information
	 * about version and incompatible improvements.
	 * 
	 * @see Configuration#Configuration(Version)
	 * @param version
	 *            the non-backward-compatible bugfixes/improvements should be
	 *            enabled
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> version(Version version) {
		this.version = version;
		return this;
	}

	/**
	 * * Sets the charset used for decoding byte sequences to character
	 * sequences when reading template files in a locale for which no explicit
	 * encoding was specified via
	 * {@link Configuration#setEncoding(Locale, String)}. Note that by default
	 * there is no locale specified for any locale, so the default encoding is
	 * always in effect.
	 * 
	 * <p>
	 * Defaults to the default system encoding, which can change from one server
	 * to another, so <b>you should always set this setting</b>. If you don't
	 * know what charset your should chose, {@code "UTF-8"} is usually a good
	 * choice.
	 * 
	 * <p>
	 * Note that individual templates may specify their own charset by starting
	 * with {@code <#ftl encoding="...">}
	 * 
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #defaultEncoding()}.
	 * 
	 * <pre>
	 * .defaultEncoding("UTF-16")
	 * .defaultEncoding()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("UTF-8")
	 * </pre>
	 * 
	 * <pre>
	 * .defaultEncoding("UTF-16")
	 * .defaultEncoding()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("UTF-8")
	 * </pre>
	 * 
	 * In both cases, {@code defaultEncoding("UTF-16")} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param charsetName
	 *            the name of the charset
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> defaultEncoding(String charsetName) {
		defaultEncodingValueBuilder.setValue(charsetName);
		return this;
	}

	/**
	 * * Sets the charset used for decoding byte sequences to character
	 * sequences when reading template files in a locale for which no explicit
	 * encoding was specified via
	 * {@link Configuration#setEncoding(Locale, String)}. Note that by default
	 * there is no locale specified for any locale, so the default encoding is
	 * always in effect.
	 * 
	 * <p>
	 * Defaults to the default system encoding, which can change from one server
	 * to another, so <b>you should always set this setting</b>. If you don't
	 * know what charset your should chose, {@code "UTF-8"} is usually a good
	 * choice.
	 * 
	 * <p>
	 * Note that individual templates may specify their own charset by starting
	 * with {@code <#ftl encoding="...">}
	 * 
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .defaultEncoding()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("UTF-8")
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #defaultEncoding(String)} takes
	 * precedence over property values and default value.
	 * 
	 * <pre>
	 * .defaultEncoding("UTF-16")
	 * .defaultEncoding()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("UTF-8")
	 * </pre>
	 * 
	 * The value {@code "UTF-16"} is used regardless of the value of the
	 * properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<FreemarkerConfigurationBuilder<P>, String> defaultEncoding() {
		return defaultEncodingValueBuilder;
	}

	/**
	 * Sets the exception handler used to handle exceptions occurring inside
	 * templates. The default is {@link TemplateExceptionHandler#DEBUG_HANDLER}.
	 * The recommended values are:
	 * 
	 * <ul>
	 * <li>In production systems:
	 * {@link TemplateExceptionHandler#RETHROW_HANDLER}
	 * <li>During development of HTML templates:
	 * {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER}
	 * <li>During development of non-HTML templates:
	 * {@link TemplateExceptionHandler#DEBUG_HANDLER}
	 * </ul>
	 * 
	 * <p>
	 * All of these will let the exception propagate further, so that you can
	 * catch it around {@link Template#process(Object, Writer)} for example. The
	 * difference is in what they print on the output before they do that.
	 * 
	 * <p>
	 * Note that the {@link TemplateExceptionHandler} is not meant to be used
	 * for generating HTTP error pages. Neither is it meant to be used to roll
	 * back the printed output. These should be solved outside template
	 * processing when the exception raises from
	 * {@link Template#process(Object, Writer) Template.process}.
	 * {@link TemplateExceptionHandler} meant to be used if you want to include
	 * special content <em>in</em> the template output, or if you want to
	 * suppress certain exceptions.
	 * 
	 * @param exceptionHandler
	 *            the exception handler
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> templateExceptionHandler(TemplateExceptionHandler exceptionHandler) {
		this.templateExceptionHandler = exceptionHandler;
		return this;
	}

	/**
	 * Adds a shared variable to the configuration. Shared sharedVariables are
	 * sharedVariables that are visible as top-level sharedVariables for all
	 * templates which use this configuration, if the data model does not
	 * contain a variable with the same name.
	 *
	 * <p>
	 * Never use <code>TemplateModel</code> implementation that is not
	 * thread-safe for shared sharedVariables, if the configuration is used by
	 * multiple threads! It is the typical situation for Servlet based Web
	 * sites.
	 * 
	 * <p>
	 * This method is <b>not</b> thread safe; use it with the same restrictions
	 * as those that modify setting values.
	 *
	 * @param name
	 *            the name used to access the data object from your template. If
	 *            a shared variable with this name already exists, it will
	 *            replace that.
	 * @param tm
	 *            the data object value directly available as a
	 *            {@link TemplateModel}
	 * 
	 * @see #addSharedVariables
	 * @see #addSharedVariable(String,Object)
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> addSharedVariable(String name, TemplateModel tm) {
		this.sharedVariables.put(name, tm);
		return this;
	}

	/**
	 * Adds shared variable to the configuration; It uses
	 * {@link Configurable#getObjectWrapper()} to wrap the {@code value}, so
	 * it's important that the object wrapper is set before this.
	 * 
	 * <p>
	 * This method is <b>not</b> thread safe; use it with the same restrictions
	 * as those that modify setting values.
	 * 
	 * <p>
	 * The added value should be thread safe, if you are running templates from
	 * multiple threads with this configuration.
	 *
	 * @param name
	 *            the name used to access the data object from your template. If
	 *            a shared variable with this name already exists, it will
	 *            replace that.
	 * @param value
	 *            the data object value
	 *
	 * @see #addSharedVariable(String,TemplateModel)
	 * @see #addSharedVariables(TemplateHashModelEx)
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> addSharedVariable(String name, Object value) {
		this.sharedVariables.put(name, value);
		return this;
	}

	/**
	 * Enable calls to static methods from templates:
	 * 
	 * <pre>
	 * ${statics['foo.bar.StringUtils'].capitalize(name)}
	 * </pre>
	 * 
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #enableStaticMethodAccess()}.
	 * 
	 * <pre>
	 * .enableStaticMethodAccess(true)
	 * .enableStaticMethodAccess()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * <pre>
	 * .enableStaticMethodAccess(true)
	 * .enableStaticMethodAccess()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * In both cases, {@code enableStaticMethodAccess(true)} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param enable
	 *            enable or disable static method access
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> enableStaticMethodAccess(Boolean enable) {
		enableStaticMethodAccessValueBuilder.setValue(enable);
		return this;
	}

	/**
	 * Enable calls to static methods from templates:
	 * 
	 * <pre>
	 * ${statics['foo.bar.StringUtils'].capitalize(name)}
	 * </pre>
	 * 
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .enableStaticMethodAccess()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #enableStaticMethodAccess(Boolean)} takes
	 * precedence over property values and default value.
	 * 
	 * <pre>
	 * .enableStaticMethodAccess(true)
	 * .enableStaticMethodAccess()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * The value {@code true} is used regardless of the value of the properties
	 * and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<FreemarkerConfigurationBuilder<P>, Boolean> enableStaticMethodAccess() {
		return enableStaticMethodAccessValueBuilder;
	}

	/**
	 * Change the name of the variable used to access static methods.
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #staticMethodAccessVariableName()}.
	 * 
	 * <pre>
	 * .staticMethodAccessVariableName("myStatics")
	 * .staticMethodAccessVariableName()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("statics")
	 * </pre>
	 * 
	 * <pre>
	 * .staticMethodAccessVariableName("myStatics")
	 * .staticMethodAccessVariableName()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("statics")
	 * </pre>
	 * 
	 * In both cases, {@code staticMethodAccessVariableName("myStatics")} is
	 * used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param variableName
	 *            the name of the variable used to access static methods from
	 *            templates
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> staticMethodAccessVariableName(String variableName) {
		staticMethodAccessVariableNameValueBuilder.setValue(variableName);
		return this;
	}

	/**
	 * Change the name of the variable used to access static methods.
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .staticMethodAccessVariableName()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("statics")
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #staticMethodAccessVariableName(String)}
	 * takes precedence over property values and default value.
	 * 
	 * <pre>
	 * .staticMethodAccessVariableName("myStatics")
	 * .staticMethodAccessVariableName()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("statics")
	 * </pre>
	 * 
	 * The value {@code "myStatics"} is used regardless of the value of the
	 * properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<FreemarkerConfigurationBuilder<P>, String> staticMethodAccessVariableName() {
		return staticMethodAccessVariableNameValueBuilder;
	}

	/**
	 * Adds all object in the hash as shared variable to the configuration; it's
	 * like doing several {@link #addSharedVariable(String, Object)} calls, one
	 * for each hash entry. It doesn't remove the already added shared variable
	 * before doing this.
	 *
	 * <p>
	 * Never use <code>TemplateModel</code> implementation that is not
	 * thread-safe for shared shared variable values, if the configuration is
	 * used by multiple threads! It is the typical situation for Servlet based
	 * Web sites.
	 *
	 * <p>
	 * This method is <b>not</b> thread safe; use it with the same restrictions
	 * as those that modify setting values.
	 *
	 * @param variables
	 *            a hash model whose objects will be copied to the configuration
	 *            with same names as they are given in the hash. If a shared
	 *            variable with these names already exist, it will be replaced
	 *            with those from the map.
	 * @return this instance for fluent chaining
	 */
	public FreemarkerConfigurationBuilder<P> addSharedVariables(TemplateHashModelEx variables) {
		this.variablesHash = variables;
		return this;
	}

	@Override
	public Configuration build() {
		Configuration configuration = getConfiguration();
		String defaultEncoding = defaultEncodingValueBuilder.getValue();
		if (defaultEncoding != null) {
			configuration.setDefaultEncoding(defaultEncoding);
		}
		if (templateExceptionHandler != null) {
			configuration.setTemplateExceptionHandler(templateExceptionHandler);
		}
		buildSharedVariables(configuration);
		buildStaticMethodAccess(configuration);
		return configuration;
	}

	private Configuration getConfiguration() {
		if (base != null) {
			if (version != null) {
				base.setIncompatibleImprovements(version);
			}
			return base;
		}
		return version == null ? new Configuration(DEFAULT_INCOMPATIBLE_IMPROVEMENTS) : new Configuration(version);
	}

	private void buildSharedVariables(Configuration configuration) {
		try {
			if (variablesHash != null) {
				configuration.setAllSharedVariables(variablesHash);
			}
			for (Entry<String, Object> entry : sharedVariables.entrySet()) {
				configuration.setSharedVariable(entry.getKey(), entry.getValue());
			}
		} catch (TemplateModelException e) {
			throw new BuildException("Failed to configure FreeMarker shared variables", e);
		}
	}

	private void buildStaticMethodAccess(Configuration configuration) {
		boolean enableStaticMethods = enableStaticMethodAccessValueBuilder.getValue(false);
		if (enableStaticMethods) {
			String staticsVariableName = staticMethodAccessVariableNameValueBuilder.getValue();
			configuration.setSharedVariable(staticsVariableName, getBeansWrapper(configuration).getStaticModels());
		}
	}

	private static BeansWrapper getBeansWrapper(Configuration configuration) {
		ObjectWrapper objectWrapper = configuration.getObjectWrapper();
		if (objectWrapper instanceof BeansWrapper) {
			return (BeansWrapper) objectWrapper;
		}
		return new BeansWrapperBuilder(configuration.getIncompatibleImprovements()).build();
	}

}