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