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