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