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.common.utils; 018 019import static java.lang.Character.*; 020import static java.nio.charset.StandardCharsets.*; 021import static org.apache.juneau.common.utils.IOUtils.*; 022import static org.apache.juneau.common.utils.ThrowableUtils.*; 023import static org.apache.juneau.common.utils.Utils.*; 024 025import java.io.*; 026import java.lang.reflect.*; 027import java.math.*; 028import java.net.*; 029import java.nio.*; 030import java.text.*; 031import java.util.*; 032import java.util.concurrent.*; 033import java.util.concurrent.atomic.*; 034import java.util.function.*; 035import java.util.regex.*; 036import java.util.stream.*; 037import java.util.zip.*; 038 039import jakarta.xml.bind.*; 040 041/** 042 * Reusable string utility methods. 043 */ 044public class StringUtils { 045 046 /** 047 * Predicate check to filter out null and empty strings. 048 */ 049 public static final Predicate<String> NOT_EMPTY = Utils::isNotEmpty; 050 051 private static final AsciiSet numberChars = AsciiSet.of("-xX.+-#pP0123456789abcdefABCDEF"); 052 053 private static final AsciiSet firstNumberChars =AsciiSet.of("+-.#0123456789"); 054 private static final AsciiSet octChars = AsciiSet.of("01234567"); 055 private static final AsciiSet decChars = AsciiSet.of("0123456789"); 056 private static final AsciiSet hexChars = AsciiSet.of("0123456789abcdefABCDEF"); 057 // Maps 6-bit nibbles to BASE64 characters. 058 private static final char[] base64m1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); 059 060 // Characters that do not need to be URL-encoded 061 private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.!~*'()\\").build(); 062 063 // Characters that really do not need to be URL-encoded 064 private static final AsciiSet unencodedCharsLax = unencodedChars.copy() 065 .chars(":@$,") // reserved, but can't be confused in a query parameter. 066 .chars("{}|\\^[]`") // unwise characters. 067 .build(); 068 069 // Valid HTTP header characters (including quoted strings and comments). 070 private static final AsciiSet httpHeaderChars = AsciiSet 071 .create() 072 .chars("\t -") 073 .ranges("!-[","]-}") 074 .build(); 075 076 // Maps BASE64 characters to 6-bit nibbles. 077 private static final byte[] base64m2 = new byte[128]; 078 079 static { 080 for (var i = 0; i < 64; i++) 081 base64m2[base64m1[i]] = (byte)i; 082 } 083 private static final Random RANDOM = new Random(); 084 085 private static final Pattern fpRegex = Pattern.compile( 086 "[+-]?(NaN|Infinity|((((\\p{Digit}+)(\\.)?((\\p{Digit}+)?)([eE][+-]?(\\p{Digit}+))?)|(\\.((\\p{Digit}+))([eE][+-]?(\\p{Digit}+))?)|(((0[xX](\\p{XDigit}+)(\\.)?)|(0[xX](\\p{XDigit}+)?(\\.)(\\p{XDigit}+)))[pP][+-]?(\\p{Digit}+)))[fFdD]?))[\\x00-\\x20]*" // NOSONAR 087 ); 088 089 static final Map<Character,AsciiSet> ESCAPE_SETS = new ConcurrentHashMap<>(); 090 091 static final AsciiSet MAP_ESCAPE_SET = AsciiSet.of(",=\\"); 092 093 static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.of("\"'\\"); 094 095 private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); 096 097 private static final Map<Class<?>,Function<Object,String>> PRIMITIVE_ARRAY_STRINGIFIERS = new HashMap<>(); 098 099 static { 100 PRIMITIVE_ARRAY_STRINGIFIERS.put(boolean[].class, x -> Arrays.toString((boolean[])x)); 101 PRIMITIVE_ARRAY_STRINGIFIERS.put(byte[].class, x -> Arrays.toString((byte[])x)); 102 PRIMITIVE_ARRAY_STRINGIFIERS.put(char[].class, x -> Arrays.toString((char[])x)); 103 PRIMITIVE_ARRAY_STRINGIFIERS.put(double[].class, x -> Arrays.toString((double[])x)); 104 PRIMITIVE_ARRAY_STRINGIFIERS.put(float[].class, x -> Arrays.toString((float[])x)); 105 PRIMITIVE_ARRAY_STRINGIFIERS.put(int[].class, x -> Arrays.toString((int[])x)); 106 PRIMITIVE_ARRAY_STRINGIFIERS.put(long[].class, x -> Arrays.toString((long[])x)); 107 PRIMITIVE_ARRAY_STRINGIFIERS.put(short[].class, x -> Arrays.toString((short[])x)); 108 } 109 110 private static final char[] HEX = "0123456789ABCDEF".toCharArray(); 111 112 private static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS = 113 AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.*/()").build(); 114 private static final AsciiSet URI_CHARS = AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0','9').range('A','Z').range('a','z').build(); 115 116 /** 117 * Abbreviates a String using ellipses. 118 * 119 * @param in The input string. 120 * @param length The max length of the resulting string. 121 * @return The abbreviated string. 122 */ 123 public static String abbreviate(String in, int length) { 124 if (in == null || in.length() <= length || in.length() <= 3) 125 return in; 126 return in.substring(0, length-3) + "..."; 127 } 128 129 /** 130 * Appends a string to a StringBuilder, creating a new one if null. 131 * 132 * @param sb The StringBuilder to append to, or <jk>null</jk> to create a new one. 133 * @param in The string to append. 134 * @return The StringBuilder with the string appended. 135 */ 136 private static StringBuilder append(StringBuilder sb, String in) { 137 if (sb == null) 138 return new StringBuilder(in); 139 sb.append(in); 140 return sb; 141 } 142 143 /** 144 * Converts an array to a List, handling both primitive and object arrays. 145 * 146 * @param array The array to convert. 147 * @return A List containing the array elements. 148 */ 149 private static List<Object> arrayAsList(Object array) { 150 if (array.getClass().getComponentType().isPrimitive()) { 151 var l = new ArrayList<>(Array.getLength(array)); 152 for (var i = 0; i < Array.getLength(array); i++) 153 l.add(Array.get(array, i)); 154 return l; 155 } 156 return Arrays.asList((Object[])array); 157 } 158 159 /** 160 * BASE64-decodes the specified string. 161 * 162 * @param in The BASE-64 encoded string. 163 * @return The decoded byte array, or null if the input was <jk>null</jk>. 164 */ 165 public static byte[] base64Decode(String in) { 166 if (in == null) 167 return null; // NOSONAR - Intentional. 168 169 var bIn = in.getBytes(IOUtils.UTF8); 170 171 Utils.assertArg(bIn.length % 4 == 0, "Invalid BASE64 string length. Must be multiple of 4."); 172 173 // Strip out any trailing '=' filler characters. 174 var inLength = bIn.length; 175 while (inLength > 0 && bIn[inLength - 1] == '=') 176 inLength--; 177 178 var outLength = (inLength * 3) / 4; 179 var out = new byte[outLength]; 180 var iIn = 0; 181 var iOut = 0; 182 while (iIn < inLength) { 183 var i0 = bIn[iIn++]; 184 var i1 = bIn[iIn++]; 185 var i2 = iIn < inLength ? bIn[iIn++] : 'A'; 186 var i3 = iIn < inLength ? bIn[iIn++] : 'A'; 187 var b0 = base64m2[i0]; 188 var b1 = base64m2[i1]; 189 var b2 = base64m2[i2]; 190 int b3 = base64m2[i3]; 191 var o0 = (b0 << 2) | (b1 >>> 4); 192 var o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); 193 var o2 = ((b2 & 3) << 6) | b3; 194 out[iOut++] = (byte)o0; 195 if (iOut < outLength) 196 out[iOut++] = (byte)o1; 197 if (iOut < outLength) 198 out[iOut++] = (byte)o2; 199 } 200 return out; 201 } 202 203 /** 204 * Shortcut for calling <c>base64Decode(String)</c> and converting the result to a UTF-8 encoded string. 205 * 206 * @param in The BASE-64 encoded string to decode. 207 * @return The decoded string. 208 */ 209 public static String base64DecodeToString(String in) { 210 var b = base64Decode(in); 211 if (b == null) 212 return null; 213 return new String(b, IOUtils.UTF8); 214 } 215 216 /** 217 * BASE64-encodes the specified byte array. 218 * 219 * @param in The input byte array to convert. 220 * @return The byte array converted to a BASE-64 encoded string. 221 */ 222 public static String base64Encode(byte[] in) { 223 if (in == null) 224 return null; 225 var outLength = (in.length * 4 + 2) / 3; // Output length without padding 226 var out = new char[((in.length + 2) / 3) * 4]; // Length includes padding. 227 var iIn = 0; 228 var iOut = 0; 229 while (iIn < in.length) { 230 var i0 = in[iIn++] & 0xff; 231 var i1 = iIn < in.length ? in[iIn++] & 0xff : 0; 232 var i2 = iIn < in.length ? in[iIn++] & 0xff : 0; 233 var o0 = i0 >>> 2; 234 var o1 = ((i0 & 3) << 4) | (i1 >>> 4); 235 var o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); 236 var o3 = i2 & 0x3F; 237 out[iOut++] = base64m1[o0]; 238 out[iOut++] = base64m1[o1]; 239 out[iOut] = iOut < outLength ? base64m1[o2] : '='; 240 iOut++; 241 out[iOut] = iOut < outLength ? base64m1[o3] : '='; 242 iOut++; 243 } 244 return new String(out); 245 } 246 247 /** 248 * Shortcut for calling <code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code> 249 * 250 * @param in The input string to convert. 251 * @return The string converted to BASE-64 encoding. 252 */ 253 public static String base64EncodeToString(String in) { 254 if (in == null) 255 return null; 256 return base64Encode(in.getBytes(IOUtils.UTF8)); 257 } 258 259 /** 260 * Returns the character at the specified index in the string without throwing exceptions. 261 * 262 * @param s The string. 263 * @param i The index position. 264 * @return 265 * The character at the specified index, or <c>0</c> if the index is out-of-range or the string 266 * is <jk>null</jk>. 267 */ 268 public static char charAt(String s, int i) { 269 if (s == null || i < 0 || i >= s.length()) 270 return 0; 271 return s.charAt(i); 272 } 273 274 /** 275 * Compares two strings, but gracefully handles <jk>nulls</jk>. 276 * 277 * @param s1 The first string. 278 * @param s2 The second string. 279 * @return The same as {@link String#compareTo(String)}. 280 */ 281 public static int compare(String s1, String s2) { 282 if (s1 == null && s2 == null) 283 return 0; 284 if (s1 == null) 285 return Integer.MIN_VALUE; 286 if (s2 == null) 287 return Integer.MAX_VALUE; 288 return s1.compareTo(s2); 289 } 290 291 /** 292 * Converts string into a GZipped input stream. 293 * 294 * @param contents The contents to compress. 295 * @return The input stream converted to GZip. 296 * @throws Exception Exception occurred. 297 */ 298 public static byte[] compress(String contents) throws Exception { 299 var baos = new ByteArrayOutputStream(contents.length()>>1); 300 try (var gos = new GZIPOutputStream(baos)) { 301 gos.write(contents.getBytes()); 302 gos.finish(); 303 gos.flush(); 304 } 305 return baos.toByteArray(); 306 } 307 308 /** 309 * Same as {@link String#contains(CharSequence)} except returns <jk>null</jk> if the value is null. 310 * 311 * @param value The string to check. 312 * @param substring The value to check for. 313 * @return <jk>true</jk> if the value contains the specified substring. 314 */ 315 public static boolean contains(String value, CharSequence substring) { 316 return value != null && value.contains(substring); 317 } 318 319 /** 320 * Returns <jk>true</jk> if the specified string contains any of the specified characters. 321 * 322 * @param s The string to test. 323 * @param chars The characters to look for. 324 * @return 325 * <jk>true</jk> if the specified string contains any of the specified characters. 326 * <br><jk>false</jk> if the string is <jk>null</jk>. 327 */ 328 public static boolean containsAny(String s, char...chars) { 329 if (s == null) 330 return false; 331 for (int i = 0, j = s.length(); i < j; i++) { 332 var c = s.charAt(i); 333 for (var c2 : chars) 334 if (c == c2) 335 return true; 336 } 337 return false; 338 } 339 340 /** 341 * Converts an object to a readable string representation for formatting. 342 * 343 * @param o The object to convert. 344 * @return A readable string representation of the object. 345 */ 346 private static String convertToReadable(Object o) { 347 if (o == null) 348 return null; 349 if (o instanceof Class) 350 return ((Class<?>)o).getName(); 351 if (o instanceof Method) 352 return Method.class.cast(o).getName(); 353 if (isArray(o)) 354 return arrayAsList(o).stream().map(StringUtils::convertToReadable).collect(Collectors.joining(", ", "[", "]")); 355 return o.toString(); 356 } 357 358 /** 359 * Counts the number of the specified character in the specified string. 360 * 361 * @param s The string to check. 362 * @param c The character to check for. 363 * @return The number of those characters or zero if the string was <jk>null</jk>. 364 */ 365 public static int countChars(String s, char c) { 366 var count = 0; 367 if (s == null) 368 return count; 369 for (var i = 0; i < s.length(); i++) 370 if (s.charAt(i) == c) 371 count++; 372 return count; 373 } 374 375 /** 376 * Debug method for rendering non-ASCII character sequences. 377 * 378 * @param s The string to decode. 379 * @return A string with non-ASCII characters converted to <js>"[hex]"</js> sequences. 380 */ 381 public static String decodeHex(String s) { 382 if (s == null) 383 return null; 384 var sb = new StringBuilder(); 385 for (var c : s.toCharArray()) { 386 if (c < ' ' || c > '~') 387 sb.append("[").append(Integer.toHexString(c)).append("]"); 388 else 389 sb.append(c); 390 } 391 return sb.toString(); 392 } 393 394 /** 395 * Converts a GZipped input stream into a string. 396 * 397 * @param is The contents to decompress. 398 * @return The string. 399 * @throws Exception Exception occurred. 400 */ 401 public static String decompress(byte[] is) throws Exception { 402 return read(new GZIPInputStream(new ByteArrayInputStream(is))); 403 } 404 405 /** 406 * Finds the position where the two strings differ. 407 * 408 * @param s1 The first string. 409 * @param s2 The second string. 410 * @return The position where the two strings differ, or <c>-1</c> if they're equal. 411 */ 412 public static int diffPosition(String s1, String s2) { 413 s1 = emptyIfNull(s1); 414 s2 = emptyIfNull(s2); 415 var i = 0; 416 var len = Math.min(s1.length(), s2.length()); 417 while (i < len) { 418 var j = s1.charAt(i) - s2.charAt(i); 419 if (j != 0) 420 return i; 421 i++; 422 } 423 if (i == len && s1.length() == s2.length()) 424 return -1; 425 return i; 426 } 427 428 /** 429 * Finds the position where the two strings differ ignoring case. 430 * 431 * @param s1 The first string. 432 * @param s2 The second string. 433 * @return The position where the two strings differ, or <c>-1</c> if they're equal. 434 */ 435 public static int diffPositionIc(String s1, String s2) { 436 s1 = emptyIfNull(s1); 437 s2 = emptyIfNull(s2); 438 var i = 0; 439 var len = Math.min(s1.length(), s2.length()); 440 while (i < len) { 441 var j = toLowerCase(s1.charAt(i)) - toLowerCase(s2.charAt(i)); 442 if (j != 0) 443 return i; 444 i++; 445 } 446 if (i == len && s1.length() == s2.length()) 447 return -1; 448 return i; 449 } 450 451 /** 452 * An efficient method for checking if a string ends with a character. 453 * 454 * @param s The string to check. Can be <jk>null</jk>. 455 * @param c The character to check for. 456 * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character. 457 */ 458 public static boolean endsWith(String s, char c) { 459 if (s != null) { 460 var i = s.length(); 461 if (i > 0) 462 return s.charAt(i-1) == c; 463 } 464 return false; 465 } 466 467 /** 468 * Same as {@link #endsWith(String, char)} except check for multiple characters. 469 * 470 * @param s The string to check. Can be <jk>null</jk>. 471 * @param c The characters to check for. 472 * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character. 473 */ 474 public static boolean endsWith(String s, char...c) { 475 if (s != null) { 476 var i = s.length(); 477 if (i > 0) { 478 var c2 = s.charAt(i-1); 479 for (var cc : c) 480 if (c2 == cc) 481 return true; 482 } 483 } 484 return false; 485 } 486 487 /** 488 * Escapes the specified characters in the string. 489 * 490 * @param s The string with characters to escape. 491 * @param escaped The characters to escape. 492 * @return The string with characters escaped, or the same string if no escapable characters were found. 493 */ 494 public static String escapeChars(String s, AsciiSet escaped) { 495 if (s == null || s.isEmpty()) 496 return s; 497 498 var count = 0; 499 for (var i = 0; i < s.length(); i++) 500 if (escaped.contains(s.charAt(i))) 501 count++; 502 if (count == 0) 503 return s; 504 505 var sb = new StringBuffer(s.length() + count); 506 for (var i = 0; i < s.length(); i++) { 507 var c = s.charAt(i); 508 if (escaped.contains(c)) 509 sb.append('\\'); 510 sb.append(c); 511 } 512 return sb.toString(); 513 } 514 515 /** 516 * Returns the first character in the specified string. 517 * 518 * @param s The string to check. 519 * @return The first character in the string, or <c>0</c> if the string is <jk>null</jk> or empty. 520 */ 521 public static char firstChar(String s) { 522 if (s == null || s.isEmpty()) 523 return 0; 524 return s.charAt(0); 525 } 526 527 /** 528 * Returns the first non-null, non-empty string in the list. 529 * 530 * @param s The strings to test. 531 * @return The first non-empty string in the list, or <jk>null</jk> if they were all <jk>null</jk> or empty. 532 */ 533 public static String firstNonEmpty(String...s) { 534 for (var ss : s) 535 if (Utils.isNotEmpty(ss)) 536 return ss; 537 return null; 538 } 539 540 /** 541 * Returns the first non-whitespace character in the string. 542 * 543 * @param s The string to check. 544 * @return 545 * The first non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed 546 * of only whitespace. 547 */ 548 public static char firstNonWhitespaceChar(String s) { 549 if (s != null) 550 for (var i = 0; i < s.length(); i++) 551 if (! isWhitespace(s.charAt(i))) 552 return s.charAt(i); 553 return 0; 554 } 555 556 /** 557 * Finds the first non-whitespace, non-comment character in a string. 558 * 559 * @param s The string to analyze. 560 * @return The first real character, or <c>-1</c> if none found. 561 */ 562 private static int firstRealCharacter(String s) { 563 try (var r = new StringReader(s)) { 564 var c = 0; 565 while ((c = r.read()) != -1) { 566 if (! isWhitespace(c)) { 567 if (c == '/') { 568 skipComments(r); 569 } else { 570 return c; 571 } 572 } 573 } 574 return -1; 575 } catch (Exception e) { 576 throw asRuntimeException(e); 577 } 578 } 579 580 /** 581 * Attempts to escape any invalid characters found in a URI. 582 * 583 * @param in The URI to fix. 584 * @return The fixed URI. 585 */ 586 public static String fixUrl(String in) { 587 588 if (in == null) 589 return null; 590 591 StringBuilder sb = null; 592 593 var m = 0; 594 595 for (var i = 0; i < in.length(); i++) { 596 var c = in.charAt(i); 597 if (c <= 127 && ! URI_CHARS.contains(c)) { 598 sb = append(sb, in.substring(m, i)); 599 if (c == ' ') 600 sb.append("+"); 601 else 602 sb.append('%').append(toHex2(c)); 603 m = i+1; 604 } 605 } 606 if (sb != null) { 607 sb.append(in.substring(m)); 608 return sb.toString(); 609 } 610 return in; 611 612 } 613 614 /** 615 * Similar to {@link MessageFormat#format(String, Object...)} except allows you to specify POJO arguments. 616 * 617 * @param pattern The string pattern. 618 * @param args The arguments. 619 * @return The formatted string. 620 */ 621 public static String format(String pattern, Object...args) { 622 if (args == null || args.length == 0) 623 return pattern; 624 var args2 = new Object[args.length]; 625 for (var i = 0; i < args.length; i++) 626 args2[i] = convertToReadable(args[i]); 627 628 var c = countChars(pattern, '\''); 629 if (c % 2 != 0) 630 throw new AssertionError("Dangling single quote found in pattern: " + pattern); 631 632 return MessageFormat.format(pattern, args2); 633 } 634 635 /** 636 * Converts a hexadecimal character string to a byte array. 637 * 638 * @param hex The string to convert to a byte array. 639 * @return A new byte array. 640 */ 641 public static byte[] fromHex(String hex) { 642 var buff = ByteBuffer.allocate(hex.length()/2); 643 for (var i = 0; i < hex.length(); i+=2) 644 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 645 buff.rewind(); 646 return buff.array(); 647 } 648 649 /** 650 * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 encoded string. 651 * 652 * @param hex The hexadecimal string. 653 * @return The UTF-8 string. 654 */ 655 public static String fromHexToUTF8(String hex) { 656 var buff = ByteBuffer.allocate(hex.length()/2); 657 for (var i = 0; i < hex.length(); i+=2) 658 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 659 buff.rewind(); // Fixes Java 11 issue. 660 return UTF_8.decode(buff).toString(); 661 } 662 663 /** 664 * Same as {@link #fromHex(String)} except expects spaces between the byte strings. 665 * 666 * @param hex The string to convert to a byte array. 667 * @return A new byte array. 668 */ 669 public static byte[] fromSpacedHex(String hex) { 670 var buff = ByteBuffer.allocate((hex.length()+1)/3); 671 for (var i = 0; i < hex.length(); i+=3) 672 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 673 buff.rewind(); 674 return buff.array(); 675 } 676 677 /** 678 * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 BC") into a UTF-8 encoded string. 679 * 680 * @param hex The hexadecimal string. 681 * @return The UTF-8 string. 682 */ 683 public static String fromSpacedHexToUTF8(String hex) { 684 var buff = ByteBuffer.allocate((hex.length()+1)/3); 685 for (var i = 0; i < hex.length(); i+=3) 686 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 687 buff.rewind(); // Fixes Java 11 issue. 688 return UTF_8.decode(buff).toString(); 689 } 690 691 /** 692 * Given an absolute URI, returns just the authority portion (e.g. <js>"http://hostname:port"</js>) 693 * 694 * @param s The URI string. 695 * @return Just the authority portion of the URI. 696 */ 697 public static String getAuthorityUri(String s) { // NOSONAR - False positive. 698 699 // Use a state machine for maximum performance. 700 701 final int 702 S1 = 1, // Looking for http 703 S2 = 2, // Found http, looking for : 704 S3 = 3, // Found :, looking for / 705 S4 = 4, // Found /, looking for / 706 S5 = 5, // Found /, looking for x 707 S6 = 6; // Found x, looking for / 708 709 var state = S1; 710 711 for (var i = 0; i < s.length(); i++) { 712 var c = s.charAt(i); 713 if (state == S1) { 714 if (c >= 'a' && c <= 'z') 715 state = S2; 716 else 717 return s; 718 } else if (state == S2) { 719 if (c == ':') 720 state = S3; 721 else if (c < 'a' || c > 'z') 722 return s; 723 } else if (state == S3) { // NOSONAR - False positive. 724 if (c == '/') 725 state = S4; 726 else 727 return s; 728 } else if (state == S4) { 729 if (c == '/') 730 state = S5; 731 else 732 return s; 733 } else if (state == S5) { 734 if (c != '/') 735 state = S6; 736 else 737 return s; 738 } else if (state == S6) { 739 if (c == '/') // NOSONAR - Intentional. 740 return s.substring(0, i); 741 } 742 } 743 744 return s; 745 } 746 747 /** 748 * Parses a duration string. 749 * 750 * <p> 751 * Examples: 752 * <ul> 753 * <li><js>"1000"</js> - 1000 milliseconds. 754 * <li><js>"10s"</js> - 10 seconds. 755 * <li><js>"10 sec"</js> - 10 seconds. 756 * <li><js>"10 seconds"</js> - 10 seconds. 757 * </ul> 758 * 759 * <p> 760 * Use any of the following suffixes: 761 * <ul> 762 * <li>None (time in milliseconds). 763 * <li><js>"s"</js>/<js>"sec"</js>/<js>"second"</js>/<js>"seconds"</js> 764 * <li><js>"m"</js>/<js>"min"</js>/<js>"minutes"</js>/<js>"seconds"</js> 765 * <li><js>"h"</js>/<js>"hour"</js>/<js>"hours"</js> 766 * <li><js>"d"</js>/<js>"day"</js>/<js>"days"</js> 767 * <li><js>"w"</js>/<js>"week"</js>/<js>"weeks"</js> 768 * </ul> 769 * 770 * <p> 771 * Suffixes are case-insensitive. 772 * <br>Whitespace is ignored. 773 * 774 * @param s The string to parse. 775 * @return 776 * The time in milliseconds, or <c>-1</c> if the string is empty or <jk>null</jk>. 777 */ 778 public static long getDuration(String s) { 779 s = trim(s); 780 if (Utils.isEmpty(s)) 781 return -1; 782 int i; 783 for (i = 0; i < s.length(); i++) { 784 var c = s.charAt(i); 785 if (c < '0' || c > '9') 786 break; 787 } 788 long l; 789 if (i == s.length()) 790 l = Long.parseLong(s); 791 else { 792 l = Long.parseLong(s.substring(0, i).trim()); 793 var r = s.substring(i).trim().toLowerCase(); 794 if (r.startsWith("s")) 795 l *= 1000; 796 else if (r.startsWith("m")) 797 l *= 1000 * 60; 798 else if (r.startsWith("h")) 799 l *= 1000 * 60 * 60; 800 else if (r.startsWith("d")) 801 l *= 1000 * 60 * 60 * 24; 802 else if (r.startsWith("w")) 803 l *= 1000 * 60 * 60 * 24 * 7; 804 } 805 return l; 806 } 807 /** 808 * Gets or creates an AsciiSet for escaping the specified character. 809 * 810 * @param c The character to create an escape set for. 811 * @return An AsciiSet containing the character and backslash. 812 */ 813 static AsciiSet getEscapeSet(char c) { 814 return ESCAPE_SETS.computeIfAbsent(c, key -> AsciiSet.create().chars(key, '\\').build()); 815 } 816 817 /** 818 * Takes in a string, splits it by lines, and then prepends each line with line numbers. 819 * 820 * @param s The string. 821 * @return The string with line numbers added. 822 */ 823 public static String getNumberedLines(String s) { 824 return getNumberedLines(s, 1, Integer.MAX_VALUE); 825 } 826 827 /** 828 * Same as {@link #getNumberedLines(String)} except only returns the specified lines. 829 * 830 * <p> 831 * Out-of-bounds values are allowed and fixed. 832 * 833 * @param s The string. 834 * @param start The starting line (1-indexed). 835 * @param end The ending line (1-indexed). 836 * @return The string with line numbers added. 837 */ 838 public static String getNumberedLines(String s, int start, int end) { 839 if (s == null) 840 return null; 841 var lines = s.split("[\r\n]+"); 842 var digits = String.valueOf(lines.length).length(); 843 if (start < 1) 844 start = 1; 845 if (end < 0) 846 end = Integer.MAX_VALUE; 847 if (end > lines.length) 848 end = lines.length; 849 var sb = new StringBuilder(); 850 for (var l : Arrays.asList(lines).subList(start-1, end)) 851 sb.append(String.format("%0"+digits+"d", start++)).append(": ").append(l).append("\n"); // NOSONAR - Intentional. 852 return sb.toString(); 853 } 854 855 /** 856 * Same as {@link String#indexOf(int)} except allows you to check for multiple characters. 857 * 858 * @param s The string to check. 859 * @param c The characters to check for. 860 * @return The index into the string that is one of the specified characters. 861 */ 862 public static int indexOf(String s, char...c) { 863 if (s == null) 864 return -1; 865 for (var i = 0; i < s.length(); i++) { 866 var c2 = s.charAt(i); 867 for (var cc : c) 868 if (c2 == cc) 869 return i; 870 } 871 return -1; 872 } 873 874 /** 875 * Efficiently determines whether a URL is of the pattern "xxx://xxx" 876 * 877 * @param s The string to test. 878 * @return <jk>true</jk> if it's an absolute path. 879 */ 880 public static boolean isAbsoluteUri(String s) { // NOSONAR - False positive. 881 882 if (Utils.isEmpty(s)) 883 return false; 884 885 // Use a state machine for maximum performance. 886 887 final int 888 S1 = 1, // Looking for http 889 S2 = 2, // Found http, looking for : 890 S3 = 3, // Found :, looking for / 891 S4 = 4, // Found /, looking for / 892 S5 = 5; // Found /, looking for x 893 894 var state = S1; 895 896 for (var i = 0; i < s.length(); i++) { 897 var c = s.charAt(i); 898 if (state == S1) { 899 if (c >= 'a' && c <= 'z') 900 state = S2; 901 else 902 return false; 903 } else if (state == S2) { 904 if (c == ':') 905 state = S3; 906 else if (c < 'a' || c > 'z') 907 return false; 908 } else if (state == S3) { // NOSONAR - False positive. 909 if (c == '/') 910 state = S4; 911 else 912 return false; 913 } else if (state == S4) { 914 if (c == '/') 915 state = S5; 916 else 917 return false; 918 } else if (state == S5) { 919 return true; 920 } 921 } 922 return false; 923 } 924 925 /** 926 * Returns <jk>true</jk> if the specified string is numeric. 927 * 928 * @param s The string to check. 929 * @return <jk>true</jk> if the specified string is numeric. 930 */ 931 public static boolean isDecimal(String s) { 932 if (s == null || s.isEmpty() || ! firstNumberChars.contains(s.charAt(0))) 933 return false; 934 var i = 0; 935 var length = s.length(); 936 var c = s.charAt(0); 937 var isPrefixed = false; 938 if (c == '+' || c == '-') { 939 isPrefixed = true; 940 i++; 941 } 942 if (i == length) 943 return false; 944 c = s.charAt(i++); 945 if (c == '0' && length > (isPrefixed ? 2 : 1)) { 946 c = s.charAt(i++); 947 if (c == 'x' || c == 'X') { 948 for (int j = i; j < length; j++) { 949 if (! hexChars.contains(s.charAt(j))) 950 return false; 951 } 952 } else if (octChars.contains(c)) { 953 for (int j = i; j < length; j++) 954 if (! octChars.contains(s.charAt(j))) 955 return false; 956 } else { 957 return false; 958 } 959 } else if (c == '#') { 960 for (int j = i; j < length; j++) { 961 if (! hexChars.contains(s.charAt(j))) 962 return false; 963 } 964 } else if (decChars.contains(c)) { 965 for (int j = i; j < length; j++) 966 if (! decChars.contains(s.charAt(j))) 967 return false; 968 } else { 969 return false; 970 } 971 return true; 972 } 973 974 /** 975 * Returns <jk>true</jk> if the specified character is a valid first character for a number. 976 * 977 * @param c The character to test. 978 * @return <jk>true</jk> if the specified character is a valid first character for a number. 979 */ 980 public static boolean isFirstNumberChar(char c) { 981 return firstNumberChars.contains(c); 982 } 983 984 /** 985 * Returns <jk>true</jk> if the specified string is a floating point number. 986 * 987 * @param s The string to check. 988 * @return <jk>true</jk> if the specified string is a floating point number. 989 */ 990 public static boolean isFloat(String s) { 991 if (s == null || s.isEmpty()) 992 return false; 993 if (! firstNumberChars.contains(s.charAt(0))) 994 return (s.equals("NaN") || s.equals("Infinity")); 995 var i = 0; 996 var length = s.length(); 997 var c = s.charAt(0); 998 if (c == '+' || c == '-') 999 i++; 1000 if (i == length) 1001 return false; 1002 c = s.charAt(i); 1003 if (c == '.' || decChars.contains(c)) { 1004 return fpRegex.matcher(s).matches(); 1005 } 1006 return false; 1007 } 1008 1009 /** 1010 * Returns <jk>true</jk> if the specified string is valid JSON. 1011 * 1012 * <p> 1013 * Leading and trailing spaces are ignored. 1014 * <br>Leading and trailing comments are not allowed. 1015 * 1016 * @param s The string to test. 1017 * @return <jk>true</jk> if the specified string is valid JSON. 1018 */ 1019 public static boolean isJson(String s) { 1020 if (s == null) 1021 return false; 1022 var c1 = firstNonWhitespaceChar(s); 1023 var c2 = lastNonWhitespaceChar(s); 1024 if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == '\'' && c2 == '\'') 1025 return true; 1026 return (isOneOf(s, "true","false","null") || isNumeric(s)); 1027 } 1028 1029 /** 1030 * Returns <jk>true</jk> if the specified string appears to be an JSON array. 1031 * 1032 * @param o The object to test. 1033 * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored. 1034 * @return <jk>true</jk> if the specified string appears to be a JSON array. 1035 */ 1036 public static boolean isJsonArray(Object o, boolean ignoreWhitespaceAndComments) { 1037 if (o instanceof CharSequence) { 1038 var s = o.toString(); 1039 if (! ignoreWhitespaceAndComments) 1040 return (s.startsWith("[") && s.endsWith("]")); 1041 if (firstRealCharacter(s) != '[') 1042 return false; 1043 var i = s.lastIndexOf(']'); 1044 if (i == -1) 1045 return false; 1046 s = s.substring(i+1); 1047 return firstRealCharacter(s) == -1; 1048 } 1049 return false; 1050 } 1051 1052 /** 1053 * Returns <jk>true</jk> if the specified string appears to be a JSON object. 1054 * 1055 * @param o The object to test. 1056 * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored. 1057 * @return <jk>true</jk> if the specified string appears to be a JSON object. 1058 */ 1059 public static boolean isJsonObject(Object o, boolean ignoreWhitespaceAndComments) { 1060 if (o instanceof CharSequence) { 1061 var s = o.toString(); 1062 if (! ignoreWhitespaceAndComments) 1063 return (s.startsWith("{") && s.endsWith("}")); 1064 if (firstRealCharacter(s) != '{') 1065 return false; 1066 var i = s.lastIndexOf('}'); 1067 if (i == -1) 1068 return false; 1069 s = s.substring(i+1); 1070 return firstRealCharacter(s) == -1; 1071 } 1072 return false; 1073 } 1074 1075 /** 1076 * Returns <jk>true</jk> if the specified character is a valid number character. 1077 * 1078 * @param c The character to check. 1079 * @return <jk>true</jk> if the specified character is a valid number character. 1080 */ 1081 public static boolean isNumberChar(char c) { 1082 return numberChars.contains(c); 1083 } 1084 1085 /** 1086 * Returns <jk>true</jk> if this string can be parsed by {@link #parseNumber(String, Class)}. 1087 * 1088 * @param s The string to check. 1089 * @return <jk>true</jk> if this string can be parsed without causing an exception. 1090 */ 1091 public static boolean isNumeric(String s) { 1092 if (s == null || s.isEmpty() || ! isFirstNumberChar(s.charAt(0))) 1093 return false; 1094 return isDecimal(s) || isFloat(s); 1095 } 1096 1097 /** 1098 * Returns <jk>true</jk> if the specified string is one of the specified values. 1099 * 1100 * @param s 1101 * The string to test. 1102 * Can be <jk>null</jk>. 1103 * @param values 1104 * The values to test. 1105 * Can contain <jk>null</jk>. 1106 * @return <jk>true</jk> if the specified string is one of the specified values. 1107 */ 1108 public static boolean isOneOf(String s, String...values) { 1109 for (var value : values) 1110 if (Utils.eq(s, value)) 1111 return true; 1112 return false; 1113 } 1114 1115 /** 1116 * Efficiently determines whether a URL is of the pattern "xxx:/xxx". 1117 * 1118 * <p> 1119 * The pattern matched is: <c>[a-z]{2,}\:\/.*</c> 1120 * 1121 * <p> 1122 * Note that this excludes filesystem paths such as <js>"C:/temp"</js>. 1123 * 1124 * @param s The string to test. 1125 * @return <jk>true</jk> if it's an absolute path. 1126 */ 1127 public static boolean isUri(String s) { // NOSONAR - False positive. 1128 1129 if (Utils.isEmpty(s)) 1130 return false; 1131 1132 // Use a state machine for maximum performance. 1133 1134 final int 1135 S1 = 1, // Looking for protocol char 1 1136 S2 = 2, // Found protocol char 1, looking for protocol char 2 1137 S3 = 3, // Found protocol char 2, looking for : 1138 S4 = 4; // Found :, looking for / 1139 1140 var state = S1; 1141 1142 for (var i = 0; i < s.length(); i++) { 1143 var c = s.charAt(i); 1144 if (state == S1) { 1145 if (c >= 'a' && c <= 'z') 1146 state = S2; 1147 else 1148 return false; 1149 } else if (state == S2) { 1150 if (c >= 'a' && c <= 'z') 1151 state = S3; 1152 else 1153 return false; 1154 } else if (state == S3) { // NOSONAR - False positive. 1155 if (c == ':') 1156 state = S4; 1157 else if (c < 'a' || c > 'z') 1158 return false; 1159 } else if (state == S4) { 1160 return c == '/'; 1161 } 1162 } 1163 return false; 1164 } 1165 1166 /** 1167 * Same as {@link Utils#join(Collection, char)} but escapes the delimiter if found in the tokens. 1168 * 1169 * @param tokens The tokens to join. 1170 * @param d The delimiter. 1171 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 1172 */ 1173 public static String joine(List<?> tokens, char d) { 1174 if (tokens == null) 1175 return null; 1176 var as = getEscapeSet(d); 1177 var sb = new StringBuilder(); 1178 for (int i = 0, j = tokens.size(); i < j; i++) { 1179 if (i > 0) 1180 sb.append(d); 1181 sb.append(escapeChars(Utils.s(tokens.get(i)), as)); 1182 } 1183 return sb.toString(); 1184 } 1185 1186 /** 1187 * Returns the last non-whitespace character in the string. 1188 * 1189 * @param s The string to check. 1190 * @return 1191 * The last non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed 1192 * of only whitespace. 1193 */ 1194 public static char lastNonWhitespaceChar(String s) { 1195 if (s != null) 1196 for (var i = s.length()-1; i >= 0; i--) 1197 if (! isWhitespace(s.charAt(i))) 1198 return s.charAt(i); 1199 return 0; 1200 } 1201 1202 /** 1203 * Determines the multiplier value based on the suffix character in a string. 1204 * 1205 * @param s The string to analyze for multiplier suffix. 1206 * @return The multiplier value (1 if no valid suffix found). 1207 */ 1208 private static int multiplier(String s) { 1209 char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1); // NOSONAR - NPE not possible. 1210 if (c == 'G') return 1024*1024*1024; 1211 if (c == 'M') return 1024*1024; 1212 if (c == 'K') return 1024; 1213 if (c == 'g') return 1000*1000*1000; 1214 if (c == 'm') return 1000*1000; 1215 if (c == 'k') return 1000; 1216 return 1; 1217 } 1218 1219 /** 1220 * Determines the long multiplier value based on the suffix character in a string. 1221 * 1222 * @param s The string to analyze for multiplier suffix. 1223 * @return The multiplier value (1 if no valid suffix found). 1224 */ 1225 private static long multiplier2(String s) { 1226 char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1); // NOSONAR - NPE not possible. 1227 if (c == 'P') return 1024*1024*1024*1024*1024l; 1228 if (c == 'T') return 1024*1024*1024*1024l; 1229 if (c == 'G') return 1024*1024*1024l; 1230 if (c == 'M') return 1024*1024l; 1231 if (c == 'K') return 1024l; 1232 if (c == 'p') return 1000*1000*1000*1000*1000l; 1233 if (c == 't') return 1000*1000*1000*1000l; 1234 if (c == 'g') return 1000*1000*1000l; 1235 if (c == 'm') return 1000*1000l; 1236 if (c == 'k') return 1000l; 1237 return 1; 1238 } 1239 1240 /** 1241 * Converts a <c>String</c> to a <c>Character</c> 1242 * 1243 * @param o The string to convert. 1244 * @return The first character of the string if the string is of length 1, or <jk>null</jk> if the string is <jk>null</jk> or empty. 1245 * @throws IllegalArgumentException If the string length is not 1. 1246 */ 1247 public static Character parseCharacter(Object o) { 1248 if (o == null) 1249 return null; 1250 var s = o.toString(); 1251 if (s.isEmpty()) 1252 return null; 1253 if (s.length() == 1) 1254 return s.charAt(0); 1255 throw new IllegalArgumentException("Invalid character: '" + s + "'"); 1256 } 1257 1258 /** 1259 * Converts a string containing a possible multiplier suffix to an integer. 1260 * 1261 * <p> 1262 * The string can contain any of the following multiplier suffixes: 1263 * <ul> 1264 * <li><js>"K"</js> - x 1024 1265 * <li><js>"M"</js> - x 1024*1024 1266 * <li><js>"G"</js> - x 1024*1024*1024 1267 * <li><js>"k"</js> - x 1000 1268 * <li><js>"m"</js> - x 1000*1000 1269 * <li><js>"g"</js> - x 1000*1000*1000 1270 * </ul> 1271 * 1272 * @param s The string to parse. 1273 * @return The parsed value. 1274 */ 1275 public static int parseIntWithSuffix(String s) { 1276 Utils.assertArgNotNull("s", s); 1277 var m = multiplier(s); 1278 if (m == 1) 1279 return Integer.decode(s); 1280 return Integer.decode(s.substring(0, s.length()-1).trim()) * m; // NOSONAR - NPE not possible here. 1281 } 1282 1283 /** 1284 * Parses an ISO8601 string into a calendar. 1285 * 1286 * <p> 1287 * Supports any of the following formats: 1288 * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c> 1289 * 1290 * @param date The date string. 1291 * @return The parsed calendar. 1292 * @throws IllegalArgumentException Value was not a valid date. 1293 */ 1294 public static Calendar parseIsoCalendar(String date) throws IllegalArgumentException { 1295 if (Utils.isEmpty(date)) 1296 return null; 1297 date = date.trim().replace(' ', 'T'); // Convert to 'standard' ISO8601 1298 if (date.indexOf(',') != -1) // Trim milliseconds 1299 date = date.substring(0, date.indexOf(',')); 1300 if (date.matches("\\d{4}")) 1301 date += "-01-01T00:00:00"; 1302 else if (date.matches("\\d{4}\\-\\d{2}")) 1303 date += "-01T00:00:00"; 1304 else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}")) 1305 date += "T00:00:00"; 1306 else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}")) 1307 date += ":00:00"; 1308 else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}")) 1309 date += ":00"; 1310 return DatatypeConverter.parseDateTime(date); 1311 } 1312 1313 /** 1314 * Parses an ISO8601 string into a date. 1315 * 1316 * <p> 1317 * Supports any of the following formats: 1318 * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c> 1319 * 1320 * @param date The date string. 1321 * @return The parsed date. 1322 * @throws IllegalArgumentException Value was not a valid date. 1323 */ 1324 public static Date parseIsoDate(String date) throws IllegalArgumentException { 1325 if (Utils.isEmpty(date)) 1326 return null; 1327 return parseIsoCalendar(date).getTime(); // NOSONAR - NPE not possible. 1328 } 1329 1330 /** 1331 * Converts a string containing a possible multiplier suffix to a long. 1332 * 1333 * <p> 1334 * The string can contain any of the following multiplier suffixes: 1335 * <ul> 1336 * <li><js>"K"</js> - x 1024 1337 * <li><js>"M"</js> - x 1024*1024 1338 * <li><js>"G"</js> - x 1024*1024*1024 1339 * <li><js>"T"</js> - x 1024*1024*1024*1024 1340 * <li><js>"P"</js> - x 1024*1024*1024*1024*1024 1341 * <li><js>"k"</js> - x 1000 1342 * <li><js>"m"</js> - x 1000*1000 1343 * <li><js>"g"</js> - x 1000*1000*1000 1344 * <li><js>"t"</js> - x 1000*1000*1000*1000 1345 * <li><js>"p"</js> - x 1000*1000*1000*1000*1000 1346 * </ul> 1347 * 1348 * @param s The string to parse. 1349 * @return The parsed value. 1350 */ 1351 public static long parseLongWithSuffix(String s) { 1352 Utils.assertArgNotNull("s", s); 1353 var m = multiplier2(s); 1354 if (m == 1) 1355 return Long.decode(s); 1356 return Long.decode(s.substring(0, s.length()-1).trim()) * m; // NOSONAR - NPE not possible here. 1357 } 1358 1359 /** 1360 * Parses a number from the specified string. 1361 * 1362 * @param s The string to parse the number from. 1363 * @param type 1364 * The number type to created. 1365 * Can be any of the following: 1366 * <ul> 1367 * <li> Integer 1368 * <li> Double 1369 * <li> Float 1370 * <li> Long 1371 * <li> Short 1372 * <li> Byte 1373 * <li> BigInteger 1374 * <li> BigDecimal 1375 * </ul> 1376 * If <jk>null</jk> or <c>Number</c>, uses the best guess. 1377 * @return The parsed number, or <jk>null</jk> if the string was null. 1378 */ 1379 public static Number parseNumber(String s, Class<? extends Number> type) { 1380 if (s == null) 1381 return null; 1382 if (s.isEmpty()) 1383 s = "0"; 1384 if (type == null) 1385 type = Number.class; 1386 1387 // Determine the data type if it wasn't specified. 1388 var isAutoDetect = (type == Number.class); 1389 var isDecimal = false; 1390 if (isAutoDetect) { 1391 // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how 1392 // long the string is. 1393 // An integer range is -2,147,483,648 to 2,147,483,647 1394 // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807 1395 isDecimal = isDecimal(s); 1396 if (isDecimal) { 1397 if (s.length() > 20) 1398 type = Double.class; 1399 else if (s.length() >= 10) 1400 type = Long.class; 1401 else 1402 type = Integer.class; 1403 } 1404 else if (isFloat(s)) 1405 type = Double.class; 1406 else 1407 throw new NumberFormatException(s); 1408 } 1409 1410 if (type == Double.class || type == Double.TYPE) { 1411 var d = Double.valueOf(s); 1412 var f = Float.valueOf(s); 1413 if (isAutoDetect && (!isDecimal) && d.toString().equals(f.toString())) 1414 return f; 1415 return d; 1416 } 1417 if (type == Float.class || type == Float.TYPE) 1418 return Float.valueOf(s); 1419 if (type == BigDecimal.class) 1420 return new BigDecimal(s); 1421 if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) { 1422 try { 1423 var l = Long.decode(s); 1424 if (type == AtomicLong.class) 1425 return new AtomicLong(l); 1426 if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) { 1427 // This occurs if the string is 10 characters long but is still a valid integer value. 1428 return l.intValue(); 1429 } 1430 return l; 1431 } catch (NumberFormatException e) { 1432 if (isAutoDetect) { 1433 // This occurs if the string is 20 characters long but still falls outside the range of a valid long. 1434 return Double.valueOf(s); 1435 } 1436 throw e; 1437 } 1438 } 1439 if (type == Integer.class || type == Integer.TYPE) 1440 return Integer.decode(s); 1441 if (type == Short.class || type == Short.TYPE) 1442 return Short.decode(s); 1443 if (type == Byte.class || type == Byte.TYPE) 1444 return Byte.decode(s); 1445 if (type == BigInteger.class) 1446 return new BigInteger(s); 1447 if (type == AtomicInteger.class) 1448 return new AtomicInteger(Integer.decode(s)); 1449 throw new NumberFormatException("Unsupported Number type: "+type.getName()); 1450 } 1451 1452 /** 1453 * Generated a random UUID with the specified number of characters. 1454 * 1455 * <p> 1456 * Characters are composed of lower-case ASCII letters and numbers only. 1457 * 1458 * <p> 1459 * This method conforms to the restrictions for hostnames as specified in <a class="doclink" href="https://tools.ietf.org/html/rfc952">RFC 952</a> 1460 * Since each character has 36 possible values, the square approximation formula for the number of generated IDs 1461 * that would produce a 50% chance of collision is: 1462 * <c>sqrt(36^N)</c>. 1463 * Dividing this number by 10 gives you an approximation of the number of generated IDs needed to produce a 1464 * <1% chance of collision. 1465 * 1466 * <p> 1467 * For example, given 5 characters, the number of generated IDs need to produce a <1% chance of collision would 1468 * be: 1469 * <c>sqrt(36^5)/10=777</c> 1470 * 1471 * @param numchars The number of characters in the generated UUID. 1472 * @return A new random UUID. 1473 */ 1474 public static String random(int numchars) { 1475 var sb = new StringBuilder(numchars); 1476 for (var i = 0; i < numchars; i++) { 1477 var c = RANDOM.nextInt(36) + 97; 1478 if (c > 'z') 1479 c -= ('z'-'0'+1); 1480 sb.append((char)c); 1481 } 1482 return sb.toString(); 1483 } 1484 1485 /** 1486 * Creates a repeated pattern. 1487 * 1488 * @param count The number of times to repeat the pattern. 1489 * @param pattern The pattern to repeat. 1490 * @return A new string consisting of the repeated pattern. 1491 */ 1492 public static String repeat(int count, String pattern) { 1493 var sb = new StringBuilder(pattern.length() * count); 1494 for (var i = 0; i < count; i++) 1495 sb.append(pattern); 1496 return sb.toString(); 1497 } 1498 1499 /** 1500 * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters. 1501 * 1502 * @param s The string to replace unicode sequences in. 1503 * @return A string with unicode sequences replaced. 1504 */ 1505 public static String replaceUnicodeSequences(String s) { 1506 1507 if (s.indexOf('\\') == -1) 1508 return s; 1509 1510 var p = Pattern.compile("\\\\u(\\p{XDigit}{4})"); 1511 var m = p.matcher(s); 1512 var sb = new StringBuffer(s.length()); 1513 1514 while (m.find()) { 1515 var ch = String.valueOf((char) Integer.parseInt(m.group(1), 16)); 1516 m.appendReplacement(sb, Matcher.quoteReplacement(ch)); 1517 } 1518 1519 m.appendTail(sb); 1520 return sb.toString(); 1521 } 1522 1523 /** 1524 * Simple utility for replacing variables of the form <js>"{key}"</js> with values in the specified map. 1525 * 1526 * <p> 1527 * Nested variables are supported in both the input string and map values. 1528 * 1529 * <p> 1530 * If the map does not contain the specified value, the variable is not replaced. 1531 * 1532 * <p> 1533 * <jk>null</jk> values in the map are treated as blank strings. 1534 * 1535 * @param s The string containing variables to replace. 1536 * @param m The map containing the variable values. 1537 * @return The new string with variables replaced, or the original string if it didn't have variables in it. 1538 */ 1539 public static String replaceVars(String s, Map<String,Object> m) { 1540 1541 if (s == null) 1542 return null; 1543 1544 if (m == null || m.isEmpty() || s.indexOf('{') == -1) 1545 return s; 1546 1547 final int 1548 S1 = 1, // Not in variable, looking for '{' 1549 S2 = 2; // Found '{', Looking for '}' 1550 1551 var state = S1; 1552 var hasInternalVar = false; 1553 var x = 0; 1554 var depth = 0; 1555 var length = s.length(); 1556 var out = new StringBuilder(); 1557 1558 for (var i = 0; i < length; i++) { 1559 var c = s.charAt(i); 1560 if (state == S1) { 1561 if (c == '{') { 1562 state = S2; 1563 x = i; 1564 } else { 1565 out.append(c); 1566 } 1567 } else /* state == S2 */ { 1568 if (c == '{') { 1569 depth++; 1570 hasInternalVar = true; 1571 } else if (c == '}') { 1572 if (depth > 0) { 1573 depth--; 1574 } else { 1575 var key = s.substring(x+1, i); 1576 key = (hasInternalVar ? replaceVars(key, m) : key); 1577 hasInternalVar = false; 1578 if (! m.containsKey(key)) 1579 out.append('{').append(key).append('}'); 1580 else { 1581 var val = m.get(key); 1582 if (val == null) 1583 val = ""; 1584 var v = val.toString(); 1585 // If the replacement also contains variables, replace them now. 1586 if (v.indexOf('{') != -1) 1587 v = replaceVars(v, m); 1588 out.append(v); 1589 } 1590 state = 1; 1591 } 1592 } 1593 } 1594 } 1595 return out.toString(); 1596 } 1597 1598 /** 1599 * Skips over comment sequences in a StringReader. 1600 * 1601 * @param r The StringReader positioned at the start of a comment. 1602 * @throws IOException If an I/O error occurs. 1603 */ 1604 private static void skipComments(StringReader r) throws IOException { 1605 var c = r.read(); 1606 // "/* */" style comments 1607 if (c == '*') { 1608 while (c != -1) 1609 if ((c = r.read()) == '*') 1610 if ((c = r.read()) == '/') // NOSONAR - Intentional. 1611 return; 1612 // "//" style comments 1613 } else if (c == '/') { 1614 while (c != -1) { 1615 c = r.read(); 1616 if (c == -1 || c == '\n') 1617 return; 1618 } 1619 } 1620 } 1621 1622 /** 1623 * An efficient method for checking if a string starts with a character. 1624 * 1625 * @param s The string to check. Can be <jk>null</jk>. 1626 * @param c The character to check for. 1627 * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character. 1628 */ 1629 public static boolean startsWith(String s, char c) { 1630 if (s != null) { 1631 var i = s.length(); 1632 if (i > 0) 1633 return s.charAt(0) == c; 1634 } 1635 return false; 1636 } 1637 1638 /** 1639 * Converts the specified array to a string. 1640 * 1641 * @param o The array to convert to a string. 1642 * @return The array converted to a string, or <jk>null</jk> if the object was null. 1643 */ 1644 public static String stringifyDeep(Object o) { 1645 if (o == null) 1646 return null; 1647 if (! isArray(o)) 1648 return o.toString(); 1649 if (o.getClass().getComponentType().isPrimitive()) 1650 return PRIMITIVE_ARRAY_STRINGIFIERS.get(o.getClass()).apply(o); 1651 return Arrays.deepToString((Object[])o); 1652 } 1653 1654 /** 1655 * Strips the first and last character from a string. 1656 * 1657 * @param s The string to strip. 1658 * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2. 1659 */ 1660 public static String strip(String s) { 1661 if (s == null || s.length() <= 1) 1662 return s; 1663 return s.substring(1, s.length()-1); 1664 } 1665 1666 /** 1667 * Strips invalid characters such as CTRL characters from a string meant to be encoded 1668 * as an HTTP header value. 1669 * 1670 * @param s The string to strip chars from. 1671 * @return The string with invalid characters removed. 1672 */ 1673 public static String stripInvalidHttpHeaderChars(String s) { 1674 1675 if (s == null) 1676 return null; 1677 1678 var needsReplace = false; 1679 for (var i = 0; i < s.length() && ! needsReplace; i++) 1680 needsReplace |= httpHeaderChars.contains(s.charAt(i)); 1681 1682 if (! needsReplace) 1683 return s; 1684 1685 var sb = new StringBuilder(s.length()); 1686 for (var i = 0; i < s.length(); i++) { 1687 var c = s.charAt(i); 1688 if (httpHeaderChars.contains(c)) 1689 sb.append(c); 1690 } 1691 1692 return sb.toString(); 1693 } 1694 1695 /** 1696 * Converts the specified object to a comma-delimited list. 1697 * 1698 * @param o The object to convert. 1699 * @return The specified object as a comma-delimited list. 1700 */ 1701 public static String toCdl(Object o) { 1702 if (o == null) 1703 return null; 1704 if (isArray(o)) { 1705 var sb = new StringBuilder(); 1706 for (int i = 0, j = Array.getLength(o); i < j; i++) { 1707 if (i > 0) 1708 sb.append(", "); 1709 sb.append(Array.get(o, i)); 1710 } 1711 return sb.toString(); 1712 } 1713 if (o instanceof Collection) 1714 return Utils.join((Collection<?>)o, ", "); 1715 return o.toString(); 1716 } 1717 1718 /** 1719 * Converts the specified byte into a 2 hexadecimal characters. 1720 * 1721 * @param b The number to convert to hex. 1722 * @return A <code><jk>char</jk>[2]</code> containing the specified characters. 1723 */ 1724 public static String toHex(byte b) { 1725 var c = new char[2]; 1726 var v = b & 0xFF; 1727 c[0] = hexArray[v >>> 4]; 1728 c[1] = hexArray[v & 0x0F]; 1729 return new String(c); 1730 } 1731 1732 /** 1733 * Converts a byte array into a simple hexadecimal character string. 1734 * 1735 * @param bytes The bytes to convert to hexadecimal. 1736 * @return A new string consisting of hexadecimal characters. 1737 */ 1738 public static String toHex(byte[] bytes) { 1739 var sb = new StringBuilder(bytes.length * 2); 1740 for (var element : bytes) { 1741 var v = element & 0xFF; 1742 sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]); 1743 } 1744 return sb.toString(); 1745 } 1746 1747 /** 1748 * Converts the contents of the specified input stream to a hex string. 1749 * 1750 * @param is The input stream to convert. 1751 * @return The hex string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>. 1752 */ 1753 public static String toHex(InputStream is) { 1754 return safe(()->is == null ? null : toHex(readBytes(is))); 1755 } 1756 1757 /** 1758 * Converts the specified number into a 2 hexadecimal characters. 1759 * 1760 * @param num The number to convert to hex. 1761 * @return A <code><jk>char</jk>[2]</code> containing the specified characters. 1762 */ 1763 public static char[] toHex2(int num) { 1764 if (num < 0 || num > 255) 1765 throw new NumberFormatException("toHex2 can only be used on numbers between 0 and 255"); 1766 var n = new char[2]; 1767 var a = num%16; 1768 n[1] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1769 a = (num/16)%16; 1770 n[0] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1771 return n; 1772 } 1773 1774 /** 1775 * Converts the specified number into a 4 hexadecimal characters. 1776 * 1777 * @param num The number to convert to hex. 1778 * @return A <code><jk>char</jk>[4]</code> containing the specified characters. 1779 */ 1780 public static char[] toHex4(int num) { 1781 var n = new char[4]; 1782 var a = num%16; 1783 n[3] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1784 var base = 16; 1785 for (var i = 1; i < 4; i++) { 1786 a = (num/base)%16; 1787 base <<= 4; 1788 n[3-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1789 } 1790 return n; 1791 } 1792 1793 /** 1794 * Converts the specified number into a 8 hexadecimal characters. 1795 * 1796 * @param num The number to convert to hex. 1797 * @return A <code><jk>char</jk>[8]</code> containing the specified characters. 1798 */ 1799 public static char[] toHex8(long num) { 1800 var n = new char[8]; 1801 var a = num%16; 1802 n[7] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1803 var base = 16; 1804 for (var i = 1; i < 8; i++) { 1805 a = (num/base)%16; 1806 base <<= 4; 1807 n[7-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1808 } 1809 return n; 1810 } 1811 1812 /** 1813 * Converts the specified object to an ISO8601 date string. 1814 * 1815 * @param c The object to convert. 1816 * @return The converted object. 1817 */ 1818 public static String toIsoDate(Calendar c) { 1819 return DatatypeConverter.printDate(c); 1820 } 1821 /** 1822 * Converts the specified object to an ISO8601 date-time string. 1823 * 1824 * @param c The object to convert. 1825 * @return The converted object. 1826 */ 1827 public static String toIsoDateTime(Calendar c) { 1828 return DatatypeConverter.printDateTime(c); 1829 } 1830 1831 /** 1832 * Converts the specified bytes into a readable string. 1833 * 1834 * @param b The number to convert to hex. 1835 * @return A <code><jk>char</jk>[2]</code> containing the specified characters. 1836 */ 1837 public static String toReadableBytes(byte[] b) { 1838 var sb = new StringBuilder(); 1839 for (var b2 : b) 1840 sb.append((b2 < ' ' || b2 > 'z') ? String.format("[%02X]", b2) : (char)b2 + " "); 1841 sb.append("\n"); 1842 for (var b2 : b) 1843 sb.append(String.format("[%02X]", b2)); 1844 return sb.toString(); 1845 } 1846 1847 /** 1848 * Same as {@link #toHex(byte[])} but puts spaces between the byte strings. 1849 * 1850 * @param bytes The bytes to convert to hexadecimal. 1851 * @return A new string consisting of hexadecimal characters. 1852 */ 1853 public static String toSpacedHex(byte[] bytes) { 1854 var sb = new StringBuilder(bytes.length * 3); 1855 for (var j = 0; j < bytes.length; j++) { 1856 if (j > 0) 1857 sb.append(' '); 1858 var v = bytes[j] & 0xFF; 1859 sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]); 1860 } 1861 return sb.toString(); 1862 } 1863 1864 /** 1865 * Converts the specified object to a URI. 1866 * 1867 * @param o The object to convert to a URI. 1868 * @return A new URI, or the same object if the object was already a URI, or 1869 */ 1870 public static URI toURI(Object o) { 1871 if (o == null || o instanceof URI) 1872 return (URI)o; 1873 try { 1874 return new URI(o.toString()); 1875 } catch (URISyntaxException e) { 1876 throw asRuntimeException(e); 1877 } 1878 } 1879 1880 /** 1881 * Converts the specified byte array to a UTF-8 string. 1882 * 1883 * @param b The byte array to convert. 1884 * @return The UTF-8 string representation, or <jk>null</jk> if the array is <jk>null</jk>. 1885 */ 1886 public static String toUtf8(byte[] b) { 1887 return b == null ? null : new String(b, IOUtils.UTF8); 1888 } 1889 1890 /** 1891 * Converts the contents of the specified input stream to a UTF-8 string. 1892 * 1893 * @param is The input stream to convert. 1894 * @return The UTF-8 string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>. 1895 */ 1896 public static String toUtf8(InputStream is) { 1897 return safe(()->is == null ? null : new String(readBytes(is), IOUtils.UTF8)); 1898 } 1899 1900 /** 1901 * Same as {@link String#trim()} but prevents <c>NullPointerExceptions</c>. 1902 * 1903 * @param s The string to trim. 1904 * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. 1905 */ 1906 public static String trim(String s) { 1907 if (s == null) 1908 return null; 1909 return s.trim(); 1910 } 1911 1912 /** 1913 * Trims whitespace characters from the end of the specified string. 1914 * 1915 * @param s The string to trim. 1916 * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. 1917 */ 1918 public static String trimEnd(String s) { 1919 if (s != null) 1920 while (Utils.isNotEmpty(s) && isWhitespace(s.charAt(s.length()-1))) 1921 s = s.substring(0, s.length()-1); 1922 return s; 1923 } 1924 1925 /** 1926 * Trims <js>'/'</js> characters from the beginning of the specified string. 1927 * 1928 * @param s The string to trim. 1929 * @return A new trimmed string, or the same string if no trimming was necessary. 1930 */ 1931 public static String trimLeadingSlashes(String s) { 1932 if (s == null) 1933 return null; 1934 while (Utils.isNotEmpty(s) && s.charAt(0) == '/') 1935 s = s.substring(1); 1936 return s; 1937 } 1938 1939 /** 1940 * Trims <js>'/'</js> characters from both the start and end of the specified string. 1941 * 1942 * @param s The string to trim. 1943 * @return A new trimmed string, or the same string if no trimming was necessary. 1944 */ 1945 public static String trimSlashes(String s) { 1946 if (s == null) 1947 return null; 1948 if (s.isEmpty()) 1949 return s; 1950 while (endsWith(s, '/')) 1951 s = s.substring(0, s.length()-1); 1952 while (Utils.isNotEmpty(s) && s.charAt(0) == '/') // NOSONAR - NPE not possible here. 1953 s = s.substring(1); 1954 return s; 1955 } 1956 1957 /** 1958 * Trims <js>'/'</js> and space characters from both the start and end of the specified string. 1959 * 1960 * @param s The string to trim. 1961 * @return A new trimmed string, or the same string if no trimming was necessary. 1962 */ 1963 public static String trimSlashesAndSpaces(String s) { 1964 if (s == null) 1965 return null; 1966 while (Utils.isNotEmpty(s) && (s.charAt(s.length()-1) == '/' || isWhitespace(s.charAt(s.length()-1)))) 1967 s = s.substring(0, s.length()-1); 1968 while (Utils.isNotEmpty(s) && (s.charAt(0) == '/' || isWhitespace(s.charAt(0)))) 1969 s = s.substring(1); 1970 return s; 1971 } 1972 1973 /** 1974 * Trims whitespace characters from the beginning of the specified string. 1975 * 1976 * @param s The string to trim. 1977 * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. 1978 */ 1979 public static String trimStart(String s) { 1980 if (s != null) 1981 while (Utils.isNotEmpty(s) && isWhitespace(s.charAt(0))) 1982 s = s.substring(1); 1983 return s; 1984 } 1985 1986 /** 1987 * Trims <js>'/'</js> characters from the end of the specified string. 1988 * 1989 * @param s The string to trim. 1990 * @return A new trimmed string, or the same string if no trimming was necessary. 1991 */ 1992 public static String trimTrailingSlashes(String s) { 1993 if (s == null) 1994 return null; 1995 while (endsWith(s, '/')) 1996 s = s.substring(0, s.length()-1); 1997 return s; 1998 } 1999 2000 /** 2001 * Removes escape characters from the specified characters. 2002 * 2003 * @param s The string to remove escape characters from. 2004 * @param escaped The characters escaped. 2005 * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>. 2006 */ 2007 public static String unEscapeChars(String s, AsciiSet escaped) { 2008 if (s == null || s.isEmpty()) 2009 return s; 2010 var count = 0; 2011 for (var i = 0; i < s.length(); i++) 2012 if (escaped.contains(s.charAt(i))) 2013 count++; 2014 if (count == 0) 2015 return s; 2016 var sb = new StringBuffer(s.length()-count); 2017 for (var i = 0; i < s.length(); i++) { 2018 var c = s.charAt(i); 2019 2020 if (c == '\\') { 2021 if (i+1 != s.length()) { // NOSONAR - Intentional. 2022 var c2 = s.charAt(i+1); 2023 if (escaped.contains(c2)) { 2024 i++; // NOSONAR - Intentional. 2025 } else if (c2 == '\\') { 2026 sb.append('\\'); 2027 i++; // NOSONAR - Intentional. 2028 } 2029 } 2030 } 2031 sb.append(s.charAt(i)); 2032 } 2033 return sb.toString(); 2034 } 2035 2036 /** 2037 * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for the specified character. 2038 * 2039 * @param c The character to create a sequence for. 2040 * @return An escaped-unicode sequence. 2041 */ 2042 public static String unicodeSequence(char c) { 2043 var sb = new StringBuilder(6); 2044 sb.append('\\').append('u'); 2045 for (var cc : toHex4(c)) 2046 sb.append(cc); 2047 return sb.toString(); 2048 } 2049 2050 /** 2051 * Decodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme. 2052 * 2053 * @param s The string to decode. 2054 * @return The decoded string, or <jk>null</jk> if input is <jk>null</jk>. 2055 */ 2056 public static String urlDecode(String s) { 2057 2058 if (s == null) 2059 return s; 2060 2061 var needsDecode = false; 2062 for (var i = 0; i < s.length() && ! needsDecode; i++) { 2063 var c = s.charAt(i); 2064 if (c == '+' || c == '%') 2065 needsDecode = true; 2066 } 2067 2068 if (needsDecode) { 2069 try { 2070 return URLDecoder.decode(s, "UTF-8"); 2071 } catch (UnsupportedEncodingException e) {/* Won't happen */} 2072 } 2073 return s; 2074 } 2075 2076 /** 2077 * Encodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme. 2078 * 2079 * @param s The string to encode. 2080 * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>. 2081 */ 2082 public static String urlEncode(String s) { 2083 2084 if (s == null) 2085 return null; 2086 2087 var needsEncode = false; 2088 2089 for (var i = 0; i < s.length() && ! needsEncode; i++) 2090 needsEncode |= (! unencodedChars.contains(s.charAt(i))); 2091 2092 if (needsEncode) { 2093 try { 2094 return URLEncoder.encode(s, "UTF-8"); 2095 } catch (UnsupportedEncodingException e) {/* Won't happen */} 2096 } 2097 2098 return s; 2099 } 2100 2101 /** 2102 * Same as {@link #urlEncode(String)} except only escapes characters that absolutely need to be escaped. 2103 * 2104 * @param s The string to escape. 2105 * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>. 2106 */ 2107 public static String urlEncodeLax(String s) { 2108 if (s == null) 2109 return null; 2110 var needsEncode = false; 2111 for (var i = 0; i < s.length() && ! needsEncode; i++) 2112 needsEncode |= (! unencodedCharsLax.contains(s.charAt(i))); 2113 if (needsEncode) { 2114 var sb = new StringBuilder(s.length()*2); 2115 for (var i = 0; i < s.length(); i++) { 2116 var c = s.charAt(i); 2117 if (unencodedCharsLax.contains(c)) 2118 sb.append(c); 2119 else if (c == ' ') 2120 sb.append("+"); 2121 else if (c <= 127) 2122 sb.append('%').append(toHex2(c)); 2123 else 2124 try { 2125 sb.append(URLEncoder.encode(""+c, "UTF-8")); // Yuck. 2126 } catch (UnsupportedEncodingException e) { 2127 // Not possible. 2128 } 2129 } 2130 s = sb.toString(); 2131 } 2132 return s; 2133 } 2134 2135 /** 2136 * Similar to {@link URLEncoder#encode(String, String)} but doesn't encode <js>"/"</js> characters. 2137 * 2138 * @param o The object to encode. 2139 * @return The URL encoded string, or <jk>null</jk> if the object was null. 2140 */ 2141 public static String urlEncodePath(Object o) { 2142 2143 if (o == null) 2144 return null; 2145 2146 var s = Utils.s(o); 2147 2148 var needsEncode = false; 2149 for (var i = 0; i < s.length() && ! needsEncode; i++) 2150 needsEncode = URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i)); 2151 if (! needsEncode) 2152 return s; 2153 2154 var sb = new StringBuilder(); 2155 var caw = new CharArrayWriter(); 2156 var caseDiff = ('a' - 'A'); 2157 2158 for (var i = 0; i < s.length();) { 2159 var c = s.charAt(i); 2160 if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) { 2161 sb.append(c); 2162 i++; // NOSONAR - Intentional. 2163 } else { 2164 if (c == ' ') { 2165 sb.append('+'); 2166 i++; // NOSONAR - Intentional. 2167 } else { 2168 do { 2169 caw.write(c); 2170 if (c >= 0xD800 && c <= 0xDBFF) { 2171 if ((i+1) < s.length()) { // NOSONAR - Intentional. 2172 int d = s.charAt(i+1); 2173 if (d >= 0xDC00 && d <= 0xDFFF) { 2174 caw.write(d); 2175 i++; // NOSONAR - Intentional. 2176 } 2177 } 2178 } 2179 i++; // NOSONAR - Intentional. 2180 } while (i < s.length() && !URL_ENCODE_PATHINFO_VALIDCHARS.contains((c = s.charAt(i)))); // NOSONAR - Intentional. 2181 2182 caw.flush(); 2183 var s2 = new String(caw.toCharArray()); 2184 var ba = s2.getBytes(IOUtils.UTF8); 2185 for (var element : ba) { 2186 sb.append('%'); 2187 var ch = forDigit((element >> 4) & 0xF, 16); 2188 if (isLetter(ch)) { 2189 ch -= caseDiff; 2190 } 2191 sb.append(ch); 2192 ch = forDigit(element & 0xF, 16); 2193 if (isLetter(ch)) { 2194 ch -= caseDiff; 2195 } 2196 sb.append(ch); 2197 } 2198 caw.reset(); 2199 } 2200 } 2201 } 2202 return sb.toString(); 2203 } 2204 2205 /** 2206 * Constructor. 2207 */ 2208 protected StringUtils() {} 2209}