/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 groovy.text.markup;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.Writable;
import groovy.text.Template;
import groovy.text.TemplateEngine;
import groovy.transform.CompileStatic;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.classgen.asm.BytecodeDumper;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A template engine which leverages {@link groovy.xml.StreamingMarkupBuilder} to generate XML/XHTML.
*
* @author Cedric Champeau
*/
public class MarkupTemplateEngine extends TemplateEngine {
static final ClassNode MARKUPTEMPLATEENGINE_CLASSNODE = ClassHelper.make(MarkupTemplateEngine.class);
static final String MODELTYPES_ASTKEY = "MTE.modelTypes";
private static final Pattern LOCALIZED_RESOURCE_PATTERN = Pattern.compile("(.+?)(?:_([a-z]{2}(?:_[A-Z]{2,3})))?\\.([\\p{Alnum}.]+)$");
private static final boolean DEBUG_BYTECODE = Boolean.valueOf(System.getProperty("markuptemplateengine.compiler.debug","false"));
private static final AtomicLong counter = new AtomicLong();
private final TemplateGroovyClassLoader groovyClassLoader;
private final CompilerConfiguration compilerConfiguration;
private final TemplateConfiguration templateConfiguration;
private final Map<String, GroovyCodeSource> codeSourceCache = new LinkedHashMap<String, GroovyCodeSource>();
private final TemplateResolver templateResolver;
public MarkupTemplateEngine() {
this(new TemplateConfiguration());
}
public MarkupTemplateEngine(final TemplateConfiguration tplConfig) {
this(MarkupTemplateEngine.class.getClassLoader(), tplConfig);
}
public MarkupTemplateEngine(final ClassLoader parentLoader, final TemplateConfiguration tplConfig) {
this(parentLoader, tplConfig, null);
}
public MarkupTemplateEngine(final ClassLoader parentLoader, final TemplateConfiguration tplConfig, final TemplateResolver resolver) {
compilerConfiguration = new CompilerConfiguration();
templateConfiguration = tplConfig;
compilerConfiguration.addCompilationCustomizers(new TemplateASTTransformer(tplConfig));
compilerConfiguration.addCompilationCustomizers(
new ASTTransformationCustomizer(Collections.singletonMap("extensions", "groovy.text.markup.MarkupTemplateTypeCheckingExtension"), CompileStatic.class));
if (templateConfiguration.isAutoNewLine()) {
compilerConfiguration.addCompilationCustomizers(
new CompilationCustomizer(CompilePhase.CONVERSION) {
@Override
public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException {
new AutoNewLineTransformer(source).visitClass(classNode);
}
}
);
}
groovyClassLoader = AccessController.doPrivileged(new PrivilegedAction<TemplateGroovyClassLoader>() {
public TemplateGroovyClassLoader run() {
return new TemplateGroovyClassLoader(parentLoader, compilerConfiguration);
}
});
if (DEBUG_BYTECODE) {
compilerConfiguration.setBytecodePostprocessor(BytecodeDumper.STANDARD_ERR);
}
templateResolver = resolver == null ? new DefaultTemplateResolver() : resolver;
templateResolver.configure(groovyClassLoader, templateConfiguration);
}
/**
* Convenience constructor to build a template engine which searches for templates into a directory
*
* @param templateDirectory directory where to find templates
* @param tplConfig template engine configuration
*/
public MarkupTemplateEngine(ClassLoader parentLoader, File templateDirectory, TemplateConfiguration tplConfig) {
this(new URLClassLoader(buildURLs(templateDirectory), parentLoader), tplConfig, null);
}
private static URL[] buildURLs(final File templateDirectory) {
try {
return new URL[]{templateDirectory.toURI().toURL()};
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid directory", e);
}
}
public Template createTemplate(final Reader reader) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(reader, null, null);
}
public Template createTemplate(final Reader reader, String sourceName) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(reader, sourceName, null);
}
public Template createTemplateByPath(final String templatePath) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(resolveTemplate(templatePath), null);
}
public Template createTypeCheckedModelTemplate(final String source, Map<String, String> modelTypes) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(new StringReader(source), null, modelTypes);
}
public Template createTypeCheckedModelTemplate(final String source, String sourceName, Map<String, String> modelTypes) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(new StringReader(source), sourceName, modelTypes);
}
public Template createTypeCheckedModelTemplate(final Reader reader, Map<String, String> modelTypes) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(reader, null, modelTypes);
}
public Template createTypeCheckedModelTemplate(final Reader reader, String sourceName, Map<String, String> modelTypes) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(reader, sourceName, modelTypes);
}
public Template createTypeCheckedModelTemplateByPath(final String templatePath, Map<String, String> modelTypes) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(resolveTemplate(templatePath), modelTypes);
}
@Override
public Template createTemplate(final URL resource) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(resource, null);
}
public Template createTypeCheckedModelTemplate(final URL resource, Map<String, String> modelTypes) throws CompilationFailedException, ClassNotFoundException, IOException {
return new MarkupTemplateMaker(resource, modelTypes);
}
public GroovyClassLoader getTemplateLoader() {
return groovyClassLoader;
}
public CompilerConfiguration getCompilerConfiguration() {
return compilerConfiguration;
}
public TemplateConfiguration getTemplateConfiguration() {
return templateConfiguration;
}
public URL resolveTemplate(String templatePath) throws IOException {
return templateResolver.resolveTemplate(templatePath);
}
/**
* Implements the {@link groovy.text.Template} interface by caching a compiled template script and keeping a
* reference to the optional map of types of the model elements.
*/
private class MarkupTemplateMaker implements Template {
final Class<BaseTemplate> templateClass;
final Map<String, String> modeltypes;
@SuppressWarnings("unchecked")
public MarkupTemplateMaker(final Reader reader, String sourceName, Map<String, String> modelTypes) {
String name = sourceName != null ? sourceName : "GeneratedMarkupTemplate" + counter.getAndIncrement();
templateClass = groovyClassLoader.parseClass(new GroovyCodeSource(reader, name, ""), modelTypes);
this.modeltypes = modelTypes;
}
@SuppressWarnings("unchecked")
public MarkupTemplateMaker(final URL resource, Map<String, String> modelTypes) throws IOException {
boolean cache = templateConfiguration.isCacheTemplates();
GroovyCodeSource codeSource;
if (cache) {
// we use a map in addition to the internal caching mechanism of Groovy because the latter
// will always read from the URL even if it's cached
String key = resource.toExternalForm();
codeSource = codeSourceCache.get(key);
if (codeSource == null) {
codeSource = new GroovyCodeSource(resource);
codeSourceCache.put(key, codeSource);
}
} else {
codeSource = new GroovyCodeSource(resource);
}
codeSource.setCachable(cache);
templateClass = groovyClassLoader.parseClass(codeSource, modelTypes);
this.modeltypes = modelTypes;
}
public Writable make() {
return make(Collections.emptyMap());
}
public Writable make(final Map binding) {
return DefaultGroovyMethods.newInstance(templateClass, new Object[]{MarkupTemplateEngine.this, binding, modeltypes, templateConfiguration});
}
}
/**
* A specialized GroovyClassLoader which will support passing values to the type checking extension thanks to a
* thread local.
*/
static class TemplateGroovyClassLoader extends GroovyClassLoader {
static final ThreadLocal<Map<String, String>> modelTypes = new ThreadLocal<Map<String, String>>();
public TemplateGroovyClassLoader(final ClassLoader parentLoader, final CompilerConfiguration compilerConfiguration) {
super(parentLoader, compilerConfiguration);
}
public Class parseClass(final GroovyCodeSource codeSource, Map<String, String> hints) throws CompilationFailedException {
modelTypes.set(hints);
try {
return super.parseClass(codeSource);
} finally {
modelTypes.set(null);
}
}
}
public static class TemplateResource {
private final String baseName;
private final String locale;
private final String extension;
public static TemplateResource parse(String fullPath) {
Matcher matcher = LOCALIZED_RESOURCE_PATTERN.matcher(fullPath);
if (!matcher.find()) {
throw new IllegalArgumentException("Illegal template path: " + fullPath);
}
return new TemplateResource(matcher.group(1), matcher.group(2), matcher.group(3));
}
private TemplateResource(final String baseName, final String locale, final String extension) {
this.baseName = baseName;
this.locale = locale;
this.extension = extension;
}
public TemplateResource withLocale(String locale) {
return new TemplateResource(baseName, locale, extension);
}
public String toString() {
return baseName + (locale != null ? "_" + locale : "") + "." + extension;
}
public boolean hasLocale() {
return locale != null && !"".equals(locale);
}
}
public static class DefaultTemplateResolver implements TemplateResolver {
private TemplateConfiguration templateConfiguration;
private ClassLoader templateClassLoader;
public DefaultTemplateResolver() {
}
public void configure(final ClassLoader templateClassLoader, final TemplateConfiguration configuration) {
this.templateClassLoader = templateClassLoader;
this.templateConfiguration = configuration;
}
public URL resolveTemplate(final String templatePath) throws IOException {
MarkupTemplateEngine.TemplateResource templateResource = MarkupTemplateEngine.TemplateResource.parse(templatePath);
String configurationLocale = templateConfiguration.getLocale().toString().replace("-", "_");
URL resource = templateResource.hasLocale() ? templateClassLoader.getResource(templateResource.toString()) : null;
if (resource == null) {
// no explicit locale in the template path or resource not found
// fallback to the default configuration locale
resource = templateClassLoader.getResource(templateResource.withLocale(configurationLocale).toString());
}
if (resource == null) {
// no resource found with the default locale, try without any locale
resource = templateClassLoader.getResource(templateResource.withLocale(null).toString());
}
if (resource == null) {
throw new IOException("Unable to load template:" + templatePath);
}
return resource;
}
}
/**
* A template resolver which avoids calling {@link ClassLoader#getResource(String)} if a template path already has
* been queried before. This improves performance if caching is enabled in the configuration.
*/
public static class CachingTemplateResolver extends DefaultTemplateResolver {
// Those member should stay protected so that subclasses may use different
// cache keys as the ones used by this implementation
protected final Map<String, URL> cache;
protected boolean useCache = false;
/**
* Creates a new caching template resolver. The cache implementation being used depends on
* the use of the template engine. If multiple templates can be rendered in parallel, it <b>must</b>
* be using a thread-safe cache.
* @param cache the backing cache
*/
public CachingTemplateResolver(final Map<String, URL> cache) {
this.cache = cache;
}
/**
* Creates a new caching template resolver using a concurrent hash map as the backing cache.
*/
public CachingTemplateResolver() {
this(new ConcurrentHashMap<String, URL>());
}
@Override
public void configure(final ClassLoader templateClassLoader, final TemplateConfiguration configuration) {
super.configure(templateClassLoader, configuration);
useCache = configuration.isCacheTemplates();
}
@Override
public URL resolveTemplate(final String templatePath) throws IOException {
if (useCache) {
URL cachedURL = cache.get(templatePath);
if (cachedURL!=null) {
return cachedURL;
}
}
URL url = super.resolveTemplate(templatePath);
if (useCache) {
cache.put(templatePath, url);
}
return url;
}
}
}