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

#アクションについて
WebFlowでは、フロー定義中に書かれた処理（Javaオブジェクトのメソッドコール）をActionと読んでいます。基本はevaluate要素にメソッドコールを記述すれば、実行できます。実行できるJavaオブエジェクトは、Springのコンテナ管理下のオブジェクトならばなんでも可能です。
例：

```xml:アクション記述の例
<evaluate expression="sampleAction.execute(sampleForm,messageContext)" result="requestScope.Result"/>		    
```
```java:アクション記述の例
public class SampleAction {

	public Object execute(Form form,MessageContext messageContext){
		return null;
	}
	
}	    
```

アクションが実行可能な場所は多岐に渡ります。
詳細は[ドキュメント](http://docs.spring.io/autorepo/docs/webflow/current/reference/html/actions.html)を参照してください。

実行結果はevaluate要素のresult属性で、各種の[スコープ](http://docs.spring.io/autorepo/docs/webflow/current/reference/html/defining-flows.html#scopes)に保存できます。


ActionはPOJOでも可能ですが、フロー定義上で使いやすいベースクラスである[AbstractAction](http://docs.spring.io/spring-webflow/docs/current/api/org/springframework/webflow/action/AbstractAction.html)と[MultiAction](http://docs.spring.io/spring-webflow/docs/current/api/org/springframework/webflow/action/MultiAction.html),また、戻り値として[Event](http://docs.spring.io/spring-webflow/docs/current/api/org/springframework/webflow/execution/Event.html)クラスが提供されています。

##Actionのレイヤーについて
[参考1](https://terasolunaorg.github.io/guideline/public_review/Overview/ApplicationLayering.html)
[やはり達人](http://www.slideshare.net/masuda220)
基本は、フロー上のActionはアプリケーションレイヤだと思います。メッセージをはじめ、Viewやフレームワークに依存せざるを得ないからです。

ですから、Domain層のサービスをフロー上で直接呼びだすことはせず、上記のActionのサブクラスで統一してしまうのも手だと思います。
※Eventを戻り値とすると、少し面倒な部分もあるのですが、IFを統一してしまった方が、後々メンテナンス性も良くなると思います。

今回は、Actionクラスのexecuteメソッドは引数が全てのモデルにアクセスできるものですので、独自の基底クラスを作ってみます。

##基底クラス
```java:flowapp/common/action/BaseActionInterface.java
package flowapp.common.action;

import org.springframework.binding.message.MessageContext;
import org.springframework.webflow.execution.Event;

public interface BaseActionInterface<P> {
	public Event execute(P param,MessageContext messageContext);
}
```

```java:flowapp/common/action/BaseAction.java
package flowapp.common.action;

import org.springframework.webflow.action.EventFactorySupport;
import org.springframework.webflow.execution.Event;

public abstract class BaseAction<P>  implements
		BaseActionInterface<P> {
	private EventFactorySupport eventFactorySupport = new EventFactorySupport();

	protected Event successO() {
		return eventFactorySupport.success(this);
	}
	protected Event success(Object result) {
		return eventFactorySupport.success(this, result);
	}
	protected Event error() {
		return eventFactorySupport.error(this);
	}
	protected Event error(Exception e) {
		return eventFactorySupport.error(this, e);
	}
}

```

##検索アクションの定義
内部実装はダミーとします。
###エンティティ


```java:flowapp/entity/Book.java
package flowapp.entity;

import java.io.Serializable;

public class Book implements Serializable{

	private static final long serialVersionUID = 7525223989361280844L;
	private String isbn;
	private String title;
	
	public Book(String isbn, String title) {
		super();
		this.isbn = isbn;
		this.title = title;
	}
	
	public String getIsbn() {
		return isbn;
	}
	public String getTitle() {
		return title;
	}
	
}

```

###フォーム

```java:flowapp/search/form/SearchForm
package flowapp.search.form;

import java.io.Serializable;

public class SearchForm implements Serializable{
	

	private static final long serialVersionUID = 8170913021455253413L;
	
	private String isbn;
	private String title;
	private String detailIsbn;
	
	public String getIsbn() {
		return isbn;
	}
	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getDetailIsbn() {
		return detailIsbn;
	}
	public void setDetailIsbn(String detailIsbn) {
		this.detailIsbn = detailIsbn;
	}
	
	
}
```

###検索アクション
```java:flowapp/search/action/SearchAction.java
package flowapp.search.action;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.stereotype.Component;
import org.springframework.webflow.execution.Event;

import com.google.common.base.Strings;

import flowapp.common.action.BaseAction;
import flowapp.entity.Book;
import flowapp.search.form.SearchForm;

@Component
public class SearchAction extends BaseAction<SearchForm> {

	private List<Book> books = Arrays.asList(new Book("1234567890", "普通の本"),
			new Book("1234567891", "難しい本"));

	@Override
	public Event execute(SearchForm form, MessageContext messageContext) {

		String fIsbn = Strings.nullToEmpty(form.getIsbn());
		String ftitl = Strings.nullToEmpty(form.getTitle());

		List<Book> result = books
				.stream()
				.filter(b -> b.getIsbn().startsWith(fIsbn)
						|| b.getTitle().contains(ftitl))
				.collect(Collectors.toList());

		if (result.isEmpty()) {
			messageContext.addMessage(new MessageBuilder().defaultText(
					"検索結果が0件です。").build());
		}
		return success(result);
	}

}
```

テスト

```groovy:SearchActionSpec.groovy
package flowapp.search.action;
class SearchActionSpec extends Specification {
	@Unroll
	def "正常系"(){
		setup:
		MessageContext msgs = Mock();
		SearchAction sut = new SearchAction()
		
		when:
		sut.books =[new Book("1234567890", "普通の本"),new Book("1234567891", "難しい本")]
		def form = new SearchForm(isbn:p_isbn,title:p_title)
		Event result=sut.execute(form, msgs)
		
		then:
		msgCount * msgs.addMessage( _ )
		with(result) {
			id == "success"
			attributes.result.size == resultSize
		}
		
		where:
		p_isbn      |p_title|resultSize|msgCount
		"1234567890"|"xxx"  |1         |0
		"123456789" |"xxx"  |2         |0
		"x23456789" |"本"  |2         |0
		"x23456789" |"普通"  |1         |0
		"1234567890"|"難しい"  |2        |0
		"0"         |"xx"  |0        |1
		""          |""  |2        |0
		"x"          |""  |2        |0
		""          |"x"  |2        |0
	}
}

```
##フロー定義
変更点
* フォームの初期化（先頭のvar要素）
* フォームバインディングの追加。（view-stateのmodel要素）
* アクションの呼び出しの追加（action-state）
* 追記：詳細から戻った時に結果が表示されている（詳細押せないのに）ので、詳細画面から戻るボタン押下で、フローを終了させるように変更。

```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">
	
	<var name="searchForm" class="flowapp.search.form.SearchForm"/>
	<!-- 検索トップ画面 -->
	<view-state id="top" view="/search/top" model="searchForm">
		<transition on="search" to="searchAction" />
	</view-state>

	<!-- 検索トップ画面（検索結果表示状態） テンプレートは同じ -->
	<view-state id="result" view="/search/top" model="searchForm">
		<transition on="search" to="searchAction" />
		<transition on="detail" to="detail" />
	</view-state>

	<!-- 詳細画面 -->
	<view-state id="detail" view="/search/detail">
		<transition on="back" to="end" />
	</view-state>
	<!-- フローの完了全てのフロースコープがクリアされる。 -->
	<end-state id="end">
	</end-state>

	<!-- 検索処理 -->         
	<action-state id="searchAction">
		<evaluate expression="searchAction.execute(searchForm,messageContext)"/>
		<transition to="result">
		  <set name="flowScope.searchResult" value="currentEvent.attributes.result"></set>
		</transition>
	</action-state>
</flow>
```

最後に画面に、searchResultを参照するテーブルと、メッセージを表示するリストを追加

```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">
		<ul
            th:unless="${#lists.isEmpty(flowRequestContext.messageContext.allMessages)}">
            <li th:each="msg : ${flowRequestContext.messageContext.allMessages}"
                th:text="${msg.severity}+':'+${msg.text}">Input is incorrect</li>
        </ul>
		
		<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 th:unless="${#lists.isEmpty(searchResult)}">
            <tr>
                <th>ISBN</th>
                <th>タイトル</th>
            </tr>
            <tr th:each="book : ${searchResult}">
                <td th:text="${book.isbn}">Onions</td>
                <td th:text="${book.title}">Onions</td>
                <td><a href=""
                    th:href="@{${flowExecutionUrl}(_eventId='detail',detailIsbn=${book.isbn})}">詳細</a></td>
            </tr>
        </table>
	</form>
</body>
</html>

```
これで検索結果が表示される。

###バリデーション
hibernate validation/JSR303を使える。やりかたはmodel属性に設定したクラスにvalidation ruleを設定するだけ。


```java:SearchForm.java
package flowapp.search.form;

import java.io.Serializable;

import javax.validation.constraints.Max;

public class SearchForm implements Serializable{
	
	
	@Max(value=10)
	private String isbn;
	
	private String title;
	
	private String detailIsbn;
	

```

一部の値だけbindしたい場合はbind属性で設定可能。戻るボタンなど、bindやvalidationをしたくない場合はtransition要素の属性で設定可能