Skip to content

Commit 358a6d6

Browse files
committed
Improve separator support in PathContainer
To make the switching of separators complete, it is also important to know whether the decoding of path segment values and the parsing of path param should be done as those are applied transparently. This commit replaces the recently added separator argument to PathContainer.parsePath with an Options type with two predefined constants. One for HTTP URLs with automatic decoding and parsing of path params, and another for "." separated message routes without decoding except for encoded sequences of the separator itself. See spring-projectsgh-23310
1 parent fbe6970 commit 358a6d6

File tree

10 files changed

+223
-84
lines changed

10 files changed

+223
-84
lines changed

spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.nio.charset.StandardCharsets;
2121
import java.util.ArrayList;
2222
import java.util.Collections;
23+
import java.util.HashMap;
2324
import java.util.List;
25+
import java.util.Map;
2426
import java.util.stream.Collectors;
2527

2628
import org.springframework.lang.Nullable;
@@ -38,11 +40,16 @@
3840
*/
3941
final class DefaultPathContainer implements PathContainer {
4042

41-
private static final MultiValueMap<String, String> EMPTY_MAP = new LinkedMultiValueMap<>();
43+
private static final MultiValueMap<String, String> EMPTY_PARAMS = new LinkedMultiValueMap<>();
4244

4345
private static final PathContainer EMPTY_PATH = new DefaultPathContainer("", Collections.emptyList());
4446

45-
private static final PathContainer.Separator SEPARATOR = () -> "/";
47+
private static final Map<Character, DefaultSeparator> SEPARATORS = new HashMap<>(2);
48+
49+
static {
50+
SEPARATORS.put('/', new DefaultSeparator('/', "%2F"));
51+
SEPARATORS.put('.', new DefaultSeparator('.', "%2E"));
52+
}
4653

4754

4855
private final String path;
@@ -72,10 +79,10 @@ public boolean equals(@Nullable Object other) {
7279
if (this == other) {
7380
return true;
7481
}
75-
if (other == null || getClass() != other.getClass()) {
82+
if (!(other instanceof PathContainer)) {
7683
return false;
7784
}
78-
return this.path.equals(((DefaultPathContainer) other).path);
85+
return value().equals(((PathContainer) other).value());
7986
}
8087

8188
@Override
@@ -89,18 +96,19 @@ public String toString() {
8996
}
9097

9198

92-
static PathContainer createFromUrlPath(String path, String separator) {
99+
static PathContainer createFromUrlPath(String path, Options options) {
93100
if (path.equals("")) {
94101
return EMPTY_PATH;
95102
}
96-
if (separator.length() == 0) {
97-
throw new IllegalArgumentException("separator should not be empty");
103+
char separator = options.separator();
104+
DefaultSeparator separatorElement = SEPARATORS.get(separator);
105+
if (separatorElement == null) {
106+
throw new IllegalArgumentException("Unexpected separator: '" + separator + "'");
98107
}
99-
Separator separatorElement = separator.equals(SEPARATOR.value()) ? SEPARATOR : () -> separator;
100108
List<Element> elements = new ArrayList<>();
101109
int begin;
102-
if (path.length() > 0 && path.startsWith(separator)) {
103-
begin = separator.length();
110+
if (path.length() > 0 && path.charAt(0) == separator) {
111+
begin = 1;
104112
elements.add(separatorElement);
105113
}
106114
else {
@@ -110,23 +118,25 @@ static PathContainer createFromUrlPath(String path, String separator) {
110118
int end = path.indexOf(separator, begin);
111119
String segment = (end != -1 ? path.substring(begin, end) : path.substring(begin));
112120
if (!segment.equals("")) {
113-
elements.add(parsePathSegment(segment));
121+
elements.add(options.shouldDecodeAndParseSegments() ?
122+
decodeAndParsePathSegment(segment) :
123+
new DefaultPathSegment(segment, separatorElement));
114124
}
115125
if (end == -1) {
116126
break;
117127
}
118128
elements.add(separatorElement);
119-
begin = end + separator.length();
129+
begin = end + 1;
120130
}
121131
return new DefaultPathContainer(path, elements);
122132
}
123133

124-
private static PathSegment parsePathSegment(String segment) {
134+
private static PathSegment decodeAndParsePathSegment(String segment) {
125135
Charset charset = StandardCharsets.UTF_8;
126136
int index = segment.indexOf(';');
127137
if (index == -1) {
128138
String valueToMatch = StringUtils.uriDecode(segment, charset);
129-
return new DefaultPathSegment(segment, valueToMatch, EMPTY_MAP);
139+
return new DefaultPathSegment(segment, valueToMatch, EMPTY_PARAMS);
130140
}
131141
else {
132142
String valueToMatch = StringUtils.uriDecode(segment.substring(0, index), charset);
@@ -192,6 +202,30 @@ static PathContainer subPath(PathContainer container, int fromIndex, int toIndex
192202
}
193203

194204

205+
private static class DefaultSeparator implements Separator {
206+
207+
private final String separator;
208+
209+
private final String encodedSequence;
210+
211+
212+
DefaultSeparator(char separator, String encodedSequence) {
213+
this.separator = String.valueOf(separator);
214+
this.encodedSequence = encodedSequence;
215+
}
216+
217+
218+
@Override
219+
public String value() {
220+
return this.separator;
221+
}
222+
223+
public String encodedSequence() {
224+
return this.encodedSequence;
225+
}
226+
}
227+
228+
195229
private static class DefaultPathSegment implements PathSegment {
196230

197231
private final String value;
@@ -202,14 +236,29 @@ private static class DefaultPathSegment implements PathSegment {
202236

203237
private final MultiValueMap<String, String> parameters;
204238

205-
public DefaultPathSegment(String value, String valueToMatch, MultiValueMap<String, String> params) {
206-
Assert.isTrue(!value.contains("/"), () -> "Invalid path segment value: " + value);
239+
240+
/**
241+
* Constructor for decoded and parsed segments.
242+
*/
243+
DefaultPathSegment(String value, String valueToMatch, MultiValueMap<String, String> params) {
207244
this.value = value;
208245
this.valueToMatch = valueToMatch;
209246
this.valueToMatchAsChars = valueToMatch.toCharArray();
210247
this.parameters = CollectionUtils.unmodifiableMultiValueMap(params);
211248
}
212249

250+
/**
251+
* Constructor for segments without decoding and parsing.
252+
*/
253+
DefaultPathSegment(String value, DefaultSeparator separator) {
254+
this.value = value;
255+
this.valueToMatch = value.contains(separator.encodedSequence()) ?
256+
value.replaceAll(separator.encodedSequence(), separator.value()) : value;
257+
this.valueToMatchAsChars = this.valueToMatch.toCharArray();
258+
this.parameters = EMPTY_PARAMS;
259+
}
260+
261+
213262
@Override
214263
public String value() {
215264
return this.value;
@@ -235,10 +284,10 @@ public boolean equals(@Nullable Object other) {
235284
if (this == other) {
236285
return true;
237286
}
238-
if (other == null || getClass() != other.getClass()) {
287+
if (!(other instanceof PathSegment)) {
239288
return false;
240289
}
241-
return this.value.equals(((DefaultPathSegment) other).value);
290+
return value().equals(((PathSegment) other).value());
242291
}
243292

244293
@Override

spring-web/src/main/java/org/springframework/http/server/PathContainer.java

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,19 @@ default PathContainer subPath(int startIndex, int endIndex) {
7272
* @return the parsed path
7373
*/
7474
static PathContainer parsePath(String path) {
75-
return DefaultPathContainer.createFromUrlPath(path, "/");
75+
return DefaultPathContainer.createFromUrlPath(path, Options.HTTP_PATH);
7676
}
7777

7878
/**
7979
* Parse the path value into a sequence of {@link Separator Separator} and
8080
* {@link PathSegment PathSegment} elements.
8181
* @param path the encoded, raw path value to parse
82-
* @param separator the decoded separator for parsing patterns
82+
* @param options to customize parsing
8383
* @return the parsed path
8484
* @since 5.2
8585
*/
86-
static PathContainer parsePath(String path, String separator) {
87-
return DefaultPathContainer.createFromUrlPath(path, separator);
86+
static PathContainer parsePath(String path, Options options) {
87+
return DefaultPathContainer.createFromUrlPath(path, options);
8888
}
8989

9090

@@ -128,4 +128,58 @@ interface PathSegment extends Element {
128128
MultiValueMap<String, String> parameters();
129129
}
130130

131+
132+
/**
133+
* Options to customize parsing based on the type of input path.
134+
* @since 5.2
135+
*/
136+
class Options {
137+
138+
/**
139+
* Options for HTTP URL paths:
140+
* <p>Separator '/' with URL decoding and parsing of path params.
141+
*/
142+
public final static Options HTTP_PATH = Options.create('/', true);
143+
144+
/**
145+
* Options for a message route:
146+
* <p>Separator '.' without URL decoding nor parsing of params. Escape
147+
* sequences for the separator char in segment values are still decoded.
148+
*/
149+
public final static Options MESSAGE_ROUTE = Options.create('.', false);
150+
151+
152+
private final char separator;
153+
154+
private final boolean decodeAndParseSegments;
155+
156+
157+
private Options(char separator, boolean decodeAndParseSegments) {
158+
this.separator = separator;
159+
this.decodeAndParseSegments = decodeAndParseSegments;
160+
}
161+
162+
163+
public char separator() {
164+
return this.separator;
165+
}
166+
167+
public boolean shouldDecodeAndParseSegments() {
168+
return this.decodeAndParseSegments;
169+
}
170+
171+
172+
/**
173+
* Create an {@link Options} instance with the given settings.
174+
* @param separator the separator for parsing the path into segments;
175+
* currently this must be slash or dot.
176+
* @param decodeAndParseSegments whether to URL decode path segment
177+
* values and parse path parameters. If set to false, only escape
178+
* sequences for the separator char are decoded.
179+
*/
180+
public static Options create(char separator, boolean decodeAndParseSegments) {
181+
return new Options(separator, decodeAndParseSegments);
182+
}
183+
}
184+
131185
}

spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,17 @@ public PathPattern parse(String pathPattern) throws PatternParseException {
105105

106106
while (this.pos < this.pathPatternLength) {
107107
char ch = this.pathPatternData[this.pos];
108-
if (ch == this.parser.getSeparator()) {
108+
char separator = this.parser.getPathOptions().separator();
109+
if (ch == separator) {
109110
if (this.pathElementStart != -1) {
110111
pushPathElement(createPathElement());
111112
}
112113
if (peekDoubleWildcard()) {
113-
pushPathElement(new WildcardTheRestPathElement(this.pos, this.parser.getSeparator()));
114+
pushPathElement(new WildcardTheRestPathElement(this.pos, separator));
114115
this.pos += 2;
115116
}
116117
else {
117-
pushPathElement(new SeparatorPathElement(this.pos, this.parser.getSeparator()));
118+
pushPathElement(new SeparatorPathElement(this.pos, separator));
118119
}
119120
}
120121
else {
@@ -221,7 +222,7 @@ else if (ch == '}' && !previousBackslash) {
221222
}
222223
curlyBracketDepth--;
223224
}
224-
if (ch == this.parser.getSeparator() && !previousBackslash) {
225+
if (ch == this.parser.getPathOptions().separator() && !previousBackslash) {
225226
throw new PatternParseException(this.pos, this.pathPatternData,
226227
PatternMessage.MISSING_CLOSE_CAPTURE);
227228
}
@@ -309,20 +310,21 @@ private PathElement createPathElement() {
309310
}
310311

311312
PathElement newPE = null;
313+
char separator = this.parser.getPathOptions().separator();
312314

313315
if (this.variableCaptureCount > 0) {
314316
if (this.variableCaptureCount == 1 && this.pathElementStart == this.variableCaptureStart &&
315317
this.pathPatternData[this.pos - 1] == '}') {
316318
if (this.isCaptureTheRestVariable) {
317319
// It is {*....}
318320
newPE = new CaptureTheRestPathElement(
319-
this.pathElementStart, getPathElementText(), this.parser.getSeparator());
321+
this.pathElementStart, getPathElementText(), separator);
320322
}
321323
else {
322324
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
323325
try {
324326
newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(),
325-
this.parser.isCaseSensitive(), this.parser.getSeparator());
327+
this.parser.isCaseSensitive(), separator);
326328
}
327329
catch (PatternSyntaxException pse) {
328330
throw new PatternParseException(pse,
@@ -340,7 +342,7 @@ private PathElement createPathElement() {
340342
}
341343
RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart,
342344
getPathElementText(), this.parser.isCaseSensitive(),
343-
this.pathPatternData, this.parser.getSeparator());
345+
this.pathPatternData, separator);
344346
for (String variableName : newRegexSection.getVariableNames()) {
345347
recordCapturedVariable(this.pathElementStart, variableName);
346348
}
@@ -350,20 +352,20 @@ private PathElement createPathElement() {
350352
else {
351353
if (this.wildcard) {
352354
if (this.pos - 1 == this.pathElementStart) {
353-
newPE = new WildcardPathElement(this.pathElementStart, this.parser.getSeparator());
355+
newPE = new WildcardPathElement(this.pathElementStart, separator);
354356
}
355357
else {
356358
newPE = new RegexPathElement(this.pathElementStart, getPathElementText(),
357-
this.parser.isCaseSensitive(), this.pathPatternData, this.parser.getSeparator());
359+
this.parser.isCaseSensitive(), this.pathPatternData, separator);
358360
}
359361
}
360362
else if (this.singleCharWildcardCount != 0) {
361363
newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(),
362-
this.singleCharWildcardCount, this.parser.isCaseSensitive(), this.parser.getSeparator());
364+
this.singleCharWildcardCount, this.parser.isCaseSensitive(), separator);
363365
}
364366
else {
365367
newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(),
366-
this.parser.isCaseSensitive(), this.parser.getSeparator());
368+
this.parser.isCaseSensitive(), separator);
367369
}
368370
}
369371

0 commit comments

Comments
 (0)