diff --git a/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java b/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java index ddcb5157f9..832a8da4f6 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java @@ -19,7 +19,6 @@ package org.springframework.mock.web.reactive.function.server; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.Arrays; import java.util.Collections; @@ -92,7 +91,7 @@ public class MockServerRequest implements ServerRequest { this.method = method; this.uri = uri; - this.pathContainer = RequestPath.create(uri, contextPath, StandardCharsets.UTF_8); + this.pathContainer = RequestPath.parse(uri, contextPath); this.headers = headers; this.cookies = cookies; this.body = body; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 99de10d2d1..00e86b9c2a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -61,7 +61,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { */ public AbstractServerHttpRequest(URI uri, String contextPath, HttpHeaders headers) { this.uri = uri; - this.path = new DefaultRequestPath(uri, contextPath, StandardCharsets.UTF_8); + this.path = RequestPath.parse(uri, contextPath); this.headers = HttpHeaders.readOnlyHttpHeaders(headers); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java index bc9b674802..149557108b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java @@ -17,9 +17,11 @@ package org.springframework.http.server.reactive; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.lang.Nullable; @@ -88,54 +90,65 @@ class DefaultPathContainer implements PathContainer { } - static PathContainer parsePath(String path, Charset charset) { + static PathContainer createFromPath(String path, String separator) { + return parsePathInternal(path, separator, DefaultPathSegment::new); + } + + private static PathContainer parsePathInternal(String path, String separator, + Function segmentParser) { + if (path.equals("")) { return EMPTY_PATH; } + Separator separatorElement = separator.equals(SEPARATOR.value()) ? SEPARATOR : () -> separator; List elements = new ArrayList<>(); int begin; - if (path.length() > 0 && path.charAt(0) == '/') { - begin = 1; - elements.add(SEPARATOR); + if (path.length() > 0 && path.startsWith(separator)) { + begin = separator.length(); + elements.add(separatorElement); } else { begin = 0; } while (begin < path.length()) { - int end = path.indexOf('/', begin); + int end = path.indexOf(separator, begin); String segment = (end != -1 ? path.substring(begin, end) : path.substring(begin)); if (!segment.equals("")) { - elements.add(parsePathSegment(segment, charset)); + elements.add(segmentParser.apply(segment)); } if (end == -1) { break; } - elements.add(SEPARATOR); - begin = end + 1; + elements.add(separatorElement); + begin = end + separator.length(); } return new DefaultPathContainer(path, elements); } - private static PathContainer.Segment parsePathSegment(String input, Charset charset) { - int index = input.indexOf(';'); - if (index == -1) { - String inputDecoded = StringUtils.uriDecode(input, charset); - return new DefaultPathSegment(input, inputDecoded, "", EMPTY_MAP); - } - String value = input.substring(0, index); - String valueDecoded = StringUtils.uriDecode(value, charset); - String semicolonContent = input.substring(index); - MultiValueMap parameters = parseParams(semicolonContent, charset); - return new DefaultPathSegment(value, valueDecoded, semicolonContent, parameters); + static PathContainer createFromUrlPath(String path) { + return parsePathInternal(path, "/", segment -> { + Charset charset = StandardCharsets.UTF_8; + int index = segment.indexOf(';'); + if (index == -1) { + String valueToMatch = StringUtils.uriDecode(segment, charset); + return new DefaultUrlPathSegment(segment, valueToMatch, EMPTY_MAP); + } + else { + String valueToMatch = StringUtils.uriDecode(segment.substring(0, index), charset); + String pathParameterContent = segment.substring(index); + MultiValueMap parameters = parsePathParams(pathParameterContent, charset); + return new DefaultUrlPathSegment(segment, valueToMatch, parameters); + } + }); } - private static MultiValueMap parseParams(String input, Charset charset) { + private static MultiValueMap parsePathParams(String input, Charset charset) { MultiValueMap result = new LinkedMultiValueMap<>(); int begin = 1; while (begin < input.length()) { int end = input.indexOf(';', begin); String param = (end != -1 ? input.substring(begin, end) : input.substring(begin)); - parseParamValues(param, charset, result); + parsePathParamValues(param, charset, result); if (end == -1) { break; } @@ -144,7 +157,7 @@ class DefaultPathContainer implements PathContainer { return result; } - private static void parseParamValues(String input, Charset charset, MultiValueMap output) { + private static void parsePathParamValues(String input, Charset charset, MultiValueMap output) { if (StringUtils.hasText(input)) { int index = input.indexOf("="); if (index != -1) { @@ -185,52 +198,32 @@ class DefaultPathContainer implements PathContainer { } - private static class DefaultPathSegment implements PathContainer.Segment { + private static class DefaultPathSegment implements PathSegment { private final String value; - private final String valueDecoded; + private final char[] valueAsChars; - private final char[] valueDecodedChars; - private final String semicolonContent; - - private final MultiValueMap parameters; - - DefaultPathSegment(String value, String valueDecoded, String semicolonContent, - MultiValueMap params) { - - Assert.isTrue(!value.contains("/"), () -> "Invalid path segment value: " + value); + DefaultPathSegment(String value) { this.value = value; - this.valueDecoded = valueDecoded; - this.valueDecodedChars = valueDecoded.toCharArray(); - this.semicolonContent = semicolonContent; - this.parameters = CollectionUtils.unmodifiableMultiValueMap(params); + this.valueAsChars = value.toCharArray(); } + @Override public String value() { return this.value; } @Override - public String valueDecoded() { - return this.valueDecoded; + public String valueToMatch() { + return this.value; } @Override - public char[] valueDecodedChars() { - return this.valueDecodedChars; - } - - @Override - public String semicolonContent() { - return this.semicolonContent; - } - - @Override - public MultiValueMap parameters() { - return this.parameters; + public char[] valueToMatchAsChars() { + return this.valueAsChars; } @Override @@ -241,25 +234,50 @@ class DefaultPathContainer implements PathContainer { if (other == null || getClass() != other.getClass()) { return false; } - - DefaultPathSegment segment = (DefaultPathSegment) other; - return (this.value.equals(segment.value) && - this.semicolonContent.equals(segment.semicolonContent) && - this.parameters.equals(segment.parameters)); + return this.value.equals(((DefaultPathSegment) other).value); } @Override public int hashCode() { - int result = this.value.hashCode(); - result = 31 * result + this.semicolonContent.hashCode(); - result = 31 * result + this.parameters.hashCode(); - return result; + return this.value.hashCode(); } public String toString() { - return "[value='" + this.value + "\', " + - "semicolonContent='" + this.semicolonContent + "\', " + - "parameters=" + this.parameters + "']"; + return "[value='" + this.value + "']"; } + } + + + private static class DefaultUrlPathSegment extends DefaultPathSegment implements UrlPathSegment { + + private final String valueToMatch; + + private final char[] valueToMatchAsChars; + + private final MultiValueMap parameters; + + + DefaultUrlPathSegment(String value, String valueToMatch, MultiValueMap params) { + super(value); + Assert.isTrue(!value.contains("/"), () -> "Invalid path segment value: " + value); + this.valueToMatch = valueToMatch; + this.valueToMatchAsChars = valueToMatch.toCharArray(); + this.parameters = CollectionUtils.unmodifiableMultiValueMap(params); + } + + + @Override + public String valueToMatch() { + return this.valueToMatch; + } + + @Override + public char[] valueToMatchAsChars() { + return this.valueToMatchAsChars; + } + + @Override + public MultiValueMap parameters() { + return this.parameters; } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultRequestPath.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultRequestPath.java index 08d9750496..b2e79eb319 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultRequestPath.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultRequestPath.java @@ -17,8 +17,6 @@ package org.springframework.http.server.reactive; import java.net.URI; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.List; import org.springframework.lang.Nullable; @@ -40,8 +38,8 @@ class DefaultRequestPath implements RequestPath { private final PathContainer pathWithinApplication; - DefaultRequestPath(URI uri, @Nullable String contextPath, Charset charset) { - this.fullPath = PathContainer.parse(uri.getRawPath(), charset); + DefaultRequestPath(URI uri, @Nullable String contextPath) { + this.fullPath = PathContainer.parseUrlPath(uri.getRawPath()); this.contextPath = initContextPath(this.fullPath, contextPath); this.pathWithinApplication = extractPathWithinApplication(this.fullPath, this.contextPath); } @@ -54,7 +52,7 @@ class DefaultRequestPath implements RequestPath { private static PathContainer initContextPath(PathContainer path, @Nullable String contextPath) { if (!StringUtils.hasText(contextPath) || "/".equals(contextPath)) { - return PathContainer.parse("", StandardCharsets.UTF_8); + return PathContainer.parseUrlPath(""); } Assert.isTrue(contextPath.startsWith("/") && !contextPath.endsWith("/") && @@ -66,11 +64,8 @@ class DefaultRequestPath implements RequestPath { for (int i=0; i < path.elements().size(); i++) { PathContainer.Element element = path.elements().get(i); counter += element.value().length(); - if (element instanceof PathContainer.Segment) { - counter += ((Segment) element).semicolonContent().length(); - } if (length == counter) { - return DefaultPathContainer.subPath(path, 0, i + 1); + return path.subPath(0, i + 1); } } @@ -80,13 +75,12 @@ class DefaultRequestPath implements RequestPath { } private static PathContainer extractPathWithinApplication(PathContainer fullPath, PathContainer contextPath) { - return PathContainer.subPath(fullPath, contextPath.elements().size()); + return fullPath.subPath(contextPath.elements().size()); } // PathContainer methods.. - @Override public String value() { return this.fullPath.value(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 43b51c98a0..3c5c24da20 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -107,14 +106,14 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { @Nullable private RequestPath getRequestPathToUse(@Nullable URI uriToUse) { - if (uriToUse == null) { - if (this.contextPath == null) { - return null; - } + if (uriToUse == null && this.contextPath == null) { + return null; + } + else if (uriToUse == null) { return new DefaultRequestPath(this.delegate.getPath(), this.contextPath); } else { - return new DefaultRequestPath(uriToUse, this.contextPath, StandardCharsets.UTF_8); + return RequestPath.parse(uriToUse, this.contextPath); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/PathContainer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/PathContainer.java index c6d00dc4eb..fa38d78a1a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/PathContainer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/PathContainer.java @@ -16,66 +16,94 @@ package org.springframework.http.server.reactive; -import java.nio.charset.Charset; import java.util.List; import org.springframework.util.MultiValueMap; /** - * Structured path representation. + * Structured representation of a path whose elements are parsed into a sequence + * of {@link Separator Separator} and {@link PathSegment PathSegment} elements. * - *

Typically consumed via {@link ServerHttpRequest#getPath()} but can also - * be created by parsing a path value via {@link #parse(String, Charset)}. + *

An instance of this class can be created via {@link #parsePath(String)} or + * {@link #parseUrlPath(String)}. For an HTTP request the path can be + * accessed via {@link ServerHttpRequest#getPath()}. + * + *

For a URL path each {@link UrlPathSegment UrlPathSegment} exposes its + * structure decoded safely without the risk of encoded reserved characters + * altering the path or segment structure and without path parameters for + * path matching purposes. * * @author Rossen Stoyanchev + * @since 5.0 */ public interface PathContainer { - /** - * The original, raw (encoded) path value including path parameters. + * The original path that was parsed. */ String value(); /** - * The list of path elements, either {@link Separator} or {@link Segment}. + * The list of path elements, either {@link Separator} or {@link PathSegment}. */ List elements(); - /** - * Parse the given path value into a {@link PathContainer}. - * @param path the encoded, raw path value to parse - * @param encoding the charset to use for decoded path segment values - * @return the parsed path - */ - static PathContainer parse(String path, Charset encoding) { - return DefaultPathContainer.parsePath(path, encoding); - } - - /** - * Extract a sub-path from the given offset into the path elements list. - * @param path the path to extract from + * Extract a sub-path from the given offset into the elements list. * @param index the start element index (inclusive) * @return the sub-path */ - static PathContainer subPath(PathContainer path, int index) { - return subPath(path, index, path.elements().size()); + default PathContainer subPath(int index) { + return subPath(index, elements().size()); } /** - * Extract a sub-path from the given start offset (inclusive) into the path + * Extract a sub-path from the given start offset (inclusive) into the * element list and to the end offset (exclusive). - * @param path the path to extract from * @param startIndex the start element index (inclusive) * @param endIndex the end element index (exclusive) * @return the sub-path */ - static PathContainer subPath(PathContainer path, int startIndex, int endIndex) { - return DefaultPathContainer.subPath(path, startIndex, endIndex); + default PathContainer subPath(int startIndex, int endIndex) { + return DefaultPathContainer.subPath(this, startIndex, endIndex); } + /** + * Parse the path value into a sequence of {@link Separator Separator} and + * {@link PathSegment PathSegment} elements. + * @param path the path value to parse + * @return the parsed path + */ + static PathContainer parsePath(String path) { + return parsePath(path, "/"); + } + + /** + * Parse the path value into a sequence of {@link Separator Separator} and + * {@link PathSegment PathSegment} elements. + * @param path the path value to parse + * @param separator the value to treat as separator + * @return the parsed path + */ + static PathContainer parsePath(String path, String separator) { + return DefaultPathContainer.createFromPath(path, separator); + } + + /** + * Parse the path value into a sequence of {@link Separator Separator} and + * {@link UrlPathSegment UrlPathSegment} elements. + * @param path the encoded, raw URL path value to parse + * @return the parsed path + */ + static PathContainer parseUrlPath(String path) { + return DefaultPathContainer.createFromUrlPath(path); + } + + + /** + * Common representation of a path element, e.g. separator or segment. + */ interface Element { /** @@ -86,33 +114,36 @@ public interface PathContainer { /** - * A path separator element. + * Path separator element. */ interface Separator extends Element { } /** - * A path segment element. + * Path segment element. */ - interface Segment extends Element { + interface PathSegment extends Element { /** - * Return the path segment {@link #value()} decoded. + * Return the path segment value to use for pattern matching purposes. + * By default this is the same as {@link #value()} but may also differ + * in sub-interfaces (e.g. decoded, sanitized, etc.). */ - String valueDecoded(); + String valueToMatch(); /** - * Variant of {@link #valueDecoded()} as a {@code char[]}. + * The same as {@link #valueToMatch()} but as a {@code char[]}. */ - char[] valueDecodedChars(); + char[] valueToMatchAsChars(); + } - /** - * Return the portion of the path segment after and including the first - * ";" (semicolon) representing path parameters. The actual parsed - * parameters if any can be obtained via {@link #parameters()}. - */ - String semicolonContent(); + + /** + * Specialization of {@link PathSegment} for a URL path. + * The {@link #valueToMatch()} is decoded and without path parameters. + */ + interface UrlPathSegment extends PathSegment { /** * Path parameters parsed from the path segment. diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/RequestPath.java b/spring-web/src/main/java/org/springframework/http/server/reactive/RequestPath.java index e63489e4f3..6c8c295b74 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/RequestPath.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/RequestPath.java @@ -16,7 +16,8 @@ package org.springframework.http.server.reactive; import java.net.URI; -import java.nio.charset.Charset; + +import org.springframework.lang.Nullable; /** * Represents the complete path for a request. @@ -41,11 +42,12 @@ public interface RequestPath extends PathContainer { */ PathContainer pathWithinApplication(); + /** * Create a new {@code RequestPath} with the given parameters. */ - static RequestPath create(URI uri, String contextPath, Charset charset) { - return new DefaultRequestPath(uri, contextPath, charset); + static RequestPath parse(URI uri, @Nullable String contextPath) { + return new DefaultRequestPath(uri, contextPath); } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java index 30a73ec6bb..9f95833afb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java @@ -19,7 +19,7 @@ package org.springframework.web.util.pattern; import java.util.List; import org.springframework.http.server.reactive.PathContainer.Element; -import org.springframework.http.server.reactive.PathContainer.Segment; +import org.springframework.http.server.reactive.PathContainer.UrlPathSegment; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.pattern.PathPattern.MatchingContext; @@ -65,8 +65,8 @@ class CaptureTheRestPathElement extends PathElement { MultiValueMap parametersCollector = null; for (int i = pathIndex; i < matchingContext.pathLength; i++) { Element element = matchingContext.pathElements.get(i); - if (element instanceof Segment) { - MultiValueMap parameters = ((Segment) element).parameters(); + if (element instanceof UrlPathSegment) { + MultiValueMap parameters = ((UrlPathSegment) element).parameters(); if (!parameters.isEmpty()) { if (parametersCollector == null) { parametersCollector = new LinkedMultiValueMap<>(); @@ -85,8 +85,8 @@ class CaptureTheRestPathElement extends PathElement { StringBuilder buf = new StringBuilder(); for (int i = fromSegment, max = pathElements.size(); i < max; i++) { Element element = pathElements.get(i); - if (element instanceof Segment) { - buf.append(((Segment)element).valueDecoded()); + if (element instanceof UrlPathSegment) { + buf.append(((UrlPathSegment)element).valueToMatch()); } else { buf.append(element.value()); diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java index 018fda6dcf..e759ca07e8 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java @@ -19,7 +19,7 @@ package org.springframework.web.util.pattern; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.http.server.reactive.PathContainer.Segment; +import org.springframework.http.server.reactive.PathContainer.UrlPathSegment; import org.springframework.lang.Nullable; /** @@ -120,7 +120,7 @@ class CaptureVariablePathElement extends PathElement { } if (match && matchingContext.extractingVariables) { - matchingContext.set(this.variableName, candidateCapture, ((Segment)matchingContext.pathElements.get(pathIndex-1)).parameters()); + matchingContext.set(this.variableName, candidateCapture, ((UrlPathSegment)matchingContext.pathElements.get(pathIndex-1)).parameters()); } return match; } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java index 0a33534061..5c897c41ae 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java @@ -16,8 +16,9 @@ package org.springframework.web.util.pattern; +import org.springframework.http.server.reactive.PathContainer; import org.springframework.http.server.reactive.PathContainer.Element; -import org.springframework.http.server.reactive.PathContainer.Segment; +import org.springframework.http.server.reactive.PathContainer.PathSegment; import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** @@ -59,16 +60,16 @@ class LiteralPathElement extends PathElement { return false; } Element element = matchingContext.pathElements.get(pathIndex); - if (!(element instanceof Segment)) { + if (!(element instanceof PathContainer.PathSegment)) { return false; } - String value = ((Segment)element).valueDecoded(); + String value = ((PathSegment)element).valueToMatch(); if (value.length() != len) { // Not enough data to match this path element return false; } - char[] data = ((Segment)element).valueDecodedChars(); + char[] data = ((PathContainer.PathSegment)element).valueToMatchAsChars(); if (this.caseSensitive) { for (int i = 0; i < len; i++) { if (data[i] != this.text[i]) { diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java index f044888091..4fa8d4730a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java @@ -16,7 +16,6 @@ package org.springframework.web.util.pattern; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Comparator; import java.util.Map; @@ -61,26 +60,26 @@ public class ParsingPathMatcher implements PathMatcher { @Override public boolean match(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - return pathPattern.matches(PathContainer.parse(path, StandardCharsets.UTF_8)); + return pathPattern.matches(PathContainer.parseUrlPath(path)); } @Override public boolean matchStart(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - return pathPattern.matchStart(PathContainer.parse(path, StandardCharsets.UTF_8)); + return pathPattern.matchStart(PathContainer.parseUrlPath(path)); } @Override public String extractPathWithinPattern(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - PathContainer pathContainer = PathContainer.parse(path, StandardCharsets.UTF_8); + PathContainer pathContainer = PathContainer.parseUrlPath(path); return pathPattern.extractPathWithinPattern(pathContainer).value(); } @Override public Map extractUriTemplateVariables(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - PathContainer pathContainer = PathContainer.parse(path, StandardCharsets.UTF_8); + PathContainer pathContainer = PathContainer.parseUrlPath(path); PathMatchResult results = pathPattern.matchAndExtract(pathContainer); // Collapse PathMatchResults to simple value results // TODO: (path parameters are lost in this translation) diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index 2a86dac8e8..414bb146e5 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -16,7 +16,6 @@ package org.springframework.web.util.pattern; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -24,7 +23,6 @@ import java.util.Map; import org.springframework.http.server.reactive.PathContainer; import org.springframework.http.server.reactive.PathContainer.Element; -import org.springframework.http.server.reactive.PathContainer.Segment; import org.springframework.http.server.reactive.PathContainer.Separator; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -71,7 +69,7 @@ import org.springframework.util.StringUtils; */ public class PathPattern implements Comparable { - private final static PathContainer EMPTY_PATH = PathContainer.parse("", StandardCharsets.UTF_8); + private final static PathContainer EMPTY_PATH = PathContainer.parsePath(""); /** The parser used to construct this pattern */ private final PathPatternParser parser; @@ -205,7 +203,7 @@ public class PathPattern implements Comparable { info = new PathRemainingMatchInfo(EMPTY_PATH, matchingContext.getPathMatchResult()); } else { - info = new PathRemainingMatchInfo(PathContainer.subPath(pathContainer, matchingContext.remainingPathIndex), + info = new PathRemainingMatchInfo(pathContainer.subPath(matchingContext.remainingPathIndex), matchingContext.getPathMatchResult()); } return info; @@ -261,12 +259,11 @@ public class PathPattern implements Comparable { * @return the subset of the path that is matched by pattern or "" if none of it is matched by pattern elements */ public PathContainer extractPathWithinPattern(PathContainer path) { + // TODO: implement extractPathWithinPattern for PathContainer String result = extractPathWithinPattern(path.value()); - return PathContainer.parse(result, StandardCharsets.UTF_8); + return PathContainer.parseUrlPath(result); } - // TODO: implement extractPathWithinPattern natively for PathContainer - private String extractPathWithinPattern(String path) { // assert this.matches(path) PathElement elem = head; @@ -406,7 +403,7 @@ public class PathPattern implements Comparable { // /usr + /user => /usr/user // /{foo} + /bar => /{foo}/bar if (!this.patternString.equals(pattern2string.patternString) && this.capturedVariableCount == 0 && - matches(PathContainer.parse(pattern2string.patternString, StandardCharsets.UTF_8))) { + matches(PathContainer.parseUrlPath(pattern2string.patternString))) { return pattern2string; } @@ -683,8 +680,8 @@ public class PathPattern implements Comparable { */ String pathElementValue(int pathIndex) { Element element = (pathIndex < pathLength) ? pathElements.get(pathIndex) : null; - if (element instanceof Segment) { - return ((Segment)element).valueDecoded(); + if (element instanceof PathContainer.PathSegment) { + return ((PathContainer.PathSegment)element).valueToMatch(); } return ""; } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java index d7743339ff..e916905476 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.http.server.reactive.PathContainer.Segment; +import org.springframework.http.server.reactive.PathContainer.UrlPathSegment; import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** @@ -173,7 +173,7 @@ class RegexPathElement extends PathElement { String value = matcher.group(i); matchingContext.set(name, value, (i == this.variableNames.size())? - ((Segment)matchingContext.pathElements.get(pathIndex)).parameters(): + ((UrlPathSegment)matchingContext.pathElements.get(pathIndex)).parameters(): NO_PARAMETERS); } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java index 95c6f58125..b435f8abaf 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java @@ -17,7 +17,7 @@ package org.springframework.web.util.pattern; import org.springframework.http.server.reactive.PathContainer.Element; -import org.springframework.http.server.reactive.PathContainer.Segment; +import org.springframework.http.server.reactive.PathContainer.PathSegment; import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** @@ -65,16 +65,16 @@ class SingleCharWildcardedPathElement extends PathElement { } Element element = matchingContext.pathElements.get(pathIndex); - if (!(element instanceof Segment)) { + if (!(element instanceof PathSegment)) { return false; } - String value = ((Segment)element).valueDecoded(); + String value = ((PathSegment)element).valueToMatch(); if (value.length() != len) { // Not enough data to match this path element return false; } - char[] data = ((Segment)element).valueDecodedChars(); + char[] data = ((PathSegment)element).valueToMatchAsChars(); if (this.caseSensitive) { for (int i = 0; i < len; i++) { char ch = this.text[i]; diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardPathElement.java index b3bf34b6a7..c25bd4af15 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardPathElement.java @@ -16,8 +16,8 @@ package org.springframework.web.util.pattern; +import org.springframework.http.server.reactive.PathContainer; import org.springframework.http.server.reactive.PathContainer.Element; -import org.springframework.http.server.reactive.PathContainer.Segment; import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** @@ -46,11 +46,11 @@ class WildcardPathElement extends PathElement { // Assert if it exists it is a segment if (pathIndex < matchingContext.pathLength) { Element element = matchingContext.pathElements.get(pathIndex); - if (!(element instanceof Segment)) { + if (!(element instanceof PathContainer.PathSegment)) { // Should not match a separator return false; } - segmentData = ((Segment)element).valueDecoded(); + segmentData = ((PathContainer.PathSegment)element).valueToMatch(); pathIndex++; } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathContainerTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathContainerTests.java index 3905478157..748a45a4b5 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathContainerTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathContainerTests.java @@ -22,10 +22,10 @@ import java.util.stream.Collectors; import org.junit.Test; +import org.springframework.http.server.reactive.PathContainer.UrlPathSegment; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; @@ -38,14 +38,14 @@ public class DefaultPathContainerTests { @Test public void pathSegment() throws Exception { // basic - testPathSegment("cars", "", "cars", "cars", new LinkedMultiValueMap<>()); + testPathSegment("cars", "cars", new LinkedMultiValueMap<>()); // empty - testPathSegment("", "", "", "", new LinkedMultiValueMap<>()); + testPathSegment("", "", new LinkedMultiValueMap<>()); // spaces - testPathSegment("%20%20", "", "%20%20", " ", new LinkedMultiValueMap<>()); - testPathSegment("%20a%20", "", "%20a%20", " a ", new LinkedMultiValueMap<>()); + testPathSegment("%20%20", " ", new LinkedMultiValueMap<>()); + testPathSegment("%20a%20", " a ", new LinkedMultiValueMap<>()); } @Test @@ -56,40 +56,38 @@ public class DefaultPathContainerTests { params.add("colors", "blue"); params.add("colors", "green"); params.add("year", "2012"); - testPathSegment("cars", ";colors=red,blue,green;year=2012", "cars", "cars", params); + testPathSegment("cars;colors=red,blue,green;year=2012", "cars", params); // trailing semicolon params = new LinkedMultiValueMap<>(); params.add("p", "1"); - testPathSegment("path", ";p=1;", "path", "path", params); + testPathSegment("path;p=1;", "path", params); // params with spaces params = new LinkedMultiValueMap<>(); params.add("param name", "param value"); - testPathSegment("path", ";param%20name=param%20value;%20", "path", "path", params); + testPathSegment("path;param%20name=param%20value;%20", "path", params); // empty params params = new LinkedMultiValueMap<>(); params.add("p", "1"); - testPathSegment("path", ";;;%20;%20;p=1;%20", "path", "path", params); + testPathSegment("path;;;%20;%20;p=1;%20", "path", params); } - private void testPathSegment(String rawValue, String semicolonContent, - String value, String valueDecoded, MultiValueMap params) { + private void testPathSegment(String rawValue, String valueToMatch, MultiValueMap params) { - PathContainer container = DefaultPathContainer.parsePath(rawValue + semicolonContent, UTF_8); + PathContainer container = PathContainer.parseUrlPath(rawValue); - if ("".equals(value)) { + if ("".equals(rawValue)) { assertEquals(0, container.elements().size()); return; } assertEquals(1, container.elements().size()); - PathContainer.Segment segment = (PathContainer.Segment) container.elements().get(0); + UrlPathSegment segment = (UrlPathSegment) container.elements().get(0); - assertEquals("value: '" + rawValue + "'", value, segment.value()); - assertEquals("valueDecoded: '" + rawValue + "'", valueDecoded, segment.valueDecoded()); - assertEquals("semicolonContent: '" + rawValue + "'", semicolonContent, segment.semicolonContent()); + assertEquals("value: '" + rawValue + "'", rawValue, segment.value()); + assertEquals("valueToMatch: '" + rawValue + "'", valueToMatch, segment.valueToMatch()); assertEquals("params: '" + rawValue + "'", params, segment.parameters()); } @@ -116,7 +114,7 @@ public class DefaultPathContainerTests { private void testPath(String input, String value, List expectedElements) { - PathContainer path = PathContainer.parse(input, UTF_8); + PathContainer path = PathContainer.parseUrlPath(input); assertEquals("value: '" + input + "'", value, path.value()); assertEquals("elements: " + input, expectedElements, path.elements().stream() @@ -126,18 +124,18 @@ public class DefaultPathContainerTests { @Test public void subPath() throws Exception { // basic - PathContainer path = PathContainer.parse("/a/b/c", UTF_8); - assertSame(path, PathContainer.subPath(path, 0)); - assertEquals("/b/c", PathContainer.subPath(path, 2).value()); - assertEquals("/c", PathContainer.subPath(path, 4).value()); + PathContainer path = PathContainer.parseUrlPath("/a/b/c"); + assertSame(path, path.subPath(0)); + assertEquals("/b/c", path.subPath(2).value()); + assertEquals("/c", path.subPath(4).value()); // root path - path = PathContainer.parse("/", UTF_8); - assertEquals("/", PathContainer.subPath(path, 0).value()); + path = PathContainer.parseUrlPath("/"); + assertEquals("/", path.subPath(0).value()); // trailing slash - path = PathContainer.parse("/a/b/", UTF_8); - assertEquals("/b/", PathContainer.subPath(path, 2).value()); + path = PathContainer.parseUrlPath("/a/b/"); + assertEquals("/b/", path.subPath(2).value()); } } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultRequestPathTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultRequestPathTests.java index 4bd8dcfd80..f1b42ee8be 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultRequestPathTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultRequestPathTests.java @@ -19,7 +19,6 @@ import java.net.URI; import org.junit.Test; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; /** @@ -54,7 +53,7 @@ public class DefaultRequestPathTests { private void testRequestPath(String fullPath, String contextPath, String pathWithinApplication) { URI uri = URI.create("http://localhost:8080" + fullPath); - RequestPath requestPath = new DefaultRequestPath(uri, contextPath, UTF_8); + RequestPath requestPath = RequestPath.parse(uri, contextPath); assertEquals(contextPath.equals("/") ? "" : contextPath, requestPath.contextPath().value()); assertEquals(pathWithinApplication, requestPath.pathWithinApplication().value()); diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java index c425bff60b..3e37945298 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java @@ -16,7 +16,6 @@ package org.springframework.web.util.pattern; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -587,7 +586,7 @@ public class PathPatternMatcherTests { PathPatternParser ppp = new PathPatternParser(); ppp.setMatchOptionalTrailingSlash(false); PathPattern pp = ppp.parse("test"); - assertFalse(pp.matchStart(PathContainer.parse("test/",StandardCharsets.UTF_8))); + assertFalse(pp.matchStart(PathContainer.parsePath("test/"))); checkStartNoMatch("test/*/","test//"); checkStartMatches("test/*","test/abc"); @@ -1317,7 +1316,7 @@ public class PathPatternMatcherTests { if (path == null) { return null; } - return PathContainer.parse(path, StandardCharsets.UTF_8); + return PathContainer.parseUrlPath(path); } private void checkMatches(String uriTemplate, String path) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java index 7a10f0da9a..3ccd3c8e89 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java @@ -19,9 +19,7 @@ package org.springframework.web.reactive.function.server; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.security.Principal; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -80,7 +78,7 @@ public interface ServerRequest { * Return the request path as {@code PathContainer}. */ default PathContainer pathContainer() { - return PathContainer.parse(path(), StandardCharsets.UTF_8); + return PathContainer.parseUrlPath(path()); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java index 536d76901f..883fbad9c0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.resource; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -174,7 +173,7 @@ public class ResourceUrlProvider implements ApplicationListener request.getPath().contextPath().value() + resolvedPath + query); } @@ -215,7 +214,7 @@ public class ResourceUrlProvider implements ApplicationListener { PathContainer path = entry.getKey().extractPathWithinPattern(lookupPath); int endIndex = lookupPath.elements().size() - path.elements().size(); - PathContainer mapping = PathContainer.subPath(lookupPath, 0, endIndex); + PathContainer mapping = lookupPath.subPath(0, endIndex); if (logger.isTraceEnabled()) { logger.trace("Invoking ResourceResolverChain for URL pattern " + "\"" + entry.getKey() + "\""); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java index 6b68f8b6ad..30babe29f3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java @@ -83,7 +83,7 @@ public class ResourceHandlerRegistryTests { public void mapPathToLocation() throws Exception { MockServerWebExchange exchange = MockServerHttpRequest.get("").toExchange(); exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, - PathContainer.parse("/testStylesheet.css", StandardCharsets.UTF_8)); + PathContainer.parsePath("/testStylesheet.css")); ResourceWebHandler handler = getHandler("/resources/**"); handler.handle(exchange).block(Duration.ofSeconds(5)); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java index fa3a67383c..644100fa1f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java @@ -19,7 +19,6 @@ package org.springframework.web.reactive.function.server; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.Arrays; import java.util.Collections; @@ -91,7 +90,7 @@ public class MockServerRequest implements ServerRequest { this.method = method; this.uri = uri; - this.pathContainer = RequestPath.create(uri, contextPath, StandardCharsets.UTF_8); + this.pathContainer = RequestPath.parse(uri, contextPath); this.headers = headers; this.cookies = cookies; this.body = body; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceUrlProviderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceUrlProviderTests.java index 9465b0c5c1..7211abcaa4 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceUrlProviderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceUrlProviderTests.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.resource; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -75,7 +74,7 @@ public class ResourceUrlProviderTests { @Test public void getStaticResourceUrl() { - PathContainer path = PathContainer.parse("/resources/foo.css", StandardCharsets.UTF_8); + PathContainer path = PathContainer.parsePath("/resources/foo.css"); String url = this.urlProvider.getForLookupPath(path).block(Duration.ofSeconds(5)); assertEquals("/resources/foo.css", url); } @@ -105,7 +104,7 @@ public class ResourceUrlProviderTests { resolvers.add(new PathResourceResolver()); this.handler.setResourceResolvers(resolvers); - PathContainer path = PathContainer.parse("/resources/foo.css", StandardCharsets.UTF_8); + PathContainer path = PathContainer.parsePath("/resources/foo.css"); String url = this.urlProvider.getForLookupPath(path).block(Duration.ofSeconds(5)); assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url); } @@ -127,7 +126,7 @@ public class ResourceUrlProviderTests { this.handlerMap.put("/resources/*.css", otherHandler); this.urlProvider.registerHandlers(this.handlerMap); - PathContainer path = PathContainer.parse("/resources/foo.css", StandardCharsets.UTF_8); + PathContainer path = PathContainer.parsePath("/resources/foo.css"); String url = this.urlProvider.getForLookupPath(path).block(Duration.ofSeconds(5)); assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index ca5a064824..9139a4690f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -553,7 +553,7 @@ public class ResourceWebHandlerTests { private void setPathWithinHandlerMapping(ServerWebExchange exchange, String path) { exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, - PathContainer.parse(path, StandardCharsets.UTF_8)); + PathContainer.parsePath(path)); } private long resourceLastModified(String resourceName) throws IOException {