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

# はじめに

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

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

### :red_circle: アンダースコア (`_`) を含むホスト

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

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

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

この問題は [JDK-8019345](https://bugs.openjdk.org/browse/JDK-8019345) や [JDK-8221675](https://bugs.openjdk.org/browse/JDK-8221675) といった複数のレポートで報告されているものですが、未だ解決されておらず恐らく今後もこの状態が続くものと予想されます。

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

- [java.net.URIで扱えるホスト名はRFC 2396 (RFC 3986じゃない・・)](https://qiita.com/kazuki43zoo/items/dba746f224ea4f7d34d6)
- [SpringのCORS機能がRFC 3986なホスト名に対応してなかった・・・](https://qiita.com/kazuki43zoo/items/43abfb72ddf0f1096fa7)

### :red_circle: IPvFuture ホスト

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

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

### :red_circle: Scheme 部分のみ持つ URI

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

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

# `org.czeal.rfc3986` ライブラリ

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

## インストール

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

`{version}` の値については、[Github のタグページ](https://github.com/hidebike712/rfc3986/tags)をご参照ください。

## 使用方法

### :white_check_mark: URI のパース

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

- `URIReference.parse(String uriRef)`
- `URIReference.parse(String uriRef, Charset charset)` 

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

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

```java
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) のパース

```java
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 のパース

```java
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 のパース

```java
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 のパース

```java
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 のパース

```java
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
```

---

### :white_check_mark: URI の解決

URI を解決するためには、

- `resolve(String uriRef)`
- `resolve(URIReference uriRef)`

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

```java
// ベース 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
```

---

### :white_check_mark: URI の正規化

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

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

```java
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 の正規化

```java
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 の正規化

```java
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: 相対レファレンスの正規化

```java
// 相対レファレンスのパース。
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
```

:::note warn
[RFC 3986, 5.2.1](https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.1) にあるように、相対レファレンスは正規化される前に解決されなければなりません。
> RFC 3986, 5.2.1. Pre-parse the Base URI
>
> A URI reference must be transformed to its target URI before
> it can be normalized.
:::

### :white_check_mark: URI の構築

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

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

```java
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 を構築

```java
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
```

:::note warn
現在の `URIReferenceBuilder` の実装では、基本的に `build()` メソッドが呼び出されるまで例外はスローされません。これは `build()` メソッドが呼び出されない限り、各 URI コンポーネントに対するバリデーションが実行されないためです。
:::

# 最後に

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

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

# 参考文献

- [RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax](https://datatracker.ietf.org/doc/html/rfc3986)
- [RFC 2396 - Uniform Resource Identifiers (URI): Generic Syntax](https://datatracker.ietf.org/doc/rfc2396)
- [java.net.URIで扱えるホスト名はRFC 2396 (RFC 3986じゃない・・)](https://qiita.com/kazuki43zoo/items/dba746f224ea4f7d34d6)
- [SpringのCORS機能がRFC 3986なホスト名に対応してなかった・・・](https://qiita.com/kazuki43zoo/items/43abfb72ddf0f1096fa7)