diff --git a/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java b/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java index 6a1e2e23c..d6095c4b4 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java +++ b/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java @@ -29,8 +29,8 @@ /** * Expands URI Templates. * - *
This Class supports Level 1 templates and all Level 4 composite templates as described in: RFC 6570. + *
This class supports URI Template Level 1, partial support for Levels 2 and 3, and Level 4 + * composite templates as described in: RFC 6570. * *
Specifically, for the variables: var := "value" list := ["red", "green", "blue"] keys :=
* [("semi", ";"),("dot", "."),("comma", ",")]
@@ -159,7 +159,7 @@ private String getEncodedValue(String value) {
String encodedValue;
if (reservedExpansion) {
// Reserved expansion allows percent-encoded triplets and characters in the reserved set.
- encodedValue = CharEscapers.escapeUriPathWithoutReserved(value);
+ encodedValue = CharEscapers.escapeUriPathWithoutReservedAndPercentEncoded(value);
} else {
encodedValue = CharEscapers.escapeUriConformant(value);
}
diff --git a/google-http-client/src/main/java/com/google/api/client/util/escape/CharEscapers.java b/google-http-client/src/main/java/com/google/api/client/util/escape/CharEscapers.java
index 4350f2711..d434403dc 100644
--- a/google-http-client/src/main/java/com/google/api/client/util/escape/CharEscapers.java
+++ b/google-http-client/src/main/java/com/google/api/client/util/escape/CharEscapers.java
@@ -44,6 +44,9 @@ public final class CharEscapers {
private static final Escaper URI_QUERY_STRING_ESCAPER =
new PercentEscaper(PercentEscaper.SAFEQUERYSTRINGCHARS_URLENCODER);
+ private static final Escaper URI_RESERVED_AND_PERCENT_ENCODED_ESCAPER =
+ new PercentEncodedEscaper(URI_RESERVED_ESCAPER);
+
/**
* Escapes the string value so it can be safely included in application/x-www-form-urlencoded
* data. This is not appropriate for generic URI escaping. In particular it encodes the space
@@ -184,6 +187,15 @@ public static String escapeUriPathWithoutReserved(String value) {
return URI_RESERVED_ESCAPER.escape(value);
}
+ /**
+ * Escapes a URI path but retains all reserved and percent-encoded characters. That is the same as
+ * {@link #escapeUriPathWithoutReserved(String)} except that it also escapes percent encoded
+ * parts.
+ */
+ public static String escapeUriPathWithoutReservedAndPercentEncoded(String value) {
+ return URI_RESERVED_AND_PERCENT_ENCODED_ESCAPER.escape(value);
+ }
+
/**
* Escapes the string value so it can be safely included in URI user info part. For details on
* escaping URIs, see RFC 3986 - section
diff --git a/google-http-client/src/main/java/com/google/api/client/util/escape/PercentEncodedEscaper.java b/google-http-client/src/main/java/com/google/api/client/util/escape/PercentEncodedEscaper.java
new file mode 100644
index 000000000..6de6a0c1d
--- /dev/null
+++ b/google-http-client/src/main/java/com/google/api/client/util/escape/PercentEncodedEscaper.java
@@ -0,0 +1,57 @@
+package com.google.api.client.util.escape;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link Escaper} implementation that preserves percent-encoded sequences in the input string.
+ *
+ * This escaper applies the provided {@link Escaper} to all parts of the input string except for
+ * valid percent-encoded sequences (e.g., %20
), which are left unchanged.
+ */
+final class PercentEncodedEscaper extends Escaper {
+
+ /** Pattern to match valid percent-encoded sequences (e.g., %20). */
+ static final Pattern PCT_ENCODE_PATTERN = Pattern.compile("%[0-9A-Fa-f]{2}");
+
+ private final Escaper escaper;
+
+ public PercentEncodedEscaper(Escaper escaper) {
+ if (escaper == null) {
+ throw new NullPointerException("Escaper cannot be null");
+ }
+ this.escaper = escaper;
+ }
+
+ /**
+ * Escapes the input string using the provided {@link Escaper}, preserving valid percent-encoded
+ * sequences.
+ *
+ * @param string the input string to escape
+ * @return the escaped string with percent-encoded sequences left unchanged
+ */
+ @Override
+ public String escape(String string) {
+ if (string == null || string.isEmpty()) {
+ return string;
+ }
+
+ Matcher matcher = PCT_ENCODE_PATTERN.matcher(string);
+ StringBuilder sb = new StringBuilder();
+
+ int lastEnd = 0;
+ while (matcher.find()) {
+ sb.append(escaper.escape(string.substring(lastEnd, matcher.start())));
+
+ sb.append(string.substring(matcher.start(), matcher.end()));
+
+ lastEnd = matcher.end();
+ }
+
+ if (lastEnd < string.length()) {
+ sb.append(escaper.escape(string.substring(lastEnd)));
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/google-http-client/src/test/java/com/google/api/client/http/UriTemplateTest.java b/google-http-client/src/test/java/com/google/api/client/http/UriTemplateTest.java
index 73492d3c4..9fc90ba85 100644
--- a/google-http-client/src/test/java/com/google/api/client/http/UriTemplateTest.java
+++ b/google-http-client/src/test/java/com/google/api/client/http/UriTemplateTest.java
@@ -380,4 +380,57 @@ public void testExpandTemplates_reservedExpansion_mustNotEscapeUnreservedCharSet
unReservedSet,
UriTemplate.expand("{+var}", requestMap, false));
}
+
+ @Test
+ // These tests are from the uri-template test suite
+ // https://github.com/uri-templates/uritemplate-test/blob/master/extended-tests.json
+ public void testExpandTemplates_reservedExpansion_alreadyEncodedInput() {
+ Map