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

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

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

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

Например

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

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

Метод UriBuilderImpl.queryParam(...) ведет себя так, когда два символа, следующие за "%" , являются шестнадцатеричными. I.e, метод "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. Поэтому он не загружает реализацию RuntimeDelegate.

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

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

Ответ 1

UriBuilder

Это возможно с помощью 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

Или:

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 в параметр ресурса/метод param (см. JSON в Параметрах запроса или Как вводить пользовательские типы Java через JAX-RS Параметр Аннотации).


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

Ответ 2

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

Следующее:

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

Будет выводиться:

  • http://localhost:8080?name=%2520
  • http://localhost:8080?name=%20
  • http://localhost:8080?name=%2520
  • http://localhost:8080?name=%20

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

Кроме того, на основе документации класса 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, т.е. проверка не выполняется выполняется.

Ответ 3

Можно перезаписать поведение по умолчанию в трикотаже вручную при запуске, например. со статическим помощником, который вызывает 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 - это то же самое.)