RESTが簡単に書けたのでRestControllerを書いてみた。

slim3のコントローラーは isGet(), isPost(), isPut(), isDelete() が用意されているのでRESTが簡単に書ける。便利だなぁ。なのでREST用のコントローラーを抽象化して RestController ってのを書いてみた。

ソースは以下の通り。

RestController.java

package tutorial.cool.controller;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletResponse;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

import com.google.appengine.repackaged.com.google.io.base.IORuntimeException;

public abstract class RestController extends Controller {

    //200
    public static final int SC_OK = HttpServletResponse.SC_OK;
    public static final int SC_CREATED = HttpServletResponse.SC_CREATED;
    public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT;
    //400
    public static final int SC_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST;
    public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND;
    public static final int SC_CONFLICT = HttpServletResponse.SC_CONFLICT;
    //500
    public static final int SC_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
    public static final int SC_NOT_IMPLEMENTED = HttpServletResponse.SC_NOT_IMPLEMENTED;

    /**
     * メインメソッド。HTTP method の内容に対応するメソッドを呼び出し実行する。
     */
    @Override
    public Navigation run() {
        if (isGet()) {
            doGet();
            return null;
        } else if (isPost()) {
            doPost();
            return null;
        } else if (isPut()) {
            doPut();
            return null;
        } else if (isDelete()) {
            doDelete();
            return null;
        }
        try {
            response.sendError(SC_NOT_IMPLEMENTED);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * HTTP method が DELETE の場合に実行されるメソッド。
     */
    public void doDelete() {
        try {
            response.sendError(SC_NOT_IMPLEMENTED);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return;
    }

    /**
     * HTTP method が PUT の場合に実行されるメソッド。
     */
    public void doPut() {
        try {
            response.sendError(SC_NOT_IMPLEMENTED);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return;
    }

    /**
     * HTTP method が POST の場合に実行されるメソッド。
     */
    public void doPost() {
        try {
            response.sendError(SC_NOT_IMPLEMENTED);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return;
    }

    /**
     * HTTP method が GET の場合に実行されるメソッド。
     */
    public void doGet() {
        try {
            response.sendError(SC_NOT_IMPLEMENTED);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return;
    }

    /**
     * 指定した str のリクエストパラメーターが null か確認するメソッド。
     * 
     * @param str
     *        リクエストパラメーターのキー
     * @return
     *            true: null である。
     *            false: null でない。
     */
    public boolean isNull(String str) {
        if (asString(str) == null) {
            try {
                response.sendError(SC_BAD_REQUEST);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }

    /**
     * レスポンスにテキストを書き込みます。
     * 
     * @param text
     *            テキスト
     */
    public void responseWriter(String text) {
        responseWriter(text, null, null);
    }

    /**
     * レスポンスにテキストを書き込みます
     * 
     * @param text
     *            テキスト
     * @param contentType
     *            コンテントタイプ。 デフォルトはtext/plain。
     */
    public void responseWriter(String text, String contentType) {
        responseWriter(text, contentType, null);
    }

    /**
     * レスポンスにテキストを書き込みます。
     * 
     * @param text
     *            テキスト
     * @param contentType
     *            コンテントタイプ。 デフォルトはtext/plain。
     * @param encoding
     *            エンコーディング。 指定しなかった場合は、リクエストのcharsetEncodingが設定される。
     *            リクエストのcharsetEncodingも指定がない場合は、UTF-8。
     */
    public void responseWriter(String text, String contentType, String encoding) {
        responseWriter(SC_OK, text, contentType, encoding);
    }

    /**
     * レスポンスにテキストを書き込みます。
     * 
     * @param status
     *            ステータスコード
     */
    public void responseWriter(int status) {
        responseWriter(status, null, null, null);
    }

    /**
     * レスポンスにテキストを書き込みます。
     * 
     * @param status
     *            ステータスコード
     * @param text
     *            テキスト
     */
    public void responseWriter(int status, String text) {
        responseWriter(status, text, null, null);
    }

    /**
     * レスポンスにテキストを書き込みます
     * 
     * @param status
     *            ステータスコード
     * @param text
     *            テキスト
     * @param contentType
     *            コンテントタイプ。 デフォルトはtext/plain。
     */
    public void responseWriter(int status, String text, String contentType) {
        responseWriter(status, text, contentType, null);
    }

    /**
     * レスポンスにテキストを書き込みます。
     * 
     * @param status
     *            ステータスコード
     * @param text
     *            テキスト
     * @param contentType
     *            コンテントタイプ。 デフォルトはtext/plain。
     * @param encoding
     *            エンコーディング。 指定しなかった場合は、リクエストのcharsetEncodingが設定される。
     *            リクエストのcharsetEncodingも指定がない場合は、UTF-8。
     */
    public void responseWriter(int status, String text, String contentType, String encoding) {
        //
        if (status == 0) {
            status = SC_OK;
        }
        if (!(status == SC_OK || status == SC_CREATED || status == SC_NO_CONTENT)) {
            try {
                response.sendError(status);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }
        //
        if (contentType == null) {
            contentType = "text/plain";
        }
        if (encoding == null) {
            encoding = request.getCharacterEncoding();
            if (encoding == null) {
                encoding = "UTF-8";
            }
        }
        response.setStatus(status);
        response.setContentType(contentType + "; charset=" + encoding);
        try {
            PrintWriter out = null;
            try {
                out =
                    new PrintWriter(new OutputStreamWriter(response
                        .getOutputStream(), encoding));
                out.print(text);
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

}

responseWriter は SAStruts の ResponsUtil#write() をパクって改変しました <(_ _)> 。

レスポンスのステータスはきちんと煮詰めるべきか。もう少し勉強してみよう。

使い方は RestController を継承して以下のように書く。

各 HTTP method に対応したメソッド doGet(), doPost(), doPut(), doDelete() をオーバーライドして定義すれば OK。

OrdersController.java

package tutorial.controller.rest;

import tutorial.cool.controller.RestController;

public class OrdersController extends RestController {

    private boolean condition = true;

    @Override
    public void doDelete() {
        if (isNull("key")) return;
        if (condition) {
            responseWriter(createXML("DELETE"), "text/xml");
        } else {
            responseWriter(SC_CONFLICT);
        }
    }

    @Override
    public void doPut() {
        if (isNull("key")) return;
        if (condition) {
            responseWriter(createXML("PUT"), "text/xml");
        } else {
            responseWriter(SC_CONFLICT);
        }
    }

    @Override
    public void doPost() {
        if (condition) {
            responseWriter(createXML("POST"), "text/xml");
        } else {
            responseWriter(SC_CONFLICT);
        }
    }

    @Override
    public void doGet() {
        if (condition) {
            responseWriter(createXML("GET"), "text/xml");
        } else {
            responseWriter(SC_NO_CONTENT, null, "text/html");
        }
    }

    protected String createXML(String methodType) {
        StringBuffer sb = new StringBuffer();

        〜〜〜

        return (new String(sb));
    }

}

ルーターは以下の様に定義。

AppRouter.java

package tutorial.controller;

import org.slim3.controller.router.RouterImpl;

public class AppRouter extends RouterImpl {

    public AppRouter() {
        addRouting("/rest/orders/{key}", "/rest/orders?key={key}");
    }

}

テストはこんな感じで。

OrdersControllerTest.java

package tutorial.controller.rest;

import org.slim3.tester.ControllerTestCase;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class OrdersControllerTest extends ControllerTestCase {

    @Test
    public void runPost() throws Exception {
        tester.request.setMethod("post");
        tester.start("/rest/orders");
        OrdersController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        assertTrue(tester.response.getOutputAsString().length() > 0);
        System.out.println(tester.response.getOutputAsString());
    }

    @Test
    public void runGet() throws Exception {
        tester.request.setMethod("get");
        tester.start("/rest/orders");
        OrdersController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        assertTrue(tester.response.getOutputAsString().length() > 0);
        System.out.println(tester.response.getOutputAsString());
    }

    @Test
    public void runPut1() throws Exception {
        tester.request.setMethod("put");
        tester.start("/rest/orders");
        OrdersController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        assertTrue(tester.response.getOutputAsString().length() == 0);
    }

    @Test
    public void runPut2() throws Exception {
        tester.request.setMethod("put");
        tester.param("key", "1");
        tester.start("/rest/orders");
        OrdersController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        assertTrue(tester.response.getOutputAsString().length() > 0);
        System.out.println(tester.response.getOutputAsString());
    }

    @Test
    public void runDelete1() throws Exception {
        tester.request.setMethod("delete");
        tester.start("/rest/orders");
        OrdersController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        assertTrue(tester.response.getOutputAsString().length() == 0);
    }

    @Test
    public void runDelete2() throws Exception {
        tester.request.setMethod("delete");
        tester.param("key", "1");
        tester.start("/rest/orders");
        OrdersController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(false));
        assertThat(tester.getDestinationPath(), is(nullValue()));
        assertTrue(tester.response.getOutputAsString().length() > 0);
        System.out.println(tester.response.getOutputAsString());
    }

}

テストクラスではなくクライアントプログラムで呼び出してテストするなら jQuery で書くか、「RESTCliant」というクライアントを使えばいい。