今回はProcessorを自作してThymeleafを拡張する方法をまとめます。
Processorは色々拡張方法があるので、今回は自作のタグを作成する方法をまとめます。
(<th:block>タグみたいなやつです。)
UtilityObjectを自作して拡張する方法は以下を参照してください。
Thymeleafを拡張する(UtilityObject編)
環境
- Thymeleaf 3.0.8
- Spring 4.3.5
自作するタグの挙動
TERASOLUNA Frameworkのt:messagesPanelタグを簡素化したものを作成してみます。
詳しい挙動は以下の通りです。
- タグは<sample:messagesPanel />とする。
- Modelに"messageList"というkeyで格納されたList<String>の中身を画面に出力する。
- デフォルトは外側にulタグ、内側にliタグを使って、以下のように出力される。
<ul>
<li>メッセージ1</li>
<li>メッセージ2</li>
</ul>
- タグにouterElement、innerElement属性でタグ名を指定すると、それを使って出力する。
<sample:messagesPanel outerElement="div" innerElement="span" />
↓
<div>
<span>メッセージ1</span>
<span>メッセージ1</span>
</div>
Processorクラスを作成する
まずは、核となるProcessorクラスを作成していきます。
今回はタグを作りたいので、AbstractElementTagProcessorを継承したクラスを作成します。
(StandardBlockTagProcessorを参考に作成しています。)
まずはコンストラクタ
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メソッド
このメソッドで具体的な処理を行います。
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というライブラリを用いてエスケープ処理を行っているため、同様のライブラリを用いてエスケープ処理を行うように上記サンプルを修正しましています。
Dialectクラスを作成する
UtilityObjectを自作したときと同様にDialectクラスを作成する必要があります。
すでにDialectクラスを作成している場合はそれに追加する形でもよいです。
Processorを追加した場合はIProcessorDialectインターフェースを実装する必要がありますが、それを実装したAbstractProcessorDialectという抽象クラスがあるので、今回はこれを継承します。
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を作成して動作を確認しました。
@Controller
public class HelloController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Model model) {
model.addAttribute("messageList", Arrays.asList("メッセージ1","メッセージ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>
以下のように出力されているはずです。
<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という使い分けがいいのかなと思います。