🥾

Uri.Builder#appendQueryParameter()の実装について調べてみる

2021/06/21に公開

URL文字列にクエリパラメータを付与するときは、
Uri.Builder クラスのaddQueryParameterメソッドを使用します。

https://developer.android.com/reference/android/net/Uri.Builder#appendQueryParameter(java.lang.String, java.lang.String)

val uriString = "https://example.com/"
val uri = Uri.parse(uriString)
    .buildUpon()
    .appendQueryParameter("key", "value")
    .appendQueryParameter("test", "fuga")
    .build()

という感じになります。
uri.toString()
"https://example.com/?key=value&test=fuga"
という文字列が取得できます。

appendQueryParameter()は何をしている?

この時点でappendQueryParameter()はクエリの初めに'?'連結部分には'&'を追加する処理だと予想していました。実装を見ていきます。

Uri.java
/**
 * Encodes the key and value and then appends the parameter to the
 * query string.
 *
 * @param key which will be encoded
 * @param value which will be encoded
 */
public Builder appendQueryParameter(String key, String value) {
    // This URI will be hierarchical.
    this.opaquePart = null;
    String encodedParameter = encode(key, null) + "="
            + encode(value, null);
    if (query == null) {
        query = Part.fromEncoded(encodedParameter);
        return this;
    }
    String oldQuery = query.getEncoded();
    if (oldQuery == null || oldQuery.length() == 0) {
        query = Part.fromEncoded(encodedParameter);
    } else {
        query = Part.fromEncoded(oldQuery + "&" + encodedParameter);
    }
    return this;

処理内容は

  1. keyとvalueをエンコード
  2. クエリがない場合はそのままエンコードされたものを返し、クエリが既に存在する場合は&で結合して返却する
    です。

...どうやらappenQueryParameter()では'?'をクエリの先頭に付与するような処理は実装されていないようでした。なるほど。

結論から言うと'?'appendQueryParameter()ではなく、toString()から呼び出される
makeUrlString()内のappendSspTo()メソッド内で付与されていました。

Uri.java
// #1255~
private void appendSspTo(StringBuilder builder) {
    String encodedAuthority = authority.getEncoded();
    if (encodedAuthority != null) {
        // Even if the authority is "", we still want to append "//".
        builder.append("//").append(encodedAuthority);
    }
    String encodedPath = path.getEncoded();
    if (encodedPath != null) {
        builder.append(encodedPath);
    }
    if (!query.isEmpty()) {
        builder.append('?').append(query.getEncoded());
    }
}

appendSspTo()処理の中でurlの//以降の部分を組み立てていることがわかります。
ここで私の知らない概念であるSSPという言葉が出てきました。

SSPって?

RFC1758[https://datatracker.ietf.org/doc/html/rfc1738]にこのような記述があります。

2.1. The main parts of URLs

   A full BNF description of the URL syntax is given in Section 5.

   In general, URLs are written as follows:

       <scheme>:<scheme-specific-part>

   A URL contains the name of the scheme being used (<scheme>) followed
   by a colon and then a string (the <scheme-specific-part>) whose
   interpretation depends on the scheme.

   Scheme names consist of a sequence of characters. The lower case
   letters "a"--"z", digits, and the characters plus ("+"), period
   ("."), and hyphen ("-") are allowed. For resiliency, programs
   interpreting URLs should treat upper case letters as equivalent to
   lower case in scheme names (e.g., allow "HTTP" as well as "http").

URLのスキーム部分+セミコロンより後ろをScheme-Specific-Partと呼ぶということです。
上で確認した実装の通りですが、つまりは//以降の部分のことを指しています。
これはappendSspTo()の呼び元であるmakeUrlString()を見てもわかります

Uri.java
// #1318~
private String makeUriString() {
    StringBuilder builder = new StringBuilder();
    if (scheme != null) {
        builder.append(scheme).append(':');
    }
    appendSspTo(builder);
    if (!fragment.isEmpty()) {
        builder.append('#').append(fragment.getEncoded());
    }
    return builder.toString();
}

シンプルに文字列を連結させています。(またソースから分かる通り、SSPの後ろには必要に応じてfragmentが付与されます。

まとめ

appendQueryParameterメソッドの実装を調べてみて、URLの基本的な構造についての知識を深めることができました。知らなくても使えるという側面には良い部分もあるとは思うのですが、理解していないが故にバグを生むこともあると思うので、興味を持って調べていきたいな〜という感想です。

参考

http://yuki312.blogspot.com/2012/03/androiduriuribuilderapi.html
https://codechacha.com/ja/android-uri-ssp/

Discussion