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