Skip to content

Commit 2d17411

Browse files
committed
Add RequestPath to http/server/reactive
Issue: SPR-15648
1 parent e2e0410 commit 2d17411

File tree

5 files changed

+577
-0
lines changed

5 files changed

+577
-0
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http.server.reactive;
17+
18+
import java.net.URI;
19+
import java.nio.charset.Charset;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
24+
import org.springframework.util.Assert;
25+
import org.springframework.util.CollectionUtils;
26+
import org.springframework.util.LinkedMultiValueMap;
27+
import org.springframework.util.MultiValueMap;
28+
import org.springframework.util.StringUtils;
29+
30+
/**
31+
*
32+
* @author Rossen Stoyanchev
33+
* @since 5.0
34+
*/
35+
class DefaultRequestPath implements RequestPath {
36+
37+
private static final MultiValueMap<String, String> EMPTY_MAP = new LinkedMultiValueMap<>(0);
38+
39+
private static final PathSegment EMPTY_PATH_SEGMENT = new DefaultPathSegment("", "", "", EMPTY_MAP);
40+
41+
private static final PathSegmentContainer EMPTY_PATH =
42+
new DefaultPathSegmentContainer("", Collections.emptyList());
43+
44+
private static final PathSegmentContainer ROOT_PATH =
45+
new DefaultPathSegmentContainer("/", Collections.singletonList(EMPTY_PATH_SEGMENT));
46+
47+
48+
private final PathSegmentContainer fullPath;
49+
50+
private final PathSegmentContainer contextPath;
51+
52+
private final PathSegmentContainer pathWithinApplication;
53+
54+
55+
DefaultRequestPath(URI uri, String contextPath, Charset charset) {
56+
this.fullPath = parsePath(uri.getRawPath(), charset);
57+
this.contextPath = initContextPath(this.fullPath, contextPath);
58+
this.pathWithinApplication = initPathWithinApplication(this.fullPath, this.contextPath);
59+
}
60+
61+
62+
private static PathSegmentContainer parsePath(String path, Charset charset) {
63+
path = StringUtils.hasText(path) ? path : "";
64+
if ("".equals(path)) {
65+
return EMPTY_PATH;
66+
}
67+
if ("/".equals(path)) {
68+
return ROOT_PATH;
69+
}
70+
List<PathSegment> result = new ArrayList<>();
71+
int begin = 1;
72+
while (true) {
73+
int end = path.indexOf('/', begin);
74+
String segment = (end != -1 ? path.substring(begin, end) : path.substring(begin));
75+
result.add(parsePathSegment(segment, charset));
76+
if (end == -1) {
77+
break;
78+
}
79+
begin = end + 1;
80+
if (begin == path.length()) {
81+
// trailing slash
82+
result.add(EMPTY_PATH_SEGMENT);
83+
break;
84+
}
85+
}
86+
return new DefaultPathSegmentContainer(path, result);
87+
}
88+
89+
private static PathSegment parsePathSegment(String input, Charset charset) {
90+
if ("".equals(input)) {
91+
return EMPTY_PATH_SEGMENT;
92+
}
93+
int index = input.indexOf(';');
94+
if (index == -1) {
95+
return new DefaultPathSegment(input, StringUtils.uriDecode(input, charset), "", EMPTY_MAP);
96+
}
97+
String value = input.substring(0, index);
98+
String valueDecoded = StringUtils.uriDecode(value, charset);
99+
String semicolonContent = input.substring(index);
100+
MultiValueMap<String, String> parameters = parseParams(semicolonContent, charset);
101+
return new DefaultPathSegment(value, valueDecoded, semicolonContent, parameters);
102+
}
103+
104+
private static MultiValueMap<String, String> parseParams(String input, Charset charset) {
105+
MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
106+
int begin = 1;
107+
while (begin < input.length()) {
108+
int end = input.indexOf(';', begin);
109+
String param = (end != -1 ? input.substring(begin, end) : input.substring(begin));
110+
parseParamValues(param, charset, result);
111+
if (end == -1) {
112+
break;
113+
}
114+
begin = end + 1;
115+
}
116+
return result;
117+
}
118+
119+
private static void parseParamValues(String input, Charset charset, MultiValueMap<String, String> output) {
120+
if (StringUtils.hasText(input)) {
121+
int index = input.indexOf("=");
122+
if (index != -1) {
123+
String name = input.substring(0, index);
124+
String value = input.substring(index + 1);
125+
for (String v : StringUtils.commaDelimitedListToStringArray(value)) {
126+
name = StringUtils.uriDecode(name, charset);
127+
if (StringUtils.hasText(name)) {
128+
output.add(name, StringUtils.uriDecode(v, charset));
129+
}
130+
}
131+
}
132+
else {
133+
String name = StringUtils.uriDecode(input, charset);
134+
if (StringUtils.hasText(name)) {
135+
output.add(input, "");
136+
}
137+
}
138+
}
139+
}
140+
141+
private static PathSegmentContainer initContextPath(PathSegmentContainer path, String contextPath) {
142+
if (!StringUtils.hasText(contextPath) || "/".equals(contextPath)) {
143+
return EMPTY_PATH;
144+
}
145+
146+
Assert.isTrue(contextPath.startsWith("/") && !contextPath.endsWith("/") &&
147+
path.value().startsWith(contextPath), "Invalid contextPath: " + contextPath);
148+
149+
int length = contextPath.length();
150+
int counter = 0;
151+
152+
List<PathSegment> result = new ArrayList<>();
153+
for (PathSegment pathSegment : path.pathSegments()) {
154+
result.add(pathSegment);
155+
counter += 1; // for '/' separators
156+
counter += pathSegment.value().length();
157+
counter += pathSegment.semicolonContent().length();
158+
if (length == counter) {
159+
return new DefaultPathSegmentContainer(contextPath, result);
160+
}
161+
}
162+
163+
// Should not happen..
164+
throw new IllegalStateException("Failed to initialize contextPath='" + contextPath + "'" +
165+
" given path='" + path.value() + "'");
166+
}
167+
168+
private static PathSegmentContainer initPathWithinApplication(PathSegmentContainer path,
169+
PathSegmentContainer contextPath) {
170+
171+
String value = path.value().substring(contextPath.value().length());
172+
List<PathSegment> pathSegments = new ArrayList<>(path.pathSegments());
173+
pathSegments.removeAll(contextPath.pathSegments());
174+
return new DefaultPathSegmentContainer(value, pathSegments);
175+
}
176+
177+
178+
@Override
179+
public String value() {
180+
return this.fullPath.value();
181+
}
182+
183+
@Override
184+
public List<PathSegment> pathSegments() {
185+
return this.fullPath.pathSegments();
186+
}
187+
188+
@Override
189+
public PathSegmentContainer contextPath() {
190+
return this.contextPath;
191+
}
192+
193+
@Override
194+
public PathSegmentContainer pathWithinApplication() {
195+
return this.pathWithinApplication;
196+
}
197+
198+
199+
private static class DefaultPathSegmentContainer implements PathSegmentContainer {
200+
201+
private final String path;
202+
203+
private final List<PathSegment> pathSegments;
204+
205+
206+
DefaultPathSegmentContainer(String path, List<PathSegment> pathSegments) {
207+
this.path = path;
208+
this.pathSegments = Collections.unmodifiableList(pathSegments);
209+
}
210+
211+
212+
@Override
213+
public String value() {
214+
return this.path;
215+
}
216+
217+
@Override
218+
public List<PathSegment> pathSegments() {
219+
return this.pathSegments;
220+
}
221+
222+
223+
@Override
224+
public boolean equals(Object other) {
225+
if (this == other) {
226+
return true;
227+
}
228+
if (other == null || getClass() != other.getClass()) {
229+
return false;
230+
}
231+
return this.path.equals(((DefaultPathSegmentContainer) other).path);
232+
}
233+
234+
@Override
235+
public int hashCode() {
236+
return this.path.hashCode();
237+
}
238+
239+
@Override
240+
public String toString() {
241+
return "[path='" + this.path + "\']";
242+
}
243+
}
244+
245+
246+
private static class DefaultPathSegment implements PathSegment {
247+
248+
private final String value;
249+
250+
private final String valueDecoded;
251+
252+
private final String semicolonContent;
253+
254+
private final MultiValueMap<String, String> parameters;
255+
256+
257+
DefaultPathSegment(String value, String valueDecoded, String semicolonContent,
258+
MultiValueMap<String, String> params) {
259+
260+
this.value = value;
261+
this.valueDecoded = valueDecoded;
262+
this.semicolonContent = semicolonContent;
263+
this.parameters = CollectionUtils.unmodifiableMultiValueMap(params);
264+
}
265+
266+
267+
@Override
268+
public String value() {
269+
return this.value;
270+
}
271+
272+
@Override
273+
public String valueDecoded() {
274+
return this.valueDecoded;
275+
}
276+
277+
@Override
278+
public String semicolonContent() {
279+
return this.semicolonContent;
280+
}
281+
282+
@Override
283+
public MultiValueMap<String, String> parameters() {
284+
return this.parameters;
285+
}
286+
287+
288+
@Override
289+
public boolean equals(Object other) {
290+
if (this == other) {
291+
return true;
292+
}
293+
if (other == null || getClass() != other.getClass()) {
294+
return false;
295+
}
296+
297+
DefaultPathSegment segment = (DefaultPathSegment) other;
298+
return (this.value.equals(segment.value) &&
299+
this.semicolonContent.equals(segment.semicolonContent) &&
300+
this.parameters.equals(segment.parameters));
301+
}
302+
303+
@Override
304+
public int hashCode() {
305+
int result = this.value.hashCode();
306+
result = 31 * result + this.semicolonContent.hashCode();
307+
result = 31 * result + this.parameters.hashCode();
308+
return result;
309+
}
310+
311+
public String toString() {
312+
return "[value='" + this.value + "\', " +
313+
"semicolonContent='" + this.semicolonContent + "\', " +
314+
"parameters=" + this.parameters + "']";
315+
}
316+
}
317+
318+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http.server.reactive;
17+
18+
import org.springframework.util.MultiValueMap;
19+
20+
/**
21+
* Represents the content of one path segment.
22+
*
23+
* @author Rossen Stoyanchev
24+
* @since 5.0
25+
*/
26+
public interface PathSegment {
27+
28+
/**
29+
* Return the original, raw (encoded) path segment value not including
30+
* path parameters.
31+
*/
32+
String value();
33+
34+
/**
35+
* The path {@link #value()} decoded.
36+
*/
37+
String valueDecoded();
38+
39+
/**
40+
* Return the portion of the path segment after and including the first
41+
* ";" (semicolon) representing path parameters. The actual parsed
42+
* parameters if any can be obtained via {@link #parameters()}.
43+
*/
44+
String semicolonContent();
45+
46+
/**
47+
* Path parameters parsed from the path segment.
48+
*/
49+
MultiValueMap<String, String> parameters();
50+
51+
}

0 commit comments

Comments
 (0)