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&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&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<String,List<String>> <jv>params</jv> = parseQuery(<js>"f1=v1&f2=v2&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&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&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&f2=v2"</js>); 213 * Map<String,List<String>> <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}