Как заставить URIBuilder.path() кодировать такие параметры, как %AD? Этот метод не всегда корректно кодирует параметры в процентах

Как заставить URIBuilder.path(...) кодировать такие параметры, как "%AD"?

Методы path, replacePath и segment из URIBuilder не всегда корректно кодируют параметры в процентах.

Если параметр содержит символ "%", за которым следуют два символа, которые вместе образуют символ в кодировке URL, "%" не кодируется как "%25".

Например

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

"тест" - это "https://dummy.com?param=%AD"
Но это должно быть "https://dummy.com?param=%25AD" (с символом " %" закодировано как "%25")

Метод UriBuilderImpl.queryParam(...) ведет себя так, когда два символа, следующие за "%", являются шестнадцатеричными. То есть метод «com.sun.jersey.api.uri.UriComponent.isHexCharacter(char)» возвращает true для символов, следующих за «%».

Я думаю, что поведение UriBuilderImpl правильное, потому что я предполагаю, что он пытается не кодировать параметры, которые уже закодированы. Но в моем сценарии я никогда не буду пытаться создавать URL-адреса с уже закодированными параметрами.

Что мне делать?

В моем веб-приложении используется Джерси, и во многих местах я создаю URI с помощью класса UriBuilder или вызываю метод getBaseUriBuilder из UriInfo объектов.

Я могу заменить "%" на "%25" каждый раз, когда я вызываю методы queryParam, replaceQueryParam или segment. Но я ищу менее громоздкое решение.

Как заставить Джерси возвращать мою собственную реализацию UriBuilder?

Я подумал о создании класса, который расширяет UriBuilderImpl, который переопределяет эти методы и выполняет эту замену перед вызовом super.queryParam(...) или чего-то еще.

Есть ли способ заставить Джерси возвращать мой собственный UriBuilder вместо UriBuilderImpl при вызове UriBuilder.fromURL(...), UriInfo.getBaseUriBuilder(...) и т. д.?

Глядя на метод RuntimeDelegate, я подумал о расширении RuntimeDelegateImpl. Моя реализация переопределит метод createUriBuilder(...), который вернет мой собственный UriBuilder вместо UriBuilderImpl. Затем я бы добавил файл META-INF/services/javax.ws.rs.ext.RuntimeDelegate и в нем полное имя класса моего RuntimeDelegateImpl.

Проблема в том, что jersey-bundle.jar уже содержит META-INF/services/javax.ws.rs.ext.RuntimeDelegate, указывающий на com.sun.jersey.server.impl.provider.RuntimeDelegateImpl, поэтому контейнер загружает этот файл вместо моего javax.ws.rs.ext.RuntimeDelegate. Поэтому он не загружает мой RuntimeDelegateimplementation.

Можно ли предоставить собственную реализацию RuntimeDelegate?

Должен ли я использовать другой подход?


person Montecarlo    schedule 29.01.2014    source источник


Ответы (3)


Урибилдер

Это возможно с помощью UriComponent из Джерси или URLEncoder непосредственно из Java:

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

Что приводит к:

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

Or:

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

Приведет к:

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

Для более сложных примеров (например, кодирование JSON в параметре запроса) этот подход также возможен. Предположим, у вас есть JSON, например {"Entity":{"foo":"foo","bar":"bar"}}. При кодировании с использованием UriComponent результат для параметра запроса будет выглядеть так:

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

Подобный JSON можно даже внедрить через @QueryParam в поле ресурса/параметр метода (см. rs-parameter-annotations/">JSON в параметрах запроса или как внедрять пользовательские типы Java с помощью аннотаций параметров JAX-RS).


Какую версию джерси вы используете? В тегах вы упоминаете Джерси 2, но в разделе RuntimeDelegate вы используете материалы из Джерси 1.

person Michal Gajdos    schedule 08.02.2014
comment
Извини. Я использую Джерси 1. Я отредактировал свой пост, удалив тег Джерси 2x и добавив Джерси 1x. - person Montecarlo; 10.02.2014
comment
UriBuilder.fromUri("https://dummy.com").queryParam("param", URLEncoder.encode("%AD", "UTF-8")).build() это решение. Однако это не идеально, поскольку предполагает изменение всех вызовов на UriBuilder для добавления элемента URLEncoder.encode(...). Я все равно дам вам награду, потому что у меня есть только два решения, и ваше лучше другого. (В Джерси 1x я должен использовать UriComponent. Думаю, URLEncoder принадлежит Джерси 2x) - person Montecarlo; 10.02.2014
comment
URLEncoder — это класс Java, доступный в JDK. UriComponent из Джерси (1 и 2). Я понимаю, что это не удобно, как вы ожидаете, но в некоторых случаях мы не можем выполнить кодирование автоматически. - person Michal Gajdos; 10.02.2014

Посмотрите, помогут ли следующие примеры. В приведенной ниже ветке подробно обсуждаются доступные функции и их различные результаты.

Следующее:

  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");

Выведет:

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

через http://comments.gmane.org/gmane.comp.java.jsr311.user/71

Кроме того, на основе документации Class UriBuilder, в следующем примере показано, как получить то, что вам нужно.

Шаблоны URI разрешены в большинстве компонентов URI, но их значение ограничено конкретным компонентом. Например.

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

приведет к кодированию '#' таким образом, что результирующий URI будет "foo%23bar". Чтобы создать URI "foo#bar", используйте

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

вместо. Имена и разделители шаблонов URI никогда не кодируются, но их значения кодируются при построении URI. Регулярные выражения параметров шаблона игнорируются при построении URI, т. е. проверка не выполняется.

person JSuar    schedule 06.02.2014
comment
Проблема с использованием шаблонов заключается в том, что я создаю URL-адреса в нескольких местах, а не только в одном. То есть: у меня есть метод, который добавляет несколько сегментов к URL-адресу, затем другой добавляет больше или добавляет параметры в зависимости от условия. Затем другой метод... Поэтому, когда я вызываю build(), я не знаю, сколько параметров имеет URL-адрес и значения этих параметров. - person Montecarlo; 10.02.2014

Можно перезаписать поведение по умолчанию в майке вручную при запуске, например. со статическим помощником, который вызывает RuntimeDelegate.setInstance(yourRuntimeDelegateImpl).

Итак, если вы хотите иметь UriBuilder, который кодирует проценты, даже если они выглядят так, как будто они являются частью уже закодированной последовательности, это будет выглядеть так:

[...]
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);
    }

}

Предостережение: Конечно, таким образом это не очень настраивается, и если вы сделаете это в каком-то коде библиотеки, который может быть повторно использован другими, это может вызвать некоторое раздражение.

Например, у меня была та же проблема, что и у OP, при написании остаточного клиента в плагине Confluence, и вместо этого я получил решение «ручное кодирование каждого параметра», поскольку плагины загружаются через OSGi и, таким образом, просто не могут коснуться RuntimeDelegateImpl (вместо этого получая java.lang.ClassNotFoundException: com.sun.ws.rs.ext.RuntimeDelegateImpl во время выполнения).

(И просто для справки, в jersey2 это выглядит очень похоже, особенно код для перехвата пользовательского RuntimeDelegateImpl такой же.)

person Clemens Klein-Robbenhaar    schedule 23.04.2015
comment
Спасибо Клеменс! Жаль, что я не знал метод RuntimeDelegate.setInstance(yourRuntimeDelegateImpl) в то время. На данный момент я создал свою собственную реализацию UriBuilder и заменил все вхождения UriBuilder.fromUri(...) на MyUriBuilder.... Конечно, мое решение подвержено ошибкам, потому что, если придет новый разработчик, он может просто использовать UriBuilder вместо MyUriBuilder. - person Montecarlo; 23.04.2015