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