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