[MAJOR FEAT] New Processor API
See original GitHub issueThe new engine introduced in Thymeleaf 3.0 (see #389) offers a refactored processor API that allows users to create custom processors, i.e. to apply custom behaviours to their templates.
Processors can be created for each type of event fired by the template engine, whichever the template mode:
- Template start/end
- Element Tags
- Texts
- Comments
- CDATA Sections
- DOCTYPE Clauses
- XML Declarations
- Processing Instructions
And also an additional type of processor, not linked with a specific event, but rather a model (see #389):
- Element Models: sequences of events representing an entire element, i.e. an element with its entire body, including any nested elements or any other kind of artifacts that might appear inside. If the modelled element is a standalone element, the model will only contain its corresponding event; but if the modelled element has a body, the model will contain every event from its open tag to its close tag, both included.
All these types of processors are created by implementing a specific interface, or by extending one of the available abstract implementations. All these artifacts conforming the Thymeleaf 3.0 Processor API live at the org.thymeleaf.processor
package.
The main interface, IProcessor
The org.thymeleaf.processor.IProcessor
is the main interface implemented by all processors in Thymeleaf 3.0. It is quite simple:
public interface IProcessor {
public TemplateMode getTemplateMode();
public int getPrecedence();
}
So every processor instance is associated with a specific template mode, which means that we will need to create different processor objects of the same class if one or several of our processor classes can be applied in more than one template mode.
Also, every processor has a precedence, which will mark the order in which processors will be executed when more than one are found to be applicable to the same event. There is a difference with Thymeleaf 2.1 here, though, as processor precedence in Thymeleaf 3.0 is relative to the owner dialect’s precedence –declared at the dialect interfaces– so that it is possible to declare all the processors in a specific dialect to be executed before or after another the processors of other specific dialect.
Element Processors
Element Tag processors are those that are executed on the open element (IOpenElementTag
) or standalone element (IStandaloneElementTag
) events, normally by means of matching the name of the element (and/or one of its attributes) with a matching configuration specified by the processor. This is what the IElementProcessor
interface looks like:
public interface IElementProcessor extends IProcessor {
public MatchingElementName getMatchingElementName();
public MatchingAttributeName getMatchingAttributeName();
}
Note however that element processor implementations are not meant to directly implement this interface. Instead, element processors should fall into one of two categories, already mentioned above:
- Element Tag Processors, implementing the
IElementTagProcessor
interface. These processors execute on open/standalone tag events only (no processors can be applied to close tags), and have no (direct) access to the element body. - Element Model Processors, implementing the
IElementModelProcessor
interface. These processors execute on complete elements, including their bodies, in the form ofIModel
objects.
We should have a look at each of these interfaces separately:
Element Tag Processors: IElementTagProcessor
Element Tag Processors, as explained, execute on the single open element or standalone element tag that matches its matching configuration (seen in IElementProcessor
). The interface to be implemented is IElementTagProcessor
, which looks like this:
public interface IElementTagProcessor extends IElementProcessor {
public void process(
final ITemplateContext context,
final IProcessableElementTag tag,
final IElementTagStructureHandler structureHandler);
}
As we can see, besides extending IElementProcessor
it only specifies a process(...)
method that will be executed when the matching configuration matches (and in the order established by its precedence, established at the IProcessor
superinterface). The process(...)
signature is quite compact, and follows a pattern found in every Thymeleaf processor interface:
- The
process(...)
method returnsvoid
. Any actions will be performed via thestructureHandler
. - The
context
argument contains the context with which the template is being executed: variables, template data, etc. - The
tag
argument is the event on which the processor is being fired. It contains both the name of the element and its attributes. - The
structureHandler
is a special object that allows the processor to give instructions to the engine about actions that it should perform as a consequence of the execution of the processor.
So we have two ways of performing actions during the execution of these processors:
Using the structureHandler
The tag
argument passed to process(...)
is an immutable object. So there is no way to, for example, directly modify the attributes of a tag on the tag
object itself. Instead, the structureHandler
should be used.
For example, let’s see how we would read the value of a specific tag
attribute, unescape it and keep it in a variable, and then remove the attribute from the tag:
// Obtain the attribute value
String attributeValue = tag.getAttributeValue(attributeName);
// Unescape the attribute value
attributeValue =
EscapedAttributeUtils.unescapeAttribute(context.getTemplateMode(), attributeValue);
// Instruct the structureHandler to remove the attribute from the tag
structureHandler.removeAttribute(attributeName);
... // do something with that attributeValue
Note that the code above is only meant to showcase some attribute management concepts – in most processors we won’t need to do this “get value + unescape + remove” operation manually as it will all be handled by an extended superclass such as AbstractAttributeTagProcessor
.
Above we’ve seen only one of the operations offered by the structureHandler
. Let’s know more.
There is a structure handler for each type of processor in Thymeleaf, and the one for element tag processors implements the IElementTagStructureHandler
interface, which looks like this:
public interface IElementTagStructureHandler {
public void reset();
public void setLocalVariable(final String name, final Object value);
public void removeLocalVariable(final String name);
public void setAttribute(final String attributeName, final String attributeValue);
public void setAttribute(final String attributeName, final String attributeValue,
final AttributeValueQuotes attributeValueQuotes);
public void replaceAttribute(final AttributeName oldAttributeName,
final String attributeName, final String attributeValue);
public void replaceAttribute(final AttributeName oldAttributeName,
final String attributeName, final String attributeValue,
final AttributeValueQuotes attributeValueQuotes);
public void removeAttribute(final String attributeName);
public void removeAttribute(final String prefix, final String name);
public void removeAttribute(final AttributeName attributeName);
public void setSelectionTarget(final Object selectionTarget);
public void setInliner(final IInliner inliner);
public void setTemplateData(final TemplateData templateData);
public void setBody(final String text, final boolean processable);
public void setBody(final IModel model, final boolean processable);
public void insertBefore(final IModel model); // cannot be processable
public void insertImmediatelyAfter(final IModel model, final boolean processable);
public void replaceWith(final String text, final boolean processable);
public void replaceWith(final IModel model, final boolean processable);
public void removeElement();
public void removeTags();
public void removeBody();
public void removeAllButFirstChild();
public void iterateElement(final String iterVariableName,
final String iterStatusVariableName,
final Object iteratedObject);
}
There we can see all the actions that a processor can ask the template engine to do as a result of its execution. The method names are quite self-explanatory (and there is javadoc for them), but very briefly:
setLocalVariable(...)
/removeLocalVariable(...)
will add a local variable to the template execution. This local variable will be accessible during the rest of the execution of the current event, and also during all its body (i.e. until its corresponding close tag)setAttribute(...)
adds a new attribute to the tag with a specified value (and maybe also type of surrounding quotes). If the attribute already exists, its value will be replaced.replaceAttribute(...)
replaces an existing attribute with a new one, taking its place in the attribute (including its surrounding white space, for example).removeAttribute(...)
removes an attribute from the tag.setSelectionTarget(...)
modifies the object that is to be considered the selection target, i.e. the object on which selection expressions (*{...}
) will be executed. In the Standard Dialect, this selection target is usually modified by means of theth:object
attribute, but custom processors can do it too. Note the selection target has the same scope as a local variable, and will therefore be accessible only inside the body of the element being processed.setInliner(...)
modifies the inliner to be used for processing all text nodes (IText
events) appearing in the body of the element being processed. This is the mechanism used by theth:inline
attribute to enable inlining in any of the specified modes (text
,javascript
, etc).setTemplateData(...)
modifies the metadata about the template that is actually being processed. When inserting fragments, this allows the engine to know data about the specific fragment being processed, and also the complete stack of fragments being nested.setBody(...)
replaces all the body of the element being processed with the passed text or model (sequence of events = fragment of markup). This is the way e.g. thatth:text
/th:utext
work. Note that the specified replacement text or model can be set as processable or not, depending on whether we want to execute any processors that might be associated with them. In the case ofth:utext="${var}"
, for example, the replacement is set as non-processable in order to avoid executing any markup that might be returned by${var}
as a part of the template.insertBefore(...)
/insertImmediatelyAfter(...)
allow the specification of a model (fragment of markup) that should appear before or immediately after the tag being processed. Note thatinsertImmediatelyAfter
means after the tag being processed (and therefore as the first part of the element’s body) and not after the entire element that opens here, and closes in a close tag somewhere.replaceWith(...)
allows the current element (entire element) to be replaced with the text or model specified as argument.removeElement()
/removeTags()
/removeBody()
/removeAllButFirstChild()
allow the processor to remove, respectively, the entire element including its body, only the executed tags (open + close) but not the body, only the body but not the wrapping tags, and lastly all the tag’s children except the first child element. Note all these options basically mirror the different values that can be used at theth:remove
attribute.iterateElement(...)
allows the current element (body included) to be iterated as many times as elements exist in theiteratedObject
(which will usually be aCollection
,Map
,Iterator
or an array). The other two arguments will be used for specifying the names of the variables used for the iterated elements and the status variable.
Abstract implementations for IElementTagProcessor
Thymeleaf offers two basic implementations of IElementTagProcessor
that processors might implement for convenience:
org.thymeleaf.processor.element.AbstractElementTagProcessor
, meant for processors that match element events by their element name (i.e. without looking at attributes). Similar to element processors in Thymeleaf 2.1.org.thymeleaf.processor.element.AbstractAttributeTagProcessor
, meant for processors that match element events by one of their attributes (and optionally also the element name). Similar to attribute processors in Thymeleaf 2.1.
Element Model Processors: IElementModelProcessor
Element Model Processors execute on the entire elements they match –including their bodies–, in the form of an IModel
object that contains the complete sequence of events that models such element and its contents. The IElementModelProcessor
is very similar to the one seen above for tag processors:
public interface IElementModelProcessor extends IElementProcessor {
public void process(
final ITemplateContext context,
final IModel model,
final IElementModelStructureHandler structureHandler);
}
Note how this interface also extends IElementProcessor
, and how the process(...)
method it contains follows the same structure as the one in tag processors, replacing tag
with model
of course:
process(...)
returnsvoid
. Actions will be performed onmodel
orstructureHandler
, not by returning anything.context
contains the execution context: variables, etc.model
is the sequence of events modelling the entire element on which the processor is being executed. This model can be directly modified from the processor.structureHandler
allows instructing the engine to perform actions beyond model modification (e.g. setting local variables).
Reading and modifying the model
The IModel
object passed as a parameter to the process()
method is a mutable model, so it allows any modifications to be done on it (models are mutable, events such as tags are immutable). For example, we might want to modify it so that we replace every text node from its body with a comment with the same contents:
final IModelFactory modelFactory = context.getModelFactory();
int n = model.size();
while (n-- != 0) {
final ITemplateEvent event = model.get(n);
if (event instanceof IText) {
final IComment comment =
modelFactory.createComment(((IText)event).getText());
model.insert(n, comment);
model.remove(n + 1);
}
}
Note also that the IModel
interface includes an accept(IModelVisitor visitor)
method, useful for traversing an entire model looking for specific nodes or relevant data the Visitor pattern.
Using the structureHandler
Similarly to tag processors, model processors are passed a structure handler object that allows them to instruct the engine to take any actions that cannot be done by directly acting on the IModel model
object itself. The interface these structure handlers implement, much smaller than the one for tag processors, is IElementModelStructureHandler
:
public interface IElementModelStructureHandler {
public void reset();
public void setLocalVariable(final String name, final Object value);
public void removeLocalVariable(final String name);
public void setSelectionTarget(final Object selectionTarget);
public void setInliner(final IInliner inliner);
public void setTemplateData(final TemplateData templateData);
}
It’s easy to see this is a subset of the one for tag processors. The few methods there work the same way:
setLocalVariable(...)
/removeLocalVariable(...)
for adding/removing local variables that will be available during the model’s execution (after the current processor’s execution).setSelectionTarget(...)
for modifying the selection target applied during the model’s execution.setInliner(...)
for setting an inliner.setTemplateData(...)
for setting metadata about the template being processed.
Abstract implementations for IElementModelProcessor
Thymeleaf offers two basic implementations of IElementModelProcessor
that processors might implement for convenience:
org.thymeleaf.processor.element.AbstractElementModelProcessor
, meant for processors that match element events by their element name (i.e. without looking at attributes). Similar to element processors in Thymeleaf 2.1.org.thymeleaf.processor.element.AbstractAttributeModelProcessor
, meant for processors that match element events by one of their attributes (and optionally also the element name). Similar to attribute processors in Thymeleaf 2.1.
Template start/end Processors: ITemplateBoundariesProcessor
Template Boundaries Processors are a kind of processors that execute on the template start and template end events fired during template processing. They allow to perform any kind of initialization or disposal of resources at beginning or end of the template processing operation. Note that these events are only fired for the first-level template, and not for each of the fragments that might be parsed and/or included into the template being processed.
The ITemplateBoundariesProcessor
interface looks like this:
public interface ITemplateBoundariesProcessor extends IProcessor {
public void processTemplateStart(
final ITemplateContext context,
final ITemplateStart templateStart,
final ITemplateBoundariesStructureHandler structureHandler);
public void processTemplateEnd(
final ITemplateContext context,
final ITemplateEnd templateEnd,
final ITemplateBoundariesStructureHandler structureHandler);
}
This time the interface offers two process*(...)
methods, one for the template start and another one for the template end events. Their signature follows the same patter as the other process(...)
methods we saw before, receiving the context, the event object, and the structure handler. Structure handler that, in this case, implements a quite simple ITemplateBoundariesStructureHandler
interface:
public interface ITemplateBoundariesStructureHandler {
public void reset();
public void setLocalVariable(final String name, final Object value);
public void removeLocalVariable(final String name);
public void setSelectionTarget(final Object selectionTarget);
public void setInliner(final IInliner inliner);
public void insert(final String text, final boolean processable);
public void insert(final IModel model, final boolean processable);
}
We can see how, besides the usual methods for managing local variables, selection target and inliner, we can also use the structure handler for inserting text or a model, which in this case will appear at the very beginning or the very end of the result (depending on the event being processed).
Other processors
There are other events for which Thymeleaf 3.0 allows processors to be declared, each of them implementing their corresponding interface:
- Text events: interface
ITextProcessor
- Comment events: interface
ICommentProcessor
- CDATA Section events: interface
ICDATASectionProcessor
- DOCTYPE Clause events: interface
IDocTypeProcessor
- XML Declaration events: interface
IXMLDeclarationProcessor
- Processing Instruction events: interface
IProcessingInstructionProcessor
All of them look pretty much like this (which is the one for text events):
public interface ITextProcessor extends IProcessor {
public void process(
final ITemplateContext context,
final IText text,
final ITextStructureHandler structureHandler);
}
Same pattern as all other process(...)
methods: context, event, structure handler. And these structure handlers are very simple, just like this (again, the one for text events):
public interface ITextStructureHandler {
public void reset();
public void setText(final CharSequence text);
public void replaceWith(final IModel model, final boolean processable);
public void removeText();
}
Issue Analytics
- State:
- Created 8 years ago
- Comments:5 (2 by maintainers)
Top GitHub Comments
The
IModelFactory.parse(...)
method seems to be what you need. It will turn aString
into anIModel
. There is no way to directly replace the IModel you receive as an argument because it is mutable on purpose: you can just reset it and then executeaddModel()
with the newly created model.I got it,I cloned thymeleaf 3.0.9 version source code, and find class AbstractStandardFragmentInsertionTagProcessor, it has computeFragment method can solve my problem.
Hope can help sb.