001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.mvc.condition; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.Iterator; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Set; 027 028import javax.servlet.http.HttpServletRequest; 029 030import org.springframework.lang.Nullable; 031import org.springframework.util.AntPathMatcher; 032import org.springframework.util.ObjectUtils; 033import org.springframework.util.PathMatcher; 034import org.springframework.util.StringUtils; 035import org.springframework.web.servlet.HandlerMapping; 036import org.springframework.web.util.UrlPathHelper; 037 038/** 039 * A logical disjunction (' || ') request condition that matches a request 040 * against a set of URL path patterns. 041 * 042 * @author Rossen Stoyanchev 043 * @since 3.1 044 */ 045public class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> { 046 047 private final static Set<String> EMPTY_PATH_PATTERN = Collections.singleton(""); 048 049 050 private final Set<String> patterns; 051 052 private final UrlPathHelper pathHelper; 053 054 private final PathMatcher pathMatcher; 055 056 private final boolean useSuffixPatternMatch; 057 058 private final boolean useTrailingSlashMatch; 059 060 private final List<String> fileExtensions = new ArrayList<>(); 061 062 063 /** 064 * Creates a new instance with the given URL patterns. Each pattern that is 065 * not empty and does not start with "/" is prepended with "/". 066 * @param patterns 0 or more URL patterns; if 0 the condition will match to 067 * every request. 068 */ 069 public PatternsRequestCondition(String... patterns) { 070 this(patterns, null, null, true, true, null); 071 } 072 073 /** 074 * Alternative constructor with additional, optional {@link UrlPathHelper}, 075 * {@link PathMatcher}, and whether to automatically match trailing slashes. 076 * @param patterns the URL patterns to use; if 0, the condition will match to every request. 077 * @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request 078 * @param pathMatcher a {@link PathMatcher} for pattern path matching 079 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash 080 * @since 5.2.4 081 */ 082 public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, 083 @Nullable PathMatcher pathMatcher, boolean useTrailingSlashMatch) { 084 085 this(patterns, urlPathHelper, pathMatcher, false, useTrailingSlashMatch, null); 086 } 087 088 /** 089 * Alternative constructor with additional optional parameters. 090 * @param patterns the URL patterns to use; if 0, the condition will match to every request. 091 * @param urlPathHelper for determining the lookup path of a request 092 * @param pathMatcher for path matching with patterns 093 * @param useSuffixPatternMatch whether to enable matching by suffix (".*") 094 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash 095 * @deprecated as of 5.2.4. See class-level note in 096 * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} 097 * on the deprecation of path extension config options. 098 */ 099 @Deprecated 100 public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, 101 @Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch) { 102 103 this(patterns, urlPathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, null); 104 } 105 106 /** 107 * Alternative constructor with additional optional parameters. 108 * @param patterns the URL patterns to use; if 0, the condition will match to every request. 109 * @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request 110 * @param pathMatcher a {@link PathMatcher} for pattern path matching 111 * @param useSuffixPatternMatch whether to enable matching by suffix (".*") 112 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash 113 * @param fileExtensions a list of file extensions to consider for path matching 114 * @deprecated as of 5.2.4. See class-level note in 115 * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} 116 * on the deprecation of path extension config options. 117 */ 118 @Deprecated 119 public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, 120 @Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, 121 boolean useTrailingSlashMatch, @Nullable List<String> fileExtensions) { 122 123 this.patterns = initPatterns(patterns); 124 this.pathHelper = urlPathHelper != null ? urlPathHelper : UrlPathHelper.defaultInstance; 125 this.pathMatcher = pathMatcher != null ? pathMatcher : new AntPathMatcher(); 126 this.useSuffixPatternMatch = useSuffixPatternMatch; 127 this.useTrailingSlashMatch = useTrailingSlashMatch; 128 129 if (fileExtensions != null) { 130 for (String fileExtension : fileExtensions) { 131 if (fileExtension.charAt(0) != '.') { 132 fileExtension = "." + fileExtension; 133 } 134 this.fileExtensions.add(fileExtension); 135 } 136 } 137 } 138 139 private static Set<String> initPatterns(String[] patterns) { 140 if (!hasPattern(patterns)) { 141 return EMPTY_PATH_PATTERN; 142 } 143 Set<String> result = new LinkedHashSet<>(patterns.length); 144 for (String pattern : patterns) { 145 if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { 146 pattern = "/" + pattern; 147 } 148 result.add(pattern); 149 } 150 return result; 151 } 152 153 private static boolean hasPattern(String[] patterns) { 154 if (!ObjectUtils.isEmpty(patterns)) { 155 for (String pattern : patterns) { 156 if (StringUtils.hasText(pattern)) { 157 return true; 158 } 159 } 160 } 161 return false; 162 } 163 164 /** 165 * Private constructor for use when combining and matching. 166 */ 167 private PatternsRequestCondition(Set<String> patterns, PatternsRequestCondition other) { 168 this.patterns = patterns; 169 this.pathHelper = other.pathHelper; 170 this.pathMatcher = other.pathMatcher; 171 this.useSuffixPatternMatch = other.useSuffixPatternMatch; 172 this.useTrailingSlashMatch = other.useTrailingSlashMatch; 173 this.fileExtensions.addAll(other.fileExtensions); 174 } 175 176 177 public Set<String> getPatterns() { 178 return this.patterns; 179 } 180 181 @Override 182 protected Collection<String> getContent() { 183 return this.patterns; 184 } 185 186 @Override 187 protected String getToStringInfix() { 188 return " || "; 189 } 190 191 /** 192 * Returns a new instance with URL patterns from the current instance ("this") and 193 * the "other" instance as follows: 194 * <ul> 195 * <li>If there are patterns in both instances, combine the patterns in "this" with 196 * the patterns in "other" using {@link PathMatcher#combine(String, String)}. 197 * <li>If only one instance has patterns, use them. 198 * <li>If neither instance has patterns, use an empty String (i.e. ""). 199 * </ul> 200 */ 201 @Override 202 public PatternsRequestCondition combine(PatternsRequestCondition other) { 203 if (isEmptyPathPattern() && other.isEmptyPathPattern()) { 204 return this; 205 } 206 else if (other.isEmptyPathPattern()) { 207 return this; 208 } 209 else if (isEmptyPathPattern()) { 210 return other; 211 } 212 Set<String> result = new LinkedHashSet<>(); 213 if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) { 214 for (String pattern1 : this.patterns) { 215 for (String pattern2 : other.patterns) { 216 result.add(this.pathMatcher.combine(pattern1, pattern2)); 217 } 218 } 219 } 220 return new PatternsRequestCondition(result, this); 221 } 222 223 private boolean isEmptyPathPattern() { 224 return this.patterns == EMPTY_PATH_PATTERN; 225 } 226 227 /** 228 * Checks if any of the patterns match the given request and returns an instance 229 * that is guaranteed to contain matching patterns, sorted via 230 * {@link PathMatcher#getPatternComparator(String)}. 231 * <p>A matching pattern is obtained by making checks in the following order: 232 * <ul> 233 * <li>Direct match 234 * <li>Pattern match with ".*" appended if the pattern doesn't already contain a "." 235 * <li>Pattern match 236 * <li>Pattern match with "/" appended if the pattern doesn't already end in "/" 237 * </ul> 238 * @param request the current request 239 * @return the same instance if the condition contains no patterns; 240 * or a new condition with sorted matching patterns; 241 * or {@code null} if no patterns match. 242 */ 243 @Override 244 @Nullable 245 public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) { 246 String lookupPath = this.pathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); 247 List<String> matches = getMatchingPatterns(lookupPath); 248 return !matches.isEmpty() ? new PatternsRequestCondition(new LinkedHashSet<>(matches), this) : null; 249 } 250 251 /** 252 * Find the patterns matching the given lookup path. Invoking this method should 253 * yield results equivalent to those of calling {@link #getMatchingCondition}. 254 * This method is provided as an alternative to be used if no request is available 255 * (e.g. introspection, tooling, etc). 256 * @param lookupPath the lookup path to match to existing patterns 257 * @return a collection of matching patterns sorted with the closest match at the top 258 */ 259 public List<String> getMatchingPatterns(String lookupPath) { 260 List<String> matches = null; 261 for (String pattern : this.patterns) { 262 String match = getMatchingPattern(pattern, lookupPath); 263 if (match != null) { 264 matches = (matches != null ? matches : new ArrayList<>()); 265 matches.add(match); 266 } 267 } 268 if (matches == null) { 269 return Collections.emptyList(); 270 } 271 if (matches.size() > 1) { 272 matches.sort(this.pathMatcher.getPatternComparator(lookupPath)); 273 } 274 return matches; 275 } 276 277 @Nullable 278 private String getMatchingPattern(String pattern, String lookupPath) { 279 if (pattern.equals(lookupPath)) { 280 return pattern; 281 } 282 if (this.useSuffixPatternMatch) { 283 if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) { 284 for (String extension : this.fileExtensions) { 285 if (this.pathMatcher.match(pattern + extension, lookupPath)) { 286 return pattern + extension; 287 } 288 } 289 } 290 else { 291 boolean hasSuffix = pattern.indexOf('.') != -1; 292 if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { 293 return pattern + ".*"; 294 } 295 } 296 } 297 if (this.pathMatcher.match(pattern, lookupPath)) { 298 return pattern; 299 } 300 if (this.useTrailingSlashMatch) { 301 if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { 302 return pattern + "/"; 303 } 304 } 305 return null; 306 } 307 308 /** 309 * Compare the two conditions based on the URL patterns they contain. 310 * Patterns are compared one at a time, from top to bottom via 311 * {@link PathMatcher#getPatternComparator(String)}. If all compared 312 * patterns match equally, but one instance has more patterns, it is 313 * considered a closer match. 314 * <p>It is assumed that both instances have been obtained via 315 * {@link #getMatchingCondition(HttpServletRequest)} to ensure they 316 * contain only patterns that match the request and are sorted with 317 * the best matches on top. 318 */ 319 @Override 320 public int compareTo(PatternsRequestCondition other, HttpServletRequest request) { 321 String lookupPath = this.pathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); 322 Comparator<String> patternComparator = this.pathMatcher.getPatternComparator(lookupPath); 323 Iterator<String> iterator = this.patterns.iterator(); 324 Iterator<String> iteratorOther = other.patterns.iterator(); 325 while (iterator.hasNext() && iteratorOther.hasNext()) { 326 int result = patternComparator.compare(iterator.next(), iteratorOther.next()); 327 if (result != 0) { 328 return result; 329 } 330 } 331 if (iterator.hasNext()) { 332 return -1; 333 } 334 else if (iteratorOther.hasNext()) { 335 return 1; 336 } 337 else { 338 return 0; 339 } 340 } 341 342}