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