001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.util; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.FileUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023 024import java.util.*; 025import java.util.regex.*; 026 027import org.apache.juneau.rest.server.*; 028 029/** 030 * A parsed path pattern constructed from a {@link RestOp#path() @RestOp(path)} value. 031 * 032 * <p> 033 * Handles aspects of matching and precedence ordering. 034 * 035 */ 036@SuppressWarnings({ 037 "java:S2160" // equals() is inherited from UrlPathMatcher which compares by pattern string; added fields are derived from the pattern 038}) 039public abstract sealed class UrlPathMatcher implements Comparable<UrlPathMatcher> { 040 041 /** 042 * A file name pattern such as "favicon.ico" or "*.jsp". 043 */ 044 static final class FileNameMatcher extends UrlPathMatcher { 045 046 private final String basePattern; 047 private final String extPattern; 048 private final String comparator; 049 050 FileNameMatcher(String pattern) { 051 super(pattern); 052 var base = getBaseName(pattern); 053 var ext = getFileExtension(pattern); 054 basePattern = eq(base, "*") ? null : base; 055 extPattern = eq(ext, "*") ? null : ext; 056 this.comparator = pattern.replaceAll("\\w+", "X").replace("*", "W"); 057 } 058 059 @Override /* Overridden from UrlPathMatcher */ 060 public String getComparator() { return comparator; } 061 062 @Override /* Overridden from UrlPathMatcher */ 063 public UrlPathMatch match(UrlPath pathInfo) { 064 Optional<String> fileName = pathInfo.getFileName(); 065 if (fileName.isPresent()) { 066 var base = getBaseName(fileName.get()); 067 var ext = getFileExtension(fileName.get()); 068 if ((basePattern == null || eq(basePattern, base)) && (extPattern == null || eq(extPattern, ext))) 069 return new UrlPathMatch(pathInfo.getPath(), pathInfo.getParts().length, new String[0], new String[0]); 070 } 071 return null; 072 } 073 } 074 075 /** 076 * A dir name pattern such as "/foo" or "/*". 077 */ 078 static final class PathMatcher extends UrlPathMatcher { 079 private static final Pattern VAR_PATTERN = Pattern.compile("\\{([^\\}]+)\\}"); 080 081 private final String pattern; 082 private final String comparator; 083 private final String[] parts; 084 private final String[] vars; 085 private final String[] varKeys; 086 private final boolean hasRemainder; 087 088 PathMatcher(String patternString) { 089 super(patternString); 090 if (ie(patternString)) { 091 this.pattern = "/"; 092 } else if (patternString.charAt(0) != '/') { 093 this.pattern = '/' + patternString; 094 } else { 095 this.pattern = patternString; 096 } 097 098 var c = patternString.replaceAll("\\{[^{}]+\\}", ".").replaceAll("\\w+", "X").replace(".", "W"); 099 if (c.isEmpty()) 100 c = "+"; 101 if (! c.endsWith("/*")) 102 c = c + "/W"; 103 this.comparator = c; 104 105 String[] parts2 = new UrlPath(pattern).getParts(); 106 107 this.hasRemainder = parts2.length > 0 && eq(parts2[parts2.length - 1], "*"); 108 109 parts2 = hasRemainder ? Arrays.copyOf(parts2, parts2.length - 1) : parts2; 110 111 this.parts = parts2; 112 this.vars = new String[parts2.length]; 113 List<String> vars2 = list(); 114 115 for (var i = 0; i < parts2.length; i++) { 116 Matcher m = VAR_PATTERN.matcher(parts2[i]); 117 if (m.matches()) { 118 this.vars[i] = m.group(1); 119 vars2.add(this.vars[i]); 120 } 121 } 122 123 this.varKeys = vars2.isEmpty() ? null : vars2.toArray(new String[vars2.size()]); 124 } 125 126 @Override 127 public String getComparator() { return comparator; } 128 129 @Override 130 public String[] getVars() { return varKeys == null ? new String[0] : Arrays.copyOf(varKeys, varKeys.length); } 131 132 @Override 133 public boolean hasVars() { 134 return nn(varKeys); 135 } 136 137 /** 138 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 139 * 140 * @param urlPath The path to match against. 141 * @return 142 * A pattern match object, or <jk>null</jk> if the path didn't match this pattern. 143 */ 144 @SuppressWarnings({ 145 "java:S3776", // Cognitive complexity acceptable for this specific logic 146 "null" // Null handling verified by context or framework 147 }) 148 @Override 149 public UrlPathMatch match(UrlPath urlPath) { 150 151 String[] pip = urlPath.getParts(); 152 153 if (parts.length != pip.length) { 154 if (hasRemainder) { 155 if ((pip.length == parts.length - 1 && ! urlPath.isTrailingSlash()) || pip.length < parts.length) 156 return null; 157 } else { 158 if (pip.length != parts.length + 1 || ! urlPath.isTrailingSlash()) 159 return null; 160 } 161 } 162 163 for (var i = 0; i < parts.length; i++) 164 if (vars[i] == null && (pip.length <= i || ! (eq(parts[i], "*") || pip[i].equals(parts[i])))) 165 return null; 166 167 String[] vals = varKeys == null ? null : new String[varKeys.length]; 168 169 var j = 0; 170 if (nn(vals)) 171 for (var i = 0; i < parts.length; i++) 172 if (nn(vars[i])) 173 vals[j++] = pip[i]; 174 175 return new UrlPathMatch(urlPath.getPath(), parts.length, varKeys, vals); 176 } 177 } 178 179 /** 180 * Constructs a matcher from the specified pattern string. 181 * 182 * @param pattern The pattern string. Can be <jk>null</jk> (treated as an empty pattern). 183 * @return A new matcher. 184 */ 185 public static UrlPathMatcher of(String pattern) { 186 pattern = emptyIfNull(pattern); 187 boolean isFilePattern = pattern.matches("[^\\/\\.]+\\.[^\\/]+"); 188 return isFilePattern ? new FileNameMatcher(pattern) : new PathMatcher(pattern); 189 190 } 191 192 private final String pattern; 193 194 UrlPathMatcher(String pattern) { 195 this.pattern = pattern; 196 } 197 198 /** 199 * Comparator for this object. 200 * 201 * <p> 202 * The comparator is designed to order URL pattern from most-specific to least-specific. 203 * For example, the following patterns would be ordered as follows: 204 * <ol> 205 * <li><c>foo.bar</c> 206 * <li><c>*.bar</c> 207 * <li><c>/foo/bar</c> 208 * <li><c>/foo/bar/*</c> 209 * <li><c>/foo/{id}/bar</c> 210 * <li><c>/foo/{id}/bar/*</c> 211 * <li><c>/foo/{id}</c> 212 * <li><c>/foo/{id}/*</c> 213 * <li><c>/foo</c> 214 * <li><c>/foo/*</c> 215 * </ol> 216 */ 217 @Override /* Overridden from Comparable */ 218 public int compareTo(UrlPathMatcher o) { 219 return o.getComparator().compareTo(getComparator()); 220 } 221 222 @Override /* Overridden from Object */ 223 public boolean equals(Object o) { 224 return o instanceof UrlPathMatcher o2 && compareTo(o2) == 0; 225 } 226 227 @Override /* Overridden from Object */ 228 public int hashCode() { 229 return getComparator() != null ? getComparator().hashCode() : 0; 230 } 231 232 /** 233 * Returns the variable names found in the pattern. 234 * 235 * @return 236 * The variable names or an empty array if no variables found. 237 * <br>Modifying the returned array does not modify this object. 238 */ 239 public String[] getVars() { return new String[0]; } 240 241 /** 242 * Returns <jk>true</jk> if this path pattern contains variables. 243 * 244 * @return <jk>true</jk> if this path pattern contains variables. 245 */ 246 public boolean hasVars() { 247 return false; 248 } 249 250 /** 251 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 252 * 253 * @param pathInfo The path to match against. 254 * @return 255 * A pattern match object, or <jk>null</jk> if the path didn't match this pattern. 256 */ 257 public abstract UrlPathMatch match(UrlPath pathInfo); 258 259 @Override /* Overridden from Object */ 260 public String toString() { 261 return pattern; 262 } 263 264 /** 265 * Returns a string that can be used to compare this matcher with other matchers to provide the ability to 266 * order URL patterns from most-specific to least-specific. 267 * 268 * @return A comparison string. 269 */ 270 protected abstract String getComparator(); 271}