SpringBoot thymeleaf 独自タグ実装

今回はthymeleafの拡張についてご紹介します。
http://www.thymeleaf.org/doc/tutorials/2.1/extendingthymeleaf.html

SpringBootのテンプレートエンジンとしてthymeleafを使うメリットがいくつかあります。

**1.PHPなどで利用していたテンプレートの流用**
PHPのフレームワーク的な書き方ができるので、JSPと違って書き直しの手間がすごく省けます。

**2.独自タグの定義**
`<p th:mytag="${tree}"></p>`
みたいな感じで独自タグを実装できます。
実装はエレメントごとかアトリビュートごとなど色々選べます。
登録した独自タグに対応するクラスを呼び出してくれるのでそこで編集を行うといったイメージです。

http://www.thymeleaf.org/apidocs/thymeleaf/2.1.2.RELEASE/org/thymeleaf/processor/AbstractProcessor.html

アトリビュート: AbstractAttrProcessor
コメント: AbstractCommentNodeProcessor
ドキュメント: AbstractDocumentProcessor
エレメント: AbstractElementProcessor
テキストノード: AbstractTextNodeProcessor

今回は全てのフォームタグCSFRトークン検証用のhideenフィールドを自動で追加して、リクエスト受信時にチェックしてみましょう。

**1.フォームの実装**
[フォームプロセッサ](https://github.com/MariMurotani/SpringDemo/blob/4_develop/src/main/java/demo/thymeleaf/FormProcessor.java)
まずは、AbstractElementProcessorを実装します。
thymeleafではテンプレートエンジンがテンプレートをパースしてViewとして返却する前に色々なプロセッサエンジンを呼び出しします。そこにコールバックをいくつでも登録できるのでフォーム処理用のプロセッサを実装しておきます。

[ダイアレクト](https://github.com/MariMurotani/SpringDemo/blob/4_develop/src/main/java/demo/thymeleaf/ThymeleafDialect.java)
AbstractDialectを実装して実際にプロセッサを登録する処理を行います。

[WebMvcConfigurerAdapter](https://github.com/MariMurotani/SpringDemo/blob/4_develop/src/main/java/demo/configs/ThymeleafConfig.java)
WebMvcConfigurerAdapter
こちらでテンプレートエンジンの設定を行います。
この際に、先ほど実装したダイアレクトの呼び出し設定をします。

以上3つの設定を正しく行うとフォームタグのカスタマイズが一律で行える様になります。

**1.リクエストの実装**
フォームタグにトークン用のhiddenフィールドが入ったら今度はリクエスト時の処理を追加しましょう。

今回はHandlerInterceptorを継承したクラスの中で行ってしまいます。
[リクエストハンドラー](https://github.com/MariMurotani/SpringDemo/blob/4_develop/src/main/java/demo/controller/ControllerHandler.java)

`@Override`
`public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)`

このメソッドをオーバーライドするとおなじみのHttpServletRequestがくるので好きな様に調理するだけです。

ちなみにこちらの記事が参考になります。
http://forum.thymeleaf.org/Support-for-RequestDataValueProcessor-td4024906.html

動作するサンプル
[ソース](https://github.com/MariMurotani/SpringDemo/tree/4_develop)
[起動](http://localhost:8080/parse/index)

```java:ThymeleafConfig.java
@Configuration
public class ThymeleafConfig extends WebMvcConfigurerAdapter {
		
		@Bean
		public ClassLoaderTemplateResolver templateResolver() {
			ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
			classLoaderTemplateResolver.setCacheable(false);
			classLoaderTemplateResolver.setPrefix("templates/");
			classLoaderTemplateResolver.setSuffix(".html");
			classLoaderTemplateResolver.setTemplateMode("HTML5");
			classLoaderTemplateResolver.setCacheable(false);
			return classLoaderTemplateResolver;
		}
	
	    
	    @Bean
	    public SpringTemplateEngine templateEngine() {
	        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
	        templateEngine.setTemplateResolver(templateResolver());
	        templateEngine.addDialect(new ThymeleafDialect());
	        return templateEngine;
	    }
	    
	    @Bean
	    public ThymeleafViewResolver viewResolver() {
	        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
	        viewResolver.setTemplateEngine(templateEngine());
	        
	        return viewResolver;
	    }
	    
	    @Override
	    public void addResourceHandlers(ResourceHandlerRegistry registry) {
	        registry.addResourceHandler("/resources/**").addResourceLocations("/WEB-INF/resources/");
	    }
}
```

```java:ThymeleafDialect.java
public class ThymeleafDialect extends AbstractDialect {
	public static final String PREFIX = "";

	
	public ThymeleafDialect() {
		super();
		// TODO Auto-generated constructor stub
	}
	
	@Override
	public String getPrefix() {
		// TODO Auto-generated method stub
		return PREFIX;
	}
	

	// The processors.
	//
	@Override
    public Set<IProcessor> getProcessors() {
		final Set<IProcessor> processors = new LinkedHashSet<IProcessor>();
        processors.add(new demo.thymeleaf.FormProcessor());
        
        return new LinkedHashSet<IProcessor>(processors);
        
    }
	
}
```

```java:FormProcessor.java
public class FormProcessor extends AbstractElementProcessor {
	static final String ELEMENT_NAME_FORM = "form";
	
	public FormProcessor() {
		super(ELEMENT_NAME_FORM);
	}

	@Override
	protected ProcessorResult processElement(Arguments arguments,Element element) {
		try{
			addCSRFHiddenFields(arguments, element);
			System.out.println(element.getNormalizedName());
			return ProcessorResult.OK;
		}catch(Exception e){
			System.out.println(e.getMessage());
		}
		return null;
	}
	
	@Override
	public IProcessorMatcher<? extends Element> getMatcher() {
		// TODO Auto-generated method stub
		return super.getMatcher();
	}
	
	private void addCSRFHiddenFields(Arguments arguments, Element element) {
		System.out.println("addExtraHiddenFields");
		//element.getAttributeFromNormalizedName("form");
		element.normalizeElementName("form");
		
		
		if (!"GET".equalsIgnoreCase(element.getAttributeValueFromNormalizedName("method"))) {
            try {
    
            	HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                HttpSession httpSession = request.getSession();
                
                String csrfTokenName = Const.CRSF_PREFIX + Encoder.getRandomMd5();
            	String csrfTokenValue = Encoder.getRandomMd5();
            	httpSession.setAttribute(csrfTokenName, csrfTokenValue);
                
            	Element csrfNode = new Element("input");
                csrfNode.setAttribute("type", "hidden");
                csrfNode.setAttribute("name", csrfTokenName);
                csrfNode.setAttribute("value", csrfTokenValue);
                element.addChild(csrfNode);
            	
            } catch (Exception e) {
                throw new RuntimeException("Could not get a CSRF token for this session", e);
            }
        }
		
		//return element.cloneElementNodeWithNewName(element.getParent(),"form",true);
	}


	private void addHidden(Element form, String name, String value) {
		Element hiddenElement = new Element("input");
		hiddenElement.setAttribute("type", "hidden");
		hiddenElement.setAttribute("name", name);
		hiddenElement.setAttribute("value", value);

		form.addChild(hiddenElement);
	}

	@Override
	public int getPrecedence() {
		return 1300;
	}
}
```