wicketの余計なお世話に絶望した!wicket1.3のナンバーフォーマットのバグ

PropertyModel等でBigDecimalにマッピングしたTextFieldを作って、10000000…と入れてみて下さい。

100,000,000,000,000,000,000,000,000,000

とフォーマットして表示されるようになっているのです。
ちょっと動作を追ってみましたが1.3.0はフォーマットしなかったので、1.3.1からのようです。(1.3.3まで確認)
ReleaseNoteを見てみましたが関係ありそうなのは[WICKET-1254] Binding to a BigDecimal don't honor browser locale - ASF JIRAぐらいでしたが直接関係あるんでしょうか?

この時点でフォーマットしたくない人は困ると思いますが、これ24桁以上入力すると化けるんですよ。

↑の値は↓となります。
99,999,999,999,999,991,433,150,857,216

入力した文字列をBigDecimalConverterでConvertしてるんですが、ソースから抜粋してわかりやすくすると以下のような動作になってました。

    	BigDecimal hoge = new BigDecimal("100000000000000000000000000000");
    	System.out.println(hoge);
        NumberFormat nf = NumberFormat.getInstance(locale);
        //各localeでNumberFormatを取得して値を文字列にフォーマット
        String formatString = nf.format(hoge);
        System.out.println(formatString);
        try {
        //同じNumberFormatにフォーマットした文字列を入れるとDoubleになる。
        	Double parseObject = (Double) nf.parseObject(formatString);
        	System.out.println(parseObject);
        //DoubleをBigDecimalに再度入れたところで精度問題で化ける
		System.out.println(new BigDecimal(parseObject.doubleValue()));
		} catch (ParseException e) {
		}

出力はこうなります

100000000000000000000000000000
100,000,000,000,000,000,000,000,000,000
1.0E29
99999999999999991433150857216

ということでjavaのNumberFormat自体がParse時にDoubleを返していて、BigDecimalに入れると化けるのでwicketのせいではないような気もします…が!
そもそもwicketが余計な事してるのが悪いと思います。

フォーマットしたければ自分でカスタムコンポーネントを作れるわけですし、wicket側でやってくれるにしてもフォーマットするConverterを設定したコンポーネントの提供等通常は何もしないで、必要なときだけフォーマットしてくれる方法は他にもありそうですし。

IConverterのエラーメッセージを変更する - 凡人プログラマもそうですが、あんまり余計な挙動は入れて欲しくないですね。こういうのはextension等に入れるべきじゃないかなーと。


こんなレアな問題で困っている不運な方の為に対処法も書いておきます

  1. Application#newConverterLocatorをオーバーライドして、自作ConverterLocatorを作成、BigDecimalのコンバータを置き換える。
  2. 問題発生するTextFieldでgetConverterを定義してやり、自作Converterを返す。

ちなみに以下の自作Converterは上記バグを修正した版です。TextFieldにgetConverterを定義して問題発生しない事を確認しました。
BigDecimalを生成する部分でnumber.doubleValue()では無く、number.toString()にしました。(すごく対症療法っぽいですが)
本家のバグもここを修正するだけで直ると思います。

	return new IConverter() {
		@Override
		public Object convertToObject(String value,
				Locale locale) {
			Number number = null;
			try {
				number = NumberFormat.getInstance(locale).parse(value);
			} catch (ParseException e) {
				e.printStackTrace();
			}
			return new BigDecimal(number.toString());
		}
		@Override
		public String convertToString(Object value,
				Locale locale) {
			return NumberFormat.getInstance(locale).format(value);
		}
    				
	};