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.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.FileUtils.*;
021import static org.apache.juneau.commons.utils.Utils.*;
022
023import java.util.*;
024import java.util.regex.*;
025
026import org.apache.juneau.rest.annotation.*;
027
028/**
029 * A parsed path pattern constructed from a {@link RestOp#path() @RestOp(path)} value.
030 *
031 * <p>
032 * Handles aspects of matching and precedence ordering.
033 *
034 */
035public abstract class UrlPathMatcher implements Comparable<UrlPathMatcher> {
036
037   /**
038    * A file name pattern such as "favicon.ico" or "*.jsp".
039    */
040   private static class FileNameMatcher extends UrlPathMatcher {
041
042      private final String basePattern, extPattern, comparator;
043
044      FileNameMatcher(String pattern) {
045         super(pattern);
046         var base = getBaseName(pattern);
047         var ext = getFileExtension(pattern);
048         basePattern = base.equals("*") ? null : base;
049         extPattern = ext.equals("*") ? null : ext;
050         this.comparator = pattern.replaceAll("\\w+", "X").replace("*", "W");
051      }
052
053      @Override /* Overridden from UrlPathMatcher */
054      public String getComparator() { return comparator; }
055
056      @Override /* Overridden from UrlPathMatcher */
057      public UrlPathMatch match(UrlPath pathInfo) {
058         Optional<String> fileName = pathInfo.getFileName();
059         if (fileName.isPresent()) {
060            var base = getBaseName(fileName.get());
061            var ext = getFileExtension(fileName.get());
062            if ((basePattern == null || basePattern.equals(base)) && (extPattern == null || extPattern.equals(ext)))
063               return new UrlPathMatch(pathInfo.getPath(), pathInfo.getParts().length, new String[0], new String[0]);
064         }
065         return null;
066      }
067   }
068
069   /**
070    * A dir name pattern such as "/foo" or "/*".
071    */
072   private static class PathMatcher extends UrlPathMatcher {
073      private static final Pattern VAR_PATTERN = Pattern.compile("\\{([^\\}]+)\\}");
074
075      private final String pattern, comparator;
076      private final String[] parts, vars, varKeys;
077      private final boolean hasRemainder;
078
079      PathMatcher(String patternString) {
080         super(patternString);
081         this.pattern = e(patternString) ? "/" : patternString.charAt(0) != '/' ? '/' + patternString : patternString;
082
083         var c = patternString.replaceAll("\\{[^\\}]+\\}", ".").replaceAll("\\w+", "X").replaceAll("\\.", "W");
084         if (c.isEmpty())
085            c = "+";
086         if (! c.endsWith("/*"))
087            c = c + "/W";
088         this.comparator = c;
089
090         String[] parts = new UrlPath(pattern).getParts();
091
092         this.hasRemainder = parts.length > 0 && "*".equals(parts[parts.length - 1]);
093
094         parts = hasRemainder ? Arrays.copyOf(parts, parts.length - 1) : parts;
095
096         this.parts = parts;
097         this.vars = new String[parts.length];
098         List<String> vars = list();
099
100         for (var i = 0; i < parts.length; i++) {
101            Matcher m = VAR_PATTERN.matcher(parts[i]);
102            if (m.matches()) {
103               this.vars[i] = m.group(1);
104               vars.add(this.vars[i]);
105            }
106         }
107
108         this.varKeys = vars.isEmpty() ? null : vars.toArray(new String[vars.size()]);
109      }
110
111      @Override
112      public String getComparator() { return comparator; }
113
114      @Override
115      public String[] getVars() { return varKeys == null ? new String[0] : Arrays.copyOf(varKeys, varKeys.length); }
116
117      @Override
118      public boolean hasVars() {
119         return nn(varKeys);
120      }
121
122      /**
123       * Returns a non-<jk>null</jk> value if the specified path matches this pattern.
124       *
125       * @param urlPath The path to match against.
126       * @return
127       *    A pattern match object, or <jk>null</jk> if the path didn't match this pattern.
128       */
129      @SuppressWarnings("null")
130      @Override
131      public UrlPathMatch match(UrlPath urlPath) {
132
133         String[] pip = urlPath.getParts();
134
135         if (parts.length != pip.length) {
136            if (hasRemainder) {
137               if (pip.length == parts.length - 1 && ! urlPath.isTrailingSlash())
138                  return null;
139               else if (pip.length < parts.length)
140                  return null;
141            } else {
142               if (pip.length != parts.length + 1 || ! urlPath.isTrailingSlash())
143                  return null;
144            }
145         }
146
147         for (var i = 0; i < parts.length; i++)
148            if (vars[i] == null && (pip.length <= i || ! ("*".equals(parts[i]) || pip[i].equals(parts[i]))))
149               return null;
150
151         String[] vals = varKeys == null ? null : new String[varKeys.length];
152
153         var j = 0;
154         if (nn(vals))
155            for (var i = 0; i < parts.length; i++)
156               if (nn(vars[i]))
157                  vals[j++] = pip[i];
158
159         return new UrlPathMatch(urlPath.getPath(), parts.length, varKeys, vals);
160      }
161   }
162
163   /**
164    * Constructs a matcher from the specified pattern string.
165    *
166    * @param pattern The pattern string.
167    * @return A new matcher.
168    */
169   public static UrlPathMatcher of(String pattern) {
170      pattern = emptyIfNull(pattern);
171      boolean isFilePattern = pattern.matches("[^\\/]+\\.[^\\/]+");
172      return isFilePattern ? new FileNameMatcher(pattern) : new PathMatcher(pattern);
173
174   }
175
176   private final String pattern;
177
178   UrlPathMatcher(String pattern) {
179      this.pattern = pattern;
180   }
181
182   /**
183    * Comparator for this object.
184    *
185    * <p>
186    * The comparator is designed to order URL pattern from most-specific to least-specific.
187    * For example, the following patterns would be ordered as follows:
188    * <ol>
189    *    <li><c>foo.bar</c>
190    *    <li><c>*.bar</c>
191    *    <li><c>/foo/bar</c>
192    *    <li><c>/foo/bar/*</c>
193    *    <li><c>/foo/{id}/bar</c>
194    *    <li><c>/foo/{id}/bar/*</c>
195    *    <li><c>/foo/{id}</c>
196    *    <li><c>/foo/{id}/*</c>
197    *    <li><c>/foo</c>
198    *    <li><c>/foo/*</c>
199    * </ol>
200    */
201   @Override /* Overridden from Comparable */
202   public int compareTo(UrlPathMatcher o) {
203      return o.getComparator().compareTo(getComparator());
204   }
205
206   /**
207    * Returns the variable names found in the pattern.
208    *
209    * @return
210    *    The variable names or an empty array if no variables found.
211    * <br>Modifying the returned array does not modify this object.
212    */
213   public String[] getVars() { return new String[0]; }
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    * Returns a non-<jk>null</jk> value if the specified path matches this pattern.
226    *
227    * @param pathInfo The path to match against.
228    * @return
229    *    A pattern match object, or <jk>null</jk> if the path didn't match this pattern.
230    */
231   public abstract UrlPathMatch match(UrlPath pathInfo);
232
233   @Override /* Overridden from Object */
234   public String toString() {
235      return pattern;
236   }
237
238   /**
239    * Returns a string that can be used to compare this matcher with other matchers to provide the ability to
240    * order URL patterns from most-specific to least-specific.
241    *
242    * @return A comparison string.
243    */
244   protected abstract String getComparator();
245}