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