Является ли 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.
Сноски:
- Я не могу утверждать, что приведенный здесь простой пример кода соответствует RFC3986.
- Например, 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/#