Skip to content

Commit a80d0e8

Browse files
committed
Merge pull request #795 from poutsma/SPR-12975
* SPR-12975: Remove duplicate separators when combining paths
2 parents 5648fbf + 76beb36 commit a80d0e8

File tree

4 files changed

+116
-57
lines changed

4 files changed

+116
-57
lines changed

spring-core/src/main/java/org/springframework/util/AntPathMatcher.java

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,26 +26,38 @@
2626
import java.util.regex.Pattern;
2727

2828
/**
29-
* PathMatcher implementation for Ant-style path patterns. Examples are provided below.
29+
* {@link PathMatcher} implementation for Ant-style path patterns.
3030
*
3131
* <p>Part of this mapping code has been kindly borrowed from <a href="http://ant.apache.org">Apache Ant</a>.
3232
*
33-
* <p>The mapping matches URLs using the following rules:<br> <ul> <li>? matches one character</li> <li>* matches zero
34-
* or more characters</li> <li>** matches zero or more 'directories' in a path</li> </ul>
33+
* <p>The mapping matches URLs using the following rules:<br>
34+
* <ul>
35+
* <li>{@code ?} matches one character</li>
36+
* <li>{@code *} matches zero or more characters</li>
37+
* <li>{@code **} matches zero or more <em>directories</em> in a path</li>
38+
* </ul>
3539
*
36-
* <p>Some examples:<br> <ul> <li>{@code com/t?st.jsp} - matches {@code com/test.jsp} but also
37-
* {@code com/tast.jsp} or {@code com/txst.jsp}</li> <li>{@code com/*.jsp} - matches all
38-
* {@code .jsp} files in the {@code com} directory</li> <li>{@code com/&#42;&#42;/test.jsp} - matches all
39-
* {@code test.jsp} files underneath the {@code com} path</li> <li>{@code org/springframework/&#42;&#42;/*.jsp}
40-
* - matches all {@code .jsp} files underneath the {@code org/springframework} path</li>
41-
* <li>{@code org/&#42;&#42;/servlet/bla.jsp} - matches {@code org/springframework/servlet/bla.jsp} but also
42-
* {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}</li> </ul>
40+
* <h3>Examples</h3>
41+
* <ul>
42+
* <li>{@code com/t?st.jsp} &mdash; matches {@code com/test.jsp} but also
43+
* {@code com/tast.jsp} or {@code com/txst.jsp}</li>
44+
* <li>{@code com/*.jsp} &mdash; matches all {@code .jsp} files in the
45+
* {@code com} directory</li>
46+
* <li><code>com/&#42;&#42;/test.jsp</code> &mdash; matches all {@code test.jsp}
47+
* files underneath the {@code com} path</li>
48+
* <li><code>org/springframework/&#42;&#42;/*.jsp</code> &mdash; matches all
49+
* {@code .jsp} files underneath the {@code org/springframework} path</li>
50+
* <li><code>org/&#42;&#42;/servlet/bla.jsp</code> &mdash; matches
51+
* {@code org/springframework/servlet/bla.jsp} but also
52+
* {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}</li>
53+
* </ul>
4354
*
4455
* @author Alef Arendsen
4556
* @author Juergen Hoeller
4657
* @author Rob Harrop
4758
* @author Arjen Poutsma
4859
* @author Rossen Stoyanchev
60+
* @author Sam Brannen
4961
* @since 16.07.2003
5062
*/
5163
public class AntPathMatcher implements PathMatcher {
@@ -80,7 +92,7 @@ public AntPathMatcher() {
8092
}
8193

8294
/**
83-
* A convenience alternative constructor to use with a custom path separator.
95+
* A convenient, alternative constructor to use with a custom path separator.
8496
* @param pathSeparator the path separator to use, must not be {@code null}.
8597
* @since 4.1
8698
*/
@@ -93,7 +105,7 @@ public AntPathMatcher(String pathSeparator) {
93105

94106
/**
95107
* Set the path separator to use for pattern parsing.
96-
* Default is "/", as in Ant.
108+
* <p>Default is "/", as in Ant.
97109
*/
98110
public void setPathSeparator(String pathSeparator) {
99111
this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
@@ -102,7 +114,7 @@ public void setPathSeparator(String pathSeparator) {
102114

103115
/**
104116
* Specify whether to trim tokenized paths and patterns.
105-
* Default is {@code true}.
117+
* <p>Default is {@code true}.
106118
*/
107119
public void setTrimTokens(boolean trimTokens) {
108120
this.trimTokens = trimTokens;
@@ -116,7 +128,7 @@ public void setTrimTokens(boolean trimTokens) {
116128
* <p>Default is for the cache to be on, but with the variant to automatically
117129
* turn it off when encountering too many patterns to cache at runtime
118130
* (the threshold is 65536), assuming that arbitrary permutations of patterns
119-
* are coming in, with little chance for encountering a reoccurring pattern.
131+
* are coming in, with little chance for encountering a recurring pattern.
120132
* @see #getStringMatcher(String)
121133
*/
122134
public void setCachePatterns(boolean cachePatterns) {
@@ -317,7 +329,7 @@ protected String[] tokenizePath(String path) {
317329
}
318330

319331
/**
320-
* Tests whether or not a string matches against a pattern.
332+
* Test whether or not a string matches against a pattern.
321333
* @param pattern the pattern to match against (never {@code null})
322334
* @param str the String which must be matched against the pattern (never {@code null})
323335
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise
@@ -331,10 +343,10 @@ private boolean matchStrings(String pattern, String str, Map<String, String> uri
331343
* <p>The default implementation checks this AntPathMatcher's internal cache
332344
* (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance
333345
* if no cached copy is found.
334-
* When encountering too many patterns to cache at runtime (the threshold is 65536),
346+
* <p>When encountering too many patterns to cache at runtime (the threshold is 65536),
335347
* it turns the default cache off, assuming that arbitrary permutations of patterns
336-
* are coming in, with little chance for encountering a reoccurring pattern.
337-
* <p>This method may get overridden to implement a custom cache strategy.
348+
* are coming in, with little chance for encountering a recurring pattern.
349+
* <p>This method may be overridden to implement a custom cache strategy.
338350
* @param pattern the pattern to match against (never {@code null})
339351
* @return a corresponding AntPathStringMatcher (never {@code null})
340352
* @see #setCachePatterns
@@ -406,23 +418,35 @@ public Map<String, String> extractUriTemplateVariables(String pattern, String pa
406418
}
407419

408420
/**
409-
* Combines two patterns into a new pattern that is returned.
410-
* <p>This implementation simply concatenates the two patterns, unless the first pattern
411-
* contains a file extension match (such as {@code *.html}. In that case, the second pattern
412-
* should be included in the first, or an {@code IllegalArgumentException} is thrown.
413-
* <p>For example: <table>
414-
* <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr> <tr><td>/hotels</td><td>{@code
415-
* null}</td><td>/hotels</td></tr> <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr>
416-
* <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr>
417-
* <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels/&#42;&#42;</td><td>/bookings</td><td>/hotels/&#42;&#42;/bookings</td></tr>
418-
* <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr>
421+
* Combine two patterns into a new pattern.
422+
*
423+
* <p>This implementation simply concatenates the two patterns, unless
424+
* the first pattern contains a file extension match (e.g., {@code *.html}).
425+
* In that case, the second pattern will be merged into the first. Otherwise,
426+
* an {@code IllegalArgumentException} will be thrown.
427+
*
428+
* <h3>Examples</h3>
429+
* <table border="1">
430+
* <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr>
431+
* <tr><td>{@code null}</td><td>{@code null}</td><td>&nbsp;</td></tr>
432+
* <tr><td>/hotels</td><td>{@code null}</td><td>/hotels</td></tr>
433+
* <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr>
434+
* <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr>
435+
* <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr>
436+
* <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr>
437+
* <tr><td>/hotels/&#42;&#42;</td><td>/bookings</td><td>/hotels/&#42;&#42;/bookings</td></tr>
438+
* <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr>
439+
* <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr>
419440
* <tr><td>/hotels/&#42;&#42;</td><td>{hotel}</td><td>/hotels/&#42;&#42;/{hotel}</td></tr>
420-
* <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr> <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr>
421-
* <tr><td>/*.html</td><td>/*.txt</td><td>IllegalArgumentException</td></tr> </table>
441+
* <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr>
442+
* <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr>
443+
* <tr><td>/*.html</td><td>/*.txt</td><td>{@code IllegalArgumentException}</td></tr>
444+
* </table>
445+
*
422446
* @param pattern1 the first pattern
423447
* @param pattern2 the second pattern
424448
* @return the combination of the two patterns
425-
* @throws IllegalArgumentException when the two patterns cannot be combined
449+
* @throws IllegalArgumentException if the two patterns cannot be combined
426450
*/
427451
@Override
428452
public String combine(String pattern1, String pattern2) {
@@ -469,10 +493,18 @@ public String combine(String pattern1, String pattern2) {
469493
}
470494

471495
private String concat(String path1, String path2) {
472-
if (path1.endsWith(this.pathSeparator) || path2.startsWith(this.pathSeparator)) {
496+
boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator);
497+
boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator);
498+
499+
if (path1EndsWithSeparator && path2StartsWithSeparator) {
500+
return path1 + path2.substring(1);
501+
}
502+
else if (path1EndsWithSeparator || path2StartsWithSeparator) {
473503
return path1 + path2;
474504
}
475-
return path1 + this.pathSeparator + path2;
505+
else {
506+
return path1 + this.pathSeparator + path2;
507+
}
476508
}
477509

478510
/**

spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ public void match() {
6363
assertFalse(pathMatcher.match("tes?", "testt"));
6464
assertFalse(pathMatcher.match("tes?", "tsst"));
6565

66-
// test matchin with *'s
66+
// test matching with *'s
6767
assertTrue(pathMatcher.match("*", "test"));
6868
assertTrue(pathMatcher.match("test*", "test"));
6969
assertTrue(pathMatcher.match("test*", "testTest"));
@@ -148,7 +148,7 @@ public void withMatchStart() {
148148
assertFalse(pathMatcher.matchStart("tes?", "testt"));
149149
assertFalse(pathMatcher.matchStart("tes?", "tsst"));
150150

151-
// test matchin with *'s
151+
// test matching with *'s
152152
assertTrue(pathMatcher.matchStart("*", "test"));
153153
assertTrue(pathMatcher.matchStart("test*", "test"));
154154
assertTrue(pathMatcher.matchStart("test*", "testTest"));
@@ -237,7 +237,7 @@ public void uniqueDeliminator() {
237237
assertFalse(pathMatcher.match("tes?", "testt"));
238238
assertFalse(pathMatcher.match("tes?", "tsst"));
239239

240-
// test matchin with *'s
240+
// test matching with *'s
241241
assertTrue(pathMatcher.match("*", "test"));
242242
assertTrue(pathMatcher.match("test*", "test"));
243243
assertTrue(pathMatcher.match("test*", "testTest"));
@@ -354,8 +354,9 @@ public void extractUriTemplateVariablesRegex() {
354354
assertEquals("1.0.0", result.get("version"));
355355
}
356356

357-
// SPR-7787
358-
357+
/**
358+
* SPR-7787
359+
*/
359360
@Test
360361
public void extractUriTemplateVarsRegexQualifiers() {
361362
Map<String, String> result = pathMatcher.extractUriTemplateVariables(
@@ -380,8 +381,9 @@ public void extractUriTemplateVarsRegexQualifiers() {
380381
assertEquals("1.0.0.{12}", result.get("version"));
381382
}
382383

383-
// SPR-8455
384-
384+
/**
385+
* SPR-8455
386+
*/
385387
@Test
386388
public void extractUriTemplateVarsRegexCapturingGroups() {
387389
try {
@@ -421,6 +423,8 @@ public void combine() {
421423
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
422424
assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062
423425
assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); // SPR-10554
426+
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
427+
assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); // SPR-12975
424428
}
425429

426430
@Test
@@ -568,8 +572,9 @@ public void patternComparatorSort() {
568572
paths.clear();
569573
}
570574

571-
// SPR-8687
572-
575+
/**
576+
* SPR-8687
577+
*/
573578
@Test
574579
public void trimTokensOff() {
575580
pathMatcher.setTrimTokens(false);
@@ -579,7 +584,7 @@ public void trimTokensOff() {
579584
}
580585

581586
@Test
582-
public void testDefaultCacheSetting() {
587+
public void defaultCacheSetting() {
583588
match();
584589
assertTrue(pathMatcher.stringMatcherCache.size() > 20);
585590

@@ -591,7 +596,7 @@ public void testDefaultCacheSetting() {
591596
}
592597

593598
@Test
594-
public void testCacheSetToTrue() {
599+
public void cachePatternsSetToTrue() {
595600
pathMatcher.setCachePatterns(true);
596601
match();
597602
assertTrue(pathMatcher.stringMatcherCache.size() > 20);
@@ -604,14 +609,14 @@ public void testCacheSetToTrue() {
604609
}
605610

606611
@Test
607-
public void testCacheSetToFalse() {
612+
public void cachePatternsSetToFalse() {
608613
pathMatcher.setCachePatterns(false);
609614
match();
610615
assertTrue(pathMatcher.stringMatcherCache.isEmpty());
611616
}
612617

613618
@Test
614-
public void testExtensionMappingWithDotPathSeparator() {
619+
public void extensionMappingWithDotPathSeparator() {
615620
pathMatcher.setPathSeparator(".");
616621
assertEquals("Extension mapping should be disabled with \".\" as path separator",
617622
"/*.html.hotel.*", pathMatcher.combine("/*.html", "hotel.*"));

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -59,17 +59,27 @@ public String toString() {
5959
return builder.toString();
6060
}
6161

62+
/**
63+
* Indicates whether this condition is empty, i.e. whether or not it
64+
* contains any discrete items.
65+
* @return {@code true} if empty; {@code false} otherwise
66+
*/
67+
public boolean isEmpty() {
68+
return getContent().isEmpty();
69+
}
70+
6271

6372
/**
6473
* Return the discrete items a request condition is composed of.
65-
* For example URL patterns, HTTP request methods, param expressions, etc.
74+
* <p>For example URL patterns, HTTP request methods, param expressions, etc.
6675
* @return a collection of objects, never {@code null}
6776
*/
6877
protected abstract Collection<?> getContent();
6978

7079
/**
7180
* The notation to use when printing discrete items of content.
72-
* For example " || " for URL patterns or " && " for param expressions.
81+
* <p>For example {@code " || "} for URL patterns or {@code " && "}
82+
* for param expressions.
7383
*/
7484
protected abstract String getToStringInfix();
7585

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,12 +326,24 @@ public int hashCode() {
326326
public String toString() {
327327
StringBuilder builder = new StringBuilder("{");
328328
builder.append(this.patternsCondition);
329-
builder.append(",methods=").append(this.methodsCondition);
330-
builder.append(",params=").append(this.paramsCondition);
331-
builder.append(",headers=").append(this.headersCondition);
332-
builder.append(",consumes=").append(this.consumesCondition);
333-
builder.append(",produces=").append(this.producesCondition);
334-
builder.append(",custom=").append(this.customConditionHolder);
329+
if (!this.methodsCondition.isEmpty()) {
330+
builder.append(",methods=").append(this.methodsCondition);
331+
}
332+
if (!this.paramsCondition.isEmpty()) {
333+
builder.append(",params=").append(this.paramsCondition);
334+
}
335+
if (!this.headersCondition.isEmpty()) {
336+
builder.append(",headers=").append(this.headersCondition);
337+
}
338+
if (!this.consumesCondition.isEmpty()) {
339+
builder.append(",consumes=").append(this.consumesCondition);
340+
}
341+
if (!this.producesCondition.isEmpty()) {
342+
builder.append(",produces=").append(this.producesCondition);
343+
}
344+
if (!this.customConditionHolder.isEmpty()) {
345+
builder.append(",custom=").append(this.customConditionHolder);
346+
}
335347
builder.append('}');
336348
return builder.toString();
337349
}

0 commit comments

Comments
 (0)