Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/EncodingTest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/EncodingTest.java (revision 1417) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/EncodingTest.java (working copy) @@ -95,6 +95,66 @@ } @Test + public void testPathParamWithDoublePercent() { + String paramWithDoublePercent = "start%%end"; + System.out.println("*** " + paramWithDoublePercent); + ClientResponse returned = testClient.getPathParam(paramWithDoublePercent); + Assert.assertNotNull(returned); + Assert.assertEquals(HttpURLConnection.HTTP_OK, returned.getStatus()); + Assert.assertEquals(paramWithDoublePercent, returned.getEntity()); + } + + @Test + public void testPathParamWithBraces() { + String paramWithBraces = "start{param}end"; + System.out.println("*** " + paramWithBraces); + ClientResponse returned = testClient.getPathParam(paramWithBraces); + Assert.assertNotNull(returned); + Assert.assertEquals(HttpURLConnection.HTTP_OK, returned.getStatus()); + Assert.assertEquals(paramWithBraces, returned.getEntity()); + } + + @Test + public void testPathParamWithLifePercentDeath() { + String paramWithLifePercentDeath = "life%death"; + System.out.println("*** " + paramWithLifePercentDeath); + ClientResponse returned = testClient.getPathParam(paramWithLifePercentDeath); + Assert.assertNotNull(returned); + Assert.assertEquals(HttpURLConnection.HTTP_OK, returned.getStatus()); + Assert.assertEquals(paramWithLifePercentDeath, returned.getEntity()); + } + + @Test + public void testQueryParamWithDoublePercent() { + String paramWithDoublePercent = "start%%end"; + System.out.println("*** " + paramWithDoublePercent); + ClientResponse returned = testClient.getQueryParam(paramWithDoublePercent); + Assert.assertNotNull(returned); + Assert.assertEquals(HttpURLConnection.HTTP_OK, returned.getStatus()); + Assert.assertEquals(paramWithDoublePercent, returned.getEntity()); + } + + @Test + public void testQueryParamWithBraces() { + String paramWithBraces = "start{param}end"; + System.out.println("*** " + paramWithBraces); + ClientResponse returned = testClient.getQueryParam(paramWithBraces); + Assert.assertNotNull(returned); + Assert.assertEquals(HttpURLConnection.HTTP_OK, returned.getStatus()); + Assert.assertEquals(paramWithBraces, returned.getEntity()); + } + + @Test + public void testQueryParamWithLifePercentDeath() { + String paramWithLifePercentDeath = "life%death"; + System.out.println("*** " + paramWithLifePercentDeath); + ClientResponse returned = testClient.getQueryParam(paramWithLifePercentDeath); + Assert.assertNotNull(returned); + Assert.assertEquals(HttpURLConnection.HTTP_OK, returned.getStatus()); + Assert.assertEquals(paramWithLifePercentDeath, returned.getEntity()); + } + + @Test public void testPercent() { encodingCharacter('\\'); @@ -206,4 +266,17 @@ Assert.assertEquals(expected, encodedQueryParam); } + @Test + public void testEncodeNonCodes() { + Assert.assertEquals("", Encode.encodeNonCodes("")); + Assert.assertEquals("a simple string", Encode.encodeNonCodes("a simple string")); + Assert.assertEquals("%25", Encode.encodeNonCodes("%")); + Assert.assertEquals("%25%25%25%25", Encode.encodeNonCodes("%%%%")); + Assert.assertEquals("%25%25", Encode.encodeNonCodes("%%25")); + Assert.assertEquals("%25a%25", Encode.encodeNonCodes("%a%25")); + Assert.assertEquals("a%25b", Encode.encodeNonCodes("a%b")); + Assert.assertEquals("a%25b", Encode.encodeNonCodes("a%25b")); + Assert.assertEquals("a%25%25%25%25b%25%25%25%25c", Encode.encodeNonCodes("a%%%%b%%25%%c")); + } + } Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/MyTestResource.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/MyTestResource.java (revision 1417) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/MyTestResource.java (working copy) @@ -4,6 +4,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; @Path("/test") public class MyTestResource @@ -15,4 +16,14 @@ { return pathParam; } + + + @GET + @Produces("text/plain") + @Path("/query-param") + public String getQueryParam(@QueryParam("queryParam") String queryParam) + { + return queryParam; + } + } Index: jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/TestClient.java =================================================================== --- jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/TestClient.java (revision 1417) +++ jaxrs/resteasy-jaxrs/src/test/java/org/jboss/resteasy/test/encoding/TestClient.java (working copy) @@ -6,6 +6,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; @Path("/test") public interface TestClient @@ -14,4 +15,10 @@ @Produces("text/plain") @Path("/path-param/{pathParam}") public ClientResponse getPathParam(@PathParam("pathParam") String pathParam); + + @GET + @Produces("text/plain") + @Path("/query-param") + public ClientResponse getQueryParam(@QueryParam("queryParam") String queryParam); + } Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/specimpl/UriBuilderImpl.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/specimpl/UriBuilderImpl.java (revision 1417) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/specimpl/UriBuilderImpl.java (working copy) @@ -647,6 +647,45 @@ return this; } + /** + * Called by ClientRequest.getUri() to add a query parameter for + * {@code @QueryParam} parameters. We do not use UriBuilder.queryParam() + * because + *
    + *
  • queryParam() supports URI template processing and this method must + * always encode braces (for parameter substitution is not possible for + * {@code @QueryParam} parameters). + * + *
  • queryParam() supports "contextual URI encoding" (i.e., it does not + * encode {@code %} characters that are followed by two hex characters). + * The JavaDoc for {@code @QueryParam.value()} explicitly states that + * the value is specified in decoded format and that "any percent + * encoded literals within the value will not be decoded and will + * instead be treated as literal text". This means that it is an + * explicit bug to perform contextual URI encoding of this method's + * name parameter; hence, we must always encode said parameter. This + * method also foregoes contextual URI encoding on this method's value + * parameter because it represents arbitrary data passed to a + * {@code QueryParam} parameter of a client proxy (since the client + * proxy is nothing more than a transport layer, it should not be + * "interpreting" such data; instead, it should faithfully transmit + * this data over the wire). + *
+ * + * @param name the name of the query parameter. + * @param value the value of the query parameter. + * @return Returns this instance to allow call chaining. + */ + public UriBuilder clientQueryParam(String name, Object value) throws IllegalArgumentException + { + if (name == null) throw new IllegalArgumentException("name parameter is null"); + if (value == null) throw new IllegalArgumentException("A passed in value was null"); + if (query == null) query = ""; + else query += "&"; + query += Encode.encodeQueryParamAsIs(name) + "=" + Encode.encodeQueryParamAsIs(value.toString()); + return this; + } + @Override public UriBuilder queryParam(String name, Object... values) throws IllegalArgumentException { Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/Encode.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/Encode.java (revision 1417) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/util/Encode.java (working copy) @@ -251,11 +251,21 @@ { Matcher matcher = nonCodes.matcher(string); StringBuffer buf = new StringBuffer(); - while (matcher.find()) + + // FYI: we do not use the no-arg matcher.find() + // coupled with matcher.appendReplacement() + // because the matched text may contain + // a second % and we must make sure we + // encode it (if necessary). + int idx = 0; + while ( matcher.find(idx) ) { - matcher.appendReplacement(buf, "%25$1"); + int start = matcher.start(); + buf.append(string.substring(idx, start)); + buf.append("%25"); + idx = start + 1; } - matcher.appendTail(buf); + buf.append(string.substring(idx)); return buf.toString(); } @@ -417,7 +427,7 @@ while (matcher.find()) { String replacement = params.get(i++); - // double encode slashes, so that slashes stay where they are + // double encode slashes, so that slashes stay where they are replacement = replacement.replace("\\", "\\\\"); matcher.appendReplacement(newSegment, replacement); } Index: jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/client/ClientRequest.java =================================================================== --- jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/client/ClientRequest.java (revision 1417) +++ jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/client/ClientRequest.java (working copy) @@ -11,6 +11,7 @@ import org.jboss.resteasy.spi.ProviderFactoryDelegate; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.StringConverter; +import org.jboss.resteasy.util.Encode; import org.jboss.resteasy.util.GenericType; import javax.ws.rs.core.Cookie; @@ -766,7 +767,7 @@ { List values = entry.getValue(); for (String value : values) - builder.queryParam(entry.getKey(), value); + builder.clientQueryParam(entry.getKey(), value); } } if (pathParameterList != null && !pathParameterList.isEmpty()) @@ -779,7 +780,10 @@ { List values = entry.getValue(); for (String value : values) - builder.substitutePathParam(entry.getKey(), value, false); + { + value = Encode.encodePathAsIs(value); + builder.substitutePathParam(entry.getKey(), value, true); + } } } if (finalUri == null)