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 withJStache
.
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:path
which is a classpath with slashes following the same format as the ClassLoader resources. The path maybe augmented withJStachePath
.template
which if not empty is used as the template contents- 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}}
Optional Spec Support
JStachio implements some optional parts of the specification. Below shows what is and is not supported.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 usingMap
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 viaJStacheFormatterTypes
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:
- 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.
- Method with requested name and annotated correctly with
JStacheLambda
and the lookup is for a section than the method lambda method will be used. - 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
- 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.-first
and@first
is boolean that is true when you are on the first item-last
and@last
is a boolean that is true when you are on the last item in the iterable-index
is a one based index. The first item would be1
and not0
@index
is zero based index (handlebars). The first item would be0
.
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 inJStacheType
.
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 viaJStacheFlags
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.2.1
-
Optional Element Summary
-
Field Summary
Modifier and TypeFieldDescriptionstatic final String
An annotation processor compiler flag ("jstache.resourcesPath") that says where the templates files are located.
-
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 insrc/main/resources
orsrc/test/resources
which get copied on build totarget/classes
or similar. However due to incremental compiling template files are not always copied totarget/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.If the path does not start with a path separator then it will be appended to the the current working directory otherwise it is assumed to be a fully qualified path.
The default location is
CWD/src/main/resources
where CWD is the current working directory. 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:
For build annotation processor configuration examples see:jstache.resourcesPath=some/path
- 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:
- ""
-