📚

Java の標準 URI ライブラリが RFC 3986 に対応していないので、自作ライブラリで対応する羽目になった話

2024/05/16に公開

はじめに

Java 開発者にとっては周知の事実かもしれませんが、Java の標準 URI ライブラリ (java.net.URI クラス) は未だに古い仕様である RFC 2396 をベースに実装されており、今日のデファクトスタンダードである RFC 3986 に完全準拠していません。RFC 6749 (The OAuth 2.0 Authorization Framework) や他の規格・仕様によって RFC 3986 が参照されているにも関わらず標準ライブラリがそれに対応していないとなると、開発者としては不足部分を何らかの手段で補完せざるを得なくなります。かく言う私も自社製品の開発おいて他ライブラリで不足部分をカバーしていましたが、利用していたライブラリに不具合があること、それにも関わらずそのアップデートが数年前でストップしていること、IPV6 が未サポートであること、他のオープンソースライブラリを覗いたもののその実装内容に納得感が持てなかったこと etc... を踏まえ、結果として URI ライブラリを自作する運びとなりました。

標準 URI ライブラリ (java.net.URI クラス) が抱える問題

🔴 アンダースコア (_) を含むホスト

RFC 3986 はアンダースコア (_) を含むホストを許容しますが、RFC 2396 はそれを許容しません。結果として java.net.URI は以下のような振舞いを示します。

// ホスト部にアンダースコア (`_`) を URI を作成。
java.net.URI u = new java.net.URI("http://my_host.com");

// 出力は 'null' となる。
System.out.println(u.getHost());

この問題は JDK-8019345JDK-8221675 といった複数のレポートで報告されているものですが、未だ解決されておらず恐らく今後もこの状態が続くものと予想されます。

また、過去には Spring Framework 関連で以下のような問題もあったようです。

🔴 IPvFuture ホスト

RFC 3986 は、ホストの一種として IPvFuture (例: v9.abc:def) を定義していますが、java.net.URI クラスは RFC 2396 準拠のため、以下のように例外をスローします。

// "java.net.URISyntaxException" がスローされる。
new java.net.URI("http://[v9.abc:def]");

🔴 Scheme 部分のみ持つ URI

RFC 3986 はスキーム部分のみを持つ URI (例: data:) を許容しますが、java.net.URI クラスはこのような URI に対して例外をスローします。

// "java.net.URISyntaxException" がスローされる。
new java.net.URI("data:");

org.czeal.rfc3986 ライブラリ

上述した問題等をクリアし、RFC 3986 への準拠を目的として生まれたのが org.czeal.rfc3986 ライブラリになります。以下ではその使用方法等について具体的に解説していきます。

インストール

<dependency>
    <groupId>org.czeal</groupId>
    <artifactId>rfc3986</artifactId>
    <version>{version}</version>
</dependency>

{version} の値については、Github のタグページをご参照ください。

使用方法

✅ URI のパース

URI (URI Reference) をパースするためには、

  • URIReference.parse(String uriRef)
  • URIReference.parse(String uriRef, Charset charset)

を利用してください。以下は URIReference.parse(String uriRef) の使用例になります。

Example 1: ベーシックな URI のパース

URIReference uriRef = URIReference.parse("http://example.com/a/b");

System.out.println(uriRef.toString());                // "http://example.com/a/b"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "example.com"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "REGNAME"
System.out.println(uriRef.getHost().getValue());      // "example.com"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // "/a/b"
System.out.println(uriRef.getQuery());                // null
System.out.println(uriRef.getFragment());             // null

Example 2: 相対レファレンス (relative reference) のパース

URIReference uriRef = URIReference.parse("//example.com/a/b");

System.out.println(uriRef.toString());                // "//example.com/a/b"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // null
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "example.com"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "REGNAME"
System.out.println(uriRef.getHost().getValue());      // "example.com"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // "/a/b"
System.out.println(uriRef.getQuery());                // null
System.out.println(uriRef.getFragment());             // null

Example 3: ホスト部が IPV4 形式の URI のパース

URIReference uriRef = URIReference.parse("http://101.102.103.104");

System.out.println(uriRef.toString());                // "http://101.102.103.104"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "101.102.103.104"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "IPV4"
System.out.println(uriRef.getHost().getValue());      // "101.102.103.104"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // null
System.out.println(uriRef.getQuery());                // null
System.out.println(uriRef.getFragment());             // null

Example 4: ホスト部が IPV6 形式の URI のパース

URIReference uriRef = URIReference.parse("http://[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]");

System.out.println(uriRef.toString());                // "http://[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "IPV6"
System.out.println(uriRef.getHost().getValue());      // "[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // null
System.out.println(uriRef.getQuery());                // null
System.out.println(uriRef.getFragment());             // null

Example 5: ホスト部が IPvFuture 形式の URI のパース

URIReference uriRef = URIReference.parse("http://[v9.abc:def]");

System.out.println(uriRef.toString());                // "http://[v9.abc:def]"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "[v9.abc:def]"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "IPVFUTURE"
System.out.println(uriRef.getHost().getValue());      // "[v9.abc:def]"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // null
System.out.println(uriRef.getQuery());                // null
System.out.println(uriRef.getFragment());             // null

Example 6: ホスト部がパーセントエンコードされた URI のパース

URIReference uriRef = URIReference.parse("http://%65%78%61%6D%70%6C%65.com");

System.out.println(uriRef.toString());                // "http://%65%78%61%6D%70%6C%65.com"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "%65%78%61%6D%70%6C%65.com"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "REGNAME"
System.out.println(uriRef.getHost().getValue());      // "%65%78%61%6D%70%6C%65.com"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // null
System.out.println(uriRef.getQuery());                // null
System.out.println(uriRef.getFragment());             // null

✅ URI の解決

URI を解決するためには、

  • resolve(String uriRef)
  • resolve(URIReference uriRef)

を使用してください。以下の例では resolve(URIReference uriRef) を利用して相対レファレンスを解決しています。

// ベース URI。
URIReference baseUri = URIReference.parse("http://example.com");

// 解決対象の相対レファレンス。
URIReference relRef = URIReference.parse("/a/b");

// ベース URI に対して、相対レファレンスを解決する。
URIReference resolved = baseUri.resolve(relRef);

System.out.println(resolved.toString());                // "http://example.com/a/b"
System.out.println(resolved.isRelativeReference());     // false
System.out.println(resolved.getScheme());               // "http"
System.out.println(resolved.hasAuthority());            // true
System.out.println(resolved.getAuthority().toString()); // "example.com"
System.out.println(resolved.getUserinfo());             // null
System.out.println(resolved.getHost().getType());       // "REGNAME"
System.out.println(resolved.getHost().getValue());      // "example.com"
System.out.println(resolved.getPort());                 // -1
System.out.println(resolved.getPath());                 // "/a/b"
System.out.println(resolved.getQuery());                // null
System.out.println(resolved.getFragment());             // null

✅ URI の正規化

URI を正規化するためには、URIReference インスタンスの normalize() メソッドを呼び出してください。

Example 1: 大文字・小文字が混在してる URI の正規化

URIReference normalized = URIReference.parse("hTTp://example.com")
                                      .normalize();

System.out.println(normalized.toString());                // "http://example.com/"
System.out.println(normalized.isRelativeReference());     // false
System.out.println(normalized.getScheme());               // "http"
System.out.println(normalized.hasAuthority());            // true
System.out.println(normalized.getAuthority().toString()); // "example.com"
System.out.println(normalized.getUserinfo());             // null
System.out.println(normalized.getHost().getType());       // "REGNAME"
System.out.println(normalized.getHost().getValue());      // "example.com"
System.out.println(normalized.getPort());                 // -1
System.out.println(normalized.getPath());                 // "/"
System.out.println(normalized.getQuery());                // null
System.out.println(normalized.getFragment());             // null

Example 2: ホスト部がパーセントエンコードされた URI の正規化

URIReference normalized = URIReference.parse("http://%65%78%61%6D%70%6C%65.com")
                                      .normalize();

System.out.println(normalized.toString());                // "http://example.com/"
System.out.println(normalized.isRelativeReference());     // false
System.out.println(normalized.getScheme());               // "http"
System.out.println(normalized.hasAuthority());            // true
System.out.println(normalized.getAuthority().toString()); // "example.com"
System.out.println(normalized.getUserinfo());             // null
System.out.println(normalized.getHost().getType());       // "REGNAME"
System.out.println(normalized.getHost().getValue());      // "example.com"
System.out.println(normalized.getPort());                 // -1
System.out.println(normalized.getPath());                 // "/"
System.out.println(normalized.getQuery());                // null
System.out.println(normalized.getFragment());             // null

Example 3: 相対パスを含む URI の正規化

URIReference normalized = URIReference.parse("http://example.com/a/b/c/../d/")
                                      .normalize();

System.out.println(normalized.toString());                // "http://example.com/a/b/d/"
System.out.println(normalized.isRelativeReference());     // false
System.out.println(normalized.getScheme());               // "http"
System.out.println(normalized.hasAuthority());            // true
System.out.println(normalized.getAuthority().toString()); // "example.com"
System.out.println(normalized.getUserinfo());             // null
System.out.println(normalized.getHost().getType());       // "REGNAME"
System.out.println(normalized.getHost().getValue());      // "example.com"
System.out.println(normalized.getPort());                 // -1
System.out.println(normalized.getPath());                 // "/a/b/d/"
System.out.println(normalized.getQuery());                // null
System.out.println(normalized.getFragment());             // null

Example 4: 相対レファレンスの正規化

// 相対レファレンスのパース。
URIReference relRef = URIReference.parse("/a/b/c/../d/");

// "http://example.com" に対して相対レファレンスを解決。
// 注意: 相対レファレンスは正規化の前に解決しておく必要がある。
URIReference resolved = relRef.resolve("http://example.com");

// 解決された URI を正規化。
URIReference normalized = resolved.normalize();

System.out.println(normalized.toString());                // "http://example.com/a/b/d/"
System.out.println(normalized.isRelativeReference());     // false
System.out.println(normalized.getScheme());               // "http"
System.out.println(normalized.hasAuthority());            // true
System.out.println(normalized.getAuthority().toString()); // "example.com"
System.out.println(normalized.getUserinfo());             // null
System.out.println(normalized.getHost().getType());       // "REGNAME"
System.out.println(normalized.getHost().getValue());      // "example.com"
System.out.println(normalized.getPort());                 // -1
System.out.println(normalized.getPath());                 // "/a/b/d/"
System.out.println(normalized.getQuery());                // null
System.out.println(normalized.getFragment());             // null

✅ URI の構築

URI を構築するためには、URIReferenceBuilder クラスを利用してください。

Example 1: ベーシックな URI の構築

URIReference uriRef = new URIReferenceBuilder()
                          .setScheme("http") 
                          .setHost("example.com")
                          .setPath("/a/b/c")
                          .query("k1", "v1")
                          .build();

System.out.println(uriRef.toString());                // "http://example.com/a/b/c?k1=v1"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "example.com"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "REGNAME"
System.out.println(uriRef.getHost().getValue());      // "example.com"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // "/a/b/c"
System.out.println(uriRef.getQuery());                // "k1=v1"
System.out.println(uriRef.getFragment());             // null

Example 2: 既存の URI から新たな URI を構築

URIReference uriRef = new URIReferenceBuilder()
                          .fromURIReference("http://example.com/a/b/c?k1=v1")
                          .appendPath("d", "e", "f")
                          .appendQueryParam("k2", "v2")
                          .build();

System.out.println(uriRef.toString());                // "http://example.comd/a/b/c/d/e/f?k1=v1&k2=v2"
System.out.println(uriRef.isRelativeReference());     // false
System.out.println(uriRef.getScheme());               // "http"
System.out.println(uriRef.hasAuthority());            // true
System.out.println(uriRef.getAuthority().toString()); // "example.com"
System.out.println(uriRef.getUserinfo());             // null
System.out.println(uriRef.getHost().getType());       // "REGNAME"
System.out.println(uriRef.getHost().getValue());      // "example.com"
System.out.println(uriRef.getPort());                 // -1
System.out.println(uriRef.getPath());                 // "/a/b/c/d/e/f"
System.out.println(uriRef.getQuery());                // "k1=v1&k2=&v2"
System.out.println(uriRef.getFragment());             // null

最後に

個人的な所感ではありますが、RFC 3986 は他の仕様に比べて記述のされ方がアカデミックであり、実装レベルまで理解を落とし込むまでに想定以上の時間を要しました。本ライブラリ開発のきっかけは本業で携わっているソフトウェアにあるのですが、実装自体も隙間時間で行っていたため開発開始から最終的なリリースまで一年以上かかってしまいました。今後も不具合や実用上での問題等あればアップデートしていく次第ですので、是非フィードバック頂けますと幸いです。

最後までご拝読頂きありがとうございました。

参考文献

Authlete

Discussion