Thymeleafを拡張して独自タグを実装する（その1）

今回はProcessorを自作してThymeleafを拡張する方法をまとめます。
Processorは色々拡張方法があるので、今回は自作のタグを作成する方法をまとめます。
（\<th:block\>タグみたいなやつです。）

UtilityObjectを自作して拡張する方法は以下を参照してください。
[Thymeleafを拡張する（UtilityObject編）](https://qiita.com/d-yosh/items/edf6ac4e19a7f967a058)

# 環境

- Thymeleaf 3.0.8
- Spring 4.3.5

# 自作するタグの挙動
TERASOLUNA Frameworkのt:messagesPanelタグを簡素化したものを作成してみます。
詳しい挙動は以下の通りです。

- タグは\<sample:messagesPanel /\>とする。
- Modelに"messageList"というkeyで格納されたList\<String\>の中身を画面に出力する。
- デフォルトは外側にulタグ、内側にliタグを使って、以下のように出力される。

```html
<ul>
  <li>メッセージ1</li>
  <li>メッセージ2</li>
</ul>
```

- タグにouterElement、innerElement属性でタグ名を指定すると、それを使って出力する。

```html
<sample:messagesPanel outerElement="div" innerElement="span" />
↓
<div>
  <span>メッセージ1</span>
  <span>メッセージ1</span>
</div>
```

# Processorクラスを作成する
まずは、核となるProcessorクラスを作成していきます。
今回はタグを作りたいので、AbstractElementTagProcessorを継承したクラスを作成します。
（StandardBlockTagProcessorを参考に作成しています。）

## まずはコンストラクタ

```java
public class MessagesPanelTagProcessor extends AbstractElementTagProcessor {

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

  // ～省略～

}
```

親クラスのコンストラクタを呼び出します。引数の意味は以下の通りです。

- 第1引数はテンプレートモードを指定します。今回はHTMLです。
- 第2引数はprefixを指定します。Dialectクラスから指定できるようにしています。（今回はsampleにする予定）
- 第3引数はタグの要素名を指定します。今回はmessagesPanelにします。
- 第4引数はタグ名にprefixを利用するかどうかを指定します。引数がnullであればfalse、そうでなければtrueにします。（今回はtrueになる予定）
- 第5引数は属性名を指定します。今回は属性は指定したくないのでnullにします。
- 第6引数は属性名にprefixを利用するかどうかを指定します。今回は属性を指定していないので、falseにしておきます。
- 第7引数は優先度を指定します。ここがいまいちわかっていないのですが、とりあえず10000にしてみました。

## つづいてdoProcessメソッド
このメソッドで具体的な処理を行います。

```java
public class MessagesPanelTagProcessor extends AbstractElementTagProcessor {

  // ～省略～

  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);
  }
}
```
| 項番 | 説明 |
|:--:|:--------------|
| (1) | RequestContextを取得することによって、そこからModelを取得することができます。 |
| (2) | タグを削除するためには、引数のIElementTagStructureHandlerのremoveElementメソッドを利用します。|
| (3) | タグの属性に指定された値を取得するには引数のIProcessableElementTagのgetAttributeValueメソッドを利用します。|
| (4) | IModelFactoryを利用して要素を作成し、IModelに追加していきます。createOpenElementTagメソッドはいくつか種類があり、属性を指定してタグを作成することもできます。任意のclassなどを指定したい場合は、これを利用することで作成できます。|
| (5) | replaceWithメソッドを用いて、もともと書いてある\<sample:messagesPanel /\>タグをこれまでに作成した要素に置き換えます。|

**※追記※**
どうやらIModelFactory.createTextメソッドは、引数で与えられた文字列にHTMLのタグが含まれる場合、そのまま表示してしまうようです。
Thymeleafのth:textに該当するStandardTextTagProcessorでは、[unbescape](http://www.unbescape.org/)というライブラリを用いてエスケープ処理を行っているため、同様のライブラリを用いてエスケープ処理を行うように上記サンプルを修正しましています。


# Dialectクラスを作成する
UtilityObjectを自作したときと同様にDialectクラスを作成する必要があります。
すでにDialectクラスを作成している場合はそれに追加する形でもよいです。

Processorを追加した場合はIProcessorDialectインターフェースを実装する必要がありますが、それを実装したAbstractProcessorDialectという抽象クラスがあるので、今回はこれを継承します。

```java
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;
    }
}
```

- 親コンストラクタの引数は順番に、Dialectの名称、DialectのPrefix、優先度となっています。  
今回はsampleというPrefixを指定しています。
- getProcessorsメソッドで自作したProcessorクラスをSetで返却します。

# Dialectを登録する
こちらもUtilityObjectで実施したのと同様のことを行います。以下を参照してください。
https://qiita.com/d-yosh/items/edf6ac4e19a7f967a058#dialect%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B

# 動かしてみる
以下のようなControllerとhtmlを作成して動作を確認しました。

```java
@Controller
public class HelloController {

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

        model.addAttribute("messageList", Arrays.asList("メッセージ1","メッセージ2"));
        return "home";
    }
}
```

```html
<!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>
```

以下のように出力されているはずです。

```html
<html><head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
  <h1>Hello world!</h1>
  <ul>
    <li>メッセージ1</li>
    <li>メッセージ2</li>
  </ul>
  <div>
    <span>メッセージ1</span>
    <span>メッセージ2</span>
  </div>
</body>
</html>
```

# ハマったところ
## Modelを取得する方法がわからなかった。  
とりあえず、デバッグして引数の中身を確認していたらRequestContextを取得できそうだったので、そこから取得するようにしてみました。
もしかしたら、他に良い方法があるのかもしれないです・・・。

## setBodyしたら最初のタグが残ったままだった
IElementTagStructureHandlerにsetBodyというメソッドがあるので、これを使えばいいのだろうと思っていました。
そうすると\<sample:messagesPanel /\>というタグも一緒に表示されてしまいました。
調べたところ、removeTagsというメソッドがあるので、これを利用すれば\<sample:messagesPanel /\>が消えるはず、と思っていたら今度はBodyに設定した内容が表示されませんでした。
こいつらの実装クラスを確認してみたところ、おおむねどのメソッドもresetAllButVariablesOrAttributesというメソッドを呼び出していて、こいつが、設定情報を初期状態に戻していました。
最初のタグを生かしたままにしておきたい場合はsetBody、最初のタグを消したいならreplaceWithという使い分けがいいのかなと思います。