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.Iterator; 022import java.util.List; 023import java.util.Locale; 024import java.util.NoSuchElementException; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.function.Function; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030import java.util.stream.Stream; 031 032/** 033 * Helper methods for working with Strings. 034 */ 035public final class StringHelper { 036 037 /** 038 * Constructor of utility class should be private. 039 */ 040 private StringHelper() { 041 } 042 043 /** 044 * Ensures that <code>s</code> is friendly for a URL or file system. 045 * 046 * @param s String to be sanitized. 047 * @return sanitized version of <code>s</code>. 048 * @throws NullPointerException if <code>s</code> is <code>null</code>. 049 */ 050 public static String sanitize(final String s) { 051 return s.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 * 061 * @param s String to be sanitized of carriage return / line feed characters 062 * @return sanitized version of <code>s</code>. 063 * @throws NullPointerException if <code>s</code> is <code>null</code>. 064 */ 065 public static String removeCRLF(String s) { 066 return s 067 .replace("\r", "") 068 .replace("\n", ""); 069 } 070 071 /** 072 * Counts the number of times the given char is in the string 073 * 074 * @param s the string 075 * @param ch the char 076 * @return number of times char is located in the string 077 */ 078 public static int countChar(String s, char ch) { 079 return countChar(s, ch, -1); 080 } 081 082 /** 083 * Counts the number of times the given char is in the string 084 * 085 * @param s the string 086 * @param ch the char 087 * @param end end index 088 * @return number of times char is located in the string 089 */ 090 public static int countChar(String s, char ch, int end) { 091 if (s == null || s.isEmpty()) { 092 return 0; 093 } 094 095 int matches = 0; 096 int len = end < 0 ? s.length() : end; 097 for (int i = 0; i < len; i++) { 098 char c = s.charAt(i); 099 if (ch == c) { 100 matches++; 101 } 102 } 103 104 return matches; 105 } 106 107 /** 108 * Limits the length of a string 109 * 110 * @param s the string 111 * @param maxLength the maximum length of the returned string 112 * @return s if the length of s is less than maxLength or the first maxLength characters of s 113 */ 114 public static String limitLength(String s, int maxLength) { 115 return limitLength(s, maxLength, null); 116 } 117 118 /** 119 * Limits the length of a string 120 * 121 * @param s the string 122 * @param maxLength the maximum length of the returned string 123 * @param prefix prefix to append if the string was limited 124 * @return s if the length of s is less than maxLength or the first maxLength characters of s 125 */ 126 public static String limitLength(String s, int maxLength, String prefix) { 127 if (ObjectHelper.isEmpty(s)) { 128 return s; 129 } 130 if (s.length() <= maxLength) { 131 return s; 132 } 133 s = s.substring(0, maxLength); 134 if (prefix != null) { 135 s = s + prefix; 136 } 137 return s; 138 } 139 140 /** 141 * Removes all quotes (single and double) from the string 142 * 143 * @param s the string 144 * @return the string without quotes (single and double) 145 */ 146 public static String removeQuotes(final String s) { 147 if (ObjectHelper.isEmpty(s)) { 148 return s; 149 } 150 151 return s.replace("'", "") 152 .replace("\"", ""); 153 } 154 155 /** 156 * Removes all leading and ending quotes (single and double) from the string 157 * 158 * @param s the string 159 * @return the string without leading and ending quotes (single and double) 160 */ 161 public static String removeLeadingAndEndingQuotes(final String s) { 162 if (ObjectHelper.isEmpty(s)) { 163 return s; 164 } 165 166 String copy = s.trim(); 167 if (copy.length() < 2) { 168 return s; 169 } 170 if (copy.startsWith("'") && copy.endsWith("'")) { 171 return copy.substring(1, copy.length() - 1); 172 } 173 if (copy.startsWith("\"") && copy.endsWith("\"")) { 174 return copy.substring(1, copy.length() - 1); 175 } 176 177 // no quotes, so return as-is 178 return s; 179 } 180 181 /** 182 * Whether the string starts and ends with either single or double quotes. 183 * 184 * @param s the string 185 * @return <tt>true</tt> if the string starts and ends with either single or double quotes. 186 */ 187 public static boolean isQuoted(String s) { 188 return isSingleQuoted(s) || isDoubleQuoted(s); 189 } 190 191 /** 192 * Whether the string starts and ends with single quotes. 193 * 194 * @param s the string 195 * @return <tt>true</tt> if the string starts and ends with single quotes. 196 */ 197 public static boolean isSingleQuoted(String s) { 198 if (ObjectHelper.isEmpty(s)) { 199 return false; 200 } 201 202 if (s.startsWith("'") && s.endsWith("'")) { 203 return true; 204 } 205 206 return false; 207 } 208 209 /** 210 * Whether the string starts and ends with double quotes. 211 * 212 * @param s the string 213 * @return <tt>true</tt> if the string starts and ends with double quotes. 214 */ 215 public static boolean isDoubleQuoted(String s) { 216 if (ObjectHelper.isEmpty(s)) { 217 return false; 218 } 219 220 if (s.startsWith("\"") && s.endsWith("\"")) { 221 return true; 222 } 223 224 return false; 225 } 226 227 /** 228 * Encodes the text into safe XML by replacing < > and & with XML tokens 229 * 230 * @param text the text 231 * @return the encoded text 232 */ 233 public static String xmlEncode(final String text) { 234 if (text == null) { 235 return ""; 236 } 237 // must replace amp first, so we dont replace < to amp later 238 return text.replace("&", "&") 239 .replace("\"", """) 240 .replace("'", "'") 241 .replace("<", "<") 242 .replace(">", ">"); 243 } 244 245 /** 246 * Decodes the text into safe XML by replacing XML tokens with character values 247 * 248 * @param text the text 249 * @return the encoded text 250 */ 251 public static String xmlDecode(final String text) { 252 if (text == null) { 253 return ""; 254 } 255 // must replace amp first, so we dont replace < to amp later 256 return text.replace("&", "&") 257 .replace(""", "\"") 258 .replace("'", "'") 259 .replace("<", "<") 260 .replace(">", ">"); 261 } 262 263 /** 264 * Determines if the string has at least one letter in upper case 265 * 266 * @param text the text 267 * @return <tt>true</tt> if at least one letter is upper case, <tt>false</tt> otherwise 268 */ 269 public static boolean hasUpperCase(String text) { 270 if (text == null) { 271 return false; 272 } 273 274 for (int i = 0; i < text.length(); i++) { 275 char ch = text.charAt(i); 276 if (Character.isUpperCase(ch)) { 277 return true; 278 } 279 } 280 281 return false; 282 } 283 284 /** 285 * Determines if the string is a fully qualified class name 286 */ 287 public static boolean isClassName(String text) { 288 if (text != null) { 289 int lastIndexOf = text.lastIndexOf('.'); 290 if (lastIndexOf <= 0) { 291 return false; 292 } 293 294 return Character.isUpperCase(text.charAt(lastIndexOf + 1)); 295 } 296 297 return false; 298 } 299 300 /** 301 * Does the expression have the language start token? 302 * 303 * @param expression the expression 304 * @param language the name of the language, such as simple 305 * @return <tt>true</tt> if the expression contains the start token, <tt>false</tt> otherwise 306 */ 307 public static boolean hasStartToken(String expression, String language) { 308 if (expression == null) { 309 return false; 310 } 311 312 // for the simple language, the expression start token could be "${" 313 if ("simple".equalsIgnoreCase(language) && expression.contains("${")) { 314 return true; 315 } 316 317 if (language != null && expression.contains("$" + language + "{")) { 318 return true; 319 } 320 321 return false; 322 } 323 324 /** 325 * Replaces the first from token in the given input string. 326 * <p/> 327 * This implementation is not recursive, not does it check for tokens in the replacement string. If from or to is 328 * null, then the input string is returned as-is 329 * 330 * @param input the input string 331 * @param from the from string 332 * @param to the replacement string 333 * @return the replaced string, or the input string if no replacement was needed 334 * @throws IllegalArgumentException if the input arguments is invalid 335 */ 336 public static String replaceFirst(String input, String from, String to) { 337 if (from == null || to == null) { 338 return input; 339 } 340 int pos = input.indexOf(from); 341 if (pos != -1) { 342 int len = from.length(); 343 return input.substring(0, pos) + to + input.substring(pos + len); 344 } else { 345 return input; 346 } 347 } 348 349 /** 350 * Creates a JSON tuple with the given name/value pair. 351 * 352 * @param name the name 353 * @param value the value 354 * @param isMap whether the tuple should be map 355 * @return the json 356 */ 357 public static String toJson(String name, String value, boolean isMap) { 358 if (isMap) { 359 return "{ " + StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value) + " }"; 360 } else { 361 return StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value); 362 } 363 } 364 365 /** 366 * Asserts whether the string is <b>not</b> empty. 367 * 368 * @param value the string to test 369 * @param name the key that resolved the value 370 * @return the passed {@code value} as is 371 * @throws IllegalArgumentException is thrown if assertion fails 372 */ 373 public static String notEmpty(String value, String name) { 374 if (ObjectHelper.isEmpty(value)) { 375 throw new IllegalArgumentException(name + " must be specified and not empty"); 376 } 377 378 return value; 379 } 380 381 /** 382 * Asserts whether the string is <b>not</b> empty. 383 * 384 * @param value the string to test 385 * @param on additional description to indicate where this problem occurred (appended as 386 * toString()) 387 * @param name the key that resolved the value 388 * @return the passed {@code value} as is 389 * @throws IllegalArgumentException is thrown if assertion fails 390 */ 391 public static String notEmpty(String value, String name, Object on) { 392 if (on == null) { 393 ObjectHelper.notNull(value, name); 394 } else if (ObjectHelper.isEmpty(value)) { 395 throw new IllegalArgumentException(name + " must be specified and not empty on: " + on); 396 } 397 398 return value; 399 } 400 401 public static String[] splitOnCharacter(String value, String needle, int count) { 402 String[] rc = new String[count]; 403 rc[0] = value; 404 for (int i = 1; i < count; i++) { 405 String v = rc[i - 1]; 406 int p = v.indexOf(needle); 407 if (p < 0) { 408 return rc; 409 } 410 rc[i - 1] = v.substring(0, p); 411 rc[i] = v.substring(p + 1); 412 } 413 return rc; 414 } 415 416 public static Iterator<String> splitOnCharacterAsIterator(String value, char needle, int count) { 417 // skip leading and trailing needles 418 int end = value.length() - 1; 419 boolean skipStart = value.charAt(0) == needle; 420 boolean skipEnd = value.charAt(end) == needle; 421 if (skipStart && skipEnd) { 422 value = value.substring(1, end); 423 count = count - 2; 424 } else if (skipStart) { 425 value = value.substring(1); 426 count = count - 1; 427 } else if (skipEnd) { 428 value = value.substring(0, end); 429 count = count - 1; 430 } 431 432 final int size = count; 433 final String text = value; 434 435 return new Iterator<>() { 436 int i; 437 int pos; 438 439 @Override 440 public boolean hasNext() { 441 return i < size; 442 } 443 444 @Override 445 public String next() { 446 if (i == size) { 447 throw new NoSuchElementException(); 448 } 449 String answer; 450 int end = text.indexOf(needle, pos); 451 if (end != -1) { 452 answer = text.substring(pos, end); 453 pos = end + 1; 454 } else { 455 answer = text.substring(pos); 456 // no more data 457 i = size; 458 } 459 return answer; 460 } 461 }; 462 } 463 464 public static List<String> splitOnCharacterAsList(String value, char needle, int count) { 465 // skip leading and trailing needles 466 int end = value.length() - 1; 467 boolean skipStart = value.charAt(0) == needle; 468 boolean skipEnd = value.charAt(end) == needle; 469 if (skipStart && skipEnd) { 470 value = value.substring(1, end); 471 count = count - 2; 472 } else if (skipStart) { 473 value = value.substring(1); 474 count = count - 1; 475 } else if (skipEnd) { 476 value = value.substring(0, end); 477 count = count - 1; 478 } 479 480 List<String> rc = new ArrayList<>(count); 481 int pos = 0; 482 for (int i = 0; i < count; i++) { 483 end = value.indexOf(needle, pos); 484 if (end != -1) { 485 String part = value.substring(pos, end); 486 pos = end + 1; 487 rc.add(part); 488 } else { 489 rc.add(value.substring(pos)); 490 break; 491 } 492 } 493 return rc; 494 } 495 496 /** 497 * Removes any starting characters on the given text which match the given character 498 * 499 * @param text the string 500 * @param ch the initial characters to remove 501 * @return either the original string or the new substring 502 */ 503 public static String removeStartingCharacters(String text, char ch) { 504 int idx = 0; 505 while (text.charAt(idx) == ch) { 506 idx++; 507 } 508 if (idx > 0) { 509 return text.substring(idx); 510 } 511 return text; 512 } 513 514 /** 515 * Capitalize the string (upper case only first character) 516 * 517 * @param text the string 518 * @return the string capitalized (upper case only first character) 519 */ 520 public static String capitalize(String text) { 521 return capitalize(text, false); 522 } 523 524 /** 525 * Capitalize the string (upper case first character in every word) 526 * 527 * @param text the string 528 * @return the string capitalized (upper case first character in every world) 529 */ 530 public static String capitalizeAll(String text) { 531 return doCapitalize(text, true); 532 } 533 534 /** 535 * Capitalize the string (upper case first character) 536 * 537 * @param text the string 538 * @param dashToCamelCase whether to also convert dash format into camel case (hello-great-world -> 539 * helloGreatWorld) 540 * @return the string capitalized (upper case first character) 541 */ 542 public static String capitalize(final String text, boolean dashToCamelCase) { 543 String ret = text; 544 if (dashToCamelCase) { 545 ret = dashToCamelCase(text); 546 } 547 return doCapitalize(ret, false); 548 } 549 550 private static String doCapitalize(String ret, boolean all) { 551 if (ret == null) { 552 return null; 553 } 554 555 final char[] chars = ret.toCharArray(); 556 557 // We are OK with the limitations of Character.toUpperCase. The symbols and ideographs 558 // for which it does not return the capitalized value should not be used here (this is 559 // mostly used to capitalize setters/getters) 560 chars[0] = Character.toUpperCase(chars[0]); 561 if (all && chars.length > 2) { 562 for (int i = 2; i < chars.length; i++) { 563 char prev = chars[i - 1]; 564 if (prev == ' ') { 565 chars[i] = Character.toUpperCase(chars[i]); 566 } 567 } 568 } 569 return new String(chars); 570 } 571 572 /** 573 * De-capitalize the string (lower case first character) 574 * 575 * @param text the string 576 * @return the string decapitalized (lower case first character) 577 */ 578 public static String decapitalize(final String text) { 579 if (text == null) { 580 return null; 581 } 582 583 final char[] chars = text.toCharArray(); 584 585 // We are OK with the limitations of Character.toLowerCase. The symbols and ideographs 586 // for which it does not return the lower case value should not be used here (this isap 587 // mostly used to convert part of setters/getters to properties) 588 chars[0] = Character.toLowerCase(chars[0]); 589 return new String(chars); 590 } 591 592 /** 593 * Whether the string contains dashes or not 594 * 595 * @param text the string to check 596 * @return true if it contains dashes or false otherwise 597 */ 598 public static boolean isDashed(String text) { 599 return !text.isEmpty() && text.indexOf('-') != -1; 600 } 601 602 /** 603 * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) 604 * 605 * @param text the string 606 * @return the string camel cased 607 */ 608 public static String dashToCamelCase(final String text) { 609 if (text == null) { 610 return null; 611 } 612 if (!isDashed(text)) { 613 return text; 614 } 615 616 // there is at least 1 dash so the capacity can be shorter 617 int length = text.length(); 618 StringBuilder sb = new StringBuilder(length - 1); 619 boolean upper = false; 620 for (int i = 0; i < length; i++) { 621 char c = text.charAt(i); 622 623 if (c == '-') { 624 upper = true; 625 } else { 626 if (upper) { 627 c = Character.toUpperCase(c); 628 upper = false; 629 } 630 sb.append(c); 631 } 632 } 633 return sb.toString(); 634 } 635 636 /** 637 * Converts the string from dash format into camel case, using the special for skip mode where we should keep text 638 * inside quotes or keys as-is. Where an input such as "camel.component.rabbitmq.args[queue.x-queue-type]" is 639 * transformed into camel.component.rabbitmq.args[queue.xQueueType] 640 * 641 * @param text the string 642 * @return the string camel cased 643 */ 644 private static String skippingDashToCamelCase(final String text) { 645 if (text == null) { 646 return null; 647 } 648 if (!isDashed(text)) { 649 return text; 650 } 651 652 // there is at least 1 dash so the capacity can be shorter 653 int length = text.length(); 654 StringBuilder sb = new StringBuilder(length - 1); 655 boolean upper = false; 656 int singleQuotes = 0; 657 int doubleQuotes = 0; 658 boolean skip = false; 659 for (int i = 0; i < length; i++) { 660 char c = text.charAt(i); 661 662 if (c == ']') { 663 skip = false; 664 } else if (c == '[') { 665 skip = true; 666 } else if (c == '\'') { 667 singleQuotes++; 668 } else if (c == '"') { 669 doubleQuotes++; 670 } 671 672 if (singleQuotes > 0) { 673 skip = singleQuotes % 2 == 1; 674 } 675 if (doubleQuotes > 0) { 676 skip = doubleQuotes % 2 == 1; 677 } 678 if (skip) { 679 sb.append(c); 680 continue; 681 } 682 683 if (c == '-') { 684 upper = true; 685 } else { 686 if (upper) { 687 c = Character.toUpperCase(c); 688 } 689 sb.append(c); 690 upper = false; 691 } 692 } 693 return sb.toString(); 694 } 695 696 /** 697 * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) 698 * 699 * @param text the string 700 * @param skipQuotedOrKeyed flag to skip converting within a quoted or keyed text 701 * @return the string camel cased 702 */ 703 public static String dashToCamelCase(final String text, boolean skipQuotedOrKeyed) { 704 if (!skipQuotedOrKeyed) { 705 return dashToCamelCase(text); 706 } else { 707 return skippingDashToCamelCase(text); 708 } 709 } 710 711 /** 712 * Returns the string after the given token 713 * 714 * @param text the text 715 * @param after the token 716 * @return the text after the token, or <tt>null</tt> if text does not contain the token 717 */ 718 public static String after(String text, String after) { 719 if (text == null) { 720 return null; 721 } 722 int pos = text.indexOf(after); 723 if (pos == -1) { 724 return null; 725 } 726 return text.substring(pos + after.length()); 727 } 728 729 /** 730 * Returns the string after the given token or the default value 731 * 732 * @param text the text 733 * @param after the token 734 * @param defaultValue the value to return if text does not contain the token 735 * @return the text after the token, or the supplied defaultValue if text does not contain the token 736 */ 737 public static String after(String text, String after, String defaultValue) { 738 String answer = after(text, after); 739 return answer != null ? answer : defaultValue; 740 } 741 742 /** 743 * Returns an object after the given token 744 * 745 * @param text the text 746 * @param after the token 747 * @param mapper a mapping function to convert the string after the token to type T 748 * @return an Optional describing the result of applying a mapping function to the text after the token. 749 */ 750 public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) { 751 String result = after(text, after); 752 if (result == null) { 753 return Optional.empty(); 754 } else { 755 return Optional.ofNullable(mapper.apply(result)); 756 } 757 } 758 759 /** 760 * Returns the string after the last occurrence of the given token 761 * 762 * @param text the text 763 * @param after the token 764 * @return the text after the token, or <tt>null</tt> if text does not contain the token 765 */ 766 public static String afterLast(String text, String after) { 767 if (text == null) { 768 return null; 769 } 770 int pos = text.lastIndexOf(after); 771 if (pos == -1) { 772 return null; 773 } 774 return text.substring(pos + after.length()); 775 } 776 777 /** 778 * Returns the string after the last occurrence of the given token, or the default value 779 * 780 * @param text the text 781 * @param after the token 782 * @param defaultValue the value to return if text does not contain the token 783 * @return the text after the token, or the supplied defaultValue if text does not contain the token 784 */ 785 public static String afterLast(String text, String after, String defaultValue) { 786 String answer = afterLast(text, after); 787 return answer != null ? answer : defaultValue; 788 } 789 790 /** 791 * Returns the string before the given token 792 * 793 * @param text the text 794 * @param before the token 795 * @return the text before the token, or <tt>null</tt> if text does not contain the token 796 */ 797 public static String before(String text, String before) { 798 if (text == null) { 799 return null; 800 } 801 int pos = text.indexOf(before); 802 return pos == -1 ? null : text.substring(0, pos); 803 } 804 805 /** 806 * Returns the string before the given token, or the default value 807 * 808 * @param text the text 809 * @param before the token 810 * @param defaultValue the value to return if text does not contain the token 811 * @return the text before the token, or the supplied defaultValue if text does not contain the token 812 */ 813 public static String before(String text, String before, String defaultValue) { 814 if (text == null) { 815 return defaultValue; 816 } 817 int pos = text.indexOf(before); 818 return pos == -1 ? defaultValue : text.substring(0, pos); 819 } 820 821 /** 822 * Returns the string before the given token or the default value 823 * 824 * @param text the text 825 * @param before the token 826 * @param defaultValue the value to return if the text does not contain the token 827 * @return the text before the token, or the supplied defaultValue if the text does not contain the 828 * token 829 */ 830 public static String before(String text, char before, String defaultValue) { 831 if (text == null) { 832 return defaultValue; 833 } 834 int pos = text.indexOf(before); 835 return pos == -1 ? defaultValue : text.substring(0, pos); 836 } 837 838 /** 839 * Returns an object before the given token 840 * 841 * @param text the text 842 * @param before the token 843 * @param mapper a mapping function to convert the string before the token to type T 844 * @return an Optional describing the result of applying a mapping function to the text before the token. 845 */ 846 public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) { 847 String result = before(text, before); 848 if (result == null) { 849 return Optional.empty(); 850 } else { 851 return Optional.ofNullable(mapper.apply(result)); 852 } 853 } 854 855 /** 856 * Returns the string before the last occurrence of the given token 857 * 858 * @param text the text 859 * @param before the token 860 * @return the text before the token, or <tt>null</tt> if the text does not contain the token 861 */ 862 public static String beforeLast(String text, String before) { 863 if (text == null) { 864 return null; 865 } 866 int pos = text.lastIndexOf(before); 867 return pos == -1 ? null : text.substring(0, pos); 868 } 869 870 /** 871 * Returns the string before the last occurrence of the given token, or the default value 872 * 873 * @param text the text 874 * @param before the token 875 * @param defaultValue the value to return if the text does not contain the token 876 * @return the text before the token, or the supplied defaultValue if the text does not contain the 877 * token 878 */ 879 public static String beforeLast(String text, String before, String defaultValue) { 880 String answer = beforeLast(text, before); 881 return answer != null ? answer : defaultValue; 882 } 883 884 /** 885 * Returns the string between the given tokens 886 * 887 * @param text the text 888 * @param after the before token 889 * @param before the after token 890 * @return the text between the tokens, or <tt>null</tt> if the text does not contain the tokens 891 */ 892 public static String between(final String text, String after, String before) { 893 String ret = after(text, after); 894 if (ret == null) { 895 return null; 896 } 897 return before(ret, before); 898 } 899 900 /** 901 * Returns an object between the given token 902 * 903 * @param text the text 904 * @param after the before token 905 * @param before the after token 906 * @param mapper a mapping function to convert the string between the token to type T 907 * @return an Optional describing the result of applying a mapping function to the text between the token. 908 */ 909 public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) { 910 String result = between(text, after, before); 911 if (result == null) { 912 return Optional.empty(); 913 } else { 914 return Optional.ofNullable(mapper.apply(result)); 915 } 916 } 917 918 /** 919 * Returns the substring between the given head and tail 920 * 921 * @param text the text 922 * @param head the head of the substring 923 * @param tail the tail of the substring 924 * @return the substring between the given head and tail 925 */ 926 public static String between(String text, int head, int tail) { 927 int len = text.length(); 928 if (head > 0) { 929 if (head <= len) { 930 text = text.substring(head); 931 } else { 932 text = ""; 933 } 934 len = text.length(); 935 } 936 if (tail > 0) { 937 if (tail <= len) { 938 text = text.substring(0, len - tail); 939 } else { 940 text = ""; 941 } 942 } 943 return text; 944 } 945 946 /** 947 * Returns the string between the most outer pair of tokens 948 * <p/> 949 * The number of token pairs must be even, e.g., there must be same number of before and after tokens, otherwise 950 * <tt>null</tt> is returned 951 * <p/> 952 * This implementation skips matching when the text is either single or double-quoted. For example: 953 * <tt>${body.matches("foo('bar')")</tt> Will not match the parenthesis from the quoted text. 954 * 955 * @param text the text 956 * @param after the before token 957 * @param before the after token 958 * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens 959 */ 960 public static String betweenOuterPair(String text, char before, char after) { 961 if (text == null) { 962 return null; 963 } 964 965 int pos = -1; 966 int pos2 = -1; 967 int count = 0; 968 int count2 = 0; 969 970 boolean singleQuoted = false; 971 boolean doubleQuoted = false; 972 for (int i = 0; i < text.length(); i++) { 973 char ch = text.charAt(i); 974 if (!doubleQuoted && ch == '\'') { 975 singleQuoted = !singleQuoted; 976 } else if (!singleQuoted && ch == '\"') { 977 doubleQuoted = !doubleQuoted; 978 } 979 if (singleQuoted || doubleQuoted) { 980 continue; 981 } 982 983 if (ch == before) { 984 count++; 985 } else if (ch == after) { 986 count2++; 987 } 988 989 if (ch == before && pos == -1) { 990 pos = i; 991 } else if (ch == after) { 992 pos2 = i; 993 } 994 } 995 996 if (pos == -1 || pos2 == -1) { 997 return null; 998 } 999 1000 // must be even paris 1001 if (count != count2) { 1002 return null; 1003 } 1004 1005 return text.substring(pos + 1, pos2); 1006 } 1007 1008 /** 1009 * Returns an object between the most outer pair of tokens 1010 * 1011 * @param text the text 1012 * @param after the before token 1013 * @param before the after token 1014 * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T 1015 * @return an Optional describing the result of applying a mapping function to the text between the most 1016 * outer pair of tokens. 1017 */ 1018 public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) { 1019 String result = betweenOuterPair(text, before, after); 1020 if (result == null) { 1021 return Optional.empty(); 1022 } else { 1023 return Optional.ofNullable(mapper.apply(result)); 1024 } 1025 } 1026 1027 /** 1028 * Returns true if the given name is a valid java identifier 1029 */ 1030 public static boolean isJavaIdentifier(String name) { 1031 if (name == null) { 1032 return false; 1033 } 1034 int size = name.length(); 1035 if (size < 1) { 1036 return false; 1037 } 1038 if (Character.isJavaIdentifierStart(name.charAt(0))) { 1039 for (int i = 1; i < size; i++) { 1040 if (!Character.isJavaIdentifierPart(name.charAt(i))) { 1041 return false; 1042 } 1043 } 1044 return true; 1045 } 1046 return false; 1047 } 1048 1049 /** 1050 * Cleans the string to a pure Java identifier so we can use it for loading class names. 1051 * <p/> 1052 * Especially from Spring DSL people can have \n \t or other characters that otherwise would result in 1053 * ClassNotFoundException 1054 * 1055 * @param name the class name 1056 * @return normalized class name that can be load by a class loader. 1057 */ 1058 public static String normalizeClassName(String name) { 1059 StringBuilder sb = new StringBuilder(name.length()); 1060 for (char ch : name.toCharArray()) { 1061 if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) { 1062 sb.append(ch); 1063 } 1064 } 1065 return sb.toString(); 1066 } 1067 1068 /** 1069 * Compares old and new text content and report back which lines are changed 1070 * 1071 * @param oldText the old text 1072 * @param newText the new text 1073 * @return a list of line numbers that are changed in the new text 1074 */ 1075 public static List<Integer> changedLines(String oldText, String newText) { 1076 if (oldText == null || oldText.equals(newText)) { 1077 return Collections.emptyList(); 1078 } 1079 1080 List<Integer> changed = new ArrayList<>(); 1081 1082 String[] oldLines = oldText.split("\n"); 1083 String[] newLines = newText.split("\n"); 1084 1085 for (int i = 0; i < newLines.length; i++) { 1086 String newLine = newLines[i]; 1087 String oldLine = i < oldLines.length ? oldLines[i] : null; 1088 if (oldLine == null) { 1089 changed.add(i); 1090 } else if (!newLine.equals(oldLine)) { 1091 changed.add(i); 1092 } 1093 } 1094 1095 return changed; 1096 } 1097 1098 /** 1099 * Removes the leading and trailing whitespace and if the resulting string is empty returns {@code null}. Examples: 1100 * <p> 1101 * Examples: <blockquote> 1102 * 1103 * <pre> 1104 * trimToNull("abc") -> "abc" 1105 * trimToNull(" abc") -> "abc" 1106 * trimToNull(" abc ") -> "abc" 1107 * trimToNull(" ") -> null 1108 * trimToNull("") -> null 1109 * </pre> 1110 * 1111 * </blockquote> 1112 */ 1113 public static String trimToNull(final String given) { 1114 if (given == null) { 1115 return null; 1116 } 1117 1118 final String trimmed = given.trim(); 1119 1120 if (trimmed.isEmpty()) { 1121 return null; 1122 } 1123 1124 return trimmed; 1125 } 1126 1127 /** 1128 * Checks if the src string contains what 1129 * 1130 * @param src is the source string to be checked 1131 * @param what is the string which will be looked up in the src argument 1132 * @return true/false 1133 */ 1134 public static boolean containsIgnoreCase(String src, String what) { 1135 if (src == null || what == null) { 1136 return false; 1137 } 1138 1139 final int length = what.length(); 1140 if (length == 0) { 1141 return true; // Empty string is contained 1142 } 1143 1144 final char firstLo = Character.toLowerCase(what.charAt(0)); 1145 final char firstUp = Character.toUpperCase(what.charAt(0)); 1146 1147 for (int i = src.length() - length; i >= 0; i--) { 1148 // Quick check before calling the more expensive regionMatches() method: 1149 final char ch = src.charAt(i); 1150 if (ch != firstLo && ch != firstUp) { 1151 continue; 1152 } 1153 1154 if (src.regionMatches(true, i, what, 0, length)) { 1155 return true; 1156 } 1157 } 1158 1159 return false; 1160 } 1161 1162 /** 1163 * Outputs the bytes in human-readable format in units of KB,MB,GB etc. 1164 * 1165 * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied. 1166 * @param bytes number of bytes 1167 * @return human readable output 1168 * @see java.lang.String#format(Locale, String, Object...) 1169 */ 1170 public static String humanReadableBytes(Locale locale, long bytes) { 1171 int unit = 1024; 1172 if (bytes < unit) { 1173 return bytes + " B"; 1174 } 1175 int exp = (int) (Math.log(bytes) / Math.log(unit)); 1176 String pre = String.valueOf("KMGTPE".charAt(exp - 1)); 1177 return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre); 1178 } 1179 1180 /** 1181 * Outputs the bytes in human-readable format in units of KB,MB,GB etc. 1182 * <p> 1183 * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 1184 * 1185 * @param bytes number of bytes 1186 * @return human readable output 1187 * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long) 1188 */ 1189 public static String humanReadableBytes(long bytes) { 1190 return humanReadableBytes(Locale.getDefault(), bytes); 1191 } 1192 1193 /** 1194 * Check for string pattern matching with a number of strategies in the following order: 1195 * <p> 1196 * - equals - null pattern always matches - * always matches - Ant style matching - Regexp 1197 * 1198 * @param pattern the pattern 1199 * @param target the string to test 1200 * @return true if target matches the pattern 1201 */ 1202 public static boolean matches(String pattern, String target) { 1203 if (Objects.equals(pattern, target)) { 1204 return true; 1205 } 1206 1207 if (Objects.isNull(pattern)) { 1208 return true; 1209 } 1210 1211 if (Objects.equals("*", pattern)) { 1212 return true; 1213 } 1214 1215 if (AntPathMatcher.INSTANCE.match(pattern, target)) { 1216 return true; 1217 } 1218 1219 Pattern p = Pattern.compile(pattern); 1220 Matcher m = p.matcher(target); 1221 1222 return m.matches(); 1223 } 1224 1225 /** 1226 * Converts the string from camel case into dot format (helloGreatWorld -> hello.great.world) 1227 * 1228 * @param text the string 1229 * @return the string dot cased 1230 */ 1231 public static String camelCaseToDot(String text) { 1232 if (text == null || text.isEmpty()) { 1233 return text; 1234 } 1235 text = camelCaseToDash(text); 1236 return text.replace('-', '.'); 1237 } 1238 1239 /** 1240 * Converts the string from camel case into dash format (helloGreatWorld -> hello-great-world) 1241 * 1242 * @param text the string 1243 * @return the string camel cased 1244 */ 1245 public static String camelCaseToDash(String text) { 1246 if (text == null || text.isEmpty()) { 1247 return text; 1248 } 1249 char prev = 0; 1250 1251 char[] arr = text.toCharArray(); 1252 StringBuilder answer = new StringBuilder(arr.length < 13 ? 16 : arr.length + 8); 1253 1254 for (int i = 0; i < arr.length; i++) { 1255 char ch = arr[i]; 1256 1257 if (ch == '-' || ch == '_') { 1258 answer.append("-"); 1259 } else { 1260 if (Character.isUpperCase(ch) && prev != 0) { 1261 char next; 1262 1263 if (i < arr.length - 1) { 1264 next = arr[i + 1]; 1265 } else { 1266 next = 0; 1267 } 1268 1269 if (!Character.isUpperCase(prev) || next != 0 && Character.isLowerCase(next)) { 1270 applyDashPrefix(prev, answer, ch); 1271 } else { 1272 answer.append(Character.toLowerCase(ch)); 1273 } 1274 } else { 1275 answer.append(Character.toLowerCase(ch)); 1276 } 1277 } 1278 prev = ch; 1279 } 1280 1281 return answer.toString(); 1282 } 1283 1284 private static void applyDashPrefix(char prev, StringBuilder answer, char ch) { 1285 if (prev != '-' && prev != '_') { 1286 answer.append("-"); 1287 } 1288 answer.append(Character.toLowerCase(ch)); 1289 } 1290 1291 /** 1292 * Does the string start with the given prefix (ignoring the case). 1293 * 1294 * @param text the string 1295 * @param prefix the prefix 1296 */ 1297 public static boolean startsWithIgnoreCase(String text, String prefix) { 1298 if (text != null && prefix != null) { 1299 return prefix.length() <= text.length() && text.regionMatches(true, 0, prefix, 0, prefix.length()); 1300 } else { 1301 return text == null && prefix == null; 1302 } 1303 } 1304 1305 /** 1306 * Converts the value to an enum constant value that is in the form of upper-cased with underscore. 1307 */ 1308 public static String asEnumConstantValue(final String value) { 1309 if (value == null || value.isEmpty()) { 1310 return value; 1311 } 1312 String ret = StringHelper.camelCaseToDash(value); 1313 // replace double dashes 1314 ret = ret.replaceAll("-+", "-"); 1315 // replace dash with underscore and upper case 1316 ret = ret.replace('-', '_').toUpperCase(Locale.ENGLISH); 1317 return ret; 1318 } 1319 1320 /** 1321 * Split the text on words, eg hello/world => becomes array with hello in index 0, and world in index 1. 1322 */ 1323 public static String[] splitWords(String text) { 1324 return text.split("[\\W]+"); 1325 } 1326 1327 /** 1328 * Creates a stream from the given input sequence around matches of the regex 1329 * 1330 * @param text the input 1331 * @param regex the expression used to split the input 1332 * @return the stream of strings computed by splitting the input with the given regex 1333 */ 1334 public static Stream<String> splitAsStream(CharSequence text, String regex) { 1335 if (text == null || regex == null) { 1336 return Stream.empty(); 1337 } 1338 1339 return Pattern.compile(regex).splitAsStream(text); 1340 } 1341 1342 /** 1343 * Returns the occurrence of a search string in to a string. 1344 * 1345 * @param text the text 1346 * @param search the string to search 1347 * @return an integer reporting the occurrences of the searched string in to the text 1348 */ 1349 public static int countOccurrence(String text, String search) { 1350 int lastIndex = 0; 1351 int count = 0; 1352 while (lastIndex != -1) { 1353 lastIndex = text.indexOf(search, lastIndex); 1354 if (lastIndex != -1) { 1355 count++; 1356 lastIndex += search.length(); 1357 } 1358 } 1359 return count; 1360 } 1361 1362 /** 1363 * Replaces a string in to a text starting from his second occurrence. 1364 * 1365 * @param text the text 1366 * @param search the string to search 1367 * @param replacement the replacement for the string 1368 * @return the string with the replacement 1369 */ 1370 public static String replaceFromSecondOccurrence(String text, String search, String replacement) { 1371 int index = text.indexOf(search); 1372 boolean replace = false; 1373 1374 while (index != -1) { 1375 String tempString = text.substring(index); 1376 if (replace) { 1377 tempString = tempString.replaceFirst(search, replacement); 1378 text = text.substring(0, index) + tempString; 1379 replace = false; 1380 } else { 1381 replace = true; 1382 } 1383 index = text.indexOf(search, index + 1); 1384 } 1385 return text; 1386 } 1387 1388 /** 1389 * Pad the string with leading spaces 1390 * 1391 * @param level level (2 blanks per level) 1392 */ 1393 public static String padString(int level) { 1394 return padString(level, 2); 1395 } 1396 1397 /** 1398 * Pad the string with leading spaces 1399 * 1400 * @param level level 1401 * @param blanks number of blanks per level 1402 */ 1403 public static String padString(int level, int blanks) { 1404 if (level == 0) { 1405 return ""; 1406 } else { 1407 return " ".repeat(level * blanks); 1408 } 1409 } 1410 1411 /** 1412 * Fills the string with repeating chars 1413 * 1414 * @param ch the char 1415 * @param count number of chars 1416 */ 1417 public static String fillChars(char ch, int count) { 1418 if (count <= 0) { 1419 return ""; 1420 } else { 1421 return Character.toString(ch).repeat(count); 1422 } 1423 } 1424 1425 /** 1426 * Is the given string only numbers 1427 */ 1428 public static boolean isDigit(String s) { 1429 for (char ch : s.toCharArray()) { 1430 if (!Character.isDigit(ch)) { 1431 return false; 1432 } 1433 } 1434 return true; 1435 } 1436 1437 /** 1438 * Converts the bytes to hex decimal 1439 */ 1440 public static String bytesToHex(byte[] hash) { 1441 StringBuilder sb = new StringBuilder(2 * hash.length); 1442 for (byte b : hash) { 1443 String hex = Integer.toHexString(0xff & b); 1444 if (hex.length() == 1) { 1445 sb.append('0'); 1446 } 1447 sb.append(hex); 1448 } 1449 return sb.toString(); 1450 } 1451 1452 /** 1453 * Normalizes the whitespaces by removing any excess spaces so there are only at most a single whitespace. 1454 */ 1455 public static String normalizeWhitespace(String text) { 1456 if (text == null) { 1457 return null; 1458 } 1459 if (text.isBlank()) { 1460 return ""; 1461 } 1462 1463 // must have at least double spaces 1464 if (!text.contains(" ")) { 1465 return text.trim(); 1466 } 1467 StringBuilder sb = new StringBuilder(text.length()); 1468 final char[] chars = text.toCharArray(); 1469 for (int i = 1; i < chars.length; i++) { 1470 char prev = chars[i - 1]; 1471 char ch = chars[i]; 1472 if (Character.isWhitespace(ch) && Character.isWhitespace(prev)) { 1473 continue; 1474 } 1475 if (i == 1) { 1476 sb.append(prev); 1477 } 1478 sb.append(ch); 1479 } 1480 return sb.toString().trim(); 1481 } 1482 1483}