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

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

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


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

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

Мое понимание RFC 3986 раздел 5.2.2 заключается в том, что если путь относительного 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" на "запрос" будет перейдите к разделу http://example.com/something/more/long?query", а не " http://example.com/something/more/?query "- другими словами, в соответствии с RFC, но не с реализацией Java.

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

Ответ 1

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

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

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

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

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

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

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

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

Существуют альтернативы классу 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 StackOverflow22203111 {

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

        StackOverflow22203111 test = new StackOverflow22203111();
        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

Ответ 2

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

в RFC2396 5.2.6a

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

в RFC3986 5.2.3

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