Actionの作り方 (HTMLスタイル)

実装の流れ

ちょっと前提

ここでは、docksidestage.org というドメイン、つまりパッケージは org.docksidestage を想定し、harbor というアプリ名であることを想定して説明していきます。

ハンズオンで Action を作ってみよう!

LastaFluteでの実装のやり方を学んでいる最中であれば、まずは、Exampleプロジェクト harbor で、実際にクラスを作りながらやっていってもよいでしょう。コードを真似て書きながら(コピーでもOK)、画面を作っていくと流れとコツがわかってくるかと思います。 (それができるようにドキュメントを作っています)

harborプロジェクトは、組み込みの H2 Database を使っているので、データベースのインストールは不要です。 ただし、clone した後、ReplaceSchema を叩くの忘れないように。(Quick Trial の欄を参考に)

まずはラフスケッチ実装

一行一行、しっかり書いていくのではなく、まずは流れを実装して、全体像を構築してから細かいところを固めていくというスタイルをオススメしています。 最初の一ターン目はラフスケッチです。わからないことがあったら保留して次に進み、全体像を把握できる状態になってからつまづいたところを解決していきましょう。

  1. URLを決める
  2. Actionクラスを作る
  3. Executeメソッドを準備
  4. Formを作る
  5. HTMLテンプレートを作る
  6. Executeメソッドを実装
  7. いろいろ調整しながら実装

URLを決める

まずは、URLを決めましょう。

ここでは /sea/land/3?pay=HAN (/sea/land/[商品ID]?pay=[支払方法]) というGETリクエストの対応する Action を作ることにしましょう。

/3 の部分はURLパラメーターで商品IDと想定し、ログイン会員(自分)とフォローしている会員の、指定された商品に対応する購入一覧 を表示するとしましょう。もし、payが指定されていたら、指定された方法で支払された購入に絞るとします。(maihamadb を参考に、よくわからなければ後で)

Actionクラスを作る

Actionの名前を決める

/3 はURLパラメーターとするならば、Actionを識別する部分が /sea/land/ となり、

  • A. SeaAction#land()
  • B. SeaLandAction#index()

の、どちらかとなります。(規約)

基本的には、一つの画面に付き、一つのActionがオススメです。ですが、業務規模などの都合で微調整してもよいでしょう。 (とりあえず漠然と決めて、あとでやっぱりこっちだった、となればリファクタリングすればOKです。変更してもURLには影響しません)

ここでは、landという画面があると想定して、SeaLandAction#index() を作りましょう。

Actionのパッケージ(配置場所)を決める

SeaLandAction であれば...

  • A. ...app.web.SeaLandAction
  • B. ...app.web.sea.SeaLandAction
  • C. ...app.web.sea.land.SeaLandAction

のどれかになります。(規約)

web直下に置くのは、よほどのメジャー感のあるものなので、基本的には B, C となるでしょう。 seaの仲間がすごく多いことが想定される場合は C ですが、まずは B にしておいて、後から移動してもよいでしょう。 (あとでパッケージを移動しても、URLやプログラムに影響はありません)

ここでは、...app.web.sea に置くことにしましょう。

実際に作る

実際に org.docksidestage.app.web.sea.SeaLandAction にクラスを作ってみましょう。

e.g. Action class location @Package
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-sea
 |  |  |  |  |-SeaLandAction.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

Executeメソッドを準備

Baseクラスを継承

まずは、作成したActionにて、そのアプリのBaseクラスを継承しましょう。アプリ名が harbor であれば、 HarborBaseAction を継承します。(そのアプリのBaseActionが用意されているはずです)

e.g. Action class extends Base class @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {
}

Executeメソッドを定義

ここでは、HTMLスタイルの Execute メソッド index() を作ってみましょう。

Executeアノテーションを付けて戻り値は HtmlResponse, そして、URLパラメーター /3 を受け取る引数を最初に定義して、GETパラメーターを受け取る Form を最後の引数に定義します。(規約)

e.g. Action class extends Base class @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {

    @Execute
    public HtmlResponse index(Integer productId, SeaLandForm form) {
    	// まだ、Formがない、returnがないのでコンパイルエラーです!
    	// この後、Formを作って戻り値を指定しますので、ちょっとそのままで。
    }
}

もし、URLパラメーターの /3 が非必須の要素であれば、productId の型を OptionalThing<Integer> にします。省略されたときは empty になります。そもそも、URLパラメーターもFormパラメーターもないのであれば、引数なしでOKです。

Formを作る

Formクラスを定義

SeaLandForm は定義してみましたが、まだ存在しないのでコンパイルエラーです。

Formのクラス名は Form で終わる 必要があります(規約)。その前の名前は任意ですが、Actionとイメージの近い名前がオススメです。 ここでは、SeaLandForm という名前で作りましょう。

パッケージ(置き場所)は、Actionクラスと同じ (つまり、Actionの隣に置く) がオススメです。

e.g. Form class location @Directory
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-sea
 |  |  |  |  |-SeaLandAction.java
 |  |  |  |  |-SeaLandForm.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

Formの実装

publicフィールドで受け取るパラメーターのプロパティを定義します。フリー入力項目でなければ、Stringではなく、Integer や LocalDate や CDef (区分値) など、ネイティヴな型で宣言してOKです。 (変換できない値だったら、それはシステム上のミス、もしくは、いたずらということで 400 になる)

ここでは、pay=HANというGETパラメーターに対応して、payプロパティを定義しましょう。 商品ステータスの区分値なので、CDef 型で宣言します。

e.g. Form class @Java
/**
 * @author yourname
 */
public class SeaLandForm {

    public CDef.PaymentMethod pay;
}

もし、バリデーションを行うなら、Validatorアノテーションを付けます。

必須チェック
@Required (String, Integerなんでも使える)
文字列の最大長
@Length
数値の最大値
@Max
などなど
いろいろ

ここでは、payプロパティを必須にしてしまいましょう。(何も付けないと説明しづらいので...)

e.g. Form class @Java
/**
 * @author yourname
 */
public class SeaLandForm {

    @Required
    public CDef.PaymentMethod pay;
}

HTMLテンプレートを作る

Actionの実装の前に、HTMLテンプレートのファイルを作って、自動生成だけやっておきましょう。

ここでは、Thymeleafテンプレートを想定します。

HTMLテンプレートのファイル名

関連するActionクラスを識別できる名前にします。(規約)

SeaLandAction であれば、sea_land.html になります。 (名前は任意になりますが、合わせておくのがオススメです)

ここでは、sea_land.html にします。

HTMLテンプレートのディレクトリ

HTMLは、src/main/webapp/WEB-INF/view の下に作成します。(規約)

そこからのディレクトリ構成は、Actionクラスと同じで、sea_land.html なら、sea ディレクトリか sea/land ディレクトリの下に配置します。 (規約)

ここでは、src/main/webapp/WEB-INF/view/sea ディレクトリにします。

FreeGenを叩いてパス自動生成

manageタスク (DBFluteクライアントの manage.bat|sh) の 12 (freegen) を叩いて、FreeGen を実行すると、そのHTMLテンプレートに対応するパス定義が自動生成されます。 ここでは HarborHtmlPathpath_Sea_SeaLandHtml というパス定義が追加されるでしょう。

e.g. execute FreeGen @Command
...$ sh manage.sh

// メニューが表示されるので、12 (freegen) を入力してEnter
// Windowsなら manage.bat, Eclipseからでも直接叩ける (ctrl+shift+Rから叩こう)

HTMLの実装...はさておいて

まだ、ラフスケッチ中なので、中身はシンプルな状態で。この時点では真っ白でもOKです。 ここでは、何も表示されないと悲しい ので、一覧を表示するHTMLだけちょこっと書いておきましょう。

e.g. execute FreeGen @Command
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<!--/*/ <th:block th:include="/common/layout.html :: head (title='List of Purchase')"> /*/-->
	<meta charset="utf-8"/>
	<link rel="stylesheet" type="text/css" href="../../../css/reset.css" />
	<link rel="stylesheet" type="text/css" href="../../../css/common.css" />
	<link rel="stylesheet" type="text/css" href="../../../css/individual.css" />
	<title>Preview Title</title>
<!--/*/ </th:block> /*/-->
<!--/* individual Css write to after */-->
</head>
<body>
<header th:replace="/common/layout.html :: header">
	<section class="nav-main cf">
		<div class="wrap">
			<h1 class="main-title"><a href="#">Harbor<span> (LastaFlute Example)</span></a></h1>
			<ul class="nav-home">
				<li><a href="../product/product_list.html"><span class="link-block">Products</span></a></li>
				<li><a href="../member/member_list.html"><span class="link-block">Members</span></a></li>
				<li><a href="../withdrawal/withdrawal.html"><span class="link-block">Withdrawal</span></a></li>
			</ul>
			<ul class="nav-user">
				<li>
					<p class="nameHeader">Welcome, Mr.Guest</p>
					<ul class="child">
						<li><a href="#">Profile</a></li>
						<li><a href="#">Sign out</a></li>
					</ul>
				</li>
			</ul>
		</div>
	</section>
</header>
<main>
<div class="contents">
	<h2 class="content-title">List of Purchase</h2>
	<section class="product-search-box">
        <h3 class="content-title-second">Search Condition</h3>
        <p>
        	Payment Method: <span th:text="${pay}"/>
        	<span la:errors="pay"/>
        </p>
	</section>
    <section class="product-result-box">
        <h3 class="content-title-second">Search Results</h3>
        <table class="list-tbl">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Member Name</th>
                    <th>Product Name</th>
                    <th>Product Handle Code</th>
                    <th>Purchase Date</th>
                    <th>Purchase Price</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="bean : ${beans}">
                    <td th:text="${bean.purchaseId}">bean.purchaseId</td>
                    <td th:text="${bean.memberName}">bean.memberName</td>
                    <td th:text="${bean.productName}">bean.productName</td>
                    <td th:text="${bean.productHandleCode}">bean.productHandleCode</td>
                    <td th:text="${bean.purchaseDate}">bean.purchaseDate</td>
                    <td th:text="${bean.purchasePrice}">bean.purchasePrice</td>
                </tr>
            </tbody>
        </table>
    </section>
</div>
</main>
<footer th:replace="/common/layout.html :: footer">
	<div class="wrap">
		<ul class="footer-link">
			<li><a href="http://dbflute.seasar.org/">DBFlute Top</a></li>
			<li><a href="http://dbflute.seasar.org/lastaflute">LastaFlute Top</a></li>
		</ul>
		<p class="copyright">© LastaFlute project</p>
	</div>
</footer>
<!--/*/ <th:block th:include="/common/layout.html :: afterScript"> /*/-->
<!-- script contents -->
<script src="../../../js/jquery-2.1.3.min.js" ></script>
<script src="../../../js/common.js" ></script>
<!--/*/ </th:block> /*/-->
<!--/* individual Script write to after */-->
</body>
</html>

Executeメソッドを実装

ひとまずreturnを書いておく

returnは後であれこあカスタマイズするかもしれませんが、コンパイルエラーのまま実装するのはつらいので、とりあえず解決しておきます。

asHtml([自動生成されたHTMLテンプレートのパス定義]) を return します。

e.g. return HtmlResponse as HTML template @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    return asHtml(path_Sea_SeaLandHtml); 
}

もし、別の Action へのリダイレクトなら、redirect([リダイレクト先のAction]) を return します。

e.g. return HtmlResponse as redirect @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    return redirect(PiariDstoreAction.class); 
}

(jflute備忘録: デモンストレーションのとき、ここで Boot で画面アクセス)

validate()を呼ぶ

FormにValidatorアノテーションを一つでも付けているなら、validate() を呼ぶ必要があります。

e.g. validate() in Action class @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    return asHtml(path_Sea_SeaLandHtml); 
}

Eclipseであれば、秘伝の EditorTemplate が設定されていれば、魔法のように補完できます。

相関バリデーションやDBを使ったバリデーションなど、アノテーションでは実現できないものは、第二引数の Lambda の中で実装します。 まあ、いったん長そうであれば、todoコメントいれて保留しましょう。

DBFluteを使う

DBFluteを使って検索・更新などを行うのであれば...基点テーブルの Behavior を DI しましょう。

ここでは、購入の一覧でしたから、基点テーブルは PURCHASE です。

e.g. DBFlute Behavior's DI @Java
@Resource
private PurchaseBhv purchaseBhv;

@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    ...
}

そして、ConditionBean で検索しましょう。

e.g. select by ConditionBean @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        cb.setupSelect_Member();
        cb.setupSelect_Product();
        cb.query().setProductId_Equal(productId);
        cb.orScopeQuery(orCB -> {
            orCB.query().setMemberId_Equal(userId);
            orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                followingCB.query().setMyMemberId_Equal(userId);
            });
        });
        cb.query().existsPurchasePayment(paymentCB -> {
            paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
        });
        cb.query().addOrderBy_PurchaseDatetime_Desc();
    });
    return asHtml(path_Sea_SeaLandHtml); 
}

※実際、商品IDが固定なので、商品は単独データ取得してリストでは要らないはずですが、簡略化しています

ログインしているユーザーの情報は、getUserBean() で取得できます。 ここでは、ログインしていることが前提なので、戻り値の OptionalThing は問答無用で get() してしまいます。

まだ、ラフスケッチ中なので、完璧な実装じゃなくてもOKです。とりあえずなんか検索できれば。

表示データの HTML Bean を作る

HTMLテンプレートへ渡すための HTML Bean クラスを作成し、検索されたデータを詰め替えます。

ここに関しては、Lastaだからどうだってのはありませんが、習慣として Bean という名前のクラスをActionの隣に作っています。 ここでは、SeaLandRowBean というクラスを作りましょう。

e.g. Bean class location @Directory
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-sea
 |  |  |  |  |-SeaLandAction.java
 |  |  |  |  |-SeaLandForm.java
 |  |  |  |  |-SeaLandRowBean.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...
e.g. properties for display in Bean class @Java
/**
 * @author yourname
 */
public class SeaLandRowBean {

    @Required
    public Long purchaseId;
    @Required
    public String memberName;
    @Required
    public String productName;
    @Required
    public String productHandleCode;
    @Required
    public LocalDate purchaseDate;
    @Required
    public Integer purchasePrice;
}

HTML Bean でも Validator Annotation を付けましょう(@Required や @NotNull など基本的なものだけ)。 HTMLテンプレートに対する入力チェックと解釈することができます。 HTMLテンプレート内で発生する例外はデバッグしづらいので事前に防ぐとともに、ドキュメント的な意味合いも兼ねます。

また、ここもネイティヴ型の Integer や LocalDate などの型で定義してしまいます。 (出力される日付フォーマットを変更したい場合は、AssistantDirector にて調整できます)

e.g. renderWith() in Action class @Java

@Resource
private PurchaseBhv purchaseBhv;

@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        ...
    });
    List<SeaLandRowBean> beans = purchaseList.stream().map(purchase -> {
        SeaLandRowBean bean = new SeaLandRowBean();
        bean.purchaseId = purchase.getPurchaseId();
        purchase.getMember().alwaysPresent(member -> {
            bean.memberName = member.getMemberName();
        });
        purchase.getProduct().alwaysPresent(product -> {
            bean.productName = product.getProductName();
            bean.productHandleCode = product.getProductHandleCode();
        });
        bean.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
        bean.purchasePrice = purchase.getPurchasePrice();
        return bean;
    }).collect(Collectors.toList());
    return asHtml(path_Sea_SeaLandHtml); 
}

ちなみに、リフレクションで詰め替えるとかは強烈に非推奨です。

表示データをResponseに設定

asHtml()を使っている場合、HTMLテンプレートに表示データを渡します。

asHtml()に続いて、renderWith()メソッドを呼び、Lambda引数の data の register() を使って、HTMLテンプレート上で参照する名前をキーに、表示データを登録します。

e.g. renderWith() in Action class @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = ...
    List<SeaLandRowBean> beans = ...
    return asHtml(path_Sea_SeaLandHtml).renderWith(data -> {
        data.register("beans", beans);
    });
}

ラフスケッチできた

さて、これでラフスケッチ実装は終了です。全体像が、見えてきたでしょうか?

この時点では、このような感じで Action ができあがっているはずです。

e.g. rough sketch finished @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {

    @Resource
    private PurchaseBhv purchaseBhv;

    @Execute
    public HtmlResponse index(int productId, SeaLandForm form) {
        validate(form, messages -> {}, () -> {
            return asHtml(path_Sea_SeaLandHtml);
        });
        Integer userId = getUserBean().get().getUserId();
        ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
            cb.setupSelect_Member();
            cb.setupSelect_Product();
            cb.query().setProductId_Equal(productId);
            cb.orScopeQuery(orCB -> {
                orCB.query().setMemberId_Equal(userId);
                orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                    followingCB.query().setMyMemberId_Equal(userId);
                });
            });
            cb.query().existsPurchasePayment(paymentCB -> {
                paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
            });
            cb.query().addOrderBy_PurchaseDatetime_Desc();
        });
        List<SeaLandRowBean> beans = purchaseList.stream().map(purchase -> {
            SeaLandRowBean bean = new SeaLandRowBean();
            bean.purchaseId = purchase.getPurchaseId();
            purchase.getMember().alwaysPresent(member -> {
                bean.memberName = member.getMemberName();
            });
            purchase.getProduct().alwaysPresent(product -> {
                bean.productName = product.getProductName();
                bean.productHandleCode = product.getProductHandleCode();
            });
            bean.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
            bean.purchasePrice = purchase.getPurchasePrice();
            return bean;
        }).collect(Collectors.toList());
        return asHtml(path_Sea_SeaLandHtml).renderWith(data -> {
            data.register("beans", beans);
        });
    }
}

Bootしてアクセスしてみましょう

Bootクラスを起動して、実際にそのURLでアクセスしてみましょう。 恐らく非常にそっけない画面しか表示されないと思いますが、ログをみてちゃんと動いていればこの先に進めます。

ここでは、HarborBoot クラスの main() を実行しましょう。

e.g. HarborBoot @Java
/**
 * @author jflute
 */
public class HarborBoot { // #change_it_first

    public static void main(String[] args) { // e.g. java -Dlasta.env=production -jar harbor.war
        new JettyBoot(8090, "/harbor").asDevelopment(isNoneEnv()).bootAwait();
    }

    private static boolean isNoneEnv() {
        return System.getProperty("lasta.env") == null;
    }
}
e.g. boot log @Console
INFO  (...@showBoot():105) - _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
INFO  (...@showBoot():106) -  the system has been initialized:
INFO  (...@showBoot():107) - 
INFO  (...@showBoot():108) -   -> Harbor (Local Development)
INFO  (...@showBoot():109) - _/_/_/_/_/_/_/_/_/_/
Boot successful as development: url -> http://localhost:8090/harbor

ここでは、http://localhost:8090/harbor/sea/land/3?pay=HAN にアクセスしてみましょう。 (いちど、ログイン画面に飛ばされますので、'Pixy', 'sea' と入力してログイン)

ちょいっとリファクタリング

ちょっとメソッド長いですね。実際に業務だと、もっと大きくなる可能性があります。

TODO jflute リファクタリングする

いろいろ調整しながら実装

TODO jflute ポイントポイントの説明を書く

ちょこっとTips

スーパークラスのメソッド、何が使える?

Actionクラスの中で、this.docu...() と document メソッドを補完して、JavaDocを表示してみてください。すると...

e.g. document methods @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    ...
    this.docu // 補完して、IDE上でJavaDoc表示
    ...
}

実装しながら、ふとActionの規約を忘れちゃったときとか、ブラウザで検索してドキュメントを探す...ではなく、 ササッと this.docu... ってやってみるといいかもですね。

事務連絡

harbor で、実際に SeaLandAction を作っていくと、HarborHtmlPath など幾つかのリソースに修正マークが付いてしまいます。 (Actionとかは、gitignoreしてますが、どうしても無視設定できない修正が...)