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.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.Iterator; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Set; 028import javax.servlet.http.HttpServletRequest; 029 030import org.springframework.util.AntPathMatcher; 031import org.springframework.util.PathMatcher; 032import org.springframework.util.StringUtils; 033import org.springframework.web.util.UrlPathHelper; 034 035/** 036 * A logical disjunction (' || ') request condition that matches a request 037 * against a set of URL path patterns. 038 * 039 * @author Rossen Stoyanchev 040 * @since 3.1 041 */ 042public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> { 043 044 private final Set<String> patterns; 045 046 private final UrlPathHelper pathHelper; 047 048 private final PathMatcher pathMatcher; 049 050 private final boolean useSuffixPatternMatch; 051 052 private final boolean useTrailingSlashMatch; 053 054 private final List<String> fileExtensions = new ArrayList<String>(); 055 056 057 /** 058 * Creates a new instance with the given URL patterns. 059 * Each pattern that is not empty and does not start with "/" is prepended with "/". 060 * @param patterns 0 or more URL patterns; if 0 the condition will match to every request. 061 */ 062 public PatternsRequestCondition(String... patterns) { 063 this(asList(patterns), null, null, true, true, null); 064 } 065 066 /** 067 * Additional constructor with flags for using suffix pattern (.*) and 068 * trailing slash matches. 069 * @param patterns the URL patterns to use; if 0, the condition will match to every request. 070 * @param urlPathHelper for determining the lookup path of a request 071 * @param pathMatcher for path matching with patterns 072 * @param useSuffixPatternMatch whether to enable matching by suffix (".*") 073 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash 074 */ 075 public PatternsRequestCondition(String[] patterns, UrlPathHelper urlPathHelper, PathMatcher pathMatcher, 076 boolean useSuffixPatternMatch, boolean useTrailingSlashMatch) { 077 078 this(asList(patterns), urlPathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, null); 079 } 080 081 /** 082 * Creates a new instance with the given URL patterns. 083 * Each pattern that is not empty and does not start with "/" is pre-pended with "/". 084 * @param patterns the URL patterns to use; if 0, the condition will match to every request. 085 * @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request 086 * @param pathMatcher a {@link PathMatcher} for pattern path matching 087 * @param useSuffixPatternMatch whether to enable matching by suffix (".*") 088 * @param useTrailingSlashMatch whether to match irrespective of a trailing slash 089 * @param fileExtensions a list of file extensions to consider for path matching 090 */ 091 public PatternsRequestCondition(String[] patterns, UrlPathHelper urlPathHelper, 092 PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, 093 List<String> fileExtensions) { 094 095 this(asList(patterns), urlPathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, fileExtensions); 096 } 097 098 /** 099 * 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 : UrlPathHelper.defaultInstance); 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, 183 this.useSuffixPatternMatch, 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 if (this.patterns.isEmpty()) { 205 return this; 206 } 207 String lookupPath = this.pathHelper.getLookupPathForRequest(request); 208 List<String> matches = getMatchingPatterns(lookupPath); 209 return (!matches.isEmpty() ? 210 new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, 211 this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null); 212 } 213 214 /** 215 * Find the patterns matching the given lookup path. Invoking this method should 216 * yield results equivalent to those of calling 217 * {@link #getMatchingCondition(javax.servlet.http.HttpServletRequest)}. 218 * This method is provided as an alternative to be used if no request is available 219 * (e.g. introspection, tooling, etc). 220 * @param lookupPath the lookup path to match to existing patterns 221 * @return a collection of matching patterns sorted with the closest match at the top 222 */ 223 public List<String> getMatchingPatterns(String lookupPath) { 224 List<String> matches = new ArrayList<String>(); 225 for (String pattern : this.patterns) { 226 String match = getMatchingPattern(pattern, lookupPath); 227 if (match != null) { 228 matches.add(match); 229 } 230 } 231 Collections.sort(matches, this.pathMatcher.getPatternComparator(lookupPath)); 232 return matches; 233 } 234 235 private String getMatchingPattern(String pattern, String lookupPath) { 236 if (pattern.equals(lookupPath)) { 237 return pattern; 238 } 239 if (this.useSuffixPatternMatch) { 240 if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) { 241 for (String extension : this.fileExtensions) { 242 if (this.pathMatcher.match(pattern + extension, lookupPath)) { 243 return pattern + extension; 244 } 245 } 246 } 247 else { 248 boolean hasSuffix = pattern.indexOf('.') != -1; 249 if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { 250 return pattern + ".*"; 251 } 252 } 253 } 254 if (this.pathMatcher.match(pattern, lookupPath)) { 255 return pattern; 256 } 257 if (this.useTrailingSlashMatch) { 258 if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { 259 return pattern + "/"; 260 } 261 } 262 return null; 263 } 264 265 /** 266 * Compare the two conditions based on the URL patterns they contain. 267 * Patterns are compared one at a time, from top to bottom via 268 * {@link PathMatcher#getPatternComparator(String)}. If all compared 269 * patterns match equally, but one instance has more patterns, it is 270 * considered a closer match. 271 * <p>It is assumed that both instances have been obtained via 272 * {@link #getMatchingCondition(HttpServletRequest)} to ensure they 273 * contain only patterns that match the request and are sorted with 274 * the best matches on top. 275 */ 276 @Override 277 public int compareTo(PatternsRequestCondition other, HttpServletRequest request) { 278 String lookupPath = this.pathHelper.getLookupPathForRequest(request); 279 Comparator<String> patternComparator = this.pathMatcher.getPatternComparator(lookupPath); 280 Iterator<String> iterator = this.patterns.iterator(); 281 Iterator<String> iteratorOther = other.patterns.iterator(); 282 while (iterator.hasNext() && iteratorOther.hasNext()) { 283 int result = patternComparator.compare(iterator.next(), iteratorOther.next()); 284 if (result != 0) { 285 return result; 286 } 287 } 288 if (iterator.hasNext()) { 289 return -1; 290 } 291 else if (iteratorOther.hasNext()) { 292 return 1; 293 } 294 else { 295 return 0; 296 } 297 } 298 299}