AbstractFreemarkerBuilder.java

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

import static freemarker.template.Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.context.DefaultBuildContext;
import fr.sii.ogham.core.builder.resolution.ClassPathResolutionBuilder;
import fr.sii.ogham.core.builder.resolution.FileResolutionBuilder;
import fr.sii.ogham.core.builder.resolution.ResourceResolutionBuilder;
import fr.sii.ogham.core.builder.resolution.ResourceResolutionBuilderHelper;
import fr.sii.ogham.core.builder.resolution.StringResolutionBuilder;
import fr.sii.ogham.core.builder.template.DetectorBuilder;
import fr.sii.ogham.core.fluent.AbstractParent;
import fr.sii.ogham.core.resource.resolver.FirstSupportingResourceResolver;
import fr.sii.ogham.core.resource.resolver.ResourceResolver;
import fr.sii.ogham.core.template.detector.FixedEngineDetector;
import fr.sii.ogham.core.template.detector.OrTemplateDetector;
import fr.sii.ogham.core.template.detector.SimpleResourceEngineDetector;
import fr.sii.ogham.core.template.detector.TemplateEngineDetector;
import fr.sii.ogham.core.template.parser.TemplateParser;
import fr.sii.ogham.template.freemarker.FreeMarkerFirstSupportingTemplateLoader;
import fr.sii.ogham.template.freemarker.FreeMarkerParser;
import fr.sii.ogham.template.freemarker.FreeMarkerTemplateDetector;
import fr.sii.ogham.template.freemarker.SkipLocaleForStringContentTemplateLookupStrategy;
import fr.sii.ogham.template.freemarker.TemplateLoaderOptions;
import fr.sii.ogham.template.freemarker.adapter.ClassPathResolverAdapter;
import fr.sii.ogham.template.freemarker.adapter.FileResolverAdapter;
import fr.sii.ogham.template.freemarker.adapter.FirstSupportingResolverAdapter;
import fr.sii.ogham.template.freemarker.adapter.StringResolverAdapter;
import fr.sii.ogham.template.freemarker.adapter.TemplateLoaderAdapter;
import freemarker.cache.TemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;

@SuppressWarnings("squid:S00119")
public abstract class AbstractFreemarkerBuilder<MYSELF extends AbstractFreemarkerBuilder<MYSELF, P>, P> extends AbstractParent<P>
		implements DetectorBuilder<MYSELF>, ResourceResolutionBuilder<MYSELF>, Builder<TemplateParser> {
	private static final Logger LOG = LoggerFactory.getLogger(AbstractFreemarkerBuilder.class);

	protected final MYSELF myself;
	protected final BuildContext buildContext;
	private TemplateEngineDetector detector;
	private ResourceResolutionBuilderHelper<MYSELF> resourceResolutionBuilderHelper;
	private Configuration configuration;
	private List<TemplateLoaderAdapter> customAdapters;
	private FreemarkerConfigurationBuilder<MYSELF> configurationBuilder;
	private ClassLoader classLoader;

	protected AbstractFreemarkerBuilder(Class<?> selfType) {
		this(selfType, null, new DefaultBuildContext());
	}

	@SuppressWarnings("unchecked")
	protected AbstractFreemarkerBuilder(Class<?> selfType, P parent, BuildContext buildContext) {
		super(parent);
		myself = (MYSELF) selfType.cast(this);
		this.buildContext = buildContext;
		customAdapters = new ArrayList<>();
	}

	@Override
	public MYSELF detector(TemplateEngineDetector detector) {
		this.detector = detector;
		return myself;
	}

	@Override
	public ClassPathResolutionBuilder<MYSELF> classpath() {
		initResolutionBuilder();
		return resourceResolutionBuilderHelper.classpath();
	}

	@Override
	public FileResolutionBuilder<MYSELF> file() {
		initResolutionBuilder();
		return resourceResolutionBuilderHelper.file();
	}

	@Override
	public StringResolutionBuilder<MYSELF> string() {
		initResolutionBuilder();
		return resourceResolutionBuilderHelper.string();
	}

	@Override
	public MYSELF resolver(ResourceResolver resolver) {
		initResolutionBuilder();
		return resourceResolutionBuilderHelper.resolver(resolver);
	}

	/**
	 * Ogham provides a generic resource resolution mechanism
	 * ({@link ResourceResolver}). Freemarker uses its own template resolution
	 * mechanism ({@link TemplateLoader}). A resolver adapter
	 * ({@link TemplateLoaderAdapter}) is the way to transform a
	 * {@link ResourceResolver} into a {@link TemplateLoader}.
	 * 
	 * <p>
	 * Ogham provides and registers default resolver adapters but you may need
	 * to use a custom {@link ResourceResolver}. So you also need to provide the
	 * corresponding {@link TemplateLoaderAdapter}.
	 * 
	 * @param adapter
	 *            the resolver adapter
	 * @return this instance for fluent chaining
	 */
	public MYSELF resolverAdapter(TemplateLoaderAdapter adapter) {
		customAdapters.add(adapter);
		return myself;
	}

	/**
	 * Fluent configurer for Freemarker configuration.
	 * 
	 * @return the fluent builder for Freemarker configuration object
	 */
	public FreemarkerConfigurationBuilder<MYSELF> configuration() {
		if (configurationBuilder == null) {
			configurationBuilder = new FreemarkerConfigurationBuilder<>(myself, buildContext);
		}
		return configurationBuilder;
	}

	/**
	 * Sets a Freemarker configuration.
	 * 
	 * This value preempts any other value defined by calling
	 * {@link #configuration()} method. It means that the provided configuration
	 * is used as-is and any call to {@link #configuration()} builder methods
	 * has no effect on the provided configuration. You have to manually
	 * configure it.
	 * 
	 * If this method is called several times, only the last provided
	 * configuration is used.
	 * 
	 * @param configuration
	 *            the Freemarker configuration
	 * @return this instance for fluent chaining
	 */
	public MYSELF configuration(Configuration configuration) {
		this.configuration = configuration;
		return myself;
	}

	/**
	 * Merge an existing Freemarker configuration with previously provided
	 * configuration.
	 * 
	 * <p>
	 * The provided configuration is used and any call to
	 * {@link #configuration()} builder methods are applied to the provided
	 * configuration.
	 * 
	 * 
	 * @param configuration
	 *            The Freemarker configuration to apply
	 * @return this instance for fluent chaining
	 */
	public MYSELF mergeConfiguration(Configuration configuration) {
		configuration().base(configuration);
		return myself;
	}

	/**
	 * Set the {@link ClassLoader} to use for loading classpath resources.
	 * 
	 * <p>
	 * Loading resources from classpath requires a {@link ClassLoader}. Several
	 * class loaders may be defined in an application to isolate parts of the
	 * application. FreeMarker requires you to provide a {@link ClassLoader} for
	 * finding resources in the classpath. This is done for security reasons.
	 * 
	 * <p>
	 * By default, Ogham uses the current thread class loader.
	 * 
	 * @param classLoader
	 *            the class loader to use
	 * @return this instance for fluent chaining
	 */
	public MYSELF classLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
		return myself;
	}

	@Override
	public TemplateParser build() {
		LOG.info("Freemarker parser is registered");
		return buildContext.register(new FreeMarkerParser(buildConfiguration(), buildResolver()));
	}

	@Override
	public TemplateEngineDetector buildDetector() {
		return detector == null ? buildDefaultDetector() : detector;
	}

	private TemplateEngineDetector buildDefaultDetector() {
		FirstSupportingResourceResolver resolver = buildResolver();
		OrTemplateDetector or = buildContext.register(new OrTemplateDetector());
		or.addDetector(buildContext.register(new FreeMarkerTemplateDetector(resolver, ".ftl", ".ftlh")));
		or.addDetector(buildContext.register(new SimpleResourceEngineDetector(resolver, buildContext.register(new FixedEngineDetector(true)))));
		return or;
	}

	/**
	 * Builds the resolver used by Freemarker to resolve resources
	 * 
	 * @return the resource resolver
	 */
	public FirstSupportingResourceResolver buildResolver() {
		return buildContext.register(new FirstSupportingResourceResolver(buildResolvers()));
	}

	private Configuration buildConfiguration() {
		Configuration builtConfiguration;
		if (this.configuration != null) {
			builtConfiguration = this.configuration;
		} else if (configurationBuilder != null) {
			builtConfiguration = configurationBuilder.build();
		} else {
			builtConfiguration = new Configuration(DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
			builtConfiguration.setDefaultEncoding("UTF-8");
			builtConfiguration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
		}
		FirstSupportingResourceResolver builtResolver = buildResolver();
		FirstSupportingResolverAdapter builtAdapter = buildAdapter();
		builtConfiguration.setTemplateLoader(buildContext.register(new FreeMarkerFirstSupportingTemplateLoader(builtResolver, builtAdapter)));
		builtConfiguration.setTemplateLookupStrategy(buildContext.register(new SkipLocaleForStringContentTemplateLookupStrategy(builtConfiguration.getTemplateLookupStrategy(), builtResolver, builtAdapter)));
		return builtConfiguration;
	}

	protected List<ResourceResolver> buildResolvers() {
		initResolutionBuilder();
		return resourceResolutionBuilderHelper.buildResolvers();
	}

	protected FirstSupportingResolverAdapter buildAdapter() {
		FirstSupportingResolverAdapter adapter = new FirstSupportingResolverAdapter();
		for (TemplateLoaderAdapter custom : customAdapters) {
			adapter.addAdapter(custom);
		}
		adapter.addAdapter(buildContext.register(new ClassPathResolverAdapter(classLoader)));
		adapter.addAdapter(buildContext.register(new FileResolverAdapter()));
		adapter.addAdapter(buildContext.register(new StringResolverAdapter()));
		adapter.setOptions(buildContext.register(new TemplateLoaderOptions()));
		return adapter;
	}

	private void initResolutionBuilder() {
		if (resourceResolutionBuilderHelper == null) {
			resourceResolutionBuilderHelper = new ResourceResolutionBuilderHelper<>(myself, buildContext);
		}
	}
}