SAFluteのJSONハンドリング

SAFluteの特徴の一つです。

Ajaxやスマートフォン対応として

クライアント側とJSONなどでやり取りすることが多くなってきました。

RomanticActionCustomizer や GodHandableActionWrapper が連携して実現しています。 オブジェクト型でやり取りすることで、JUnitTestもしやすくなります。

JsonParameter

ActionFormのプロパティに @JsonParameter を付けると、リクエストパラメーターのJSON文字列をパースして、Beanにマッピングしてくれます。

e.g. JsonParameterでJSONをBeanで受け取る (ActionFormにて) @Java
@JsonParameter
public FooWebBean jsonBean;

※こちら、Form送信の一部のJSON文字列に対しての機能であり、リクエストボディ直接のJSONには対応できていません。

JsonResponse

さらには Action の戻り値で、JSON をオブジェクトとして指定できるようにしています。 (自分で Response に write するのではなく、オブジェクト型を戻すだけ)

Actionの@Executeメソッドの戻り値の型を JsonResponse にして...

e.g. JsonResponseを戻す @Java
@Execute(validator = false)
public JsonResponse submit() { // application/json
    FooWebBean bean = ...;
    return new JsonResponse(bean); // JSONになって欲しいBeanを引数に
}
e.g. JSONP形式の JsonResponse を戻す @Java
@Execute(validator = false)
public JsonResponse submit() { // application/javascript
    FooWebBean bean = ...;
    return new JsonResponse(bean).asJsonp("callback");
}

XmlResponse (XMLも指定できます)

XMLも指定できます。Actionの@Executeメソッドの戻り値の型を XmlResponse にして...

e.g. XmlResponseを戻す @Java
@Execute(validator = false)
public XmlResponse submit() { // text/xml
    String xmlStr = ...;
    return new XmlResponse(xmlStr);
}

StreamResponse (というかStreamも)

Actionの@Executeメソッドの戻り値の型を StreamResponse にして...

e.g. StreamResponseを戻す (ダウンロードする) @Java
@Execute(validator = false)
public StreamResponse submit() { // application/octet-stream
    InputStream ins = ...;
    return new StreamResponse("dbflute.pdf").stream(ins);
}

content-typeは、デフォルトで application/octet-stream ですが、変更したいときは...

e.g. StreamResponseを戻す (ダウンロードする) @Java
@Execute(validator = false)
public StreamResponse submit() {
    InputStream ins = ...;
    return new StreamResponse("dbflute.pdf").stream(ins).contentType("dream/cruise");
}

独自に header を、定義したいときは...

e.g. StreamResponseを戻す (ダウンロードする) @Java
@Execute(validator = false)
public StreamResponse submit() {
    InputStream ins = ...;
    return new StreamResponse("dbflute.pdf").stream(ins).header("dream", "cruise");
}

JSON戻りの例外ハンドリング

JSON戻りのActionにて、例外 (主に業務例外) が発生したときに、JSONを戻す仕組みがあります。

AssistantDirector で Provider 設定

AssistantDirectorで、アプリ独自の ApiResultProvider を設定すると、例外発生時にその provider が処理されて、好きなようにJSONを戻すことができます。

JsonResponseが戻り値になっている Action の中で例外が発生したときに呼ばれます。

e.g. AssistantDirectory で ApiResultProvider を設定 @Java
...
protected OptionalActionDirection prepareOptionalActionDirection() {
    ...
    prepareApiCall(direction);
}

...

protected void prepareApiCall(OptionalActionDirection direction) {
    direction.directApiCall(createApiResultProvider());
}

protected XxxApiResultProvider createApiResultProvider() { // アプリ独自のクラス
    return new XxxApiResultProvider(); // ApiResultProviderをimplements
}

ApiResultProvider の実装

ApiResultProvider では、それぞれの状況に応じて、ApiResult を implements したBeanを戻します。 そのBeanは、JSONに変換されてレスポンスになります。

その中で、メッセージなども取得できるようになっています。 アプリケーションのルールに従って、実装してみてください。

e.g. ApiResultProvider の実装 @Java
// ===================================================================================
//                                                                      Prepare Result
//                                                                      ==============
@Override
public ApiResult prepareLoginRequiredFailureResult(ActionMessages errors, ActionExecuteMeta meta) {
    final ApiResultWebBean bean = createApiResultWebBean(ApiResultMode.LOGIN_REQUIRED);
    if (errors != null) {
        bean.setMessage(getConcatedMessage(errors));
    }
    return bean;
}

@Override
public ApiResult prepareLoginPerformRedirectResult(ActionMessages errors, ActionExecuteMeta meta) {
    final ApiResultWebBean bean = createApiResultWebBean(ApiResultMode.SUCCESS);
    bean.setMessage(getConcatedMessage(errors));
    return bean;
}

@Override
public ApiResult prepareValidationErrorResult(ActionMessages errors, ActionExecuteMeta meta) {
    final ApiResultWebBean bean = createApiResultWebBean(ApiResultMode.FAIL);
    bean.setMessage(getConcatedMessage(errors));
    return bean;
}

@Override
public ApiResult prepareApplicationExceptionResult(ActionMessages errors, ActionExecuteMeta meta,
        RuntimeException cause) {
    final ApiResultWebBean bean = createApiResultWebBean(ApiResultMode.FAIL);
    bean.setMessage(getConcatedMessage(errors));
    return bean;
}

@Override
public ApiResult prepareSystemExceptionResult(HttpServletResponse response, ActionExecuteMeta executeMeta,
        Throwable cause) {
    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    final ApiResultWebBean bean = createApiResultWebBean(ApiResultMode.FAIL);
    return bean; // Not Required
}

// ===================================================================================
//                                                                             WebBean
//                                                                             =======
protected ApiResultWebBean createApiResultWebBean(ApiResultMode mode) {
    return new ApiResultWebBean(mode);
}

// ===================================================================================
//                                                                             Message
//                                                                             =======
protected List getMessageList(ActionMessages errors) {
    final MessageManager messageManager = getMessageManager();
    final Locale userLocale = getRequestManager().getUserLocale();
    return messageManager.getMessageList(userLocale, errors);
}

protected String getConcatedMessage(ActionMessages errors) {
    List<String> messageList = getMessageList(errors);
    if (CollectionUtils.isNotEmpty(messageList)) {
        StringBuilder sb = new StringBuilder();
        for (String message : messageList) {
            sb.append(message);
            sb.append("\n");
        }
        return sb.toString().trim();
    }
    return "";
}

protected Map<String, List<String>> getPropertyMessageMap(ActionMessages errors) {
    final MessageManager messageManager = getMessageManager();
    final Locale userLocale = getRequestManager().getUserLocale();
    return messageManager.getPropertyMessageMap(userLocale, errors);
}

protected MessageManager getMessageManager() {
    return ContainerUtil.getComponent(MessageManager.class);
}

protected RequestManager getRequestManager() {
    return ContainerUtil.getComponent(RequestManager.class);
}

業務例外のハンドリングはActionCallback

どの例外が業務例外で、どういうメッセージを戻すかという設定は、ActionCallback の中で行われています。TypicalBaseAction の godHandExceptionMonologue() から辿っていくとわかります。