オレオレURLプロトコル

任意の情報を独自プロトコルの URL で指定したいなぁと思い立った今日この頃。

たとえば、hogeプロトコル

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については、次のサイトでわかりやすく説明されています。


A Beginner's Guide to URLs


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)

*1:やっぱ Groovy はラクチンでいいなぁ。

*2:静的なリソースを取得するコードを書くのがめんどくさいという理由もあるが、それは秘密だw

*3:javaで普通に書いたら何行になるかなw