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.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.Utils.*; 022 023import java.util.*; 024import java.util.regex.*; 025 026import org.apache.juneau.rest.annotation.*; 027 028/** 029 * A parsed path pattern constructed from a {@link RestOp#path() @RestOp(path)} value. 030 * 031 * <p> 032 * Handles aspects of matching and precedence ordering. 033 * 034 */ 035public abstract class UrlPathMatcher implements Comparable<UrlPathMatcher> { 036 037 /** 038 * A file name pattern such as "favicon.ico" or "*.jsp". 039 */ 040 private static class FileNameMatcher extends UrlPathMatcher { 041 042 private final String basePattern, extPattern, comparator; 043 044 FileNameMatcher(String pattern) { 045 super(pattern); 046 var base = getBaseName(pattern); 047 var ext = getFileExtension(pattern); 048 basePattern = base.equals("*") ? null : base; 049 extPattern = ext.equals("*") ? null : ext; 050 this.comparator = pattern.replaceAll("\\w+", "X").replace("*", "W"); 051 } 052 053 @Override /* Overridden from UrlPathMatcher */ 054 public String getComparator() { return comparator; } 055 056 @Override /* Overridden from UrlPathMatcher */ 057 public UrlPathMatch match(UrlPath pathInfo) { 058 Optional<String> fileName = pathInfo.getFileName(); 059 if (fileName.isPresent()) { 060 var base = getBaseName(fileName.get()); 061 var ext = getFileExtension(fileName.get()); 062 if ((basePattern == null || basePattern.equals(base)) && (extPattern == null || extPattern.equals(ext))) 063 return new UrlPathMatch(pathInfo.getPath(), pathInfo.getParts().length, new String[0], new String[0]); 064 } 065 return null; 066 } 067 } 068 069 /** 070 * A dir name pattern such as "/foo" or "/*". 071 */ 072 private static class PathMatcher extends UrlPathMatcher { 073 private static final Pattern VAR_PATTERN = Pattern.compile("\\{([^\\}]+)\\}"); 074 075 private final String pattern, comparator; 076 private final String[] parts, vars, varKeys; 077 private final boolean hasRemainder; 078 079 PathMatcher(String patternString) { 080 super(patternString); 081 this.pattern = e(patternString) ? "/" : patternString.charAt(0) != '/' ? '/' + patternString : patternString; 082 083 var c = patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W"); 084 if (c.isEmpty()) 085 c = "+"; 086 if (! c.endsWith("/*")) 087 c = c + "/W"; 088 this.comparator = c; 089 090 String[] parts = new UrlPath(pattern).getParts(); 091 092 this.hasRemainder = parts.length > 0 && "*".equals(parts[parts.length - 1]); 093 094 parts = hasRemainder ? Arrays.copyOf(parts, parts.length - 1) : parts; 095 096 this.parts = parts; 097 this.vars = new String[parts.length]; 098 List<String> vars = list(); 099 100 for (var i = 0; i < parts.length; i++) { 101 Matcher m = VAR_PATTERN.matcher(parts[i]); 102 if (m.matches()) { 103 this.vars[i] = m.group(1); 104 vars.add(this.vars[i]); 105 } 106 } 107 108 this.varKeys = vars.isEmpty() ? null : vars.toArray(new String[vars.size()]); 109 } 110 111 @Override 112 public String getComparator() { return comparator; } 113 114 @Override 115 public String[] getVars() { return varKeys == null ? new String[0] : Arrays.copyOf(varKeys, varKeys.length); } 116 117 @Override 118 public boolean hasVars() { 119 return nn(varKeys); 120 } 121 122 /** 123 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 124 * 125 * @param urlPath The path to match against. 126 * @return 127 * A pattern match object, or <jk>null</jk> if the path didn't match this pattern. 128 */ 129 @SuppressWarnings("null") 130 @Override 131 public UrlPathMatch match(UrlPath urlPath) { 132 133 String[] pip = urlPath.getParts(); 134 135 if (parts.length != pip.length) { 136 if (hasRemainder) { 137 if (pip.length == parts.length - 1 && ! urlPath.isTrailingSlash()) 138 return null; 139 else if (pip.length < parts.length) 140 return null; 141 } else { 142 if (pip.length != parts.length + 1 || ! urlPath.isTrailingSlash()) 143 return null; 144 } 145 } 146 147 for (var i = 0; i < parts.length; i++) 148 if (vars[i] == null && (pip.length <= i || ! ("*".equals(parts[i]) || pip[i].equals(parts[i])))) 149 return null; 150 151 String[] vals = varKeys == null ? null : new String[varKeys.length]; 152 153 var j = 0; 154 if (nn(vals)) 155 for (var i = 0; i < parts.length; i++) 156 if (nn(vars[i])) 157 vals[j++] = pip[i]; 158 159 return new UrlPathMatch(urlPath.getPath(), parts.length, varKeys, vals); 160 } 161 } 162 163 /** 164 * Constructs a matcher from the specified pattern string. 165 * 166 * @param pattern The pattern string. 167 * @return A new matcher. 168 */ 169 public static UrlPathMatcher of(String pattern) { 170 pattern = emptyIfNull(pattern); 171 boolean isFilePattern = pattern.matches("[^\\/]+\\.[^\\/]+"); 172 return isFilePattern ? new FileNameMatcher(pattern) : new PathMatcher(pattern); 173 174 } 175 176 private final String pattern; 177 178 UrlPathMatcher(String pattern) { 179 this.pattern = pattern; 180 } 181 182 /** 183 * Comparator for this object. 184 * 185 * <p> 186 * The comparator is designed to order URL pattern from most-specific to least-specific. 187 * For example, the following patterns would be ordered as follows: 188 * <ol> 189 * <li><c>foo.bar</c> 190 * <li><c>*.bar</c> 191 * <li><c>/foo/bar</c> 192 * <li><c>/foo/bar/*</c> 193 * <li><c>/foo/{id}/bar</c> 194 * <li><c>/foo/{id}/bar/*</c> 195 * <li><c>/foo/{id}</c> 196 * <li><c>/foo/{id}/*</c> 197 * <li><c>/foo</c> 198 * <li><c>/foo/*</c> 199 * </ol> 200 */ 201 @Override /* Overridden from Comparable */ 202 public int compareTo(UrlPathMatcher o) { 203 return o.getComparator().compareTo(getComparator()); 204 } 205 206 /** 207 * Returns the variable names found in the pattern. 208 * 209 * @return 210 * The variable names or an empty array if no variables found. 211 * <br>Modifying the returned array does not modify this object. 212 */ 213 public String[] getVars() { return new String[0]; } 214 215 /** 216 * Returns <jk>true</jk> if this path pattern contains variables. 217 * 218 * @return <jk>true</jk> if this path pattern contains variables. 219 */ 220 public boolean hasVars() { 221 return false; 222 } 223 224 /** 225 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 226 * 227 * @param pathInfo The path to match against. 228 * @return 229 * A pattern match object, or <jk>null</jk> if the path didn't match this pattern. 230 */ 231 public abstract UrlPathMatch match(UrlPath pathInfo); 232 233 @Override /* Overridden from Object */ 234 public String toString() { 235 return pattern; 236 } 237 238 /** 239 * Returns a string that can be used to compare this matcher with other matchers to provide the ability to 240 * order URL patterns from most-specific to least-specific. 241 * 242 * @return A comparison string. 243 */ 244 protected abstract String getComparator(); 245}