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