question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[MAJOR FEAT] New Processor API

See original GitHub issue

The 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 of IModel 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 returns void. Any actions will be performed via the structureHandler.
  • 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 the th: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 the th: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. that th: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 of th: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 that insertImmediatelyAfter 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 the th:remove attribute.
  • iterateElement(...) allows the current element (body included) to be iterated as many times as elements exist in the iteratedObject (which will usually be a Collection, 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(...) returns void. Actions will be performed on model or structureHandler, 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:closed
  • Created 8 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

2reactions
danielfernandezcommented, Apr 22, 2016

The IModelFactory.parse(...) method seems to be what you need. It will turn a String into an IModel. 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 execute addModel() with the newly created model.

0reactions
0nebeancommented, May 7, 2018

I got it,I cloned thymeleaf 3.0.9 version source code, and find class AbstractStandardFragmentInsertionTagProcessor, it has computeFragment method can solve my problem.

        Object templFragmentObj = computeFragment(context, "~{/public/orgTree :: orgTreeTips}");
        final IModel treeTempl = modelFactory.parse(context.getTemplateData(),templFragmentObj.toString());

Hope can help sb.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Kafka Streams Processor API | Confluent Documentation
The Processor API allows developers to define and connect custom processors and to interact with state stores. With the Processor API, you can...
Read more >
Beyond the DSL—Unlocking the power of Kafka Streams with ...
Many people are unaware of the Processor API (PAPI) - or are intimidated by it ... as well as opportunities for major latency...
Read more >
Processing — sagemaker 2.121.2 documentation
Starts a new processing job using the provided inputs and outputs. ... The Amazon SageMaker training jobs and APIs that create Amazon SageMaker...
Read more >
Processor API - Apache Kafka
The Processor API allows developers to define and connect custom processors and to interact with state stores. With the Processor API, you can...
Read more >
Complex Event Processing on top of Kafka Streams - GitHub
Complex Event Processing on top of Kafka Streams Processor API ! ... toSysOut()); KafkaStreams kafkaStreams = new KafkaStreams(builder.build(), ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found