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.internal.StringUtils.*; 016 017import java.util.*; 018import java.util.regex.*; 019 020import org.apache.juneau.annotation.*; 021import org.apache.juneau.rest.annotation.*; 022 023/** 024 * A parsed path pattern constructed from a {@link RestMethod#path() @RestMethod(path)} value. 025 * 026 * <p> 027 * Handles aspects of matching and precedence ordering. 028 */ 029@BeanIgnore 030public final class UrlPathPattern implements Comparable<UrlPathPattern> { 031 032 private static final Pattern VAR_PATTERN = Pattern.compile("\\{([^\\}]+)\\}"); 033 034 private final String pattern, comparator; 035 private final String[] parts, vars, varKeys; 036 private final boolean hasRemainder; 037 038 /** 039 * Constructor. 040 * 041 * @param patternString The raw pattern string from the {@link RestMethod#path() @RestMethod(path)} annotation. 042 */ 043 public UrlPathPattern(String patternString) { 044 this.pattern = isEmpty(patternString) ? "/" : patternString.charAt(0) != '/' ? '/' + patternString : patternString; 045 046 String c = patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W"); 047 if (c.isEmpty()) 048 c = "+"; 049 if (! c.endsWith("/*")) 050 c = c + "/W"; 051 this.comparator = c; 052 053 String[] parts = new UrlPathInfo(pattern).getParts(); 054 055 this.hasRemainder = parts.length > 0 && "*".equals(parts[parts.length-1]); 056 057 parts = hasRemainder ? Arrays.copyOf(parts, parts.length-1) : parts; 058 059 this.parts = parts; 060 this.vars = new String[parts.length]; 061 List<String> vars = new ArrayList<>(); 062 063 for (int i = 0; i < parts.length; i++) { 064 Matcher m = VAR_PATTERN.matcher(parts[i]); 065 if (m.matches()) { 066 this.vars[i] = m.group(1); 067 vars.add(this.vars[i]); 068 } 069 } 070 071 this.varKeys = vars.isEmpty() ? null : vars.toArray(new String[vars.size()]); 072 } 073 074 /** 075 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 076 * 077 * @param path The path to match against. 078 * @return 079 * A pattern match object, or <jk>null</jk> if the path didn't match this pattern. 080 */ 081 public UrlPathPatternMatch match(String path) { 082 return match(new UrlPathInfo(path)); 083 } 084 085 /** 086 * Returns a non-<jk>null</jk> value if the specified path matches this pattern. 087 * 088 * @param pathInfo The path to match against. 089 * @return 090 * A pattern match object, or <jk>null</jk> if the path didn't match this pattern. 091 */ 092 public UrlPathPatternMatch match(UrlPathInfo pathInfo) { 093 094 String[] pip = pathInfo.getParts(); 095 096 if (parts.length != pip.length) { 097 if (hasRemainder) { 098 if (pip.length == parts.length - 1 && ! pathInfo.isTrailingSlash()) 099 return null; 100 else if (pip.length < parts.length) 101 return null; 102 } else { 103 if (pip.length != parts.length + 1) 104 return null; 105 if (! pathInfo.isTrailingSlash()) 106 return null; 107 } 108 } 109 110 for (int i = 0; i < parts.length; i++) 111 if (vars[i] == null && (pip.length <= i || ! ("*".equals(parts[i]) || pip[i].equals(parts[i])))) 112 return null; 113 114 String[] vals = varKeys == null ? null : new String[varKeys.length]; 115 116 int j = 0; 117 if (vals != null) 118 for (int i = 0; i < parts.length; i++) 119 if (vars[i] != null) 120 vals[j++] = pip[i]; 121 122 return new UrlPathPatternMatch(pathInfo.getPath(), parts.length, varKeys, vals); 123 } 124 125 /** 126 * Returns the variable names found in the pattern. 127 * 128 * @return 129 * The variable names or an empty array if no variables found. 130 * <br>Modifying the returned array does not modify this object. 131 */ 132 public String[] getVars() { 133 return varKeys == null ? new String[0] : Arrays.copyOf(varKeys, varKeys.length); 134 } 135 136 /** 137 * Returns <jk>true</jk> if this path pattern contains variables. 138 * 139 * @return <jk>true</jk> if this path pattern contains variables. 140 */ 141 public boolean hasVars() { 142 return varKeys != null; 143 } 144 145 /** 146 * Comparator for this object. 147 * 148 * <p> 149 * The comparator is designed to order URL pattern from most-specific to least-specific. 150 * For example, the following patterns would be ordered as follows: 151 * <ol> 152 * <li><c>/foo/bar</c> 153 * <li><c>/foo/bar/*</c> 154 * <li><c>/foo/{id}/bar</c> 155 * <li><c>/foo/{id}/bar/*</c> 156 * <li><c>/foo/{id}</c> 157 * <li><c>/foo/{id}/*</c> 158 * <li><c>/foo</c> 159 * <li><c>/foo/*</c> 160 * </ol> 161 */ 162 @Override /* Comparable */ 163 public int compareTo(UrlPathPattern o) { 164 return o.comparator.compareTo(comparator); 165 } 166 167 @Override /* Object */ 168 public String toString() { 169 return pattern.toString(); 170 } 171}