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