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