GroovyでJSON返すAPIサーバを作る(Embedded Jetty)

こちらのAPIサーバの記事に感激して、JSON返すバージョンにしてみた。
http://qiita.com/kimromi/items/1c797b1f53fbc1412c8d

一旦分かってしまえばお手軽なのがGroovyの良いところだと思う。

# 方針

* GroovyでAPIサーバを作る
* 設定ファイルは書かかない
* 戻り値はJSONにする

# コード

## サーバ

追加したのはjersey-jsonのインストールと、
com.sun.jersey.api.json.POJOMappingFeatureの設定。

```server.groovy
@Grab('org.eclipse.jetty.aggregate:jetty-all:9.2.6.v20141205')
@Grab('com.sun.jersey:jersey-server:1.18.3')
@Grab('com.sun.jersey:jersey-servlet:1.18.3')
@Grab('com.sun.jersey:jersey-core:1.18.3')
@Grab('com.sun.jersey:jersey-json:1.18.3')

import com.sun.jersey.spi.container.servlet.ServletContainer

import org.eclipse.jetty.server.handler.ContextHandler
import org.eclipse.jetty.server.handler.HandlerList
import org.eclipse.jetty.server.handler.ResourceHandler
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder
import org.eclipse.jetty.server.Server


/*----- jersey -----*/
//JAX-RSのresource、providerに含めるクラスを指定
def ResourceClass = [
    HelloResource.class,
]
def servletContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
def servletHolder = new ServletHolder(ServletContainer.class);

//サーブレットとして動作させるクラス名を指定（"HogeClass;FugaClass;…"の形）
servletHolder.setInitParameters([
    "com.sun.jersey.config.property.resourceConfigClass": "com.sun.jersey.api.core.ClassNamesResourceConfig",
    "com.sun.jersey.config.property.classnames": ResourceClass.collect{it.getName()}.join(";"),
    "com.sun.jersey.api.json.POJOMappingFeature": "true"
])
servletContext.setContextPath("/api")        //URL設定
servletContext.addServlet(servletHolder, "/*")

/*----- サーバ起動 -----*/
def server = new Server(8888);               //ポート設定
//コンテキスト定義をサーバにハンドル
def handlerList = new HandlerList()
handlerList.setHandlers(servletContext)
server.setHandler(handlerList)

println "APIサーバーを起動します"
server.start()                               //サーバ起動

/*----- 終了時 -----*/
addShutdownHook {
  println "APIサーバーを停止します"
  server.stop()
}
```

## API
やっていること

* APIに対応するメソッドの戻り値をデータクラス(DTO)の型にする
* MediaTypeにAPPLICATION_JSONを指定する

これで上手く変換できる。

```HelloResource.groovy
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType

//TODOこれ不要では？★
import HelloResultDto

@Path("/")
class HelloResource {

    @GET
    @Path("/hello")
    String getHello() {
        return "hello!"
    }

    @GET
    @Path("/helloJson")
    @Produces(MediaType.APPLICATION_JSON)
    public HelloResultDto helloJson() {
        HelloResultDto dto = new HelloResultDto()
        dto.name1 = "AAA"
        dto.name2 = "BBB"
        dto.name3 = "CCC"
        dto.name4 = "DDD"
        return dto
    }

    @GET
    @Path("/helloJson2")
    @Produces(MediaType.APPLICATION_JSON)
    public HelloResultDto helloJson2() {
        return new HelloResultDto("AAA", "BBB", "CCC", "DDD")
    }

    // JSONのリストを返す
    @GET
    @Path("/helloJsonList")
    @Produces(MediaType.APPLICATION_JSON)
    public List<HelloResultDto> helloJsonList() {
        def jsonList = new ArrayList()
        jsonList.add( new HelloResultDto("AAA", "BBB", "CCC", "DDD") )
        jsonList.add( new HelloResultDto("EEE", "FFF", "GGG", "HHH") )
        return jsonList
    }

    @GET
    @Path("/error")
    String getError() {
        throw new HelloException()
    }

    @GET
    @Path("/error2")
    String getError2() {
        throw new HelloException("P", "O", "I", "!")
    }
}
```

## JSON戻り値用のDTOクラス

```HelloResultDto.groovy
import javax.xml.bind.annotation.XmlRootElement

@XmlRootElement
class HelloResultDto {
    String name1
    String name2
    String name3
    String name4

    //引数なしでもnewできるようにする
    HelloResultDto() {
    }

    HelloResultDto(name1, name2, name3, name4) {
        this.name1 = name1
        this.name2 = name2
        this.name3 = name3
        this.name4 = name4
    }
}
```

## エラー処理用例外クラス

```HelloException.groovy
import javax.ws.rs.core.Response
import javax.ws.rs.core.Response.ResponseBuilder
import javax.ws.rs.core.Response.Status
import javax.ws.rs.WebApplicationException

class HelloException extends WebApplicationException {
    HelloException() {
        //ステータス404を入れる
        super(Response
            .status(Status.NOT_FOUND)
            .build()
        )
    }

    HelloException(name1, name2, name3, name4) {
        super(Response
            //ステータス400を入れる
            .status(Status.BAD_REQUEST)
            .entity(new HelloResultDto(name1, name2, name3, name4))
            //JSONとして返す
            .type("application/json")
            .build()
        )
    }
}
```
# 動作確認

## サーバ起動

```bash
groovy server.groovy
```

## HTTPアクセス 正常系

```
http://localhost:8888/api/helloJson
```
### 出力

```
{"name1":"AAA","name2":"BBB","name3":"CCC","name4":"DDD"}
```

## 正常系その２

```
http://localhost:8888/api/helloJson
```
### 出力
さっきのと同じ

```
{"name1":"AAA","name2":"BBB","name3":"CCC","name4":"DDD"}
```

## 異常系

```
http://localhost:8888/api/error
```
### 出力
何も出ない。
Chromeの開発者ツールで見ると、コンソールログからHTTPステータスが404に設定できていることが分かる。

```コンソールログ
Failed to load resource: the server responded with a status of 404 (Not Found)
```

## 異常系その２
今度は応答JSON有り。

```
http://localhost:8888/api/error2
```
### 出力
Chromeの開発者ツールで見ると、コンソールログからHTTPステータスが400に設定できていることが分かる。

```
{"name1":"P","name2":"O","name3":"I","name4":"!"}
```

# 参考

Embedded JettyをGroovyで使う話を参考にしました。
http://qiita.com/kimromi/items/1c797b1f53fbc1412c8d

JerseyでJSON返す話を参考にしました。
http://qiita.com/mmmm/items/d0e78daa2cb9fab51ee9

JerseyでJSON返す時のマッピングエラーの解決策の参考にしました。
http://stackoverflow.com/questions/13108161/a-message-body-writer-for-java-class-not-found

例外クラスの書き方の参考にしました。
(Example 7.5. Application specific exception implementation)
https://jersey.java.net/documentation/latest/representations.html#d0e6632

可変パスの話を追記したい
http://www.onestepbeyond.jp/2013/01/jax-rsjersey-2.html