コントローラ
ビジネスロジックはドメインモデル層で管理されます。クライアント (通常は web ブラウザ) が直接このコードを呼び出すことができないことから、ドメインオブジェクトの機能性は URI によって表されたリソースとして公開されます。
クライアントは、HTTP プロトコルによって提供された統一的な API を使用して、これらのリソースと、暗黙的にその下にあるビジネスロジックを操作します。しかし、ドメインオブジェクトとリソースのマッピングは一対一ではありません: 粒度は異なるレベルで表現され、あるリソースは仮想化されたものかもしれませんし、あるリソースは別名が定義されているかもしれません...
これは、まさにコントローラ層によって果たされる役割です: ドメインモデルオブジェクトとトランスポート層イベントの間の 接着剤 を提供します。モデル層においては、モデルオブジェクトに容易にアクセスし変更するために、コントローラは純粋な Java で書かれます。HTTP インタフェースのように、コントローラは手続き的で、リクエスト/レスポンス指向です。
コントローラ 層 は HTTP とドメインモデルの間の インピーダンスミスマッチ を減少させます。
注意
異なる戦略をもった異なるアーキテクチャモデルがあります。いくつかのプロトコルはドメインモデルオブジェクトに直接アクセスします。これは、EJB や CORBA プロトコルによく見られます。これらの場合、そのアーキテクチャスタイルは、RPC (Remote Procedure Call) を使います。これらのコミュニケーションスタイルは、web アーキテクチャとほとんど互換性がありません。
SOAP のようないくつかの技術は Web を通してドメインモデルオブジェクトへのアクセスをていきょうします。しかし、SOAP はただのRPC スタイルプロトコルであり、この場合、HTTP はトランスポートプロトコルとして使用されます。アプリケーションプロトコルではありません。
web の原則は基本的にオブジェクト指向ではありません。そのため、お気に入りの言語に HTTP を適合させる層が必要になります。
コントローラの概要
コントローラは Java のクラスであり、 controllers パッケージで管理される play.mvc.Controller のサブクラスです。
コントローラはこのようなものになります:
package controllers;
import models.Client;
import play.mvc.Controller;
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
public static void delete(Long id) {
Client client = Client.findById(id);
client.delete();
}
}
コントローラの public かつ static なそれぞれのメソッドはアクションと呼ばれます。アクションメソッドのシグネチャは以下の通りです:
public static void action_name(params...);
アクションメソッドのシグネチャに引数を定義できます。これらのパラメタは、フレームワークによって対応する HTTP パラメタから自動的に解決されます。
通常、アクションメソッドは return 構文を持ちません。アクションメソッドは result メソッドの起動によって終了します。今回の例では、テンプレートを実行して表示する render(...) が result メソッドです。
HTTP パラメータの取得
HTTP リクエストはデータを含んでいます。以下のようにしてこのデータを抽出することができます:
- URI パス: /clients/1541 という URI パターンにおいて、1541 が動的な部分です。
- クエリ文字列: /clients?id=1541.
- リクエスト本文: リクエストが HTML フォームから送信される場合、そのリクエスト本文には x-www-urlform-encoded としてエンコードされたフォームデータを含んでいます。
いずれの場合でも、Play はデータを抽出して、すべての HTTP パラメータを含む Map<String, String[]> を構築します。このマップのキーはパラメータ名です。パラメータ名は以下のようにして導出されます。
- (ルーティングで指定された) URI の動的部分の名前
- クエリ文字列から取得される名前-値のペアの名前の部分
- エンコードされた本文の内容
パラメータマップの使い方
params オブジェクトはすべてのコントローラクラスで利用できます (スーパークラス play.mvc.Controller で定義されています) 。このオブジェクトは、現在のリクエストから見つけられるすべての HTTP パラメータを含んでいます。
例えば:
public static void show() {
String id = params.get("id");
String[] names = params.getAll("names");
}
Play に型変換を指示することもできます:
public static void show() {
Long id = params.get("id", Long.class);
}
でも、ちょっと待ってください。もっと良い方法があります:)
アクションメソッドのシグネチャ
アクションメソッドのシグネチャから HTTP パラメータを直接検索することができます。Java 引数の名前は HTTP パラメータのものと同じであるに違いありません。
例えば、このリクエストでは:
/clients?id=1451
アクションメソッドは、シグネチャにおいて id 引数を宣言することによって、 id パラメータの値を検索することができます:
public static void show(String id) {
System.out.println(id);
}
String 以外の Java の型も使えます。この場合、フレームワークはパラメータの値を正しい Java 型にキャストしようとします:
public static void show(Long id) {
System.out.println(id);
}
パラメータが多値である場合は、配列引数を宣言することができます:
public static void show(Long[] id) {
for(String anId : id) {
System.out.println(anid);
}
}
コレクションも宣言することができます:
public static void show(List<Long> id) {
for(String anId : id) {
System.out.println(anid);
}
}
例外
アクションメソッド引数に対応する HTTP パラメータが見つからない場合、対応するメソッド引数はデフォルト値 (通常、オブジェクト型は null、基本データ型は 0) に設定されます。値が見つかっても、要求された Java 型に適切にキャストできない場合、バリデーションエラーのコレクションにエラーが追加され、デフォルト値が設定されます。
HTTP と Java の高度な紐付け
シンプルな型
すべての基本データ型と、そして、一般的な Java の型は自動的に紐付けられます:
int, long, boolean, char, byte, float, double, Integer, Long, Boolean, Char, String, Byte, Float, Double.
HTTP リクエスト中にパラメータが見つからないか、または自動変換に失敗した場合、オブジェクト型には null、基本データ型にはそれらのデフォルト値が設定されることに注意してください。
日付
日付の文字列表現が以下のパターンのいずれか 1 つにマッチする場合、自動的に日付オブジェクトに紐付けられます:
- yyyy-MM-dd’T’hh:mm:ss’Z' // ISO8601 + timezone
- yyyy-MM-dd’T’hh:mm:ss" // ISO8601
- yyyy-MM-dd
- yyyyMMdd’T’hhmmss
- yyyyMMddhhmmss
- dd'/‘MM’/'yyyy
- dd-MM-yyyy
- ddMMyyyy
- MMddyy
- MM-dd-yy
- MM'/‘dd’/'yy
@As アノテーションを使って日付フォーマットを指定することができます。
例えば:
archives?from=21/12/1980
public static void articlesSince(@As("dd/MM/yyyy") Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
言語によって日付フォーマットを最適化することもできます。例えば:
public static void articlesSince(@As(lang={"fr,de","*"}, value={"dd-MM-yyyy","MM-dd-yyyy"}) Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
この例の場合、フランス語とドイツ語には日付フォーマットに dd-MM-yyyy を指定し、その他の言語には MM-dd-yyyy を指定しています。言語の値をカンマで区切れることに注意してください。言語パラメータの数と値パラメータの数を合わせることが重要です。
@As アノテーションが指定されていない場合、Play! はロケールに従ったデフォルトの日付フォーマットを使用します。
使用するデフォルト日付フォーマットを設定するには、application.conf を編集し、以下のプロパティを設定します:
date.format=yyy-MM-dd
date.format.fr=dd/MM/yyyy
言語 fr も application.conf において同様に使用可能でなくてはならないことに注意してください:
application.langs=fr
このプロパティは、テンプレートにおいて ${date.format()} を使用した日付がどのようにレンダリングされるかについても影響します。
カレンダ
Play がロケールに従って Calendar オブジェクトを選択する場合を除いて、カレンダの紐付けは日付とまるっきり同じように動作します。 @Bind アノテーションを使用することもできます。
ファイル
Play によるファイルアップロードは簡単です。 multipart/form-data エンコードされたリクエストを使ってサーバにファイルをポストしたら、 java.io.File 型を使ってファイルオブジェクトを取得します:
public static void create(String comment, File attachment) {
String s3Key = S3.post(attachment);
Document doc = new Document(comment, s3Key);
doc.save();
show(doc.id);
}
作成されたファイルは、元のファイルと同じ名前になります。ファイルは一時ディレクトリに保存されて、リクエストの完了時に削除されます。このため、作成されたファイルは安全なディレクトリにコピーしなければなりません。そうでなければファイルは無くなってしまいます。
サポートされた型の配列またはコレクション
すべてのサポートされた型は配列またはオブジェクトのコレクションとして取得することができます:
public static void show(Long[] id) {
...
}
または:
public static void show(List<Long> id) {
...
}
または:
public static void show(Set<Long> id) {
...
}
POJO オブジェクトの紐付け
Play は簡単な命名規約ルールを使用することで、どんなモデルクラスでも自動的に紐付けることができます。
public static void create(Client client ) {
client.save();
show(client);
}
このアクションを使って client を作るクエリ文字列は次のようになるでしょう:
?client.name=Zenexity&[email protected]
Play は Client インスタンスを作成し、HTTP パラメータの名前を Client オブジェクトのプロパティに解決します。解決できないパラメータ名は安全に無視されます。型のミスマッチも安全に無視されます。
パラメータの紐付けは再帰的に行われるので、完全なオブジェクトグラフを扱うことができます:
?client.name=Zenexity
&client.address.street=64+rue+taitbout
&client.address.zip=75009
&client.address.country=France
モデルオブジェクトのリストを更新するには、配列記法とオブジェクトを参照する ID を使用してください。例えば、Client モデルが List Customer customers として宣言された Customer モデルのリストを持つと想像してください。Customer のリストを更新するために、以下のようなクエリ文字列を提供するでしょう:
?client.customers[0].id=123
&client.customers[1].id=456
&client.customers[2].id=789
JPA オブジェクトの紐付け
HTTP と Java の紐付けを使って、自動的に JPA オブジェクトを紐付けることができます。
HTTP パラメータ中に user.id フィールドを提供することできます。Play は id フィールドを見つけると、user を編集する前に、データベースからマッチするインスタンスをロードします。そして、HTTP リクエストで提供された他のパラメータを適用します。このため、直接 user を保存することができます。
public static void save(User user) {
user.save(); // ok with 1.0.1
}
カスタムバインディング
バインディングシステムはより多くのカスタマイズをサポートするようになりました。
@play.data.binding.As
最初に紹介するのは、文脈的にバインディングを構成する新しい @play.data.binding.As アノテーションです。これは例えば、 DateBinder によって使用される日付のフォーマットを指定するために使います:
public static void update(@As("dd/MM/yyyy") Date updatedAt) {
...
}
この @As アノテーションは国際化もサポートします。これは、ロケールごとに特定のアノテーションを提供できることを意味しています:
public static void update(
@As(
lang={"fr,de","en","*"},
value={"dd/MM/yyyy","dd-MM-yyyy","MM-dd-yy"}
)
Date updatedAt
) {
...
}
この @As アノテーションは、これをサポートするすべてのバインダと共に動作します。以下は、 ListBinder を使用する例です:
public static void update(@As(",") List<String> items) {
...
}
このバインダは、単純にカンマで分けられた String を List にバイドします。
@play.data.binding.NoBinding
新たに追加された @play.data.binding.NoBinding は、バインド非対象フィールドをマークし、潜在的なセキュリティ問題を解決します。以下に例を示します:
public class User extends Model {
@NoBinding("profile") public boolean isAdmin;
@As("dd, MM yyyy") Date birthDate;
public String name;
}
public static void editProfile(@As("profile") User user) {
...
}
このようにすると、例え悪意あるユーザが偽のフォームから user.isAdmin=true というフィールドを含めてポストしたとしても、 isAdmin フィールドは決して editProfile アクションからはバインドされません。
play.data.binding.TypeBinder
@As アノテーションを使って完全に独自のバインダを定義することができます。独自のバインダは、プロジェクト内にて TypeBinder のサブクラスとして定義されます。以下に例を示します:
public class MyCustomStringBinder implements TypeBinder<String> {
public Object bind(String name, Annotation[] anns, String value, Class clazz) {
return "!!" + value + "!!";
}
}
以下のようにして、いずれのアクションにおいてもこのバインダを使用することができます:
public static void anyAction(@As(binder=MyCustomStringBinder.class) String name) {
...
}
@play.data.binding.Global
対応する型にだけ適用されるグローバルなカスタムバインダを定義することもできます。例えば、以下のようにして java.awt.Point クラスにバインドできるバインダを定義することができます:
@Global
public class PointBinder implements TypeBinder<Point> {
public Object bind(String name, Annotation[] anns, String value, Class class) {
String[] values = value.split(",");
return new Point(
Integer.parseInt(values[0]),
Integer.parseInt(values[1])
);
}
}
見てのとおり、グローバルバインダは @play.data.binding.Global でアノテーションされた古典的なバインダです。外部モジュールは再利用可能な拡張バインダを定義することで、プロジェクトにバインダを提供することができます。
戻り値の型
アクションメソッドは、HTTP レスポンスを生成しなければなりません。HTTP レスポンスを生成するもっとも簡単な方法は、Result オブジェクトを発行することです。Result オブジェクトが発行されると、通常の実行フローは中断され、メソッドはリターンされます。
例えば:
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
System.out.println("This message will never be displayed !");
}
render(…) メソッドは Result オブジェクトを発行し、以降のメソッドは実行しません。
テキスト内容の返却
renderText(…) メソッドは基本的な HTTP レスポンスに何らかのテキストを直接書き込むシンプルな Result イベントを発行します。
例えば:
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderText(unreadMessages);
}
Java 標準のフォーマット構文を使ってテキストメッセージをフォーマットすることができます:
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderText("There are %s unread messages", unreadMessages);
}
テンプレートの実行
生成する内容が複雑である場合、レスポンスの内容を生成するためにテンプレートを使用するべきです。
public class Clients extends Controller {
public static void index() {
render();
}
}
テンプレート名は Play の規約から自動的に推測されます。デフォルトのテンプレートのパスは、コントローラとアクションの名前を使って解決されます。
この例で呼び出されるテンプレートは以下の通りです:
app/views/Clients/index.html
テンプレートスコープへの値の追加
テンプレートはしばしばデータを必要とします。 renderArgs オブジェクトを使用することでテンプレートスコープにこれらのデータを追加することができます:
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
renderArgs.put("client", client);
render();
}
}
テンプレートが実行される間、この client 変数が定義されます。
例えば、以下のようになります。:
<h1>Client ${client.name}</h1>
テンプレートスコープにデータを追加するより簡単な方法
render(…) メソッドの引数を使って、直接テンプレートにデータを渡すことができます:
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
この場合、テンプレートからアクセスする変数は、Java のローカル変数と同じ名前になります。
複数の変数を渡すこともできます:
public static void show(Long id) {
Client client = Client.findById(id);
render(id, client);
}
重要!
この方法で渡せるのは ローカル変数 だけです。
別のテンプレートの指定
デフォルトのテンプレートを使用したくない場合、 renderTemplate(…) メソッドの第一引数にテンプレート名を渡して使うことで、独自のテンプレートファイルを指定することができます。
例えば、以下のようにします:
public static void show(Long id) {
Client client = Client.findById(id);
renderTemplate("Clients/showClient.html", id, client);
}
別の URL へのリダイレクト
redirect(…) メソッドは HTTP Redirect レスポンスを生成する Redirect イベントを発行します。
public static void index() {
redirect("http://www.zenexity.fr");
}
アクションチェーン
Servlet API の forward に該当するものはありません。HTTP リクエストは 1 つのアクションだけを呼び出します。別のアクションを呼び出す必要がある場合は、そのアクションを呼び出すことができる URL にブラウザをリダイレクトさせなければなりません。このようにすることで、ブラウザの URL は常に実行されるアクションと一致し、 戻る/進む/更新 の管理がはるかに簡単になります。
単に Java のやり方でアクションメソッドを実行するだけで、どんなアクションに対しても Redirect レスポンスを送ることができます。Java の呼び出しはフレームワークによってインターセプトされ、適切な HTTP Redirect が生成されます。
例えば:
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
public static void create(String name) {
Client client = new Client(name);
client.save();
show(client.id);
}
}
以下のような routes ファイルの場合:
GET /clients/{id} Clients.show
POST /clients Clients.create
- ブラウザは /clients URL に POST を送ります。
- Router は Clients コントローラの create アクションを起動します。
- create アクションは show アクションを直接呼び出します。
- Java 呼び出しはインターセプトされ、Router は id パラメータと共に Clients.show を実行するために必要な URL をリバース生成します。
- HTTP レスポンスは 302 Location:/clients/3132 です。
- ブラウザは GET /clients/3132 を発行します。
- …
インターセプション
コントローラにインターセプタを定義することができます。インターセプタは、コントローラクラスとその子孫におけるすべてのアクションに対して実行されます。すべてのアクションに共通する処理: ユーザ認証されていることの確認、リクエストスコープ情報のロード... を定義するのは便利なやり方です。
これらのメソッドは、 static ですが、 public である必要はありません。適切なインターセプションマーカでこれらのメソッドを注釈しなければなりません。
@Before
@Before アノテーションで注釈されたメソッドは、このコントローラにおけるすべてのアクション呼び出しの前に実行されます。
例えば、セキュリティチェックを行うには以下のようにします:
public class Admin extends Application {
@Before
static void checkAuthentification() {
if(session.get("user") == null) login();
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
...
}
@Before メソッドにすべてのアクション呼び出しをインターセプトさせたくない場合、除外アクションのリストを指定することができます:
public class Admin extends Application {
@Before(unless="login")
static void checkAuthentification() {
if(session.get("user") == null) login();
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
...
}
一連のアクション呼び出しを @Before メソッドでインターセプトさせたい場合は、only パラメータを指定することができます :
public class Admin extends Application {
@Before(only={"login","logout"})
static void doSomething() {
...
}
...
}
unless パラメータと only パラメータは @After, @Before と @Finally で使うことができます。
@After
@After アノテーションで注釈されたメソッドは、このコントローラにおけるすべてのアクション呼び出しの後に実行されます。
public class Admin extends Application {
@After
static void log() {
Logger.info("Action executed ...");
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
...
}
@Finally
@Finally アノテーションで注釈されたメソッドは、このコントローラにおけるすべてのアクション呼び出しの結果が確定された後に実行されます。
public class Admin extends Application {
@Finally
static void log() {
Logger.info("Response contains : " + response.out);
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
...
}
コントローラ階層
コントローラクラスが別のコントローラのクラスのサブクラスである場合、インターセプションは完全なクラス階層に対して適用されます。
@With アノテーションによる更なるインターセプタの追加
Java は多重継承を認めないので、インターセプタの適用はクラス階層に制限されたとても限定的なものになりがちです。しかし、完全に異なるクラス中にいくつかのインターセプタを定義し、 @With アノテーションを使用していかなるコントローラにもこれらをリンクすることができます。
例えば、以下のようにします:
public class Secure extends Controller {
@Before
static void checkAuthenticated() {
if(!session.containsKey("user")) {
unAuthorized();
}
}
}
そして、別のコントローラにおいて、以下のようにします:
@With(Secure.class)
public class Admin extends Application {
...
}
Session と Flash スコープ
複数の HTTP リクエストにまたがってデータを保持しなければならない場合、Session または Flash スコープにそれらを保存することができます。Session に保存されたデータは、ユーザセッションにおける全ての間で利用可能であり、Flash スコープに保存されたデータは、次のリクエストにおいてのみ利用可能です。
Session と Flash のデータはサーバに保存されず、Cookie メカニズムを使って次の HTTP リクエストに追加されることを理解するのは重要です。このため、そのデータサイズはとても制限 (最大で 4 KB) され、また、文字列しか保存できません。
もちろん、クライアントが cookie のデータを変更できない (変更した場合は無効にされる) よう、cookie は秘密鍵で署名されます。Play の Session は、キャッシュとして使用されることを目的としません。特定のセッションに関連するいくつかのデータをキャッシュする必要がある場合は、Play 内蔵のキャッシュ機構と、特定のユーザセッションにそれらを継続して紐付けるための session.getId() を使用することができます。
例えば、以下のようにします:
public static void index() {
List messages = Cache.get(session.getId() + "-messages", List.class);
if(messages == null) {
// Cache miss
messages = Message.findByUser(session.get("user"));
Cache.set(session.getId() + "-messages", messages, "30mn");
}
render(messages);
}
キャッシュには、古典的な Servlet HTTP セッションオブジェクトとは異なる意味があります。これらのオブジェクトが常にキャッシュにあるとは仮定できません。そのため、キャッシュに失敗した場合について扱わなければなりませんが、アプリケーションは完全にステートレスであり続けます。
考察を続けます
MVC モデルの次の重要な層は、Play が テンプレートエンジン により、効率的なテンプレートシステムを提供する View 層です。