先週先々週とJava SEでCookieを扱う方法を解説してきました。そこでは、Java SE 6でCookieを扱うためのCookieManagerクラスが導入され、デフォルトではメモリにCookieを保持することを説明しました。

メモリに保持しているため、アプリケーションが終了すれば、保持していたCookieはすべて消失してしまいます。

できれば、ファイルなどにCookieを保存したいのですが、そのためのクラスはJava SEでは提供されていません。

そこで、今週はCookieの保存、読み込み機能を独自に作成してみましょう。

図1は先週示したものと同じCookie関連のクラス図です。

中心となるのがjava.net.CookieManagerクラス、そして、Cookieを保持するのがjava.net.CookieStoreインタフェースになります。

CookieStoreインタフェースの実装クラスはCookieManagerクラスのコンストラクタで指定します。デフォルトではメモリにCookieを保持するsun.net.www.protocol.http.InMemoryCookieStoreクラスが使用されます。

Cookie関連のクラス図
図1 Cookie関連のクラス図

Cookieをファイルに保存するためにはCookieStoreインタフェースを実装したクラスを作ればいいようです。とはいうものの、Cookieの有効期限のチェックなど、すべての処理を実装するのは大変です。

そこで、Cookieの基本的な処理はInMemoryCookieStoreクラスに委譲し、保存と読み込みの部分だけ作成することにしました。

ここで、図1をもう一度見てください。Cookieはjava.net.HttpCookieクラスで表されることがお分かりでしょうか。

とすれば、HttpCookieオブジェクトをそのままシリアライズしてしまえばいいと思いますね。オブジェクトをシリアライズ/デシリアライズするにはObjectOutputStreamクラス/ObjectInputStreamクラスなどが使えます。

ところが、HttpCookieクラスはSerializableではありません。このため、ObjectOutputStreamクラス/ObjectInputStreamクラスでは直接扱えないのです。

しかたがないので、Serializableなクラスを定義し、そこにHttpCookieクラスから取得した情報を持たせることにしましょう。ここではそのクラスをSerializableHttpCookieクラスとしました。

HttpCookieクラスはプロパティに対するsetter、getterが定義されているので、それらを使用してSerializableHttpCookieクラスと相互に変換できるようにすれば、扱いも楽です。

ところが、ここでもう1つ問題が。

HttpCookieクラスはオブジェクトが生成された時刻をフィールドとして保持しています。この時刻と有効期限を表すMaxAgeプロパティから、有効期限が切れているかどうか判断します。

しかし、オブジェクトの生成時刻はsetter/getterが用意されていないため、アクセスすることができません。

このため、ここで作成するサンプルは有効期限のチェックが正しく行なわれないバグがあることをご承知ください。

それにしても、HttpCookieはなぜ保存することをまったく考慮しない仕様になっているのでしょう。ちょっと不思議です。

サンプルとして先週使用したカウンタを題材にしてみました。

サンプルのソース FileCookieStore.java, SerializableHttpCookie.java, CookieManagerSample3.java
サーバのServlet TestServlet3.java

それではSerializableHttpCookieクラスから見ていきましょう。

このクラスはシリアライズを行ないやすくするためにBeanにしました。Beanにするためには、デフォルトコンストラクタとプロパティのsetter/getterが必要です。プロパティはHttpCookieと同一です。

// HttpCookie をシリアライズするための Bean
public class SerializableHttpCookie implements Serializable {
    private static final long serialVersionUID = 1L;
 
    private String name;
    private String value;
    private String comment;
    private String commentURL;
    private boolean toDiscard;
    private String domain;
    private long maxAge;
    private String path;
    private String portlist;
    private boolean secure;
    private int version;
    
    // Bean はデフォルトコンストラクタが必須
    public SerializableHttpCookie() {}
 
    public String getComment() {
        return comment;
    }
 
    public void setComment(String comment) {
        this.comment = comment;
    }
	
        <<以下、略>>	

残るはHttpCookieクラスとの相互変換ルーチンです。ここでは、SerializableHttpCookieクラスのコンストラクタでHttpCookieオブジェクトのプロパティをコピーするようにしました。

    public SerializableHttpCookie(HttpCookie cookie) {
        // HttpCookie のプロパティをコピー
        name = cookie.getName();
        value = cookie.getValue();
        comment = cookie.getComment();
        commentURL = cookie.getCommentURL();
        toDiscard = cookie.getDiscard();
        domain = cookie.getDomain();
        maxAge = cookie.getMaxAge();
        path = cookie.getPath();
        portlist = cookie.getPortlist();
        secure = cookie.getSecure();
        version = cookie.getVersion();
    }

もう一方のSerializableHttpCookieオブジェクトからHttpCookieオブジェクトの生成は、toHttpCookieメソッドで行ないました。

    // HttpCookie への変換
    // ただし、クッキーの生成時刻が、
    // オリジナルとは異なってしまう
    public HttpCookie toHttpCookie() {
        HttpCookie cookie = new HttpCookie(name, value);

        cookie.setComment(comment);
        cookie.setCommentURL(commentURL);
        cookie.setDiscard(toDiscard);
        cookie.setDomain(domain);
        cookie.setMaxAge(maxAge);
        cookie.setPath(path);
        cookie.setPortlist(portlist);
        cookie.setSecure(secure);
        cookie.setVersion(version);

        return cookie;
    }

toHttpCookieメソッドは先ほどとは逆にSerializableHttpCookieオブジェクトのプロパティをHttpCookieオブジェクトにコピーしています。ただし、前述したようにHttpCookieオブジェクトの生成時刻だけはコピーできず、オリジナルのHttpCookieオブジェクトとは異なる値になってしまっています。

シリアライズするクラスができたので、シリアライズ/デシリアライズを行なうFileCookieStoreクラスを作っていきます。前述したようにCookieに関する基本的な処理はすべてInMemoryCookieStoreクラスに委譲します。

public class FileCookieStore implements CookieStore {
    // 委譲を行なう CookieStore
    private CookieStore store;
 
    // Cookieをシリアライズ/デシリアライズするファイル
    private File file;
     
    public FileCookieStore() {
        store = new InMemoryCookieStore();
 
        // システムプロパティからホームディレクトリを取得し
        // .cookies ファイルを配置する
        String home = System.getProperty("user.home");
        file = new File(home + File.separator + ".cookies");
    }
	
    public void add(URI uri, HttpCookie cookie) {
        store.add(uri, cookie);
    }
 
    public List<HttpCookie> get(URI uri) {
        return store.get(uri);
    }
        <<以下、略>>

Cookieはユーザのホームディレクトリに.cookiesというファイルに保存するようにしました。ホームディレクトリはシステムプロパティから取得しています。

addメソッド、getメソッドなどCookieStoreインタフェースで定義されているメソッドはInMemoryCookieStoreクラスに委譲しています。

そして重要なのがCookieの保存、読み込み処理です。まず、保存から。

    public void save() throws IOException {
        HashMap<URI, List<SerializableHttpCookie>> map
            = new HashMap<URI, List<SerializableHttpCookie>>();
        
        // URI ごとに InMemoryCookieStore からクッキーを取り出し、
        // シリアライズが可能なように変換
        List<URI> uris = store.getURIs();
        for (URI uri: uris) {
            List<HttpCookie> cookies = store.get(uri);
            List<SerializableHttpCookie> tmpCookies 
                = new ArrayList<SerializableHttpCookie>();
            for(HttpCookie cookie: cookies) {
                // MaxAge が -1 であれば保存しない
                if (cookie.getMaxAge() != -1) {
                    tmpCookies.add(new SerializableHttpCookie(cookie));
                }
            }
            if (tmpCookies.size() != 0) {
                map.put(uri, tmpCookies);
            }
        }
 
        if (map.size() != 0) {
            // XML ファイルにシリアライズ
            XMLEncoder encoder 
                = new XMLEncoder(new BufferedOutputStream(
                                     new FileOutputStream(file)));
            encoder.writeObject(map);
            encoder.close();
        }
    }

赤で示した部分がCookieをSerializableHttpCookieに変換して、Mapオブジェクトに格納する処理を示しています。青で示した部分でMapオブジェクトをシリアライズしています。

HttpCookieクラスはURIは保持しないので、まずCookieStore#getURIsメソッドでURIの一覧を取得し、URIに対応するCookieを取得するという手順をとりました。

CookieStore#getメソッドの戻り値はList<HttpCookie>オブジェクトなので、それらをSerializableHttpCookieオブジェクトに変換します。

このとき、取得したCookieのMaxAgeが-1の場合は、Cookieの有効期間がアプリケーション終了までなので、保存しないようしています。

URIと対応させるために、HashMapクラスを使用し、キーにURI、値にSerializableHttpCookieオブジェクトのリストを保持させました。

シリアライズにはjava.beans.XMLEncoderクラスを使用しました。XMLEncoderクラスでシリアライズすると、その名の通りXMLで出力されます。ただし、XMLEndoderクラスでシリアライズできるのはBeanに限られています。

XMLEndoderクラスのcloseメソッドはストリームも一緒にクローズするので、ストリームのクローズは不要です。