2007/09/03

今日のこんなところで国際化

このエントリーをはてなブックマークに追加

ちょうど今日、公開されましたが、「Java 技術最前線」に Cookie の記事を書きました。内容は読んでいただくとして、そこで HttpCookie クラスのバグについて言及しました。

記事中には、本題とは離れてしまうので、あまり具体的な言及はしなかったのですが、せっかく見つけたのでここに書いておきます。

バグを見つけたのはサーブレットで Cookie を送信しても、クライアントで Cookie を受けとらないという現象が起きたためです。

Cookie を受けとらないのは有効期限 (Max-Age) を設定した時ということまで分かりました。

調べてみると、Max-Age を設定しても、なぜか HttpCookie では MaxAge が 0 になってしまうのです。MaxAge が 0 というのは保存しないということなので、Cookie を受けとってないように見えたのでした。

デバッガで追ってみると、MaxAge の解釈は HttpCookie クラスの parse メソッドで行なわれていることが分かります。

そして、MaxAge の設定はアトリビュートを解釈する CookieAttributeAssignor インタフェースを派生させた無名クラスで行なわれています。

        assignors.put("max-age", new CookieAttributeAssignor(){
                public void assign(HttpCookie cookie, String attrName, String attrValue) {
                    try {
                        long maxage = Long.parseLong(attrValue);
                        if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) cookie.setMaxAge(maxage);
                    } catch (NumberFormatException ignored) {
                        throw new IllegalArgumentException("Illegal cookie max-age attribute");
                    }
                }
            });

MaxAge は秒数で表され、その値は整数になります。なので、Long.parseLong で文字列を long に setMaxAge メソッドでその値を設定しているわけです。

この記述には特に問題はなさそうです。

じゃあ、なぜ?

もう一度、サーバから送られてくる HTTP のヘッダを見てみたところ、有効期限の設定に Max-Age が使われておらず、Expires が使われているのでした。

ということで、Expires の解釈処理を見てみると...

        assignors.put("expires", new CookieAttributeAssignor(){ // Netscape only
                public void assign(HttpCookie cookie, String attrName, String attrValue) {
                    if (cookie.getMaxAge() == MAX_AGE_UNSPECIFIED) {
                        cookie.setMaxAge(cookie.expiryDate2DeltaSeconds(attrValue));
                    }
                }
            });

なにやら、expryDate2DeltaSeconds メソッドが臭います ^^;;

    private final static String NETSCAPE_COOKIE_DATE_FORMAT = "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'";
  
    private long expiryDate2DeltaSeconds(String dateString) {
        SimpleDateFormat df = new SimpleDateFormat(NETSCAPE_COOKIE_DATE_FORMAT);
        df.setTimeZone(TimeZone.getTimeZone("GMT"));

        try {
            Date date = df.parse(dateString);
            return (date.getTime() - whenCreated) / 1000;
        } catch (Exception e) {
            return 0;
        }
    }

なにがおかしいか分かりますか?

こんなところで国際化が関係してくるとは思いもしませんでしたが、SimpleDateFormat のフォーマット記述子がおかしいのです。

曜日を表すフォーマット記述子 EEE は、ロケールが Locale.US の時は Mon など曜日を 3 文字で表します。ところが、Locale.JAPAN の時は 月 というように漢字で表されるのです。また、MMM は Locale.US では Sep と月を 3 文字表示で表しますが、 Locale.JAPAN の時は単に 9 と数字で表されます。

つまり、"EEE, dd-MMM-yyyy HH:mm:ss GMT" で月日を表すと、Locale.US だと

Mon, 03-Sep-2007 05:03:41 GMT

Local.JAPAN だと

月, 03-9-2007 05:03:41 GMT

となるわけです。

さて、サーブレットから送られてくる Expires はどうなっているかというと

Set-Cookie: id=1234; Expires=Mon, 03-Sep-2007 05:03:41 GMT;

のように Locale.US で表したものと同じになっています。

しかし、Locale.JAPAN の場合、曜日は漢字だと想定しているところにアルファベットになっているので、 ParseException を起こしてしまいます。expryDate2DeltaSeconds メソッドの例外処理を見てみると、例外が発生すると MaxAge を 0 にするだけです。

結局、MaxAge が 0 になってしまうため、Cookie を保存できないのでした。

欧米の人から見れば、まさか日時の表し方がロケールによって異なるとは思わないでしょうね。

で、修正の方法ですが、これは簡単。単に SimpleDateFormat でロケールを指定すればいいのです。ようするにデフォルトロケールで処理させるから、前述したようにパースを失敗するのです。だから、強制的にロケールを指定するということですね。

    private long expiryDate2DeltaSeconds(String dateString) {
        SimpleDateFormat df = new SimpleDateFormat(NETSCAPE_COOKIE_DATE_FORMAT,
                                                   Locale.JAPAN);
        df.setTimeZone(TimeZone.getTimeZone("GMT"));

        try {
            Date date = df.parse(dateString);
            return (date.getTime() - whenCreated) / 1000;
        } catch (Exception e) {
            return 0;
        }
    }

3 件のコメント:

Suzumura さんのコメント...
このコメントは投稿者によって削除されました。
Suzumura さんのコメント...

このバグって以前からあったんでしょうかねぇ。
かなり前(5年くらい前?)にCookieの動作の確認をしていて、MaxAge = -1を確かめていたのですが、うまく動かなかった記憶があります。あまり重要な事項じゃなかったのでそのまま原因究明しなかったのですが、まさか国際化対応が関係していた(かもしれない)とは...
(当時の環境を詳しく覚えていないので、同じ原因かどうかは今となってはわかりませんが..)
いまだにこういう基本的なところが動かない可能性があるんですね。
非常に参考になりました。

skrb さんのコメント...

HttpCookieクラスはJava SE 6からなので、suzumuraさんの件とは異なると思います。でも、同じようなコーディングがされていないとは限らないですからねぇ ^^;;
Expiers を使わないで、素直に Max-Age にしてくれればと思うのですが...