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}