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