Является ли Java URI.resolve несовместимым с RFC 3986, когда относительный URI содержит пустой путь?

Я считаю, что определение и реализация метода Java URI.resolve несовместимы с разделом 5.2.2 RFC 3986. Я понимаю, что Java API определяет, как работает этот метод, и если бы он был изменен сейчас, он сломал бы существующие приложения, но мой вопрос таков: может ли кто-нибудь подтвердить мое понимание того, что этот метод несовместим с RFC 3986?

Я использую пример из этого вопроса: java.net.URI разрешает только строку запроса, которую я скопирую здесь:


Я пытаюсь создать URI, используя JDK java.net.URI. Я хочу добавить к абсолютному объекту URI, запрос (в строке). В примере:

URI base = new URI("http://example.com/something/more/long");
String queryString = "query=http://local:282/rand&action=aaaa";
URI query = new URI(null, null, null, queryString, null);
URI result = base.resolve(query);

Теория (или то, что я думаю) заключается в том, что решимость должна вернуться:

http://example.com/something/more/long?query=http://local:282/rand&action=aaaa

Но то, что я получил, это:

http://example.com/something/more/?query=http://local:282/rand&action=aaaa

Мое понимание раздела 5.2.2 RFC 3986 состоит в том, что если путь относительного URI пуст, то должен использоваться весь путь базового URI:

        if (R.path == "") then
           T.path = Base.path;
           if defined(R.query) then
              T.query = R.query;
           else
              T.query = Base.query;
           endif;

и только если указан путь, относительный путь должен быть объединен с базовым путем:

        else
           if (R.path starts-with "/") then
              T.path = remove_dot_segments(R.path);
           else
              T.path = merge(Base.path, R.path);
              T.path = remove_dot_segments(T.path);
           endif;
           T.query = R.query;
        endif;

но реализация Java всегда выполняет слияние, даже если путь пуст:

    String cp = (child.path == null) ? "" : child.path;
    if ((cp.length() > 0) && (cp.charAt(0) == '/')) {
      // 5.2 (5): Child path is absolute
      ru.path = child.path;
    } else {
      // 5.2 (6): Resolve relative path
      ru.path = resolvePath(base.path, cp, base.isAbsolute());
    }

Если мое чтение верно, чтобы получить такое поведение из псевдокода RFC, вы могли бы поставить точку в качестве пути в относительном URI перед строкой запроса, что из моего опыта использования относительных URI в качестве ссылок на веб-страницах является тем, что я ожидал бы:

transform(Base="http://example.com/something/more/long", R=".?query")
    => T="http://example.com/something/more/?query"

Но я ожидаю, что на веб-странице ссылка на странице " http://example.com/something/more/long" на "? Query" будет идти на " http://example.com/something/more/long?query", а не" http://example.com/something/more/?query"- другими словами, в соответствии с RFC, но не с реализацией Java.

Является ли мое чтение RFC правильным и метод Java несовместимым с ним, или я что-то упустил?

4 ответа

Решение

Да я согласен что URI.resolve(URI) Этот метод несовместим с RFC 3986. Сам по себе оригинальный вопрос представляет собой фантастическое количество исследований, которые способствуют этому выводу. Во-первых, давайте проясним любую путаницу.

Как объяснил Райдвальд (в удаленном ответе), существует различие между базовыми путями, которые заканчиваются или не заканчиваются /:

  • fizzотносительно/foo/barявляется:/foo/fizz
  • fizzотносительно/foo/bar/является:/foo/bar/fizz

Хотя это правильный ответ, он не является полным, потому что в исходном вопросене задан вопрос о пути (т. Е. Выше указано "fizz"). Вместо этого вопрос касается отдельного компонента запроса относительной ссылки URI. Конструктор класса URI, используемый в примере кода, принимает пять различных аргументов String, и все, кроме queryString аргумент был принят как null, (Обратите внимание, что Java принимает пустую строку в качестве параметра пути, и это логически приводит к "пустому" компоненту пути, потому что " компонент пути никогда не является неопределенным", хотя " может быть пустым (нулевой длины)".) Это будет важно позже,

В предыдущем комментарии Саджан Чандран указал, что java.net.URI Класс задокументирован для реализации RFC 2396 и не является предметом вопроса RFC 3986. Первый был устаревшим последним в 2005 году. То, что в классе URI Javadoc не упоминается более новый RFC, можно интерпретировать как дополнительное доказательство его несовместимости. Давайте навалим еще немного:

  • JDK-6791060 - это открытая проблема, в которой предлагается, чтобы этот класс "был обновлен для RFC 3986". Комментарий там предупреждает, что "RFC3986 не полностью обратно совместим с 2396".

  • Предыдущие попытки были сделаны для обновления частей класса URI, чтобы они были совместимы с RFC 3986, такие как JDK-6348622, но затем были отменены для нарушения обратной совместимости. (Также см. Это обсуждение в списке рассылки JDK.)

  • Хотя логика пути слияния звучит похоже, как отмечено в SubOptimal, псевдокод, указанный в более новом RFC, не соответствует фактической реализации. В псевдокоде, когда относительный путь URI пуст, результирующий целевой путь копируется как есть из базового URI. Логика "слияния" не выполняется в этих условиях. В отличие от этой спецификации, реализация URI в Java обрезает базовый путь после последнего / характер, как отмечено в вопросе.

Существуют альтернативы классу URI, если вы хотите поведение RFC 3986. Реализации Java EE 6 обеспечивают javax.ws.rs.core.UriBuilder, который (в Джерси 1.18), кажется, ведет себя так, как вы ожидали (см. ниже). Он, по крайней мере, заявляет о осведомленности о RFC в том, что касается кодирования различных компонентов URI.

Вне J2EE Spring 3.0 представил UriUtils, специально документированный для "кодирования и декодирования на основе RFC 3986". Spring 3.1 отказался от некоторых из этих функций и представил UriComponentsBuilder, но, к сожалению, он не документирует приверженность каким-либо конкретным RFC.


Тестовая программа, демонстрирующая различное поведение:

import java.net.*;
import java.util.*;
import java.util.function.*;
import javax.ws.rs.core.UriBuilder; // using Jersey 1.18

public class Stackru22203111 {

    private URI withResolveURI(URI base, String targetQuery) {
        URI reference = queryOnlyURI(targetQuery);
        return base.resolve(reference);
    }

    private URI withUriBuilderReplaceQuery(URI base, String targetQuery) {
        UriBuilder builder = UriBuilder.fromUri(base);
        return builder.replaceQuery(targetQuery).build();
    }

    private URI withUriBuilderMergeURI(URI base, String targetQuery) {
        URI reference = queryOnlyURI(targetQuery);
        UriBuilder builder = UriBuilder.fromUri(base);
        return builder.uri(reference).build();
    }

    public static void main(String... args) throws Exception {

        final URI base = new URI("http://example.com/something/more/long");
        final String queryString = "query=http://local:282/rand&action=aaaa";
        final String expected =
            "http://example.com/something/more/long?query=http://local:282/rand&action=aaaa";

        Stackru22203111 test = new Stackru22203111();
        Map<String, BiFunction<URI, String, URI>> strategies = new LinkedHashMap<>();
        strategies.put("URI.resolve(URI)", test::withResolveURI);
        strategies.put("UriBuilder.replaceQuery(String)", test::withUriBuilderReplaceQuery);
        strategies.put("UriBuilder.uri(URI)", test::withUriBuilderMergeURI);

        strategies.forEach((name, method) -> {
            System.out.println(name);
            URI result = method.apply(base, queryString);
            if (expected.equals(result.toString())) {
                System.out.println("   MATCHES: " + result);
            }
            else {
                System.out.println("  EXPECTED: " + expected);
                System.out.println("   but WAS: " + result);
            }
        });
    }

    private URI queryOnlyURI(String queryString)
    {
        try {
            String scheme = null;
            String authority = null;
            String path = null;
            String fragment = null;
            return new URI(scheme, authority, path, queryString, fragment);
        }
        catch (URISyntaxException syntaxError) {
            throw new IllegalStateException("unexpected", syntaxError);
        }
    }
}

Выходы:

URI.resolve(URI)
  EXPECTED: http://example.com/something/more/long?query=http://local:282/rand&action=aaaa
   but WAS: http://example.com/something/more/?query=http://local:282/rand&action=aaaa
UriBuilder.replaceQuery(String)
   MATCHES: http://example.com/something/more/long?query=http://local:282/rand&action=aaaa
UriBuilder.uri(URI)
   MATCHES: http://example.com/something/more/long?query=http://local:282/rand&action=aaaa

Если вы хотите улучшить1 поведение отURI.resolve()и не хочу включать в свою программу еще одну большую зависимость2, то я обнаружил, что следующий код хорошо работает в рамках моих требований:

public URI resolve(URI base, URI relative) {
    if (Strings.isNullOrEmpty(base.getPath()))
        base = new URI(base.getScheme(), base.getAuthority(), "/",
            base.getQuery(), base.getFragment());
    if (Strings.isNullOrEmpty(uri.getPath()))
        uri = new URI(uri.getScheme(), uri.getAuthority(), base.getPath(),
            uri.getQuery(), uri.getFragment());
    return base.resolve(uri);
}

Единственная вещь, не относящаяся к JDK, есть Strings от Guava, для удобства чтения - замените его собственным однострочным методом, если у вас нет Guava.

Сноски:

  1. Я не могу утверждать, что приведенный здесь простой пример кода соответствует RFC3986.
  2. Например, Spring, javax.ws или, как упоминалось в этом ответе, Apache HTTPClient.

Для меня нет расхождений. С поведением Java.

в RFC2396 5.2.6a

Все, кроме последнего сегмента компонента пути базового URI, копируются в буфер. Другими словами, любые символы после последнего (самого правого) символа косой черты, если таковые имеются, исключаются.

в RFC3986 5.2.3

вернуть строку, состоящую из компонента пути ссылки, добавленного ко всем, кроме последнего сегмента пути базового URI (т. е. исключая любые символы после самого правого /"в пути базового URI, или исключая весь путь базового URI, если это так не содержит символов "/").

Решение, предложенное @Guss, является достаточно хорошим решением, но, к сожалению, в нем есть зависимость от Guava и некоторые незначительные ошибки.

Это рефакторинг его решения, удаляющий зависимость Guava и ошибки. Я использую его вместо и помещаю во вспомогательный класс под названием URIUtils моего, вместе с другими методами, которые были бы частью расширенного URI класс, если бы это не было final.

      public static URI resolve(URI base, URI uri) throws URISyntaxException {
  if (base.getPath() == null || base.getPath().isEmpty())
    base = new URI(base.getScheme(), base.getAuthority(), "/", base.getQuery(), base.getFragment());
  if (uri.getPath() == null || uri.getPath().isEmpty())
    uri = new URI(uri.getScheme(), uri.getAuthority(), base.getPath(), uri.getQuery(), uri.getFragment());
  return base.resolve(uri);
}

Легко проверить, работает ли URI.resolve() просто сравнив их результаты на предмет некоторых распространенных ошибок:

      public static void main(String[] args) throws URISyntaxException {
  URI host = new URI("https://www.test.com");

  URI uri = new URI("mypage.html");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();

  uri = new URI("./mypage.html");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();

  uri = new URI("#");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();

  uri = new URI("#second_block");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();
}
      https://www.test.commypage.html
https://www.test.com/mypage.html

https://www.test.commypage.html
https://www.test.com/mypage.html

https://www.test.com#
https://www.test.com/#
Другие вопросы по тегам