Jerseyで拡張子に応じて出力コンテンツを振り分ける方法

最近急遽Restful Webサービスを提供するサーバーを構築する必要に迫られ、3年ぶりくらいにJavaEEを扱うことになりました。別に何で作っても良かったのですが、サービスの運用も含めて考えると一番慣れているのがJavaEEなので。

JavaEE6にはRestful Webサービスを構築するための仕様であるJAX-RSが含まれています。これを使ってWebサービスを提供することにしました。
APサーバーとしてはGlassFishを使うことにしたので、JAX-RSの実装系としては Jersey を採用しました。

今回構築することになったWebサービスではクライアントに返すフォーマットとしてJSONとHTMLの2種類を用意する必要がありました。
HTTPにはクライアントがサーバーに対して欲しいコンテンツタイプを要求できる、コンテンツネゴシエーションという仕様があります。
この仕様では、クライアントは Accept ヘッダに欲しいコンテンツタイプを指定することになっており、JAX-RSもこの仕様に対応しており、同じパスに対して、異なるコンテンツタイプを出力するメソッドがあった場合、Accept ヘッダの値に応じて呼び出されるメソッドを振り分けてもらうようになっています。

ですが、世の中のWebサービスでは、どちらかというとURI拡張子で返却するコンテンツタイプを指定するものが多いように見受けられます。拡張子が ".json" であったらJSONを返し、".xml" であったらXMLを返す、といった感じです。今回もそのように実装したいと考えました。
ところが、JAX-RS1.0の時点ではこの機能はサポートしていません。どうもJAX-RSのエキスパートグループ内でこの機能のセマンティクスについて合意ができず、見送られたようです。
とは言え、いくつかあるJAX-RS実装系ではそれぞれ独自にこの機能を備えているようです。Jerseyでも用意されていましたので、ここではその利用方法について示したいと思います。

JerseyではURIによるコンテンツネゴシーエーションを行うために UriConnegFilter というフィルタを用意しています。
使い方は次のように継承したクラスを用意して、コンストラクタ拡張子とそれに対応するコンテンツタイプの関連を設定した Map を渡すだけです。この例では拡張子 jsonxml、html に対応するようにしています。

public class UriConnegFilterImpl extends UriConnegFilter {
    private static final Map<String, MediaType> MEDIA_TYPE_MAPPING = new HashMap<String, MediaType>();
    static {
        MEDIA_TYPE_MAPPING.put("json", MediaType.APPLICATION_JSON_TYPE);
        MEDIA_TYPE_MAPPING.put("xml", MediaType.APPLICATION_XML_TYPE);
        MEDIA_TYPE_MAPPING.put("html", MediaType.TEXT_HTML_TYPE);
    }

    public UriConnegFilterImpl() {
        super(MEDIA_TYPE_MAPPING);
    }
}

そして web.xml にて Jersey のアダプタサーブレットの初期化パラメータ com.sun.jersey.spi.container.ContainerRequestFilters を使ってこの実装クラスを指定します。

    <servlet>
        <servlet-name>ServletAdaptror</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
            <param-value>packagename.UriConnegFilterImpl</param-value>
        </init-param>
    </servlet>

こうすると、URI拡張子に応じて、このフィルタが Acccept ヘッダを書き換えてくれるようになります。
後はWebサービス実装クラス側で、次のように返却するコンテンツタイプに応じた処理を用意するだけです。

    @GET
    @Produces(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
    public List<Users> findAll() {
        return repository.findAll();
    }

    @GET
    @Produces(MediaType.TEXT_HTML)
    public Viewable findAllToHtml() {
        List<Users> users = repository.findAll();
        return new Viewable("/user_list.jsp", users);
    }

この例では、拡張子json もしくは xml ならば findAll() が、html ならば findAllToHtml が呼び出されるようになります。
今回の主題とは外れますが、Jersey では Viewable クラスを使えば HTML を出力するためのテンプレートに処理を委譲できるようになっているのがいいですね。

今回初めて本格的に Jersey を使ってみましたが、なかなかいいですね。クライアントライブラリも用意されて、これもなかなか良くできています。
ちょっとした MVC フレームワーク的な機能もあるので、Webサービスだけでなく、一緒にブラウザ向けのWebアプリケーションも作成することができますし、なによりJAX-RSJavaEEの一員なので、EJBやBean Validation、JPA、JMSなどと一緒に使うことができます。
JavaWebサービスを提供したい場合にはお勧めなライブラリです。