Annotation Interface JStache


Generates a JStachio Renderer from a template and a model (the annotated class).

Classes annotated are typically called "models" as they will be the root context for the template.

Contents

Example Usage

 
 @JStache(template = """
     {{#people}}
     {{message}} {{name}}! You are {{#ageInfo}}{{age}}{{/ageInfo}} years old!
     {{#-last}}
     That is all for now!
     {{/-last}}
     {{/people}}
     """)
 public record HelloWorld(String message, List<Person> people) implements AgeLambdaSupport {}

 public record Person(String name, LocalDate birthday) {}

 public record AgeInfo(long age, String date) {}

 public interface AgeLambdaSupport {
   @JStacheLambda
   default AgeInfo ageInfo(
       Person person) {
     long age = ChronoUnit.YEARS.between(person.birthday(), LocalDate.now());
     String date = person.birthday().format(DateTimeFormatter.ISO_DATE);
     return new AgeInfo(age, date);
   }
 }
  

Models and Templates

Because JStachio checks types its best to think of the model and template as married. With the exception of partials JStachio cannot have a template without a model and vice versa. The way to create Renderer (what we call the model and template combined) is to annotate your model with JStache.

Models

@JStache

A JStachio model can be any class type including Records and Enums so long as you can you annotate the type with JStache.

When the compiler runs the annotation processor will create readable java classes that are suffixed with "Renderer" which will have methods to write the model to an Appendable. The generated instance methods are named execute and the corresponding static methods are named render.

TIP: If you like to see the generated classes from the annotation processor they usually get put in target/generated-sources/annotations for Maven projects.

Adding interfaces to models and renderers

@JStacheInterfaces

Java has a huge advantage over JSON and Javascript. You can use interfaces to add additional variables as well as lambda methods (JStacheLambda)! To enforce that certain interfaces are added to models (the ones annotated) and renderers (the generated classes) you can use JStacheInterfaces on packages or the classes themselves.

You can also make generated classes have ElementType.TYPE annotations (see JStacheInterfaces.templateAnnotations()) and extend a class JStacheInterfaces.templateExtends()) as well which maybe useful for integration with other frameworks particularly DI frameworks.

Templates

The format of the templates should by default be Mustache. The syntax is informally explained by the mustache manual and formally explained by the spec. There are some subtle differences in JStachio version of Mustache due to the static nature that are discussed in context lookup. Template finding is as follows:
  1. path which is a classpath with slashes following the same format as the ClassLoader resources. The path maybe augmented with JStachePath.
  2. template which if not empty is used as the template contents
  3. if the above is not set then the name of the class suffixed with ".mustache" is used as the resource.

Inline Templates

template()

Inline templates are pretty straight forward. Just set template() to a literal string. If you go this route it is highly recommend you use the new triple quote string literal for inline templates

Resource Templates

path() and @JStachePath

Resource templates are files that are in the classpath and are more complicated because of lookup resolution.

When the annotation processor runs these files usually are in: javax.tools.StandardLocation#CLASS_OUTPUT and in a Maven or Gradle project they normally would reside in src/main/resources or src/test/resources which get copied on build to target/classes or similar. N.B. becareful not to have resource filtering turned on for mustache templates.

Ideally JStachio would use javax.tools.StandardLocation#SOURCE_PATH to find resource templates but that is currently problematic with incremental compilers such as Eclipse.

Another issue with incremental compiling is that template files are not always copied after being edited to target/classes and thus are not found by the annotation processor. To deal with this issue JStachio during compilation fallsback to direct filesystem access and assumes that your templates are located: CWD/src/main/resources. That location is configurable via the annotation processor option RESOURCES_PATH_OPTION ("jstache.resourcesPath").

Normally you need to specify the full path in path() which is a resource path (and not a file path) as specified by ClassLoader.getResource(String) however you can make path expansion happen with JStachePath which allows you to prefix and suffix the path.

Partials

{{> partial }} and {{< parent }}{{/parent}}

JStachio supports Mustache partials (and parents) and by default works just like template resources such that JStachePath is used for resolution if specified.

You may also remap partial names via JStachePartial to a different location as well as to an inline template (string literal).

Fragments

JStachio allows referencing a subset of a template called "fragments". The subset of a template can be any type of section block and the first one found from top to bottom reading with the matching name is picked. The subset of a resource template is referenced with the URI fragment notation.

For example let us say we have a template like /contacts/details.mustache:


 <html>
     <body>
         <div hx-target="this">
           {{#archive-ui}}
             {{#contact.archived}}
             <button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
             {{/contact.archived}}
             {{^contact.archived}}
             <button hx-delete="/contacts/${contact.id}">Archive</button>
             {{/contact.archived}}
           {{/archive-ui}}
         </div>
         <h3>Contact</h3>
         <p>${contact.email}</p>
     </body>
 </html>
  

 @JStache("/contacts/details.mustache#archive-ui")
 public record ContactDetails(Contact contact){}
  
The effective template for ContactDetails would be:

   {{#contact.archived}}
   <button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
   {{/contact.archived}}
   {{^contact.archived}}
   <button hx-delete="/contacts/${contact.id}">Archive</button>
   {{/contact.archived}}
  
If the fragment start tag is "standalone" and all the content inside the fragment start tag starts with the same whitespace (or more) as the fragment start tag starting whitespace will be stripped from each line of the content. This is to allow a partial references to dictate the indentation based on spec whitespace handling.

Fragment section blocks can be of these type:

  • {{#fragment}}
  • {{$fragment}}
  • {{<fragment}}
  • {{^fragment}}
The semantics of the section block are ignored as well as the rest of the template.

Optional Spec Support

JStachio implements some optional parts of the specification. Below shows what is and is not supported.
Optional Spec Features Table
Name Supported Manual Description
Lambda variables (arity 0) NO An optional part of the specification states that if the final key in the name is a lambda that returns a string, then that string should be rendered as a Mustache template before interpolation. It will be rendered using the default delimiters (see Set Delimiter below) against the current context.
Lambda sections (arity 1) YES An optional part of the specification states that if the final key in the name is a lambda that returns a string, then that string replaces the content of the section. It will be rendered using the same delimiters (see Set Delimiter below) as the original section content. In this way you can implement filters or caching.
Delimiters YES Set Delimiter tags are used to change the tag delimiters for all content following the tag in the current compilation unit. The tag's content MUST be any two non-whitespace sequences (separated by whitespace) EXCEPT an equals sign ('=') followed by the current closing delimiter. Set Delimiter tags SHOULD be treated as standalone when appropriate. (this feature is non-optional in current mustache but some mustaches implemenations treat it as optional)
Dynamic Names NO Partials can be loaded dynamically at runtime using Dynamic Names; an optional part of the Mustache specification which allows to dynamically determine a tag's content at runtime.
Blocks YES A block begins with a dollar and ends with a slash. That is, {{$title}} begins a "title" block and {{/title}} ends it.
Parents YES A parent begins with a less than sign and ends with a slash. That is, {{<article}} begins an "article" parent and {{/article}} ends it.

Context Lookup

JStachio unlike almost all other Mustache implementations does its context lookup statically during compile time. Consequently JStachio pedantically is early bound where as Mustache is traditionally late bound. Most of the time this difference will not manifest itself so long as you avoid using Map in your models.

The other notable difference is JStachio does not like missing variables (a compiler error will happen) where as many Mustache implementations sometimes allow this and will just not output anything. n.b. This doc and various other docs often appears to use the term "variable" and "binding" interchangeable. "variable" however is generally a subset of "binding" and is the leaf nodes of the model tree that are to be outputted like String interpolation. In mustache spec parlance it is the last key (all the way to the right) of a dotted name. Also in some cases this doc calls dotted names "path".

Interpretation of Java-types and values

When some value is null nothing is rendered if it is used as a section. If some value is null and it is used as a variable a null pointer exception will be thrown by default. This is configurable via JStacheFormatterTypes and custom JStacheFormatter.

Boxed and unboxed boolean can be used for mustache-sections. Section is only rendered if value is true.

Optional empty is treated like an empty list or a boolean false. Optional values are always assumed to be non null.

Map<String,?> follow different nesting rules than other types. If you are in a Map nested section the rest of the context is checked before the Map. Once that is done the Map is then checked using Map.get(Object)' where the key is the last part of the dotted name.

Data-binding contexts are nested. Names are looked up in innermost context first. If name is not found in current context, parent context is inspected. This process continues up to root context. In each rendering context name lookup is performed as follows:

  1. Method with requested name is looked up. Method should have no arguments and should throw no checked exceptions. If there is such method it is used to fetch actual data to render. Compile-time error is raised if there is method with given name, but it is not accessible, has parameters or throws checked exceptions.
  2. Method with requested name and annotated correctly with JStacheLambda and the lookup is for a section than the method lambda method will be used.
  3. Method with getter-name for requested name is looked up. (For example, if 'age' is requested, 'getAge' method is looked up.) Method should have no arguments and should throw no checked exceptions. If there is such method it is used to fetch actual data to render. Compile-time error is raised if there is method with such name, but it is not accessible, has parameters or throws checked exceptions
  4. Field with requested name is looked up. Compile-time error is raised if there is field with such name but it's not accessible.

Enum Matching Extension

Basically enums have boolean keys that are the enums name (Enum.name()) that can be used as conditional sections. Assume light is an enum like:
 
 public enum Light {
   RED,
   GREEN,
   YELLOW
 }
  
You can conditinally select on the enum like a pattern match:
 
 {{#light.RED}}
 STOP
 {{/light.RED}}
 {{#light.GREEN}}
 GO
 {{/light.GREEN}}
 {{#light.YELLOW}}
 Proceeed with caution
 {{/light.YELLOW}}
  

Index Extension

JStachio is compatible with both handlebars.js (handlebars.java as well) and JMustache index keys for iterable sections.
  1. "-first" and "@first" is boolean that is true when you are on the first item.
  2. "-last" and "-last" is a boolean that is true when you are on the last item in the iterable
  3. "-index" is a one based index. The first item would be 1 and not 0
  4. "@index" is zero based index (handlebars). The first item would be 0.

Lambda Support

@JStacheLambda

JStachio supports lambda section calls in a similar manner to JMustache. Just tag your methods with JStacheLambda and the returned models will be used to render the contents of the lambda section. The top of the context stack can be passed to the lambda.

Code Generation

@JStacheConfig.type()

JStachio by default reads mustache syntax and generates code that needs the jstachio runtime (io.jstache.jstachio). However it is possible to generate code that does not need the runtime and possibly in the future other syntaxs like Handlebars might be supported.

Generated Renderer Classes

JStachio generates a single class from a mustache template and model (class annotated with JStache) pair. The generated classes are generally called "Renderers" or sometimes "Templates". Depending on which JStache type is picked different methods are generated. The guaranteed generated methods not to change on minor version or less on the renderer classes are discussed in JStacheType.

Zero dependency code generation

@JStacheConfig.type() == JStacheType.STACHE

Zero dependency code generation is useful if you want to avoid coupling your runtime and downstream dependencies with JStachio (including the annotations themselves) as well as minimize the overall footprint and or classes loaded. A common use case would be using jstachio for code generation in an annotation processing library where you want as minimal class path issues as possible.

If this configuration is selected generated code will ONLY have references to stock base JDK module (java.base) classes. However one major caveat is that generated classes will not be reflectively accessible to the JStachio runtime and thus fallback and filtering will not work. Thus in a web framework environment this configuration choice is less desirable.

n.b. as long as the jstachio annotations are not accessed reflectively you do not need the annotation jar in the classpath during runtime thus the annotations jar is effectively an optional compile time dependency.

Formatting variables

JStachio has strict control on what happens when you output a variable (a binding that is not an iterable) like {{variable}} or {{{variable}}}.

Allowed formatting types

@JStacheFormatterTypes

Only a certain set of types are allowed to be formatted and if they are not a compiler error will happen (as in the annotation processor will fail). To understand more about that see JStacheFormatterTypes.

Runtime formatting

@JStacheFormatter and @JStacheConfig.formatter()

Assuming the compiler allowed the variable to be formatted you can control the output via JStacheFormatter and setting JStacheConfig.formatter().

If you are using the JStachio runtime (io.jstach.jstachio) and have JStacheConfig.type() set to JStacheType.JSTACHIO (or UNSPECIFIED aka default) the default formatter will be used (see io.jstach.jstachio.formatters.DefaultFormatter). The default formatter is slightly different than the mustache spec in that it does not allow formatting nulls. If you would like to follow the spec rules where null should be an empty string use io.jstach.jstachio.formatters.SpecFormatter.

Escaping and Content Type

@JStacheContentType, and @JStacheConfig.contentType()

If you are using the JStachio runtime (io.jstach.jstachio) and have JStacheConfig.type() set to JStacheType.JSTACHIO (or UNSPECIFIED aka default) you will get out of the box escaping for HTML (see io.jstach.jstachio.escapers.Html) per the mustache spec.

To disable escaping set JStacheConfig.contentType() to io.jstach.jstachio.escapers.PlainText.

Configuration

@JStacheConfig

You can set global configuration on class, packages and module elements. See JStacheConfig for more details on config resolution. Some configuration is set through compiler flags and annotation processor options. However JStacheConfig unlike compiler flags and annotation processor options are available during runtime through reflective access.

Compiler flags

The compiler has some boolean flags that can be set statically via JStacheFlags as well as through annotation processor options.

Annotation processor options

Some configuration is available as an annotation processor option. Current available options are: The previously mentioned compiler flags are also available as annotation options. The flags are prefixed with "jstache.". For example JStacheFlags.Flag.DEBUG would be:

jstache.debug=true/false.

Configuring options with Maven

Example configuration with Maven:

 <plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <version>3.8.1</version>
     <configuration>
         <source>17</source>
         <target>17</target>
         <annotationProcessorPaths>
             <path>
                 <groupId>io.jstach</groupId>
                 <artifactId>jstachio-apt</artifactId>
                 <version>${io.jstache.version}</version>
             </path>
         </annotationProcessorPaths>
         <compilerArgs>
             <arg>
                 -Ajstache.resourcesPath=src/main/resources
             </arg>
             <arg>
                 -Ajstache.debug=false
             </arg>
         </compilerArgs>
     </configuration>
 </plugin>
 

Configuring options with Gradle

Example configuration with Gradle:
 
 compileJava {
     options.compilerArgs += [
     '-Ajstache.resourcesPath=src/main/resources'
     ]
 }
  
Author:
agentgt
See Also:
See JStachio User Guide 1.4.0-SNAPSHOT
  • Field Details

    • RESOURCES_PATH_OPTION

      An annotation processor compiler flag ("jstache.resourcesPath") that says where the templates files are located.

      When the annotation processor runs these files usually are in: javax.tools.StandardLocation#CLASS_OUTPUT and in a Maven or Gradle project they normally would reside in src/main/resources or src/test/resources which get copied on build to target/classes or similar. However due to incremental compiling template files are not always copied to target/classes and thus are not found by the annotation processor. To deal with this issue JStachio during compilation fallsback to direct filesystem access of the source directory instead of the output (javax.tools.StandardLocation#CLASS_OUTPUT) if the files cannot be found.

      JStachio tries to resolve your project layout and current working directory automatically based on variety of heuristics so that you do not need to use this flag. JStachio may not find the correct source folder for your templates but it usually can find the correct "current working directory" (which will be called "CWD" for the rest of this passage).

      Multiple paths can be passed by comma separating them. The paths are tried in order. If a path does not start with a path separator then it will be appended to the resolved CWD otherwise it is assumed to be a fully qualified path. NOTE the CWD may not be correct!

      The default location is CWD/src/main/resources. Again the CWD may not be resolved correctly so the only guarantee is to give the absolute path if nothing else is working. Please file an issue with info on your build system and IDE if you project is mostly canonical in layout if you are experiencing issues.

      If the option is blank or empty then NO fallback will happen and effectively disables the above behavior. You can change it by passing to the annotation processor a setting for "jstache.resourcesPath" like:
      jstache.resourcesPath=some/path
      For build annotation processor configuration examples see:
      1. Configuring options with Maven
      2. Configuring options with Gradle
      See Also:
    • INCREMENTAL_OPTION

      static final String INCREMENTAL_OPTION
      EXPERIMENTAL: annotation processor compiler flag ("jstache.incremental") that turns on incremental compiling of supported platforms (currently only Gradle). This option may turn off other settings and if it does it will warn you!

      Incremental compiling does not support generating catalogs or Service Provider files (META-INF/services).

      See Also:
    • ROOT_BINDING_NAME

      static final String ROOT_BINDING_NAME
      A virtual variable that is direct analog to handlebars @root. The root model instance is accessible to mustache templates with this global binding name. This allows easier access to the model if deeply nested in a context stack.
      See Also:
    • FIRST_BINDING_NAME

      static final String FIRST_BINDING_NAME
      A handlebars inspired virtual variable that is bound while in list contexts. If on the first element then it is true.
      See Also:
    • FIRST_JMUSTACHE_BINDING_NAME

      A JMustache inspired virtual variable that is bound while in list contexts. If on the first element then it is true.
      See Also:
    • LAST_BINDING_NAME

      static final String LAST_BINDING_NAME
      A handlebars inspired virtual variable that is bound while in list contexts. If on the last element then it is true.
      See Also:
    • LAST_JMUSTACHE_BINDING_NAME

      A JMustache inspired virtual variable that is bound while in list contexts. If on the last element then it is true.
      See Also:
    • INDEX_BINDING_NAME

      static final String INDEX_BINDING_NAME
      A handlebars inspired virtual variable that is bound while in list contexts. A zero based index that is an integer.
      See Also:
    • INDEX_JMUSTACHE_BINDING_NAME

      A JMustache inspired virtual variable that is bound while in list contexts. A one based index that is an integer.
      See Also:
  • Element Details

    • path

      Resource path to template
      Returns:
      Path to mustache template
      See Also:
      Default:
      ""
    • template

      Inline the template as a Java string instead of a file. Use the new triple quote string literal for complex templates.
      Returns:
      An inline template
      Default:
      ""
    • name

      Name of generated class.

      name can be omitted. model.getClass().getName() + JStacheName.DEFAULT_SUFFIX name is used by default.

      Returns:
      Name of generated class
      Default:
      ""