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.common.internal; 014 015import static org.apache.juneau.common.internal.ArgUtils.*; 016import static org.apache.juneau.common.internal.IOUtils.*; 017import static org.apache.juneau.common.internal.ThrowableUtils.*; 018 019import java.io.*; 020import java.lang.reflect.*; 021import java.math.*; 022import java.net.*; 023import java.nio.*; 024import java.nio.charset.*; 025import java.text.*; 026import java.util.*; 027import java.util.concurrent.*; 028import java.util.concurrent.atomic.*; 029import java.util.function.*; 030import java.util.regex.*; 031import java.util.stream.*; 032import java.util.zip.*; 033 034import javax.xml.bind.*; 035 036/** 037 * Reusable string utility methods. 038 */ 039public final class StringUtils { 040 041 /** 042 * Predicate check to filter out null and empty strings. 043 */ 044 public static final Predicate<String> NOT_EMPTY = StringUtils::isNotEmpty; 045 046 private static final AsciiSet numberChars = AsciiSet.create("-xX.+-#pP0123456789abcdefABCDEF"); 047 private static final AsciiSet firstNumberChars =AsciiSet.create("+-.#0123456789"); 048 private static final AsciiSet octChars = AsciiSet.create("01234567"); 049 private static final AsciiSet decChars = AsciiSet.create("0123456789"); 050 private static final AsciiSet hexChars = AsciiSet.create("0123456789abcdefABCDEF"); 051 052 // Maps 6-bit nibbles to BASE64 characters. 053 private static final char[] base64m1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); 054 055 // Characters that do not need to be URL-encoded 056 private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.!~*'()\\").build(); 057 058 // Characters that really do not need to be URL-encoded 059 private static final AsciiSet unencodedCharsLax = unencodedChars.copy() 060 .chars(":@$,") // reserved, but can't be confused in a query parameter. 061 .chars("{}|\\^[]`") // unwise characters. 062 .build(); 063 064 // Valid HTTP header characters (including quoted strings and comments). 065 private static final AsciiSet httpHeaderChars = AsciiSet 066 .create() 067 .chars("\t -") 068 .ranges("!-[","]-}") 069 .build(); 070 071 // Maps BASE64 characters to 6-bit nibbles. 072 private static final byte[] base64m2 = new byte[128]; 073 static { 074 for (int i = 0; i < 64; i++) 075 base64m2[base64m1[i]] = (byte)i; 076 } 077 078 /** 079 * Parses a number from the specified string. 080 * 081 * @param s The string to parse the number from. 082 * @param type 083 * The number type to created. 084 * Can be any of the following: 085 * <ul> 086 * <li> Integer 087 * <li> Double 088 * <li> Float 089 * <li> Long 090 * <li> Short 091 * <li> Byte 092 * <li> BigInteger 093 * <li> BigDecimal 094 * </ul> 095 * If <jk>null</jk> or <c>Number</c>, uses the best guess. 096 * @return The parsed number, or <jk>null</jk> if the string was null. 097 */ 098 public static Number parseNumber(String s, Class<? extends Number> type) { 099 if (s == null) 100 return null; 101 if (s.isEmpty()) 102 s = "0"; 103 if (type == null) 104 type = Number.class; 105 106 // Determine the data type if it wasn't specified. 107 boolean isAutoDetect = (type == Number.class); 108 boolean isDecimal = false; 109 if (isAutoDetect) { 110 // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how 111 // long the string is. 112 // An integer range is -2,147,483,648 to 2,147,483,647 113 // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807 114 isDecimal = isDecimal(s); 115 if (isDecimal) { 116 if (s.length() > 20) 117 type = Double.class; 118 else if (s.length() >= 10) 119 type = Long.class; 120 else 121 type = Integer.class; 122 } 123 else if (isFloat(s)) 124 type = Double.class; 125 else 126 throw new NumberFormatException(s); 127 } 128 129 if (type == Double.class || type == Double.TYPE) { 130 Double d = Double.valueOf(s); 131 Float f = Float.valueOf(s); 132 if (isAutoDetect && (!isDecimal) && d.toString().equals(f.toString())) 133 return f; 134 return d; 135 } 136 if (type == Float.class || type == Float.TYPE) 137 return Float.valueOf(s); 138 if (type == BigDecimal.class) 139 return new BigDecimal(s); 140 if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) { 141 try { 142 Long l = Long.decode(s); 143 if (type == AtomicLong.class) 144 return new AtomicLong(l); 145 if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) { 146 // This occurs if the string is 10 characters long but is still a valid integer value. 147 return l.intValue(); 148 } 149 return l; 150 } catch (NumberFormatException e) { 151 if (isAutoDetect) { 152 // This occurs if the string is 20 characters long but still falls outside the range of a valid long. 153 return Double.valueOf(s); 154 } 155 throw e; 156 } 157 } 158 if (type == Integer.class || type == Integer.TYPE) 159 return Integer.decode(s); 160 if (type == Short.class || type == Short.TYPE) 161 return Short.decode(s); 162 if (type == Byte.class || type == Byte.TYPE) 163 return Byte.decode(s); 164 if (type == BigInteger.class) 165 return new BigInteger(s); 166 if (type == AtomicInteger.class) 167 return new AtomicInteger(Integer.decode(s)); 168 throw new NumberFormatException("Unsupported Number type: "+type.getName()); 169 } 170 171 private static final Pattern fpRegex = Pattern.compile( 172 "[+-]?(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]*" 173 ); 174 175 /** 176 * Converts a <c>String</c> to a <c>Character</c> 177 * 178 * @param o The string to convert. 179 * @return The first character of the string if the string is of length 0, or <jk>null</jk> if the string is <jk>null</jk> or empty. 180 */ 181 public static Character parseCharacter(Object o) { 182 if (o == null) 183 return null; 184 String s = o.toString(); 185 if (s.isEmpty()) 186 return null; 187 if (s.length() == 1) 188 return s.charAt(0); 189 throw new IllegalArgumentException("Invalid character: '" + s + "'"); 190 } 191 192 /** 193 * Returns <jk>true</jk> if this string can be parsed by {@link #parseNumber(String, Class)}. 194 * 195 * @param s The string to check. 196 * @return <jk>true</jk> if this string can be parsed without causing an exception. 197 */ 198 public static boolean isNumeric(String s) { 199 if (s == null || s.isEmpty() || ! isFirstNumberChar(s.charAt(0))) 200 return false; 201 return isDecimal(s) || isFloat(s); 202 } 203 204 /** 205 * Returns <jk>true</jk> if the specified character is a valid first character for a number. 206 * 207 * @param c The character to test. 208 * @return <jk>true</jk> if the specified character is a valid first character for a number. 209 */ 210 public static boolean isFirstNumberChar(char c) { 211 return firstNumberChars.contains(c); 212 } 213 214 /** 215 * Returns <jk>true</jk> if the specified string is a floating point number. 216 * 217 * @param s The string to check. 218 * @return <jk>true</jk> if the specified string is a floating point number. 219 */ 220 public static boolean isFloat(String s) { 221 if (s == null || s.isEmpty()) 222 return false; 223 if (! firstNumberChars.contains(s.charAt(0))) 224 return (s.equals("NaN") || s.equals("Infinity")); 225 int i = 0; 226 int length = s.length(); 227 char c = s.charAt(0); 228 if (c == '+' || c == '-') 229 i++; 230 if (i == length) 231 return false; 232 c = s.charAt(i++); 233 if (c == '.' || decChars.contains(c)) { 234 return fpRegex.matcher(s).matches(); 235 } 236 return false; 237 } 238 239 /** 240 * Returns <jk>true</jk> if the specified string is numeric. 241 * 242 * @param s The string to check. 243 * @return <jk>true</jk> if the specified string is numeric. 244 */ 245 public static boolean isDecimal(String s) { 246 if (s == null || s.isEmpty() || ! firstNumberChars.contains(s.charAt(0))) 247 return false; 248 int i = 0; 249 int length = s.length(); 250 char c = s.charAt(0); 251 boolean isPrefixed = false; 252 if (c == '+' || c == '-') { 253 isPrefixed = true; 254 i++; 255 } 256 if (i == length) 257 return false; 258 c = s.charAt(i++); 259 if (c == '0' && length > (isPrefixed ? 2 : 1)) { 260 c = s.charAt(i++); 261 if (c == 'x' || c == 'X') { 262 for (int j = i; j < length; j++) { 263 if (! hexChars.contains(s.charAt(j))) 264 return false; 265 } 266 } else if (octChars.contains(c)) { 267 for (int j = i; j < length; j++) 268 if (! octChars.contains(s.charAt(j))) 269 return false; 270 } else { 271 return false; 272 } 273 } else if (c == '#') { 274 for (int j = i; j < length; j++) { 275 if (! hexChars.contains(s.charAt(j))) 276 return false; 277 } 278 } else if (decChars.contains(c)) { 279 for (int j = i; j < length; j++) 280 if (! decChars.contains(s.charAt(j))) 281 return false; 282 } else { 283 return false; 284 } 285 return true; 286 } 287 288 /** 289 * Join the specified tokens into a delimited string. 290 * 291 * @param tokens The tokens to join. 292 * @param separator The delimiter. 293 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 294 */ 295 public static String join(Object[] tokens, String separator) { 296 if (tokens == null) 297 return null; 298 StringBuilder sb = new StringBuilder(); 299 for (int i = 0; i < tokens.length; i++) { 300 if (i > 0) 301 sb.append(separator); 302 sb.append(tokens[i]); 303 } 304 return sb.toString(); 305 } 306 307 /** 308 * Join the specified tokens into a delimited string. 309 * 310 * @param tokens The tokens to join. 311 * @param d The delimiter. 312 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 313 */ 314 public static String join(Collection<?> tokens, String d) { 315 if (tokens == null) 316 return null; 317 return join(tokens, d, new StringBuilder()).toString(); 318 } 319 320 /** 321 * Join the specified tokens into a delimited string. 322 * 323 * @param tokens The tokens to join. 324 * @param d The delimiter. 325 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 326 */ 327 public static String join(List<?> tokens, String d) { 328 if (tokens == null) 329 return null; 330 return join(tokens, d, new StringBuilder()).toString(); 331 } 332 333 /** 334 * Joins the specified tokens into a delimited string and writes the output to the specified string builder. 335 * 336 * @param tokens The tokens to join. 337 * @param d The delimiter. 338 * @param sb The string builder to append the response to. 339 * @return The same string builder passed in as <c>sb</c>. 340 */ 341 public static StringBuilder join(Collection<?> tokens, String d, StringBuilder sb) { 342 if (tokens == null) 343 return sb; 344 for (Iterator<?> iter = tokens.iterator(); iter.hasNext();) { 345 sb.append(iter.next()); 346 if (iter.hasNext()) 347 sb.append(d); 348 } 349 return sb; 350 } 351 352 /** 353 * Joins the specified tokens into a delimited string and writes the output to the specified string builder. 354 * 355 * @param tokens The tokens to join. 356 * @param d The delimiter. 357 * @param sb The string builder to append the response to. 358 * @return The same string builder passed in as <c>sb</c>. 359 */ 360 public static StringBuilder join(List<?> tokens, String d, StringBuilder sb) { 361 if (tokens == null) 362 return sb; 363 for (int i = 0, j = tokens.size(); i < j; i++) { 364 if (i > 0) 365 sb.append(d); 366 sb.append(tokens.get(i)); 367 } 368 return sb; 369 } 370 371 /** 372 * Joins the specified tokens into a delimited string. 373 * 374 * @param tokens The tokens to join. 375 * @param d The delimiter. 376 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 377 */ 378 public static String join(Object[] tokens, char d) { 379 if (tokens == null) 380 return null; 381 if (tokens.length == 1) 382 return emptyIfNull(stringify(tokens[0])); 383 return join(tokens, d, new StringBuilder()).toString(); 384 } 385 386 private static AsciiSet getEscapeSet(char c) { 387 AsciiSet s = ESCAPE_SETS.get(c); 388 if (s == null) { 389 s = AsciiSet.create().chars(c, '\\').build(); 390 ESCAPE_SETS.put(c, s); 391 } 392 return s; 393 } 394 static Map<Character,AsciiSet> ESCAPE_SETS = new ConcurrentHashMap<>(); 395 396 /** 397 * Join the specified tokens into a delimited string and writes the output to the specified string builder. 398 * 399 * @param tokens The tokens to join. 400 * @param d The delimiter. 401 * @param sb The string builder to append the response to. 402 * @return The same string builder passed in as <c>sb</c>. 403 */ 404 public static StringBuilder join(Object[] tokens, char d, StringBuilder sb) { 405 if (tokens == null) 406 return sb; 407 for (int i = 0; i < tokens.length; i++) { 408 if (i > 0) 409 sb.append(d); 410 sb.append(tokens[i]); 411 } 412 return sb; 413 } 414 415 /** 416 * Join the specified tokens into a delimited string. 417 * 418 * @param tokens The tokens to join. 419 * @param d The delimiter. 420 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 421 */ 422 public static String join(int[] tokens, char d) { 423 if (tokens == null) 424 return null; 425 StringBuilder sb = new StringBuilder(); 426 for (int i = 0; i < tokens.length; i++) { 427 if (i > 0) 428 sb.append(d); 429 sb.append(tokens[i]); 430 } 431 return sb.toString(); 432 } 433 434 /** 435 * Join the specified tokens into a delimited string. 436 * 437 * @param tokens The tokens to join. 438 * @param d The delimiter. 439 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 440 */ 441 public static String join(Collection<?> tokens, char d) { 442 if (tokens == null) 443 return null; 444 StringBuilder sb = new StringBuilder(); 445 for (Iterator<?> iter = tokens.iterator(); iter.hasNext();) { 446 sb.append(iter.next()); 447 if (iter.hasNext()) 448 sb.append(d); 449 } 450 return sb.toString(); 451 } 452 453 /** 454 * Join the specified tokens into a delimited string. 455 * 456 * @param tokens The tokens to join. 457 * @param d The delimiter. 458 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 459 */ 460 public static String join(List<?> tokens, char d) { 461 if (tokens == null) 462 return null; 463 StringBuilder sb = new StringBuilder(); 464 for (int i = 0, j = tokens.size(); i < j; i++) { 465 if (i > 0) 466 sb.append(d); 467 sb.append(tokens.get(i)); 468 } 469 return sb.toString(); 470 } 471 472 /** 473 * Same as {@link #join(Collection, char)} but escapes the delimiter if found in the tokens. 474 * 475 * @param tokens The tokens to join. 476 * @param d The delimiter. 477 * @return The delimited string. If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>. 478 */ 479 public static String joine(List<?> tokens, char d) { 480 if (tokens == null) 481 return null; 482 AsciiSet as = getEscapeSet(d); 483 StringBuilder sb = new StringBuilder(); 484 for (int i = 0, j = tokens.size(); i < j; i++) { 485 if (i > 0) 486 sb.append(d); 487 sb.append(escapeChars(stringify(tokens.get(i)), as)); 488 } 489 return sb.toString(); 490 } 491 492 /** 493 * Joins tokens with newlines. 494 * 495 * @param tokens The tokens to concatenate. 496 * @return A string with the specified tokens contatenated with newlines. 497 */ 498 public static String joinnl(Object[] tokens) { 499 return join(tokens, '\n'); 500 } 501 502 /** 503 * Shortcut for calling <code>split(s, <js>','</js>)</code> 504 * 505 * @param s The string to split. Can be <jk>null</jk>. 506 * @return The tokens, or <jk>null</jk> if the string was null. 507 */ 508 public static String[] split(String s) { 509 return split(s, ','); 510 } 511 512 /** 513 * Same as {@link #split(String)} but consumes the tokens instead of creating an array. 514 * 515 * @param s The string to split. 516 * @param consumer The consumer of the tokens. 517 */ 518 public static void split(String s, Consumer<String> consumer) { 519 split(s, ',', consumer); 520 } 521 522 /** 523 * Splits a character-delimited string into a string array. 524 * 525 * <p> 526 * Does not split on escaped-delimiters (e.g. "\,"); 527 * Resulting tokens are trimmed of whitespace. 528 * 529 * <p> 530 * <b>NOTE:</b> This behavior is different than the Jakarta equivalent. 531 * split("a,b,c",',') -> {"a","b","c"} 532 * split("a, b ,c ",',') -> {"a","b","c"} 533 * split("a,,c",',') -> {"a","","c"} 534 * split(",,",',') -> {"","",""} 535 * split("",',') -> {} 536 * split(null,',') -> null 537 * split("a,b\,c,d", ',', false) -> {"a","b\,c","d"} 538 * split("a,b\\,c,d", ',', false) -> {"a","b\","c","d"} 539 * split("a,b\,c,d", ',', true) -> {"a","b,c","d"} 540 * 541 * @param s The string to split. Can be <jk>null</jk>. 542 * @param c The character to split on. 543 * @return The tokens, or <jk>null</jk> if the string was null. 544 */ 545 public static String[] split(String s, char c) { 546 return split(s, c, Integer.MAX_VALUE); 547 } 548 549 /** 550 * Same as {@link #split(String,char)} but consumes the tokens instead of creating an array. 551 * 552 * @param s The string to split. 553 * @param c The character to split on. 554 * @param consumer The consumer of the tokens. 555 */ 556 public static void split(String s, char c, Consumer<String> consumer) { 557 AsciiSet escapeChars = getEscapeSet(c); 558 559 if (isEmpty(s)) 560 return; 561 if (s.indexOf(c) == -1) { 562 consumer.accept(s); 563 return; 564 } 565 566 int x1 = 0, escapeCount = 0; 567 for (int i = 0; i < s.length(); i++) { 568 if (s.charAt(i) == '\\') 569 escapeCount++; 570 else if (s.charAt(i)==c && escapeCount % 2 == 0) { 571 String s2 = s.substring(x1, i); 572 String s3 = unEscapeChars(s2, escapeChars); 573 consumer.accept(s3.trim()); 574 x1 = i+1; 575 } 576 if (s.charAt(i) != '\\') 577 escapeCount = 0; 578 } 579 String s2 = s.substring(x1); 580 String s3 = unEscapeChars(s2, escapeChars); 581 consumer.accept(s3.trim()); 582 } 583 584 /** 585 * Same as {@link #split(String, char)} but limits the number of tokens returned. 586 * 587 * @param s The string to split. Can be <jk>null</jk>. 588 * @param c The character to split on. 589 * @param limit The maximum number of tokens to return. 590 * @return The tokens, or <jk>null</jk> if the string was null. 591 */ 592 public static String[] split(String s, char c, int limit) { 593 594 AsciiSet escapeChars = getEscapeSet(c); 595 596 if (s == null) 597 return null; 598 if (isEmpty(s)) 599 return new String[0]; 600 if (s.indexOf(c) == -1) 601 return new String[]{s}; 602 603 List<String> l = new LinkedList<>(); 604 char[] sArray = s.toCharArray(); 605 int x1 = 0, escapeCount = 0; 606 limit--; 607 for (int i = 0; i < sArray.length && limit > 0; i++) { 608 if (sArray[i] == '\\') 609 escapeCount++; 610 else if (sArray[i]==c && escapeCount % 2 == 0) { 611 String s2 = new String(sArray, x1, i-x1); 612 String s3 = unEscapeChars(s2, escapeChars); 613 l.add(s3.trim()); 614 limit--; 615 x1 = i+1; 616 } 617 if (sArray[i] != '\\') 618 escapeCount = 0; 619 } 620 String s2 = new String(sArray, x1, sArray.length-x1); 621 String s3 = unEscapeChars(s2, escapeChars); 622 l.add(s3.trim()); 623 624 return l.toArray(new String[l.size()]); 625 } 626 627 /** 628 * Same as {@link #split(String, char)} except splits all strings in the input and returns a single result. 629 * 630 * @param s The string to split. Can be <jk>null</jk>. 631 * @param c The character to split on. 632 * @return The tokens. 633 */ 634 public static String[] split(String[] s, char c) { 635 if (s == null) 636 return null; 637 List<String> l = new LinkedList<>(); 638 for (String ss : s) { 639 if (ss == null || ss.indexOf(c) == -1) 640 l.add(ss); 641 else 642 Collections.addAll(l, split(ss, c)); 643 } 644 return l.toArray(new String[l.size()]); 645 } 646 647 /** 648 * Splits a list of key-value pairs into an ordered map. 649 * 650 * <p> 651 * Example: 652 * <p class='bjava'> 653 * String <jv>in</jv> = <js>"foo=1;bar=2"</js>; 654 * Map <jv>map</jv> = StringUtils.<jsm>splitMap</jsm>(in, <js>';'</js>, <js>'='</js>, <jk>true</jk>); 655 * </p> 656 * 657 * @param s The string to split. 658 * @param trim Trim strings after parsing. 659 * @return The parsed map. Never <jk>null</jk>. 660 */ 661 public static Map<String,String> splitMap(String s, boolean trim) { 662 663 if (s == null) 664 return null; 665 if (isEmpty(s)) 666 return Collections.emptyMap(); 667 668 Map<String,String> m = new LinkedHashMap<>(); 669 670 int 671 S1 = 1, // Found start of key, looking for equals. 672 S2 = 2; // Found equals, looking for delimiter (or end). 673 674 int state = S1; 675 676 char[] sArray = s.toCharArray(); 677 int x1 = 0, escapeCount = 0; 678 String key = null; 679 for (int i = 0; i < sArray.length + 1; i++) { 680 char c = i == sArray.length ? ',' : sArray[i]; 681 if (c == '\\') 682 escapeCount++; 683 if (escapeCount % 2 == 0) { 684 if (state == S1) { 685 if (c == '=') { 686 key = s.substring(x1, i); 687 if (trim) 688 key = trim(key); 689 key = unEscapeChars(key, MAP_ESCAPE_SET); 690 state = S2; 691 x1 = i+1; 692 } else if (c == ',') { 693 key = s.substring(x1, i); 694 if (trim) 695 key = trim(key); 696 key = unEscapeChars(key, MAP_ESCAPE_SET); 697 m.put(key, ""); 698 state = S1; 699 x1 = i+1; 700 } 701 } else if (state == S2) { 702 if (c == ',') { 703 String val = s.substring(x1, i); 704 if (trim) 705 val = trim(val); 706 val = unEscapeChars(val, MAP_ESCAPE_SET); 707 m.put(key, val); 708 key = null; 709 x1 = i+1; 710 state = S1; 711 } 712 } 713 } 714 if (c != '\\') 715 escapeCount = 0; 716 } 717 718 return m; 719 } 720 721 private static final AsciiSet MAP_ESCAPE_SET = AsciiSet.create(",=\\"); 722 723 /** 724 * Returns <jk>true</jk> if the specified string contains any of the specified characters. 725 * 726 * @param s The string to test. 727 * @param chars The characters to look for. 728 * @return 729 * <jk>true</jk> if the specified string contains any of the specified characters. 730 * <br><jk>false</jk> if the string is <jk>null</jk>. 731 */ 732 public static boolean containsAny(String s, char...chars) { 733 if (s == null) 734 return false; 735 for (int i = 0, j = s.length(); i < j; i++) { 736 char c = s.charAt(i); 737 for (char c2 : chars) 738 if (c == c2) 739 return true; 740 } 741 return false; 742 } 743 744 /** 745 * Splits a space-delimited string with optionally quoted arguments. 746 * 747 * <p> 748 * Examples: 749 * <ul> 750 * <li><js>"foo"</js> => <c>["foo"]</c> 751 * <li><js>" foo "</js> => <c>["foo"]</c> 752 * <li><js>"foo bar baz"</js> => <c>["foo","bar","baz"]</c> 753 * <li><js>"foo 'bar baz'"</js> => <c>["foo","bar baz"]</c> 754 * <li><js>"foo \"bar baz\""</js> => <c>["foo","bar baz"]</c> 755 * <li><js>"foo 'bar\'baz'"</js> => <c>["foo","bar'baz"]</c> 756 * </ul> 757 * 758 * @param s The input string. 759 * @return 760 * The results, or <jk>null</jk> if the input was <jk>null</jk>. 761 * <br>An empty string results in an empty array. 762 */ 763 public static String[] splitQuoted(String s) { 764 return splitQuoted(s, false); 765 } 766 767 /** 768 * Same as {@link #splitQuoted(String)} but allows you to optionally keep the quote characters. 769 * 770 * @param s The input string. 771 * @param keepQuotes If <jk>true</jk>, quote characters are kept on the tokens. 772 * @return 773 * The results, or <jk>null</jk> if the input was <jk>null</jk>. 774 * <br>An empty string results in an empty array. 775 */ 776 public static String[] splitQuoted(String s, boolean keepQuotes) { 777 778 if (s == null) 779 return null; 780 781 s = s.trim(); 782 783 if (isEmpty(s)) 784 return new String[0]; 785 786 if (! containsAny(s, ' ', '\t', '\'', '"')) 787 return new String[]{s}; 788 789 int 790 S1 = 1, // Looking for start of token. 791 S2 = 2, // Found ', looking for end ' 792 S3 = 3, // Found ", looking for end " 793 S4 = 4; // Found non-whitespace, looking for end whitespace. 794 795 int state = S1; 796 797 boolean isInEscape = false, needsUnescape = false; 798 int mark = 0; 799 800 List<String> l = new ArrayList<>(); 801 for (int i = 0; i < s.length(); i++) { 802 char c = s.charAt(i); 803 804 if (state == S1) { 805 if (c == '\'') { 806 state = S2; 807 mark = keepQuotes ? i : i+1; 808 } else if (c == '"') { 809 state = S3; 810 mark = keepQuotes ? i : i+1; 811 } else if (c != ' ' && c != '\t') { 812 state = S4; 813 mark = i; 814 } 815 } else if (state == S2 || state == S3) { 816 if (c == '\\') { 817 isInEscape = ! isInEscape; 818 needsUnescape = ! keepQuotes; 819 } else if (! isInEscape) { 820 if (c == (state == S2 ? '\'' : '"')) { 821 String s2 = s.substring(mark, keepQuotes ? i+1 : i); 822 if (needsUnescape) 823 s2 = unEscapeChars(s2, QUOTE_ESCAPE_SET); 824 l.add(s2); 825 state = S1; 826 isInEscape = needsUnescape = false; 827 } 828 } else { 829 isInEscape = false; 830 } 831 } else if (state == S4) { 832 if (c == ' ' || c == '\t') { 833 l.add(s.substring(mark, i)); 834 state = S1; 835 } 836 } 837 } 838 if (state == S4) 839 l.add(s.substring(mark)); 840 else if (state == S2 || state == S3) 841 throw new IllegalArgumentException("Unmatched string quotes: " + s); 842 return l.toArray(new String[l.size()]); 843 } 844 845 private static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.create("\"'\\"); 846 847 /** 848 * Returns <jk>true</jk> if specified string is <jk>null</jk> or empty. 849 * 850 * @param s The string to check. 851 * @return <jk>true</jk> if specified string is <jk>null</jk> or empty. 852 */ 853 public static boolean isEmpty(String s) { 854 return s == null || s.isEmpty(); 855 } 856 857 /** 858 * Returns <jk>true</jk> if specified charsequence is <jk>null</jk> or empty. 859 * 860 * @param s The string to check. 861 * @return <jk>true</jk> if specified charsequence is <jk>null</jk> or empty. 862 */ 863 public static boolean isEmpty(CharSequence s) { 864 return s == null || s.length() == 0; 865 } 866 867 /** 868 * Returns <jk>true</jk> if specified string is <jk>null</jk> or empty or consists of only blanks. 869 * 870 * @param s The string to check. 871 * @return <jk>true</jk> if specified string is <jk>null</jk> or emptyor consists of only blanks. 872 */ 873 public static boolean isEmptyOrBlank(String s) { 874 return s == null || s.trim().isEmpty(); 875 } 876 877 /** 878 * Returns <jk>true</jk> if specified string is not <jk>null</jk> or empty. 879 * 880 * @param s The string to check. 881 * @return <jk>true</jk> if specified string is not <jk>null</jk> or empty. 882 */ 883 public static boolean isNotEmpty(String s) { 884 return ! isEmpty(s); 885 } 886 887 /** 888 * Returns <jk>true</jk> if either of the specified strings are not <jk>null</jk> or empty. 889 * 890 * @param s1 The string to check. 891 * @param s2 The string to check. 892 * @return <jk>true</jk> if either of the specified strings are not <jk>null</jk> or empty. 893 */ 894 public static boolean isNotEmpty(String s1, String s2) { 895 return isNotEmpty(s1) || isNotEmpty(s2); 896 } 897 898 /** 899 * Returns <jk>null</jk> if the specified string is <jk>null</jk> or empty. 900 * 901 * @param s The string to check. 902 * @return <jk>null</jk> if the specified string is <jk>null</jk> or empty, or the same string if not. 903 */ 904 public static String nullIfEmpty(String s) { 905 if (s == null || s.isEmpty()) 906 return null; 907 return s; 908 } 909 910 /** 911 * Returns an empty string if the specified string is <jk>null</jk>. 912 * 913 * @param s The string to check. 914 * @return An empty string if the specified string is <jk>null</jk>, or the same string otherwise. 915 */ 916 public static String emptyIfNull(String s) { 917 if (s == null) 918 return ""; 919 return s; 920 } 921 922 /** 923 * Returns an empty string if the specified object is <jk>null</jk>. 924 * 925 * @param o The object to check. 926 * @return An empty string if the specified object is <jk>null</jk>, or the object converted to a string using {@link String#toString()}. 927 */ 928 public static String emptyIfNull(Object o) { 929 if (o == null) 930 return ""; 931 return o.toString(); 932 } 933 934 935 /** 936 * Removes escape characters from the specified characters. 937 * 938 * @param s The string to remove escape characters from. 939 * @param escaped The characters escaped. 940 * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>. 941 */ 942 public static String unEscapeChars(String s, AsciiSet escaped) { 943 if (s == null || s.isEmpty()) 944 return s; 945 int count = 0; 946 for (int i = 0; i < s.length(); i++) 947 if (escaped.contains(s.charAt(i))) 948 count++; 949 if (count == 0) 950 return s; 951 StringBuffer sb = new StringBuffer(s.length()-count); 952 for (int i = 0; i < s.length(); i++) { 953 char c = s.charAt(i); 954 955 if (c == '\\') { 956 if (i+1 != s.length()) { 957 char c2 = s.charAt(i+1); 958 if (escaped.contains(c2)) { 959 i++; 960 } else if (c2 == '\\') { 961 sb.append('\\'); 962 i++; 963 } 964 } 965 } 966 sb.append(s.charAt(i)); 967 } 968 return sb.toString(); 969 } 970 971 /** 972 * Escapes the specified characters in the string. 973 * 974 * @param s The string with characters to escape. 975 * @param escaped The characters to escape. 976 * @return The string with characters escaped, or the same string if no escapable characters were found. 977 */ 978 public static String escapeChars(String s, AsciiSet escaped) { 979 if (s == null || s.length() == 0) 980 return s; 981 982 int count = 0; 983 for (int i = 0; i < s.length(); i++) 984 if (escaped.contains(s.charAt(i))) 985 count++; 986 if (count == 0) 987 return s; 988 989 StringBuffer sb = new StringBuffer(s.length() + count); 990 for (int i = 0; i < s.length(); i++) { 991 char c = s.charAt(i); 992 if (escaped.contains(c)) 993 sb.append('\\'); 994 sb.append(c); 995 } 996 return sb.toString(); 997 } 998 999 /** 1000 * Debug method for rendering non-ASCII character sequences. 1001 * 1002 * @param s The string to decode. 1003 * @return A string with non-ASCII characters converted to <js>"[hex]"</js> sequences. 1004 */ 1005 public static String decodeHex(String s) { 1006 if (s == null) 1007 return null; 1008 StringBuilder sb = new StringBuilder(); 1009 for (char c : s.toCharArray()) { 1010 if (c < ' ' || c > '~') 1011 sb.append("["+Integer.toHexString(c)+"]"); 1012 else 1013 sb.append(c); 1014 } 1015 return sb.toString(); 1016 } 1017 1018 /** 1019 * An efficient method for checking if a string starts with a character. 1020 * 1021 * @param s The string to check. Can be <jk>null</jk>. 1022 * @param c The character to check for. 1023 * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character. 1024 */ 1025 public static boolean startsWith(String s, char c) { 1026 if (s != null) { 1027 int i = s.length(); 1028 if (i > 0) 1029 return s.charAt(0) == c; 1030 } 1031 return false; 1032 } 1033 1034 /** 1035 * An efficient method for checking if a string ends with a character. 1036 * 1037 * @param s The string to check. Can be <jk>null</jk>. 1038 * @param c The character to check for. 1039 * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character. 1040 */ 1041 public static boolean endsWith(String s, char c) { 1042 if (s != null) { 1043 int i = s.length(); 1044 if (i > 0) 1045 return s.charAt(i-1) == c; 1046 } 1047 return false; 1048 } 1049 1050 /** 1051 * Same as {@link #endsWith(String, char)} except check for multiple characters. 1052 * 1053 * @param s The string to check. Can be <jk>null</jk>. 1054 * @param c The characters to check for. 1055 * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character. 1056 */ 1057 public static boolean endsWith(String s, char...c) { 1058 if (s != null) { 1059 int i = s.length(); 1060 if (i > 0) { 1061 char c2 = s.charAt(i-1); 1062 for (char cc : c) 1063 if (c2 == cc) 1064 return true; 1065 } 1066 } 1067 return false; 1068 } 1069 1070 /** 1071 * Converts the specified number into a 2 hexadecimal characters. 1072 * 1073 * @param num The number to convert to hex. 1074 * @return A <code><jk>char</jk>[2]</code> containing the specified characters. 1075 */ 1076 public static char[] toHex2(int num) { 1077 if (num < 0 || num > 255) 1078 throw new NumberFormatException("toHex2 can only be used on numbers between 0 and 255"); 1079 char[] n = new char[2]; 1080 int a = num%16; 1081 n[1] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1082 a = (num/16)%16; 1083 n[0] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1084 return n; 1085 } 1086 1087 private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); 1088 1089 /** 1090 * Converts the specified byte into a 2 hexadecimal characters. 1091 * 1092 * @param b The number to convert to hex. 1093 * @return A <code><jk>char</jk>[2]</code> containing the specified characters. 1094 */ 1095 public static String toHex(byte b) { 1096 char[] c = new char[2]; 1097 int v = b & 0xFF; 1098 c[0] = hexArray[v >>> 4]; 1099 c[1] = hexArray[v & 0x0F]; 1100 return new String(c); 1101 } 1102 1103 /** 1104 * Converts the specified bytes into a readable string. 1105 * 1106 * @param b The number to convert to hex. 1107 * @return A <code><jk>char</jk>[2]</code> containing the specified characters. 1108 */ 1109 public static String toReadableBytes(byte[] b) { 1110 StringBuilder sb = new StringBuilder(); 1111 for (byte b2 : b) 1112 sb.append((b2 < ' ' || b2 > 'z') ? String.format("[%02X]", b2) : (char)b2 + " "); 1113 sb.append("\n"); 1114 for (byte b2 : b) 1115 sb.append(String.format("[%02X]", b2)); 1116 return sb.toString(); 1117 } 1118 1119 /** 1120 * Converts the specified number into a 4 hexadecimal characters. 1121 * 1122 * @param num The number to convert to hex. 1123 * @return A <code><jk>char</jk>[4]</code> containing the specified characters. 1124 */ 1125 public static char[] toHex4(int num) { 1126 char[] n = new char[4]; 1127 int a = num%16; 1128 n[3] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1129 int base = 16; 1130 for (int i = 1; i < 4; i++) { 1131 a = (num/base)%16; 1132 base <<= 4; 1133 n[3-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1134 } 1135 return n; 1136 } 1137 1138 /** 1139 * Converts the specified number into a 8 hexadecimal characters. 1140 * 1141 * @param num The number to convert to hex. 1142 * @return A <code><jk>char</jk>[8]</code> containing the specified characters. 1143 */ 1144 public static char[] toHex8(long num) { 1145 char[] n = new char[8]; 1146 long a = num%16; 1147 n[7] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1148 int base = 16; 1149 for (int i = 1; i < 8; i++) { 1150 a = (num/base)%16; 1151 base <<= 4; 1152 n[7-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a); 1153 } 1154 return n; 1155 } 1156 1157 /** 1158 * Tests two strings for equality, but gracefully handles nulls. 1159 * 1160 * @param s1 String 1. 1161 * @param s2 String 2. 1162 * @return <jk>true</jk> if the strings are equal. 1163 */ 1164 public static boolean eq(String s1, String s2) { 1165 if (s1 == null) 1166 return s2 == null; 1167 if (s2 == null) 1168 return false; 1169 return s1.equals(s2); 1170 } 1171 1172 /** 1173 * Tests two strings for equality, but gracefully handles nulls. 1174 * 1175 * @param caseInsensitive Use case-insensitive matching. 1176 * @param s1 String 1. 1177 * @param s2 String 2. 1178 * @return <jk>true</jk> if the strings are equal. 1179 */ 1180 public static boolean eq(boolean caseInsensitive, String s1, String s2) { 1181 return caseInsensitive ? eqic(s1, s2) : eq(s1, s2); 1182 } 1183 1184 /** 1185 * Finds the position where the two strings differ. 1186 * 1187 * @param s1 The first string. 1188 * @param s2 The second string. 1189 * @return The position where the two strings differ, or <c>-1</c> if they're equal. 1190 */ 1191 public static int diffPosition(String s1, String s2) { 1192 s1 = emptyIfNull(s1); 1193 s2 = emptyIfNull(s2); 1194 int i = 0; 1195 int len = Math.min(s1.length(), s2.length()); 1196 while (i < len) { 1197 int j = s1.charAt(i) - s2.charAt(i); 1198 if (j != 0) 1199 return i; 1200 i++; 1201 } 1202 if (i == len && s1.length() == s2.length()) 1203 return -1; 1204 return i; 1205 } 1206 1207 /** 1208 * Finds the position where the two strings differ ignoring case. 1209 * 1210 * @param s1 The first string. 1211 * @param s2 The second string. 1212 * @return The position where the two strings differ, or <c>-1</c> if they're equal. 1213 */ 1214 public static int diffPositionIc(String s1, String s2) { 1215 s1 = emptyIfNull(s1); 1216 s2 = emptyIfNull(s2); 1217 int i = 0; 1218 int len = Math.min(s1.length(), s2.length()); 1219 while (i < len) { 1220 int j = Character.toLowerCase(s1.charAt(i)) - Character.toLowerCase(s2.charAt(i)); 1221 if (j != 0) 1222 return i; 1223 i++; 1224 } 1225 if (i == len && s1.length() == s2.length()) 1226 return -1; 1227 return i; 1228 } 1229 1230 /** 1231 * Tests two strings for case-insensitive equality, but gracefully handles nulls. 1232 * 1233 * @param s1 String 1. 1234 * @param s2 String 2. 1235 * @return <jk>true</jk> if the strings are equal. 1236 */ 1237 public static boolean eqic(String s1, String s2) { 1238 if (s1 == null) 1239 return s2 == null; 1240 if (s2 == null) 1241 return false; 1242 return s1.equalsIgnoreCase(s2); 1243 } 1244 1245 /** 1246 * Tests two strings for non-equality, but gracefully handles nulls. 1247 * 1248 * @param s1 String 1. 1249 * @param s2 String 2. 1250 * @return <jk>true</jk> if the strings are not equal. 1251 */ 1252 public static boolean ne(String s1, String s2) { 1253 return ! eq(s1, s2); 1254 } 1255 1256 /** 1257 * Tests two strings for non-equality ignoring case, but gracefully handles nulls. 1258 * 1259 * @param s1 String 1. 1260 * @param s2 String 2. 1261 * @return <jk>true</jk> if the strings are not equal ignoring case. 1262 */ 1263 public static boolean neic(String s1, String s2) { 1264 return ! eqic(s1, s2); 1265 } 1266 1267 /** 1268 * Shortcut for calling <code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code> 1269 * 1270 * @param in The input string to convert. 1271 * @return The string converted to BASE-64 encoding. 1272 */ 1273 public static String base64EncodeToString(String in) { 1274 if (in == null) 1275 return null; 1276 return base64Encode(in.getBytes(IOUtils.UTF8)); 1277 } 1278 1279 /** 1280 * BASE64-encodes the specified byte array. 1281 * 1282 * @param in The input byte array to convert. 1283 * @return The byte array converted to a BASE-64 encoded string. 1284 */ 1285 public static String base64Encode(byte[] in) { 1286 if (in == null) 1287 return null; 1288 int outLength = (in.length * 4 + 2) / 3; // Output length without padding 1289 char[] out = new char[((in.length + 2) / 3) * 4]; // Length includes padding. 1290 int iIn = 0; 1291 int iOut = 0; 1292 while (iIn < in.length) { 1293 int i0 = in[iIn++] & 0xff; 1294 int i1 = iIn < in.length ? in[iIn++] & 0xff : 0; 1295 int i2 = iIn < in.length ? in[iIn++] & 0xff : 0; 1296 int o0 = i0 >>> 2; 1297 int o1 = ((i0 & 3) << 4) | (i1 >>> 4); 1298 int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); 1299 int o3 = i2 & 0x3F; 1300 out[iOut++] = base64m1[o0]; 1301 out[iOut++] = base64m1[o1]; 1302 out[iOut] = iOut < outLength ? base64m1[o2] : '='; 1303 iOut++; 1304 out[iOut] = iOut < outLength ? base64m1[o3] : '='; 1305 iOut++; 1306 } 1307 return new String(out); 1308 } 1309 1310 /** 1311 * Shortcut for calling <c>base64Decode(String)</c> and converting the result to a UTF-8 encoded string. 1312 * 1313 * @param in The BASE-64 encoded string to decode. 1314 * @return The decoded string. 1315 */ 1316 public static String base64DecodeToString(String in) { 1317 byte[] b = base64Decode(in); 1318 if (b == null) 1319 return null; 1320 return new String(b, IOUtils.UTF8); 1321 } 1322 1323 /** 1324 * BASE64-decodes the specified string. 1325 * 1326 * @param in The BASE-64 encoded string. 1327 * @return The decoded byte array. 1328 */ 1329 public static byte[] base64Decode(String in) { 1330 if (in == null) 1331 return null; 1332 1333 byte bIn[] = in.getBytes(IOUtils.UTF8); 1334 1335 assertArg(bIn.length % 4 == 0, "Invalid BASE64 string length. Must be multiple of 4."); 1336 1337 // Strip out any trailing '=' filler characters. 1338 int inLength = bIn.length; 1339 while (inLength > 0 && bIn[inLength - 1] == '=') 1340 inLength--; 1341 1342 int outLength = (inLength * 3) / 4; 1343 byte[] out = new byte[outLength]; 1344 int iIn = 0; 1345 int iOut = 0; 1346 while (iIn < inLength) { 1347 int i0 = bIn[iIn++]; 1348 int i1 = bIn[iIn++]; 1349 int i2 = iIn < inLength ? bIn[iIn++] : 'A'; 1350 int i3 = iIn < inLength ? bIn[iIn++] : 'A'; 1351 int b0 = base64m2[i0]; 1352 int b1 = base64m2[i1]; 1353 int b2 = base64m2[i2]; 1354 int b3 = base64m2[i3]; 1355 int o0 = (b0 << 2) | (b1 >>> 4); 1356 int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); 1357 int o2 = ((b2 & 3) << 6) | b3; 1358 out[iOut++] = (byte)o0; 1359 if (iOut < outLength) 1360 out[iOut++] = (byte)o1; 1361 if (iOut < outLength) 1362 out[iOut++] = (byte)o2; 1363 } 1364 return out; 1365 } 1366 1367 /** 1368 * Generated a random UUID with the specified number of characters. 1369 * 1370 * <p> 1371 * Characters are composed of lower-case ASCII letters and numbers only. 1372 * 1373 * <p> 1374 * This method conforms to the restrictions for hostnames as specified in <a class="doclink" href="https://tools.ietf.org/html/rfc952">RFC 952</a> 1375 * Since each character has 36 possible values, the square approximation formula for the number of generated IDs 1376 * that would produce a 50% chance of collision is: 1377 * <c>sqrt(36^N)</c>. 1378 * Dividing this number by 10 gives you an approximation of the number of generated IDs needed to produce a 1379 * <1% chance of collision. 1380 * 1381 * <p> 1382 * For example, given 5 characters, the number of generated IDs need to produce a <1% chance of collision would 1383 * be: 1384 * <c>sqrt(36^5)/10=777</c> 1385 * 1386 * @param numchars The number of characters in the generated UUID. 1387 * @return A new random UUID. 1388 */ 1389 public static String random(int numchars) { 1390 Random r = new Random(); 1391 StringBuilder sb = new StringBuilder(numchars); 1392 for (int i = 0; i < numchars; i++) { 1393 int c = r.nextInt(36) + 97; 1394 if (c > 'z') 1395 c -= ('z'-'0'+1); 1396 sb.append((char)c); 1397 } 1398 return sb.toString(); 1399 } 1400 1401 /** 1402 * Same as {@link String#trim()} but prevents <c>NullPointerExceptions</c>. 1403 * 1404 * @param s The string to trim. 1405 * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. 1406 */ 1407 public static String trim(String s) { 1408 if (s == null) 1409 return null; 1410 return s.trim(); 1411 } 1412 1413 /** 1414 * Strips the first and last character from a string. 1415 * 1416 * @param s The string to strip. 1417 * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2. 1418 */ 1419 public static String strip(String s) { 1420 if (s == null || s.length() <= 1) 1421 return s; 1422 return s.substring(1, s.length()-1); 1423 } 1424 1425 /** 1426 * Parses an ISO8601 string into a date. 1427 * 1428 * <p> 1429 * Supports any of the following formats: 1430 * <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> 1431 * 1432 * @param date The date string. 1433 * @return The parsed date. 1434 * @throws IllegalArgumentException Value was not a valid date. 1435 */ 1436 public static Date parseIsoDate(String date) throws IllegalArgumentException { 1437 if (isEmpty(date)) 1438 return null; 1439 return parseIsoCalendar(date).getTime(); 1440 } 1441 1442 /** 1443 * Parses an ISO8601 string into a calendar. 1444 * 1445 * <p> 1446 * Supports any of the following formats: 1447 * <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> 1448 * 1449 * @param date The date string. 1450 * @return The parsed calendar. 1451 * @throws IllegalArgumentException Value was not a valid date. 1452 */ 1453 public static Calendar parseIsoCalendar(String date) throws IllegalArgumentException { 1454 if (isEmpty(date)) 1455 return null; 1456 date = date.trim().replace(' ', 'T'); // Convert to 'standard' ISO8601 1457 if (date.indexOf(',') != -1) // Trim milliseconds 1458 date = date.substring(0, date.indexOf(',')); 1459 if (date.matches("\\d{4}")) 1460 date += "-01-01T00:00:00"; 1461 else if (date.matches("\\d{4}\\-\\d{2}")) 1462 date += "-01T00:00:00"; 1463 else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}")) 1464 date += "T00:00:00"; 1465 else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}")) 1466 date += ":00:00"; 1467 else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}")) 1468 date += ":00"; 1469 return DatatypeConverter.parseDateTime(date); 1470 } 1471 1472 /** 1473 * Converts the specified object to an ISO8601 date string. 1474 * 1475 * @param c The object to convert. 1476 * @return The converted object. 1477 */ 1478 public static String toIsoDate(Calendar c) { 1479 return DatatypeConverter.printDate(c); 1480 } 1481 1482 /** 1483 * Converts the specified object to an ISO8601 date-time string. 1484 * 1485 * @param c The object to convert. 1486 * @return The converted object. 1487 */ 1488 public static String toIsoDateTime(Calendar c) { 1489 return DatatypeConverter.printDateTime(c); 1490 } 1491 1492 /** 1493 * Simple utility for replacing variables of the form <js>"{key}"</js> with values in the specified map. 1494 * 1495 * <p> 1496 * Nested variables are supported in both the input string and map values. 1497 * 1498 * <p> 1499 * If the map does not contain the specified value, the variable is not replaced. 1500 * 1501 * <p> 1502 * <jk>null</jk> values in the map are treated as blank strings. 1503 * 1504 * @param s The string containing variables to replace. 1505 * @param m The map containing the variable values. 1506 * @return The new string with variables replaced, or the original string if it didn't have variables in it. 1507 */ 1508 public static String replaceVars(String s, Map<String,Object> m) { 1509 1510 if (s == null) 1511 return null; 1512 1513 if (m == null || m.isEmpty() || s.indexOf('{') == -1) 1514 return s; 1515 1516 int S1 = 1; // Not in variable, looking for { 1517 int S2 = 2; // Found {, Looking for } 1518 1519 int state = S1; 1520 boolean hasInternalVar = false; 1521 int x = 0; 1522 int depth = 0; 1523 int length = s.length(); 1524 StringBuilder out = new StringBuilder(); 1525 for (int i = 0; i < length; i++) { 1526 char c = s.charAt(i); 1527 if (state == S1) { 1528 if (c == '{') { 1529 state = S2; 1530 x = i; 1531 } else { 1532 out.append(c); 1533 } 1534 } else /* state == S2 */ { 1535 if (c == '{') { 1536 depth++; 1537 hasInternalVar = true; 1538 } else if (c == '}') { 1539 if (depth > 0) { 1540 depth--; 1541 } else { 1542 String key = s.substring(x+1, i); 1543 key = (hasInternalVar ? replaceVars(key, m) : key); 1544 hasInternalVar = false; 1545 if (! m.containsKey(key)) 1546 out.append('{').append(key).append('}'); 1547 else { 1548 Object val = m.get(key); 1549 if (val == null) 1550 val = ""; 1551 String v = val.toString(); 1552 // If the replacement also contains variables, replace them now. 1553 if (v.indexOf('{') != -1) 1554 v = replaceVars(v, m); 1555 out.append(v); 1556 } 1557 state = 1; 1558 } 1559 } 1560 } 1561 } 1562 return out.toString(); 1563 } 1564 1565 /** 1566 * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters. 1567 * 1568 * @param s The string to replace unicode sequences in. 1569 * @return A string with unicode sequences replaced. 1570 */ 1571 public static String replaceUnicodeSequences(String s) { 1572 if (s.indexOf('\\') == -1) 1573 return s; 1574 Pattern p = Pattern.compile("\\\\u(\\p{XDigit}{4})"); 1575 Matcher m = p.matcher(s); 1576 StringBuffer sb = new StringBuffer(s.length()); 1577 while (m.find()) { 1578 String ch = String.valueOf((char) Integer.parseInt(m.group(1), 16)); 1579 m.appendReplacement(sb, Matcher.quoteReplacement(ch)); 1580 } 1581 m.appendTail(sb); 1582 return sb.toString(); 1583 } 1584 1585 /** 1586 * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for the specified character. 1587 * 1588 * @param c The character to create a sequence for. 1589 * @return An escaped-unicode sequence. 1590 */ 1591 public static String unicodeSequence(char c) { 1592 StringBuilder sb = new StringBuilder(6); 1593 sb.append('\\').append('u'); 1594 for (char cc : toHex4(c)) 1595 sb.append(cc); 1596 return sb.toString(); 1597 } 1598 1599 /** 1600 * Calls {@link #toString()} on the specified object if it's not null. 1601 * 1602 * @param o The object to convert to a string. 1603 * @return The object converted to a string, or <jk>null</jk> if the object was null. 1604 */ 1605 public static String stringify(Object o) { 1606 return o == null ? null : o.toString(); 1607 } 1608 1609 /** 1610 * Converts the specified array to a string. 1611 * 1612 * @param o The array to convert to a string. 1613 * @return The array converted to a string, or <jk>null</jk> if the object was null. 1614 */ 1615 public static String stringifyDeep(Object o) { 1616 if (o == null) 1617 return null; 1618 if (! o.getClass().isArray()) 1619 return o.toString(); 1620 if (o.getClass().getComponentType().isPrimitive()) 1621 return PRIMITIVE_ARRAY_STRINGIFIERS.get(o.getClass()).apply(o); 1622 return Arrays.deepToString((Object[])o); 1623 } 1624 1625 private static final Map<Class<?>,Function<Object,String>> PRIMITIVE_ARRAY_STRINGIFIERS = new HashMap<>(); 1626 static { 1627 PRIMITIVE_ARRAY_STRINGIFIERS.put(boolean[].class, x -> Arrays.toString((boolean[])x)); 1628 PRIMITIVE_ARRAY_STRINGIFIERS.put(byte[].class, x -> Arrays.toString((byte[])x)); 1629 PRIMITIVE_ARRAY_STRINGIFIERS.put(char[].class, x -> Arrays.toString((char[])x)); 1630 PRIMITIVE_ARRAY_STRINGIFIERS.put(double[].class, x -> Arrays.toString((double[])x)); 1631 PRIMITIVE_ARRAY_STRINGIFIERS.put(float[].class, x -> Arrays.toString((float[])x)); 1632 PRIMITIVE_ARRAY_STRINGIFIERS.put(int[].class, x -> Arrays.toString((int[])x)); 1633 PRIMITIVE_ARRAY_STRINGIFIERS.put(long[].class, x -> Arrays.toString((long[])x)); 1634 PRIMITIVE_ARRAY_STRINGIFIERS.put(short[].class, x -> Arrays.toString((short[])x)); 1635 } 1636 1637 /** 1638 * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 encoded string. 1639 * 1640 * @param hex The hexadecimal string. 1641 * @return The UTF-8 string. 1642 */ 1643 public static String fromHexToUTF8(String hex) { 1644 ByteBuffer buff = ByteBuffer.allocate(hex.length()/2); 1645 for (int i = 0; i < hex.length(); i+=2) 1646 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 1647 ((Buffer)buff).rewind(); // Fixes Java 11 issue. 1648 Charset cs = Charset.forName("UTF-8"); 1649 return cs.decode(buff).toString(); 1650 } 1651 1652 /** 1653 * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 BC") into a UTF-8 encoded string. 1654 * 1655 * @param hex The hexadecimal string. 1656 * @return The UTF-8 string. 1657 */ 1658 public static String fromSpacedHexToUTF8(String hex) { 1659 ByteBuffer buff = ByteBuffer.allocate((hex.length()+1)/3); 1660 for (int i = 0; i < hex.length(); i+=3) 1661 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 1662 ((Buffer)buff).rewind(); // Fixes Java 11 issue. 1663 Charset cs = Charset.forName("UTF-8"); 1664 return cs.decode(buff).toString(); 1665 } 1666 1667 private static final char[] HEX = "0123456789ABCDEF".toCharArray(); 1668 1669 /** 1670 * Converts a byte array into a simple hexadecimal character string. 1671 * 1672 * @param bytes The bytes to convert to hexadecimal. 1673 * @return A new string consisting of hexadecimal characters. 1674 */ 1675 public static String toHex(byte[] bytes) { 1676 StringBuilder sb = new StringBuilder(bytes.length * 2); 1677 for (byte element : bytes) { 1678 int v = element & 0xFF; 1679 sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]); 1680 } 1681 return sb.toString(); 1682 } 1683 1684 /** 1685 * Same as {@link #toHex(byte[])} but puts spaces between the byte strings. 1686 * 1687 * @param bytes The bytes to convert to hexadecimal. 1688 * @return A new string consisting of hexadecimal characters. 1689 */ 1690 public static String toSpacedHex(byte[] bytes) { 1691 StringBuilder sb = new StringBuilder(bytes.length * 3); 1692 for (int j = 0; j < bytes.length; j++) { 1693 if (j > 0) 1694 sb.append(' '); 1695 int v = bytes[j] & 0xFF; 1696 sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]); 1697 } 1698 return sb.toString(); 1699 } 1700 1701 /** 1702 * Converts a hexadecimal character string to a byte array. 1703 * 1704 * @param hex The string to convert to a byte array. 1705 * @return A new byte array. 1706 */ 1707 public static byte[] fromHex(String hex) { 1708 ByteBuffer buff = ByteBuffer.allocate(hex.length()/2); 1709 for (int i = 0; i < hex.length(); i+=2) 1710 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 1711 ((Buffer)buff).rewind(); // Fixes Java 11 issue. 1712 return buff.array(); 1713 } 1714 1715 /** 1716 * Same as {@link #fromHex(String)} except expects spaces between the byte strings. 1717 * 1718 * @param hex The string to convert to a byte array. 1719 * @return A new byte array. 1720 */ 1721 public static byte[] fromSpacedHex(String hex) { 1722 ByteBuffer buff = ByteBuffer.allocate((hex.length()+1)/3); 1723 for (int i = 0; i < hex.length(); i+=3) 1724 buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16)); 1725 ((Buffer)buff).rewind(); // Fixes Java 11 issue. 1726 return buff.array(); 1727 } 1728 1729 /** 1730 * Creates a repeated pattern. 1731 * 1732 * @param count The number of times to repeat the pattern. 1733 * @param pattern The pattern to repeat. 1734 * @return A new string consisting of the repeated pattern. 1735 */ 1736 public static String repeat(int count, String pattern) { 1737 StringBuilder sb = new StringBuilder(pattern.length() * count); 1738 for (int i = 0; i < count; i++) 1739 sb.append(pattern); 1740 return sb.toString(); 1741 } 1742 1743 /** 1744 * Trims whitespace characters from the beginning of the specified string. 1745 * 1746 * @param s The string to trim. 1747 * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. 1748 */ 1749 public static String trimStart(String s) { 1750 if (s != null) 1751 while (s.length() > 0 && Character.isWhitespace(s.charAt(0))) 1752 s = s.substring(1); 1753 return s; 1754 } 1755 1756 /** 1757 * Trims whitespace characters from the end of the specified string. 1758 * 1759 * @param s The string to trim. 1760 * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. 1761 */ 1762 public static String trimEnd(String s) { 1763 if (s != null) 1764 while (s.length() > 0 && Character.isWhitespace(s.charAt(s.length()-1))) 1765 s = s.substring(0, s.length()-1); 1766 return s; 1767 } 1768 1769 /** 1770 * Returns <jk>true</jk> if the specified string is one of the specified values. 1771 * 1772 * @param s 1773 * The string to test. 1774 * Can be <jk>null</jk>. 1775 * @param values 1776 * The values to test. 1777 * Can contain <jk>null</jk>. 1778 * @return <jk>true</jk> if the specified string is one of the specified values. 1779 */ 1780 public static boolean isOneOf(String s, String...values) { 1781 for (String value : values) 1782 if (StringUtils.eq(s, value)) 1783 return true; 1784 return false; 1785 } 1786 1787 /** 1788 * Trims <js>'/'</js> characters from both the start and end of the specified string. 1789 * 1790 * @param s The string to trim. 1791 * @return A new trimmed string, or the same string if no trimming was necessary. 1792 */ 1793 public static String trimSlashes(String s) { 1794 if (s == null) 1795 return null; 1796 if (s.isEmpty()) 1797 return s; 1798 while (endsWith(s, '/')) 1799 s = s.substring(0, s.length()-1); 1800 while (s.length() > 0 && s.charAt(0) == '/') 1801 s = s.substring(1); 1802 return s; 1803 } 1804 1805 /** 1806 * Trims <js>'/'</js> and space characters from both the start and end of the specified string. 1807 * 1808 * @param s The string to trim. 1809 * @return A new trimmed string, or the same string if no trimming was necessary. 1810 */ 1811 public static String trimSlashesAndSpaces(String s) { 1812 if (s == null) 1813 return null; 1814 while (s.length() > 0 && (s.charAt(s.length()-1) == '/' || Character.isWhitespace(s.charAt(s.length()-1)))) 1815 s = s.substring(0, s.length()-1); 1816 while (s.length() > 0 && (s.charAt(0) == '/' || Character.isWhitespace(s.charAt(0)))) 1817 s = s.substring(1); 1818 return s; 1819 } 1820 1821 /** 1822 * Trims <js>'/'</js> characters from the end of the specified string. 1823 * 1824 * @param s The string to trim. 1825 * @return A new trimmed string, or the same string if no trimming was necessary. 1826 */ 1827 public static String trimTrailingSlashes(String s) { 1828 if (s == null) 1829 return null; 1830 while (endsWith(s, '/')) 1831 s = s.substring(0, s.length()-1); 1832 return s; 1833 } 1834 1835 /** 1836 * Trims <js>'/'</js> characters from the beginning of the specified string. 1837 * 1838 * @param s The string to trim. 1839 * @return A new trimmed string, or the same string if no trimming was necessary. 1840 */ 1841 public static String trimLeadingSlashes(String s) { 1842 if (s == null) 1843 return null; 1844 while (s.length() > 0 && s.charAt(0) == '/') 1845 s = s.substring(1); 1846 return s; 1847 } 1848 1849 private static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS = 1850 AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.*/()").build(); 1851 1852 /** 1853 * Similar to {@link URLEncoder#encode(String, String)} but doesn't encode <js>"/"</js> characters. 1854 * 1855 * @param o The object to encode. 1856 * @return The URL encoded string, or <jk>null</jk> if the object was null. 1857 */ 1858 public static String urlEncodePath(Object o) { 1859 if (o == null) 1860 return null; 1861 String s = stringify(o); 1862 1863 boolean needsEncode = false; 1864 for (int i = 0; i < s.length() && ! needsEncode; i++) 1865 needsEncode = URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i)); 1866 if (! needsEncode) 1867 return s; 1868 1869 StringBuilder sb = new StringBuilder(); 1870 CharArrayWriter caw = new CharArrayWriter(); 1871 int caseDiff = ('a' - 'A'); 1872 1873 for (int i = 0; i < s.length();) { 1874 char c = s.charAt(i); 1875 if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) { 1876 sb.append(c); 1877 i++; 1878 } else { 1879 if (c == ' ') { 1880 sb.append('+'); 1881 i++; 1882 } else { 1883 do { 1884 caw.write(c); 1885 if (c >= 0xD800 && c <= 0xDBFF) { 1886 if ( (i+1) < s.length()) { 1887 int d = s.charAt(i+1); 1888 if (d >= 0xDC00 && d <= 0xDFFF) { 1889 caw.write(d); 1890 i++; 1891 } 1892 } 1893 } 1894 i++; 1895 } while (i < s.length() && !URL_ENCODE_PATHINFO_VALIDCHARS.contains((c = s.charAt(i)))); 1896 1897 caw.flush(); 1898 String s2 = new String(caw.toCharArray()); 1899 byte[] ba = s2.getBytes(IOUtils.UTF8); 1900 for (byte element : ba) { 1901 sb.append('%'); 1902 char ch = Character.forDigit((element >> 4) & 0xF, 16); 1903 if (Character.isLetter(ch)) { 1904 ch -= caseDiff; 1905 } 1906 sb.append(ch); 1907 ch = Character.forDigit(element & 0xF, 16); 1908 if (Character.isLetter(ch)) { 1909 ch -= caseDiff; 1910 } 1911 sb.append(ch); 1912 } 1913 caw.reset(); 1914 } 1915 } 1916 } 1917 return sb.toString(); 1918 } 1919 1920 /** 1921 * Decodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme. 1922 * 1923 * @param s The string to decode. 1924 * @return The decoded string, or <jk>null</jk> if input is <jk>null</jk>. 1925 */ 1926 public static String urlDecode(String s) { 1927 if (s == null) 1928 return s; 1929 boolean needsDecode = false; 1930 for (int i = 0; i < s.length() && ! needsDecode; i++) { 1931 char c = s.charAt(i); 1932 if (c == '+' || c == '%') 1933 needsDecode = true; 1934 } 1935 if (needsDecode) { 1936 try { 1937 return URLDecoder.decode(s, "UTF-8"); 1938 } catch (UnsupportedEncodingException e) {/* Won't happen */} 1939 } 1940 return s; 1941 } 1942 1943 /** 1944 * Encodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme. 1945 * 1946 * @param s The string to encode. 1947 * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>. 1948 */ 1949 public static String urlEncode(String s) { 1950 if (s == null) 1951 return null; 1952 boolean needsEncode = false; 1953 for (int i = 0; i < s.length() && ! needsEncode; i++) 1954 needsEncode |= (! unencodedChars.contains(s.charAt(i))); 1955 if (needsEncode) { 1956 try { 1957 return URLEncoder.encode(s, "UTF-8"); 1958 } catch (UnsupportedEncodingException e) {/* Won't happen */} 1959 } 1960 return s; 1961 } 1962 1963 /** 1964 * Same as {@link #urlEncode(String)} except only escapes characters that absolutely need to be escaped. 1965 * 1966 * @param s The string to escape. 1967 * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>. 1968 */ 1969 public static String urlEncodeLax(String s) { 1970 if (s == null) 1971 return null; 1972 boolean needsEncode = false; 1973 for (int i = 0; i < s.length() && ! needsEncode; i++) 1974 needsEncode |= (! unencodedCharsLax.contains(s.charAt(i))); 1975 if (needsEncode) { 1976 StringBuilder sb = new StringBuilder(s.length()*2); 1977 for (int i = 0; i < s.length(); i++) { 1978 char c = s.charAt(i); 1979 if (unencodedCharsLax.contains(c)) 1980 sb.append(c); 1981 else if (c == ' ') 1982 sb.append("+"); 1983 else if (c <= 127) 1984 sb.append('%').append(toHex2(c)); 1985 else 1986 try { 1987 sb.append(URLEncoder.encode(""+c, "UTF-8")); // Yuck. 1988 } catch (UnsupportedEncodingException e) { 1989 // Not possible. 1990 } 1991 } 1992 s = sb.toString(); 1993 } 1994 return s; 1995 } 1996 1997 /** 1998 * Returns the first non-whitespace character in the string. 1999 * 2000 * @param s The string to check. 2001 * @return 2002 * The first non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed 2003 * of only whitespace. 2004 */ 2005 public static char firstNonWhitespaceChar(String s) { 2006 if (s != null) 2007 for (int i = 0; i < s.length(); i++) 2008 if (! Character.isWhitespace(s.charAt(i))) 2009 return s.charAt(i); 2010 return 0; 2011 } 2012 2013 /** 2014 * Returns the last non-whitespace character in the string. 2015 * 2016 * @param s The string to check. 2017 * @return 2018 * The last non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed 2019 * of only whitespace. 2020 */ 2021 public static char lastNonWhitespaceChar(String s) { 2022 if (s != null) 2023 for (int i = s.length()-1; i >= 0; i--) 2024 if (! Character.isWhitespace(s.charAt(i))) 2025 return s.charAt(i); 2026 return 0; 2027 } 2028 2029 /** 2030 * Returns the character at the specified index in the string without throwing exceptions. 2031 * 2032 * @param s The string. 2033 * @param i The index position. 2034 * @return 2035 * The character at the specified index, or <c>0</c> if the index is out-of-range or the string 2036 * is <jk>null</jk>. 2037 */ 2038 public static char charAt(String s, int i) { 2039 if (s == null || i < 0 || i >= s.length()) 2040 return 0; 2041 return s.charAt(i); 2042 } 2043 2044 /** 2045 * Efficiently determines whether a URL is of the pattern "xxx://xxx" 2046 * 2047 * @param s The string to test. 2048 * @return <jk>true</jk> if it's an absolute path. 2049 */ 2050 public static boolean isAbsoluteUri(String s) { 2051 2052 if (isEmpty(s)) 2053 return false; 2054 2055 // Use a state machine for maximum performance. 2056 2057 int S1 = 1; // Looking for http 2058 int S2 = 2; // Found http, looking for : 2059 int S3 = 3; // Found :, looking for / 2060 int S4 = 4; // Found /, looking for / 2061 int S5 = 5; // Found /, looking for x 2062 2063 int state = S1; 2064 for (int i = 0; i < s.length(); i++) { 2065 char c = s.charAt(i); 2066 if (state == S1) { 2067 if (c >= 'a' && c <= 'z') 2068 state = S2; 2069 else 2070 return false; 2071 } else if (state == S2) { 2072 if (c == ':') 2073 state = S3; 2074 else if (c < 'a' || c > 'z') 2075 return false; 2076 } else if (state == S3) { 2077 if (c == '/') 2078 state = S4; 2079 else 2080 return false; 2081 } else if (state == S4) { 2082 if (c == '/') 2083 state = S5; 2084 else 2085 return false; 2086 } else if (state == S5) { 2087 return true; 2088 } 2089 } 2090 return false; 2091 } 2092 2093 /** 2094 * Efficiently determines whether a URL is of the pattern "xxx:/xxx". 2095 * 2096 * <p> 2097 * The pattern matched is: <c>[a-z]{2,}\:\/.*</c> 2098 * 2099 * <p> 2100 * Note that this excludes filesystem paths such as <js>"C:/temp"</js>. 2101 * 2102 * @param s The string to test. 2103 * @return <jk>true</jk> if it's an absolute path. 2104 */ 2105 public static boolean isUri(String s) { 2106 2107 if (isEmpty(s)) 2108 return false; 2109 2110 // Use a state machine for maximum performance. 2111 2112 int S1 = 1; // Looking for protocol char 1 2113 int S2 = 2; // Found protocol char 1, looking for protocol char 2 2114 int S3 = 3; // Found protocol char 2, looking for : 2115 int S4 = 4; // Found :, looking for / 2116 2117 2118 int state = S1; 2119 for (int i = 0; i < s.length(); i++) { 2120 char c = s.charAt(i); 2121 if (state == S1) { 2122 if (c >= 'a' && c <= 'z') 2123 state = S2; 2124 else 2125 return false; 2126 } else if (state == S2) { 2127 if (c >= 'a' && c <= 'z') 2128 state = S3; 2129 else 2130 return false; 2131 } else if (state == S3) { 2132 if (c == ':') 2133 state = S4; 2134 else if (c < 'a' || c > 'z') 2135 return false; 2136 } else if (state == S4) { 2137 if (c == '/') 2138 return true; 2139 return false; 2140 } 2141 } 2142 return false; 2143 } 2144 2145 /** 2146 * Given an absolute URI, returns just the authority portion (e.g. <js>"http://hostname:port"</js>) 2147 * 2148 * @param s The URI string. 2149 * @return Just the authority portion of the URI. 2150 */ 2151 public static String getAuthorityUri(String s) { 2152 2153 // Use a state machine for maximum performance. 2154 2155 int S1 = 1; // Looking for http 2156 int S2 = 2; // Found http, looking for : 2157 int S3 = 3; // Found :, looking for / 2158 int S4 = 4; // Found /, looking for / 2159 int S5 = 5; // Found /, looking for x 2160 int S6 = 6; // Found x, looking for / 2161 2162 int state = S1; 2163 for (int i = 0; i < s.length(); i++) { 2164 char c = s.charAt(i); 2165 if (state == S1) { 2166 if (c >= 'a' && c <= 'z') 2167 state = S2; 2168 else 2169 return s; 2170 } else if (state == S2) { 2171 if (c == ':') 2172 state = S3; 2173 else if (c < 'a' || c > 'z') 2174 return s; 2175 } else if (state == S3) { 2176 if (c == '/') 2177 state = S4; 2178 else 2179 return s; 2180 } else if (state == S4) { 2181 if (c == '/') 2182 state = S5; 2183 else 2184 return s; 2185 } else if (state == S5) { 2186 if (c != '/') 2187 state = S6; 2188 else 2189 return s; 2190 } else if (state == S6) { 2191 if (c == '/') 2192 return s.substring(0, i); 2193 } 2194 } 2195 return s; 2196 } 2197 2198 /** 2199 * Converts the specified object to a URI. 2200 * 2201 * @param o The object to convert to a URI. 2202 * @return A new URI, or the same object if the object was already a URI, or 2203 */ 2204 public static URI toURI(Object o) { 2205 if (o == null || o instanceof URI) 2206 return (URI)o; 2207 try { 2208 return new URI(o.toString()); 2209 } catch (URISyntaxException e) { 2210 throw asRuntimeException(e); 2211 } 2212 } 2213 2214 /** 2215 * Returns the first non-null, non-empty string in the list. 2216 * 2217 * @param s The strings to test. 2218 * @return The first non-empty string in the list, or <jk>null</jk> if they were all <jk>null</jk> or empty. 2219 */ 2220 public static String firstNonEmpty(String...s) { 2221 for (String ss : s) 2222 if (isNotEmpty(ss)) 2223 return ss; 2224 return null; 2225 } 2226 2227 /** 2228 * Same as {@link String#indexOf(int)} except allows you to check for multiple characters. 2229 * 2230 * @param s The string to check. 2231 * @param c The characters to check for. 2232 * @return The index into the string that is one of the specified characters. 2233 */ 2234 public static int indexOf(String s, char...c) { 2235 if (s == null) 2236 return -1; 2237 for (int i = 0; i < s.length(); i++) { 2238 char c2 = s.charAt(i); 2239 for (char cc : c) 2240 if (c2 == cc) 2241 return i; 2242 } 2243 return -1; 2244 } 2245 2246 /** 2247 * Similar to {@link MessageFormat#format(String, Object...)} except allows you to specify POJO arguments. 2248 * 2249 * @param pattern The string pattern. 2250 * @param args The arguments. 2251 * @return The formatted string. 2252 */ 2253 public static String format(String pattern, Object...args) { 2254 if (args == null || args.length == 0) 2255 return pattern; 2256 Object[] args2 = new Object[args.length]; 2257 for (int i = 0; i < args.length; i++) 2258 args2[i] = convertToReadable(args[i]); 2259 2260 int c = countChars(pattern, '\''); 2261 if (c % 2 != 0) 2262 throw new AssertionError("Dangling single quote found in pattern: " + pattern); 2263 2264 String msg = MessageFormat.format(pattern, args2); 2265 return msg; 2266 } 2267 2268 private static String convertToReadable(Object o) { 2269 if (o == null) 2270 return null; 2271 if (o instanceof Class) 2272 return ((Class<?>)o).getName(); 2273 if (o instanceof Method) 2274 return Method.class.cast(o).getName(); 2275 if (o.getClass().isArray()) 2276 return arrayAsList(o).stream().map(StringUtils::convertToReadable).collect(Collectors.joining(", ", "[", "]")); 2277 return o.toString(); 2278 } 2279 2280 private static List<Object> arrayAsList(Object array) { 2281 if (array.getClass().getComponentType().isPrimitive()) { 2282 List<Object> l = new ArrayList<>(Array.getLength(array)); 2283 for (int i = 0; i < Array.getLength(array); i++) 2284 l.add(Array.get(array, i)); 2285 return l; 2286 } 2287 return Arrays.asList((Object[])array); 2288 } 2289 2290 /** 2291 * Converts a string containing a possible multiplier suffix to an integer. 2292 * 2293 * <p> 2294 * The string can contain any of the following multiplier suffixes: 2295 * <ul> 2296 * <li><js>"K"</js> - x 1024 2297 * <li><js>"M"</js> - x 1024*1024 2298 * <li><js>"G"</js> - x 1024*1024*1024 2299 * <li><js>"k"</js> - x 1000 2300 * <li><js>"m"</js> - x 1000*1000 2301 * <li><js>"g"</js> - x 1000*1000*1000 2302 * </ul> 2303 * 2304 * @param s The string to parse. 2305 * @return The parsed value. 2306 */ 2307 public static int parseIntWithSuffix(String s) { 2308 assertArgNotNull("s", s); 2309 int m = multiplier(s); 2310 if (m == 1) 2311 return Integer.decode(s); 2312 return Integer.decode(s.substring(0, s.length()-1).trim()) * m; 2313 } 2314 2315 private static int multiplier(String s) { 2316 char c = s.isEmpty() ? null : s.charAt(s.length()-1); 2317 if (c == 'G') return 1024*1024*1024; 2318 if (c == 'M') return 1024*1024; 2319 if (c == 'K') return 1024; 2320 if (c == 'g') return 1000*1000*1000; 2321 if (c == 'm') return 1000*1000; 2322 if (c == 'k') return 1000; 2323 return 1; 2324 } 2325 2326 /** 2327 * Converts a string containing a possible multiplier suffix to a long. 2328 * 2329 * <p> 2330 * The string can contain any of the following multiplier suffixes: 2331 * <ul> 2332 * <li><js>"K"</js> - x 1024 2333 * <li><js>"M"</js> - x 1024*1024 2334 * <li><js>"G"</js> - x 1024*1024*1024 2335 * <li><js>"T"</js> - x 1024*1024*1024*1024 2336 * <li><js>"P"</js> - x 1024*1024*1024*1024*1024 2337 * <li><js>"k"</js> - x 1000 2338 * <li><js>"m"</js> - x 1000*1000 2339 * <li><js>"g"</js> - x 1000*1000*1000 2340 * <li><js>"t"</js> - x 1000*1000*1000*1000 2341 * <li><js>"p"</js> - x 1000*1000*1000*1000*1000 2342 * </ul> 2343 * 2344 * @param s The string to parse. 2345 * @return The parsed value. 2346 */ 2347 public static long parseLongWithSuffix(String s) { 2348 assertArgNotNull("s", s); 2349 long m = multiplier2(s); 2350 if (m == 1) 2351 return Long.decode(s); 2352 return Long.decode(s.substring(0, s.length()-1).trim()) * m; 2353 } 2354 2355 private static long multiplier2(String s) { 2356 char c = s.isEmpty() ? null : s.charAt(s.length()-1); 2357 if (c == 'P') return 1024*1024*1024*1024*1024; 2358 if (c == 'T') return 1024*1024*1024*1024; 2359 if (c == 'G') return 1024*1024*1024; 2360 if (c == 'M') return 1024*1024; 2361 if (c == 'K') return 1024; 2362 if (c == 'p') return 1000*1000*1000*1000*1000; 2363 if (c == 't') return 1000*1000*1000*1000; 2364 if (c == 'g') return 1000*1000*1000; 2365 if (c == 'm') return 1000*1000; 2366 if (c == 'k') return 1000; 2367 return 1; 2368 } 2369 2370 /** 2371 * Same as {@link String#contains(CharSequence)} except returns <jk>null</jk> if the value is null. 2372 * 2373 * @param value The string to check. 2374 * @param substring The value to check for. 2375 * @return <jk>true</jk> if the value contains the specified substring. 2376 */ 2377 public static boolean contains(String value, CharSequence substring) { 2378 return value == null ? false : value.contains(substring); 2379 } 2380 2381 /** 2382 * Returns <jk>true</jk> if the specified string appears to be an JSON array. 2383 * 2384 * @param o The object to test. 2385 * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored. 2386 * @return <jk>true</jk> if the specified string appears to be a JSON array. 2387 */ 2388 public static boolean isJsonArray(Object o, boolean ignoreWhitespaceAndComments) { 2389 if (o instanceof CharSequence) { 2390 String s = o.toString(); 2391 if (! ignoreWhitespaceAndComments) 2392 return (s.startsWith("[") && s.endsWith("]")); 2393 if (firstRealCharacter(s) != '[') 2394 return false; 2395 int i = s.lastIndexOf(']'); 2396 if (i == -1) 2397 return false; 2398 s = s.substring(i+1); 2399 if (firstRealCharacter(s) != -1) 2400 return false; 2401 return true; 2402 } 2403 return false; 2404 } 2405 2406 /** 2407 * Returns <jk>true</jk> if the specified string is valid JSON. 2408 * 2409 * <p> 2410 * Leading and trailing spaces are ignored. 2411 * <br>Leading and trailing comments are not allowed. 2412 * 2413 * @param s The string to test. 2414 * @return <jk>true</jk> if the specified string is valid JSON. 2415 */ 2416 public static boolean isJson(String s) { 2417 if (s == null) 2418 return false; 2419 char c1 = firstNonWhitespaceChar(s), c2 = lastNonWhitespaceChar(s); 2420 if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == '\'' && c2 == '\'') 2421 return true; 2422 if (isOneOf(s, "true","false","null") || isNumeric(s)) 2423 return true; 2424 return false; 2425 } 2426 2427 /** 2428 * Returns <jk>true</jk> if the specified string appears to be a JSON object. 2429 * 2430 * @param o The object to test. 2431 * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored. 2432 * @return <jk>true</jk> if the specified string appears to be a JSON object. 2433 */ 2434 public static boolean isJsonObject(Object o, boolean ignoreWhitespaceAndComments) { 2435 if (o instanceof CharSequence) { 2436 String s = o.toString(); 2437 if (! ignoreWhitespaceAndComments) 2438 return (s.startsWith("{") && s.endsWith("}")); 2439 if (firstRealCharacter(s) != '{') 2440 return false; 2441 int i = s.lastIndexOf('}'); 2442 if (i == -1) 2443 return false; 2444 s = s.substring(i+1); 2445 if (firstRealCharacter(s) != -1) 2446 return false; 2447 return true; 2448 } 2449 return false; 2450 } 2451 2452 private static int firstRealCharacter(String s) { 2453 try (StringReader r = new StringReader(s)) { 2454 int c = 0; 2455 while ((c = r.read()) != -1) { 2456 if (! Character.isWhitespace(c)) { 2457 if (c == '/') { 2458 skipComments(r); 2459 } else { 2460 return c; 2461 } 2462 } 2463 } 2464 return -1; 2465 } catch (Exception e) { 2466 throw asRuntimeException(e); 2467 } 2468 } 2469 private static void skipComments(StringReader r) throws IOException { 2470 int c = r.read(); 2471 // "/* */" style comments 2472 if (c == '*') { 2473 while (c != -1) 2474 if ((c = r.read()) == '*') 2475 if ((c = r.read()) == '/') 2476 return; 2477 // "//" style comments 2478 } else if (c == '/') { 2479 while (c != -1) { 2480 c = r.read(); 2481 if (c == -1 || c == '\n') 2482 return; 2483 } 2484 } 2485 } 2486 2487 /** 2488 * Takes in a string, splits it by lines, and then prepends each line with line numbers. 2489 * 2490 * @param s The string. 2491 * @return The string with line numbers added. 2492 */ 2493 public static String getNumberedLines(String s) { 2494 return getNumberedLines(s, 1, Integer.MAX_VALUE); 2495 } 2496 2497 /** 2498 * Same as {@link #getNumberedLines(String)} except only returns the specified lines. 2499 * 2500 * <p> 2501 * Out-of-bounds values are allowed and fixed. 2502 * 2503 * @param s The string. 2504 * @param start The starting line (1-indexed). 2505 * @param end The ending line (1-indexed). 2506 * @return The string with line numbers added. 2507 */ 2508 public static String getNumberedLines(String s, int start, int end) { 2509 if (s == null) 2510 return null; 2511 String[] lines = s.split("[\r\n]+"); 2512 final int digits = String.valueOf(lines.length).length(); 2513 if (start < 1) 2514 start = 1; 2515 if (end < 0) 2516 end = Integer.MAX_VALUE; 2517 if (end > lines.length) 2518 end = lines.length; 2519 StringBuilder sb = new StringBuilder(); 2520 for (String l : Arrays.asList(lines).subList(start-1, end)) 2521 sb.append(String.format("%0"+digits+"d", start++)).append(": ").append(l).append("\n"); 2522 return sb.toString(); 2523 } 2524 2525 /** 2526 * Compares two strings, but gracefully handles <jk>nulls</jk>. 2527 * 2528 * @param s1 The first string. 2529 * @param s2 The second string. 2530 * @return The same as {@link String#compareTo(String)}. 2531 */ 2532 public static int compare(String s1, String s2) { 2533 if (s1 == null && s2 == null) 2534 return 0; 2535 if (s1 == null) 2536 return Integer.MIN_VALUE; 2537 if (s2 == null) 2538 return Integer.MAX_VALUE; 2539 return s1.compareTo(s2); 2540 } 2541 2542 /** 2543 * Returns the first character in the specified string. 2544 * 2545 * @param s The string to check. 2546 * @return The first character in the string, or <c>0</c> if the string is <jk>null</jk> or empty. 2547 */ 2548 public static char firstChar(String s) { 2549 if (s == null || s.isEmpty()) 2550 return 0; 2551 return s.charAt(0); 2552 } 2553 2554 /** 2555 * Converts a string containing <js>"*"</js> meta characters with a regular expression pattern. 2556 * 2557 * @param s The string to create a pattern from. 2558 * @return A regular expression pattern. 2559 */ 2560 public static Pattern getMatchPattern(String s) { 2561 return getMatchPattern(s, 0); 2562 } 2563 2564 /** 2565 * Converts a string containing <js>"*"</js> meta characters with a regular expression pattern. 2566 * 2567 * @param s The string to create a pattern from. 2568 * @param flags Regular expression flags. 2569 * @return A regular expression pattern. 2570 */ 2571 public static Pattern getMatchPattern(String s, int flags) { 2572 if (s == null) 2573 return null; 2574 StringBuilder sb = new StringBuilder(); 2575 sb.append("\\Q"); 2576 for (int i = 0; i < s.length(); i++) { 2577 char c = s.charAt(i); 2578 if (c == '*') 2579 sb.append("\\E").append(".*").append("\\Q"); 2580 else if (c == '?') 2581 sb.append("\\E").append(".").append("\\Q"); 2582 else 2583 sb.append(c); 2584 } 2585 sb.append("\\E"); 2586 return Pattern.compile(sb.toString(), flags); 2587 } 2588 2589 /** 2590 * Parses a duration string. 2591 * 2592 * <p> 2593 * Examples: 2594 * <ul> 2595 * <li><js>"1000"</js> - 1000 milliseconds. 2596 * <li><js>"10s"</js> - 10 seconds. 2597 * <li><js>"10 sec"</js> - 10 seconds. 2598 * <li><js>"10 seconds"</js> - 10 seconds. 2599 * </ul> 2600 * 2601 * <p> 2602 * Use any of the following suffixes: 2603 * <ul> 2604 * <li>None (time in milliseconds). 2605 * <li><js>"s"</js>/<js>"sec"</js>/<js>"second"</js>/<js>"seconds"</js> 2606 * <li><js>"m"</js>/<js>"min"</js>/<js>"minutes"</js>/<js>"seconds"</js> 2607 * <li><js>"h"</js>/<js>"hour"</js>/<js>"hours"</js> 2608 * <li><js>"d"</js>/<js>"day"</js>/<js>"days"</js> 2609 * <li><js>"w"</js>/<js>"week"</js>/<js>"weeks"</js> 2610 * </ul> 2611 * 2612 * <p> 2613 * Suffixes are case-insensitive. 2614 * <br>Whitespace is ignored. 2615 * 2616 * @param s The string to parse. 2617 * @return 2618 * The time in milliseconds, or <c>-1</c> if the string is empty or <jk>null</jk>. 2619 */ 2620 public static long getDuration(String s) { 2621 s = trim(s); 2622 if (isEmpty(s)) 2623 return -1; 2624 int i; 2625 for (i = 0; i < s.length(); i++) { 2626 char c = s.charAt(i); 2627 if (c < '0' || c > '9') 2628 break; 2629 } 2630 long l; 2631 if (i == s.length()) 2632 l = Long.parseLong(s); 2633 else { 2634 l = Long.parseLong(s.substring(0, i).trim()); 2635 String r = s.substring(i).trim().toLowerCase(); 2636 if (r.startsWith("s")) 2637 l *= 1000; 2638 else if (r.startsWith("m")) 2639 l *= 1000 * 60; 2640 else if (r.startsWith("h")) 2641 l *= 1000 * 60 * 60; 2642 else if (r.startsWith("d")) 2643 l *= 1000 * 60 * 60 * 24; 2644 else if (r.startsWith("w")) 2645 l *= 1000 * 60 * 60 * 24 * 7; 2646 } 2647 return l; 2648 } 2649 2650 /** 2651 * Strips invalid characters such as CTRL characters from a string meant to be encoded 2652 * as an HTTP header value. 2653 * 2654 * @param s The string to strip chars from. 2655 * @return The string with invalid characters removed. 2656 */ 2657 public static String stripInvalidHttpHeaderChars(String s) { 2658 if (s == null) 2659 return null; 2660 2661 boolean needsReplace = false; 2662 for (int i = 0; i < s.length() && ! needsReplace; i++) 2663 needsReplace |= httpHeaderChars.contains(s.charAt(i)); 2664 2665 if (! needsReplace) 2666 return s; 2667 2668 StringBuilder sb = new StringBuilder(s.length()); 2669 for (int i = 0; i < s.length(); i++) { 2670 char c = s.charAt(i); 2671 if (httpHeaderChars.contains(c)) 2672 sb.append(c); 2673 } 2674 2675 return sb.toString(); 2676 } 2677 2678 /** 2679 * Abbreviates a String using ellipses. 2680 * 2681 * @param in The input string. 2682 * @param length The max length of the resulting string. 2683 * @return The abbreviated string. 2684 */ 2685 public static String abbreviate(String in, int length) { 2686 if (in == null || in.length() <= length || in.length() <= 3) 2687 return in; 2688 return in.substring(0, length-3) + "..."; 2689 } 2690 2691 /** 2692 * Splits the method arguments in the signature of a method. 2693 * 2694 * @param s The arguments to split. 2695 * @return The split arguments. 2696 */ 2697 public static String[] splitMethodArgs(String s) { 2698 if (s == null) 2699 return null; 2700 if (isEmpty(s)) 2701 return new String[0]; 2702 if (s.indexOf(',') == -1) 2703 return new String[]{s}; 2704 2705 List<String> l = new LinkedList<>(); 2706 char[] sArray = s.toCharArray(); 2707 int x1 = 0, paramDepth = 0; 2708 for (int i = 0; i < sArray.length; i++) { 2709 char c = s.charAt(i); 2710 if (c == '>') 2711 paramDepth++; 2712 else if (c == '<') 2713 paramDepth--; 2714 else if (c == ',' && paramDepth == 0) { 2715 String s2 = new String(sArray, x1, i-x1); 2716 l.add(s2.trim()); 2717 x1 = i+1; 2718 } 2719 } 2720 String s2 = new String(sArray, x1, sArray.length-x1); 2721 l.add(s2.trim()); 2722 2723 return l.toArray(new String[l.size()]); 2724 } 2725 2726 private static final AsciiSet URI_CHARS = AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0','9').range('A','Z').range('a','z').build(); 2727 2728 /** 2729 * Attempts to escape any invalid characters found in a URI. 2730 * 2731 * @param in The URI to fix. 2732 * @return The fixed URI. 2733 */ 2734 public static String fixUrl(String in) { 2735 2736 if (in == null) 2737 return null; 2738 2739 StringBuilder sb = null; 2740 2741 int m = 0; 2742 for (int i = 0; i < in.length(); i++) { 2743 char c = in.charAt(i); 2744 if (c <= 127 && ! URI_CHARS.contains(c)) { 2745 sb = append(sb, in.substring(m, i)); 2746 if (c == ' ') 2747 sb.append("+"); 2748 else 2749 sb.append('%').append(toHex2(c)); 2750 m = i+1; 2751 } 2752 } 2753 if (sb != null) { 2754 sb.append(in.substring(m)); 2755 return sb.toString(); 2756 } 2757 return in; 2758 2759 } 2760 2761 private static StringBuilder append(StringBuilder sb, String in) { 2762 if (sb == null) 2763 return new StringBuilder(in); 2764 sb.append(in); 2765 return sb; 2766 } 2767 2768 /** 2769 * Counts the number of the specified character in the specified string. 2770 * 2771 * @param s The string to check. 2772 * @param c The character to check for. 2773 * @return The number of those characters or zero if the string was <jk>null</jk>. 2774 */ 2775 public static int countChars(String s, char c) { 2776 int count = 0; 2777 if (s == null) 2778 return count; 2779 for (int i = 0; i < s.length(); i++) 2780 if (s.charAt(i) == c) 2781 count++; 2782 return count; 2783 } 2784 2785 /** 2786 * Converts string into a GZipped input stream. 2787 * 2788 * @param contents The contents to compress. 2789 * @return The input stream converted to GZip. 2790 * @throws Exception Exception occurred. 2791 */ 2792 public static byte[] compress(String contents) throws Exception { 2793 ByteArrayOutputStream baos = new ByteArrayOutputStream(contents.length()>>1); 2794 try (GZIPOutputStream gos = new GZIPOutputStream(baos)) { 2795 gos.write(contents.getBytes()); 2796 gos.finish(); 2797 gos.flush(); 2798 } 2799 return baos.toByteArray(); 2800 } 2801 2802 /** 2803 * Converts a GZipped input stream into a string. 2804 * 2805 * @param is The contents to decompress. 2806 * @return The string. 2807 * @throws Exception Exception occurred. 2808 */ 2809 public static String decompress(byte[] is) throws Exception { 2810 return read(new GZIPInputStream(new ByteArrayInputStream(is))); 2811 } 2812 2813 /** 2814 * Converts the specified object to a comma-delimited list. 2815 * 2816 * @param o The object to convert. 2817 * @return The specified object as a comma-delimited list. 2818 */ 2819 public static String cdl(Object o) { 2820 if (o == null) 2821 return null; 2822 if (o.getClass().isArray()) { 2823 StringBuilder sb = new StringBuilder(); 2824 for (int i = 0, j = Array.getLength(o); i < j; i++) { 2825 if (i > 0) 2826 sb.append(", "); 2827 sb.append(Array.get(o, i)); 2828 } 2829 return sb.toString(); 2830 } 2831 if (o instanceof Collection) 2832 return join((Collection<?>)o, ", "); 2833 return o.toString(); 2834 } 2835 2836 /** 2837 * Returns <jk>true</jk> if the specified character is a valid number character. 2838 * 2839 * @param c The character to check. 2840 * @return <jk>true</jk> if the specified character is a valid number character. 2841 */ 2842 public static boolean isNumberChar(char c) { 2843 return numberChars.contains(c); 2844 } 2845}