Browse Source
The new PathContainer represent the path as a series of elements including separators. This naturally represents leading/trailing slashes and empty path segments which in turn makes it easier to match in PathPattern as well as to reconstruct the path.pull/1735/head
9 changed files with 326 additions and 425 deletions
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.server.reactive; |
||||
|
||||
import java.nio.charset.Charset; |
||||
import java.util.List; |
||||
|
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
/** |
||||
* Structured path representation. |
||||
* |
||||
* <p>Typically consumed via {@link ServerHttpRequest#getPath()} but can also |
||||
* be created by parsing a path value via {@link #parse(String, Charset)}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public interface PathContainer { |
||||
|
||||
|
||||
/** |
||||
* The original, raw (encoded) path value including path parameters. |
||||
*/ |
||||
String value(); |
||||
|
||||
/** |
||||
* The list of path elements, either {@link Separator} or {@link Segment}. |
||||
*/ |
||||
List<Element> 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 |
||||
* @param index the start element index (inclusive) |
||||
* @return the sub-path |
||||
*/ |
||||
static PathContainer subPath(PathContainer path, int index) { |
||||
return DefaultPathContainer.subPath(path, index, path.elements().size()); |
||||
} |
||||
|
||||
|
||||
interface Element { |
||||
|
||||
/** |
||||
* Return the original, raw (encoded) value for the path component. |
||||
*/ |
||||
String value(); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A path separator element. |
||||
*/ |
||||
interface Separator extends Element { |
||||
} |
||||
|
||||
/** |
||||
* A path segment element. |
||||
*/ |
||||
interface Segment extends Element { |
||||
|
||||
/** |
||||
* Return the path segment {@link #value()} decoded. |
||||
*/ |
||||
String valueDecoded(); |
||||
|
||||
/** |
||||
* Variant of {@link #valueDecoded()} as a {@code char[]}. |
||||
*/ |
||||
char[] valueDecodedChars(); |
||||
|
||||
/** |
||||
* 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(); |
||||
|
||||
/** |
||||
* Path parameters parsed from the path segment. |
||||
*/ |
||||
MultiValueMap<String, String> parameters(); |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,75 +0,0 @@
@@ -1,75 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.server.reactive; |
||||
|
||||
import java.nio.charset.Charset; |
||||
|
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
/** |
||||
* Represents the content of one path segment. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public interface PathSegment { |
||||
|
||||
/** |
||||
* Return the original, raw (encoded) path segment value not including |
||||
* path parameters. |
||||
*/ |
||||
String value(); |
||||
|
||||
/** |
||||
* Return the path {@link #value()} decoded. |
||||
*/ |
||||
String valueDecoded(); |
||||
|
||||
/** |
||||
* Return the same as {@link #valueDecoded()} but as a {@code char[]}. |
||||
*/ |
||||
char[] valueCharsDecoded(); |
||||
|
||||
/** |
||||
* Whether the path value (encoded or decoded) is empty meaning that it has |
||||
* {@link Character#isWhitespace whitespace} characters or none. |
||||
*/ |
||||
boolean isEmpty(); |
||||
|
||||
/** |
||||
* 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(); |
||||
|
||||
/** |
||||
* Path parameters parsed from the path segment. |
||||
*/ |
||||
MultiValueMap<String, String> parameters(); |
||||
|
||||
|
||||
/** |
||||
* Parse the given path segment value. |
||||
* @param path the value to parse |
||||
* @param encoding the charset to use for the decoded value |
||||
* @return the parsed path segment |
||||
*/ |
||||
static PathSegment parse(String path, Charset encoding) { |
||||
return DefaultPathSegmentContainer.parsePathSegment(path, encoding); |
||||
} |
||||
|
||||
} |
||||
@ -1,80 +0,0 @@
@@ -1,80 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.server.reactive; |
||||
|
||||
import java.nio.charset.Charset; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Container for 0..N path segments. |
||||
* |
||||
* <p>Typically consumed via {@link ServerHttpRequest#getPath()} but can also |
||||
* be created by parsing a path value via {@link #parse(String, Charset)}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
* @see RequestPath |
||||
*/ |
||||
public interface PathSegmentContainer { |
||||
|
||||
/** |
||||
* The original, raw (encoded) path value including path parameters. |
||||
*/ |
||||
String value(); |
||||
|
||||
/** |
||||
* Whether the path (encoded or decoded) is empty meaning that it has |
||||
* {@link Character#isWhitespace whitespace} characters or none. |
||||
*/ |
||||
boolean isEmpty(); |
||||
|
||||
/** |
||||
* Whether the path {@link #value()} starts with "/". |
||||
*/ |
||||
boolean isAbsolute(); |
||||
|
||||
/** |
||||
* The list of path segments contained. |
||||
*/ |
||||
List<PathSegment> pathSegments(); |
||||
|
||||
/** |
||||
* Whether the path {@link #value()} ends with "/". |
||||
*/ |
||||
boolean hasTrailingSlash(); |
||||
|
||||
|
||||
/** |
||||
* Parse the given path value into a {@link PathSegmentContainer}. |
||||
* @param path the value to parse |
||||
* @param encoding the charset to use for decoded path segment values |
||||
* @return the parsed path |
||||
*/ |
||||
static PathSegmentContainer parse(String path, Charset encoding) { |
||||
return DefaultPathSegmentContainer.parsePath(path, encoding); |
||||
} |
||||
|
||||
/** |
||||
* Extract a sub-path starting at the given offset into the path segment list. |
||||
* @param path the path to extract from |
||||
* @param pathSegmentIndex the start index (inclusive) |
||||
* @return the sub-path |
||||
*/ |
||||
static PathSegmentContainer subPath(PathSegmentContainer path, int pathSegmentIndex) { |
||||
return DefaultPathSegmentContainer.subPath(path, pathSegmentIndex, path.pathSegments().size()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.server.reactive; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
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; |
||||
|
||||
/** |
||||
* Unit tests for {@link DefaultPathContainer}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class DefaultPathContainerTests { |
||||
|
||||
@Test |
||||
public void pathSegment() throws Exception { |
||||
// basic
|
||||
testPathSegment("cars", "", "cars", "cars", new LinkedMultiValueMap<>()); |
||||
|
||||
// empty
|
||||
testPathSegment("", "", "", "", new LinkedMultiValueMap<>()); |
||||
|
||||
// spaces
|
||||
testPathSegment("%20%20", "", "%20%20", " ", new LinkedMultiValueMap<>()); |
||||
testPathSegment("%20a%20", "", "%20a%20", " a ", new LinkedMultiValueMap<>()); |
||||
} |
||||
|
||||
@Test |
||||
public void pathSegmentParams() throws Exception { |
||||
// basic
|
||||
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>(); |
||||
params.add("colors", "red"); |
||||
params.add("colors", "blue"); |
||||
params.add("colors", "green"); |
||||
params.add("year", "2012"); |
||||
testPathSegment("cars", ";colors=red,blue,green;year=2012", "cars", "cars", params); |
||||
|
||||
// trailing semicolon
|
||||
params = new LinkedMultiValueMap<>(); |
||||
params.add("p", "1"); |
||||
testPathSegment("path", ";p=1;", "path", "path", params); |
||||
|
||||
// params with spaces
|
||||
params = new LinkedMultiValueMap<>(); |
||||
params.add("param name", "param value"); |
||||
testPathSegment("path", ";param%20name=param%20value;%20", "path", "path", params); |
||||
|
||||
// empty params
|
||||
params = new LinkedMultiValueMap<>(); |
||||
params.add("p", "1"); |
||||
testPathSegment("path", ";;;%20;%20;p=1;%20", "path", "path", params); |
||||
} |
||||
|
||||
private void testPathSegment(String rawValue, String semicolonContent, |
||||
String value, String valueDecoded, MultiValueMap<String, String> params) { |
||||
|
||||
PathContainer container = DefaultPathContainer.parsePath(rawValue + semicolonContent, UTF_8); |
||||
|
||||
if ("".equals(value)) { |
||||
assertEquals(0, container.elements().size()); |
||||
return; |
||||
} |
||||
|
||||
assertEquals(1, container.elements().size()); |
||||
PathContainer.Segment segment = (PathContainer.Segment) container.elements().get(0); |
||||
|
||||
assertEquals("value: '" + rawValue + "'", value, segment.value()); |
||||
assertEquals("valueDecoded: '" + rawValue + "'", valueDecoded, segment.valueDecoded()); |
||||
assertEquals("semicolonContent: '" + rawValue + "'", semicolonContent, segment.semicolonContent()); |
||||
assertEquals("params: '" + rawValue + "'", params, segment.parameters()); |
||||
} |
||||
|
||||
@Test |
||||
public void path() throws Exception { |
||||
// basic
|
||||
testPath("/a/b/c", "/a/b/c", Arrays.asList("/", "a", "/", "b", "/", "c")); |
||||
|
||||
// root path
|
||||
testPath("/", "/", Collections.singletonList("/")); |
||||
|
||||
// empty path
|
||||
testPath("", "", Collections.emptyList()); |
||||
testPath("%20%20", "%20%20", Collections.singletonList("%20%20")); |
||||
|
||||
// trailing slash
|
||||
testPath("/a/b/", "/a/b/", Arrays.asList("/", "a", "/", "b", "/")); |
||||
testPath("/a/b//", "/a/b//", Arrays.asList("/", "a", "/", "b", "/", "/")); |
||||
|
||||
// extra slashes and spaces
|
||||
testPath("/%20", "/%20", Arrays.asList("/", "%20")); |
||||
testPath("//%20/%20", "//%20/%20", Arrays.asList("/", "/", "%20", "/", "%20")); |
||||
} |
||||
|
||||
private void testPath(String input, String value, List<String> expectedElements) { |
||||
|
||||
PathContainer path = PathContainer.parse(input, UTF_8); |
||||
|
||||
assertEquals("value: '" + input + "'", value, path.value()); |
||||
assertEquals("elements: " + input, expectedElements, path.elements().stream() |
||||
.map(PathContainer.Element::value).collect(Collectors.toList())); |
||||
} |
||||
|
||||
@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()); |
||||
|
||||
// root path
|
||||
path = PathContainer.parse("/", UTF_8); |
||||
assertEquals("/", PathContainer.subPath(path, 0).value()); |
||||
|
||||
// trailing slash
|
||||
path = PathContainer.parse("/a/b/", UTF_8); |
||||
assertEquals("/b/", PathContainer.subPath(path, 2).value()); |
||||
} |
||||
|
||||
} |
||||
@ -1,142 +0,0 @@
@@ -1,142 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.server.reactive; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
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; |
||||
|
||||
/** |
||||
* Unit tests for {@link DefaultPathSegmentContainer}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class DefaultPathSegmentContainerTests { |
||||
|
||||
@Test |
||||
public void pathSegment() throws Exception { |
||||
// basic
|
||||
testPathSegment("cars", "", "cars", "cars", false, new LinkedMultiValueMap<>()); |
||||
|
||||
// empty
|
||||
testPathSegment("", "", "", "", true, new LinkedMultiValueMap<>()); |
||||
|
||||
// spaces
|
||||
testPathSegment("%20%20", "", "%20%20", " ", true, new LinkedMultiValueMap<>()); |
||||
testPathSegment("%20a%20", "", "%20a%20", " a ", false, new LinkedMultiValueMap<>()); |
||||
} |
||||
|
||||
@Test |
||||
public void pathSegmentParams() throws Exception { |
||||
// basic
|
||||
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>(); |
||||
params.add("colors", "red"); |
||||
params.add("colors", "blue"); |
||||
params.add("colors", "green"); |
||||
params.add("year", "2012"); |
||||
testPathSegment("cars", ";colors=red,blue,green;year=2012", "cars", "cars", false, params); |
||||
|
||||
// trailing semicolon
|
||||
params = new LinkedMultiValueMap<>(); |
||||
params.add("p", "1"); |
||||
testPathSegment("path", ";p=1;", "path", "path", false, params); |
||||
|
||||
// params with spaces
|
||||
params = new LinkedMultiValueMap<>(); |
||||
params.add("param name", "param value"); |
||||
testPathSegment("path", ";param%20name=param%20value;%20", "path", "path", false, params); |
||||
|
||||
// empty params
|
||||
params = new LinkedMultiValueMap<>(); |
||||
params.add("p", "1"); |
||||
testPathSegment("path", ";;;%20;%20;p=1;%20", "path", "path", false, params); |
||||
} |
||||
|
||||
private void testPathSegment(String pathSegment, String semicolonContent, |
||||
String value, String valueDecoded, boolean empty, MultiValueMap<String, String> params) { |
||||
|
||||
PathSegment segment = PathSegment.parse(pathSegment + semicolonContent, UTF_8); |
||||
|
||||
assertEquals("value: '" + pathSegment + "'", value, segment.value()); |
||||
assertEquals("valueDecoded: '" + pathSegment + "'", valueDecoded, segment.valueDecoded()); |
||||
assertEquals("isEmpty: '" + pathSegment + "'", empty, segment.isEmpty()); |
||||
assertEquals("semicolonContent: '" + pathSegment + "'", semicolonContent, segment.semicolonContent()); |
||||
assertEquals("params: '" + pathSegment + "'", params, segment.parameters()); |
||||
} |
||||
|
||||
@Test |
||||
public void path() throws Exception { |
||||
// basic
|
||||
testPath("/a/b/c", "/a/b/c", false, true, Arrays.asList("a", "b", "c"), false); |
||||
|
||||
// root path
|
||||
testPath("/", "/", false, true, Collections.emptyList(), false); |
||||
|
||||
// empty path
|
||||
testPath("", "", true, false, Collections.emptyList(), false); |
||||
testPath("%20%20", "%20%20", true, false, Collections.singletonList("%20%20"), false); |
||||
|
||||
// trailing slash
|
||||
testPath("/a/b/", "/a/b/", false, true, Arrays.asList("a", "b"), true); |
||||
testPath("/a/b//", "/a/b//", false, true, Arrays.asList("a", "b", ""), true); |
||||
|
||||
// extra slashes and spaces
|
||||
testPath("/%20", "/%20", false, true, Collections.singletonList("%20"), false); |
||||
testPath("//%20/%20", "//%20/%20", false, true, Arrays.asList("", "%20", "%20"), false); |
||||
} |
||||
|
||||
private void testPath(String input, String value, boolean empty, boolean absolute, |
||||
List<String> segments, boolean trailingSlash) { |
||||
|
||||
PathSegmentContainer path = PathSegmentContainer.parse(input, UTF_8); |
||||
|
||||
List<String> segmentValues = path.pathSegments().stream().map(PathSegment::value) |
||||
.collect(Collectors.toList()); |
||||
|
||||
assertEquals("value: '" + input + "'", value, path.value()); |
||||
assertEquals("empty: '" + input + "'", empty, path.isEmpty()); |
||||
assertEquals("isAbsolute: '" + input + "'", absolute, path.isAbsolute()); |
||||
assertEquals("pathSegments: " + input, segments, segmentValues); |
||||
assertEquals("hasTrailingSlash: '" + input + "'", trailingSlash, path.hasTrailingSlash()); |
||||
} |
||||
|
||||
@Test |
||||
public void subPath() throws Exception { |
||||
// basic
|
||||
PathSegmentContainer path = PathSegmentContainer.parse("/a/b/c", UTF_8); |
||||
assertSame(path, PathSegmentContainer.subPath(path, 0)); |
||||
assertEquals("/b/c", PathSegmentContainer.subPath(path, 1).value()); |
||||
assertEquals("/c", PathSegmentContainer.subPath(path, 2).value()); |
||||
|
||||
// root path
|
||||
path = PathSegmentContainer.parse("/", UTF_8); |
||||
assertEquals("/", PathSegmentContainer.subPath(path, 0).value()); |
||||
|
||||
// trailing slash
|
||||
path = PathSegmentContainer.parse("/a/b/", UTF_8); |
||||
assertEquals("/b/", PathSegmentContainer.subPath(path, 1).value()); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue