Jersey MVCでResponse Headerにcharsetを指定する

JAX-RS実装であるJerseyですが、通常の画面をMVCで構築するための拡張が用意されています。
https://jersey.java.net/documentation/latest/mvc.html

基本的な使い方は上のリンク先を見てもらうなりぐぐってもらうなりして、今回Responseヘッダにcharsetを指定しようとしたときにちょっとつまずいたポイントがあるのでメモしておきます。

まずはシンプルな例です。

    @GET
    @Path("list.html")
    @Produces(MediaType.TEXT_HTML)
    public Viewable getList() {
        Map<String, Object> it = new HashMap<>();
	~~中略~~
        return new Viewable("list", it);// "list"でもいい
    }

ただし、これだとcharsetが指定されていないためクライアント側で文字化けが起こることがあります。

シンプルな方法

ひとつめの方法としては、@Producesにcharsetまで指定する方法があります。

    @GET
    @Path("list.html")
    @Produces(MediaType.TEXT_HTML + "; charset=UTF-8")

これは毎回書いておく必要があるのでちょっと面倒かもしれません。

もうひとつの方法としてはhtml内で指定します。

    <meta charset="UTF-8">

これは比較的部品化しやすいのでそれほど面倒ではないと思います。

ここで上げた2つの方法は片方だけでも問題ないと思いますが、両方しておくデメリットもないので両方しておきましょう。

filterを使う

上の@Producesにcharsetまで指定する方法がちょっと面倒なので全てのレスポンスにcharsetを自動でつけれないかと考えるのは自然なことだと思います。
これを可能にするのがfilterです。
https://jersey.java.net/documentation/latest/filters-and-interceptors.html

下記のようなクラスをつくりResourceConfigに登録すればOKです。

@Provider
public class CharsetResponseFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        MediaType mediaType = responseContext.getMediaType();
        if (mediaType != null) {
            if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) {
                responseContext.getHeaders().putSingle("Content-Type", new MediaType(mediaType.getType(), mediaType.getSubtype(), "UTF-8"));
            }
        }
    }
}

これで、Content-Typeがtext/htmlのレスポンスのcharsetはUTF-8となります。
ただし、
Jersey MVCを使っているとうまくいきません。
なぜかというと設定が上書きされるからです。
※ GlassFish4.0で見ているのでjerseyのバージョンは2.0です。
他のバージョンでは確認できていませんのであしからず。

詳しく見ていきます。
まず、処理の順序としては次のとおりです。

  • 11 リソースメソッド実行
  • 12 ContainerResponseFilters are executed(上のfilterです)
  • 14 MessageBodyWriter is executed

https://jersey.java.net/documentation/latest/filters-and-interceptors.html#d0e6796
で、jersey MVCでは上のMessageBodyWriterの実装クラスは
org.glassfish.jersey.server.mvc.internal.ViewableMessageBodyWriter
です。
この中で次のように実装されています。

    @Override
    public void writeTo(final Viewable viewable,
                        final Class<?> type,
                        final Type genericType,
                        final Annotation[] annotations,
                        final MediaType mediaType,
                        final MultivaluedMap<String, Object> httpHeaders,
                        final OutputStream entityStream) throws IOException, WebApplicationException {
        final Template template = TemplateHelper.getTemplateAnnotation(annotations);

        try {
            final ResolvedViewable resolvedViewable = resolve(viewable, template);
            if (resolvedViewable == null) {
                throw new WebApplicationException(
                        new ProcessingException(LocalizationMessages.TEMPLATE_NAME_COULD_NOT_BE_RESOLVED(viewable.getTemplateName())),
                        Response.Status.NOT_FOUND);
            }

            httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, resolvedViewable.getMediaType());
            resolvedViewable.writeTo(entityStream);
        } catch (ViewableContextException vce) {
            throw new NotFoundException(vce);
        }
    }

この中で、Content-Typeがセットされなおしています。
パラメータでMediaTypeが渡されていますが、これがfilterでセットされている値です。しかし、このメソッド内ではそれを使わずにresolvedViewable.getMediaType()をヘッダにセットしています。これは@Producesに指定した値です。

というわけでfilterとかを使って共通処理としてcharsetをヘッダに指定するのはちょっと難しそうです。