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.commons.lang.StateEnum.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
024
025import java.io.*;
026import java.util.*;
027
028import org.apache.juneau.commons.utils.*;
029import org.apache.juneau.json.*;
030import org.apache.juneau.parser.*;
031import org.apache.juneau.uon.*;
032
033import jakarta.servlet.http.*;
034
035/**
036 * Various reusable utility methods.
037 *
038 */
039@SuppressWarnings("resource")
040public class RestUtils {
041
042   // @formatter:off
043   private static Map<Integer,String> httpMsgs = mapb(Integer.class, String.class)
044      .unmodifiable()
045      .add(100, "Continue")
046      .add(101, "Switching Protocols")
047      .add(102, "Processing")
048      .add(103, "Early Hints")
049      .add(200, "OK")
050      .add(201, "Created")
051      .add(202, "Accepted")
052      .add(203, "Non-Authoritative Information")
053      .add(204, "No Content")
054      .add(205, "Reset Content")
055      .add(206, "Partial Content")
056      .add(300, "Multiple Choices")
057      .add(301, "Moved Permanently")
058      .add(302, "Temporary Redirect")
059      .add(303, "See Other")
060      .add(304, "Not Modified")
061      .add(305, "Use Proxy")
062      .add(307, "Temporary Redirect")
063      .add(400, "Bad Request")
064      .add(401, "Unauthorized")
065      .add(402, "Payment Required")
066      .add(403, "Forbidden")
067      .add(404, "Not Found")
068      .add(405, "Method Not Allowed")
069      .add(406, "Not Acceptable")
070      .add(407, "Proxy Authentication Required")
071      .add(408, "Request Time-Out")
072      .add(409, "Conflict")
073      .add(410, "Gone")
074      .add(411, "Length Required")
075      .add(412, "Precondition Failed")
076      .add(413, "Request Entity Too Large")
077      .add(414, "Request-URI Too Large")
078      .add(415, "Unsupported Media Type")
079      .add(500, "Internal Server Error")
080      .add(501, "Not Implemented")
081      .add(502, "Bad Gateway")
082      .add(503, "Service Unavailable")
083      .add(504, "Gateway Timeout")
084      .add(505, "HTTP Version Not Supported")
085      .build()
086   ;
087   // @formatter:on
088
089   /**
090    * Returns readable text for an HTTP response code.
091    *
092    * @param rc The HTTP response code.
093    * @return Readable text for an HTTP response code, or <jk>null</jk> if it's an invalid code.
094    */
095   public static String getHttpResponseText(int rc) {
096      return httpMsgs.get(rc);
097   }
098
099   /**
100    * Identical to {@link HttpServletRequest#getPathInfo()} but doesn't decode encoded characters.
101    *
102    * @param req The HTTP request
103    * @return The un-decoded path info.
104    */
105   public static String getPathInfoUndecoded(HttpServletRequest req) {
106      var requestURI = req.getRequestURI();
107      var contextPath = req.getContextPath();
108      var servletPath = req.getServletPath();
109      var l = contextPath.length() + servletPath.length();
110      return requestURI.length() == l ? null : requestURI.substring(l);
111   }
112
113   /**
114    * Parses a string as JSON if it appears to be JSON, otherwise returns the string as-is.
115    *
116    * <p>
117    * This method attempts to intelligently detect whether the input string is JSON or plain text.
118    * If the string appears to be JSON (starts with <c>{</c>, <c>[</c>, or other JSON indicators),
119    * it is parsed and returned as a Java object (<jk>Map</jk>, <jk>List</jk>, <jk>String</jk>, <jk>Number</jk>, etc.).
120    * Otherwise, the original string is returned unchanged.
121    *
122    * <p>
123    * This is useful when processing input that could be either a JSON value or a plain string,
124    * such as configuration values or user input that may or may not be JSON-encoded.
125    *
126    * <h5 class='section'>Example:</h5>
127    * <p class='bjava'>
128    *    <jc>// JSON object is parsed</jc>
129    *    Object <jv>result1</jv> = parseIfJson(<js>"{\"name\":\"John\"}"</js>);
130    *    <jc>// Returns a Map with key "name" and value "John"</jc>
131    *
132    *    <jc>// JSON array is parsed</jc>
133    *    Object <jv>result2</jv> = parseIfJson(<js>"[1,2,3]"</js>);
134    *    <jc>// Returns a List containing [1, 2, 3]</jc>
135    *
136    *    <jc>// Plain string is returned as-is</jc>
137    *    Object <jv>result3</jv> = parseIfJson(<js>"hello world"</js>);
138    *    <jc>// Returns the string "hello world"</jc>
139    * </p>
140    *
141    * @param value The string to parse. Can be <jk>null</jk>.
142    * @return
143    *    The parsed JSON object (if the string was JSON), or the original string (if it was not JSON).
144    *    Returns <jk>null</jk> if the input is <jk>null</jk>.
145    *    <br>Return type can be: <jk>Map</jk>, <jk>List</jk>, <jk>String</jk>, <jk>Number</jk>, <jk>Boolean</jk>, or <jk>null</jk>.
146    * @throws ParseException If the string appears to be JSON but contains invalid JSON syntax.
147    */
148   public static Object parseIfJson(String value) throws ParseException {
149      return isProbablyJson(value) ? JsonParser.DEFAULT.parse(value, Object.class) : value;
150   }
151
152   /**
153    * Parses a URL query string or form-data content from a string.
154    *
155    * <p>
156    * Parses key-value pairs from a query string format (e.g., <c>key1=value1&key2=value2</c>).
157    * Supports multiple values for the same key, which are collected into a <jk>List</jk>.
158    *
159    * <p>
160    * Special cases:
161    * <ul>
162    *    <li>Empty or <jk>null</jk> strings return an empty map</li>
163    *    <li>Keys without values (e.g., <c>key1&key2</c>) are stored with <jk>null</jk> values</li>
164    *    <li>Keys with empty values (e.g., <c>key=</c>) are stored with empty strings</li>
165    *    <li>Multiple occurrences of the same key append values to the list</li>
166    * </ul>
167    *
168    * <h5 class='section'>Example:</h5>
169    * <p class='bjava'>
170    *    Map&lt;String,List&lt;String&gt;&gt; <jv>params</jv> = parseQuery(<js>"f1=v1&f2=v2&f1=v3"</js>);
171    *    <jv>params</jv>.get(<js>"f1"</js>);  <jc>// Returns [v1, v3]</jc>
172    *    <jv>params</jv>.get(<js>"f2"</js>);  <jc>// Returns [v2]</jc>
173    * </p>
174    *
175    * @param qs The query string to parse. Can be <jk>null</jk> or empty.
176    * @return A map of parameter names to lists of values. Returns an empty map if the input is <jk>null</jk> or empty.
177    */
178   public static Map<String,List<String>> parseQuery(String qs) {
179      if (isEmpty(qs)) return Collections.emptyMap();
180      return parseQuery((Object)qs);
181   }
182
183   /**
184    * Parses a URL query string or form-data content from a reader.
185    *
186    * <p>
187    * Parses key-value pairs from a query string format (e.g., <c>key1=value1&key2=value2</c>).
188    * Supports multiple values for the same key, which are collected into a <jk>List</jk>.
189    *
190    * <p>
191    * Special cases:
192    * <ul>
193    *    <li><jk>null</jk> readers return an empty map</li>
194    *    <li>Keys without values (e.g., <c>key1&key2</c>) are stored with <jk>null</jk> values</li>
195    *    <li>Keys with empty values (e.g., <c>key=</c>) are stored with empty strings</li>
196    *    <li>Multiple occurrences of the same key append values to the list</li>
197    * </ul>
198    *
199    * <h5 class='section'>Example:</h5>
200    * <p class='bjava'>
201    *    <jc>// Parse from a reader</jc>
202    *    Reader <jv>reader</jv> = <jk>new</jk> StringReader(<js>"f1=v1&f2=v2"</js>);
203    *    Map&lt;String,List&lt;String&gt;&gt; <jv>params</jv> = parseQuery(<jv>reader</jv>);
204    *    <jv>params</jv>.get(<js>"f1"</js>);  <jc>// Returns [v1]</jc>
205    * </p>
206    *
207    * @param qs The reader containing the query string to parse. Can be <jk>null</jk>.
208    * @return A map of parameter names to lists of values. Returns an empty map if the input is <jk>null</jk>.
209    */
210   public static Map<String,List<String>> parseQuery(Reader qs) {
211      if (n(qs)) return Collections.emptyMap();
212      return parseQuery((Object)qs);
213   }
214
215   private static Map<String,List<String>> parseQuery(Object qs) {
216
217      var m = CollectionUtils.<String,List<String>>map();
218
219      try (var p = new ParserPipe(qs)) {
220
221         // S1: Looking for attrName start.
222         // S2: Found attrName start, looking for = or & or end.
223         // S3: Found =, looking for valStart or &.
224         // S4: Found valStart, looking for & or end.
225
226         try (var r = new UonReader(p, true)) {
227            var c = r.peekSkipWs();
228            if (c == '?')
229               r.read();
230
231            var state = S1;
232            var currAttr = (String)null;
233            while (c != -1) {
234               c = r.read();
235               if (state == S1) {
236                  if (c != -1) {
237                     r.unread();
238                     r.mark();
239                     state = S2;
240                  }
241               } else if (state == S2) {
242                  if (c == -1) {
243                     add(m, r.getMarked(), null);
244                  } else if (c == '\u0001') {
245                     add(m, r.getMarked(0, -1), null);
246                     state = S1;
247                  } else if (c == '\u0002') {
248                     currAttr = r.getMarked(0, -1);
249                     state = S3;
250                  }
251               } else if (state == S3) {
252                  if (c == -1 || c == '\u0001') {
253                     add(m, currAttr, "");
254                     state = S1;
255                  } else {
256                     if (c == '\u0002')
257                        r.replace('=');
258                     r.unread();
259                     r.mark();
260                     state = S4;
261                  }
262               } else if (state == S4) {
263                  if (c == -1) {
264                     add(m, currAttr, r.getMarked());
265                  } else if (c == '\u0001') {
266                     add(m, currAttr, r.getMarked(0, -1));
267                     state = S1;
268                  } else if (c == '\u0002') {
269                     r.replace('=');
270                  }
271               }
272            }
273         }
274
275         return m;
276      } catch (IOException e) {
277         throw toRex(e); // Should never happen.
278      }
279   }
280
281   /**
282    * Converts the specified path segment to a valid context path.
283    *
284    * <ul>
285    *    <li><jk>nulls</jk> and <js>"/"</js> are converted to empty strings.
286    *    <li>Trailing slashes are trimmed.
287    *    <li>Leading slash is added if needed.
288    * </ul>
289    *
290    * @param s The value to convert.
291    * @return The converted path.
292    */
293   public static String toValidContextPath(String s) {
294      if (s == null || s.isEmpty())
295         return "";
296      s = trimTrailingSlashes(s);
297      if (s.isEmpty())
298         return s;
299      if (s.charAt(0) != '/')
300         s = '/' + s;
301      return s;
302   }
303
304   /**
305    * Validates that the specified value is a valid path-info path and returns it.
306    *
307    * <p>
308    * A valid path-info path must be:
309    * <ul>
310    *    <li><jk>null</jk> (valid, indicates no extra path information)</li>
311    *    <li>Non-empty and starting with <c>/</c> (e.g., <c>/users/123</c>)</li>
312    * </ul>
313    *
314    * <p>
315    * The path-info follows the servlet path but precedes the query string.
316    *
317    * @param value The value to validate.
318    * @return The validated value (may be <jk>null</jk>).
319    * @throws RuntimeException If the value is not a valid path-info path.
320    */
321   public static String validatePathInfo(String value) {
322      if (value != null && (value.isEmpty() || value.charAt(0) != '/'))
323         throw rex("Value is not a valid path-info path: [{0}]", value);
324      return value;
325   }
326
327   /**
328    * Validates that the specified value is a valid servlet path and returns it.
329    *
330    * <p>
331    * A valid servlet path must be:
332    * <ul>
333    *    <li>An empty string <c>""</c> (valid, indicates servlet matched using <c>/*</c> pattern)</li>
334    *    <li>Non-empty, starting with <c>/</c>, not ending with <c>/</c>, and not exactly <c>/</c> (e.g., <c>/api</c>, <c>/api/users</c>)</li>
335    * </ul>
336    *
337    * <p>
338    * The servlet path includes either the servlet name or a path to the servlet, but does not include any extra path information or a query string.
339    *
340    * @param value The value to validate.
341    * @return The validated value (never <jk>null</jk>).
342    * @throws RuntimeException If the value is <jk>null</jk> or not a valid servlet path.
343    */
344   public static String validateServletPath(String value) {
345      if (value == null)
346         throw rex("Value is not a valid servlet path: [{0}]", value);
347      if (! value.isEmpty() && (value.equals("/") || value.charAt(value.length() - 1) == '/' || value.charAt(0) != '/'))
348         throw rex("Value is not a valid servlet path: [{0}]", value);
349      return value;
350   }
351
352   private static void add(Map<String,List<String>> m, String key, String val) {
353      if (val == null) {
354         if (! m.containsKey(key))
355            m.put(key, null);
356      } else {
357         m.compute(key, (k, existing) -> {
358            if (existing != null)
359               return addAll(existing, val);
360            return list(val);
361         });
362      }
363   }
364}