View Javadoc

1   /*
2    * Copyright 2002-2012 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  
17  package org.springframework.util;
18  
19  import java.util.Comparator;
20  import java.util.LinkedHashMap;
21  import java.util.Map;
22  import java.util.concurrent.ConcurrentHashMap;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  /**
27   * PathMatcher implementation for Ant-style path patterns. Examples are provided below.
28   *
29   * <p>Part of this mapping code has been kindly borrowed from <a href="http://ant.apache.org">Apache Ant</a>.
30   *
31   * <p>The mapping matches URLs using the following rules:<br> <ul> <li>? matches one character</li> <li>* matches zero
32   * or more characters</li> <li>** matches zero or more 'directories' in a path</li> </ul>
33   *
34   * <p>Some examples:<br> <ul> <li><code>com/t?st.jsp</code> - matches <code>com/test.jsp</code> but also
35   * <code>com/tast.jsp</code> or <code>com/txst.jsp</code></li> <li><code>com/*.jsp</code> - matches all
36   * <code>.jsp</code> files in the <code>com</code> directory</li> <li><code>com/&#42;&#42;/test.jsp</code> - matches all
37   * <code>test.jsp</code> files underneath the <code>com</code> path</li> <li><code>org/springframework/&#42;&#42;/*.jsp</code>
38   * - matches all <code>.jsp</code> files underneath the <code>org/springframework</code> path</li>
39   * <li><code>org/&#42;&#42;/servlet/bla.jsp</code> - matches <code>org/springframework/servlet/bla.jsp</code> but also
40   * <code>org/springframework/testing/servlet/bla.jsp</code> and <code>org/servlet/bla.jsp</code></li> </ul>
41   *
42   * @author Alef Arendsen
43   * @author Juergen Hoeller
44   * @author Rob Harrop
45   * @author Arjen Poutsma
46   * @since 16.07.2003
47   */
48  public class AntPathMatcher implements PathMatcher {
49  
50  	private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}");
51  
52  	/** Default path separator: "/" */
53  	public static final String DEFAULT_PATH_SEPARATOR = "/";
54  
55  	private String pathSeparator = DEFAULT_PATH_SEPARATOR;
56  
57  	private final Map<String, AntPathStringMatcher> stringMatcherCache =
58  			new ConcurrentHashMap<String, AntPathStringMatcher>();
59  
60  
61  	/** Set the path separator to use for pattern parsing. Default is "/", as in Ant. */
62  	public void setPathSeparator(String pathSeparator) {
63  		this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
64  	}
65  
66  
67  	public boolean isPattern(String path) {
68  		return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
69  	}
70  
71  	public boolean match(String pattern, String path) {
72  		return doMatch(pattern, path, true, null);
73  	}
74  
75  	public boolean matchStart(String pattern, String path) {
76  		return doMatch(pattern, path, false, null);
77  	}
78  
79  
80  	/**
81  	 * Actually match the given <code>path</code> against the given <code>pattern</code>.
82  	 * @param pattern the pattern to match against
83  	 * @param path the path String to test
84  	 * @param fullMatch whether a full pattern match is required (else a pattern match
85  	 * as far as the given base path goes is sufficient)
86  	 * @return <code>true</code> if the supplied <code>path</code> matched, <code>false</code> if it didn't
87  	 */
88  	protected boolean doMatch(String pattern, String path, boolean fullMatch,
89  			Map<String, String> uriTemplateVariables) {
90  
91  		if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
92  			return false;
93  		}
94  
95  		String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
96  		String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);
97  
98  		int pattIdxStart = 0;
99  		int pattIdxEnd = pattDirs.length - 1;
100 		int pathIdxStart = 0;
101 		int pathIdxEnd = pathDirs.length - 1;
102 
103 		// Match all elements up to the first **
104 		while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
105 			String patDir = pattDirs[pattIdxStart];
106 			if ("**".equals(patDir)) {
107 				break;
108 			}
109 			if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
110 				return false;
111 			}
112 			pattIdxStart++;
113 			pathIdxStart++;
114 		}
115 
116 		if (pathIdxStart > pathIdxEnd) {
117 			// Path is exhausted, only match if rest of pattern is * or **'s
118 			if (pattIdxStart > pattIdxEnd) {
119 				return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) :
120 						!path.endsWith(this.pathSeparator));
121 			}
122 			if (!fullMatch) {
123 				return true;
124 			}
125 			if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
126 				return true;
127 			}
128 			for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
129 				if (!pattDirs[i].equals("**")) {
130 					return false;
131 				}
132 			}
133 			return true;
134 		}
135 		else if (pattIdxStart > pattIdxEnd) {
136 			// String not exhausted, but pattern is. Failure.
137 			return false;
138 		}
139 		else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
140 			// Path start definitely matches due to "**" part in pattern.
141 			return true;
142 		}
143 
144 		// up to last '**'
145 		while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
146 			String patDir = pattDirs[pattIdxEnd];
147 			if (patDir.equals("**")) {
148 				break;
149 			}
150 			if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
151 				return false;
152 			}
153 			pattIdxEnd--;
154 			pathIdxEnd--;
155 		}
156 		if (pathIdxStart > pathIdxEnd) {
157 			// String is exhausted
158 			for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
159 				if (!pattDirs[i].equals("**")) {
160 					return false;
161 				}
162 			}
163 			return true;
164 		}
165 
166 		while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
167 			int patIdxTmp = -1;
168 			for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
169 				if (pattDirs[i].equals("**")) {
170 					patIdxTmp = i;
171 					break;
172 				}
173 			}
174 			if (patIdxTmp == pattIdxStart + 1) {
175 				// '**/**' situation, so skip one
176 				pattIdxStart++;
177 				continue;
178 			}
179 			// Find the pattern between padIdxStart & padIdxTmp in str between
180 			// strIdxStart & strIdxEnd
181 			int patLength = (patIdxTmp - pattIdxStart - 1);
182 			int strLength = (pathIdxEnd - pathIdxStart + 1);
183 			int foundIdx = -1;
184 
185 			strLoop:
186 			for (int i = 0; i <= strLength - patLength; i++) {
187 				for (int j = 0; j < patLength; j++) {
188 					String subPat = pattDirs[pattIdxStart + j + 1];
189 					String subStr = pathDirs[pathIdxStart + i + j];
190 					if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
191 						continue strLoop;
192 					}
193 				}
194 				foundIdx = pathIdxStart + i;
195 				break;
196 			}
197 
198 			if (foundIdx == -1) {
199 				return false;
200 			}
201 
202 			pattIdxStart = patIdxTmp;
203 			pathIdxStart = foundIdx + patLength;
204 		}
205 
206 		for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
207 			if (!pattDirs[i].equals("**")) {
208 				return false;
209 			}
210 		}
211 
212 		return true;
213 	}
214 
215 	/**
216 	 * Tests whether or not a string matches against a pattern. The pattern may contain two special characters:<br> '*'
217 	 * means zero or more characters<br> '?' means one and only one character
218 	 * @param pattern pattern to match against. Must not be <code>null</code>.
219 	 * @param str string which must be matched against the pattern. Must not be <code>null</code>.
220 	 * @return <code>true</code> if the string matches against the pattern, or <code>false</code> otherwise.
221 	 */
222 	private boolean matchStrings(String pattern, String str, Map<String, String> uriTemplateVariables) {
223 		AntPathStringMatcher matcher = this.stringMatcherCache.get(pattern);
224 		if (matcher == null) {
225 			matcher = new AntPathStringMatcher(pattern);
226 			this.stringMatcherCache.put(pattern, matcher);
227 		}
228 		return matcher.matchStrings(str, uriTemplateVariables);
229 	}
230 
231 	/**
232 	 * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul>
233 	 * <li>'<code>/docs/cvs/commit.html</code>' and '<code>/docs/cvs/commit.html</code> -> ''</li>
234 	 * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> -> '<code>cvs/commit</code>'</li>
235 	 * <li>'<code>/docs/cvs/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>commit.html</code>'</li>
236 	 * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> -> '<code>cvs/commit</code>'</li>
237 	 * <li>'<code>/docs/**\/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>cvs/commit.html</code>'</li>
238 	 * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>docs/cvs/commit.html</code>'</li>
239 	 * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>/docs/cvs/commit.html</code>'</li>
240 	 * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>/docs/cvs/commit.html</code>'</li> </ul>
241 	 * <p>Assumes that {@link #match} returns <code>true</code> for '<code>pattern</code>' and '<code>path</code>', but
242 	 * does <strong>not</strong> enforce this.
243 	 */
244 	public String extractPathWithinPattern(String pattern, String path) {
245 		String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
246 		String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator);
247 
248 		StringBuilder builder = new StringBuilder();
249 
250 		// Add any path parts that have a wildcarded pattern part.
251 		int puts = 0;
252 		for (int i = 0; i < patternParts.length; i++) {
253 			String patternPart = patternParts[i];
254 			if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {
255 				if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) {
256 					builder.append(this.pathSeparator);
257 				}
258 				builder.append(pathParts[i]);
259 				puts++;
260 			}
261 		}
262 
263 		// Append any trailing path parts.
264 		for (int i = patternParts.length; i < pathParts.length; i++) {
265 			if (puts > 0 || i > 0) {
266 				builder.append(this.pathSeparator);
267 			}
268 			builder.append(pathParts[i]);
269 		}
270 
271 		return builder.toString();
272 	}
273 
274 	public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
275 		Map<String, String> variables = new LinkedHashMap<String, String>();
276 		boolean result = doMatch(pattern, path, true, variables);
277 		Assert.state(result, "Pattern \"" + pattern + "\" is not a match for \"" + path + "\"");
278 		return variables;
279 	}
280 
281 	/**
282 	 * Combines two patterns into a new pattern that is returned.
283 	 * <p>This implementation simply concatenates the two patterns, unless the first pattern
284 	 * contains a file extension match (such as {@code *.html}. In that case, the second pattern
285 	 * should be included in the first, or an {@code IllegalArgumentException} is thrown.
286 	 * <p>For example: <table>
287 	 * <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr> <tr><td>/hotels</td><td>{@code
288 	 * null}</td><td>/hotels</td></tr> <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr>
289 	 * <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr>
290 	 * <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>
291 	 * <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr>
292 	 * <tr><td>/hotels/&#42;&#42;</td><td>{hotel}</td><td>/hotels/&#42;&#42;/{hotel}</td></tr>
293 	 * <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>
294 	 * <tr><td>/*.html</td><td>/*.txt</td><td>IllegalArgumentException</td></tr> </table>
295 	 * @param pattern1 the first pattern
296 	 * @param pattern2 the second pattern
297 	 * @return the combination of the two patterns
298 	 * @throws IllegalArgumentException when the two patterns cannot be combined
299 	 */
300 	public String combine(String pattern1, String pattern2) {
301 		if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) {
302 			return "";
303 		}
304 		else if (!StringUtils.hasText(pattern1)) {
305 			return pattern2;
306 		}
307 		else if (!StringUtils.hasText(pattern2)) {
308 			return pattern1;
309 		}
310 		else if (!pattern1.contains("{") && match(pattern1, pattern2)) {
311 			return pattern2;
312 		}
313 		else if (pattern1.endsWith("/*")) {
314 			if (pattern2.startsWith("/")) {
315 				// /hotels/* + /booking -> /hotels/booking
316 				return pattern1.substring(0, pattern1.length() - 1) + pattern2.substring(1);
317 			}
318 			else {
319 				// /hotels/* + booking -> /hotels/booking
320 				return pattern1.substring(0, pattern1.length() - 1) + pattern2;
321 			}
322 		}
323 		else if (pattern1.endsWith("/**")) {
324 			if (pattern2.startsWith("/")) {
325 				// /hotels/** + /booking -> /hotels/**/booking
326 				return pattern1 + pattern2;
327 			}
328 			else {
329 				// /hotels/** + booking -> /hotels/**/booking
330 				return pattern1 + "/" + pattern2;
331 			}
332 		}
333 		else {
334 			int dotPos1 = pattern1.indexOf('.');
335 			if (dotPos1 == -1) {
336 				// simply concatenate the two patterns
337 				if (pattern1.endsWith("/") || pattern2.startsWith("/")) {
338 					return pattern1 + pattern2;
339 				}
340 				else {
341 					return pattern1 + "/" + pattern2;
342 				}
343 			}
344 			String fileName1 = pattern1.substring(0, dotPos1);
345 			String extension1 = pattern1.substring(dotPos1);
346 			String fileName2;
347 			String extension2;
348 			int dotPos2 = pattern2.indexOf('.');
349 			if (dotPos2 != -1) {
350 				fileName2 = pattern2.substring(0, dotPos2);
351 				extension2 = pattern2.substring(dotPos2);
352 			}
353 			else {
354 				fileName2 = pattern2;
355 				extension2 = "";
356 			}
357 			String fileName = fileName1.endsWith("*") ? fileName2 : fileName1;
358 			String extension = extension1.startsWith("*") ? extension2 : extension1;
359 
360 			return fileName + extension;
361 		}
362 	}
363 
364 	/**
365 	 * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of explicitness.
366 	 * <p>The returned <code>Comparator</code> will {@linkplain java.util.Collections#sort(java.util.List,
367 	 * java.util.Comparator) sort} a list so that more specific patterns (without uri templates or wild cards) come before
368 	 * generic patterns. So given a list with the following patterns: <ol> <li><code>/hotels/new</code></li>
369 	 * <li><code>/hotels/{hotel}</code></li> <li><code>/hotels/*</code></li> </ol> the returned comparator will sort this
370 	 * list so that the order will be as indicated.
371 	 * <p>The full path given as parameter is used to test for exact matches. So when the given path is {@code /hotels/2},
372 	 * the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}.
373 	 * @param path the full path to use for comparison
374 	 * @return a comparator capable of sorting patterns in order of explicitness
375 	 */
376 	public Comparator<String> getPatternComparator(String path) {
377 		return new AntPatternComparator(path);
378 	}
379 
380 
381 	private static class AntPatternComparator implements Comparator<String> {
382 
383 		private final String path;
384 
385 		private AntPatternComparator(String path) {
386 			this.path = path;
387 		}
388 
389 		public int compare(String pattern1, String pattern2) {
390 			if (pattern1 == null && pattern2 == null) {
391 				return 0;
392 			}
393 			else if (pattern1 == null) {
394 				return 1;
395 			}
396 			else if (pattern2 == null) {
397 				return -1;
398 			}
399 			boolean pattern1EqualsPath = pattern1.equals(path);
400 			boolean pattern2EqualsPath = pattern2.equals(path);
401 			if (pattern1EqualsPath && pattern2EqualsPath) {
402 				return 0;
403 			}
404 			else if (pattern1EqualsPath) {
405 				return -1;
406 			}
407 			else if (pattern2EqualsPath) {
408 				return 1;
409 			}
410 			int wildCardCount1 = getWildCardCount(pattern1);
411 			int wildCardCount2 = getWildCardCount(pattern2);
412 
413 			int bracketCount1 = StringUtils.countOccurrencesOf(pattern1, "{");
414 			int bracketCount2 = StringUtils.countOccurrencesOf(pattern2, "{");
415 
416 			int totalCount1 = wildCardCount1 + bracketCount1;
417 			int totalCount2 = wildCardCount2 + bracketCount2;
418 
419 			if (totalCount1 != totalCount2) {
420 				return totalCount1 - totalCount2;
421 			}
422 
423 			int pattern1Length = getPatternLength(pattern1);
424 			int pattern2Length = getPatternLength(pattern2);
425 
426 			if (pattern1Length != pattern2Length) {
427 				return pattern2Length - pattern1Length;
428 			}
429 
430 			if (wildCardCount1 < wildCardCount2) {
431 				return -1;
432 			}
433 			else if (wildCardCount2 < wildCardCount1) {
434 				return 1;
435 			}
436 
437 			if (bracketCount1 < bracketCount2) {
438 				return -1;
439 			}
440 			else if (bracketCount2 < bracketCount1) {
441 				return 1;
442 			}
443 
444 			return 0;
445 		}
446 
447 		private int getWildCardCount(String pattern) {
448 			if (pattern.endsWith(".*")) {
449 				pattern = pattern.substring(0, pattern.length() - 2);
450 			}
451 			return StringUtils.countOccurrencesOf(pattern, "*");
452 		}
453 
454 		/**
455 		 * Returns the length of the given pattern, where template variables are considered to be 1 long.
456 		 */
457 		private int getPatternLength(String pattern) {
458 			Matcher m = VARIABLE_PATTERN.matcher(pattern);
459 			return m.replaceAll("#").length();
460 		}
461 	}
462 
463 }