アクションについて
WebFlowでは、フロー定義中に書かれた処理(Javaオブジェクトのメソッドコール)をActionと読んでいます。基本はevaluate要素にメソッドコールを記述すれば、実行できます。実行できるJavaオブエジェクトは、Springのコンテナ管理下のオブジェクトならばなんでも可能です。
例:
<evaluate expression="sampleAction.execute(sampleForm,messageContext)" result="requestScope.Result"/>
public class SampleAction {
public Object execute(Form form,MessageContext messageContext){
return null;
}
}
アクションが実行可能な場所は多岐に渡ります。
詳細はドキュメントを参照してください。
実行結果はevaluate要素のresult属性で、各種のスコープに保存できます。
ActionはPOJOでも可能ですが、フロー定義上で使いやすいベースクラスであるAbstractActionとMultiAction,また、戻り値としてEventクラスが提供されています。
Actionのレイヤーについて
参考1
やはり達人
基本は、フロー上のActionはアプリケーションレイヤだと思います。メッセージをはじめ、Viewやフレームワークに依存せざるを得ないからです。
ですから、Domain層のサービスをフロー上で直接呼びだすことはせず、上記のActionのサブクラスで統一してしまうのも手だと思います。
※Eventを戻り値とすると、少し面倒な部分もあるのですが、IFを統一してしまった方が、後々メンテナンス性も良くなると思います。
今回は、Actionクラスのexecuteメソッドは引数が全てのモデルにアクセスできるものですので、独自の基底クラスを作ってみます。
基底クラス
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);
}
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);
}
}
検索アクションの定義
内部実装はダミーとします。
エンティティ
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;
}
}
フォーム
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;
}
}
検索アクション
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);
}
}
テスト
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 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を参照するテーブルと、メッセージを表示するリストを追加
<!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を設定するだけ。
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要素の属性で設定可能