• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.libraries.mobiledatadownload.file.common;
17 
18 import static com.google.android.libraries.mobiledatadownload.file.common.internal.Charsets.UTF_8;
19 
20 import android.net.Uri;
21 import android.text.TextUtils;
22 import com.google.android.libraries.mobiledatadownload.file.common.internal.Preconditions;
23 import com.google.errorprone.annotations.CanIgnoreReturnValue;
24 import java.io.UnsupportedEncodingException;
25 import java.net.URLDecoder;
26 import java.net.URLEncoder;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.List;
30 import javax.annotation.Nullable;
31 
32 /**
33  * A URI fragment parser and builder. Parses fragment params is similar to parsing of query params,
34  * and are delimited by "&". For example, <code>#foo=bar&us=them</code>
35  *
36  * <p>produces two params: one "foo" with value "bar", and another "us" with value "them".
37  *
38  * <p>Each fragment param must have at least one value; multiple values are delimited by "+". For
39  * example, <code>#foo=bar+baz</code>
40  *
41  * <p>produces one param, "foo", with two values "bar" and "baz".
42  *
43  * <p>Furthermore, fragment values can have subparams, which are additional information scoped to
44  * the value. Subparams have keys and optional values, and are delimited by ",". For example, <code>
45  * #foo=bar(x=1)+baz(this=that,when)</code>
46  *
47  * <p>produces one param, "foo", with two values "bar" and "baz" where "bar" has subparam "x" set at
48  * 1, baz has subparam "this" set at "that", and "when" is unset.
49  *
50  * <p>While the <internal> spec requires that keys and values are [0-9a-zA-Z-_]+, this class
51  * encodes/decodes keys/values, including subparams using java.net.URLEncoder/decoder. In
52  * particular, this class is responsible for producing strings suitable for being appended verbatim
53  * to the fragment part of an RFC3986 URI.
54  */
55 public final class Fragment {
56   public static final Fragment EMPTY_FRAGMENT = new Fragment(null);
57   public static final Fragment.ParamValue EMPTY_FRAGMENT_PARAM_VALUE = new ParamValue(null, null);
58 
59   private final List<Param> params = new ArrayList<Param>();
60 
Fragment(@ullable List<Param> params)61   private Fragment(@Nullable List<Param> params) {
62     if (params != null) {
63       this.params.addAll(params);
64     }
65   }
66 
67   /** Create a new, empty builder. */
builder()68   public static Fragment.Builder builder() {
69     return new Fragment.Builder(null);
70   }
71 
72   /** Return a builder based on this Fragment. */
toBuilder()73   public Fragment.Builder toBuilder() {
74     Fragment.Builder builder = builder();
75     for (Param param : params) {
76       builder.addParam(param.toBuilder());
77     }
78     return builder;
79   }
80 
81   /** Iterate over the params. */
params()82   public List<Param> params() {
83     return Collections.unmodifiableList(params);
84   }
85 
86   /** Finds the param with the given key. Returns null if not found. */
87   @Nullable
findParam(String key)88   public Param findParam(String key) {
89     for (Param param : params) {
90       if (param.key.equals(key)) {
91         return param;
92       }
93     }
94     return null;
95   }
96 
97   @Override
toString()98   public String toString() {
99     return TextUtils.join("&", params);
100   }
101 
102   /** Builder for the whole URI fragment. */
103   public static final class Builder {
104     private final List<Param.Builder> params = new ArrayList<Param.Builder>();
105 
Builder(@ullable List<Param.Builder> params)106     private Builder(@Nullable List<Param.Builder> params) {
107       if (params == null) {
108         return;
109       }
110       for (Param.Builder param : params) {
111         addParam(param);
112       }
113     }
114 
115     /** Get all of the params as a list. */
params()116     public List<Param.Builder> params() {
117       return Collections.unmodifiableList(params);
118     }
119 
120     /** Finds the param with the given key. Returns null if not found. */
121     @Nullable
findParam(String key)122     public Param.Builder findParam(String key) {
123       for (Param.Builder param : params) {
124         if (param.key.equals(key)) {
125           return param;
126         }
127       }
128       return null;
129     }
130 
131     /** Adds a param. If a param with same key already exists, this replaces it. */
132     @CanIgnoreReturnValue
addParam(Param param)133     public Builder addParam(Param param) {
134       addParam(param.toBuilder());
135       return this;
136     }
137 
138     /** Adds a param. If a param with the same key already exist, this replaces it. */
139     @CanIgnoreReturnValue
addParam(Param.Builder param)140     public Builder addParam(Param.Builder param) {
141       for (int i = 0; i < params.size(); i++) {
142         if (params.get(i).key.equals(param.key)) {
143           params.set(i, param);
144           return this;
145         }
146       }
147       params.add(param);
148       return this;
149     }
150 
151     /** Adds a simple param with no value. */
152     @CanIgnoreReturnValue
addParam(String key)153     public Builder addParam(String key) {
154       return addParam(Param.builder(key));
155     }
156 
157     /** Return an immutable Fragment. Unset params are ignored. */
build()158     public Fragment build() {
159       List<Param> params = new ArrayList<Param>();
160       for (Param.Builder builder : this.params) {
161         Param param = builder.build();
162         if (param != null) {
163           params.add(param);
164         }
165       }
166       return new Fragment(params);
167     }
168   }
169 
170   /** A fragment param. */
171   public static final class Param {
172     private final String key;
173     private final List<ParamValue> values = new ArrayList<ParamValue>();
174 
175     /**
176      * @throws IllegalArgumentException if {@code values} is empty.
177      */
Param(String key, List<ParamValue> values)178     private Param(String key, List<ParamValue> values) {
179       Preconditions.checkArgument(!values.isEmpty(), "Missing param values");
180       this.key = key;
181       this.values.addAll(values);
182     }
183 
184     /** Gets the key for the param. */
key()185     public String key() {
186       return key;
187     }
188 
189     /** Iterate over the values. */
values()190     public List<ParamValue> values() {
191       return Collections.unmodifiableList(values);
192     }
193 
194     /** Find a value by name. Returns null if not found. */
195     @Nullable
findValue(String name)196     public ParamValue findValue(String name) {
197       for (ParamValue value : values) {
198         if (value.name.equals(name)) {
199           return value;
200         }
201       }
202       return null;
203     }
204 
205     /**
206      * Create a new param identified with a key.
207      *
208      * @param key The unique key.
209      * @return The param.
210      */
builder(String key)211     public static Param.Builder builder(String key) {
212       return new Param.Builder(key, null);
213     }
214 
215     /** Return a builder based on this Param. */
toBuilder()216     public Param.Builder toBuilder() {
217       Param.Builder builder = builder(key);
218       for (ParamValue value : values) {
219         builder.addValue(value.toBuilder());
220       }
221       return builder;
222     }
223 
224     @Override
toString()225     public String toString() {
226       StringBuilder builder = new StringBuilder();
227       builder.append(urlEncode(key));
228       builder.append("=");
229       builder.append(TextUtils.join("+", values));
230       return builder.toString();
231     }
232 
233     /** Builder for a fragment param. */
234     public static final class Builder {
235       private final String key;
236       private final List<ParamValue.Builder> values = new ArrayList<ParamValue.Builder>();
237 
Builder(String key, @Nullable List<ParamValue.Builder> values)238       private Builder(String key, @Nullable List<ParamValue.Builder> values) {
239         this.key = key;
240         if (values == null) {
241           return;
242         }
243         for (ParamValue.Builder value : values) {
244           addValue(value);
245         }
246       }
247 
248       /** Gets the key for the param. */
key()249       public String key() {
250         return key;
251       }
252 
253       /** Get all of the param values as a list. */
values()254       public List<ParamValue.Builder> values() {
255         return Collections.unmodifiableList(values);
256       }
257 
258       /** Find a value by name. Returns null if not found. */
259       @Nullable
findValue(String name)260       public ParamValue.Builder findValue(String name) {
261         for (ParamValue.Builder value : values) {
262           if (value.name.equals(name)) {
263             return value;
264           }
265         }
266         return null;
267       }
268 
269       /**
270        * Adds a value to this param. If a value already exists with the same name, this will replace
271        * it.
272        */
273       @CanIgnoreReturnValue
addValue(ParamValue value)274       public Builder addValue(ParamValue value) {
275         addValue(value.toBuilder());
276         return this;
277       }
278 
279       /**
280        * Adds a value to this param. If a value already exists with the same name, this will replace
281        * it.
282        */
283       @CanIgnoreReturnValue
addValue(ParamValue.Builder value)284       public Builder addValue(ParamValue.Builder value) {
285         for (int i = 0; i < values.size(); i++) {
286           if (values.get(i).name.equals(value.name)) {
287             values.set(i, value);
288             return this;
289           }
290         }
291         values.add(value);
292         return this;
293       }
294 
295       /** Adds a value that has no subparams. Also replaces existing value if present. */
296       @CanIgnoreReturnValue
addValue(String name)297       public Builder addValue(String name) {
298         return addValue(new ParamValue.Builder(name, null));
299       }
300 
301       /** Return a new immutable Param from this builder, or null if the param is unset. */
302       @Nullable
build()303       public Param build() {
304         if (this.values.isEmpty()) {
305           return null;
306         }
307         List<ParamValue> values = new ArrayList<ParamValue>();
308         for (ParamValue.Builder value : this.values) {
309           values.add(value.build());
310         }
311         return new Param(key, values);
312       }
313     }
314   }
315 
316   /** A value of a fragment param. Each fragment param can have multiple values. */
317   public static final class ParamValue {
318 
319     private final String name;
320     private final List<SubParam> subparams = new ArrayList<SubParam>();
321 
ParamValue(String name, @Nullable List<SubParam> subparams)322     private ParamValue(String name, @Nullable List<SubParam> subparams) {
323       this.name = name;
324       if (subparams != null) {
325         this.subparams.addAll(subparams);
326       }
327     }
328 
329     /** Creates a new param value with the given name. */
builder(String name)330     public static ParamValue.Builder builder(String name) {
331       return new ParamValue.Builder(name, null);
332     }
333 
334     /** Return a builder based on this ParamValue. */
toBuilder()335     public ParamValue.Builder toBuilder() {
336       ParamValue.Builder builder = builder(name);
337       for (SubParam subparam : subparams) {
338         builder.addSubParam(subparam);
339       }
340       return builder;
341     }
342 
343     /** The name of the param value. */
name()344     public String name() {
345       return name;
346     }
347 
348     /** Iterate over the subparams. */
subParams()349     public List<SubParam> subParams() {
350       return Collections.unmodifiableList(subparams);
351     }
352 
353     /** Finds a subparam with the given key. If not found, returns null. */
354     @Nullable
findSubParam(String key)355     public SubParam findSubParam(String key) {
356       for (SubParam subparam : subparams) {
357         if (subparam.key.equals(key)) {
358           return subparam;
359         }
360       }
361       return null;
362     }
363 
364     /**
365      * Finds the subparam value with the given key. If the subparam or value is null, returns null.
366      *
367      * @param key
368      * @return The value of the subparam or null.
369      */
370     @Nullable
findSubParamValue(String key)371     public String findSubParamValue(String key) {
372       SubParam subparam = findSubParam(key);
373       return (subparam == null) ? null : subparam.value;
374     }
375 
376     @Override
toString()377     public String toString() {
378       StringBuilder builder = new StringBuilder();
379       builder.append(urlEncode(name));
380       if (subparams.isEmpty()) {
381         return builder.toString();
382       }
383       builder.append("(");
384       builder.append(TextUtils.join(",", subparams));
385       builder.append(")");
386       return builder.toString();
387     }
388 
389     /** Builder for a fragment param value. */
390     public static final class Builder {
391       private final String name;
392       private final List<SubParam> subparams = new ArrayList<SubParam>();
393 
Builder(String name, @Nullable List<SubParam> subparams)394       private Builder(String name, @Nullable List<SubParam> subparams) {
395         this.name = name;
396         if (subparams == null) {
397           return;
398         }
399         for (SubParam subparam : subparams) {
400           addSubParam(subparam);
401         }
402       }
403 
404       /** The name of the param value. */
name()405       public String name() {
406         return name;
407       }
408 
409       /** Get all of the subparams as a list. */
subparams()410       public List<SubParam> subparams() {
411         return Collections.unmodifiableList(subparams);
412       }
413 
414       /** Finds a subparam with the given key. If not found, returns null. */
415       @Nullable
findSubParam(String key)416       public SubParam findSubParam(String key) {
417         for (SubParam subparam : subparams) {
418           if (subparam.key.equals(key)) {
419             return subparam;
420           }
421         }
422         return null;
423       }
424 
425       /**
426        * Finds the subparam value with the given key. If the subparam or value is null, returns
427        * null.
428        *
429        * @param key
430        * @return The value of the subparam or null.
431        */
432       @Nullable
findSubParamValue(String key)433       public String findSubParamValue(String key) {
434         SubParam subparam = findSubParam(key);
435         return (subparam == null) ? null : subparam.value;
436       }
437 
438       /**
439        * Adds a subparam. If an existing subparam exists with the same key, this will replace it.
440        *
441        * @param subparam
442        * @return The subparam or null if not found.
443        */
444       @CanIgnoreReturnValue
addSubParam(SubParam subparam)445       public Builder addSubParam(SubParam subparam) {
446         for (int i = 0; i < subparams.size(); i++) {
447           if (subparams.get(i).key.equals(subparam.key)) {
448             subparams.set(i, subparam);
449             return this;
450           }
451         }
452         subparams.add(subparam);
453         return this;
454       }
455 
456       /**
457        * Shortcut to add a subparam with this key and value. Replaces existing subparam with same
458        * key if present.
459        *
460        * @param key The subparam key.
461        * @param value The subparam value.
462        */
463       @CanIgnoreReturnValue
addSubParam(String key, String value)464       public Builder addSubParam(String key, String value) {
465         return addSubParam(new SubParam(key, value));
466       }
467 
468       /** Build an immutable ParamValue from this builder. */
build()469       public ParamValue build() {
470         return new ParamValue(name, subparams);
471       }
472     }
473   }
474 
475   /** A fragment param value subparam. */
476   public static final class SubParam {
477 
478     private final String key;
479     @Nullable private final String value;
480 
481     /** Creates a new subparam with the given key and value. */
build(String key, String value)482     public static SubParam build(String key, String value) {
483       return new SubParam(key, value);
484     }
485 
486     /** Creates a new subparam with the given key and no value. */
build(String key)487     public static SubParam build(String key) {
488       return new SubParam(key, null);
489     }
490 
SubParam(String key, @Nullable String value)491     private SubParam(String key, @Nullable String value) {
492       this.key = key;
493       this.value = value;
494     }
495 
496     /** Returns the subparam key. */
key()497     public String key() {
498       return key;
499     }
500 
501     /** Returns the subparam value, or null if not set. */
502     @Nullable
value()503     public String value() {
504       return value;
505     }
506 
507     /** Returns true if the subparam has a value set. */
hasValue()508     public boolean hasValue() {
509       return value != null;
510     }
511 
512     @Override
toString()513     public String toString() {
514       if (hasValue()) {
515         return urlEncode(key) + "=" + urlEncode(value);
516       } else {
517         return urlEncode(key);
518       }
519     }
520   }
521 
522   /** Parses a fragment from the uri as described in {@link Fragment}. */
parse(Uri uri)523   public static Fragment parse(Uri uri) {
524     return parse(uri.getEncodedFragment());
525   }
526 
527   /** Parses a fragment from an encoded string as described in {@link Fragment}. */
parse(@ullable String encodedFragment)528   public static Fragment parse(@Nullable String encodedFragment) {
529     if (TextUtils.isEmpty(encodedFragment)) {
530       return EMPTY_FRAGMENT;
531     }
532     List<Param.Builder> params = new ArrayList<Param.Builder>();
533     for (String kvPair : encodedFragment.split("&")) {
534       String[] kv = kvPair.split("=", 2);
535       List<ParamValue.Builder> values = new ArrayList<ParamValue.Builder>();
536       String key = kv[0];
537       Preconditions.checkArgument(!TextUtils.isEmpty(key), "malformed key: %s", encodedFragment);
538       Preconditions.checkArgument(
539           kv.length == 2 && !TextUtils.isEmpty(kv[1]), "missing param value: %s", encodedFragment);
540       String rawValues = kv[1];
541       String[] splitValues = rawValues.split("\\+");
542       for (int i = 0; i < splitValues.length; i++) {
543         String value = splitValues[i];
544         if (value.isEmpty()) {
545           continue;
546         }
547         List<SubParam> subparams = null;
548         int lparen = value.indexOf("(");
549         if (lparen != -1) {
550           String rawSubparams = value.substring(lparen);
551           Preconditions.checkArgument(
552               rawSubparams.charAt(0) == '('
553                   && rawSubparams.charAt(rawSubparams.length() - 1) == ')',
554               "malformed fragment subparams: %s",
555               encodedFragment);
556           subparams = parseSubParams(rawSubparams.substring(1, rawSubparams.length() - 1));
557           value = value.substring(0, lparen);
558         } else {
559           Preconditions.checkArgument(
560               !value.contains(")"), "malformed fragment subparams: %s", encodedFragment);
561         }
562         values.add(new ParamValue.Builder(urlDecode(value), subparams));
563       }
564       params.add(new Param.Builder(urlDecode(key), values));
565     }
566     return new Fragment.Builder(params).build();
567   }
568 
569   // TODO: This method probably should be elsewhere, perhaps in a lite fragment helper class.
570   @Nullable
getTransformSubParam(Uri uri, String transformName, String subParamKey)571   public static String getTransformSubParam(Uri uri, String transformName, String subParamKey) {
572     Fragment.ParamValue value = getTransformParamValue(uri, transformName);
573     if (value == null) {
574       return null;
575     }
576     String result = value.findSubParamValue(subParamKey);
577     return !TextUtils.isEmpty(result) ? result : null;
578   }
579 
580   @Nullable
getTransformParamValue(Uri uri, String transformName)581   public static Fragment.ParamValue getTransformParamValue(Uri uri, String transformName) {
582     Fragment.Param param = Fragment.parse(uri).findParam("transform");
583     if (param == null) {
584       return null;
585     }
586     return param.findValue(transformName);
587   }
588 
parseSubParams(String rawSubparams)589   private static List<SubParam> parseSubParams(String rawSubparams) {
590     List<SubParam> subparams = new ArrayList<SubParam>();
591     String[] pairs = rawSubparams.split(",");
592     for (int i = 0; i < pairs.length; i++) {
593       String[] kv = pairs[i].split("=", 2);
594       String key = kv[0];
595       Preconditions.checkArgument(
596           !TextUtils.isEmpty(key), "missing fragment subparam key: %s", rawSubparams);
597       if (kv.length == 2 && !TextUtils.isEmpty(kv[1])) {
598         subparams.add(new SubParam(urlDecode(key), urlDecode(kv[1])));
599       } else {
600         subparams.add(new SubParam(urlDecode(key), null));
601       }
602     }
603     return subparams;
604   }
605 
urlEncode(String str)606   private static final String urlEncode(String str) {
607     try {
608       return URLEncoder.encode(str, UTF_8.displayName());
609     } catch (UnsupportedEncodingException e) {
610       throw new IllegalStateException(); // Not really
611     }
612   }
613 
urlDecode(String str)614   private static final String urlDecode(String str) {
615     try {
616       return URLDecoder.decode(str, UTF_8.displayName());
617     } catch (UnsupportedEncodingException e) {
618       throw new IllegalStateException(); // Not really
619     }
620   }
621 }
622