/* * 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.checkArgumentNotEmpty; import static org.trimou.util.Checker.checkArgumentNotNull; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.trimou.Mustache; import org.trimou.engine.cache.ComputingCache; import org.trimou.engine.config.Configuration; import org.trimou.engine.config.ConfigurationFactory; import org.trimou.engine.config.EngineConfigurationKey; import org.trimou.engine.listener.MustacheCompilationEvent; import org.trimou.engine.listener.MustacheListener; import org.trimou.engine.listener.MustacheParsingEvent; import org.trimou.engine.locator.TemplateLocator; import org.trimou.engine.parser.ParserFactory; import org.trimou.engine.parser.ParsingHandler; import org.trimou.engine.parser.ParsingHandlerFactory; import org.trimou.exception.MustacheException; import org.trimou.exception.MustacheProblem; import org.trimou.util.IOUtils; /** * The default Mustache engine implementation. * * @author Martin Kouba */ class DefaultMustacheEngine implements MustacheEngine { private static final Logger LOGGER = LoggerFactory .getLogger(DefaultMustacheEngine.class); private final ComputingCache<String, Optional<Mustache>> templateCache; private final ComputingCache<String, Optional<String>> sourceCache; private final Configuration configuration; private final ParserFactory parserFactory; private final ParsingHandlerFactory parsingHandlerFactory; /** * Workaround for CDI (JSR 299, JSR 346) - make this type proxyable so that * it's possible to produce an application-scoped CDI bean. */ DefaultMustacheEngine() { configuration = null; parserFactory = null; parsingHandlerFactory = null; templateCache = null; sourceCache = null; } /** * * @param builder */ DefaultMustacheEngine(MustacheEngineBuilder builder) { // First create the engine configuration configuration = new ConfigurationFactory().createConfiguration(builder); parserFactory = new ParserFactory(); parsingHandlerFactory = new ParsingHandlerFactory(); if (configuration .getBooleanPropertyValue(EngineConfigurationKey.DEBUG_MODE)) { templateCache = null; sourceCache = null; LOGGER.warn( "Attention! Debug mode enabled: template cache disabled, additional logging enabled"); } else { if (configuration.getBooleanPropertyValue( EngineConfigurationKey.TEMPLATE_CACHE_ENABLED)) { templateCache = buildTemplateCache(); sourceCache = configuration.getBooleanPropertyValue( EngineConfigurationKey.TEMPLATE_CACHE_USED_FOR_SOURCE) ? buildSourceCache() : null; if (configuration.getBooleanPropertyValue( EngineConfigurationKey.PRECOMPILE_ALL_TEMPLATES)) { precompileTemplates(); } } else { templateCache = null; sourceCache = null; LOGGER.info("Template cache explicitly disabled!"); } } } public Mustache getMustache(String templateId) { checkArgumentNotEmpty(templateId); return templateCache != null ? getTemplateFromCache(templateId) : locateAndParse(templateId); } public String getMustacheSource(String templateId) { checkArgumentNotEmpty(templateId); return sourceCache != null ? getSourceFromCache(templateId) : locateAndRead(templateId); } public Mustache compileMustache(String templateId, String templateContent) { checkArgumentNotEmpty(templateId); checkArgumentNotEmpty(templateContent); return parse(templateId, new StringReader(templateContent)); } public Configuration getConfiguration() { return configuration; } public void invalidateTemplateCache() { if (isCacheEnabled()) { templateCache.clear(); if (sourceCache != null) { sourceCache.clear(); } } } @Override public void invalidateTemplateCache(Predicate<String> predicate) { if (isCacheEnabled()) { checkArgumentNotNull(predicate); templateCache.invalidate(predicate::test); if (sourceCache != null) { sourceCache.invalidate(predicate::test); } } } private boolean isCacheEnabled() { if (templateCache == null) { LOGGER.warn( "Unable to invalidate the template cache - it's disabled!"); return false; } return true; } private ComputingCache<String, Optional<Mustache>> buildTemplateCache() { return buildCache("Template", key -> Optional.ofNullable(locateAndParse(key)), (key, cause) -> LOGGER.debug("Removed template from cache [templateId: {}, cause: {}]", key, cause)); } /** * Properties of the source cache are dependent on that of the template * cache. */ private ComputingCache<String, Optional<String>> buildSourceCache() { return buildCache("Source", key -> Optional.ofNullable(locateAndRead(key)), (key, cause) -> LOGGER.debug("Removed template source from cache [templateId: {}, cause: {}]", key, cause)); } private <K, V> ComputingCache<K, V> buildCache(String name, ComputingCache.Function<K, V> loader, ComputingCache.Listener<K> listener) { Long expirationTimeout = configuration.getLongPropertyValue( EngineConfigurationKey.TEMPLATE_CACHE_EXPIRATION_TIMEOUT); if (expirationTimeout > 0) { LOGGER.info("{} cache expiration timeout set: {} seconds", name, expirationTimeout); expirationTimeout = expirationTimeout * 1000L; } else { expirationTimeout = null; } return configuration.getComputingCacheFactory().create( MustacheEngine.COMPUTING_CACHE_CONSUMER_ID, loader, expirationTimeout, null, listener); } private void precompileTemplates() { Set<String> templateNames = new HashSet<>(); for (TemplateLocator locator : configuration.getTemplateLocators()) { templateNames.addAll(locator.getAllIdentifiers()); } for (String templateName : templateNames) { getTemplateFromCache(templateName); } } private Mustache parse(String templateId, Reader reader) { ParsingHandler handler = parsingHandlerFactory.createParsingHandler(); reader = notifyListenersBeforeParsing(templateId, reader); parserFactory.createParser(this).parse(templateId, reader, handler); Mustache mustache = handler.getCompiledTemplate(); notifyListenersAfterCompilation(mustache); return mustache; } private Reader locate(String templateId) { List<TemplateLocator> locators = configuration.getTemplateLocators(); if (locators == null || locators.isEmpty()) { return null; } Reader reader = null; for (TemplateLocator locator : locators) { reader = locator.locate(templateId); if (reader != null) { break; } } return reader; } private Mustache locateAndParse(String templateId) { Reader reader = null; try { reader = locate(templateId); if (reader == null) { return null; } return parse(templateId, reader); } finally { closeReader(reader, templateId); } } private String locateAndRead(String templateId) { Reader reader = null; try { reader = locate(templateId); if (reader == null) { return null; } return IOUtils.toString(reader); } catch (Exception e) { LOGGER.error(e.getMessage(), e); return null; } finally { closeReader(reader, templateId); } } private void closeReader(Reader reader, String templateId) { if (reader != null) { try { reader.close(); } catch (IOException e) { LOGGER.warn("Unable to close the reader for " + templateId, e); } } } private Reader notifyListenersBeforeParsing(String templateName, Reader reader) { if (configuration.getMustacheListeners() != null) { MustacheParsingEvent event = new DefaultMustacheParsingEvent( templateName, reader); for (MustacheListener listener : configuration .getMustacheListeners()) { listener.parsingStarted(event); } return event.getMustacheContents(); } return reader; } private void notifyListenersAfterCompilation(Mustache mustache) { if (configuration.getMustacheListeners() != null) { MustacheCompilationEvent event = new DefaultMustacheCompilationEvent( mustache); for (MustacheListener listener : configuration .getMustacheListeners()) { listener.compilationFinished(event); } } } private Mustache getTemplateFromCache(String templateName) { try { return templateCache.get(templateName).orElse(null); } catch (Exception e) { throw unwrapUncheckedExecutionException(e); } } private String getSourceFromCache(String templateName) { try { return sourceCache.get(templateName).orElse(null); } catch (Exception e) { throw unwrapUncheckedExecutionException(e); } } private RuntimeException unwrapUncheckedExecutionException(Exception e) { Throwable cause = e.getCause() == null ? e : e.getCause(); if (cause instanceof RuntimeException) { return (RuntimeException) cause; } return new MustacheException(MustacheProblem.TEMPLATE_LOADING_ERROR, cause); } /** * * @author Martin Kouba */ private static class DefaultMustacheCompilationEvent implements MustacheCompilationEvent { private final Mustache mustache; public DefaultMustacheCompilationEvent(Mustache mustache) { super(); this.mustache = mustache; } @Override public Mustache getMustache() { return mustache; } } /** * * @author Martin Kouba */ private static class DefaultMustacheParsingEvent implements MustacheParsingEvent { private final String mustacheName; private Reader reader; public DefaultMustacheParsingEvent(String mustacheName, Reader reader) { super(); this.mustacheName = mustacheName; this.reader = reader; } public String getMustacheName() { return mustacheName; } @Override public Reader getMustacheContents() { return reader; } @Override public void setMustacheContents(Reader reader) { this.reader = reader; } } }