時代遅れかもしれないSpring Web Flow入門 3

乗りかかった船なので。。。
#検索フローの実装
##フロー定義
まずは単純な画面遷移のみを行うフローを定義します。
flow-search.xmlは「/src/main/resources/templates/search」直下に配置します。

```xml:flow-search.xml
<?xml version="1.0" encoding="UTF-8"?>
<flow  
    xmlns="http://www.springframework.org/schema/webflow"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
    <!-- 検索トップ画面 -->
    <view-state id="top" view="/search/top">
        <transition on="search" to="result"/>
    </view-state>
    
    <!-- 検索トップ画面（検索結果表示状態） テンプレートは同じ-->
    <view-state id="result" view="/search/top">
        <transition on="search" to="result"/>
        <transition on="detail" to="detail"/>
    </view-state>
    
    <!-- 詳細画面 -->
    <view-state id="detail" view="/search/detail">
       <transition on="back" to="top"/>
    </view-state>
</flow>
```

##設定

```java:WebConfig.java
package flowapp;

@Configuration
public class WebFlowConfig extends AbstractFlowConfiguration {

	@Autowired
	SpringTemplateEngine templateEngine;
		
	/**
	 * WebFlow を利用している場合、enfoce=trueだ文字化けする。
	 * @return
	 */
	@Bean
	public CharacterEncodingFilter characterEncodingFilter() {

		CharacterEncodingFilter filter = new CharacterEncodingFilter();
		filter.setEncoding("UTF-8");
		filter.setForceEncoding(false);
		return filter;
	}
	
	/**
	 * フロー定義を配置するルートフォルダを定義。
	 * テンプレートと同じ階層にするため、Spring bootデフォルトの「templates」フォルダを指定
	 * @return
	 */
	@Bean
	public FlowDefinitionRegistry flowRegistry() {
		return getFlowDefinitionRegistryBuilder()
				.setBasePath("classpath:templates")
				.addFlowLocationPattern("/**/flow-*.xml")
				.setFlowBuilderServices(flowBuilderServices()).build();
	}

	@Bean
	public FlowBuilderServices flowBuilderServices() {
		return getFlowBuilderServicesBuilder()
				.setViewFactoryCreator(viewFactoryCreator())
				.setValidator(validator())
				.setDevelopmentMode(true)
				.build();
	}

	@Bean
	public Validator validator(){
		return new LocalValidatorFactoryBean();
	}
	
	@Bean
	public ViewFactoryCreator viewFactoryCreator() {
		MvcViewFactoryCreator bean = new MvcViewFactoryCreator();
		bean.setViewResolvers(Arrays.asList(viewResolver()));				
		bean.setUseSpringBeanBinding(true);
		return bean;
	}

	@Bean
	public FlowExecutor flowExecutor() {
		return getFlowExecutorBuilder(flowRegistry())
				.build();
	}

	@Bean
	public FlowHandlerAdapter flowHandler() {
		FlowHandlerAdapter bean = new FlowHandlerAdapter();
		bean.setFlowExecutor(flowExecutor());
		bean.setSaveOutputToFlashScopeOnRedirect(true);
		return bean;
	}

	@Bean
	public FlowHandlerMapping flowHandlerMapping() {
		FlowHandlerMapping bean = new FlowHandlerMapping();
		bean.setFlowRegistry(flowRegistry());
		bean.setOrder(0);
		return bean;
	}
	
	@Bean
	public AjaxThymeleafViewResolver viewResolver() {
		AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver();
		viewResolver.setViewClass(FlowAjaxThymeleafView.class);
		viewResolver.setTemplateEngine(templateEngine);
		viewResolver.setCharacterEncoding("UTF-8");
		return viewResolver;
	}
}
```

##フロー定義のテスト
AbstractXmlFlowExecutionTestsはスーパークラスとなるため、Spockで使うためにTestRuleクラスにしてみました。（多分、何かバグはある。）

```java:FlowExecutionTestsRule.java
package flowapp.test;

public class FlowExecutionTestsRule extends AbstractXmlFlowExecutionTests
		implements TestRule {

	private String flowDefPath;
	private Map<String, Object> registoredBeans = new HashMap<>();

	public FlowExecutionTestsRule(String flowDefPath) {
		super();
		this.flowDefPath = flowDefPath;
	}

	public void registorBean(String beanName, Object bean) {
		registoredBeans.put(beanName, bean);
	}

	@Override
	public Statement apply(Statement base, Description description) {
		return new Statement() {

			@Override
			public void evaluate() throws Throwable {
				base.evaluate();
			}
		};
	}

	@Override
	protected FlowDefinitionResource getResource(
			FlowDefinitionResourceFactory resourceFactory) {
		return resourceFactory.createFileResource(flowDefPath);
	}

	@Override
	protected void configureFlowBuilderContext(
			MockFlowBuilderContext builderContext) {

		registoredBeans.entrySet().forEach(e -> 
		  builderContext.registerBean(e.getKey(), e.getValue())
		  );
	}

}

```

テストクラス。

```groovy:SearchFlowSpec
package flowapp;

class SearchFlowSpec extends Specification{

	@Rule
	public FlowExecutionTestsRule flowExecutionTestsRule = new FlowExecutionTestsRule("src/main/resources/templates/search/flow-search.xml");
	
	
	def "フローの初期化"() {
		when:
		MockExternalContext context = new MockExternalContext();
		flowExecutionTestsRule.startFlow(context);
		
		then:
		flowExecutionTestsRule.assertCurrentStateEquals("top");
	}
	
	def "画面遷移のテスト"(){
		setup:
		MockExternalContext context = new MockExternalContext();
		
		when:
		flowExecutionTestsRule.setCurrentState(currentState)
		context.setEventId(eventId)
		flowExecutionTestsRule.resumeFlow(context)
		
		then:
		flowExecutionTestsRule.assertCurrentStateEquals(resultState);
		
		where:
		currentState|eventId |resultState
		"top"       |"search"|"result"
		"result"    |"search"|"result"
		"result"    |"detail"|"detail"
		"detail"    |"back"  |"top"
	}

}
```

この段階では、まだ、画面との結合はできていません。

##画面の作成
モック画面を作成して、遷移の動作確認をします。
src/main/resources/search直下にファイルを作成します。

```html:top.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:tiles="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org" lang="jp">
<head>
<meta charset="UTF-8" />
<title>検索画面</title>
</head>
<body>
    <!-- ステートIDを表示 -->
	<span th:text="${flowRequestContext.currentState.id}"></span>
	<form action="#" th:action="${flowExecutionUrl}" method="POST">
		<table>
			<tr>
				<td>ISBN</td>
				<td><input type="text" name="isbn" /></td>
			</tr>
			<tr>
				<td>タイトル</td>
				<td><input type="text" name="title" /></td>
			</tr>
		</table>
		<input type="submit" name="_eventId_search" value="検索" />
		<table>
			<tr>
				<th>ISBN</th>
				<th>タイトル</th>
				<th>詳細</th>
			</tr>
			<tr>
				<td>1234567890</td>
				<td>タイトル1</td>
	            <td><a href="#" th:href="@{${flowExecutionUrl}(_eventId='detail')}">詳細</a></td>
    		</tr>
			<tr> <td>1234567891</td>
                <td>タイトル2</td>
                <td><a href="#" th:href="@{${flowExecutionUrl}(_eventId='detail')}">詳細</a></td></tr>
		</table>
	</form>
</body>
</html>
```

```html:detail.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:tiles="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org" lang="en">
<head>
<script type="text/javascript" th:src="@{/resources/dojo/dojo.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring.js}"></script>
<script type="text/javascript"
	th:src="@{/resources/spring/Spring-Dojo.js}"></script>
<meta charset="UTF-8" />
<title>詳細</title>
</head>
<body>
	<form action="" th:action="${flowExecutionUrl}" method="POST">
		<table>
			<tr>
				<th>ISBN</th>
				<td>1234567891</td>
			</tr>
			<tr>
				<th>タイトル</th>
				<td>タイトル2</td></tr>
		</table>
		<input type="submit" name="_eventId_back" value="戻る" />
	</form>
</body>
</html>
```
###イベントID
ステートの遷移に利用されるeventIdは、submit時のname属性のほか、queryStringでも渡すことができます。


##動作確認
gradle bootRun,もしくは、IDE上からApplicationクラスを実行してください。
下記の通りに遷移すると正常に遷移できます。
top→result→detail→top
しかし、下記のようなフロー定義に記載がない遷移はできません。
top→detail
本来なら、クライアント側でも操作できないように制御するべきですが、まずは、サーバ側でしっかりチェックされているので安心です。

###実行キー
実行はqueryStringに渡される実行キー（execution=exsx）によって管理されます。xは自動でインクリメントされていきます。メニュー画面からもう一度入りなおした場合、新たに実行キーが割り振られ、過去の実行中のデータとは別のコンテキストで実行できます。複数画面でも、相互に干渉せずに操作が行えます。（デフォルトでは５つまでの実行状態を保持します。５つを超えると上書きされます。）
原則として、フローが開始されればフローの終了まで実行することを想定しているようです。参照系の画面などは、WebFlowではなく、通常のコントローラの方が都合がよいかもしれません。