@@ -80,6 +80,13 @@ public class GenericUrl extends GenericData {
8080 /** Fragment component or {@code null} for none. */
8181 private String fragment ;
8282
83+ /**
84+ * If true, the URL string originally given is used as is (without encoding, decoding and
85+ * escaping) whenever referenced; otherwise, part of the URL string may be encoded or decoded as
86+ * deemed appropriate or necessary.
87+ */
88+ private boolean verbatim ;
89+
8390 public GenericUrl () {}
8491
8592 /**
@@ -99,24 +106,52 @@ public GenericUrl() {}
99106 * @throws IllegalArgumentException if URL has a syntax error
100107 */
101108 public GenericUrl (String encodedUrl ) {
102- this (parseURL (encodedUrl ));
109+ this (encodedUrl , false );
110+ }
111+
112+ /**
113+ * Constructs from an encoded URL.
114+ *
115+ * <p>Any known query parameters with pre-defined fields as data keys are parsed based on
116+ * their data type. Any unrecognized query parameter are always parsed as a string.
117+ *
118+ * <p>Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
119+ *
120+ * @param encodedUrl encoded URL, including any existing query parameters that should be parsed
121+ * @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
122+ * @throws IllegalArgumentException if URL has a syntax error
123+ */
124+ public GenericUrl (String encodedUrl , boolean verbatim ) {
125+ this (parseURL (encodedUrl ), verbatim );
103126 }
104127
128+
105129 /**
106130 * Constructs from a URI.
107131 *
108132 * @param uri URI
109133 * @since 1.14
110134 */
111135 public GenericUrl (URI uri ) {
136+ this (uri , false );
137+ }
138+
139+ /**
140+ * Constructs from a URI.
141+ *
142+ * @param uri URI
143+ * @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
144+ */
145+ public GenericUrl (URI uri , boolean verbatim ) {
112146 this (
113147 uri .getScheme (),
114148 uri .getHost (),
115149 uri .getPort (),
116150 uri .getRawPath (),
117151 uri .getRawFragment (),
118152 uri .getRawQuery (),
119- uri .getRawUserInfo ());
153+ uri .getRawUserInfo (),
154+ verbatim );
120155 }
121156
122157 /**
@@ -126,14 +161,26 @@ public GenericUrl(URI uri) {
126161 * @since 1.14
127162 */
128163 public GenericUrl (URL url ) {
164+ this (url , false );
165+ }
166+
167+ /**
168+ * Constructs from a URL.
169+ *
170+ * @param url URL
171+ * @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
172+ * @since 1.14
173+ */
174+ public GenericUrl (URL url , boolean verbatim ) {
129175 this (
130176 url .getProtocol (),
131177 url .getHost (),
132178 url .getPort (),
133179 url .getPath (),
134180 url .getRef (),
135181 url .getQuery (),
136- url .getUserInfo ());
182+ url .getUserInfo (),
183+ verbatim );
137184 }
138185
139186 private GenericUrl (
@@ -143,16 +190,26 @@ private GenericUrl(
143190 String path ,
144191 String fragment ,
145192 String query ,
146- String userInfo ) {
193+ String userInfo ,
194+ boolean verbatim ) {
147195 this .scheme = scheme .toLowerCase (Locale .US );
148196 this .host = host ;
149197 this .port = port ;
150- this .pathParts = toPathParts (path );
151- this .fragment = fragment != null ? CharEscapers .decodeUri (fragment ) : null ;
152- if (query != null ) {
153- UrlEncodedParser .parse (query , this );
154- }
155- this .userInfo = userInfo != null ? CharEscapers .decodeUri (userInfo ) : null ;
198+ this .pathParts = toPathParts (path , verbatim );
199+ this .verbatim = verbatim ;
200+ if (verbatim ) {
201+ this .fragment = fragment ;
202+ if (query != null ) {
203+ UrlEncodedParser .parse (query , this , false );
204+ }
205+ this .userInfo = userInfo ;
206+ } else {
207+ this .fragment = fragment != null ? CharEscapers .decodeUri (fragment ) : null ;
208+ if (query != null ) {
209+ UrlEncodedParser .parse (query , this );
210+ }
211+ this .userInfo = userInfo != null ? CharEscapers .decodeUri (userInfo ) : null ;
212+ }
156213 }
157214
158215 @ Override
@@ -333,7 +390,7 @@ public final String buildAuthority() {
333390 buf .append (Preconditions .checkNotNull (scheme ));
334391 buf .append ("://" );
335392 if (userInfo != null ) {
336- buf .append (CharEscapers .escapeUriUserInfo (userInfo )).append ('@' );
393+ buf .append (verbatim ? userInfo : CharEscapers .escapeUriUserInfo (userInfo )).append ('@' );
337394 }
338395 buf .append (Preconditions .checkNotNull (host ));
339396 int port = this .port ;
@@ -357,12 +414,12 @@ public final String buildRelativeUrl() {
357414 if (pathParts != null ) {
358415 appendRawPathFromParts (buf );
359416 }
360- addQueryParams (entrySet (), buf );
417+ addQueryParams (entrySet (), buf , verbatim );
361418
362419 // URL fragment
363420 String fragment = this .fragment ;
364421 if (fragment != null ) {
365- buf .append ('#' ).append (URI_FRAGMENT_ESCAPER .escape (fragment ));
422+ buf .append ('#' ).append (verbatim ? fragment : URI_FRAGMENT_ESCAPER .escape (fragment ));
366423 }
367424 return buf .toString ();
368425 }
@@ -467,7 +524,7 @@ public String getRawPath() {
467524 * @param encodedPath raw encoded path or {@code null} to set {@link #pathParts} to {@code null}
468525 */
469526 public void setRawPath (String encodedPath ) {
470- pathParts = toPathParts (encodedPath );
527+ pathParts = toPathParts (encodedPath , verbatim );
471528 }
472529
473530 /**
@@ -482,7 +539,7 @@ public void setRawPath(String encodedPath) {
482539 */
483540 public void appendRawPath (String encodedPath ) {
484541 if (encodedPath != null && encodedPath .length () != 0 ) {
485- List <String > appendedPathParts = toPathParts (encodedPath );
542+ List <String > appendedPathParts = toPathParts (encodedPath , verbatim );
486543 if (pathParts == null || pathParts .isEmpty ()) {
487544 this .pathParts = appendedPathParts ;
488545 } else {
@@ -492,7 +549,6 @@ public void appendRawPath(String encodedPath) {
492549 }
493550 }
494551 }
495-
496552 /**
497553 * Returns the decoded path parts for the given encoded path.
498554 *
@@ -503,6 +559,20 @@ public void appendRawPath(String encodedPath) {
503559 * or {@code ""} input
504560 */
505561 public static List <String > toPathParts (String encodedPath ) {
562+ return toPathParts (encodedPath , false );
563+ }
564+
565+ /**
566+ * Returns the path parts (decoded if not {@code verbatim}).
567+ *
568+ * @param encodedPath slash-prefixed encoded path, for example {@code
569+ * "/m8/feeds/contacts/default/full"}
570+ * @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
571+ * @return path parts (decoded if not {@code verbatim}), with each part assumed to be preceded by a {@code '/'}, for example
572+ * {@code "", "m8", "feeds", "contacts", "default", "full"}, or {@code null} for {@code null}
573+ * or {@code ""} input
574+ */
575+ public static List <String > toPathParts (String encodedPath , boolean verbatim ) {
506576 if (encodedPath == null || encodedPath .length () == 0 ) {
507577 return null ;
508578 }
@@ -518,7 +588,7 @@ public static List<String> toPathParts(String encodedPath) {
518588 } else {
519589 sub = encodedPath .substring (cur );
520590 }
521- result .add (CharEscapers .decodeUri (sub ));
591+ result .add (verbatim ? sub : CharEscapers .decodeUri (sub ));
522592 cur = slash + 1 ;
523593 }
524594 return result ;
@@ -532,40 +602,40 @@ private void appendRawPathFromParts(StringBuilder buf) {
532602 buf .append ('/' );
533603 }
534604 if (pathPart .length () != 0 ) {
535- buf .append (CharEscapers .escapeUriPath (pathPart ));
605+ buf .append (verbatim ? pathPart : CharEscapers .escapeUriPath (pathPart ));
536606 }
537607 }
538608 }
539609
540610 /** Adds query parameters from the provided entrySet into the buffer. */
541- static void addQueryParams (Set <Entry <String , Object >> entrySet , StringBuilder buf ) {
611+ static void addQueryParams (Set <Entry <String , Object >> entrySet , StringBuilder buf , boolean verbatim ) {
542612 // (similar to UrlEncodedContent)
543613 boolean first = true ;
544614 for (Map .Entry <String , Object > nameValueEntry : entrySet ) {
545615 Object value = nameValueEntry .getValue ();
546616 if (value != null ) {
547- String name = CharEscapers .escapeUriQuery (nameValueEntry .getKey ());
617+ String name = verbatim ? nameValueEntry . getKey () : CharEscapers .escapeUriQuery (nameValueEntry .getKey ());
548618 if (value instanceof Collection <?>) {
549619 Collection <?> collectionValue = (Collection <?>) value ;
550620 for (Object repeatedValue : collectionValue ) {
551- first = appendParam (first , buf , name , repeatedValue );
621+ first = appendParam (first , buf , name , repeatedValue , verbatim );
552622 }
553623 } else {
554- first = appendParam (first , buf , name , value );
624+ first = appendParam (first , buf , name , value , verbatim );
555625 }
556626 }
557627 }
558628 }
559629
560- private static boolean appendParam (boolean first , StringBuilder buf , String name , Object value ) {
630+ private static boolean appendParam (boolean first , StringBuilder buf , String name , Object value , boolean verbatim ) {
561631 if (first ) {
562632 first = false ;
563633 buf .append ('?' );
564634 } else {
565635 buf .append ('&' );
566636 }
567637 buf .append (name );
568- String stringValue = CharEscapers .escapeUriQuery (value .toString ());
638+ String stringValue = verbatim ? value . toString () : CharEscapers .escapeUriQuery (value .toString ());
569639 if (stringValue .length () != 0 ) {
570640 buf .append ('=' ).append (stringValue );
571641 }
0 commit comments