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.StringUtils.*;
020import static org.apache.juneau.common.utils.ThrowableUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022import static org.apache.juneau.internal.ArrayUtils.*;
023import static org.apache.juneau.internal.CollectionUtils.*;
024import static org.apache.juneau.internal.CollectionUtils.map;
025
026import java.io.*;
027import java.util.*;
028import java.util.regex.*;
029
030import org.apache.juneau.*;
031import org.apache.juneau.common.utils.*;
032import org.apache.juneau.json.*;
033import org.apache.juneau.parser.*;
034import org.apache.juneau.rest.annotation.*;
035import org.apache.juneau.uon.*;
036
037import jakarta.servlet.http.*;
038
039/**
040 * Various reusable utility methods.
041 *
042 * <h5 class='section'>See Also:</h5><ul>
043
044 * </ul>
045 */
046public class RestUtils {
047
048   /**
049    * Returns readable text for an HTTP response code.
050    *
051    * @param rc The HTTP response code.
052    * @return Readable text for an HTTP response code, or <jk>null</jk> if it's an invalid code.
053    */
054   public static String getHttpResponseText(int rc) {
055      return httpMsgs.get(rc);
056   }
057
058   private static Map<Integer,String> httpMsgs = mapBuilder(Integer.class, String.class)
059      .add(100, "Continue")
060      .add(101, "Switching Protocols")
061      .add(102, "Processing")
062      .add(103, "Early Hints")
063      .add(200, "OK")
064      .add(201, "Created")
065      .add(202, "Accepted")
066      .add(203, "Non-Authoritative Information")
067      .add(204, "No Content")
068      .add(205, "Reset Content")
069      .add(206, "Partial Content")
070      .add(300, "Multiple Choices")
071      .add(301, "Moved Permanently")
072      .add(302, "Temporary Redirect")
073      .add(303, "See Other")
074      .add(304, "Not Modified")
075      .add(305, "Use Proxy")
076      .add(307, "Temporary Redirect")
077      .add(400, "Bad Request")
078      .add(401, "Unauthorized")
079      .add(402, "Payment Required")
080      .add(403, "Forbidden")
081      .add(404, "Not Found")
082      .add(405, "Method Not Allowed")
083      .add(406, "Not Acceptable")
084      .add(407, "Proxy Authentication Required")
085      .add(408, "Request Time-Out")
086      .add(409, "Conflict")
087      .add(410, "Gone")
088      .add(411, "Length Required")
089      .add(412, "Precondition Failed")
090      .add(413, "Request Entity Too Large")
091      .add(414, "Request-URI Too Large")
092      .add(415, "Unsupported Media Type")
093      .add(500, "Internal Server Error")
094      .add(501, "Not Implemented")
095      .add(502, "Bad Gateway")
096      .add(503, "Service Unavailable")
097      .add(504, "Gateway Timeout")
098      .add(505, "HTTP Version Not Supported")
099      .build()
100   ;
101
102   /**
103    * Identical to {@link HttpServletRequest#getPathInfo()} but doesn't decode encoded characters.
104    *
105    * @param req The HTTP request
106    * @return The un-decoded path info.
107    */
108   public static String getPathInfoUndecoded(HttpServletRequest req) {
109      String requestURI = req.getRequestURI();
110      String contextPath = req.getContextPath();
111      String servletPath = req.getServletPath();
112      int l = contextPath.length() + servletPath.length();
113      if (requestURI.length() == l)
114         return null;
115      return requestURI.substring(l);
116   }
117
118   /**
119    * Efficiently trims the path info part from a request URI.
120    *
121    * <p>
122    * The result is the URI of the servlet itself.
123    *
124    * @param requestURI The value returned by {@link HttpServletRequest#getRequestURL()}
125    * @param contextPath The value returned by {@link HttpServletRequest#getContextPath()}
126    * @param servletPath The value returned by {@link HttpServletRequest#getServletPath()}
127    * @return The same StringBuilder with remainder trimmed.
128    */
129   public static StringBuffer trimPathInfo(StringBuffer requestURI, String contextPath, String servletPath) {
130      if (servletPath.equals("/"))
131         servletPath = "";
132      if (contextPath.equals("/"))
133         contextPath = "";
134
135      try {
136         // Given URL:  http://hostname:port/servletPath/extra
137         // We want:    http://hostname:port/servletPath
138         int sc = 0;
139         for (int i = 0; i < requestURI.length(); i++) {
140            char c = requestURI.charAt(i);
141            if (c == '/') {
142               sc++;
143               if (sc == 3) {
144                  if (servletPath.isEmpty()) {
145                     requestURI.setLength(i);
146                     return requestURI;
147                  }
148
149                  // Make sure context path follows the authority.
150                  for (int j = 0; j < contextPath.length(); i++, j++)
151                     if (requestURI.charAt(i) != contextPath.charAt(j))
152                        throw new Exception("case=1");
153
154                  // Make sure servlet path follows the authority.
155                  for (int j = 0; j < servletPath.length(); i++, j++)
156                     if (requestURI.charAt(i) != servletPath.charAt(j))
157                        throw new Exception("case=2");
158
159                  // Make sure servlet path isn't a false match (e.g. /foo2 should not match /foo)
160                  c = (requestURI.length() == i ? '/' : requestURI.charAt(i));
161                  if (c == '/' || c == '?') {
162                     requestURI.setLength(i);
163                     return requestURI;
164                  }
165
166                  throw new Exception("case=3");
167               }
168            } else if (c == '?') {
169               if (sc != 2)
170                  throw new Exception("case=4");
171               if (servletPath.isEmpty()) {
172                  requestURI.setLength(i);
173                  return requestURI;
174               }
175               throw new Exception("case=5");
176            }
177         }
178         if (servletPath.isEmpty())
179            return requestURI;
180         throw new Exception("case=6");
181      } catch (Exception e) {
182         throw new BasicRuntimeException(e, "Could not find servlet path in request URI.  URI=''{0}'', servletPath=''{1}''", requestURI, servletPath);
183      }
184   }
185
186   /**
187    * Parses HTTP header.
188    *
189    * @param s The string to parse.
190    * @return The parsed string.
191    */
192   public static String[] parseHeader(String s) {
193      int i = s.indexOf(':');
194      if (i == -1)
195         i = s.indexOf('=');
196      if (i == -1)
197         return null;
198      String name = s.substring(0, i).trim().toLowerCase(Locale.ENGLISH);
199      String val = s.substring(i+1).trim();
200      return new String[]{name,val};
201   }
202
203   /**
204    * Parses key/value pairs separated by either : or =
205    *
206    * @param s The string to parse.
207    * @return The parsed string.
208    */
209   public static String[] parseKeyValuePair(String s) {
210      int i = -1;
211      for (int j = 0; j < s.length() && i < 0; j++) {
212         char c = s.charAt(j);
213         if (c == '=' || c == ':')
214            i = j;
215      }
216      if (i == -1)
217         return null;
218      String name = s.substring(0, i).trim();
219      String val = s.substring(i+1).trim();
220      return new String[]{name,val};
221   }
222
223   static String resolveNewlineSeparatedAnnotation(String[] value, String fromParent) {
224      if (value.length == 0)
225         return fromParent;
226
227      List<String> l = list();
228      for (String v : value) {
229         if (! "INHERIT".equals(v))
230            l.add(v);
231         else if (fromParent != null)
232            l.add(fromParent);
233      }
234      return Utils.join(l, '\n');
235   }
236
237   private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)");
238
239   static String[] resolveLinks(String[] links, String[] parentLinks) {
240      if (links.length == 0)
241         return parentLinks;
242
243      List<String> list = list();
244      for (String l : links) {
245         if ("INHERIT".equals(l))
246            addAll(list, parentLinks);
247         else if (l.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(l).matches()) {
248            Matcher lm = INDEXED_LINK_PATTERN.matcher(l);
249            lm.matches();
250            String key = lm.group(1);
251            int index = Math.min(list.size(), Integer.parseInt(lm.group(2)));
252            String remainder = lm.group(3);
253            list.add(index, key.isEmpty() ? remainder : key + ":" + remainder);
254         } else {
255            list.add(l);
256         }
257      }
258      return Utils.array(list, String.class);
259   }
260
261   static String[] resolveContent(String[] content, String[] parentContent) {
262      if (content.length == 0)
263         return parentContent;
264
265      List<String> list = list();
266      for (String l : content) {
267         if ("INHERIT".equals(l)) {
268            addAll(list, parentContent);
269         } else if ("NONE".equals(l)) {
270            return new String[0];
271         } else {
272            list.add(l);
273         }
274      }
275      return Utils.array(list, String.class);
276   }
277
278   /**
279    * Parses a URL query string or form-data content.
280    *
281    * @param qs A reader or string containing the query string to parse.
282    * @return A new map containing the parsed query.
283    */
284   public static Map<String,String[]> parseQuery(Object qs) {
285      return parseQuery(qs, null);
286   }
287
288   /**
289    * Same as {@link #parseQuery(Object)} but allows you to specify the map to insert values into.
290    *
291    * @param qs A reader containing the query string to parse.
292    * @param map The map to pass the values into.
293    * @return The same map passed in, or a new map if it was <jk>null</jk>.
294    */
295   public static Map<String,String[]> parseQuery(Object qs, Map<String,String[]> map) {
296
297      try {
298         Map<String,String[]> m = map;
299         if (m == null)
300            m = map();
301
302         if (qs == null || ((qs instanceof CharSequence) && Utils.isEmpty(Utils.s(qs))))
303            return m;
304
305         try (ParserPipe p = new ParserPipe(qs)) {
306
307            final int S1=1; // Looking for attrName start.
308            final int S2=2; // Found attrName start, looking for = or & or end.
309            final int S3=3; // Found =, looking for valStart or &.
310            final int S4=4; // Found valStart, looking for & or end.
311
312            try (UonReader r = new UonReader(p, true)) {
313               int c = r.peekSkipWs();
314               if (c == '?')
315                  r.read();
316
317               int state = S1;
318               String currAttr = null;
319               while (c != -1) {
320                  c = r.read();
321                  if (state == S1) {
322                     if (c != -1) {
323                        r.unread();
324                        r.mark();
325                        state = S2;
326                     }
327                  } else if (state == S2) {
328                     if (c == -1) {
329                        add(m, r.getMarked(), null);
330                     } else if (c == '\u0001') {
331                        m.put(r.getMarked(0,-1), null);
332                        state = S1;
333                     } else if (c == '\u0002') {
334                        currAttr = r.getMarked(0,-1);
335                        state = S3;
336                     }
337                  } else if (state == S3) {
338                     if (c == -1 || c == '\u0001') {
339                        add(m, currAttr, "");
340                        state = S1;
341                     } else {
342                        if (c == '\u0002')
343                           r.replace('=');
344                        r.unread();
345                        r.mark();
346                        state = S4;
347                     }
348                  } else if (state == S4) {
349                     if (c == -1) {
350                        add(m, currAttr, r.getMarked());
351                     } else if (c == '\u0001') {
352                        add(m, currAttr, r.getMarked(0,-1));
353                        state = S1;
354                     } else if (c == '\u0002') {
355                        r.replace('=');
356                     }
357                  }
358               }
359            }
360
361            return m;
362         }
363      } catch (IOException e) {
364         throw asRuntimeException(e); // Should never happen.
365      }
366   }
367
368   private static void add(Map<String,String[]> m, String key, String val) {
369      boolean b = m.containsKey(key);
370      if (val == null) {
371         if (! b)
372            m.put(key, null);
373      } else if (b && m.get(key) != null) {
374         m.put(key, append(m.get(key), val));
375      } else {
376         m.put(key, new String[]{val});
377      }
378   }
379
380   /**
381    * Parses a string that can consist of a simple string or JSON object/array.
382    *
383    * @param s The string to parse.
384    * @return The parsed value, or <jk>null</jk> if the input is null.
385    * @throws ParseException Invalid JSON in string.
386    */
387   public static Object parseAnything(String s) throws ParseException {
388      if (isJson(s))
389         return JsonParser.DEFAULT.parse(s, Object.class);
390      return s;
391   }
392
393   /**
394    * If the specified path-info starts with the specified context path, trims the context path from the path info.
395    *
396    * @param contextPath The context path.
397    * @param path The URL path.
398    * @return The path following the context path, or the original path.
399    */
400   public static String trimContextPath(String contextPath, String path) {
401      if (path == null)
402         return null;
403      if (path.isEmpty() || path.equals("/") || contextPath.isEmpty() || contextPath.equals("/"))
404         return path;
405      String op = path;
406      if (path.charAt(0) == '/')
407         path = path.substring(1);
408      if (contextPath.charAt(0) == '/')
409         contextPath = contextPath.substring(1);
410      if (path.startsWith(contextPath)) {
411         if (path.length() == contextPath.length())
412            return "/";
413         path = path.substring(contextPath.length());
414         if (path.isEmpty() || path.charAt(0) == '/')
415            return path;
416      }
417      return op;
418   }
419
420   /**
421    * Normalizes the {@link RestOp#path()} value.
422    *
423    * @param path The path to normalize.
424    * @return The normalized path.
425    */
426   public static String fixMethodPath(String path) {
427      if (path == null)
428         return null;
429      if (path.equals("/"))
430         return path;
431      return trimTrailingSlashes(path);
432   }
433
434   /**
435    * Returns <jk>true</jk> if the specified value is a valid context path.
436    *
437    * The path must start with a "/" character but not end with a "/" character.
438    * For servlets in the default (root) context, the value should be "".
439    *
440    * @param value The value to test.
441    * @return <jk>true</jk> if the specified value is a valid context path.
442    */
443   public static boolean isValidContextPath(String value) {
444      if (value == null)
445         return false;
446      if (value.isEmpty())
447         return true;
448      if (value.charAt(value.length()-1) == '/' || value.charAt(0) != '/')
449         return false;
450      return true;
451   }
452
453   /**
454    * Converts the specified path segment to a valid context path.
455    *
456    * <ul>
457    *    <li><jk>nulls</jk> and <js>"/"</js> are converted to empty strings.
458    *    <li>Trailing slashes are trimmed.
459    *    <li>Leading slash is added if needed.
460    * </ul>
461    *
462    * @param s The value to convert.
463    * @return The converted path.
464    */
465   public static String toValidContextPath(String s) {
466      if (s == null || s.isEmpty())
467         return "";
468      s = trimTrailingSlashes(s);
469      if (s.isEmpty())
470         return s;
471      if (s.charAt(0) != '/')
472         s = '/' + s;
473      return s;
474   }
475
476   /**
477    * Throws a {@link RuntimeException} if the method {@link #isValidContextPath(String)} returns <jk>false</jk> for the specified value.
478    *
479    * @param value The value to test.
480    */
481   public static void validateContextPath(String value) {
482      if (! isValidContextPath(value))
483         throw new BasicRuntimeException("Value is not a valid context path: [{0}]", value);
484   }
485
486   /**
487    * Returns <jk>true</jk> if the specified value is a valid servlet path.
488    *
489    * This path must with a "/" character and includes either the servlet name or a path to the servlet,
490    * but does not include any extra path information or a query string.
491    * Should be an empty string ("") if the servlet used to process this request was matched using the "/*" pattern.
492    *
493    * @param value The value to test.
494    * @return <jk>true</jk> if the specified value is a valid servlet path.
495    */
496   public static boolean isValidServletPath(String value) {
497      if (value == null)
498         return false;
499      if (value.isEmpty())
500         return true;
501      if (value.equals("/") || value.charAt(value.length()-1) == '/' || value.charAt(0) != '/')
502         return false;
503      return true;
504   }
505
506   /**
507    * Throws a {@link RuntimeException} if the method {@link #isValidServletPath(String)} returns <jk>false</jk> for the specified value.
508    *
509    * @param value The value to test.
510    */
511   public static void validateServletPath(String value) {
512      if (! isValidServletPath(value))
513         throw new BasicRuntimeException("Value is not a valid servlet path: [{0}]", value);
514   }
515
516   /**
517    * Returns <jk>true</jk> if the specified value is a valid path-info path.
518    *
519    * The extra path information follows the servlet path but precedes the query string and will start with a "/" character.
520    * The value should be null if there was no extra path information.
521    *
522    * @param value The value to test.
523    * @return <jk>true</jk> if the specified value is a valid path-info path.
524    */
525   public static boolean isValidPathInfo(String value) {
526      if (value == null)
527         return true;
528      if (value.isEmpty() || value.charAt(0) != '/')
529         return false;
530      return true;
531   }
532
533   /**
534    * Throws a {@link RuntimeException} if the method {@link #isValidPathInfo(String)} returns <jk>false</jk> for the specified value.
535    *
536    * @param value The value to test.
537    */
538   public static void validatePathInfo(String value) {
539      if (! isValidPathInfo(value))
540         throw new BasicRuntimeException("Value is not a valid path-info path: [{0}]", value);
541   }
542}