オレオレURLプロトコル
任意の情報を独自プロトコルの URL で指定したいなぁと思い立った今日この頃。
hoge://example.com/foo/bar
とか。
でも、URL って基本的に http プロトコルくらいしか使わんですよねー (´・ω・`)
たとえば http://www.yahoo.co.jp のテキストを取得するとか。(以下 Groovy の例)*1
new URL("http://www.yahoo.co.jp").text
では、http や ftp のようにデフォルトの実装が存在しないプロトコルはどのように扱うんだろ?
たとえば、DB内の情報検索で
oreoredb://example.com/user?userId=10
とか、
sum://a,b
で a と b の和を返すとか。
ということで、ちょっと調べてみました。
URLクラス
まずは、 URL クラスの基本的な情報。
java のクラスライブラリ上では、URL クラスが World Wide Web 上のリソースを特定する Uniform Resource Locator を表現しています。URLについては、次のサイトでわかりやすく説明されています。
URLのシンタックスは RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax で定義され、RFC 2732: Format for Literal IPv6 Addresses in URLs で修正されています。
URLConnection クラス
URLConnection クラスは、アプリケーションと URL の間のコミュニケーション・リンクを表します。
このクラスの利用者は、プロトコルや情報の物理的等を意識しなくても、 getInputStream() メソッド経由でコンテンツにアクセスすることができます。
コンテンツが DB 上にあっても、Webサービス経由での取得であっても、計算による動的であっても、このメソッドを適切に実装すればOKということですねー。
URLStreamHandler クラス
URL クラスでは、プロトコル(http とか ftp とか)ごとに URLStreamHandler を用意することにより、各プロトコルに対する処理を個別に扱っています。
URL#openConnection メソッドは abstract になっていて、各プロトコル用のサブクラス毎に実装する必要があります。
protected abstract URLConnection openConnection(URL u) throws IOException
URLStreamHandlerFactory インタフェース
独自プロトコルを実装するためには、URLStreamHandler を実装してあげればよさそうです。
しかし、URL クラスはどのようにして URLStreamHandler を取得するのでしょうか?
それは、URLStreamHandlerFactory というファクトリを仲介役として行われます。
URLStreamHandlerFactory はインタフェースで、以下のメソッドのみ持っています。
URLStreamHandler createURLStreamHandler(String protocol)
そのため、URLStreamHandlerFactory の実装を URL クラスに設定することにより、URL クラスが特定プロトコルに対する URLStreamHandler を取得できるようになります。
URLStreamHandlerFactory は、URL#setURLStreamHandlerFactory(URLStreamHandlerFactory) メソッドで URL クラスに登録することができます。(ただし、1度しか登録できないので注意。おまけ2参照のこと。)
独自URLプロトコルの作成
ということで、独自プロトコルの作成開始 (`・ω・´)
※実装例は groovy で書かれていますが、特に groovy 依存のクラスは使用していません。この手のソースは純粋な java だと長くなるので、groovy を使ってラクをしようという魂胆ですw
仕様
動的に情報を生成したほうが面白そうなので*2、以下のような仕様を考えてみました。
sum://a,b,...,x という形式をとり、コンテンツとして a + b + ... + x の結果を返します。
URL の例
sum://3,5,11
コンテンツの例
19
URLConnection の実装
sum://a,b,...,x というような書式に対して、a + b + ... + x の結果を返すようにします。
たとえば、URL が sum://3,5,11 の場合に、"19" という文字列を表すInputStream を返します。*3
class SumURLConnection extends URLConnection { SumURLConnection(URL url) { super(url) } void connect() {} InputStream getInputStream() { new ByteArrayInputStream(String.valueOf(getURL().getHost().split(",").inject(0){a,x->a+=Integer.parseInt(x)}).getBytes()) } }
URLStreamHandler の実装
SumURLConnection を作成して返します。
class SumURLStreamHandler extends URLStreamHandler { URLConnection openConnection(URL url) { new SumURLConnection(url) } }
URLStreamHandlerFactory の実装
プロトコル名が sum 以外の場合に SumURLStreamHandler を返します。
class SumURLStreamHandlerFactory implements URLStreamHandlerFactory { URLStreamHandler createURLStreamHandler(String protocol) { "sum"==protocol ? new SumURLStreamHandler() : null } }
URLStreamHandlerFactory実装のURL登録
URL クラスに SumURLStreamHandlerFactory を登録して、sum プロトコルを使えるようにします。
URL.setURLStreamHandlerFactory(new SumURLStreamHandlerFactory())
実行例
1+2+3+4+5の結果をURL経由で取得します。
ans = new URL("sum://1,2,3,4,5").text assert "15" == ans
ということで、 sum プロトコルの実装完了 (`・ω・´)
おまけ
上記の全ソースを1つにつなげたものです。
class SumURLConnection extends URLConnection { SumURLConnection(URL url) { super(url) } void connect() {} InputStream getInputStream() { new ByteArrayInputStream(String.valueOf(getURL().getHost().split(",").inject(0){a,x->a+=Integer.parseInt(x)}).getBytes()) } } class SumURLStreamHandler extends URLStreamHandler { URLConnection openConnection(URL url) { new SumURLConnection(url) } } class SumURLStreamHandlerFactory implements URLStreamHandlerFactory { URLStreamHandler createURLStreamHandler(String protocol) { "sum"==protocol ? new SumURLStreamHandler() : null } } URL.setURLStreamHandlerFactory(new SumURLStreamHandlerFactory()) ans = new URL("sum://1,2,3,4,5").text assert "15" == ans
おまけ2
URL#setURLStreamHandlerFactory(URLStreamHandlerFactory) メソッドを2回以上呼び出そうとすると、以下のようなエラーが出ます (´・ω・`)
2008/10/26 20:41:47 org.codehaus.groovy.runtime.StackTraceUtils sanitize 警告: Sanitizing stacktrace: java.lang.Error: factory already defined at java.net.URL.setURLStreamHandlerFactory(URL.java:1076) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:86) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:230) at groovy.lang.MetaClassImpl.invokeStaticMethod(MetaClassImpl.java:1105) at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:749) at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodN(ScriptBytecodeAdapter.java:170) at Script3.run(Script3:17) at groovy.lang.GroovyShell.evaluate(GroovyShell.java:543) at groovy.lang.GroovyShell.evaluate(GroovyShell.java:484) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:86) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:230) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:912) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:756) at org.codehaus.groovy.runtime.InvokerHelper.invokePogoMethod(InvokerHelper.java:778) at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:758) at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodN(ScriptBytecodeAdapter.java:170) at groovy.ui.Console$_runScriptImpl_closure4.doCall(Console.groovy:491) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:86) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:230) at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:248) at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodOnCurrentN(ScriptBytecodeAdapter.java:78) at groovy.ui.Console$_runScriptImpl_closure4.doCall(Console.groovy) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:86) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:230) at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:248) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:756) at groovy.lang.Closure.call(Closure.java:292) at groovy.lang.Closure.call(Closure.java:287) at groovy.lang.Closure.run(Closure.java:368) at java.lang.Thread.run(Thread.java:619)