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<String,List<String>> <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<String,List<String>> <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}