Wicket 1.4-m3 で Generics の仕様変更

ちょうど少し前にWicket 1.4でのGenerics化にて大論争というページを教えてもらったのですが、その結論が見えてきたみたいです。

This release is our final take on how to apply generics to our codebase with restraint. We hope to provide an elegant programming model without having to specify generic parameters everywhere, while still providing type safety when you need it.

Apache Wicket 1.4-m3 with improved generics

Wicket 1.4-m3では、ComponentはGenerics化せず、Genericsは一部のコンポーネントだけに導入する形になったようです。個人的には、RepeatableなクラスぐらいでしかGenericsのありがたみを感じていなかったので、記述がシンプルになるのはうれしい変更です。

A significant change from the earlier Wicket 1.4 milestone versions is that Component is no longer generified. This means that Component and most of its subclasses in Wicket do not take a class-level type parameter.

Apache Wicket 1.4-m3 with improved generics

問題となっていたgetModelObjectの扱いについては、既存のObject型を返すメソッドはgetDefaultModelObjectというメソッドに改名し、Genericsを導入したクラスにだけパラメータ化された型のオブジェクトを返すgetModelObjectを実装することにしたようです。

言葉にすると分かりにくいのですが、つまりは下記のような感じになるみたいです。

// Componentクラス
Object getDefaultModelObject()

// 一部のクラス(FormとかListView)
T getModelObject() // Generics化されたクラスではこれが追加される
Object getDefaultModelObject()

この関係でComponentクラスのModel関係のメソッドは全てDefaultが加わった形に変更されています。ComponentクラスからgetModelObjectが消えちゃったので、ソースレベルでの互換性は失われてしまった気もしますが、Genericsを利用する方を標準にしたいという意図なのでしょうか?

* getModel -> getDefaultModel
* setModel -> setDefaultModel
* getModelObject -> getDefaultModelObject
* setModelObject -> setDefaultModelObject

Wicket 1.4 M3 - New and Noteworthy

ちなみに、

We have released our third and hopefully final milestone release of our Java 5 based web framework and are anxious to receive feedback on our use of generics.

Apache Wicket 1.4-m3 with improved generics

とのことなので、いつものごとく(?)遅れていた Wicket 1.4 のリリースも近そうです。

URLのパス部分に%2Fを含む時の扱い

お知り合いのページを見て初めて知ったのですが、Apacheは標準ではパス部分に %2F(/をエスケープしたもの) を含むURLに対して404を返すそうです。

このままだと、最近流行り(?)の "/" でパラメータを区切る場合に、"/" 自体を値に含めることができません。そこでApacheの設定で、AllowEncodedSlashes を On にすると、URL中に %2F も使用できるようになります。

というのがメインな部分なのですが、その中で、

ところで、RFCとか規則上って、「/」をURL encodeして「%2F」にしても良いのか?
それともしてはいけないのか。これがよく分からん。

AllowEncodedSlashes ディレクティブにはまる。

という疑問が書かれていたので、RFCでどうなっているのかちょっとだけ調べてみました。

3.2.3 URI Comparison

Characters other than those in the "reserved" and "unsafe" sets (see
RFC 2396 [42]) are equivalent to their ""%" HEX HEX" encoding.

RFC2616 Hypertext Transfer Protocol -- HTTP/1.1

URIの "reserved" と "unsafe" 以外は、エスケープされたものと等しく扱うみたいです。
そしてRFCでのURIの定義はこんな感じです。

reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","

RFC2396 Uniform Resource Identifiers (URI): Generic Syntax

これを見ると "/" は "reserved" に含まれていますね*1。これらを単純に解釈すると、"/" と %2F は等しくは扱われないのだと思います。

RFC2616の先ほどの行の後に具体的な例が書かれています。

For example, the following three URIs are equivalent:

http://abc.com:80/~smith/home.html
http://ABC.com/%7Esmith/home.html
http://ABC.com:/%7esmith/home.html

RFC2616 Hypertext Transfer Protocol -- HTTP/1.1

これは ~ が "reserved" に含まれていないので、等しく扱うということだと思います。
一方、"/" は "reserved" に含まれているので、以下の二つは等しくないはずです。

      http://abc.com/hoge/hoge.html
      http://abc.com/hoge%2Fhoge.html

ざっと見た限りでは、これくらいしか記述がありませんでした。これらの部分は、等しいかどうかだけですので、サーバの振る舞いまでは制約を受けないのかもしれませんが、Apacheが "/" と "%2F" を同じに扱うのはRFC的に微妙な気もしますね。

PATH_INFOの仕様

さて、これでApache自身がファイルを扱う場合は分かったのですが、もう1点気になった部分があります。

PATH_INFOが/で渡されたか、%2fで渡されたか判断がつかなくなる。まぁ、PATH_INFOは変な値を持ってることが多いので(つか、勝手に正規化されていたりする)使わないようにしているので、問題なし(ぉぃ

AllowEncodedSlashes ディレクティブにはまる。

なんでそんな不便な動作なんだろう? と思って AllowEncodedSlashes のドキュメントを見てみると、

符号化されたスラッシュを許可することは、復号をすることを 意味しません。%2F や (関係するシステムでの) %5C は、他の部分が復号された URL の中でもそのままの形式で 残されます。

http://httpd.apache.org/docs/2.0/ja/mod/core.html#allowencodedslashes

あれ、%2F はそのまま残るって書かれていますね。どういうことかと調べてみると、該当するバグが bugzillaに登録されてました。

%2F will be decoded in PATH_INFO (Documentation to AllowEncodedSlashes says no decoding will be done)

https://issues.apache.org/bugzilla/show_bug.cgi?id=35256

本来は %2F を残すために ap_unescape_url_keep2f() という関数でデコードしてるんだけど、実際は %2F を含めて全部デコードしちゃうらしいです。(関数名に keep2f って付いてるのに…)

2.0.47では大丈夫だったけど、2.0.52ではダメだったと書かれています。何かのタイミングで関数を書き換えちゃったのでしょうか?

ちなみに、

this bug exists in 2.2.6 as well

https://issues.apache.org/bugzilla/show_bug.cgi?id=35256

とあるので、まだ治ってないみたいです。とありますが、追記2に書いたようにtrunkでは修正されてます。もっとも、今更変更しても混乱するだけなので、%2Fもデコードしてしまうのが実質的な仕様になるのでしょうね。

というわけで結論としては、やっぱりPATH_INFOで "/" と "%2F" を区別する方法はないみたいです。

実際Googleしてみると、PATH_INFO は怪しいので REQUEST_URI を使いましょう、という記事がいっぱい出てきます。みんな PATH_INFO は使っていないので、バグも気にならないのかもしれませんね。

(追記)PATH_INFOが壊れたのはいつ?

2.0.47では大丈夫だったけど、2.0.52ではダメだったと書かれています。何かのタイミングで関数を書き換えちゃったのでしょうか?

と書いた後に少し気になったので、実際にソースコードの変更履歴を調べてみました。
まず現在のコードになった場所はここのようです。

handling of encoded non-slashes was borked in the
AllowEncodedSlashes path

thanks to FirstBill for pointing that out!

http://svn.apache.org/viewvc/httpd/httpd/trunk/server/util.c?view=log#rev104937

どうやら既存コードだと壊れてしまうので、修正したってことみたいです。
そして肝心の修正内容はこんな感じです。

                 decoded = x2c(y + 1);
-                if (!IS_SLASH(decoded)) {
-                    *x++ = *y++;
-                    *x = *y;
+                if (decoded == '\0') {
+                    badpath = 1;
                 }
                 else {
                     *x = decoded;
                     y += 2;
-                    if (decoded == '\0') {
-                        badpath = 1;
-                    }
                 }

元々はIS_SLASHでデコード結果が / かどうかを判定していたようですが、バグ修正の際に勢い余って消してしまったような感じですね。

さて、この修正の付近をよく見てみると、実は前日にも同じ場所が変更されていました。

Fix the handling of URIs containing %2F when AllowEncodedSlashes is enabled.
Previously, such urls would still be rejected with 404.

http://svn.apache.org/viewvc/httpd/httpd/trunk/server/util.c?view=log#rev104925

この修正前までは、AllowEncodedSlashesオプションを指定しても、実際には動作していなかったはずだ、と書かれています。そして実際の修正内容はこんな感じです。

                 char decoded;
                 decoded = x2c(y + 1);
-                if (IS_SLASH(decoded)) {
+                if (!IS_SLASH(decoded)) {
                     *x++ = *y++;
                     *x = *y;
                 }

IS_SLASHの条件判定が逆になっています。

ところがこの部分は、デコード結果が / だった場合に、デコード前の文字(%2F)の方を採用するルーチンのようです。そのため、この条件判定を逆にしてしまうと、全部の文字がデコードされなくなります。

ということで結論としては、この時の修正に失敗したものを、翌日に再修正してみたら別のバグが混入した、という悲しい理由だったみたいです。修正日は2004年の9月ですので、ずいぶんと昔から今の動作になっていたようです。

ちなみに元々のコードですが、デコード前の文字列が(%2Fのように)3文字あるはずなのに、2文字しかコピーしていません。本来は下記のように修正するべきだったのだと思います。

                 if (IS_SLASH(decoded)) {
                     *x++ = *y++;
+                    *x++ = *y++;
                     *x = *y;
                 }

ただロジック的には、最初の % さえコピーしてしまえば、2文字でも3文字でも別に問題ないコードに見えます。わざわざ修正したということは、何か問題が発生する条件があったのでしょうかね?

ちなみにbugzillaに投稿されているパッチの一つは、上記のコードと同じ内容になっています。

(追記2)修正されてた

よく見たら2007年の9月に修正されてたみたいです。unescapeが一つの関数にまとまってスッキリした作りになっています。それとreservedな文字列を無視できるようになっていて、ちゃんとRFCに書かれた通りに動作可能になったみたいです*2

時期的にはbugzillaで議論してる最中に既に直ってたみたいですが、その後にも症状が出るという人がいるところを見ると、ディストリビューションによっては含まれていなかったりするのかもしれませんね。

*1:ちなみに "unsafe" の定義は見当たりませんでした。

*2:実際の動作は呼び出し側でどのように使っているかに依存しますが

自然言語で検索ってどうなんだろう?

グーグル先生を超える良回答連発、Powersetを使ってみた

タイトルは「グーグル先生を超える良回答連発」なのですが、個別の検索結果をページレベルで見るとそれほど違わない感じです。

Googleでは答えが見つけられなかった例として「MacBook AirってUSBポートあったけ?」という質問が出てきますが、現在のPowersetwikipediaとFreebaseだけを検索対象にしているというのが考慮されていません。

実際に、

site:wikipedia.org Does the MacBook Air has any USB ports?

という感じでwikipediaに限定して検索してあげると、[In addition, the MacBook Air offers only a single USB port...]というPowersetとまったく同じ答えが返ってきます。古典的(?)な手法は十分に有効というべきなのか、単にGoogleが良く出来ているだけなのかは分かりませんが。

もう少しセマンティックな要素として、How manyで数字を認識した例が出てきますが、表示された数字は正解とは違うものになっています。これは文章の内容までを理解できないためだと思いますが、自然言語で検索できると言われて期待するのは、内容理解を含んだレベルではないかと思います。結局は人間が答えを確認する必要があるならば、Googleで十分ということになりそうです。

もちろんセマンティックを利用して検索結果を改善すること自体は良いと思うのですが、セマンティック(自然言語処理)を過剰に期待させるのは逆効果な気がします。Googleのような検索が少し賢くなったレベルが現実的だと思うのですが、マーケティング上の必要性からは無理なのかもしれませんね。

ちなみに個人的に欲しいセマンティックを利用した検索はこんな感じです。

製品名 -page:(価格比較) -page:(ニュースリリース)

最近は検索結果の上位に価格比較が大量に出てきて煩わしいので、そういうサイトを除外できるとうれしいです。Googleとかで実装してくれないかなぁ。

int *n; *n = 5;

はてなブックマークとかで話題になっていたこのコード

int main( void ) {
  int *n;
  *n = 5;
  printf( "%d\n", *n );
  ...
C/C++のポインタの機能--参照渡しのような処理

一見すると普通(?)のコードですが、初期化されていないnを使っているので危険ってやつですね。これだけだと単なる間違いなのですが、説明文も含めてポインタを理解していないのがバレて話題になったということのようです。

何でこういう間違いになってしまったのか、理由と思わしきものがちゃんと書いてあって、

このときnには代入された値を記憶した場所(アドレス)が自動的に代入される

C/C++のポインタの機能--参照渡しのような処理

という勘違いが原因のようです。偶然動いた場合は確かにそのように見えるので、原理を無視すればその通り(?)な気がしますが、残念ながら値を代入する前と後でnの値は変わらないです。

この手のポインタの間違いでいつも思うのですが、

ポインタ変数の宣言では、一般の変数の場合とは異なり、名称の先頭に*がつけられる。

C/C++のポインタの機能--参照渡しのような処理

という説明に問題があるんじゃないかと思います。
コードとして書くとこういうことなんですが、

  int *n;

これだと何か特殊なものを作成してる感じに見えちゃうんだと思います。実際に大学の授業とかの経験でも、int n; は分かっても、int *n; は「魔法の言葉」扱いになっちゃう場合が多いですし。

ポインタより前に float や char のような型の説明をしてるはずなので、

  int* n;

こう書いて int* も型の一種です、という説明の方が素直で分かりやすいと思うのですが、残念ながらそんな感じの入門書に遭遇したことがないです。やっぱり前者の方が主流なので、それに合わせてあるのでしょうか?

あと歴史的経緯を全然知らないのですが、前者の方が主流な理由は何なんでしょう?1行で複数の変数を宣言した場合に紛らわしいから、とかなのかな。

Wicket+SpringをJavaRebelでリロード可能にしてみた

JavaRebel 1.1M2 の変更点に Springでも動くようになったという記述を発見して、ちょうど良い機会だったので、前から気になっていた Wicket + Spring (wicket-spring-annot) な環境での問題に対処してみました。

Now you can develop Spring with JavaRebel (tested!)

JavaRebel Devel Changelog

この記述だけでは何をすれば良いのかさっぱりですが、BLOGの方にIntegrating JavaRebel with Springというタイムリーな記事が追加されてました。Spring自体にApplicationContextを更新する機能が用意されているので、Beanが見つからない場合に実行すれば良いみたいです。こんな機能があるとは知りませんでした。

 ((AbstractRefreshableApplicationContext)getWebApplicationContext()).refresh();


WicketとSpringを使った場合の問題として、実行中に @SpringBean なフィールドを追加しても無視されてしまうというのがあります。本来、JavaRebel はフィールドの追加やリフレクションに対応しているので、実行中に追加しても問題はないはずです。

インジェクション関係の処理を追ってみたところ、org.apache.wicket.injection.Injector がクラスのフィールドをリフレクションで取り出して、@SpringBeanの有無を確認するクラスだと分かりました。この Injector クラスは、最初にクラスのフィールドを取り出したときに、フィールドをキャッシュに格納しています。そのため、後からフィールドを追加しても無視されていたようです。

この問題を解決するには、JavaRebelがクラスをリロードした時に、Injector クラスのキャッシュから削除すれば良さそうです。そこで JavaRebel 1.1M1 の時に出てきた JavaRebel SDK を使ってみることにしました。

JavaRebel SDKGuice に適用した例を見たところ、JavaRebel にリスナークラスを登録しておくと、クラスをリロードした時に呼び出してくれる仕組みになっているようです。

そこで Injector クラスに JavaRebel SDK に用意されている ReloadListener を実装してみました。

//変更前
public class Injector
//変更後
public class Injector implements ReloadListener

クラスがリロードされると、ReloadListener.reloaded() が呼び出されます。Injectorでキャッシュされたフィールドは classToFields という Map に入っていますので、リロードされたクラスに該当するものを削除してやります。

	private ConcurrentHashMap/* <Class, Field[]> */classToFields = new ConcurrentHashMap();
	public void reloaded(Class klass) {
		Iterator it = classToFields.keySet().iterator();
		while(it.hasNext()) {
			if (klass.isAssignableFrom((Class)it.next())) {
				it.remove();
			}
		}
	}

Listener の実装が終わったら、これを JavaRebel に登録してやる必要があります。登録する場所はどこでも良いのですが、Spring利用時に確実に呼び出されそうな org.apache.wicket.injetion.web.InjectorHolder の setInjector() にしてみました。

 	// 変更前
 	public static void setInjector(ConfigurableInjector newInjector)
 	{
 		injector = newInjector;
 	}

ConfigurableInjector は Injector(ReloadListener) を継承しているので、そのまま JavaRebel に登録可能です。ReloaderFactory.getInstance()でReloaderFactoryを取得した後、isReloadEnabled()でJavaRebelが有効かどうかを確認してからListenerとして登録を行います。

 	// 変更後
 	public static void setInjector(ConfigurableInjector newInjector)
 	{
 		injector = newInjector;

		if (ReloaderFactory.getInstance().isReloadEnabled()) {
			ReloaderFactory.getInstance().addReloadListener(injector);
		}
 	}

これでひとまず @SpringBean なフィールドを追加しても再起動が必要なくなりました。

ちなみに Injector クラスのコメントに書かれていた WICKET-625 によると、Injector のキャッシュは将来的に削除されるようです。Wicket 1.3.3 を予定しているみたいなので、将来的にこの変更は必要なくなるようです。


@SpringBeanに関連して、SpringのBeanを追加した場合には、ApplicationContext の更新が必要となるという問題もあります。この問題について、BLOGに書かれていた方法で対処してみました。

BLOGに書かれていた方法は、ApplicationContext で Bean が見つからない場合は、ApplicationContext.refresh()してからもう一度試してみる、というものです。Wicket がApplicationContext を利用している場所は複数あるのですが、実行パス的にはorg.apache.wicket.spring.SpringBeanLocator の2箇所で対応すれば良いようです。

一つは SpringBeanLocator.isSingletonBean() です。

 	// 変更前
	public boolean isSingletonBean()
	{
		if (singletonCache == null)
		{
			singletonCache = Boolean.valueOf(getSpringContext().isSingleton(getBeanName()));
		}
		return singletonCache.booleanValue();
	}
 	// 変更後
	public boolean isSingletonBean()
	{
		if (singletonCache == null)
		{
			try {
				singletonCache = Boolean.valueOf(getSpringContext().isSingleton(getBeanName()));
			} catch (Exception e) {
				refreshApplicationContext();
				singletonCache = Boolean.valueOf(getSpringContext().isSingleton(getBeanName()));
			}
		}
		return singletonCache.booleanValue();
	}

もう一つは SpringBeanLocator.locateProxyTarget() です。

 	// 変更前
	public Object locateProxyTarget()
	{
		final ApplicationContext context = getSpringContext();

		if (beanName != null && beanName.length() > 0)
		{
			try {
				return lookupSpringBean(context, beanName, getBeanType());
			} catch (Exception e) {
				refreshApplicationContext();
				return lookupSpringBean(context, beanName, getBeanType());
			}
		}
		else
		{
			try {
				return lookupSpringBean(context, getBeanType());
			} catch (Exception e) {
				refreshApplicationContext();
				return lookupSpringBean(context, getBeanType());
			}
		}
	}
 	// 変更後
	public Object locateProxyTarget()
	{
		final ApplicationContext context = getSpringContext();

		if (beanName != null && beanName.length() > 0)
		{
			try {
				return lookupSpringBean(context, beanName, getBeanType());
			} catch (Exception e) {
				refreshApplicationContext();
				return lookupSpringBean(context, beanName, getBeanType());
			}
		}
		else
		{
			try {
				return lookupSpringBean(context, getBeanType());
			} catch (Exception e) {
				refreshApplicationContext();
				return lookupSpringBean(context, getBeanType());
			}
		}
	}

ちなみに refreshApplicationContext() はこんな感じで単純にrefresh()を呼び出しているだけです。

	private void refreshApplicationContext() {
		ApplicationContext context = getSpringContext();
		if (context instanceof AbstractRefreshableApplicationContext) {
			((AbstractRefreshableApplicationContext)context).refresh();
		}
	}

以上の変更で、Spring の Bean や @SpringBean なフィールドを追加した場合でもサーバの再起動が必要なくなります。特にページやサービスの追加といった、日常的な部分ではほとんど再起動が必要なくなりますので、結構快適になるのではないかと思います。


今回の変更のpatchと変更を適用したjarを置いておきます。wicket-ioc の方にJavaRebel SDKを含めた状態でビルドしてありますので、jarを置き換えてJavaRebelを有効にするだけで動くと思います。JavaRebelとWicketという恐ろしくマイナー(?)な組み合わせをお使いの方はお試し下さい。