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.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   public 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}