From fc3fcf05fd4a84aa06634aebe92b503c7cd4c01e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 27 Jun 2017 17:16:29 -0400 Subject: [PATCH] Refactor PathSegmentContainer -> PathContainer 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. --- ...ntainer.java => DefaultPathContainer.java} | 115 +++++--------- .../server/reactive/DefaultRequestPath.java | 58 +++---- .../http/server/reactive/PathContainer.java | 110 ++++++++++++++ .../http/server/reactive/PathSegment.java | 75 --------- .../server/reactive/PathSegmentContainer.java | 80 ---------- .../http/server/reactive/RequestPath.java | 6 +- .../reactive/DefaultPathContainerTests.java | 143 ++++++++++++++++++ .../DefaultPathSegmentContainerTests.java | 142 ----------------- .../reactive/DefaultRequestPathTests.java | 22 ++- 9 files changed, 326 insertions(+), 425 deletions(-) rename spring-web/src/main/java/org/springframework/http/server/reactive/{DefaultPathSegmentContainer.java => DefaultPathContainer.java} (65%) create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/PathContainer.java delete mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/PathSegment.java delete mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/PathSegmentContainer.java create mode 100644 spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathContainerTests.java delete mode 100644 spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathSegmentContainerTests.java diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathSegmentContainer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java similarity index 65% rename from spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathSegmentContainer.java rename to spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java index 00cf5174b7c..726a3e685f2 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathSegmentContainer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultPathContainer.java @@ -30,41 +30,28 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** - * Default implementations of {@link PathSegmentContainer} and {@link PathSegment}. + * Default implementation of {@link PathContainer}. * * @author Rossen Stoyanchev * @since 5.0 */ -class DefaultPathSegmentContainer implements PathSegmentContainer { +class DefaultPathContainer implements PathContainer { private static final MultiValueMap EMPTY_MAP = new LinkedMultiValueMap<>(0); - private static final PathSegment EMPTY_PATH_SEGMENT = new DefaultPathSegment("", "", "", EMPTY_MAP); + private static final PathContainer EMPTY_PATH = new DefaultPathContainer("", Collections.emptyList()); - static final PathSegmentContainer EMPTY_PATH = - new DefaultPathSegmentContainer("", Collections.emptyList()); - - private static final PathSegmentContainer ROOT_PATH = - new DefaultPathSegmentContainer("/", Collections.emptyList()); + private static final PathContainer.Separator SEPARATOR = () -> "/"; private final String path; - private final boolean empty; - - private final boolean absolute; - - private final List pathSegments; + private final List elements; - private final boolean trailingSlash; - - private DefaultPathSegmentContainer(String path, List segments) { + private DefaultPathContainer(String path, List elements) { this.path = path; - this.absolute = path.startsWith("/"); - this.pathSegments = Collections.unmodifiableList(segments); - this.trailingSlash = path.endsWith("/") && path.length() > 1; - this.empty = !this.absolute && !this.trailingSlash && segments.stream().allMatch(PathSegment::isEmpty); + this.elements = Collections.unmodifiableList(elements); } @@ -74,23 +61,8 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { } @Override - public boolean isEmpty() { - return this.empty; - } - - @Override - public boolean isAbsolute() { - return this.absolute; - } - - @Override - public List pathSegments() { - return this.pathSegments; - } - - @Override - public boolean hasTrailingSlash() { - return this.trailingSlash; + public List elements() { + return this.elements; } @@ -102,7 +74,7 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { if (other == null || getClass() != other.getClass()) { return false; } - return this.path.equals(((DefaultPathSegmentContainer) other).path); + return this.path.equals(((DefaultPathContainer) other).path); } @Override @@ -116,32 +88,35 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { } - static PathSegmentContainer parsePath(String path, Charset charset) { - path = StringUtils.hasText(path) ? path : ""; - if ("".equals(path)) { + static PathContainer parsePath(String path, Charset charset) { + if (path.equals("")) { return EMPTY_PATH; } - if ("/".equals(path)) { - return ROOT_PATH; + List elements = new ArrayList<>(); + int begin; + if (path.length() > 0 && path.charAt(0) == '/') { + begin = 1; + elements.add(SEPARATOR); + } + else { + begin = 0; } - List result = new ArrayList<>(); - int begin = (path.charAt(0) == '/' ? 1 : 0); while (begin < path.length()) { int end = path.indexOf('/', begin); String segment = (end != -1 ? path.substring(begin, end) : path.substring(begin)); - result.add(parsePathSegment(segment, charset)); + if (!segment.equals("")) { + elements.add(parsePathSegment(segment, charset)); + } if (end == -1) { break; } + elements.add(SEPARATOR); begin = end + 1; } - return new DefaultPathSegmentContainer(path, result); + return new DefaultPathContainer(path, elements); } - static PathSegment parsePathSegment(String input, Charset charset) { - if ("".equals(input)) { - return EMPTY_PATH_SEGMENT; - } + private static PathContainer.Segment parsePathSegment(String input, Charset charset) { int index = input.indexOf(';'); if (index == -1) { String inputDecoded = StringUtils.uriDecode(input, charset); @@ -191,9 +166,9 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { } } - static PathSegmentContainer subPath(PathSegmentContainer container, int fromIndex, int toIndex) { - List segments = container.pathSegments(); - if (fromIndex == 0 && toIndex == segments.size()) { + static PathContainer subPath(PathContainer container, int fromIndex, int toIndex) { + List elements = container.elements(); + if (fromIndex == 0 && toIndex == elements.size()) { return container; } if (fromIndex == toIndex) { @@ -201,26 +176,22 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { } Assert.isTrue(fromIndex < toIndex, "fromIndex: " + fromIndex + " should be < toIndex " + toIndex); - Assert.isTrue(fromIndex >= 0 && fromIndex < segments.size(), "Invalid fromIndex: " + fromIndex); - Assert.isTrue(toIndex >= 0 && toIndex <= segments.size(), "Invalid toIndex: " + toIndex); - - List subList = segments.subList(fromIndex, toIndex); - String prefix = fromIndex > 0 || container.isAbsolute() ? "/" : ""; - String suffix = toIndex == segments.size() && container.hasTrailingSlash() ? "/" : ""; - String path = subList.stream().map(PathSegment::value).collect(Collectors.joining(prefix, "/", suffix)); - return new DefaultPathSegmentContainer(path, subList); + Assert.isTrue(fromIndex >= 0 && fromIndex < elements.size(), "Invalid fromIndex: " + fromIndex); + Assert.isTrue(toIndex >= 0 && toIndex <= elements.size(), "Invalid toIndex: " + toIndex); + + List subList = elements.subList(fromIndex, toIndex); + String path = subList.stream().map(Element::value).collect(Collectors.joining("")); + return new DefaultPathContainer(path, subList); } - private static class DefaultPathSegment implements PathSegment { + private static class DefaultPathSegment implements PathContainer.Segment { private final String value; private final String valueDecoded; - private final char[] valueCharsDecoded; - - private final boolean empty; + private final char[] valueDecodedChars; private final String semicolonContent; @@ -232,8 +203,7 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { Assert.isTrue(!value.contains("/"), "Invalid path segment value: " + value); this.value = value; this.valueDecoded = valueDecoded; - this.valueCharsDecoded = valueDecoded.toCharArray(); - this.empty = !StringUtils.hasText(this.valueDecoded); + this.valueDecodedChars = valueDecoded.toCharArray(); this.semicolonContent = semicolonContent; this.parameters = CollectionUtils.unmodifiableMultiValueMap(params); } @@ -249,13 +219,8 @@ class DefaultPathSegmentContainer implements PathSegmentContainer { } @Override - public char[] valueCharsDecoded() { - return this.valueCharsDecoded; - } - - @Override - public boolean isEmpty() { - return this.empty; + public char[] valueDecodedChars() { + return this.valueDecodedChars; } @Override 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 9b8882ed47a..08d97504964 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 @@ -18,6 +18,7 @@ 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; @@ -32,15 +33,15 @@ import org.springframework.util.StringUtils; */ class DefaultRequestPath implements RequestPath { - private final PathSegmentContainer fullPath; + private final PathContainer fullPath; - private final PathSegmentContainer contextPath; + private final PathContainer contextPath; - private final PathSegmentContainer pathWithinApplication; + private final PathContainer pathWithinApplication; DefaultRequestPath(URI uri, @Nullable String contextPath, Charset charset) { - this.fullPath = PathSegmentContainer.parse(uri.getRawPath(), charset); + this.fullPath = PathContainer.parse(uri.getRawPath(), charset); this.contextPath = initContextPath(this.fullPath, contextPath); this.pathWithinApplication = extractPathWithinApplication(this.fullPath, this.contextPath); } @@ -51,9 +52,9 @@ class DefaultRequestPath implements RequestPath { this.pathWithinApplication = extractPathWithinApplication(this.fullPath, this.contextPath); } - private static PathSegmentContainer initContextPath(PathSegmentContainer path, @Nullable String contextPath) { + private static PathContainer initContextPath(PathContainer path, @Nullable String contextPath) { if (!StringUtils.hasText(contextPath) || "/".equals(contextPath)) { - return DefaultPathSegmentContainer.EMPTY_PATH; + return PathContainer.parse("", StandardCharsets.UTF_8); } Assert.isTrue(contextPath.startsWith("/") && !contextPath.endsWith("/") && @@ -62,13 +63,14 @@ class DefaultRequestPath implements RequestPath { int length = contextPath.length(); int counter = 0; - for (int i=0; i < path.pathSegments().size(); i++) { - PathSegment pathSegment = path.pathSegments().get(i); - counter += 1; // for slash separators - counter += pathSegment.value().length(); - counter += pathSegment.semicolonContent().length(); + 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 DefaultPathSegmentContainer.subPath(path, 0, i + 1); + return DefaultPathContainer.subPath(path, 0, i + 1); } } @@ -77,20 +79,13 @@ class DefaultRequestPath implements RequestPath { " given path='" + path.value() + "'"); } - private static PathSegmentContainer extractPathWithinApplication(PathSegmentContainer fullPath, - PathSegmentContainer contextPath) { - - return PathSegmentContainer.subPath(fullPath, contextPath.pathSegments().size()); + private static PathContainer extractPathWithinApplication(PathContainer fullPath, PathContainer contextPath) { + return PathContainer.subPath(fullPath, contextPath.elements().size()); } - // PathSegmentContainer methods.. - + // PathContainer methods.. - @Override - public boolean isEmpty() { - return this.contextPath.isEmpty() && this.pathWithinApplication.isEmpty(); - } @Override public String value() { @@ -98,31 +93,20 @@ class DefaultRequestPath implements RequestPath { } @Override - public boolean isAbsolute() { - return !this.contextPath.isEmpty() && this.contextPath.isAbsolute() || - this.pathWithinApplication.isAbsolute(); - } - - @Override - public List pathSegments() { - return this.fullPath.pathSegments(); - } - - @Override - public boolean hasTrailingSlash() { - return this.pathWithinApplication.hasTrailingSlash(); + public List elements() { + return this.fullPath.elements(); } // RequestPath methods.. @Override - public PathSegmentContainer contextPath() { + public PathContainer contextPath() { return this.contextPath; } @Override - public PathSegmentContainer pathWithinApplication() { + public PathContainer pathWithinApplication() { return this.pathWithinApplication; } 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 new file mode 100644 index 00000000000..ee8492822f1 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/PathContainer.java @@ -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. + * + *

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 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 parameters(); + + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/PathSegment.java b/spring-web/src/main/java/org/springframework/http/server/reactive/PathSegment.java deleted file mode 100644 index 1c3be8446a0..00000000000 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/PathSegment.java +++ /dev/null @@ -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 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); - } - -} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/PathSegmentContainer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/PathSegmentContainer.java deleted file mode 100644 index 6fc677f2889..00000000000 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/PathSegmentContainer.java +++ /dev/null @@ -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. - * - *

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 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()); - } - -} 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 a6e8531425c..7228ca29339 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 @@ -21,7 +21,7 @@ package org.springframework.http.server.reactive; * @author Rossen Stoyanchev * @since 5.0 */ -public interface RequestPath extends PathSegmentContainer { +public interface RequestPath extends PathContainer { /** * Returns the portion of the URL path that represents the application. @@ -31,11 +31,11 @@ public interface RequestPath extends PathSegmentContainer { * when deploying as a WAR to a Servlet container or it may also be assigned * through the use of {@link ContextPathCompositeHandler} or both. */ - PathSegmentContainer contextPath(); + PathContainer contextPath(); /** * The portion of the request path after the context path. */ - PathSegmentContainer pathWithinApplication(); + PathContainer pathWithinApplication(); } 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 new file mode 100644 index 00000000000..3905478157f --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathContainerTests.java @@ -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 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 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 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()); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathSegmentContainerTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathSegmentContainerTests.java deleted file mode 100644 index 836319e3b8a..00000000000 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/DefaultPathSegmentContainerTests.java +++ /dev/null @@ -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 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 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 segments, boolean trailingSlash) { - - PathSegmentContainer path = PathSegmentContainer.parse(input, UTF_8); - - List 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()); - } - -} 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 20d126bd240..4bd8dcfd80c 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 @@ -31,35 +31,31 @@ public class DefaultRequestPathTests { @Test public void requestPath() throws Exception { // basic - testRequestPath("/app/a/b/c", "/app", "/a/b/c", false, true, false); + testRequestPath("/app/a/b/c", "/app", "/a/b/c"); // no context path - testRequestPath("/a/b/c", "", "/a/b/c", false, true, false); + testRequestPath("/a/b/c", "", "/a/b/c"); // context path only - testRequestPath("/a/b", "/a/b", "", false, true, false); + testRequestPath("/a/b", "/a/b", ""); // root path - testRequestPath("/", "", "/", false, true, false); + testRequestPath("/", "", "/"); // empty path - testRequestPath("", "", "", true, false, false); - testRequestPath("", "/", "", true, false, false); + testRequestPath("", "", ""); + testRequestPath("", "/", ""); // trailing slash - testRequestPath("/app/a/", "/app", "/a/", false, true, true); - testRequestPath("/app/a//", "/app", "/a//", false, true, true); + testRequestPath("/app/a/", "/app", "/a/"); + testRequestPath("/app/a//", "/app", "/a//"); } - private void testRequestPath(String fullPath, String contextPath, String pathWithinApplication, - boolean empty, boolean absolute, boolean trailingSlash) { + 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); - assertEquals(empty, requestPath.isEmpty()); - assertEquals(absolute, requestPath.isAbsolute()); - assertEquals(trailingSlash, requestPath.hasTrailingSlash()); assertEquals(contextPath.equals("/") ? "" : contextPath, requestPath.contextPath().value()); assertEquals(pathWithinApplication, requestPath.pathWithinApplication().value()); }