001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.util;
018
019import static org.apache.juneau.common.utils.Utils.*;
020import static org.apache.juneau.internal.FileUtils.*;
021
022import java.util.*;
023import java.util.regex.*;
024
025import org.apache.juneau.rest.annotation.*;
026
027/**
028 * A parsed path pattern constructed from a {@link RestOp#path() @RestOp(path)} value.
029 *
030 * <p>
031 * Handles aspects of matching and precedence ordering.
032 *
033 * <h5 class='section'>See Also:</h5><ul>
034
035 * </ul>
036 */
037public abstract class UrlPathMatcher implements Comparable<UrlPathMatcher> {
038
039   /**
040    * Constructs a matcher from the specified pattern string.
041    *
042    * @param pattern The pattern string.
043    * @return A new matcher.
044    */
045   public static UrlPathMatcher of(String pattern) {
046      pattern = emptyIfNull(pattern);
047      boolean isFilePattern = pattern.matches("[^\\/]+\\.[^\\/]+");
048      return isFilePattern ? new FileNameMatcher(pattern) : new PathMatcher(pattern);
049
050   }
051
052   private final String pattern;
053
054   UrlPathMatcher(String pattern) {
055      this.pattern = pattern;
056   }
057
058   /**
059    * A file name pattern such as "favicon.ico" or "*.jsp".
060    */
061   private static class FileNameMatcher extends UrlPathMatcher {
062
063      private final String basePattern, extPattern, comparator;
064
065      FileNameMatcher(String pattern) {
066         super(pattern);
067         String base = getBaseName(pattern), ext = getExtension(pattern);
068         basePattern = base.equals("*") ? null : base;
069         extPattern = ext.equals("*") ? null : ext;
070         this.comparator = pattern.replaceAll("\\w+", "X").replace("*", "W");
071      }
072
073      @Override /* UrlPathMatcher */
074      public UrlPathMatch match(UrlPath pathInfo) {
075         Optional<String> fileName = pathInfo.getFileName();
076         if (fileName.isPresent()) {
077            String base = getBaseName(fileName.get()), ext = getExtension(fileName.get());
078            if ((basePattern == null || basePattern.equals(base)) && (extPattern == null || extPattern.equals(ext)))
079               return new UrlPathMatch(pathInfo.getPath(), pathInfo.getParts().length, new String[0], new String[0]);
080         }
081         return null;
082      }
083
084      @Override /* UrlPathMatcher */
085      public String getComparator() {
086         return comparator;
087      }
088   }
089
090   /**
091    * A dir name pattern such as "/foo" or "/*".
092    */
093   private static class PathMatcher extends UrlPathMatcher {
094      private static final Pattern VAR_PATTERN = Pattern.compile("\\{([^\\}]+)\\}");
095
096      private final String pattern, comparator;
097      private final String[] parts, vars, varKeys;
098      private final boolean hasRemainder;
099
100      PathMatcher(String patternString) {
101         super(patternString);
102         this.pattern = isEmpty(patternString) ? "/" : patternString.charAt(0) != '/' ? '/' + patternString : patternString;
103
104         String c = patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W");
105         if (c.isEmpty())
106            c = "+";
107         if (! c.endsWith("/*"))
108            c = c + "/W";
109         this.comparator = c;
110
111         String[] parts = new UrlPath(pattern).getParts();
112
113         this.hasRemainder = parts.length > 0 && "*".equals(parts[parts.length-1]);
114
115         parts = hasRemainder ? Arrays.copyOf(parts, parts.length-1) : parts;
116
117         this.parts = parts;
118         this.vars = new String[parts.length];
119         List<String> vars = list();
120
121         for (int i = 0; i < parts.length; i++) {
122            Matcher m = VAR_PATTERN.matcher(parts[i]);
123            if (m.matches()) {
124               this.vars[i] = m.group(1);
125               vars.add(this.vars[i]);
126            }
127         }
128
129         this.varKeys = vars.isEmpty() ? null : vars.toArray(new String[vars.size()]);
130      }
131
132      /**
133       * Returns a non-<jk>null</jk> value if the specified path matches this pattern.
134       *
135       * @param urlPath The path to match against.
136       * @return
137       *    A pattern match object, or <jk>null</jk> if the path didn't match this pattern.
138       */
139      @Override
140      public UrlPathMatch match(UrlPath urlPath) {
141
142         String[] pip = urlPath.getParts();
143
144         if (parts.length != pip.length) {
145            if (hasRemainder) {
146               if (pip.length == parts.length - 1 && ! urlPath.isTrailingSlash())
147                  return null;
148               else if (pip.length < parts.length)
149                  return null;
150            } else {
151               if (pip.length != parts.length + 1 || ! urlPath.isTrailingSlash())
152                  return null;
153            }
154         }
155
156         for (int i = 0; i < parts.length; i++)
157            if (vars[i] == null && (pip.length <= i || ! ("*".equals(parts[i]) || pip[i].equals(parts[i]))))
158               return null;
159
160         String[] vals = varKeys == null ? null : new String[varKeys.length];
161
162         int j = 0;
163         if (vals != null)
164            for (int i = 0; i < parts.length; i++)
165               if (vars[i] != null)
166                  vals[j++] = pip[i];
167
168         return new UrlPathMatch(urlPath.getPath(), parts.length, varKeys, vals);
169      }
170
171      @Override
172      public String[] getVars() {
173         return varKeys == null ? new String[0] : Arrays.copyOf(varKeys, varKeys.length);
174      }
175
176      @Override
177      public boolean hasVars() {
178         return varKeys != null;
179      }
180
181      @Override
182      public String getComparator() {
183         return comparator;
184      }
185   }
186
187   /**
188    * Returns a non-<jk>null</jk> value if the specified path matches this pattern.
189    *
190    * @param pathInfo The path to match against.
191    * @return
192    *    A pattern match object, or <jk>null</jk> if the path didn't match this pattern.
193    */
194   public abstract UrlPathMatch match(UrlPath pathInfo);
195
196   /**
197    * Returns a string that can be used to compare this matcher with other matchers to provide the ability to
198    * order URL patterns from most-specific to least-specific.
199    *
200    * @return A comparison string.
201    */
202   protected abstract String getComparator();
203
204   /**
205    * Returns the variable names found in the pattern.
206    *
207    * @return
208    *    The variable names or an empty array if no variables found.
209    * <br>Modifying the returned array does not modify this object.
210    */
211   public String[] getVars() {
212      return new String[0];
213   }
214
215   /**
216    * Returns <jk>true</jk> if this path pattern contains variables.
217    *
218    * @return <jk>true</jk> if this path pattern contains variables.
219    */
220   public boolean hasVars() {
221      return false;
222   }
223
224   /**
225    * Comparator for this object.
226    *
227    * <p>
228    * The comparator is designed to order URL pattern from most-specific to least-specific.
229    * For example, the following patterns would be ordered as follows:
230    * <ol>
231    *    <li><c>foo.bar</c>
232    *    <li><c>*.bar</c>
233    *    <li><c>/foo/bar</c>
234    *    <li><c>/foo/bar/*</c>
235    *    <li><c>/foo/{id}/bar</c>
236    *    <li><c>/foo/{id}/bar/*</c>
237    *    <li><c>/foo/{id}</c>
238    *    <li><c>/foo/{id}/*</c>
239    *    <li><c>/foo</c>
240    *    <li><c>/foo/*</c>
241    * </ol>
242    */
243   @Override /* Comparable */
244   public int compareTo(UrlPathMatcher o) {
245      return o.getComparator().compareTo(getComparator());
246   }
247
248   @Override /* Object */
249   public String toString() {
250      return pattern;
251   }
252}