[JAVA] Extend Thymeleaf to implement unique tags (Part 1)

This time I will summarize how to make your own Processor and extend Thymeleaf. There are various extension methods for Processor, so this time I will summarize how to create your own tags. (It's like a \ <th: block > tag.)

See below for how to create and extend a UtilityObject yourself. Extend Thymeleaf (UtilityObject)

environment

Behavior of self-made tags

Let's create a simplified version of the T: messagesPanel tag of the TERASOLUNA Framework. The detailed behavior is as follows.

--The tag is \ <sample: messagesPanel / >. --Output the contents of List \ <String > stored in Model with the key "messageList" to the screen. --The default is to use the ul tag on the outside and the li tag on the inside, and the output is as follows.

<ul>
  <li>Message 1</li>
  <li>Message 2</li>
</ul>

--If you specify the tag name in the outerElement and innerElement attributes for the tag, it will be used for output.

<sample:messagesPanel outerElement="div" innerElement="span" />
↓
<div>
  <span>Message 1</span>
  <span>Message 1</span>
</div>

Create a Processor class

First, we will create the core Processor class. Since we want to create a tag this time, we will create a class that inherits AbstractElementTagProcessor. (Created with reference to StandardBlockTagProcessor.)

First of all, the constructor

public class MessagesPanelTagProcessor extends AbstractElementTagProcessor {

  public MessagesPanelTagProcessor(String dialectPrefix){
    super(TemplateMode.HTML, dialectPrefix, "messagesPanel", dialectPrefix != null, null, false, 100000);
  }

  //~ Omitted ~

}

Call the constructor of the parent class. The meanings of the arguments are as follows.

--The first argument specifies the template mode. This time it's HTML. --Specify prefix as the second argument. It can be specified from the Dialect class. (This time it will be a sample) --The third argument specifies the element name of the tag. This time, it will be messagesPanel. --The 4th argument specifies whether to use prefix for the tag name. Set to false if the argument is null, true otherwise. (This time it will be true) --The fifth argument specifies the attribute name. I don't want to specify the attribute this time, so set it to null. --The 6th argument specifies whether to use prefix for the attribute name. Since the attribute is not specified this time, leave it as false. --The 7th argument specifies the priority. I'm not sure about this, but I set it to 10000 for the time being.

Then doProcess method

Use this method for specific processing.

public class MessagesPanelTagProcessor extends AbstractElementTagProcessor {

  //~ Omitted ~

  protected void doProcess(ITemplateContext context, IProcessableElementTag tag, IElementTagStructureHandler structureHandler) {

    // (1)
    RequestContext requestContext =
                (RequestContext) context
                        .getVariable(SpringContextVariableNames.SPRING_REQUEST_CONTEXT);

    Map<String, Object> model = requestContext.getModel();
    Object messages = model.get("messageList");
    
    if (messages == null) {
      // (2)
      structureHandler.removeElement();
      return;
    }

    // (3)
    List<String> messageList = (List<String>) model.get("messageList");
    String outerElement =
          tag.getAttributeValue("outerElement") == null ? "ul" : tag
                    .getAttributeValue("outerElement");
    String innerElement =
            tag.getAttributeValue("innerElement") == null ? "li" : tag
                    .getAttributeValue("innerElement");
    
    // (4)
    IModelFactory modelFactory = context.getModelFactory();
    IModel iModel = modelFactory.createModel();
    iModel.add(modelFactory.createOpenElementTag(outerElement));
    for (String message : messageList) {
        iModel.add(modelFactory.createOpenElementTag(innerElement));
        iModel.add(modelFactory.createText(HtmlEscape.escapeHtml4Xml(message)));
        iModel.add(modelFactory.createCloseElementTag(innerElement));
    }
    iModel.add(modelFactory.createCloseElementTag(outerElement));

    // (5)
    structureHandler.replaceWith(iModel, false);
  }
}
Item number Description
(1) By getting the RequestContext, you can get the Model from it.
(2) To remove the tag, use the removeElement method of the argument IElementTagStructureHandler.
(3) To get the value specified for the attribute of the tag, use the getAttributeValue method of the argument IProcessableElementTag.
(4) Create an element using IModelFactory and add it to IModel. There are several types of createOpenElementTag methods, and you can also create tags by specifying attributes. If you want to specify an arbitrary class etc., you can create it by using this.
(5) Originally written using the replaceWith method<sample:messagesPanel />Replace the tag with the element you created so far.

** * Addition *** Apparently, the IModelFactory.createText method will display the HTML tag as it is if the string given as an argument contains an HTML tag. In StandardTextTagProcessor corresponding to th: text of Thymeleaf, escape processing is performed using a library called unbescape, so escape processing should be performed using a similar library. The above sample has been modified.

Create a Dialect class

You need to create the Dialect class as you did when you created the UtilityObject yourself. If you have already created a Dialect class, you can add it to it.

If you add a Processor, you need to implement the IProcessorDialect interface, but since there is an abstract class called AbstractProcessorDialect that implements it, this time we will inherit it.

public class SampleDialect extends AbstractProcessorDialect {

    public SampleDialect() {
        super("sample", "sample", 1000);
    }

    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        Set<IProcessor> processors = new HashSet<>();
        processors.add(new MessagesPanelTagProcessor(dialectPrefix));
        return processors;
    }
}

--The arguments of the parent constructor are, in order, Dialect name, Dialect Prefix, and priority. This time, a prefix called sample is specified. --Return the Processor class created by the getProcessors method with Set.

Register Dialect

This also does the same thing that we did with the Utility Object. Please refer to the following. https://qiita.com/d-yosh/items/edf6ac4e19a7f967a058#dialect%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B

Try to move

I created the following Controller and html and confirmed the operation.

@Controller
public class HelloController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Model model) {

        model.addAttribute("messageList", Arrays.asList("Message 1","Message 2"));
        return "home";
    }
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
  <h1>Hello world!</h1>
  <hoge:messagesPanel />
  <hoge:messagesPanel outerElement="div" innerElement="span"/>
</body>
</html>

The output should look like this:

<html><head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
  <h1>Hello world!</h1>
  <ul>
    <li>Message 1</li>
    <li>Message 2</li>
  </ul>
  <div>
    <span>Message 1</span>
    <span>Message 2</span>
  </div>
</body>
</html>

Where I was addicted

I didn't know how to get the Model.

For the time being, if I debugged and checked the contents of the argument, it seemed that I could get the RequestContext, so I tried to get it from there. Maybe there is another good way ...

After setBody, the first tag remained

IElementTagStructureHandler has a method called setBody, so I was wondering if I should use this. Then, the tag \ <sample: messagesPanel / > was also displayed. When I checked, there was a method called removeTags, so I thought that \ <sample: messagesPanel / > should disappear if I used this, but this time the contents set in Body were not displayed. When I checked their implementation classes, almost every method called a method called resetAllButVariablesOrAttributes, and this one returned the setting information to the initial state. If you want to keep the first tag alive, use setBody, and if you want to delete the first tag, use replaceWith.

Recommended Posts

Extend Thymeleaf to implement unique tags (Part 1)
Extend Thymeleaf (UtilityObject)
Introduction to Ratpack (9) --Thymeleaf
I want to implement a product information editing function ~ part1 ~