Skip to content

Commit f21e05a

Browse files
committed
Introduce new URL parser
This commit introduces a new URL parser based on algorithm provided in the Living URL standard. This new UrlParser is used by UriComponentsBuilder::fromUriString, replacing the regular expressions. Closes gh-32513
1 parent 8727d72 commit f21e05a

File tree

8 files changed

+2530
-110
lines changed

8 files changed

+2530
-110
lines changed

Diff for: spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java

+6
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,12 @@ public boolean isAllowed(int c) {
657657
public boolean isAllowed(int c) {
658658
return isUnreserved(c);
659659
}
660+
},
661+
C0 {
662+
@Override
663+
public boolean isAllowed(int c) {
664+
return !(c >= 0 && c <= 0x1f) && !(c > '~');
665+
}
660666
};
661667

662668
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.util;
18+
19+
/**
20+
* Thrown when a URL string cannot be parsed.
21+
*
22+
* @author Arjen Poutsma
23+
* @since 6.2
24+
*/
25+
public class InvalidUrlException extends IllegalArgumentException {
26+
27+
private static final long serialVersionUID = 7409308391039105562L;
28+
29+
30+
/**
31+
* Construct a {@code InvalidUrlException} with the specified detail message.
32+
* @param msg the detail message
33+
*/
34+
public InvalidUrlException(String msg) {
35+
super(msg);
36+
}
37+
38+
/**
39+
* Construct a {@code InvalidUrlException} with the specified detail message
40+
* and nested exception.
41+
* @param msg the detail message
42+
* @param cause the nested exception
43+
*/
44+
public InvalidUrlException(String msg, Throwable cause) {
45+
super(msg, cause);
46+
}
47+
}

Diff for: spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java

+35-66
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,10 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
9797
"^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
9898
")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
9999

100-
private static final Pattern HTTP_URL_PATTERN = Pattern.compile(
101-
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
102-
PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
103-
104100
private static final Object[] EMPTY_VALUES = new Object[0];
105101

102+
private static final UrlParser.UrlRecord EMPTY_URL_RECORD = new UrlParser.UrlRecord();
103+
106104

107105
@Nullable
108106
private String scheme;
@@ -214,52 +212,45 @@ public static UriComponentsBuilder fromUri(URI uri) {
214212
* </pre>
215213
* @param uri the URI string to initialize with
216214
* @return the new {@code UriComponentsBuilder}
215+
* @throws InvalidUrlException if {@code uri} cannot be parsed
217216
*/
218-
public static UriComponentsBuilder fromUriString(String uri) {
217+
public static UriComponentsBuilder fromUriString(String uri) throws InvalidUrlException {
219218
Assert.notNull(uri, "URI must not be null");
220-
Matcher matcher = URI_PATTERN.matcher(uri);
221-
if (matcher.matches()) {
222-
UriComponentsBuilder builder = new UriComponentsBuilder();
223-
String scheme = matcher.group(2);
224-
String userInfo = matcher.group(5);
225-
String host = matcher.group(6);
226-
String port = matcher.group(8);
227-
String path = matcher.group(9);
228-
String query = matcher.group(11);
229-
String fragment = matcher.group(13);
230-
boolean opaque = false;
231-
if (StringUtils.hasLength(scheme)) {
232-
String rest = uri.substring(scheme.length());
233-
if (!rest.startsWith(":/")) {
234-
opaque = true;
235-
}
219+
220+
UriComponentsBuilder builder = new UriComponentsBuilder();
221+
if (!uri.isEmpty()) {
222+
UrlParser.UrlRecord urlRecord = UrlParser.parse(uri, EMPTY_URL_RECORD, null, null);
223+
if (!urlRecord.scheme().isEmpty()) {
224+
builder.scheme(urlRecord.scheme());
236225
}
237-
builder.scheme(scheme);
238-
if (opaque) {
239-
String ssp = uri.substring(scheme.length() + 1);
240-
if (StringUtils.hasLength(fragment)) {
241-
ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1));
226+
if (urlRecord.includesCredentials()) {
227+
StringBuilder userInfo = new StringBuilder(urlRecord.username());
228+
if (!urlRecord.password().isEmpty()) {
229+
userInfo.append(':');
230+
userInfo.append(urlRecord.password());
242231
}
243-
builder.schemeSpecificPart(ssp);
232+
builder.userInfo(userInfo.toString());
233+
}
234+
if (urlRecord.host() != null && !(urlRecord.host() instanceof UrlParser.EmptyHost)) {
235+
builder.host(urlRecord.host().toString());
236+
}
237+
if (urlRecord.port() != null) {
238+
builder.port(urlRecord.port());
239+
}
240+
if (urlRecord.path().isOpaque()) {
241+
builder.schemeSpecificPart(urlRecord.path().toString());
244242
}
245243
else {
246-
checkSchemeAndHost(uri, scheme, host);
247-
builder.userInfo(userInfo);
248-
builder.host(host);
249-
if (StringUtils.hasLength(port)) {
250-
builder.port(port);
244+
builder.path(urlRecord.path().toString());
245+
if (StringUtils.hasLength(urlRecord.query())) {
246+
builder.query(urlRecord.query());
251247
}
252-
builder.path(path);
253-
builder.query(query);
254248
}
255-
if (StringUtils.hasText(fragment)) {
256-
builder.fragment(fragment);
249+
if (StringUtils.hasLength(urlRecord.fragment())) {
250+
builder.fragment(urlRecord.fragment());
257251
}
258-
return builder;
259-
}
260-
else {
261-
throw new IllegalArgumentException("[" + uri + "] is not a valid URI");
262252
}
253+
return builder;
263254
}
264255

265256
/**
@@ -275,33 +266,11 @@ public static UriComponentsBuilder fromUriString(String uri) {
275266
* </pre>
276267
* @param httpUrl the source URI
277268
* @return the URI components of the URI
269+
* @deprecated as of 6.2, in favor of {@link #fromUriString(String)}
278270
*/
279-
public static UriComponentsBuilder fromHttpUrl(String httpUrl) {
280-
Assert.notNull(httpUrl, "HTTP URL must not be null");
281-
Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
282-
if (matcher.matches()) {
283-
UriComponentsBuilder builder = new UriComponentsBuilder();
284-
String scheme = matcher.group(1);
285-
builder.scheme(scheme != null ? scheme.toLowerCase() : null);
286-
builder.userInfo(matcher.group(4));
287-
String host = matcher.group(5);
288-
checkSchemeAndHost(httpUrl, scheme, host);
289-
builder.host(host);
290-
String port = matcher.group(7);
291-
if (StringUtils.hasLength(port)) {
292-
builder.port(port);
293-
}
294-
builder.path(matcher.group(8));
295-
builder.query(matcher.group(10));
296-
String fragment = matcher.group(12);
297-
if (StringUtils.hasText(fragment)) {
298-
builder.fragment(fragment);
299-
}
300-
return builder;
301-
}
302-
else {
303-
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
304-
}
271+
@Deprecated(since = "6.2")
272+
public static UriComponentsBuilder fromHttpUrl(String httpUrl) throws InvalidUrlException {
273+
return fromUriString(httpUrl);
305274
}
306275

307276
private static void checkSchemeAndHost(String uri, @Nullable String scheme, @Nullable String host) {

0 commit comments

Comments
 (0)