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