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