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.util.*;
019import java.util.regex.*;
020
021import javax.servlet.http.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.internal.*;
025import org.apache.juneau.json.*;
026import org.apache.juneau.parser.*;
027import org.apache.juneau.rest.*;
028import org.apache.juneau.uon.*;
029import org.apache.juneau.utils.*;
030
031/**
032 * Various reusable utility methods.
033 */
034public final class RestUtils {
035
036   /**
037    * Returns readable text for an HTTP response code.
038    *
039    * @param rc The HTTP response code.
040    * @return Readable text for an HTTP response code, or <jk>null</jk> if it's an invalid code.
041    */
042   public static String getHttpResponseText(int rc) {
043      return httpMsgs.get(rc);
044   }
045
046   private static Map<Integer,String> httpMsgs = new AMap<Integer,String>()
047      .append(100, "Continue")
048      .append(101, "Switching Protocols")
049      .append(102, "Processing")
050      .append(103, "Early Hints")
051      .append(200, "OK")
052      .append(201, "Created")
053      .append(202, "Accepted")
054      .append(203, "Non-Authoritative Information")
055      .append(204, "No Content")
056      .append(205, "Reset Content")
057      .append(206, "Partial Content")
058      .append(300, "Multiple Choices")
059      .append(301, "Moved Permanently")
060      .append(302, "Temporary Redirect")
061      .append(303, "See Other")
062      .append(304, "Not Modified")
063      .append(305, "Use Proxy")
064      .append(307, "Temporary Redirect")
065      .append(400, "Bad Request")
066      .append(401, "Unauthorized")
067      .append(402, "Payment Required")
068      .append(403, "Forbidden")
069      .append(404, "Not Found")
070      .append(405, "Method Not Allowed")
071      .append(406, "Not Acceptable")
072      .append(407, "Proxy Authentication Required")
073      .append(408, "Request Time-Out")
074      .append(409, "Conflict")
075      .append(410, "Gone")
076      .append(411, "Length Required")
077      .append(412, "Precondition Failed")
078      .append(413, "Request Entity Too Large")
079      .append(414, "Request-URI Too Large")
080      .append(415, "Unsupported Media Type")
081      .append(500, "Internal Server Error")
082      .append(501, "Not Implemented")
083      .append(502, "Bad Gateway")
084      .append(503, "Service Unavailable")
085      .append(504, "Gateway Timeout")
086      .append(505, "HTTP Version Not Supported")
087   ;
088
089   /**
090    * Identical to {@link HttpServletRequest#getPathInfo()} but doesn't decode encoded characters.
091    *
092    * @param req The HTTP request
093    * @return The un-decoded path info.
094    */
095   public static String getPathInfoUndecoded(HttpServletRequest req) {
096      String requestURI = req.getRequestURI();
097      String contextPath = req.getContextPath();
098      String servletPath = req.getServletPath();
099      int l = contextPath.length() + servletPath.length();
100      if (requestURI.length() == l)
101         return null;
102      return requestURI.substring(l);
103   }
104
105   /**
106    * Efficiently trims the path info part from a request URI.
107    *
108    * <p>
109    * The result is the URI of the servlet itself.
110    *
111    * @param requestURI The value returned by {@link HttpServletRequest#getRequestURL()}
112    * @param contextPath The value returned by {@link HttpServletRequest#getContextPath()}
113    * @param servletPath The value returned by {@link HttpServletRequest#getServletPath()}
114    * @return The same StringBuilder with remainder trimmed.
115    */
116   public static StringBuffer trimPathInfo(StringBuffer requestURI, String contextPath, String servletPath) {
117      if (servletPath.equals("/"))
118         servletPath = "";
119      if (contextPath.equals("/"))
120         contextPath = "";
121
122      try {
123         // Given URL:  http://hostname:port/servletPath/extra
124         // We want:    http://hostname:port/servletPath
125         int sc = 0;
126         for (int i = 0; i < requestURI.length(); i++) {
127            char c = requestURI.charAt(i);
128            if (c == '/') {
129               sc++;
130               if (sc == 3) {
131                  if (servletPath.isEmpty()) {
132                     requestURI.setLength(i);
133                     return requestURI;
134                  }
135
136                  // Make sure context path follows the authority.
137                  for (int j = 0; j < contextPath.length(); i++, j++)
138                     if (requestURI.charAt(i) != contextPath.charAt(j))
139                        throw new Exception("case=1");
140
141                  // Make sure servlet path follows the authority.
142                  for (int j = 0; j < servletPath.length(); i++, j++)
143                     if (requestURI.charAt(i) != servletPath.charAt(j))
144                        throw new Exception("case=2");
145
146                  // Make sure servlet path isn't a false match (e.g. /foo2 should not match /foo)
147                  c = (requestURI.length() == i ? '/' : requestURI.charAt(i));
148                  if (c == '/' || c == '?') {
149                     requestURI.setLength(i);
150                     return requestURI;
151                  }
152
153                  throw new Exception("case=3");
154               }
155            } else if (c == '?') {
156               if (sc != 2)
157                  throw new Exception("case=4");
158               if (servletPath.isEmpty()) {
159                  requestURI.setLength(i);
160                  return requestURI;
161               }
162               throw new Exception("case=5");
163            }
164         }
165         if (servletPath.isEmpty())
166            return requestURI;
167         throw new Exception("case=6");
168      } catch (Exception e) {
169         throw new FormattedRuntimeException(e, "Could not find servlet path in request URI.  URI=''{0}'', servletPath=''{1}''", requestURI, servletPath);
170      }
171   }
172
173   /**
174    * Parses HTTP header.
175    *
176    * @param s The string to parse.
177    * @return The parsed string.
178    */
179   public static String[] parseHeader(String s) {
180      int i = s.indexOf(':');
181      if (i == -1)
182         return null;
183      String name = s.substring(0, i).trim().toLowerCase(Locale.ENGLISH);
184      String val = s.substring(i+1).trim();
185      return new String[]{name,val};
186   }
187
188   /**
189    * Parses key/value pairs separated by either : or =
190    *
191    * @param s The string to parse.
192    * @return The parsed string.
193    */
194   public static String[] parseKeyValuePair(String s) {
195      int i = -1;
196      for (int j = 0; j < s.length() && i < 0; j++) {
197         char c = s.charAt(j);
198         if (c == '=' || c == ':')
199            i = j;
200      }
201      if (i == -1)
202         return null;
203      String name = s.substring(0, i).trim();
204      String val = s.substring(i+1).trim();
205      return new String[]{name,val};
206   }
207
208   static String resolveNewlineSeparatedAnnotation(String[] value, String fromParent) {
209      if (value.length == 0)
210         return fromParent;
211
212      List<String> l = new ArrayList<>();
213      for (String v : value) {
214         if (! "INHERIT".equals(v))
215            l.add(v);
216         else if (fromParent != null)
217            l.addAll(Arrays.asList(fromParent));
218      }
219      return join(l, '\n');
220   }
221
222   private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)");
223
224   static String[] resolveLinks(String[] links, String[] parentLinks) {
225      if (links.length == 0)
226         return parentLinks;
227
228      List<String> list = new ArrayList<>();
229      for (String l : links) {
230         if ("INHERIT".equals(l))
231            list.addAll(Arrays.asList(parentLinks));
232         else if (l.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(l).matches()) {
233               Matcher lm = INDEXED_LINK_PATTERN.matcher(l);
234               lm.matches();
235               String key = lm.group(1);
236               int index = Math.min(list.size(), Integer.parseInt(lm.group(2)));
237               String remainder = lm.group(3);
238               list.add(index, key.isEmpty() ? remainder : key + ":" + remainder);
239         } else {
240            list.add(l);
241         }
242      }
243      return list.toArray(new String[list.size()]);
244   }
245
246   static String[] resolveContent(String[] content, String[] parentContent) {
247      if (content.length == 0)
248         return parentContent;
249
250      List<String> list = new ArrayList<>();
251      for (String l : content) {
252         if ("INHERIT".equals(l)) {
253            list.addAll(Arrays.asList(parentContent));
254         } else if ("NONE".equals(l)) {
255            return new String[0];
256         } else {
257            list.add(l);
258         }
259      }
260      return list.toArray(new String[list.size()]);
261   }
262
263   /**
264    * Parses a URL query string or form-data body.
265    *
266    * @param qs A reader or string containing the query string to parse.
267    * @return A new map containing the parsed query.
268    * @throws Exception
269    */
270   public static Map<String,String[]> parseQuery(Object qs) throws Exception {
271      return parseQuery(qs, null);
272   }
273
274   /**
275    * Same as {@link #parseQuery(Object)} but allows you to specify the map to insert values into.
276    *
277    * @param qs A reader containing the query string to parse.
278    * @param map The map to pass the values into.
279    * @return The same map passed in, or a new map if it was <jk>null</jk>.
280    * @throws Exception
281    */
282   public static Map<String,String[]> parseQuery(Object qs, Map<String,String[]> map) throws Exception {
283
284      Map<String,String[]> m = map == null ? new TreeMap<String,String[]>() : map;
285
286      if (qs == null || ((qs instanceof CharSequence) && isEmpty(qs)))
287         return m;
288
289      try (ParserPipe p = new ParserPipe(qs)) {
290
291         final int S1=1; // Looking for attrName start.
292         final int S2=2; // Found attrName start, looking for = or & or end.
293         final int S3=3; // Found =, looking for valStart or &.
294         final int S4=4; // Found valStart, looking for & or end.
295
296         try (UonReader r = new UonReader(p, true)) {
297            int c = r.peekSkipWs();
298            if (c == '?')
299               r.read();
300
301            int state = S1;
302            String currAttr = null;
303            while (c != -1) {
304               c = r.read();
305               if (state == S1) {
306                  if (c != -1) {
307                     r.unread();
308                     r.mark();
309                     state = S2;
310                  }
311               } else if (state == S2) {
312                  if (c == -1) {
313                     add(m, r.getMarked(), null);
314                  } else if (c == '\u0001') {
315                     m.put(r.getMarked(0,-1), null);
316                     state = S1;
317                  } else if (c == '\u0002') {
318                     currAttr = r.getMarked(0,-1);
319                     state = S3;
320                  }
321               } else if (state == S3) {
322                  if (c == -1 || c == '\u0001') {
323                     add(m, currAttr, "");
324                     state = S1;
325                  } else {
326                     if (c == '\u0002')
327                        r.replace('=');
328                     r.unread();
329                     r.mark();
330                     state = S4;
331                  }
332               } else if (state == S4) {
333                  if (c == -1) {
334                     add(m, currAttr, r.getMarked());
335                  } else if (c == '\u0001') {
336                     add(m, currAttr, r.getMarked(0,-1));
337                     state = S1;
338                  } else if (c == '\u0002') {
339                     r.replace('=');
340                  }
341               }
342            }
343         }
344
345         return m;
346      }
347   }
348
349   private static void add(Map<String,String[]> m, String key, String val) {
350      boolean b = m.containsKey(key);
351      if (val == null) {
352         if (! b)
353            m.put(key, null);
354      } else if (b && m.get(key) != null) {
355         m.put(key, append(m.get(key), val));
356      } else {
357         m.put(key, new String[]{val});
358      }
359   }
360
361   /**
362    * Parses a string that can consist of a simple string or JSON object/array.
363    *
364    * @param s The string to parse.
365    * @return The parsed value, or <jk>null</jk> if the input is null.
366    * @throws ParseException
367    */
368   public static Object parseAnything(String s) throws ParseException {
369      if (isJson(s))
370         return JsonParser.DEFAULT.parse(s, Object.class);
371      return s;
372   }
373
374   /**
375    * Merges the specified parent and child arrays.
376    *
377    * <p>
378    * The general concept is to allow child values to override parent values.
379    *
380    * <p>
381    * The rules are:
382    * <ul>
383    *    <li>If the child array is not empty, then the child array is returned.
384    *    <li>If the child array is empty, then the parent array is returned.
385    *    <li>If the child array contains {@link None}, then an empty array is always returned.
386    *    <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.
387    * </ul>
388    *
389    * @param fromParent The parent array.
390    * @param fromChild The child array.
391    * @return A new merged array.
392    */
393   public static Object[] merge(Object[] fromParent, Object[] fromChild) {
394
395      if (ArrayUtils.contains(None.class, fromChild))
396         return new Object[0];
397
398      if (fromChild.length == 0)
399         return fromParent;
400
401      if (! ArrayUtils.contains(Inherit.class, fromChild))
402         return fromChild;
403
404      List<Object> l = new ArrayList<>(fromParent.length + fromChild.length);
405      for (Object o : fromChild) {
406         if (o == Inherit.class)
407            l.addAll(Arrays.asList(fromParent));
408         else
409            l.add(o);
410      }
411      return l.toArray(new Object[l.size()]);
412   }
413}