All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gg.jte.TemplateEngine Maven / Gradle / Ivy

There is a newer version: 3.1.12
Show newest version
package gg.jte;

import gg.jte.html.HtmlPolicy;
import gg.jte.html.HtmlTemplateOutput;
import gg.jte.html.OwaspHtmlTemplateOutput;
import gg.jte.html.HtmlInterceptor;
import gg.jte.runtime.*;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * jte is a simple, yet powerful template engine for Java.
 * All jte templates are compiled to Java class files, meaning jte adds essentially zero overhead to your application.
 * jte is designed to introduce as few new keywords as possible and builds upon existing Java features,
 * so that it is very easy to reason about what a template does.
 * 
* Read more at the official documentation */ public final class TemplateEngine { private final TemplateLoader templateLoader; private final TemplateMode templateMode; private final ConcurrentHashMap templateCache; private final TemplateConfig config; private final ContentType contentType; private final Path classDirectory; private final ClassLoader parentClassLoader; private HtmlInterceptor htmlInterceptor; /** * Creates a new template engine. * All templates are compiled to Java class files on demand. * A JDK is required. * Every template has its own class loader. * This is recommended when running templates on your developer machine. * * @param codeResolver to lookup jte templates * @param contentType the content type of all templates this engine manages * @return a fresh TemplateEngine instance */ public static TemplateEngine create(CodeResolver codeResolver, ContentType contentType) { return create(codeResolver, Paths.get("jte-classes"), contentType); } /** * Creates a new template engine. * All templates are compiled to Java class files on demand. * A JDK is required. * Every template has its own class loader. * This is recommended when running templates on your developer machine. * * @param codeResolver to lookup jte templates * @param classDirectory where template class files are compiled to * @param contentType the content type of all templates this engine manages * @return a fresh TemplateEngine instance */ public static TemplateEngine create(CodeResolver codeResolver, Path classDirectory, ContentType contentType) { return create(codeResolver, classDirectory, contentType, null); } /** * Creates a new template engine. * All templates are compiled to Java class files on demand. * A JDK is required. * Every template has its own class loader. * This is recommended when running templates on your developer machine. * * @param codeResolver to lookup jte templates * @param classDirectory where template class files are compiled to * @param contentType the content type of all templates this engine manages * @param parentClassLoader the parent classloader to use, or null to use the application class loader as parent * @return a fresh TemplateEngine instance */ public static TemplateEngine create(CodeResolver codeResolver, Path classDirectory, ContentType contentType, ClassLoader parentClassLoader) { return create(codeResolver, classDirectory, contentType, parentClassLoader, Constants.PACKAGE_NAME_ON_DEMAND); } /** * Creates a new template engine. * All templates are compiled to Java class files on demand. * A JDK is required. * Every template has its own class loader. * This is recommended when running templates on your developer machine. * * @param codeResolver to lookup jte templates * @param classDirectory where template class files are compiled to * @param contentType the content type of all templates this engine manages * @param parentClassLoader the parent classloader to use, or null to use the application class loader as parent * @param packageName the package name, where template classes are generated to * @return a fresh TemplateEngine instance */ public static TemplateEngine create(CodeResolver codeResolver, Path classDirectory, ContentType contentType, ClassLoader parentClassLoader, String packageName) { return new TemplateEngine(codeResolver, classDirectory, contentType, TemplateMode.OnDemand, parentClassLoader, packageName); } /** * Creates a new template engine. * All templates must have been precompiled to Java class files already. * The template engine will load them from the specified classDirectory. * No JDK is required. * All templates share one class loader with each other. * This is recommended when running templates in production. * How to precompile templates. * * @param classDirectory where template class files are located * @param contentType the content type of all templates this engine manages * @return a fresh TemplateEngine instance */ public static TemplateEngine createPrecompiled(Path classDirectory, ContentType contentType) { return createPrecompiled(classDirectory, contentType, null); } /** * Creates a new template engine. * All templates must have been precompiled to Java class files already. * The template engine will load them via the application class loader. * This means all template classes must be bundled in you application JAR file. * No JDK is required. * This is recommended when running templates in production. * How to precompile templates. * * @param contentType the content type of all templates this engine manages * @return a fresh TemplateEngine instance */ public static TemplateEngine createPrecompiled(ContentType contentType) { return createPrecompiled(null, contentType); } /** * Creates a new template engine. * All templates must have been precompiled to Java class files already. * The template engine will load them from the specified classDirectory. * No JDK is required. * All templates share one class loader with each other. * This is recommended when running templates in production. * How to precompile templates. * * @param classDirectory where template class files are located * @param contentType the content type of all templates this engine manages * @param parentClassLoader the parent classloader to use, or null to use the application class loader as parent (only has an effect if classDirectory is not null) * @return a fresh TemplateEngine instance */ public static TemplateEngine createPrecompiled(Path classDirectory, ContentType contentType, ClassLoader parentClassLoader) { return createPrecompiled(classDirectory, contentType, parentClassLoader, Constants.PACKAGE_NAME_PRECOMPILED); } /** * Creates a new template engine. * All templates must have been precompiled to Java class files already. * The template engine will load them from the specified classDirectory. * No JDK is required. * All templates share one class loader with each other. * This is recommended when running templates in production. * How to precompile templates. * * @param classDirectory where template class files are located * @param contentType the content type of all templates this engine manages * @param parentClassLoader the parent classloader to use, or null to use the application class loader as parent (only has an effect if classDirectory is not null) * @param packageName the package name, where template classes are generated to * @return a fresh TemplateEngine instance */ public static TemplateEngine createPrecompiled(Path classDirectory, ContentType contentType, ClassLoader parentClassLoader, String packageName) { return new TemplateEngine(null, classDirectory, contentType, TemplateMode.Precompiled, parentClassLoader, packageName); } private TemplateEngine(CodeResolver codeResolver, Path classDirectory, ContentType contentType, TemplateMode templateMode, ClassLoader parentClassLoader, String packageName) { if (contentType == null) { throw new NullPointerException("Content type must be specified."); } this.config = new TemplateConfig(contentType, packageName); this.templateLoader = createTemplateLoader(config, codeResolver, classDirectory, templateMode, parentClassLoader); this.templateMode = templateMode; this.templateCache = new ConcurrentHashMap<>(); this.contentType = contentType; this.classDirectory = classDirectory; this.parentClassLoader = parentClassLoader; if (templateMode == TemplateMode.OnDemand) { cleanAll(); } } private static TemplateLoader createTemplateLoader(TemplateConfig config, CodeResolver codeResolver, Path classDirectory, TemplateMode templateMode, ClassLoader parentClassLoader) { if (templateMode == TemplateMode.Precompiled) { return new RuntimeTemplateLoader(classDirectory, parentClassLoader, config.packageName); } else { try { Class compilerClass = Class.forName("gg.jte.compiler.TemplateCompiler"); return (TemplateLoader)compilerClass.getConstructor(TemplateConfig.class, CodeResolver.class, Path.class, ClassLoader.class).newInstance(config, codeResolver, classDirectory, parentClassLoader); } catch (Exception e) { throw new TemplateException("TemplateCompiler could not be located. Maybe jte isn't on your classpath?", e); } } } /** * Renders the template with the given name. * It is preferred to use this method, if all your templates have exactly one parameter. * @param name the template name relative to the specified root directory, for instance "pages/welcome.jte". * @param param the param passed to the template. * @param output any implementation of {@link TemplateOutput}, where the template will be written to. * @throws TemplateException in case the template failed to render, containing information where the error happened. */ public void render(String name, Object param, TemplateOutput output) throws TemplateException { Template template = resolveTemplate(name); try { template.render(checkOutput(output), htmlInterceptor, param); } catch (Throwable e) { throw handleRenderException(name, template, e); } } /** * Renders the template with the given name. * Parameters in the params map are mapped to the corresponding parameters in the template. * Template parameters with a default value don't have to be provided in the map. * @param name the template name relative to the specified root directory, for instance "pages/welcome.jte". * @param params the parameters passed to the template as key value pairs. * @param output any implementation of {@link TemplateOutput}, where the template will be written to. * @throws TemplateException in case the template failed to render, containing information where the error happened. */ public void render(String name, Map params, TemplateOutput output) throws TemplateException { Template template = resolveTemplate(name); try { template.renderMap(checkOutput(output), htmlInterceptor, params); } catch (Throwable e) { throw handleRenderException(name, template, e); } } private TemplateOutput checkOutput(TemplateOutput templateOutput) { if (contentType == ContentType.Html && !(templateOutput instanceof HtmlTemplateOutput)) { return new OwaspHtmlTemplateOutput(templateOutput); } return templateOutput; } private TemplateException handleRenderException(String name, Template template, Throwable e) { if (e instanceof TemplateException) { return (TemplateException)e; } ClassLoader classLoader = template.getClassLoader(); StackTraceElement[] stackTrace = e.getStackTrace(); DebugInfo debugInfo = templateLoader.resolveDebugInfo(classLoader, stackTrace); String message = "Failed to render " + name; if (debugInfo != null) { message += ", error at " + debugInfo.name + ":" + debugInfo.line; templateLoader.rewriteStackTrace(e, classLoader, stackTrace); } return new TemplateException(message, e); } public boolean hasTemplate(String name) { try { resolveTemplate(name); return true; } catch (TemplateNotFoundException e) { return false; } } public List getTemplatesUsing(String name) { return templateLoader.getTemplatesUsing(name); } /** * Obtain parameter information about a specific template. * @param name the template name relative to the specified root directory, for instance "my/example.jte". * @return a map containing all template parameters names and their classes * @throws TemplateException in case parameter information is not available (jte classes must be compiled with -parameters compiler flag.) */ public Map> getParamInfo(String name) throws TemplateException { return resolveTemplate(name).getParamInfo(); } /** * Prepares the template with the given name for rendering * @param name the template name relative to the specified root directory, for instance "pages/welcome.jte". */ public void prepareForRendering(String name) { resolveTemplate(name); } /** * Cleans the directory containing the compiled template classes. */ public void cleanAll() { templateLoader.cleanAll(); } /** * Generates all template classes in the sources directory, to the compiled template classes directory. * This only generates .java files, not .class files. * @return list of .java template files that were generated */ public List generateAll() { return templateLoader.generateAll(); } /** * Compiles all templates located in the sources directory, to the compiled template classes directory. * @return list of .java template files that were compiled */ public List precompileAll() { return templateLoader.precompileAll(); } /** * Compiles all templates located in the sources directory, to the compiled template classes directory. * @param classPath additional class path arguments for the Java compiler. See also {@link TemplateEngine#setClassPath(List)} * @return list of .java template files that were compiled */ public List precompileAll(List classPath) { setClassPath(classPath); return templateLoader.precompileAll(); } /** * Clears all cached templates. * Future invocations of render methods will result in reloaded template classes. */ public void clearCache() { templateCache.clear(); } private Template resolveTemplate(String name) { if (templateMode == TemplateMode.OnDemand) { return resolveTemplateOnDemand(name); } else { return templateCache.computeIfAbsent(name, templateLoader::load); } } private Template resolveTemplateOnDemand(String name) { RuntimeException[] exception = {null}; // We use the guarantees of ConcurrentHashMap#compute to avoid synchronized with double hasChanged() check Template result = templateCache.compute(name, (templateName, template) -> { if (template != null && !templateLoader.hasChanged(templateName)) { return template; } try { if (template == null) { return templateLoader.load(templateName); } else { return templateLoader.hotReload(templateName); } } catch (RuntimeException e) { exception[0] = e; // Store exception to throw later return null; // null removes the template from the template cache } }); if (exception[0] != null) { throw exception[0]; } return result; } /** * Useful, if this engine is in precompiled mode (probably production) but you still want to be able to apply a hotfix without deployment. * This template engine will be entirely unaffected by this call. Instead, a fresh template engine will be created. * When this call succeeds, you can safely switch the template engine reference to the new instance. The old instance including all * old templates should then be subject to garbage collection. *
* This only works if you're running on the JDK and if templates have their own classloader. * * @param precompiler a template engine that is configured exactly as you usually would precompile your templates. * @return a fresh template engine with a warmed up cache. * @throws TemplateException in case there was a compilation error, in this case you should keep the current engine running! */ public TemplateEngine reloadPrecompiled(TemplateEngine precompiler) throws TemplateException { precompiler.precompileAll(); return reloadPrecompiled(precompiler.classDirectory); } /** * Useful, if this engine is in precompiled mode (probably production) but you still want to be able to apply a hotfix without deployment. * This template engine will be entirely unaffected by this call. Instead, a fresh template engine will be created. * When this call succeeds, you can safely switch the template engine reference to the new instance. The old instance including all * old templates should then be subject to garbage collection. *
* Basically you could recompile all templates on your build server, upload them to your production server * and call this method afterwards. *
* This only works if templates have their own classloader. * * @param classDirectory the class directory to load the new templates from. * @return a fresh template engine with a warmed up cache. * @throws TemplateException in case there was an error during class loading, in this case you should keep the current engine running! */ public TemplateEngine reloadPrecompiled(Path classDirectory) throws TemplateException { TemplateEngine engine = createPrecompiled(classDirectory, contentType, parentClassLoader, config.packageName); engine.setHtmlInterceptor(htmlInterceptor); Set templates = new HashSet<>(templateCache.keySet()); for (String templateName : templates) { engine.prepareForRendering(templateName); } return engine; } /** * Sets additional compiler arguments for jte templates. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param compileArgs for instance templateEngine.setCompileArgs("--enable-preview", "--release", "" + Runtime.version().feature()); */ public void setCompileArgs(String ... compileArgs) { config.compileArgs = compileArgs; } /** * Sets additional compiler arguments for kte templates. *
* This is a template compilation setting and has no effect when loading precompiled templates. * Currently, only -jvm-target is supported. * @param compileArgs for instance templateEngine.setCompileArgs("-jvm-target", "17"); */ public void setKotlinCompileArgs(String ... compileArgs) { config.kotlinCompileArgs = compileArgs; } /** * Trims control structures, resulting in prettier output. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param value true, to enable */ public void setTrimControlStructures(boolean value) { config.trimControlStructures = value; } /** * Policy that checks the parsed HTML at compile time. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param htmlPolicy the policy * @throws NullPointerException if policy is null */ public void setHtmlPolicy(HtmlPolicy htmlPolicy) { if (htmlPolicy == null) { throw new NullPointerException("htmlPolicy must not be null"); } config.htmlPolicy = htmlPolicy; } /** * Intercepts the given html tags during template compilation * and calls the configured htmlInterceptor during template rendering. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param htmlTags tags to be intercepted, for instance setHtmlTags("form", "input"); */ public void setHtmlTags(String ... htmlTags) { config.htmlTags = htmlTags; } /** * Interceptor that is called during template rendering when one of the * configured htmlTags is rendered. * This allows to integrate existing frameworks into jte. * @param htmlInterceptor the interceptor */ public void setHtmlInterceptor(HtmlInterceptor htmlInterceptor) { this.htmlInterceptor = htmlInterceptor; } /** * By default, jte omits all HTML/CSS/JS comments, when compiling with {@link ContentType#Html}. * If you don't want this behavior, you can disable it here. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param htmlCommentsPreserved true, to preserve HTML comments in templates */ public void setHtmlCommentsPreserved(boolean htmlCommentsPreserved) { config.htmlCommentsPreserved = htmlCommentsPreserved; } /** * Setting, that UTF-8 encodes all static template parts at compile time. * Only makes sense if you use a binary output, like {@link gg.jte.output.Utf8ByteOutput}. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param binaryStaticContent true, to pre-generate UTF-8 encoded byte arrays for all static template parts */ public void setBinaryStaticContent(boolean binaryStaticContent) { config.binaryStaticContent = binaryStaticContent; } /** * The class path used for compiling templates. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param classPath list of elements on the class path */ public void setClassPath(List classPath) { config.classPath = classPath; } /** * Directory in which to generate non-java files (resources). Typically, set by plugin rather than end user. * Optional - if null, resources will not be generated *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param targetResourceDirectory directory to generate resources in */ public void setTargetResourceDirectory(Path targetResourceDirectory) { config.resourceDirectory = targetResourceDirectory; } /** * "group/artifact" of the project using jte. Typically, set by plugin rather than end user. * If null, the compiler will make one up. *
* This is a template compilation setting and has no effect when loading precompiled templates. * @param projectNamespace "groupId/artifactId" */ public void setProjectNamespace(String projectNamespace) { config.projectNamespace = projectNamespace; } /** * Optional - Extensions this template engine should load. Currently, the following extensions exist: * *
    *
  • gg.jte.models.generator.ModelExtension
  • *
  • gg.jte.nativeimage.NativeResourcesExtension
  • *
* * Sample usage: * * * templateEngine.setExtensions(Map.of("gg.jte.models.generator.ModelExtension", Map.of())); * */ public void setExtensions(Map> extensionSettings) { config.extensionClasses.putAll(extensionSettings); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy