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 029/** 030 * Helper methods for working with Strings. 031 */ 032public final class StringHelper { 033 034 /** 035 * Constructor of utility class should be private. 036 */ 037 private StringHelper() { 038 } 039 040 /** 041 * Ensures that <code>s</code> is friendly for a URL or file system. 042 * 043 * @param s String to be sanitized. 044 * @return sanitized version of <code>s</code>. 045 * @throws NullPointerException if <code>s</code> is <code>null</code>. 046 */ 047 public static String sanitize(String s) { 048 return s 049 .replace(':', '-') 050 .replace('_', '-') 051 .replace('.', '-') 052 .replace('/', '-') 053 .replace('\\', '-'); 054 } 055 056 /** 057 * Remove carriage return and line feeds from a String, replacing them with an empty String. 058 * @param s String to be sanitized of carriage return / line feed characters 059 * @return sanitized version of <code>s</code>. 060 * @throws NullPointerException if <code>s</code> is <code>null</code>. 061 */ 062 public static String removeCRLF(String s) { 063 return s 064 .replace("\r", "") 065 .replace("\n", ""); 066 } 067 068 /** 069 * Counts the number of times the given char is in the string 070 * 071 * @param s the string 072 * @param ch the char 073 * @return number of times char is located in the string 074 */ 075 public static int countChar(String s, char ch) { 076 return countChar(s, ch, -1); 077 } 078 079 /** 080 * Counts the number of times the given char is in the string 081 * 082 * @param s the string 083 * @param ch the char 084 * @param end end index 085 * @return number of times char is located in the string 086 */ 087 public static int countChar(String s, char ch, int end) { 088 if (s == null || s.isEmpty()) { 089 return 0; 090 } 091 092 int matches = 0; 093 int len = end < 0 ? s.length() : end; 094 for (int i = 0; i < len; i++) { 095 char c = s.charAt(i); 096 if (ch == c) { 097 matches++; 098 } 099 } 100 101 return matches; 102 } 103 104 /** 105 * Limits the length of a string 106 * 107 * @param s the string 108 * @param maxLength the maximum length of the returned string 109 * @return s if the length of s is less than maxLength or the first maxLength characters of s 110 */ 111 public static String limitLength(String s, int maxLength) { 112 if (ObjectHelper.isEmpty(s)) { 113 return s; 114 } 115 return s.length() <= maxLength ? s : s.substring(0, maxLength); 116 } 117 118 /** 119 * Removes all quotes (single and double) from the string 120 * 121 * @param s the string 122 * @return the string without quotes (single and double) 123 */ 124 public static String removeQuotes(String s) { 125 if (ObjectHelper.isEmpty(s)) { 126 return s; 127 } 128 129 s = replaceAll(s, "'", ""); 130 s = replaceAll(s, "\"", ""); 131 return s; 132 } 133 134 /** 135 * Removes all leading and ending quotes (single and double) from the string 136 * 137 * @param s the string 138 * @return the string without leading and ending quotes (single and double) 139 */ 140 public static String removeLeadingAndEndingQuotes(String s) { 141 if (ObjectHelper.isEmpty(s)) { 142 return s; 143 } 144 145 String copy = s.trim(); 146 if (copy.startsWith("'") && copy.endsWith("'")) { 147 return copy.substring(1, copy.length() - 1); 148 } 149 if (copy.startsWith("\"") && copy.endsWith("\"")) { 150 return copy.substring(1, copy.length() - 1); 151 } 152 153 // no quotes, so return as-is 154 return s; 155 } 156 157 /** 158 * Whether the string starts and ends with either single or double quotes. 159 * 160 * @param s the string 161 * @return <tt>true</tt> if the string starts and ends with either single or double quotes. 162 */ 163 public static boolean isQuoted(String s) { 164 if (ObjectHelper.isEmpty(s)) { 165 return false; 166 } 167 168 if (s.startsWith("'") && s.endsWith("'")) { 169 return true; 170 } 171 if (s.startsWith("\"") && s.endsWith("\"")) { 172 return true; 173 } 174 175 return false; 176 } 177 178 /** 179 * Encodes the text into safe XML by replacing < > and & with XML tokens 180 * 181 * @param text the text 182 * @return the encoded text 183 */ 184 public static String xmlEncode(String text) { 185 if (text == null) { 186 return ""; 187 } 188 // must replace amp first, so we dont replace < to amp later 189 text = replaceAll(text, "&", "&"); 190 text = replaceAll(text, "\"", """); 191 text = replaceAll(text, "<", "<"); 192 text = replaceAll(text, ">", ">"); 193 return text; 194 } 195 196 /** 197 * Determines if the string has at least one letter in upper case 198 * @param text the text 199 * @return <tt>true</tt> if at least one letter is upper case, <tt>false</tt> otherwise 200 */ 201 public static boolean hasUpperCase(String text) { 202 if (text == null) { 203 return false; 204 } 205 206 for (int i = 0; i < text.length(); i++) { 207 char ch = text.charAt(i); 208 if (Character.isUpperCase(ch)) { 209 return true; 210 } 211 } 212 213 return false; 214 } 215 216 /** 217 * Determines if the string is a fully qualified class name 218 */ 219 public static boolean isClassName(String text) { 220 boolean result = false; 221 if (text != null) { 222 String[] split = text.split("\\."); 223 if (split.length > 0) { 224 String lastToken = split[split.length - 1]; 225 if (lastToken.length() > 0) { 226 result = Character.isUpperCase(lastToken.charAt(0)); 227 } 228 } 229 } 230 return result; 231 } 232 233 /** 234 * Does the expression have the language start token? 235 * 236 * @param expression the expression 237 * @param language the name of the language, such as simple 238 * @return <tt>true</tt> if the expression contains the start token, <tt>false</tt> otherwise 239 */ 240 public static boolean hasStartToken(String expression, String language) { 241 if (expression == null) { 242 return false; 243 } 244 245 // for the simple language the expression start token could be "${" 246 if ("simple".equalsIgnoreCase(language) && expression.contains("${")) { 247 return true; 248 } 249 250 if (language != null && expression.contains("$" + language + "{")) { 251 return true; 252 } 253 254 return false; 255 } 256 257 /** 258 * Replaces all the from tokens in the given input string. 259 * <p/> 260 * This implementation is not recursive, not does it check for tokens in the replacement string. 261 * 262 * @param input the input string 263 * @param from the from string, must <b>not</b> be <tt>null</tt> or empty 264 * @param to the replacement string, must <b>not</b> be empty 265 * @return the replaced string, or the input string if no replacement was needed 266 * @throws IllegalArgumentException if the input arguments is invalid 267 */ 268 public static String replaceAll(String input, String from, String to) { 269 // TODO: Use String.replace instead of this method when using JDK11 as minimum (as its much faster in JDK 11 onwards) 270 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 "{ " + StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value) + " }"; 320 } else { 321 return StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.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 int pos = text.indexOf(after); 470 if (pos == -1) { 471 return null; 472 } 473 return text.substring(pos + after.length()); 474 } 475 476 /** 477 * Returns an object after the given token 478 * 479 * @param text the text 480 * @param after the token 481 * @param mapper a mapping function to convert the string after the token to type T 482 * @return an Optional describing the result of applying a mapping function to the text after the token. 483 */ 484 public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) { 485 String result = after(text, after); 486 if (result == null) { 487 return Optional.empty(); 488 } else { 489 return Optional.ofNullable(mapper.apply(result)); 490 } 491 } 492 493 /** 494 * Returns the string before the given token 495 * 496 * @param text the text 497 * @param before the token 498 * @return the text before the token, or <tt>null</tt> if text does not 499 * contain the token 500 */ 501 public static String before(String text, String before) { 502 int pos = text.indexOf(before); 503 return pos == -1 ? null : text.substring(0, pos); 504 } 505 506 /** 507 * Returns an object before the given token 508 * 509 * @param text the text 510 * @param before the token 511 * @param mapper a mapping function to convert the string before the token to type T 512 * @return an Optional describing the result of applying a mapping function to the text before the token. 513 */ 514 public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) { 515 String result = before(text, before); 516 if (result == null) { 517 return Optional.empty(); 518 } else { 519 return Optional.ofNullable(mapper.apply(result)); 520 } 521 } 522 523 /** 524 * Returns the string between the given tokens 525 * 526 * @param text the text 527 * @param after the before token 528 * @param before the after token 529 * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens 530 */ 531 public static String between(String text, String after, String before) { 532 text = after(text, after); 533 if (text == null) { 534 return null; 535 } 536 return before(text, before); 537 } 538 539 /** 540 * Returns an object between the given token 541 * 542 * @param text the text 543 * @param after the before token 544 * @param before the after token 545 * @param mapper a mapping function to convert the string between the token to type T 546 * @return an Optional describing the result of applying a mapping function to the text between the token. 547 */ 548 public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) { 549 String result = between(text, after, before); 550 if (result == null) { 551 return Optional.empty(); 552 } else { 553 return Optional.ofNullable(mapper.apply(result)); 554 } 555 } 556 557 /** 558 * Returns the string between the most outer pair of tokens 559 * <p/> 560 * 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 561 * <p/> 562 * This implementation skips matching when the text is either single or double quoted. 563 * For example: 564 * <tt>${body.matches("foo('bar')")</tt> 565 * Will not match the parenthesis from the quoted text. 566 * 567 * @param text the text 568 * @param after the before token 569 * @param before the after token 570 * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens 571 */ 572 public static String betweenOuterPair(String text, char before, char after) { 573 if (text == null) { 574 return null; 575 } 576 577 int pos = -1; 578 int pos2 = -1; 579 int count = 0; 580 int count2 = 0; 581 582 boolean singleQuoted = false; 583 boolean doubleQuoted = false; 584 for (int i = 0; i < text.length(); i++) { 585 char ch = text.charAt(i); 586 if (!doubleQuoted && ch == '\'') { 587 singleQuoted = !singleQuoted; 588 } else if (!singleQuoted && ch == '\"') { 589 doubleQuoted = !doubleQuoted; 590 } 591 if (singleQuoted || doubleQuoted) { 592 continue; 593 } 594 595 if (ch == before) { 596 count++; 597 } else if (ch == after) { 598 count2++; 599 } 600 601 if (ch == before && pos == -1) { 602 pos = i; 603 } else if (ch == after) { 604 pos2 = i; 605 } 606 } 607 608 if (pos == -1 || pos2 == -1) { 609 return null; 610 } 611 612 // must be even paris 613 if (count != count2) { 614 return null; 615 } 616 617 return text.substring(pos + 1, pos2); 618 } 619 620 /** 621 * Returns an object between the most outer pair of tokens 622 * 623 * @param text the text 624 * @param after the before token 625 * @param before the after token 626 * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T 627 * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens. 628 */ 629 public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) { 630 String result = betweenOuterPair(text, before, after); 631 if (result == null) { 632 return Optional.empty(); 633 } else { 634 return Optional.ofNullable(mapper.apply(result)); 635 } 636 } 637 638 /** 639 * Returns true if the given name is a valid java identifier 640 */ 641 public static boolean isJavaIdentifier(String name) { 642 if (name == null) { 643 return false; 644 } 645 int size = name.length(); 646 if (size < 1) { 647 return false; 648 } 649 if (Character.isJavaIdentifierStart(name.charAt(0))) { 650 for (int i = 1; i < size; i++) { 651 if (!Character.isJavaIdentifierPart(name.charAt(i))) { 652 return false; 653 } 654 } 655 return true; 656 } 657 return false; 658 } 659 660 /** 661 * Cleans the string to a pure Java identifier so we can use it for loading class names. 662 * <p/> 663 * Especially from Spring DSL people can have \n \t or other characters that otherwise 664 * would result in ClassNotFoundException 665 * 666 * @param name the class name 667 * @return normalized classname that can be load by a class loader. 668 */ 669 public static String normalizeClassName(String name) { 670 StringBuilder sb = new StringBuilder(name.length()); 671 for (char ch : name.toCharArray()) { 672 if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) { 673 sb.append(ch); 674 } 675 } 676 return sb.toString(); 677 } 678 679 /** 680 * Compares old and new text content and report back which lines are changed 681 * 682 * @param oldText the old text 683 * @param newText the new text 684 * @return a list of line numbers that are changed in the new text 685 */ 686 public static List<Integer> changedLines(String oldText, String newText) { 687 if (oldText == null || oldText.equals(newText)) { 688 return Collections.emptyList(); 689 } 690 691 List<Integer> changed = new ArrayList<>(); 692 693 String[] oldLines = oldText.split("\n"); 694 String[] newLines = newText.split("\n"); 695 696 for (int i = 0; i < newLines.length; i++) { 697 String newLine = newLines[i]; 698 String oldLine = i < oldLines.length ? oldLines[i] : null; 699 if (oldLine == null) { 700 changed.add(i); 701 } else if (!newLine.equals(oldLine)) { 702 changed.add(i); 703 } 704 } 705 706 return changed; 707 } 708 709 /** 710 * Removes the leading and trailing whitespace and if the resulting 711 * string is empty returns {@code null}. Examples: 712 * <p> 713 * Examples: 714 * <blockquote><pre> 715 * trimToNull("abc") -> "abc" 716 * trimToNull(" abc") -> "abc" 717 * trimToNull(" abc ") -> "abc" 718 * trimToNull(" ") -> null 719 * trimToNull("") -> null 720 * </pre></blockquote> 721 */ 722 public static String trimToNull(final String given) { 723 if (given == null) { 724 return null; 725 } 726 727 final String trimmed = given.trim(); 728 729 if (trimmed.isEmpty()) { 730 return null; 731 } 732 733 return trimmed; 734 } 735 736 /** 737 * Checks if the src string contains what 738 * 739 * @param src is the source string to be checked 740 * @param what is the string which will be looked up in the src argument 741 * @return true/false 742 */ 743 public static boolean containsIgnoreCase(String src, String what) { 744 if (src == null || what == null) { 745 return false; 746 } 747 748 final int length = what.length(); 749 if (length == 0) { 750 return true; // Empty string is contained 751 } 752 753 final char firstLo = Character.toLowerCase(what.charAt(0)); 754 final char firstUp = Character.toUpperCase(what.charAt(0)); 755 756 for (int i = src.length() - length; i >= 0; i--) { 757 // Quick check before calling the more expensive regionMatches() method: 758 final char ch = src.charAt(i); 759 if (ch != firstLo && ch != firstUp) { 760 continue; 761 } 762 763 if (src.regionMatches(true, i, what, 0, length)) { 764 return true; 765 } 766 } 767 768 return false; 769 } 770 771 /** 772 * Outputs the bytes in human readable format in units of KB,MB,GB etc. 773 * 774 * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied. 775 * @param bytes number of bytes 776 * @return human readable output 777 * @see java.lang.String#format(Locale, String, Object...) 778 */ 779 public static String humanReadableBytes(Locale locale, long bytes) { 780 int unit = 1024; 781 if (bytes < unit) { 782 return bytes + " B"; 783 } 784 int exp = (int) (Math.log(bytes) / Math.log(unit)); 785 String pre = "KMGTPE".charAt(exp - 1) + ""; 786 return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre); 787 } 788 789 /** 790 * Outputs the bytes in human readable format in units of KB,MB,GB etc. 791 * 792 * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 793 * 794 * @param bytes number of bytes 795 * @return human readable output 796 * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long) 797 */ 798 public static String humanReadableBytes(long bytes) { 799 return humanReadableBytes(Locale.getDefault(), bytes); 800 } 801 802 /** 803 * Check for string pattern matching with a number of strategies in the 804 * following order: 805 * 806 * - equals 807 * - null pattern always matches 808 * - * always matches 809 * - Ant style matching 810 * - Regexp 811 * 812 * @param pattern the pattern 813 * @param target the string to test 814 * @return true if target matches the pattern 815 */ 816 public static boolean matches(String pattern, String target) { 817 if (Objects.equals(pattern, target)) { 818 return true; 819 } 820 821 if (Objects.isNull(pattern)) { 822 return true; 823 } 824 825 if (Objects.equals("*", pattern)) { 826 return true; 827 } 828 829 if (AntPathMatcher.INSTANCE.match(pattern, target)) { 830 return true; 831 } 832 833 Pattern p = Pattern.compile(pattern); 834 Matcher m = p.matcher(target); 835 836 return m.matches(); 837 } 838 839 public static String camelCaseToDash(String text) { 840 StringBuilder answer = new StringBuilder(); 841 842 Character prev = null; 843 Character next = null; 844 char[] arr = text.toCharArray(); 845 for (int i = 0; i < arr.length; i++) { 846 char ch = arr[i]; 847 if (i < arr.length - 1) { 848 next = arr[i + 1]; 849 } else { 850 next = null; 851 } 852 if (ch == '-' || ch == '_') { 853 answer.append("-"); 854 } else if (Character.isUpperCase(ch) && prev != null && !Character.isUpperCase(prev)) { 855 answer.append("-").append(ch); 856 } else if (Character.isUpperCase(ch) && prev != null && next != null && Character.isLowerCase(next)) { 857 answer.append("-").append(ch); 858 } else { 859 answer.append(ch); 860 } 861 prev = ch; 862 } 863 864 return answer.toString().toLowerCase(Locale.ENGLISH); 865 } 866 867}