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}