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; 014 015import static org.apache.juneau.internal.StringUtils.*; 016 017import java.util.*; 018import java.util.regex.*; 019 020import org.apache.juneau.rest.annotation.*; 021 022/** 023 * A parsed path pattern constructed from a {@link RestMethod#path() @RestMethod.path()} value. 024 * 025 * <p> 026 * Handles aspects of matching and precedence ordering. 027 */ 028public final class UrlPathPattern implements Comparable<UrlPathPattern> { 029 030 private final Pattern pattern; 031 private final String patternString; 032 private final boolean isOnlyDotAll, isDotAll; 033 private final String[] vars; 034 035 /** 036 * Constructor. 037 * 038 * @param patternString The raw pattern string from the {@link RestMethod#path() @RestMethod.path()} annotation. 039 */ 040 public UrlPathPattern(String patternString) { 041 this.patternString = patternString; 042 Builder b = new Builder(patternString); 043 pattern = b.pattern; 044 isDotAll = b.isDotAll; 045 isOnlyDotAll = b.isOnlyDotAll; 046 vars = b.vars.toArray(new String[b.vars.size()]); 047 } 048 049 private final class Builder { 050 boolean isDotAll, isOnlyDotAll; 051 Pattern pattern; 052 List<String> vars = new LinkedList<>(); 053 054 Builder(String patternString) { 055 if (! startsWith(patternString, '/')) 056 patternString = '/' + patternString; 057 if (patternString.equals("/*")) { 058 isOnlyDotAll = true; 059 return; 060 } 061 if (patternString.endsWith("/*")) 062 isDotAll = true; 063 064 // Find all {xxx} variables. 065 Pattern p = Pattern.compile("\\{([^\\}]+)\\}"); 066 Matcher m = p.matcher(patternString); 067 while (m.find()) 068 vars.add(m.group(1)); 069 070 patternString = patternString.replaceAll("\\{[^\\}]+\\}", "([^\\/]+)"); 071 patternString = patternString.replaceAll("\\/\\*$", "((?:)|(?:\\/.*))"); 072 pattern = Pattern.compile(patternString); 073 } 074 } 075 076 /** 077 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 078 * 079 * @param path The path to match against. 080 * @return 081 * An array of values matched against <js>"{var}"</js> variable in the pattern, or an empty array if the 082 * pattern matched but no vars were present, or <jk>null</jk> if the specified path didn't match the pattern. 083 */ 084 protected String[] match(String path) { 085 086 if (isOnlyDotAll) { 087 // Remainder always gets leading slash trimmed. 088 if (path != null) 089 path = path.substring(1); 090 return new String[]{path}; 091 } 092 093 if (path == null) 094 return (patternString.equals("/") ? new String[]{} : null); 095 096 // If we're not doing a /* match, ignore all trailing slashes. 097 if (! isDotAll) 098 while (path.length() > 1 && path.charAt(path.length()-1) == '/') 099 path = path.substring(0, path.length()-1); 100 101 Matcher m = pattern.matcher(path); 102 if (! m.matches()) 103 return null; 104 105 int len = m.groupCount(); 106 String[] v = new String[len]; 107 108 for (int i = 0; i < len; i++) { 109 if (isDotAll && i == len-1) 110 v[i] = m.group(i+1).isEmpty() ? null : m.group(i+1).substring(1); 111 else 112 v[i] = urlDecode(m.group(i+1)); 113 } 114 115 return v; 116 } 117 118 /** 119 * Comparator for this object. 120 * 121 * <p> 122 * The comparator is designed to order URL pattern from most-specific to least-specific. 123 * For example, the following patterns would be ordered as follows: 124 * <ol> 125 * <li><code>/foo/bar</code> 126 * <li><code>/foo/bar/*</code> 127 * <li><code>/foo/{id}/bar</code> 128 * <li><code>/foo/{id}/bar/*</code> 129 * <li><code>/foo/{id}</code> 130 * <li><code>/foo/{id}/*</code> 131 * <li><code>/foo</code> 132 * <li><code>/foo/*</code> 133 * </ol> 134 */ 135 @Override /* Comparable */ 136 public int compareTo(UrlPathPattern o) { 137 String s1 = patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W"); 138 String s2 = o.patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W"); 139 if (s1.isEmpty()) 140 s1 = "+"; 141 if (s2.isEmpty()) 142 s2 = "+"; 143 if (! s1.endsWith("/*")) 144 s1 = s1 + "/W"; 145 if (! s2.endsWith("/*")) 146 s2 = s2 + "/W"; 147 int c = s2.compareTo(s1); 148 if (c == 0) 149 return o.toRegEx().compareTo(toRegEx()); 150 return c; 151 } 152 153 @Override /* Object */ 154 public boolean equals(Object o) { 155 if (! (o instanceof UrlPathPattern)) 156 return false; 157 return (compareTo((UrlPathPattern)o) == 0); 158 } 159 160 @Override /* Object */ 161 public int hashCode() { 162 return super.hashCode(); 163 } 164 165 @Override /* Object */ 166 public String toString() { 167 return patternString; 168 } 169 170 /** 171 * Returns this path pattern as the compiled regular expression. 172 * 173 * <p> 174 * Useful for debugging. 175 * 176 * @return The path pattern. 177 */ 178 public String toRegEx() { 179 return isOnlyDotAll ? "*" : pattern.pattern(); 180 } 181 182 /** 183 * Bean property getter: <property>vars</property>. 184 * 185 * @return The value of the <property>vars</property> property on this bean, or <jk>null</jk> if it is not set. 186 */ 187 public String[] getVars() { 188 return vars; 189 } 190 191 /** 192 * Bean property getter: <property>patternString</property>. 193 * 194 * @return The value of the <property>patternString</property> property on this bean, or <jk>null</jk> if it is not set. 195 */ 196 public String getPatternString() { 197 return patternString; 198 } 199}