Tuesday, January 15, 2013

ExtJS 覚書 #2 validation

Ext.data.Modelを使ったクライアントサイドバリデーション

データモデルにvalidationの定義を含めることができる。
Ext.define('User', {
    extend: 'Ext.data.Model',
    fields: [
        {name: 'name',     type: 'string'},
        {name: 'age',      type: 'int'},
        {name: 'phone',    type: 'string'},
        {name: 'gender',   type: 'string'},
        {name: 'username', type: 'string'},
        {name: 'alive',    type: 'boolean', defaultValue: true}
    ],
    validations: [
        {type: 'presence',  field: 'age'},
        {type: 'length',    field: 'name',     min: 2},
        {type: 'inclusion', field: 'gender',   list: ['Male', 'Female']},
        {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
        {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
    ]
});

バリデートはExt.data.Modelのvalidate()で行う。
結果は、Ext.data.Errorsが返される。

var errors = instance.validate();

サーバサイドバリデーション

サーバサイドのバリデーションの結果をExt.data.Model.validate()と同じ形で得るためには、データモ
デルのproxyにreaderを正しく設定する必要がある。
 Ext.define('Employee', {
    extend : 'Ext.data.Model',
    idProperty : 'id',
    fields : [
        { name : 'id', type : 'string' },
        { name : 'firstName', type : 'string' },
        { name : 'lastName', type : 'string' },
        { name : 'gender', type : 'string' }
    ],
    proxy : {
        url : "/コンテキストルート/employeeService",
        type : "rest",
        reader : {
            type : 'json',
            messageProperty : 'errors'
        }
    }
});

このデータモデルのsave()メソッドに対して、サーバ側バリデーションを返す場合、以下のようなJSONデータをサーバは返さなければならない。
{
    'success' : false,
    'errors' : { 'id' : 'Error Message1', 'firstName' : 'Error Message 2', 'lastName' : 'Error Message3' }
}

readerのmessagePropertyの値と、JSONデータのエラーのプロパティを一致させること。
バリデーション結果は、モデルのload(), save(), destroy()メソッドのsuccess, failureコールバックの引数 operation (ext.data.Operation) から得られる。
var emp = Ext.ModelManager.getModel('Employee');
emp.set('123', 'Fidel', 'Castro', 'Male');
emp.save({
    waitMsg : 'Saving...',
    success : function(operation){ },
    failure : function(operation){
        var errors = operation.getError();
    }
});

validationの結果をformに適用する
formのmarkInvalid()を使ってvalidationの結果をformに適用する事が出来る。
var errors = instance.validate();
form.markInvalid(errors);

jax-rs(jersey)覚書 #4 自前のシリアライザー : 日付フォーマットを指定する

jax-rsはオリコウさんで、pojoがそのままjsonになって飛んでいきます。
ところが、java.uti.Dateのフィールドがイケてない形で行ってしまいました。
その結果、Ext JSのグリッドやformのdate型のフィールドに表示されず。
jsonにシリアライズするときにフォーマットの指定できないかしらと調べてみた。

まず、org.codehaus.jackson.JsonGenerator を継承したシリアライザーを作る。
serializeというabstractのメソッドを実装してあげればよい。
Date型を yyyy/MM/DD にするならこんな感じ。

import java.io.IOException;
import java.text.SimpleDateFormat;

import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;

public class MyDateSerializer extends JsonSerializer<Object> {

    @Override
    public void serialize(Object value, JsonGenerator gen,
   SerializerProvider provider) throws IOException,
   JsonProcessingException {

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd");
        String formattedDate = formatter.format(value);

        gen.writeString(formattedDate);
  
    }
}

そして、jsonに変身するクラスの、作ったシリアライザーを適用したいフィールドを @JsonSerialize アノテーションで修飾する
public class Person {
 
    private String firstName;
    private String lastName
    
    @JsonSerialize(using = MyDateSerializer.class)
    private Date birthday;
    

こんな感じでした。

ExtJS覚書 #1 Ext.data.ModelでREST


ExtJSでは、Ext.data.Modelを拡張したデータモデルをを使って、正しくRESTでJSONデータをやり取りする事できる。
データのライフサイクルまでそれなりに管理してくれていて便利ではあるが、何をしてくれるかを正しく理解する必要がある。
ここでは、モデルとフォーム(Ext.form.Panel)を使って一意のデータをCRUDする方法を説明する。

データモデル(Ext.data.Model)とCRUD

id, 姓, 名, 性別 という要素を持つEmployeeというデータをCRUDするRESTのクライアントの定義は以下のようになる。
idPropertyはこのデータモデルのIDとなるプロパティを指定する。
proxyのtypeに"rest"を指定することにより、Ext.data.proxy.Restが使われる。正しくRESTの型にはまる。

Ext.define('Employee', {
    extend : 'Ext.data.Model',
    idProperty : 'id',
    fields : [ 
        { name : 'id', type : 'string' },
        { name : 'firstName', type : 'string' },
        { name : 'lastName', type : 'string' },
        { name : 'gender', type : 'string' }
    ],
    proxy : {
        url : "/コンテキストルート/employeeService",
        type : "rest"
    }
});

データをロードする(loadメソッド)

このデータモデルを使ってデータをロードするコードは以下の通り。
var emp = Ext.ModelManager.getModel('Employee');
emp.load('123', {
        waitMsg : 'Loading...',
        success : function(record, operation){ },
        failure : function(record, operation){ }
        });

これで、 http://ホスト/コンテキストルート/employeeService/123 にGETリクエストが送られる。
success, failure関数の引数recordには、得られたデータ(Employeeのインスタンス)が入る。

データモデルをformにセットする

form(Ext.form.Basic)のloadRecord()を使って、データモデルをformにセットする事が出来る。
この時データモデルの内容で、formの要素は書きかえられる。
データモデルのfieldと、formの要素の名前(と型)が一致していればOK。
var emp = Ext.ModelManager.getModel('Employee');
emp.load('123', {
        waitMsg : 'Loading...',
        success : function(record, operation){
            empForm.getForm().loadRecord(record);
        },
        failure : function(record, operation){ }
        });

データを新規作成する(saveメソッド)

Ext.ModelManager.create()を使ってデータモデルを新規作成する。
save()メソッドでJSONデータをPOSTする。
var emp = Ext.ModelManager.create('Employee',{id : '123', firstName : 'Fidel', lastName : 'Castro', genger: 'male'});
emp.save({
        waitMsg : 'Saving...',
        success : function(record, operation){ },
        failure : function(record, operation){ }
        });

これで、 http://ホストコンテキストルート/employeeService に、JSONデータがPOSTされる。
success,failure時の関数の引数 record はにデータモデルのインスタンス、operationには、ext.data.Operationのインスタンスがセットされる。
ここで、setId()メソッドは使ってはならない。この件については『データを更新する』のセクションで説明する。
成功時、operationのconfig、"action"には"create"が入っている。

formのデータを使って新規作成する

フォームのデータを使って作る場合はExt.ModelManager.create()の第2引数をform.getValues()にすればよい。
formの要素と、データモデルのフィールドの名前と型は合わせておく。
var form = this.up('form').getForm();
var emp = Ext.ModelManager.create('Employee', empForm.getForm().getValues(false));
emp.save({
        waitMsg : 'Saving...',
        success : function(record, operation){
            empForm.getForm().loadRecord(record);
        },
        failure : function(record, operation){ }
        });

データを更新する(saveメソッド)

load()されたModelでsave()メソッドを使うとJSONデータがPUTされる。
この時のURLはproxyで設定したURLに"/id文字列"が付いたものになる。
上記の例で、idPropertyに設定したフィールド(デフォルトは"id")の値が"123"であった場合、
http://ホスト/コンテキストルート/employeeService/123
となる。
新規作成もsave()メソッドを使う、save()メソッドによってPOST(create)されるのかPUT(update)されるのかは、phantomというプロパティがtrueかどうかで決まる。trueならPOSTとなる。
このphantomというプロパティは、Modelのidプロパティ(デフォルトはid)に値が入っているかどうかで決まる。
model.setId(値);を実行するとphantomがfalseにセットされるようになっている。
どうもExt.data.Modelは、

  • idは一つ
  • idは新規作成時に付与される

というポリシーで作られている。
複合キーであったり、id(コード等)をユーザが決める場合はこの事を意識してプログラミングする必要がある。


formからデータモデルのインスタンスを取り出してPUTする

form(Ext.form.Basic)のgetRecord()を使って、formからModelのインスタンスを得る事が出来る。form.loadRecord()によってセットされているModelが得られる。
フォームにModelがロードされていない状態(loadRecord()されていないform)では、getRecord()はundefinedを返す。
ただし、ここで得られたrecordは、フォーム上での編集が反映されていない(編集前のデータが残っているということ)。
form(Ext.form.Basic)のupdateRecord()で、現在のフォームのデータを反映する事が出来る。

var emp = form.getRecord();    /* フォームよりデータモデルのインスタンスを得る */
form.updateRecord(emp);        /* 現在のフォーム上のデータでモデルの内容を更新する */
emp.save({
        waitMsg : 'Saving...',
        success : function(record, operation){ },
        failure : function(record, operation){ }
        });

成功時、operationのconfig、"action"には"update"が入っている。

データを削除する(destroy()メソッド) 

削除するコードは以下の通り。
var emp = Ext.ModelManager.getModel('Employee');
emp.set('123', 'Fidel', 'Castro', 'Male');
emp.destroy({
        waitMsg : 'Deleting...',
        success : function(operation){ },
        failure : function(operation){ }
        });

これで、 http://ホスト/コンテキストルート/employeeService/123 にDELETEリクエストが送られる。
saveと同様に、JSONデータが送られる。
123の部分には、データモデル定義のidPropertyに指定したフィールドの値がセットされる。

jax-rs(jersey)覚書 #3 bean validation


これはjax-rsというわけではないが、セットで使うことが多いと思う。

JSR303 bean validationで、文字列の長さ、数値の大きさ、日付、nullなどの値チェックはほとんど可能。
使い方は非常に簡単で、validationしたいクラスのフィールドにアノテーションを設定すればよい。
javax.validation.constraintsには以下のアノテーションが定義されている。

  • @AssertFalse,@AssertTrue
  • @DecimalMax,@DecimalMin
  • @Max,@Min
  • @Digits
  • @Future,@Past
  • @Null,@NotNull
  • @Pattern
  • @Size

文字列の長さをチェックするには以下のようにする。
public class Employee {
    @Size(max=3, message="3桁までですよ!")
    private String empId;
    private String firstName;
    private String LastName;
    private String gender;

validationは、インターフェース javax.validation.Validator を使う。
SpringにDIさせるにはbean定義ファイルに以下の設定を行う。

;

実際にvalidationする例は以下の通り

@Service
public class EmployeeService {
    @Autowired
    private Validator validator;
    
    private void validateEmployee(Employee emp) {
        Set<Constraintviolation<Employee>> violations = validator.validate(emp);
    
        for (ConstraintViolation<employee> violation : violations) {
             System.out.print(violation.getPropertyPath().toString());
             System.out.print(" : ");
             System.out.println(violation.getMessage());
        }
    }

javax.validation.Validator.validate()で、javax.validation.ConstraintViolationのSetが得られる。
ConstraintViolation.getPropertyPath().toString()で、フィールド名が得られる。
ConstraintViolation.getMessage()で、アノテーションで設定したメッセージが得られる。

メッセージをプロパティファイルから得ることもできる(そうすべき)。
デフォルトのプロパティファイル名はValidationMessages.properties 。
public class Employee {
    @Size(max=3, message="{emp.id.size}")
    private String empId;

jax-rs(jersey)覚書 #2 基本編


サービスのパスの設定

@Pathアノテーション

クラス、メソッドに指定して、RESTサービスのパス(URL)を決める。
@Path("employee")
public class EmpService{
    
    @GET
    @Path("service1")
    public Response service1() {
この場合サービスのURLは
http://ホスト/コンテキストルート/employee/service1
となる。

可変パスの扱い
@Pathと@PathParam

RESTでは”一意なURL”というのがが基本的な考え方としてある。URLのパスにIDを含める形になる(”サービス/{id} ”というような形)。
jax-rsではパスの値が可変なURLを扱うことができる。
@Path("employee")
public class EmpService{
    
    @GET
    @Path("{id}")
    public Response service1(@PathParam String id) {
これで、
http://ホスト/コンテキストルート/employee/123
にアクセスした場合、 @PathParamアノテーションで修飾した引数 id に"123"が入ってくる。

複合キーならどうするか?

複合キーを"-"等で繋いだ場合、は以下のような形になる。
http://ホスト/コンテキストルート/employee/123-111 の場合

@Path("employee")
public class EmpService{
    
    @GET
    @Path("{id1}-{id2}")
    public Response service1(@PathParam String id1, @PathParam Strind id2) {


HTTPメソッドの指定

@GET, @PUT, @POST, @DELETE, @HEADアノテーションを使って、どのHTTPメソッドで受けるかを指定する。
同一パスでもHTTPメソッドによって処理を切り分けることができる。
@Path("employee")
public class EmpService{
    
    @GET
    @Path("{id}")
    public Response get(@PathParam String id) {
        
    }
    
    @PUT
    @Path("{id}")
    public Response update(@PathParam String id) {
        
    }
    
    @POST
    @Path("{id}")
    public Response insert(@PathParam String id) {
        
    }
    
    @DELETE
    @Path("{id}")
    public Response delete(@PathParam String id) {
        
    }
}
この場合、同じURL
http://ホスト/コンテキストルート/employee/123
を、HTTPメソッドを分けて使うことができる。正しいRESTの形はこのように実現できる。
実装されていないメソッドでアクセスがあった場合、405 Method Not Allowed がクライアントに返される。

POST,PUTされる様々なContent-TypeのデータをJavaオブジェクトへ変換する

@Consumesアノテーションで受け取るデータのContent-Typeを指定することができる。
たとえば以下のJSONデータをPOSTされて、Employeeというクラス(Value Object)にマッピングする場合は以下のようになる。

{ "empId" : "123", "firstName" : "Oscar" , "lastName" : "Jarjays", "gender" : "female" }

@Path("employee")
public class EmpService{

 @POST
 @Consumes({ MediaType.APPLICATION_JSON })
 public Response post(Employee employee) {

public class Employee {

    private String empId;
    private String firstName;
    private String LastName;
    private String gender;
    
    public String getEmpId() {
        return empId;
    }
    public void setEmpId(String empId) {
        this.empId = empId;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return LastName;
    }
    public void setLastName(String lastName) {
        LastName = lastName;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }

}
JSON以外のContent-Typeにも対応している。

  • application/json : MediaType.APPLICATION_JSON
  • application/xml : MediaType.APPLICATION_XML
  • application/x-www-form-urlencoded : MediaType.APPLICATION_FORM_URLENCODED
  • application/octet-stream : MediaType.APPLICATION_OCTET_STREAM
  • multipart/form-data : MediaType.MULTIPART_FORM_DATA
  • text/plain : MediaType.TEXT_PLAIN
  • etc..

Javaオブジェクトを様々なContent-Typeに変換してレスポンスを返す


@Producesアノテーションでレスポンスとして返すContent-Typeを指定する。
javax.ws.rs.core.Responseを使って、実際に変換を行ってレスポンスを返す。
EmployeeをJSONにして返す場合は以下のようになる。
@Path("myRestService")
public class MyRestService {
    
    @GET
    @Path("{id}")
    @Produces({ MediaType.APPLICATION_JSON })
    public Response get(@PathParam(name = "id") String id) {
        
        Employee emp = getEmp(id);
        return Response.ok(emp, MediaType.APPLICATION_JSON).build();
    }

Response.ok()ではHTTPステータスコード200を返す。

OK以外のレスポンスを返す
javax.ws.rs.core.Response.status() を使う。


  • status(int status)
  • status(Response.Status status)
  • status(Response.StatusType status)


  • Response.status(404);
  • Response.status(500);
  • Response.status(Status.FORBIDDEN);
  • Response.status(Status.INTERNAL_SERVER_ERROR);


クエリパラメータを扱う

http://ホスト/コンテキストルート/myRestService?param1=value1&param2=value2
という形で、パラメータを指定した場合は、引数を@QueryParamアノテーションで修飾することによって受け取ることができる。
@Path("myRestService")
public class MyRestService {
    
    @GET
    public Response get(@QueryParam(value="param1") param1, @QueryParam(value="param2" param2)) {

この方法だと、パラメータ値一つ一つをバラバラに受け取ることになる。
クラスにマッピングする方法もある。

まず、パラメータを受け取るクラスのフィールドに@QueryParamアノテーションをつける。
public class Employee {

    @QueryParam("id")
    String id;
    
    @QueryParam("firstName")
    String firstName;

    @QueryParam("lastName");
    String lastName;

サービスのメソッドの引数に@Contextアノテーションをつけたcom.sun.jersey.api.core.ResourceContextを指定する。
ResourceContext.getResource()にパラメータを格納するクラスを指定して、インスタンスを得ることができる。
これは@QueryParamだけでなく、@PathParam等にも使える。

@Path("myRestService")
public class MyRestService {
    
    @GET
    public Response get(@Context ResourceContext rc)) {
        
        Employee emp = rc.getResource(Employee.class);
このやり方の問題は、引数がResourceContextになってしまって、欲しいパラメータはResourceContextから得ることになってUnit Testがやりにくくなってしまう。
まとまった数のパラメータをPOJOで受け取りたいのであれば、POSTを使ってPOJOで受け取る方が良さそう。

jax-rs(jersey)覚書 #1 pom.xmlとweb.xml


jax-rs(jersey)あたりのネタはすでに色々落ちてるのですが、せっかくなので書いておくことにします。

mavenのdependency


  • group id: com.sun.jersey
  • artifact id:  jersey-core, jersey-server, jersey-json, jersey-grizzly2, jersey-spring
  • version: 1.16 (2013/01/07時点)

mavenでjersey-springの1.16を指定するとspring3.0のモノが落ちてくる。Springは3.1を使うのでexcludeしている。


    com.sun.jersey
    jersey-core
    1.16


    com.sun.jersey
    jersey-server
    1.16


    com.sun.jersey
    jersey-json
    1.16


    com.sun.jersey
    jersey-grizzly2
    1.16


    com.sun.jersey.contribs
    jersey-spring
    1.16
    
        
            org.springframework
            spring
        
        
            org.springframework
            spring-core
        
        
            org.springframework
            spring-web
        
        
            org.springframework
            spring-beans
        
        
            org.springframework
            spring-context
        
    



servletの設定(web.xml)

init-paramでcom.sun.jersey.api.json.POJOMappingFeatureをtrueに設定しないと、JSON(XML)とPOJOとのマッピングがされないので注意。
com.sun.jersey.config.property.packagesで、RESTサービスになるクラスのパッケージを指定する。


    Jersey REST Service
    com.sun.jersey.spi.spring.container.servlet.SpringServlet
    
        com.sun.jersey.api.json.POJOMappingFeature
        true
    
    
        com.sun.jersey.config.property.resorceConfigClass
        com.sun.jersey.api.core.PackagesResourceConfig
    
    
        com.sun.jersey.config.property.packages
        jp.onestepbeyond.ckndemo0.service.rest
    
    1

    

    Jersey REST Service
    /rest/*