How to force URIBuilder.path(...) to encode parameters like "%AD"? This method doesn't always encode parameters with percentage, correctly
Asked Answered
Z

3

23

How to force URIBuilder.path(...) to encode parameters like "%AD"?

The methods path, replacePath and segment of URIBuilder do not always encode parameters with percentage, correctly.

When a parameter contains the character "%" followed by two characters that together form an URL-encoded character, the "%" is not encoded as "%25".

For example

URI uri = UriBuilder.fromUri("https://dummy.com").queryParam("param", "%AD");
String test = uri.build().toString();

"test" is "https://dummy.com?param=%AD"
But it should be "https://dummy.com?param=%25AD" (with the character "%" encoded as "%25")

The method UriBuilderImpl.queryParam(...) behaves like this when the two characters following the "%" are hexadecimal. I.e, the method "com.sun.jersey.api.uri.UriComponent.isHexCharacter(char)" returns true for the characters following the "%".

I think the behavior of UriBuilderImpl is correct because I guess it tries to not encode parameters that already are encoded. But in my scenario, I will never try to create URLs with parameters that already encoded.

What should I do?

My Web application uses Jersey and in many places I build URIs using the class UriBuilder or invoke the method getBaseUriBuilder of UriInfo objects.

I can replace "%" with "%25", every time I invoke the methods queryParam, replaceQueryParam or segment. But I am looking for a less cumbersome solution.

How can I make Jersey to return my own implementation of UriBuilder?

I thought of creating a class that extends UriBuilderImpl that overrides these methods and that perform this replacing before invoking super.queryParam(...) or whatever.

Is there any way of making Jersey to return my own UriBuilder instead of UriBuilderImpl, when invoking UriBuilder.fromURL(...), UriInfo.getBaseUriBuilder(...), etc?

Looking at the method RuntimeDelegate, I thought of extending RuntimeDelegateImpl. My implementation would override the method createUriBuilder(...), which would return my own UriBuilder, instead of UriBuilderImpl. Then, I would add the file META-INF/services/javax.ws.rs.ext.RuntimeDelegate and in it, a the full class name of my RuntimeDelegateImpl.

The problem is that the jersey-bundle.jar already contains a META-INF/services/javax.ws.rs.ext.RuntimeDelegate that points to com.sun.jersey.server.impl.provider.RuntimeDelegateImpl, so the container loads that file instead of my javax.ws.rs.ext.RuntimeDelegate. Therefore, it does not load my RuntimeDelegateimplementation.

Is it possible to provide my own implementation of RuntimeDelegate?

Should I take a different approach?

Zuber answered 29/1, 2014 at 13:2 Comment(1)
Sorry for the tiny off topic, please note that RestEasy has the same problem: docs.jboss.org/resteasy/docs/4.2.0.Final/javadocs/org/jboss/… It's called "contextual URI encoding" (i.e., it does not encode % characters that are followed by two hex characters).Gaddi
W
39

UriBuilder

This is possible with help of UriComponent from Jersey or URLEncoder directly from Java:

UriBuilder.fromUri("https://dummy.com")
        .queryParam("param",
                UriComponent.encode("%AD",
                    UriComponent.Type.QUERY_PARAM_SPACE_ENCODED))
        .build();

Which result in:

https://dummy.com/?param=%25AD

Or:

UriBuilder.fromUri("https://dummy.com")
        .queryParam("param", URLEncoder.encode("%AD", "UTF-8"))
        .build()

Will result in:

https://dummy.com/?param=%25AD

For a more complex examples (i.e. encoding JSON in query param) this approach is also possible. Let's assume you have a JSON like {"Entity":{"foo":"foo","bar":"bar"}}. When encoded using UriComponent the result for query param would look like:

https://dummy.com/?param=%7B%22Entity%22:%7B%22foo%22:%22foo%22,%22bar%22:%22bar%22%7D%7D

JSON like this could be even injected via @QueryParam into resource field / method param (see JSON in Query Params or How to Inject Custom Java Types via JAX-RS Parameter Annotations).


Which Jersey version do you use? In the tags you mention Jersey 2 but in the RuntimeDelegate section you're using Jersey 1 stuff.

Willable answered 8/2, 2014 at 20:8 Comment(4)
Sorry. I use Jersey 1. I have edited my post to remove the tag "jersey 2x" and added "Jersey 1x".Zuber
UriBuilder.fromUri("https://dummy.com").queryParam("param", URLEncoder.encode("%AD", "UTF-8")).build() is a solution. However it is not ideal because it implies modifying all the invocations to UriBuilder to add the URLEncoder.encode(...) thing. I will give you the bounty anyway because I only got two solutions and yours is better than the other. (In Jersey 1x, I have to use UriComponent. I guess URLEncoder belongs to Jersy 2x)Zuber
URLEncoder is a Java class available in JDK. UriComponent is from Jersey (1 & 2). I understand that it's not convenient as you'd expect but there are some cases in which we cannot do the encoding automatically.Willable
If you don't know the query parameter value beforehand (which is usually the case), blindly encoding it before passing it to the UriBuilder can cause problems. For example, if your value is "hello world", encoding it will transform it to "hello+world" and then the UriBuilder will transform it to "hello%2Bworld" (double encoding). The problem is that UriBuilder also does the encoding... Ideally UriBuilder should not do anything when the encoding is explicitly done by the caller as suggested.Zagazig
C
6

See if the following examples help. The thread linked below has an extensive discussion on the available functions and their differing outputs.

The following:

  1. UriBuilder.fromUri("http://localhost:8080").queryParam("name", "{value}").build("%20");
  2. UriBuilder.fromUri("http://localhost:8080").queryParam("name", "{value}").buildFromEncoded("%20");
  3. UriBuilder.fromUri("http://localhost:8080").replaceQuery("name={value}).build("%20");
  4. UriBuilder.fromUri("http://localhost:8080").replaceQuery("name={value}).buildFromEncoded("%20");

Will output:

  1. http://localhost:8080?name=%2520
  2. http://localhost:8080?name=%20
  3. http://localhost:8080?name=%2520
  4. http://localhost:8080?name=%20

via http://comments.gmane.org/gmane.comp.java.jsr311.user/71

Also, based on the Class UriBuilder documentation, the following example shows how to obtain what you're after.

URI templates are allowed in most components of a URI but their value is restricted to a particular component. E.g.

UriBuilder.fromPath("{arg1}").build("foo#bar");

would result in encoding of the '#' such that the resulting URI is "foo%23bar". To create a URI "foo#bar" use

UriBuilder.fromPath("{arg1}").fragment("{arg2}").build("foo", "bar")

instead. URI template names and delimiters are never encoded but their values are encoded when a URI is built. Template parameter regular expressions are ignored when building a URI, i.e. no validation is performed.

Charmion answered 6/2, 2014 at 3:32 Comment(1)
The problem with using templates is that I build the URLs in several places and not just one. I.e: I have a method that adds some segments to the URL, then, another one adds more, or adds parameters depending on a condition. Then, another method... Therefore, when I invoke build() I do not know how many parameters the URL has nor the values of these parameters.Zuber
S
4

It is possible to overwrite the default behavior in jersey manually at start up e.g. with a static helper that calls RuntimeDelegate.setInstance(yourRuntimeDelegateImpl).

So if you want to have an UriBuilder that encodes percents even if they look like they are part of an already encoded sequence, this would look like:

[...]
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.ext.RuntimeDelegate;

import com.sun.jersey.api.uri.UriBuilderImpl;
import com.sun.ws.rs.ext.RuntimeDelegateImpl;
// or for jersey2:
// import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
// import org.glassfish.jersey.internal.RuntimeDelegateImpl;

public class SomeBaseClass {

    [...]

    // this is the lengthier custom implementation of UriBuilder
    // replace this with your own according to your needs
    public static class AlwaysPercentEncodingUriBuilder extends UriBuilderImpl {

        @Override
        public UriBuilder queryParam(String name, Object... values) {
            Object[] encValues = new Object[values.length];
            for (int i=0; i<values.length; i++) {
                String value = values[i].toString(); // TODO: better null check here, like in base class
                encValues[i] = percentEncode(value);
            }
            return super.queryParam(name, encValues);
        }

        private String percentEncode(String value) {
            StringBuilder sb = null;
            for (int i=0;  i < value.length(); i++) {
                char c = value.charAt(i);
                // if this condition is is true, the base class will not encode the percent
                if (c == '%' 
                    && i + 2 < value.length()
                    && isHexCharacter(value.charAt(i + 1)) 
                    && isHexCharacter(value.charAt(i + 2))) {
                    if (sb == null) {
                        sb = new StringBuilder(value.substring(0, i));
                    }
                    sb.append("%25");
                } else {
                    if (sb != null) sb.append(c);
                }
            }
            return (sb != null) ? sb.toString() : value;
        }

        // in jersey2 one can call public UriComponent.isHexCharacter
        // but in jersey1 we need to provide this on our own
        private static boolean isHexCharacter(char c) {
            return ('0' <= c && c <= '9')
                || ('A' <=c && c <= 'F')
                || ('a' <=c && c <= 'f');
        }
    }

    // here starts the code to hook up the implementation
    public static class AlwaysPercentEncodingRuntimeDelegateImpl extends RuntimeDelegateImpl {
        @Override
        public UriBuilder createUriBuilder() {
            return new AlwaysPercentEncodingUriBuilder();
        }
    }

    static {
        RuntimeDelegate myDelegate = new AlwaysPercentEncodingRuntimeDelegateImpl();
        RuntimeDelegate.setInstance(myDelegate);
    }

}

Caveat: Of course that way it is not very configurable, and if you do that in some library code that might be reused by others, this might cause some irritation.

For example I had the same problem as the OP when writing a rest client in a Confluence plugin, and ended up with the "manual encode every parameter" solution instead, as the plugins are loaded via OSGi and thus are simply not able to touch the RuntimeDelegateImpl (getting java.lang.ClassNotFoundException: com.sun.ws.rs.ext.RuntimeDelegateImpl at runtime instead).

(And just for the record, in jersey2 this looks very similar; especially the code to hook the custom RuntimeDelegateImpl is the same.)

Skiver answered 23/4, 2015 at 9:49 Comment(1)
Thanks Clemens! I wish I had known the method RuntimeDelegate.setInstance(yourRuntimeDelegateImpl) at the time. What I did at the moment was create my own implementation of UriBuilder and in replace all occurrences of UriBuilder.fromUri(...) with MyUriBuilder.... Of course my solution is error-prone because if a new developer comes in, she might simply use UriBuilder instead of MyUriBuilder.Zuber

© 2022 - 2024 — McMap. All rights reserved.