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.util; 014 015import static org.apache.juneau.common.internal.StringUtils.*; 016import static org.apache.juneau.common.internal.ThrowableUtils.*; 017import static org.apache.juneau.internal.ArrayUtils.*; 018import static org.apache.juneau.internal.CollectionUtils.*; 019 020import java.io.*; 021import java.util.*; 022import java.util.regex.*; 023 024import jakarta.servlet.http.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.json.*; 028import org.apache.juneau.parser.*; 029import org.apache.juneau.rest.annotation.*; 030import org.apache.juneau.uon.*; 031 032/** 033 * Various reusable utility methods. 034 * 035 * <h5 class='section'>See Also:</h5><ul> 036 037 * </ul> 038 */ 039public final class RestUtils { 040 041 /** 042 * Returns readable text for an HTTP response code. 043 * 044 * @param rc The HTTP response code. 045 * @return Readable text for an HTTP response code, or <jk>null</jk> if it's an invalid code. 046 */ 047 public static String getHttpResponseText(int rc) { 048 return httpMsgs.get(rc); 049 } 050 051 private static Map<Integer,String> httpMsgs = mapBuilder(Integer.class, String.class) 052 .add(100, "Continue") 053 .add(101, "Switching Protocols") 054 .add(102, "Processing") 055 .add(103, "Early Hints") 056 .add(200, "OK") 057 .add(201, "Created") 058 .add(202, "Accepted") 059 .add(203, "Non-Authoritative Information") 060 .add(204, "No Content") 061 .add(205, "Reset Content") 062 .add(206, "Partial Content") 063 .add(300, "Multiple Choices") 064 .add(301, "Moved Permanently") 065 .add(302, "Temporary Redirect") 066 .add(303, "See Other") 067 .add(304, "Not Modified") 068 .add(305, "Use Proxy") 069 .add(307, "Temporary Redirect") 070 .add(400, "Bad Request") 071 .add(401, "Unauthorized") 072 .add(402, "Payment Required") 073 .add(403, "Forbidden") 074 .add(404, "Not Found") 075 .add(405, "Method Not Allowed") 076 .add(406, "Not Acceptable") 077 .add(407, "Proxy Authentication Required") 078 .add(408, "Request Time-Out") 079 .add(409, "Conflict") 080 .add(410, "Gone") 081 .add(411, "Length Required") 082 .add(412, "Precondition Failed") 083 .add(413, "Request Entity Too Large") 084 .add(414, "Request-URI Too Large") 085 .add(415, "Unsupported Media Type") 086 .add(500, "Internal Server Error") 087 .add(501, "Not Implemented") 088 .add(502, "Bad Gateway") 089 .add(503, "Service Unavailable") 090 .add(504, "Gateway Timeout") 091 .add(505, "HTTP Version Not Supported") 092 .build() 093 ; 094 095 /** 096 * Identical to {@link HttpServletRequest#getPathInfo()} but doesn't decode encoded characters. 097 * 098 * @param req The HTTP request 099 * @return The un-decoded path info. 100 */ 101 public static String getPathInfoUndecoded(HttpServletRequest req) { 102 String requestURI = req.getRequestURI(); 103 String contextPath = req.getContextPath(); 104 String servletPath = req.getServletPath(); 105 int l = contextPath.length() + servletPath.length(); 106 if (requestURI.length() == l) 107 return null; 108 return requestURI.substring(l); 109 } 110 111 /** 112 * Efficiently trims the path info part from a request URI. 113 * 114 * <p> 115 * The result is the URI of the servlet itself. 116 * 117 * @param requestURI The value returned by {@link HttpServletRequest#getRequestURL()} 118 * @param contextPath The value returned by {@link HttpServletRequest#getContextPath()} 119 * @param servletPath The value returned by {@link HttpServletRequest#getServletPath()} 120 * @return The same StringBuilder with remainder trimmed. 121 */ 122 public static StringBuffer trimPathInfo(StringBuffer requestURI, String contextPath, String servletPath) { 123 if (servletPath.equals("/")) 124 servletPath = ""; 125 if (contextPath.equals("/")) 126 contextPath = ""; 127 128 try { 129 // Given URL: http://hostname:port/servletPath/extra 130 // We want: http://hostname:port/servletPath 131 int sc = 0; 132 for (int i = 0; i < requestURI.length(); i++) { 133 char c = requestURI.charAt(i); 134 if (c == '/') { 135 sc++; 136 if (sc == 3) { 137 if (servletPath.isEmpty()) { 138 requestURI.setLength(i); 139 return requestURI; 140 } 141 142 // Make sure context path follows the authority. 143 for (int j = 0; j < contextPath.length(); i++, j++) 144 if (requestURI.charAt(i) != contextPath.charAt(j)) 145 throw new Exception("case=1"); 146 147 // Make sure servlet path follows the authority. 148 for (int j = 0; j < servletPath.length(); i++, j++) 149 if (requestURI.charAt(i) != servletPath.charAt(j)) 150 throw new Exception("case=2"); 151 152 // Make sure servlet path isn't a false match (e.g. /foo2 should not match /foo) 153 c = (requestURI.length() == i ? '/' : requestURI.charAt(i)); 154 if (c == '/' || c == '?') { 155 requestURI.setLength(i); 156 return requestURI; 157 } 158 159 throw new Exception("case=3"); 160 } 161 } else if (c == '?') { 162 if (sc != 2) 163 throw new Exception("case=4"); 164 if (servletPath.isEmpty()) { 165 requestURI.setLength(i); 166 return requestURI; 167 } 168 throw new Exception("case=5"); 169 } 170 } 171 if (servletPath.isEmpty()) 172 return requestURI; 173 throw new Exception("case=6"); 174 } catch (Exception e) { 175 throw new BasicRuntimeException(e, "Could not find servlet path in request URI. URI=''{0}'', servletPath=''{1}''", requestURI, servletPath); 176 } 177 } 178 179 /** 180 * Parses HTTP header. 181 * 182 * @param s The string to parse. 183 * @return The parsed string. 184 */ 185 public static String[] parseHeader(String s) { 186 int i = s.indexOf(':'); 187 if (i == -1) 188 i = s.indexOf('='); 189 if (i == -1) 190 return null; 191 String name = s.substring(0, i).trim().toLowerCase(Locale.ENGLISH); 192 String val = s.substring(i+1).trim(); 193 return new String[]{name,val}; 194 } 195 196 /** 197 * Parses key/value pairs separated by either : or = 198 * 199 * @param s The string to parse. 200 * @return The parsed string. 201 */ 202 public static String[] parseKeyValuePair(String s) { 203 int i = -1; 204 for (int j = 0; j < s.length() && i < 0; j++) { 205 char c = s.charAt(j); 206 if (c == '=' || c == ':') 207 i = j; 208 } 209 if (i == -1) 210 return null; 211 String name = s.substring(0, i).trim(); 212 String val = s.substring(i+1).trim(); 213 return new String[]{name,val}; 214 } 215 216 static String resolveNewlineSeparatedAnnotation(String[] value, String fromParent) { 217 if (value.length == 0) 218 return fromParent; 219 220 List<String> l = list(); 221 for (String v : value) { 222 if (! "INHERIT".equals(v)) 223 l.add(v); 224 else if (fromParent != null) 225 l.add(fromParent); 226 } 227 return join(l, '\n'); 228 } 229 230 private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)"); 231 232 static String[] resolveLinks(String[] links, String[] parentLinks) { 233 if (links.length == 0) 234 return parentLinks; 235 236 List<String> list = list(); 237 for (String l : links) { 238 if ("INHERIT".equals(l)) 239 addAll(list, parentLinks); 240 else if (l.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(l).matches()) { 241 Matcher lm = INDEXED_LINK_PATTERN.matcher(l); 242 lm.matches(); 243 String key = lm.group(1); 244 int index = Math.min(list.size(), Integer.parseInt(lm.group(2))); 245 String remainder = lm.group(3); 246 list.add(index, key.isEmpty() ? remainder : key + ":" + remainder); 247 } else { 248 list.add(l); 249 } 250 } 251 return array(list, String.class); 252 } 253 254 static String[] resolveContent(String[] content, String[] parentContent) { 255 if (content.length == 0) 256 return parentContent; 257 258 List<String> list = list(); 259 for (String l : content) { 260 if ("INHERIT".equals(l)) { 261 addAll(list, parentContent); 262 } else if ("NONE".equals(l)) { 263 return new String[0]; 264 } else { 265 list.add(l); 266 } 267 } 268 return array(list, String.class); 269 } 270 271 /** 272 * Parses a URL query string or form-data content. 273 * 274 * @param qs A reader or string containing the query string to parse. 275 * @return A new map containing the parsed query. 276 */ 277 public static Map<String,String[]> parseQuery(Object qs) { 278 return parseQuery(qs, null); 279 } 280 281 /** 282 * Same as {@link #parseQuery(Object)} but allows you to specify the map to insert values into. 283 * 284 * @param qs A reader containing the query string to parse. 285 * @param map The map to pass the values into. 286 * @return The same map passed in, or a new map if it was <jk>null</jk>. 287 */ 288 public static Map<String,String[]> parseQuery(Object qs, Map<String,String[]> map) { 289 290 try { 291 Map<String,String[]> m = map; 292 if (m == null) 293 m = map(); 294 295 if (qs == null || ((qs instanceof CharSequence) && isEmpty(stringify(qs)))) 296 return m; 297 298 try (ParserPipe p = new ParserPipe(qs)) { 299 300 final int S1=1; // Looking for attrName start. 301 final int S2=2; // Found attrName start, looking for = or & or end. 302 final int S3=3; // Found =, looking for valStart or &. 303 final int S4=4; // Found valStart, looking for & or end. 304 305 try (UonReader r = new UonReader(p, true)) { 306 int c = r.peekSkipWs(); 307 if (c == '?') 308 r.read(); 309 310 int state = S1; 311 String currAttr = null; 312 while (c != -1) { 313 c = r.read(); 314 if (state == S1) { 315 if (c != -1) { 316 r.unread(); 317 r.mark(); 318 state = S2; 319 } 320 } else if (state == S2) { 321 if (c == -1) { 322 add(m, r.getMarked(), null); 323 } else if (c == '\u0001') { 324 m.put(r.getMarked(0,-1), null); 325 state = S1; 326 } else if (c == '\u0002') { 327 currAttr = r.getMarked(0,-1); 328 state = S3; 329 } 330 } else if (state == S3) { 331 if (c == -1 || c == '\u0001') { 332 add(m, currAttr, ""); 333 state = S1; 334 } else { 335 if (c == '\u0002') 336 r.replace('='); 337 r.unread(); 338 r.mark(); 339 state = S4; 340 } 341 } else if (state == S4) { 342 if (c == -1) { 343 add(m, currAttr, r.getMarked()); 344 } else if (c == '\u0001') { 345 add(m, currAttr, r.getMarked(0,-1)); 346 state = S1; 347 } else if (c == '\u0002') { 348 r.replace('='); 349 } 350 } 351 } 352 } 353 354 return m; 355 } 356 } catch (IOException e) { 357 throw asRuntimeException(e); // Should never happen. 358 } 359 } 360 361 private static void add(Map<String,String[]> m, String key, String val) { 362 boolean b = m.containsKey(key); 363 if (val == null) { 364 if (! b) 365 m.put(key, null); 366 } else if (b && m.get(key) != null) { 367 m.put(key, append(m.get(key), val)); 368 } else { 369 m.put(key, new String[]{val}); 370 } 371 } 372 373 /** 374 * Parses a string that can consist of a simple string or JSON object/array. 375 * 376 * @param s The string to parse. 377 * @return The parsed value, or <jk>null</jk> if the input is null. 378 * @throws ParseException Invalid JSON in string. 379 */ 380 public static Object parseAnything(String s) throws ParseException { 381 if (isJson(s)) 382 return JsonParser.DEFAULT.parse(s, Object.class); 383 return s; 384 } 385 386 /** 387 * If the specified path-info starts with the specified context path, trims the context path from the path info. 388 * 389 * @param contextPath The context path. 390 * @param path The URL path. 391 * @return The path following the context path, or the original path. 392 */ 393 public static String trimContextPath(String contextPath, String path) { 394 if (path == null) 395 return null; 396 if (path.isEmpty() || path.equals("/") || contextPath.isEmpty() || contextPath.equals("/")) 397 return path; 398 String op = path; 399 if (path.charAt(0) == '/') 400 path = path.substring(1); 401 if (contextPath.charAt(0) == '/') 402 contextPath = contextPath.substring(1); 403 if (path.startsWith(contextPath)) { 404 if (path.length() == contextPath.length()) 405 return "/"; 406 path = path.substring(contextPath.length()); 407 if (path.isEmpty() || path.charAt(0) == '/') 408 return path; 409 } 410 return op; 411 } 412 413 /** 414 * Normalizes the {@link RestOp#path()} value. 415 * 416 * @param path The path to normalize. 417 * @return The normalized path. 418 */ 419 public static String fixMethodPath(String path) { 420 if (path == null) 421 return null; 422 if (path.equals("/")) 423 return path; 424 return trimTrailingSlashes(path); 425 } 426 427 /** 428 * Returns <jk>true</jk> if the specified value is a valid context path. 429 * 430 * The path must start with a "/" character but not end with a "/" character. 431 * For servlets in the default (root) context, the value should be "". 432 * 433 * @param value The value to test. 434 * @return <jk>true</jk> if the specified value is a valid context path. 435 */ 436 public static boolean isValidContextPath(String value) { 437 if (value == null) 438 return false; 439 if (value.isEmpty()) 440 return true; 441 if (value.charAt(value.length()-1) == '/' || value.charAt(0) != '/') 442 return false; 443 return true; 444 } 445 446 /** 447 * Converts the specified path segment to a valid context path. 448 * 449 * <ul> 450 * <li><jk>nulls</jk> and <js>"/"</js> are converted to empty strings. 451 * <li>Trailing slashes are trimmed. 452 * <li>Leading slash is added if needed. 453 * </ul> 454 * 455 * @param s The value to convert. 456 * @return The converted path. 457 */ 458 public static String toValidContextPath(String s) { 459 if (s == null || s.isEmpty()) 460 return ""; 461 s = trimTrailingSlashes(s); 462 if (s.isEmpty()) 463 return s; 464 if (s.charAt(0) != '/') 465 s = '/' + s; 466 return s; 467 } 468 469 /** 470 * Throws a {@link RuntimeException} if the method {@link #isValidContextPath(String)} returns <jk>false</jk> for the specified value. 471 * 472 * @param value The value to test. 473 */ 474 public static void validateContextPath(String value) { 475 if (! isValidContextPath(value)) 476 throw new BasicRuntimeException("Value is not a valid context path: [{0}]", value); 477 } 478 479 /** 480 * Returns <jk>true</jk> if the specified value is a valid servlet path. 481 * 482 * This path must with a "/" character and includes either the servlet name or a path to the servlet, 483 * but does not include any extra path information or a query string. 484 * Should be an empty string ("") if the servlet used to process this request was matched using the "/*" pattern. 485 * 486 * @param value The value to test. 487 * @return <jk>true</jk> if the specified value is a valid servlet path. 488 */ 489 public static boolean isValidServletPath(String value) { 490 if (value == null) 491 return false; 492 if (value.isEmpty()) 493 return true; 494 if (value.equals("/") || value.charAt(value.length()-1) == '/' || value.charAt(0) != '/') 495 return false; 496 return true; 497 } 498 499 /** 500 * Throws a {@link RuntimeException} if the method {@link #isValidServletPath(String)} returns <jk>false</jk> for the specified value. 501 * 502 * @param value The value to test. 503 */ 504 public static void validateServletPath(String value) { 505 if (! isValidServletPath(value)) 506 throw new BasicRuntimeException("Value is not a valid servlet path: [{0}]", value); 507 } 508 509 /** 510 * Returns <jk>true</jk> if the specified value is a valid path-info path. 511 * 512 * The extra path information follows the servlet path but precedes the query string and will start with a "/" character. 513 * The value should be null if there was no extra path information. 514 * 515 * @param value The value to test. 516 * @return <jk>true</jk> if the specified value is a valid path-info path. 517 */ 518 public static boolean isValidPathInfo(String value) { 519 if (value == null) 520 return true; 521 if (value.isEmpty() || value.charAt(0) != '/') 522 return false; 523 return true; 524 } 525 526 /** 527 * Throws a {@link RuntimeException} if the method {@link #isValidPathInfo(String)} returns <jk>false</jk> for the specified value. 528 * 529 * @param value The value to test. 530 */ 531 public static void validatePathInfo(String value) { 532 if (! isValidPathInfo(value)) 533 throw new BasicRuntimeException("Value is not a valid path-info path: [{0}]", value); 534 } 535}