/* * Copyright 2013 Martin Kouba * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.trimou.engine; import static org.trimou.util.Checker.checkArgumentNotNull; import static org.trimou.util.Checker.checkArgumentsNotNull; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.trimou.Mustache; import org.trimou.engine.cache.ComputingCacheFactory; import org.trimou.engine.config.ConfigurationAware; import org.trimou.engine.config.ConfigurationExtension; import org.trimou.engine.config.ConfigurationExtension.ConfigurationExtensionBuilder; import org.trimou.engine.config.ConfigurationKey; import org.trimou.engine.convert.ValueConverter; import org.trimou.engine.id.IdentifierGenerator; import org.trimou.engine.interpolation.KeySplitter; import org.trimou.engine.interpolation.LiteralSupport; import org.trimou.engine.interpolation.MissingValueHandler; import org.trimou.engine.listener.MustacheListener; import org.trimou.engine.locale.LocaleSupport; import org.trimou.engine.locator.TemplateLocator; import org.trimou.engine.resolver.Resolver; import org.trimou.engine.text.TextSupport; import org.trimou.handlebars.Helper; import org.trimou.util.ImmutableList; import org.trimou.util.ImmutableMap; import org.trimou.util.ImmutableSet; import org.trimou.util.Strings; /** * A builder for {@link MustacheEngine}. It's not thread-safe. The builder is * considered immutable once the {@link #build()} method is called. Subsequent * invocations of any modifying method or {@link #build()} result in an * {@link IllegalStateException}. * <p> * Note that most {@link ConfigurationAware} components are tied to the specific * engine instance and cannot be reused as well. * * @author Martin Kouba */ public final class MustacheEngineBuilder implements ConfigurationExtensionBuilder { private static final Logger LOGGER = LoggerFactory .getLogger(MustacheEngineBuilder.class); private static final String BUILD_PROPERTIES_FILE = "/trimou-build.properties"; private boolean isBuilt; private boolean omitServiceLoaderConfigurationExtensions; private final Set<Resolver> resolvers; private final Set<TemplateLocator> templateLocators; private final Map<String, Object> globalData; private TextSupport textSupport; private LocaleSupport localeSupport; private final Map<String, Object> properties; private final List<EngineBuiltCallback> engineReadyCallbacks; private final List<MustacheListener> mustacheListeners; private KeySplitter keySplitter; private MissingValueHandler missingValueHandler; private final Map<String, Helper> helpers; private ComputingCacheFactory computingCacheFactory; private IdentifierGenerator identifierGenerator; private ExecutorService executorService; private LiteralSupport literalSupport; private ClassLoader configurationExtensionClassLoader; private final Set<ValueConverter> valueConverters; /** * Don't create a new instance. * * @see #newBuilder() */ private MustacheEngineBuilder() { this.omitServiceLoaderConfigurationExtensions = false; this.resolvers = new HashSet<>(); this.templateLocators = new HashSet<>(); this.globalData = new HashMap<>(); this.properties = new HashMap<>(); this.mustacheListeners = new ArrayList<>(); this.helpers = new HashMap<>(); this.engineReadyCallbacks = new ArrayList<>(); this.valueConverters = new HashSet<>(); } /** * Builds the engine instance. * * @return the built engine */ public synchronized MustacheEngine build() { MustacheEngine engine = new DefaultMustacheEngine(this); for (EngineBuiltCallback callback : engineReadyCallbacks) { callback.engineBuilt(engine); } String version = null; String timestamp = null; try { // First try to get trimou-build.properties file InputStream in = MustacheEngineBuilder.class .getResourceAsStream(BUILD_PROPERTIES_FILE); if (in != null) { try { Properties buildProperties = new Properties(); buildProperties.load(in); version = buildProperties.getProperty("version"); timestamp = buildProperties.getProperty("timestamp"); } finally { in.close(); } } } catch (IOException e) { // No-op } if (version == null) { // If not available use the manifest info Package pack = MustacheEngineBuilder.class.getPackage(); version = pack.getSpecificationVersion(); timestamp = pack.getImplementationVersion(); } if (Strings.isEmpty(version)) { version = "SNAPSHOT"; } if (Strings.isEmpty(timestamp)) { timestamp = "n/a"; } int idx = timestamp.indexOf('T'); if (idx > 0) { timestamp = timestamp.substring(0, idx); } LOGGER.info("Engine built {} ({})", version, timestamp); LOGGER.debug("Engine configuration: {}", engine.getConfiguration().getInfo()); isBuilt = true; return engine; } /** * Adds a value (e.g. Lambda) that is available during execution of all * templates. * * Global values have to be thread-safe. * * @param value * @param name * @return self */ public MustacheEngineBuilder addGlobalData(String name, Object value) { checkArgumentsNotNull(name, value); checkNotBuilt(); this.globalData.put(name, value); return this; } /** * Adds a template locator. * * @param locator * @return self */ public MustacheEngineBuilder addTemplateLocator(TemplateLocator locator) { checkArgumentNotNull(locator); checkNotBuilt(); this.templateLocators.add(locator); return this; } /** * Adds a value resolver. * * @param resolver * @return self */ public MustacheEngineBuilder addResolver(Resolver resolver) { checkArgumentNotNull(resolver); checkNotBuilt(); this.resolvers.add(resolver); return this; } /** * Sets a configuration property. * * @param key * @param value * @return self */ public MustacheEngineBuilder setProperty(String key, Object value) { checkArgumentsNotNull(key, value); checkNotBuilt(); this.properties.put(key, value); return this; } /** * Sets a configuration property. * * @param configurationKey * @param value * @param <T> * The type of configuration key * @return self */ public <T extends ConfigurationKey> MustacheEngineBuilder setProperty( T configurationKey, Object value) { checkArgumentsNotNull(configurationKey, value); checkNotBuilt(); setProperty(configurationKey.get(), value); return this; } /** * Sets a text support instance. * * @param textSupport * @return self */ public MustacheEngineBuilder setTextSupport(TextSupport textSupport) { checkArgumentNotNull(textSupport); checkNotBuilt(); this.textSupport = textSupport; return this; } /** * Sets a locale support instance. * * @param localeSupport * @return self */ public MustacheEngineBuilder setLocaleSupport(LocaleSupport localeSupport) { checkArgumentNotNull(localeSupport); checkNotBuilt(); this.localeSupport = localeSupport; return this; } /** * Callback is useful to configure a component instantiated before the * engine is built. * * @param callback * @return self */ public MustacheEngineBuilder registerCallback( EngineBuiltCallback callback) { checkArgumentNotNull(callback); checkNotBuilt(); this.engineReadyCallbacks.add(callback); return this; } /** * Adds a {@link Mustache} listener. Manually added listeners are always * registered before listeners added via configuration extensions. * * @param listener * @return self */ public MustacheEngineBuilder addMustacheListener( MustacheListener listener) { checkArgumentNotNull(listener); checkNotBuilt(); this.mustacheListeners.add(listener); return this; } /** * * @param keySplitter * @return self */ public MustacheEngineBuilder setKeySplitter(KeySplitter keySplitter) { checkArgumentNotNull(keySplitter); checkNotBuilt(); this.keySplitter = keySplitter; return this; } /** * * @param missingValueHandler * @return self */ public MustacheEngineBuilder setMissingValueHandler( MissingValueHandler missingValueHandler) { checkArgumentNotNull(missingValueHandler); checkNotBuilt(); this.missingValueHandler = missingValueHandler; return this; } /** * Each helper must be registered with a unique name. If there are more * helpers registered with the same name an {@link IllegalArgumentException} * is thrown. Use {@link #registerHelper(String, Helper, boolean)} to * overwrite the helper. * * @param name * @param helper * @return self */ public MustacheEngineBuilder registerHelper(String name, Helper helper) { return registerHelper(name, helper, false); } /** * Each helper must be registered with a unique name. If there is a helper * registered with the same name and the param <code>overwrite</code> is * <code>true</code> the previous instance is replaced, otherwise an * {@link IllegalArgumentException} is thrown. * * @param name * @param helper * @param overwrite * @return self */ public MustacheEngineBuilder registerHelper(String name, Helper helper, boolean overwrite) { checkArgumentsNotNull(name, helper); checkNotBuilt(); if (!overwrite && helpers.containsKey(name)) { throw new IllegalArgumentException( "A helper with this name is already registered: " + name); } helpers.put(name, helper); return this; } /** * Each helper must be registered with a unique name. If there are more * helpers registered with the same name an {@link IllegalArgumentException} * is thrown. Use {@link #registerHelpers(Map, boolean)} to overwrite the * helpers. * * @param helpers * @return self */ public MustacheEngineBuilder registerHelpers(Map<String, Helper> helpers) { return registerHelpers(helpers, false); } /** * Each helper must be registered with a unique name. If there is a helper * registered with the same name and the param <code>overwrite</code> is * <code>true</code> the previous instance is replaced, otherwise an * {@link IllegalArgumentException} is thrown. * * @param helpers * @param overwrite * @return */ public MustacheEngineBuilder registerHelpers(Map<String, Helper> helpers, boolean overwrite) { checkArgumentNotNull(helpers); checkNotBuilt(); for (Entry<String, Helper> entry : helpers.entrySet()) { registerHelper(entry.getKey(), entry.getValue(), overwrite); } return this; } /** * Don't use the ServiceLoader mechanism to load configuration extensions * (i.e. the default resolvers are not added automatically). * * @see ConfigurationExtension */ public MustacheEngineBuilder omitServiceLoaderConfigurationExtensions() { checkNotBuilt(); this.omitServiceLoaderConfigurationExtensions = true; return this; } /** * Set the custom {@link ComputingCacheFactory}. * * @param cacheFactory * @return self */ public MustacheEngineBuilder setComputingCacheFactory( ComputingCacheFactory cacheFactory) { checkArgumentNotNull(cacheFactory); checkNotBuilt(); this.computingCacheFactory = cacheFactory; return this; } /** * Set the custom {@link IdentifierGenerator}. * * @param identifierGenerator * @return self */ public MustacheEngineBuilder setIdentifierGenerator( IdentifierGenerator identifierGenerator) { checkArgumentNotNull(identifierGenerator); checkNotBuilt(); this.identifierGenerator = identifierGenerator; return this; } /** * Set the {@link ExecutorService} to be used for async tasks. * * @param executorService * @return self */ public MustacheEngineBuilder setExecutorService( ExecutorService executorService) { checkArgumentNotNull(executorService); checkNotBuilt(); this.executorService = executorService; return this; } /** * Set the custom {@link LiteralSupport}. * * @param literalSupport * @return self */ public MustacheEngineBuilder setLiteralSupport( LiteralSupport literalSupport) { checkArgumentNotNull(literalSupport); checkNotBuilt(); this.literalSupport = literalSupport; return this; } /** * Set the {@link ClassLoader} used to load {@link ConfigurationExtension}s. * * @param configurationExtensionClassLoader * @return self */ public MustacheEngineBuilder setConfigurationExtensionClassLoader( ClassLoader configurationExtensionClassLoader) { checkArgumentNotNull(configurationExtensionClassLoader); checkNotBuilt(); this.configurationExtensionClassLoader = configurationExtensionClassLoader; return this; } /** * Add a value converter. * * @param converter * @return self */ public MustacheEngineBuilder addValueConverter(ValueConverter converter) { checkArgumentNotNull(converter); checkNotBuilt(); this.valueConverters.add(converter); return this; } /** * * @return a new instance of builder */ public static MustacheEngineBuilder newBuilder() { return new MustacheEngineBuilder(); } /** * * @author Martin Kouba * @see MustacheEngineBuilder#registerCallback(EngineBuiltCallback) */ @FunctionalInterface public interface EngineBuiltCallback { void engineBuilt(MustacheEngine engine); } public Set<TemplateLocator> buildTemplateLocators() { return ImmutableSet.copyOf(templateLocators); } public Set<Resolver> buildResolvers() { return ImmutableSet.copyOf(resolvers); } public Map<String, Object> buildGlobalData() { return ImmutableMap.copyOf(globalData); } public TextSupport getTextSupport() { return textSupport; } public LocaleSupport getLocaleSupport() { return localeSupport; } public boolean isOmitServiceLoaderConfigurationExtensions() { return omitServiceLoaderConfigurationExtensions; } public Map<String, Object> buildProperties() { return ImmutableMap.copyOf(properties); } public List<MustacheListener> buildMustacheListeners() { return ImmutableList.copyOf(mustacheListeners); } public KeySplitter getKeySplitter() { return keySplitter; } public MissingValueHandler getMissingValueHandler() { return missingValueHandler; } public Map<String, Helper> buildHelpers() { return ImmutableMap.copyOf(helpers); } public ComputingCacheFactory getComputingCacheFactory() { return computingCacheFactory; } public IdentifierGenerator getIdentifierGenerator() { return identifierGenerator; } public ExecutorService getExecutorService() { return executorService; } public LiteralSupport getLiteralSupport() { return literalSupport; } public ClassLoader getConfigurationExtensionClassLoader() { return configurationExtensionClassLoader; } public Set<ValueConverter> buildValueConverters() { return ImmutableSet.copyOf(valueConverters); } private void checkNotBuilt() { if (isBuilt) { throw new IllegalStateException( "Invalid method invocation - builder already built!"); } } }