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