2022年9月30日金曜日

Cozy Web/HelloForm

Webアプリケーション開発では、フォーム処理の開発が一つのポイントになります。やりたいことそのものは単純でもHTTPプロトコルを使ってWebブラウザとサーバー間でやり取りをするシーケンスの中で実現するのは案外大変です。

Cozy Webではこの煩雑な処理を簡単に実現する機能を提供しています。

今回はCozy Webでのフォーム入力の処理方法について説明します。

HelloForm

フォームを用いたWebアプリケーションHelloFormを作成します。

準備

cozyを起動するディレクトリのwebappsディレクトリに、アプリケーションのホームディレクトリとなるHelloFormを作成します。

Formページ

フォームを使った入力画面として以下のページをindex.jadeとしてホームディレクトリに配置します。

-@val form: ViewForm
html
  head
    title HelloForm
  body
    form(method="POST" action={form.action})
      input(hidden="true" name="$scenario" value={form.scenario})
      input(name="name" value={form.name})
      button(type="submit" id="submitbutton" name="$submit" value="ok") Submit
      button(type="submit" id="cancelbutton" name="$submit" value="cancel") Cancel

ViewFormオブジェクト

以下の宣言でフォームの設定に必要なViewFormを参照可能にします。

-@val form: ViewForm

Form/action

actionにViewFormオブジェクトのaction属性を設定します。

    form(method="POST" action={form.action})

今回の場合は空文字が設定されます。入力と同じページにフォームのPOSTが行われるということですね。

POST処理に必要な情報は後述する「$scenario」に設定されています。

Input/Hidden

Hidden属性のInputで入力する「$scenario」プロパティにViewFormオブジェクトのscenario属性を設定します。

      input(hidden="true" name="$scenario" value={form.scenario})

今回の場合は「{"name":"invoke-operation","state":"input","data":{}}」が設定されています。URLエンコーディングを解除した値は「{"name":"invoke-operation","state":"input","data":{}}」です。

Cozy Webがフォーム処理を行うために必要な情報が設定されています。

Inputデータ

Hidden属性のないInputでデータ入力を行います。

今回は以下の1行が対象です。

      input(name="name" value={form.name})

nameに「name」指定しているので、nameプロパティの入力ということになります。値のデフォルト値としてViewFormオブジェクトのname属性の値を設定しています。今回のケースでは空文字が設定されます。

Input/Ok

OK用のサブミットボタンとして以下のButtonを設定しました。

      button(type="submit" id="submitbutton" name="$submit" value="ok") Submit

name属性に「$submit」、value属性に「ok」を指定しています。

Input/Cancel

キャンセル用のサブミットボタンとして以下のButtonを設定しました。

      button(type="submit" id="cancelbutton" name="$submit" value="cancel") Cancel

name属性に「$submit」、value属性に「cancel」を指定しています。

SSPの場合

フォームのような構造を記述するページの場合、今回の例ではJadeの方が簡潔に記述できるので本ページではJadeを使っていますが、JSP(JavaServer Page)のようなHTMLライクな文法を持つSSP(Scala Server Pages)を使って記述することも可能です。

SSPの場合は以下になります。

<%@ val form: ViewForm %>
<html>
    <head>
	<title>HelloForm</title>
    </head>
    <body>
	<form method="POST" action="${form.action}">
	    <input hidden="true" name="$scenario" value="${form.scenario}"/>
	    <input name="name" value="${form.name}"/>
	    <button type="submit" id="submitbutton" name="$submit" value="ok">Submit</button>
	    <button type="submit" id="cancelbutton" name="$submit" value="cancel">Cancel</button>
	</form>
    </body>
</html>

フォーム記述に使用するViewFormオブジェクトを変数formに束縛する宣言は以下になります。

<%@ val form: ViewForm %>

Formタグに設定する情報はJade版と同じものです。

コントローラ

WEB-INF/controllersにindex.jade用のコントローラである以下のindex.jsonを配置します。

{
  "action": "invoke-operation-scenario",
  "operation": "loopback",
  "method": "POST",
  "successView": "index_complete",
  "errorView": "index_error",
  "parameters": [{
    "name": "token"
  }]
}

action

コントローラのアクションとしてinvoke-operation-scenarioを指定しています。

  "action": "invoke-operation-scenario",

invoke-operation-scenarioは、フォームでパラメタ入力し、このパラメタを使ってオペレーションを呼び出し、その結果をビューに渡すモデルとして生成する処理を行うシナリオです。

フォーム入力にまつわるWebブラウザとサーバ間のインタラクションをシナリオに従って実現します。

operation

実行するオペレーションとしてloopbackを指定しています。

  "operation": "loopback",

loopbackは入力したデータをそのまま表形式のモデルとして受け渡すオペレーションです。

method

メソッドはPOSTを指定しています。

  "method": "POST",

FormのメソッドがPOSTのものを受け付けます。

successView

successViewにはindex_completeを指定しています。

  "successView": "index_complete",

コントローラの処理が成功するとこのページに遷移します。

errorView

errorViewにはindex_errorを指定しています。

  "errorView": "index_error",

コントローラの処理が失敗するとこのページに遷移します。

parameters

フォームから入力されるパラメタとして、パラメタ名とデータ型を指定しています。

  "parameters": [{
    "name": "token"
  }]

パラメタは、パラメタ名nameでデータ型はトークン文字列の一つだけです。

成功ページ

成功ページとして以下のindex_complete.htmlを用意します。コントローラのsuccessViewで指定したページです。

<html>
    <head>
	<title>Form Success</title>
    </head>
    <body>
	<h1>Form Success</h1>
	<c:model/>
    </body>
</html>

このページ内の以下のタグはコントローラの実行結果のモデルの内容を表形式で表示するものです。

	<c:model/>

今回の場合は、loopbackオペレーションの結果が出力されます。

エラーページ

成功ページとして以下のindex_error.htmlを用意します。コントローラのerrorViewで指定したページです。

<html>
    <head>
	<title>Form Error</title>
    </head>
    <body>
	<h1>Form Error</h1>
	<c:error/>
    </body>
</html>

このページ内の以下のタグはコントローラの実行時にエラーが発生した場合、そのエラーを表形式で表示するものです。

	<c:error/>

今回の場合は、loopbackオペレーションがエラーとなる場合に出力されます。

実行

Formページ

curlコマンドによってローカルホストの8080ポート上の/web/HelloForm を取得します。

$ curl http://localhost:8080/web/HelloForm/

以下のHTML文書が返されます。

<!DOCTYPE html>
<html>
  <head>
    <title>HelloForm</title>
  </head>
  <body>
    <form action="" method="POST">
      <input value="{&quot;name&quot;:&quot;invoke-operation&quot;,&quot;state&quot;:&quot;input&quot;,&quot;data&quot;:{}}" name="$scenario" hidden="true" />
      <input value="" name="name" />
      <button value="ok" name="$submit" id="submitbutton" type="submit">Submit</button>
      <button value="cancel" name="$submit" id="cancelbutton" type="submit">Cancel</button>
    </form>
  </body>
</html>

Web画面は以下のとおりです。

OK

$ curl http://localhost:8080/web/HelloForm/ -X POST \
--data-urlencode '$submit=ok' \
--data-urlencode 'name=abc' \
--data-urlencode '$scenario={"name":"invoke-operation","state":"input","data":{}}'
<!DOCTYPE html>
<html><head>
	<title>Form Success</title>
    </head><body>
	<h1>Form Success</h1>
	<table class="">
      <tbody><tr class=""><th scope="row" class="">$submit</th><td class="">ok</td></tr><tr class=""><th scope="row" class="">Name</th><td class="">abc</td></tr><tr class=""><th scope="row" class="">$scenario</th><td class="">{&quot;name&quot;:&quot;invoke-operation&quot;,&quot;state&quot;:&quot;input&quot;,&quot;data&quot;:{}}</td></tr></tbody>
    </table>
    
</body></html>

Web画面は以下のとおりです。

エラー

コントローラで指定したloopbackオペレーションでは、データのいずれかに「#500」のような「#」記号の後ろに数値を入れた文字列を入力すると、この数値をエラー番号としたエラーが発生するようになっています。

この機能を利用してエラーページの動作を確認してみましょう。

Webページの入力から「#500」を入れてSubmitボタンを押したときのHTTPの送信は以下のcurlで実現できます。

「name=#500」となっているところがキモです。

$ curl -v http://localhost:8080/web/HelloForm/ -X POST \
--data-urlencode '$submit=ok' \
--data-urlencode 'name=#500' \
--data-urlencode '$scenario={"name":"invoke-operation","state":"input","data":{}}'

この結果以下のHTMLが返ってきました。

<!DOCTYPE html>
<html><head>
	<title>Form Error</title>
    </head><body>
	<h1>Form Error</h1>
	<table class="">
      <tbody><tr class=""><th scope="row" class="">Code</th><td class="">500</td></tr><tr class=""><th scope="row" class="">Message</th><td class="" /></tr><tr class=""><th scope="row" class="">Top URI</th><td class="" /></tr><tr class=""><th scope="row" class="">Back URI</th><td class="" /></tr><tr class=""><th scope="row" class="">Exception Message</th><td class="" /></tr><tr class=""><th scope="row" class="">Exception Stack</th><td class="" /></tr><tr class=""><th scope="row" class="">Call Tree</th><td class="" /></tr></tbody>
    </table>
    
</body></html>

Web画面は以下のとおりです。

キャンセル

invoke-operattionシナリオでは、cancelがサブミットされるとキャンセル動作が行われます。

$ curl -v http://localhost:8080/web/HelloForm/ -X POST \
--data-urlencode '$submit=cancel' \
--data-urlencode 'name=#500' \
--data-urlencode '$scenario={"name":"invoke-operation","state":"input","data":{}}'

キャンセル動作を行うと、元のページにリダイレクトされ、新規にデータ入力の状態になります。

* Connected to localhost (::1) port 8080 (#0)
> POST /web/HelloForm HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.76.1
> Accept: */*
> Content-Length: 128
> Content-Type: application/x-www-form-urlencoded
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Date: Thu, 29 Sep 2022 01:29:31 GMT
< Content-Type: text/html;charset=utf-8
< Cache-Control: private,no-store,no-cache,must-revalidate
< Location: http://localhost:8080/web/HelloForm
< Content-Length: 0
< Server: Jetty(9.4.38.v20210224)
< 
* Connection #0 to host localhost left intact

curlの-Lスイッチで、リダイレクトをフォローしてみます。

$ curl -L http://localhost:8080/web/HelloForm/ -X POST \
--data-urlencode '$submit=cancel' \
--data-urlencode 'name=#500' \
--data-urlencode '$scenario={"name":"invoke-operation","state":"input","data":{}}'

そうすると以下のように、無事元のフォームページが返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>HelloForm</title>
  </head>
  <body>
    <form action="" method="POST">
      <input value="{&quot;name&quot;:&quot;invoke-operation&quot;,&quot;state&quot;:&quot;input&quot;,&quot;data&quot;:{}}" name="$scenario" hidden="true" />
      <input value="" name="name" />
      <button value="ok" name="$submit" id="submitbutton" type="submit">Submit</button>
      <button value="cancel" name="$submit" id="cancelbutton" type="submit">Cancel</button>
    </form>
  </body>
</html>

Web画面は以下のとおりです。

まとめ

今回はCozy WebでHTMLフォームを取り扱う方法について説明しました。

フォームを扱う場合、Webとサーバ間のインタラクションに伴う状態遷移の管理を行う処理を作るのがなかなか大変です。Cozy Webでは、コントローラに宣言的な定義をするだけで、この処理を簡単に行ってくれることが分かりました。

諸元

Cozy
0.0.7