001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.util; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.List; 022import java.util.Locale; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.function.Function; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import static org.apache.camel.util.StringQuoteHelper.doubleQuote; 030 031/** 032 * Helper methods for working with Strings. 033 */ 034public final class StringHelper { 035 036 /** 037 * Constructor of utility class should be private. 038 */ 039 private StringHelper() { 040 } 041 042 /** 043 * Ensures that <code>s</code> is friendly for a URL or file system. 044 * 045 * @param s String to be sanitized. 046 * @return sanitized version of <code>s</code>. 047 * @throws NullPointerException if <code>s</code> is <code>null</code>. 048 */ 049 public static String sanitize(String s) { 050 return s 051 .replace(':', '-') 052 .replace('_', '-') 053 .replace('.', '-') 054 .replace('/', '-') 055 .replace('\\', '-'); 056 } 057 058 /** 059 * Remove carriage return and line feeds from a String, replacing them with an empty String. 060 * @param s String to be sanitized of carriage return / line feed characters 061 * @return sanitized version of <code>s</code>. 062 * @throws NullPointerException if <code>s</code> is <code>null</code>. 063 */ 064 public static String removeCRLF(String s) { 065 return s 066 .replaceAll("\r", "") 067 .replaceAll("\n", ""); 068 } 069 070 /** 071 * Counts the number of times the given char is in the string 072 * 073 * @param s the string 074 * @param ch the char 075 * @return number of times char is located in the string 076 */ 077 public static int countChar(String s, char ch) { 078 if (ObjectHelper.isEmpty(s)) { 079 return 0; 080 } 081 082 int matches = 0; 083 for (int i = 0; i < s.length(); i++) { 084 char c = s.charAt(i); 085 if (ch == c) { 086 matches++; 087 } 088 } 089 090 return matches; 091 } 092 093 /** 094 * Limits the length of a string 095 * 096 * @param s the string 097 * @param maxLength the maximum length of the returned string 098 * @return s if the length of s is less than maxLength or the first maxLength characters of s 099 * @deprecated use {@link #limitLength(String, int)} 100 */ 101 @Deprecated 102 public static String limitLenght(String s, int maxLength) { 103 return limitLength(s, maxLength); 104 } 105 106 /** 107 * Limits the length of a string 108 * 109 * @param s the string 110 * @param maxLength the maximum length of the returned string 111 * @return s if the length of s is less than maxLength or the first maxLength characters of s 112 */ 113 public static String limitLength(String s, int maxLength) { 114 if (ObjectHelper.isEmpty(s)) { 115 return s; 116 } 117 return s.length() <= maxLength ? s : s.substring(0, maxLength); 118 } 119 120 /** 121 * Removes all quotes (single and double) from the string 122 * 123 * @param s the string 124 * @return the string without quotes (single and double) 125 */ 126 public static String removeQuotes(String s) { 127 if (ObjectHelper.isEmpty(s)) { 128 return s; 129 } 130 131 s = replaceAll(s, "'", ""); 132 s = replaceAll(s, "\"", ""); 133 return s; 134 } 135 136 /** 137 * Removes all leading and ending quotes (single and double) from the string 138 * 139 * @param s the string 140 * @return the string without leading and ending quotes (single and double) 141 */ 142 public static String removeLeadingAndEndingQuotes(String s) { 143 if (ObjectHelper.isEmpty(s)) { 144 return s; 145 } 146 147 String copy = s.trim(); 148 if (copy.startsWith("'") && copy.endsWith("'")) { 149 return copy.substring(1, copy.length() - 1); 150 } 151 if (copy.startsWith("\"") && copy.endsWith("\"")) { 152 return copy.substring(1, copy.length() - 1); 153 } 154 155 // no quotes, so return as-is 156 return s; 157 } 158 159 /** 160 * Whether the string starts and ends with either single or double quotes. 161 * 162 * @param s the string 163 * @return <tt>true</tt> if the string starts and ends with either single or double quotes. 164 */ 165 public static boolean isQuoted(String s) { 166 if (ObjectHelper.isEmpty(s)) { 167 return false; 168 } 169 170 if (s.startsWith("'") && s.endsWith("'")) { 171 return true; 172 } 173 if (s.startsWith("\"") && s.endsWith("\"")) { 174 return true; 175 } 176 177 return false; 178 } 179 180 /** 181 * Encodes the text into safe XML by replacing < > and & with XML tokens 182 * 183 * @param text the text 184 * @return the encoded text 185 */ 186 public static String xmlEncode(String text) { 187 if (text == null) { 188 return ""; 189 } 190 // must replace amp first, so we dont replace < to amp later 191 text = replaceAll(text, "&", "&"); 192 text = replaceAll(text, "\"", """); 193 text = replaceAll(text, "<", "<"); 194 text = replaceAll(text, ">", ">"); 195 return text; 196 } 197 198 /** 199 * Determines if the string has at least one letter in upper case 200 * @param text the text 201 * @return <tt>true</tt> if at least one letter is upper case, <tt>false</tt> otherwise 202 */ 203 public static boolean hasUpperCase(String text) { 204 if (text == null) { 205 return false; 206 } 207 208 for (int i = 0; i < text.length(); i++) { 209 char ch = text.charAt(i); 210 if (Character.isUpperCase(ch)) { 211 return true; 212 } 213 } 214 215 return false; 216 } 217 218 /** 219 * Determines if the string is a fully qualified class name 220 */ 221 public static boolean isClassName(String text) { 222 boolean result = false; 223 if (text != null) { 224 String[] split = text.split("\\."); 225 if (split.length > 0) { 226 String lastToken = split[split.length - 1]; 227 if (lastToken.length() > 0) { 228 result = Character.isUpperCase(lastToken.charAt(0)); 229 } 230 } 231 } 232 return result; 233 } 234 235 /** 236 * Does the expression have the language start token? 237 * 238 * @param expression the expression 239 * @param language the name of the language, such as simple 240 * @return <tt>true</tt> if the expression contains the start token, <tt>false</tt> otherwise 241 */ 242 public static boolean hasStartToken(String expression, String language) { 243 if (expression == null) { 244 return false; 245 } 246 247 // for the simple language the expression start token could be "${" 248 if ("simple".equalsIgnoreCase(language) && expression.contains("${")) { 249 return true; 250 } 251 252 if (language != null && expression.contains("$" + language + "{")) { 253 return true; 254 } 255 256 return false; 257 } 258 259 /** 260 * Replaces all the from tokens in the given input string. 261 * <p/> 262 * This implementation is not recursive, not does it check for tokens in the replacement string. 263 * 264 * @param input the input string 265 * @param from the from string, must <b>not</b> be <tt>null</tt> or empty 266 * @param to the replacement string, must <b>not</b> be empty 267 * @return the replaced string, or the input string if no replacement was needed 268 * @throws IllegalArgumentException if the input arguments is invalid 269 */ 270 public static String replaceAll(String input, String from, String to) { 271 if (ObjectHelper.isEmpty(input)) { 272 return input; 273 } 274 if (from == null) { 275 throw new IllegalArgumentException("from cannot be null"); 276 } 277 if (to == null) { 278 // to can be empty, so only check for null 279 throw new IllegalArgumentException("to cannot be null"); 280 } 281 282 // fast check if there is any from at all 283 if (!input.contains(from)) { 284 return input; 285 } 286 287 final int len = from.length(); 288 final int max = input.length(); 289 StringBuilder sb = new StringBuilder(max); 290 for (int i = 0; i < max;) { 291 if (i + len <= max) { 292 String token = input.substring(i, i + len); 293 if (from.equals(token)) { 294 sb.append(to); 295 // fast forward 296 i = i + len; 297 continue; 298 } 299 } 300 301 // append single char 302 sb.append(input.charAt(i)); 303 // forward to next 304 i++; 305 } 306 return sb.toString(); 307 } 308 309 /** 310 * Creates a json tuple with the given name/value pair. 311 * 312 * @param name the name 313 * @param value the value 314 * @param isMap whether the tuple should be map 315 * @return the json 316 */ 317 public static String toJson(String name, String value, boolean isMap) { 318 if (isMap) { 319 return "{ " + doubleQuote(name) + ": " + doubleQuote(value) + " }"; 320 } else { 321 return doubleQuote(name) + ": " + doubleQuote(value); 322 } 323 } 324 325 /** 326 * Asserts whether the string is <b>not</b> empty. 327 * 328 * @param value the string to test 329 * @param name the key that resolved the value 330 * @return the passed {@code value} as is 331 * @throws IllegalArgumentException is thrown if assertion fails 332 */ 333 public static String notEmpty(String value, String name) { 334 if (ObjectHelper.isEmpty(value)) { 335 throw new IllegalArgumentException(name + " must be specified and not empty"); 336 } 337 338 return value; 339 } 340 341 /** 342 * Asserts whether the string is <b>not</b> empty. 343 * 344 * @param value the string to test 345 * @param on additional description to indicate where this problem occurred (appended as toString()) 346 * @param name the key that resolved the value 347 * @return the passed {@code value} as is 348 * @throws IllegalArgumentException is thrown if assertion fails 349 */ 350 public static String notEmpty(String value, String name, Object on) { 351 if (on == null) { 352 ObjectHelper.notNull(value, name); 353 } else if (ObjectHelper.isEmpty(value)) { 354 throw new IllegalArgumentException(name + " must be specified and not empty on: " + on); 355 } 356 357 return value; 358 } 359 360 public static String[] splitOnCharacter(String value, String needle, int count) { 361 String rc[] = new String[count]; 362 rc[0] = value; 363 for (int i = 1; i < count; i++) { 364 String v = rc[i - 1]; 365 int p = v.indexOf(needle); 366 if (p < 0) { 367 return rc; 368 } 369 rc[i - 1] = v.substring(0, p); 370 rc[i] = v.substring(p + 1); 371 } 372 return rc; 373 } 374 375 /** 376 * Removes any starting characters on the given text which match the given 377 * character 378 * 379 * @param text the string 380 * @param ch the initial characters to remove 381 * @return either the original string or the new substring 382 */ 383 public static String removeStartingCharacters(String text, char ch) { 384 int idx = 0; 385 while (text.charAt(idx) == ch) { 386 idx++; 387 } 388 if (idx > 0) { 389 return text.substring(idx); 390 } 391 return text; 392 } 393 394 /** 395 * Capitalize the string (upper case first character) 396 * 397 * @param text the string 398 * @return the string capitalized (upper case first character) 399 */ 400 public static String capitalize(String text) { 401 return capitalize(text, false); 402 } 403 404 /** 405 * Capitalize the string (upper case first character) 406 * 407 * @param text the string 408 * @param dashToCamelCase whether to also convert dash format into camel case (hello-great-world -> helloGreatWorld) 409 * @return the string capitalized (upper case first character) 410 */ 411 public static String capitalize(String text, boolean dashToCamelCase) { 412 if (dashToCamelCase) { 413 text = dashToCamelCase(text); 414 } 415 if (text == null) { 416 return null; 417 } 418 int length = text.length(); 419 if (length == 0) { 420 return text; 421 } 422 String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH); 423 if (length > 1) { 424 answer += text.substring(1, length); 425 } 426 return answer; 427 } 428 429 /** 430 * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) 431 * 432 * @param text the string 433 * @return the string camel cased 434 */ 435 public static String dashToCamelCase(String text) { 436 if (text == null) { 437 return null; 438 } 439 int length = text.length(); 440 if (length == 0) { 441 return text; 442 } 443 if (text.indexOf('-') == -1) { 444 return text; 445 } 446 447 StringBuilder sb = new StringBuilder(); 448 449 for (int i = 0; i < text.length(); i++) { 450 char c = text.charAt(i); 451 if (c == '-') { 452 i++; 453 sb.append(Character.toUpperCase(text.charAt(i))); 454 } else { 455 sb.append(c); 456 } 457 } 458 return sb.toString(); 459 } 460 461 /** 462 * Returns the string after the given token 463 * 464 * @param text the text 465 * @param after the token 466 * @return the text after the token, or <tt>null</tt> if text does not contain the token 467 */ 468 public static String after(String text, String after) { 469 if (!text.contains(after)) { 470 return null; 471 } 472 return text.substring(text.indexOf(after) + after.length()); 473 } 474 475 /** 476 * Returns an object after the given token 477 * 478 * @param text the text 479 * @param after the token 480 * @param mapper a mapping function to convert the string after the token to type T 481 * @return an Optional describing the result of applying a mapping function to the text after the token. 482 */ 483 public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) { 484 String result = after(text, after); 485 if (result == null) { 486 return Optional.empty(); 487 } else { 488 return Optional.ofNullable(mapper.apply(result)); 489 } 490 } 491 492 /** 493 * Returns the string before the given token 494 * 495 * @param text the text 496 * @param before the token 497 * @return the text before the token, or <tt>null</tt> if text does not 498 * contain the token 499 */ 500 public static String before(String text, String before) { 501 if (!text.contains(before)) { 502 return null; 503 } 504 return text.substring(0, text.indexOf(before)); 505 } 506 507 /** 508 * Returns an object before the given token 509 * 510 * @param text the text 511 * @param before the token 512 * @param mapper a mapping function to convert the string before the token to type T 513 * @return an Optional describing the result of applying a mapping function to the text before the token. 514 */ 515 public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) { 516 String result = before(text, before); 517 if (result == null) { 518 return Optional.empty(); 519 } else { 520 return Optional.ofNullable(mapper.apply(result)); 521 } 522 } 523 524 /** 525 * Returns the string between the given tokens 526 * 527 * @param text the text 528 * @param after the before token 529 * @param before the after token 530 * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens 531 */ 532 public static String between(String text, String after, String before) { 533 text = after(text, after); 534 if (text == null) { 535 return null; 536 } 537 return before(text, before); 538 } 539 540 /** 541 * Returns an object between the given token 542 * 543 * @param text the text 544 * @param after the before token 545 * @param before the after token 546 * @param mapper a mapping function to convert the string between the token to type T 547 * @return an Optional describing the result of applying a mapping function to the text between the token. 548 */ 549 public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) { 550 String result = between(text, after, before); 551 if (result == null) { 552 return Optional.empty(); 553 } else { 554 return Optional.ofNullable(mapper.apply(result)); 555 } 556 } 557 558 /** 559 * Returns the string between the most outer pair of tokens 560 * <p/> 561 * The number of token pairs must be evenly, eg there must be same number of before and after tokens, otherwise <tt>null</tt> is returned 562 * <p/> 563 * This implementation skips matching when the text is either single or double quoted. 564 * For example: 565 * <tt>${body.matches("foo('bar')")</tt> 566 * Will not match the parenthesis from the quoted text. 567 * 568 * @param text the text 569 * @param after the before token 570 * @param before the after token 571 * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens 572 */ 573 public static String betweenOuterPair(String text, char before, char after) { 574 if (text == null) { 575 return null; 576 } 577 578 int pos = -1; 579 int pos2 = -1; 580 int count = 0; 581 int count2 = 0; 582 583 boolean singleQuoted = false; 584 boolean doubleQuoted = false; 585 for (int i = 0; i < text.length(); i++) { 586 char ch = text.charAt(i); 587 if (!doubleQuoted && ch == '\'') { 588 singleQuoted = !singleQuoted; 589 } else if (!singleQuoted && ch == '\"') { 590 doubleQuoted = !doubleQuoted; 591 } 592 if (singleQuoted || doubleQuoted) { 593 continue; 594 } 595 596 if (ch == before) { 597 count++; 598 } else if (ch == after) { 599 count2++; 600 } 601 602 if (ch == before && pos == -1) { 603 pos = i; 604 } else if (ch == after) { 605 pos2 = i; 606 } 607 } 608 609 if (pos == -1 || pos2 == -1) { 610 return null; 611 } 612 613 // must be even paris 614 if (count != count2) { 615 return null; 616 } 617 618 return text.substring(pos + 1, pos2); 619 } 620 621 /** 622 * Returns an object between the most outer pair of tokens 623 * 624 * @param text the text 625 * @param after the before token 626 * @param before the after token 627 * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T 628 * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens. 629 */ 630 public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) { 631 String result = betweenOuterPair(text, before, after); 632 if (result == null) { 633 return Optional.empty(); 634 } else { 635 return Optional.ofNullable(mapper.apply(result)); 636 } 637 } 638 639 /** 640 * Returns true if the given name is a valid java identifier 641 */ 642 public static boolean isJavaIdentifier(String name) { 643 if (name == null) { 644 return false; 645 } 646 int size = name.length(); 647 if (size < 1) { 648 return false; 649 } 650 if (Character.isJavaIdentifierStart(name.charAt(0))) { 651 for (int i = 1; i < size; i++) { 652 if (!Character.isJavaIdentifierPart(name.charAt(i))) { 653 return false; 654 } 655 } 656 return true; 657 } 658 return false; 659 } 660 661 /** 662 * Cleans the string to a pure Java identifier so we can use it for loading class names. 663 * <p/> 664 * Especially from Spring DSL people can have \n \t or other characters that otherwise 665 * would result in ClassNotFoundException 666 * 667 * @param name the class name 668 * @return normalized classname that can be load by a class loader. 669 */ 670 public static String normalizeClassName(String name) { 671 StringBuilder sb = new StringBuilder(name.length()); 672 for (char ch : name.toCharArray()) { 673 if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) { 674 sb.append(ch); 675 } 676 } 677 return sb.toString(); 678 } 679 680 /** 681 * Compares old and new text content and report back which lines are changed 682 * 683 * @param oldText the old text 684 * @param newText the new text 685 * @return a list of line numbers that are changed in the new text 686 */ 687 public static List<Integer> changedLines(String oldText, String newText) { 688 if (oldText == null || oldText.equals(newText)) { 689 return Collections.emptyList(); 690 } 691 692 List<Integer> changed = new ArrayList<>(); 693 694 String[] oldLines = oldText.split("\n"); 695 String[] newLines = newText.split("\n"); 696 697 for (int i = 0; i < newLines.length; i++) { 698 String newLine = newLines[i]; 699 String oldLine = i < oldLines.length ? oldLines[i] : null; 700 if (oldLine == null) { 701 changed.add(i); 702 } else if (!newLine.equals(oldLine)) { 703 changed.add(i); 704 } 705 } 706 707 return changed; 708 } 709 710 /** 711 * Removes the leading and trailing whitespace and if the resulting 712 * string is empty returns {@code null}. Examples: 713 * <p> 714 * Examples: 715 * <blockquote><pre> 716 * trimToNull("abc") -> "abc" 717 * trimToNull(" abc") -> "abc" 718 * trimToNull(" abc ") -> "abc" 719 * trimToNull(" ") -> null 720 * trimToNull("") -> null 721 * </pre></blockquote> 722 */ 723 public static String trimToNull(final String given) { 724 if (given == null) { 725 return null; 726 } 727 728 final String trimmed = given.trim(); 729 730 if (trimmed.isEmpty()) { 731 return null; 732 } 733 734 return trimmed; 735 } 736 737 /** 738 * Checks if the src string contains what 739 * 740 * @param src is the source string to be checked 741 * @param what is the string which will be looked up in the src argument 742 * @return true/false 743 */ 744 public static boolean containsIgnoreCase(String src, String what) { 745 if (src == null || what == null) { 746 return false; 747 } 748 749 final int length = what.length(); 750 if (length == 0) { 751 return true; // Empty string is contained 752 } 753 754 final char firstLo = Character.toLowerCase(what.charAt(0)); 755 final char firstUp = Character.toUpperCase(what.charAt(0)); 756 757 for (int i = src.length() - length; i >= 0; i--) { 758 // Quick check before calling the more expensive regionMatches() method: 759 final char ch = src.charAt(i); 760 if (ch != firstLo && ch != firstUp) { 761 continue; 762 } 763 764 if (src.regionMatches(true, i, what, 0, length)) { 765 return true; 766 } 767 } 768 769 return false; 770 } 771 772 /** 773 * Outputs the bytes in human readable format in units of KB,MB,GB etc. 774 * 775 * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied. 776 * @param bytes number of bytes 777 * @return human readable output 778 * @see java.lang.String#format(Locale, String, Object...) 779 */ 780 public static String humanReadableBytes(Locale locale, long bytes) { 781 int unit = 1024; 782 if (bytes < unit) { 783 return bytes + " B"; 784 } 785 int exp = (int) (Math.log(bytes) / Math.log(unit)); 786 String pre = "KMGTPE".charAt(exp - 1) + ""; 787 return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre); 788 } 789 790 /** 791 * Outputs the bytes in human readable format in units of KB,MB,GB etc. 792 * 793 * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 794 * 795 * @param bytes number of bytes 796 * @return human readable output 797 * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long) 798 */ 799 public static String humanReadableBytes(long bytes) { 800 return humanReadableBytes(Locale.getDefault(), bytes); 801 } 802 803 /** 804 * Check for string pattern matching with a number of strategies in the 805 * following order: 806 * 807 * - equals 808 * - null pattern always matches 809 * - * always matches 810 * - Ant style matching 811 * - Regexp 812 * 813 * @param patter the pattern 814 * @param target the string to test 815 * @return true if target matches the pattern 816 */ 817 public static boolean matches(String patter, String target) { 818 if (Objects.equals(patter, target)) { 819 return true; 820 } 821 822 if (Objects.isNull(patter)) { 823 return true; 824 } 825 826 if (Objects.equals("*", patter)) { 827 return true; 828 } 829 830 if (AntPathMatcher.INSTANCE.match(patter, target)) { 831 return true; 832 } 833 834 Pattern p = Pattern.compile(patter); 835 Matcher m = p.matcher(target); 836 837 return m.matches(); 838 } 839 840}