View Javadoc
1   /*
2    * Copyright 2002-2014 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.web.servlet.mvc.condition;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.Comparator;
24  import java.util.Iterator;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Set;
28  import javax.servlet.http.HttpServletRequest;
29  
30  import org.springframework.util.AntPathMatcher;
31  import org.springframework.util.PathMatcher;
32  import org.springframework.util.StringUtils;
33  import org.springframework.web.util.UrlPathHelper;
34  
35  /**
36   * A logical disjunction (' || ') request condition that matches a request
37   * against a set of URL path patterns.
38   *
39   * @author Rossen Stoyanchev
40   * @since 3.1
41   */
42  public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
43  
44  	private final Set<String> patterns;
45  
46  	private final UrlPathHelper pathHelper;
47  
48  	private final PathMatcher pathMatcher;
49  
50  	private final boolean useSuffixPatternMatch;
51  
52  	private final boolean useTrailingSlashMatch;
53  
54  	private final List<String> fileExtensions = new ArrayList<String>();
55  
56  
57  	/**
58  	 * Creates a new instance with the given URL patterns.
59  	 * Each pattern that is not empty and does not start with "/" is prepended with "/".
60  	 * @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
61  	 */
62  	public PatternsRequestCondition(String... patterns) {
63  		this(asList(patterns), null, null, true, true, null);
64  	}
65  
66  	/**
67  	 * Additional constructor with flags for using suffix pattern (.*) and
68  	 * trailing slash matches.
69  	 * @param patterns the URL patterns to use; if 0, the condition will match to every request.
70  	 * @param urlPathHelper for determining the lookup path of a request
71  	 * @param pathMatcher for path matching with patterns
72  	 * @param useSuffixPatternMatch whether to enable matching by suffix (".*")
73  	 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash
74  	 */
75  	public PatternsRequestCondition(String[] patterns, UrlPathHelper urlPathHelper, PathMatcher pathMatcher,
76  			boolean useSuffixPatternMatch, boolean useTrailingSlashMatch) {
77  
78  		this(asList(patterns), urlPathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, null);
79  	}
80  
81  	/**
82  	 * Creates a new instance with the given URL patterns.
83  	 * Each pattern that is not empty and does not start with "/" is pre-pended with "/".
84  	 * @param patterns the URL patterns to use; if 0, the condition will match to every request.
85  	 * @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request
86  	 * @param pathMatcher a {@link PathMatcher} for pattern path matching
87  	 * @param useSuffixPatternMatch whether to enable matching by suffix (".*")
88  	 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash
89  	 * @param fileExtensions a list of file extensions to consider for path matching
90  	 */
91  	public PatternsRequestCondition(String[] patterns, UrlPathHelper urlPathHelper,
92  			PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,
93  			List<String> fileExtensions) {
94  
95  		this(asList(patterns), urlPathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, fileExtensions);
96  	}
97  
98  	/**
99  	 * Private constructor accepting a collection of patterns.
100 	 */
101 	private PatternsRequestCondition(Collection<String> patterns, UrlPathHelper urlPathHelper,
102 			PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,
103 			List<String> fileExtensions) {
104 
105 		this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns));
106 		this.pathHelper = urlPathHelper != null ? urlPathHelper : new UrlPathHelper();
107 		this.pathMatcher = pathMatcher != null ? pathMatcher : new AntPathMatcher();
108 		this.useSuffixPatternMatch = useSuffixPatternMatch;
109 		this.useTrailingSlashMatch = useTrailingSlashMatch;
110 		if (fileExtensions != null) {
111 			for (String fileExtension : fileExtensions) {
112 				if (fileExtension.charAt(0) != '.') {
113 					fileExtension = "." + fileExtension;
114 				}
115 				this.fileExtensions.add(fileExtension);
116 			}
117 		}
118 	}
119 
120 
121 	private static List<String> asList(String... patterns) {
122 		return (patterns != null ? Arrays.asList(patterns) : Collections.<String>emptyList());
123 	}
124 
125 	private static Set<String> prependLeadingSlash(Collection<String> patterns) {
126 		if (patterns == null) {
127 			return Collections.emptySet();
128 		}
129 		Set<String> result = new LinkedHashSet<String>(patterns.size());
130 		for (String pattern : patterns) {
131 			if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
132 				pattern = "/" + pattern;
133 			}
134 			result.add(pattern);
135 		}
136 		return result;
137 	}
138 
139 	public Set<String> getPatterns() {
140 		return this.patterns;
141 	}
142 
143 	@Override
144 	protected Collection<String> getContent() {
145 		return this.patterns;
146 	}
147 
148 	@Override
149 	protected String getToStringInfix() {
150 		return " || ";
151 	}
152 
153 	/**
154 	 * Returns a new instance with URL patterns from the current instance ("this") and
155 	 * the "other" instance as follows:
156 	 * <ul>
157 	 * <li>If there are patterns in both instances, combine the patterns in "this" with
158 	 * the patterns in "other" using {@link PathMatcher#combine(String, String)}.
159 	 * <li>If only one instance has patterns, use them.
160 	 * <li>If neither instance has patterns, use an empty String (i.e. "").
161 	 * </ul>
162 	 */
163 	@Override
164 	public PatternsRequestCondition combine(PatternsRequestCondition other) {
165 		Set<String> result = new LinkedHashSet<String>();
166 		if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) {
167 			for (String pattern1 : this.patterns) {
168 				for (String pattern2 : other.patterns) {
169 					result.add(this.pathMatcher.combine(pattern1, pattern2));
170 				}
171 			}
172 		}
173 		else if (!this.patterns.isEmpty()) {
174 			result.addAll(this.patterns);
175 		}
176 		else if (!other.patterns.isEmpty()) {
177 			result.addAll(other.patterns);
178 		}
179 		else {
180 			result.add("");
181 		}
182 		return new PatternsRequestCondition(result, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch,
183 				this.useTrailingSlashMatch, this.fileExtensions);
184 	}
185 
186 	/**
187 	 * Checks if any of the patterns match the given request and returns an instance
188 	 * that is guaranteed to contain matching patterns, sorted via
189 	 * {@link PathMatcher#getPatternComparator(String)}.
190 	 * <p>A matching pattern is obtained by making checks in the following order:
191 	 * <ul>
192 	 * <li>Direct match
193 	 * <li>Pattern match with ".*" appended if the pattern doesn't already contain a "."
194 	 * <li>Pattern match
195 	 * <li>Pattern match with "/" appended if the pattern doesn't already end in "/"
196 	 * </ul>
197 	 * @param request the current request
198 	 * @return the same instance if the condition contains no patterns;
199 	 * or a new condition with sorted matching patterns;
200 	 * or {@code null} if no patterns match.
201 	 */
202 	@Override
203 	public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
204 
205 		if (this.patterns.isEmpty()) {
206 			return this;
207 		}
208 
209 		String lookupPath = this.pathHelper.getLookupPathForRequest(request);
210 		List<String> matches = getMatchingPatterns(lookupPath);
211 
212 		return matches.isEmpty() ? null :
213 			new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch,
214 					this.useTrailingSlashMatch, this.fileExtensions);
215 	}
216 
217 	/**
218 	 * Find the patterns matching the given lookup path. Invoking this method should
219 	 * yield results equivalent to those of calling
220 	 * {@link #getMatchingCondition(javax.servlet.http.HttpServletRequest)}.
221 	 * This method is provided as an alternative to be used if no request is available
222 	 * (e.g. introspection, tooling, etc).
223 	 *
224 	 * @param lookupPath the lookup path to match to existing patterns
225 	 * @return a collection of matching patterns sorted with the closest match at the top
226 	 */
227 	public List<String> getMatchingPatterns(String lookupPath) {
228 		List<String> matches = new ArrayList<String>();
229 		for (String pattern : this.patterns) {
230 			String match = getMatchingPattern(pattern, lookupPath);
231 			if (match != null) {
232 				matches.add(match);
233 			}
234 		}
235 		Collections.sort(matches, this.pathMatcher.getPatternComparator(lookupPath));
236 		return matches;
237 	}
238 
239 	private String getMatchingPattern(String pattern, String lookupPath) {
240 		if (pattern.equals(lookupPath)) {
241 			return pattern;
242 		}
243 		if (this.useSuffixPatternMatch) {
244 			if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
245 				for (String extension : this.fileExtensions) {
246 					if (this.pathMatcher.match(pattern + extension, lookupPath)) {
247 						return pattern + extension;
248 					}
249 				}
250 			}
251 			else {
252 				boolean hasSuffix = pattern.indexOf('.') != -1;
253 				if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
254 					return pattern + ".*";
255 				}
256 			}
257 		}
258 		if (this.pathMatcher.match(pattern, lookupPath)) {
259 			return pattern;
260 		}
261 		if (this.useTrailingSlashMatch) {
262 			if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
263 				return pattern +"/";
264 			}
265 		}
266 		return null;
267 	}
268 
269 	/**
270 	 * Compare the two conditions based on the URL patterns they contain.
271 	 * Patterns are compared one at a time, from top to bottom via
272 	 * {@link PathMatcher#getPatternComparator(String)}. If all compared
273 	 * patterns match equally, but one instance has more patterns, it is
274 	 * considered a closer match.
275 	 * <p>It is assumed that both instances have been obtained via
276 	 * {@link #getMatchingCondition(HttpServletRequest)} to ensure they
277 	 * contain only patterns that match the request and are sorted with
278 	 * the best matches on top.
279 	 */
280 	@Override
281 	public int compareTo(PatternsRequestCondition other, HttpServletRequest request) {
282 		String lookupPath = this.pathHelper.getLookupPathForRequest(request);
283 		Comparator<String> patternComparator = this.pathMatcher.getPatternComparator(lookupPath);
284 		Iterator<String> iterator = this.patterns.iterator();
285 		Iterator<String> iteratorOther = other.patterns.iterator();
286 		while (iterator.hasNext() && iteratorOther.hasNext()) {
287 			int result = patternComparator.compare(iterator.next(), iteratorOther.next());
288 			if (result != 0) {
289 				return result;
290 			}
291 		}
292 		if (iterator.hasNext()) {
293 			return -1;
294 		}
295 		else if (iteratorOther.hasNext()) {
296 			return 1;
297 		}
298 		else {
299 			return 0;
300 		}
301 	}
302 
303 }