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 first character) 516 * 517 * @param text the string 518 * @return the string capitalized (upper case 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) 526 * 527 * @param text the string 528 * @param dashToCamelCase whether to also convert dash format into camel case (hello-great-world -> 529 * helloGreatWorld) 530 * @return the string capitalized (upper case first character) 531 */ 532 public static String capitalize(final String text, boolean dashToCamelCase) { 533 String ret = text; 534 if (dashToCamelCase) { 535 ret = dashToCamelCase(text); 536 } 537 return doCapitalize(ret); 538 } 539 540 private static String doCapitalize(String ret) { 541 if (ret == null) { 542 return null; 543 } 544 545 final char[] chars = ret.toCharArray(); 546 547 // We are OK with the limitations of Character.toUpperCase. The symbols and ideographs 548 // for which it does not return the capitalized value should not be used here (this is 549 // mostly used to capitalize setters/getters) 550 chars[0] = Character.toUpperCase(chars[0]); 551 return new String(chars); 552 } 553 554 /** 555 * De-capitalize the string (lower case first character) 556 * 557 * @param text the string 558 * @return the string decapitalized (lower case first character) 559 */ 560 public static String decapitalize(final String text) { 561 if (text == null) { 562 return null; 563 } 564 565 final char[] chars = text.toCharArray(); 566 567 // We are OK with the limitations of Character.toLowerCase. The symbols and ideographs 568 // for which it does not return the lower case value should not be used here (this isap 569 // mostly used to convert part of setters/getters to properties) 570 chars[0] = Character.toLowerCase(chars[0]); 571 return new String(chars); 572 } 573 574 /** 575 * Whether the string contains dashes or not 576 * 577 * @param text the string to check 578 * @return true if it contains dashes or false otherwise 579 */ 580 public static boolean isDashed(String text) { 581 return !text.isEmpty() && text.indexOf('-') != -1; 582 } 583 584 /** 585 * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) 586 * 587 * @param text the string 588 * @return the string camel cased 589 */ 590 public static String dashToCamelCase(final String text) { 591 if (text == null) { 592 return null; 593 } 594 if (!isDashed(text)) { 595 return text; 596 } 597 598 // there is at least 1 dash so the capacity can be shorter 599 int length = text.length(); 600 StringBuilder sb = new StringBuilder(length - 1); 601 boolean upper = false; 602 for (int i = 0; i < length; i++) { 603 char c = text.charAt(i); 604 605 if (c == '-') { 606 upper = true; 607 } else { 608 if (upper) { 609 c = Character.toUpperCase(c); 610 upper = false; 611 } 612 sb.append(c); 613 } 614 } 615 return sb.toString(); 616 } 617 618 /** 619 * Converts the string from dash format into camel case, using the special for skip mode where we should keep text 620 * inside quotes or keys as-is. Where an input such as "camel.component.rabbitmq.args[queue.x-queue-type]" is 621 * transformed into camel.component.rabbitmq.args[queue.xQueueType] 622 * 623 * @param text the string 624 * @return the string camel cased 625 */ 626 private static String skippingDashToCamelCase(final String text) { 627 if (text == null) { 628 return null; 629 } 630 if (!isDashed(text)) { 631 return text; 632 } 633 634 // there is at least 1 dash so the capacity can be shorter 635 int length = text.length(); 636 StringBuilder sb = new StringBuilder(length - 1); 637 boolean upper = false; 638 int singleQuotes = 0; 639 int doubleQuotes = 0; 640 boolean skip = false; 641 for (int i = 0; i < length; i++) { 642 char c = text.charAt(i); 643 644 if (c == ']') { 645 skip = false; 646 } else if (c == '[') { 647 skip = true; 648 } else if (c == '\'') { 649 singleQuotes++; 650 } else if (c == '"') { 651 doubleQuotes++; 652 } 653 654 if (singleQuotes > 0) { 655 skip = singleQuotes % 2 == 1; 656 } 657 if (doubleQuotes > 0) { 658 skip = doubleQuotes % 2 == 1; 659 } 660 if (skip) { 661 sb.append(c); 662 continue; 663 } 664 665 if (c == '-') { 666 upper = true; 667 } else { 668 if (upper) { 669 c = Character.toUpperCase(c); 670 } 671 sb.append(c); 672 upper = false; 673 } 674 } 675 return sb.toString(); 676 } 677 678 /** 679 * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) 680 * 681 * @param text the string 682 * @param skipQuotedOrKeyed flag to skip converting within a quoted or keyed text 683 * @return the string camel cased 684 */ 685 public static String dashToCamelCase(final String text, boolean skipQuotedOrKeyed) { 686 if (!skipQuotedOrKeyed) { 687 return dashToCamelCase(text); 688 } else { 689 return skippingDashToCamelCase(text); 690 } 691 } 692 693 /** 694 * Returns the string after the given token 695 * 696 * @param text the text 697 * @param after the token 698 * @return the text after the token, or <tt>null</tt> if text does not contain the token 699 */ 700 public static String after(String text, String after) { 701 if (text == null) { 702 return null; 703 } 704 int pos = text.indexOf(after); 705 if (pos == -1) { 706 return null; 707 } 708 return text.substring(pos + after.length()); 709 } 710 711 /** 712 * Returns the string after the given token or the default value 713 * 714 * @param text the text 715 * @param after the token 716 * @param defaultValue the value to return if text does not contain the token 717 * @return the text after the token, or the supplied defaultValue if text does not contain the token 718 */ 719 public static String after(String text, String after, String defaultValue) { 720 String answer = after(text, after); 721 return answer != null ? answer : defaultValue; 722 } 723 724 /** 725 * Returns an object after the given token 726 * 727 * @param text the text 728 * @param after the token 729 * @param mapper a mapping function to convert the string after the token to type T 730 * @return an Optional describing the result of applying a mapping function to the text after the token. 731 */ 732 public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) { 733 String result = after(text, after); 734 if (result == null) { 735 return Optional.empty(); 736 } else { 737 return Optional.ofNullable(mapper.apply(result)); 738 } 739 } 740 741 /** 742 * Returns the string after the last occurrence of the given token 743 * 744 * @param text the text 745 * @param after the token 746 * @return the text after the token, or <tt>null</tt> if text does not contain the token 747 */ 748 public static String afterLast(String text, String after) { 749 if (text == null) { 750 return null; 751 } 752 int pos = text.lastIndexOf(after); 753 if (pos == -1) { 754 return null; 755 } 756 return text.substring(pos + after.length()); 757 } 758 759 /** 760 * Returns the string after the last occurrence of the given token, or the default value 761 * 762 * @param text the text 763 * @param after the token 764 * @param defaultValue the value to return if text does not contain the token 765 * @return the text after the token, or the supplied defaultValue if text does not contain the token 766 */ 767 public static String afterLast(String text, String after, String defaultValue) { 768 String answer = afterLast(text, after); 769 return answer != null ? answer : defaultValue; 770 } 771 772 /** 773 * Returns the string before the given token 774 * 775 * @param text the text 776 * @param before the token 777 * @return the text before the token, or <tt>null</tt> if text does not contain the token 778 */ 779 public static String before(String text, String before) { 780 if (text == null) { 781 return null; 782 } 783 int pos = text.indexOf(before); 784 return pos == -1 ? null : text.substring(0, pos); 785 } 786 787 /** 788 * Returns the string before the given token, or the default value 789 * 790 * @param text the text 791 * @param before the token 792 * @param defaultValue the value to return if text does not contain the token 793 * @return the text before the token, or the supplied defaultValue if text does not contain the token 794 */ 795 public static String before(String text, String before, String defaultValue) { 796 if (text == null) { 797 return defaultValue; 798 } 799 int pos = text.indexOf(before); 800 return pos == -1 ? defaultValue : text.substring(0, pos); 801 } 802 803 /** 804 * Returns the string before the given token or the default value 805 * 806 * @param text the text 807 * @param before the token 808 * @param defaultValue the value to return if the text does not contain the token 809 * @return the text before the token, or the supplied defaultValue if the text does not contain the 810 * token 811 */ 812 public static String before(String text, char before, String defaultValue) { 813 if (text == null) { 814 return defaultValue; 815 } 816 int pos = text.indexOf(before); 817 return pos == -1 ? defaultValue : text.substring(0, pos); 818 } 819 820 /** 821 * Returns an object before the given token 822 * 823 * @param text the text 824 * @param before the token 825 * @param mapper a mapping function to convert the string before the token to type T 826 * @return an Optional describing the result of applying a mapping function to the text before the token. 827 */ 828 public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) { 829 String result = before(text, before); 830 if (result == null) { 831 return Optional.empty(); 832 } else { 833 return Optional.ofNullable(mapper.apply(result)); 834 } 835 } 836 837 /** 838 * Returns the string before the last occurrence of the given token 839 * 840 * @param text the text 841 * @param before the token 842 * @return the text before the token, or <tt>null</tt> if the text does not contain the token 843 */ 844 public static String beforeLast(String text, String before) { 845 if (text == null) { 846 return null; 847 } 848 int pos = text.lastIndexOf(before); 849 return pos == -1 ? null : text.substring(0, pos); 850 } 851 852 /** 853 * Returns the string before the last occurrence of the given token, or the default value 854 * 855 * @param text the text 856 * @param before the token 857 * @param defaultValue the value to return if the text does not contain the token 858 * @return the text before the token, or the supplied defaultValue if the text does not contain the 859 * token 860 */ 861 public static String beforeLast(String text, String before, String defaultValue) { 862 String answer = beforeLast(text, before); 863 return answer != null ? answer : defaultValue; 864 } 865 866 /** 867 * Returns the string between the given tokens 868 * 869 * @param text the text 870 * @param after the before token 871 * @param before the after token 872 * @return the text between the tokens, or <tt>null</tt> if the text does not contain the tokens 873 */ 874 public static String between(final String text, String after, String before) { 875 String ret = after(text, after); 876 if (ret == null) { 877 return null; 878 } 879 return before(ret, before); 880 } 881 882 /** 883 * Returns an object between the given token 884 * 885 * @param text the text 886 * @param after the before token 887 * @param before the after token 888 * @param mapper a mapping function to convert the string between the token to type T 889 * @return an Optional describing the result of applying a mapping function to the text between the token. 890 */ 891 public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) { 892 String result = between(text, after, before); 893 if (result == null) { 894 return Optional.empty(); 895 } else { 896 return Optional.ofNullable(mapper.apply(result)); 897 } 898 } 899 900 /** 901 * Returns the substring between the given head and tail 902 * 903 * @param text the text 904 * @param head the head of the substring 905 * @param tail the tail of the substring 906 * @return the substring between the given head and tail 907 */ 908 public static String between(String text, int head, int tail) { 909 int len = text.length(); 910 if (head > 0) { 911 if (head <= len) { 912 text = text.substring(head); 913 } else { 914 text = ""; 915 } 916 len = text.length(); 917 } 918 if (tail > 0) { 919 if (tail <= len) { 920 text = text.substring(0, len - tail); 921 } else { 922 text = ""; 923 } 924 } 925 return text; 926 } 927 928 /** 929 * Returns the string between the most outer pair of tokens 930 * <p/> 931 * The number of token pairs must be even, e.g., there must be same number of before and after tokens, otherwise 932 * <tt>null</tt> is returned 933 * <p/> 934 * This implementation skips matching when the text is either single or double-quoted. For example: 935 * <tt>${body.matches("foo('bar')")</tt> Will not match the parenthesis from the quoted text. 936 * 937 * @param text the text 938 * @param after the before token 939 * @param before the after token 940 * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens 941 */ 942 public static String betweenOuterPair(String text, char before, char after) { 943 if (text == null) { 944 return null; 945 } 946 947 int pos = -1; 948 int pos2 = -1; 949 int count = 0; 950 int count2 = 0; 951 952 boolean singleQuoted = false; 953 boolean doubleQuoted = false; 954 for (int i = 0; i < text.length(); i++) { 955 char ch = text.charAt(i); 956 if (!doubleQuoted && ch == '\'') { 957 singleQuoted = !singleQuoted; 958 } else if (!singleQuoted && ch == '\"') { 959 doubleQuoted = !doubleQuoted; 960 } 961 if (singleQuoted || doubleQuoted) { 962 continue; 963 } 964 965 if (ch == before) { 966 count++; 967 } else if (ch == after) { 968 count2++; 969 } 970 971 if (ch == before && pos == -1) { 972 pos = i; 973 } else if (ch == after) { 974 pos2 = i; 975 } 976 } 977 978 if (pos == -1 || pos2 == -1) { 979 return null; 980 } 981 982 // must be even paris 983 if (count != count2) { 984 return null; 985 } 986 987 return text.substring(pos + 1, pos2); 988 } 989 990 /** 991 * Returns an object between the most outer pair of tokens 992 * 993 * @param text the text 994 * @param after the before token 995 * @param before the after token 996 * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T 997 * @return an Optional describing the result of applying a mapping function to the text between the most 998 * outer pair of tokens. 999 */ 1000 public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) { 1001 String result = betweenOuterPair(text, before, after); 1002 if (result == null) { 1003 return Optional.empty(); 1004 } else { 1005 return Optional.ofNullable(mapper.apply(result)); 1006 } 1007 } 1008 1009 /** 1010 * Returns true if the given name is a valid java identifier 1011 */ 1012 public static boolean isJavaIdentifier(String name) { 1013 if (name == null) { 1014 return false; 1015 } 1016 int size = name.length(); 1017 if (size < 1) { 1018 return false; 1019 } 1020 if (Character.isJavaIdentifierStart(name.charAt(0))) { 1021 for (int i = 1; i < size; i++) { 1022 if (!Character.isJavaIdentifierPart(name.charAt(i))) { 1023 return false; 1024 } 1025 } 1026 return true; 1027 } 1028 return false; 1029 } 1030 1031 /** 1032 * Cleans the string to a pure Java identifier so we can use it for loading class names. 1033 * <p/> 1034 * Especially from Spring DSL people can have \n \t or other characters that otherwise would result in 1035 * ClassNotFoundException 1036 * 1037 * @param name the class name 1038 * @return normalized class name that can be load by a class loader. 1039 */ 1040 public static String normalizeClassName(String name) { 1041 StringBuilder sb = new StringBuilder(name.length()); 1042 for (char ch : name.toCharArray()) { 1043 if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) { 1044 sb.append(ch); 1045 } 1046 } 1047 return sb.toString(); 1048 } 1049 1050 /** 1051 * Compares old and new text content and report back which lines are changed 1052 * 1053 * @param oldText the old text 1054 * @param newText the new text 1055 * @return a list of line numbers that are changed in the new text 1056 */ 1057 public static List<Integer> changedLines(String oldText, String newText) { 1058 if (oldText == null || oldText.equals(newText)) { 1059 return Collections.emptyList(); 1060 } 1061 1062 List<Integer> changed = new ArrayList<>(); 1063 1064 String[] oldLines = oldText.split("\n"); 1065 String[] newLines = newText.split("\n"); 1066 1067 for (int i = 0; i < newLines.length; i++) { 1068 String newLine = newLines[i]; 1069 String oldLine = i < oldLines.length ? oldLines[i] : null; 1070 if (oldLine == null) { 1071 changed.add(i); 1072 } else if (!newLine.equals(oldLine)) { 1073 changed.add(i); 1074 } 1075 } 1076 1077 return changed; 1078 } 1079 1080 /** 1081 * Removes the leading and trailing whitespace and if the resulting string is empty returns {@code null}. Examples: 1082 * <p> 1083 * Examples: <blockquote> 1084 * 1085 * <pre> 1086 * trimToNull("abc") -> "abc" 1087 * trimToNull(" abc") -> "abc" 1088 * trimToNull(" abc ") -> "abc" 1089 * trimToNull(" ") -> null 1090 * trimToNull("") -> null 1091 * </pre> 1092 * 1093 * </blockquote> 1094 */ 1095 public static String trimToNull(final String given) { 1096 if (given == null) { 1097 return null; 1098 } 1099 1100 final String trimmed = given.trim(); 1101 1102 if (trimmed.isEmpty()) { 1103 return null; 1104 } 1105 1106 return trimmed; 1107 } 1108 1109 /** 1110 * Checks if the src string contains what 1111 * 1112 * @param src is the source string to be checked 1113 * @param what is the string which will be looked up in the src argument 1114 * @return true/false 1115 */ 1116 public static boolean containsIgnoreCase(String src, String what) { 1117 if (src == null || what == null) { 1118 return false; 1119 } 1120 1121 final int length = what.length(); 1122 if (length == 0) { 1123 return true; // Empty string is contained 1124 } 1125 1126 final char firstLo = Character.toLowerCase(what.charAt(0)); 1127 final char firstUp = Character.toUpperCase(what.charAt(0)); 1128 1129 for (int i = src.length() - length; i >= 0; i--) { 1130 // Quick check before calling the more expensive regionMatches() method: 1131 final char ch = src.charAt(i); 1132 if (ch != firstLo && ch != firstUp) { 1133 continue; 1134 } 1135 1136 if (src.regionMatches(true, i, what, 0, length)) { 1137 return true; 1138 } 1139 } 1140 1141 return false; 1142 } 1143 1144 /** 1145 * Outputs the bytes in human-readable format in units of KB,MB,GB etc. 1146 * 1147 * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied. 1148 * @param bytes number of bytes 1149 * @return human readable output 1150 * @see java.lang.String#format(Locale, String, Object...) 1151 */ 1152 public static String humanReadableBytes(Locale locale, long bytes) { 1153 int unit = 1024; 1154 if (bytes < unit) { 1155 return bytes + " B"; 1156 } 1157 int exp = (int) (Math.log(bytes) / Math.log(unit)); 1158 String pre = String.valueOf("KMGTPE".charAt(exp - 1)); 1159 return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre); 1160 } 1161 1162 /** 1163 * Outputs the bytes in human-readable format in units of KB,MB,GB etc. 1164 * 1165 * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 1166 * 1167 * @param bytes number of bytes 1168 * @return human readable output 1169 * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long) 1170 */ 1171 public static String humanReadableBytes(long bytes) { 1172 return humanReadableBytes(Locale.getDefault(), bytes); 1173 } 1174 1175 /** 1176 * Check for string pattern matching with a number of strategies in the following order: 1177 * 1178 * - equals - null pattern always matches - * always matches - Ant style matching - Regexp 1179 * 1180 * @param pattern the pattern 1181 * @param target the string to test 1182 * @return true if target matches the pattern 1183 */ 1184 public static boolean matches(String pattern, String target) { 1185 if (Objects.equals(pattern, target)) { 1186 return true; 1187 } 1188 1189 if (Objects.isNull(pattern)) { 1190 return true; 1191 } 1192 1193 if (Objects.equals("*", pattern)) { 1194 return true; 1195 } 1196 1197 if (AntPathMatcher.INSTANCE.match(pattern, target)) { 1198 return true; 1199 } 1200 1201 Pattern p = Pattern.compile(pattern); 1202 Matcher m = p.matcher(target); 1203 1204 return m.matches(); 1205 } 1206 1207 /** 1208 * Converts the string from camel case into dot format (helloGreatWorld -> hello.great.world) 1209 * 1210 * @param text the string 1211 * @return the string dot cased 1212 */ 1213 public static String camelCaseToDot(String text) { 1214 if (text == null || text.isEmpty()) { 1215 return text; 1216 } 1217 text = camelCaseToDash(text); 1218 return text.replace('-', '.'); 1219 } 1220 1221 /** 1222 * Converts the string from camel case into dash format (helloGreatWorld -> hello-great-world) 1223 * 1224 * @param text the string 1225 * @return the string camel cased 1226 */ 1227 public static String camelCaseToDash(String text) { 1228 if (text == null || text.isEmpty()) { 1229 return text; 1230 } 1231 char prev = 0; 1232 1233 char[] arr = text.toCharArray(); 1234 StringBuilder answer = new StringBuilder(arr.length < 13 ? 16 : arr.length + 8); 1235 1236 for (int i = 0; i < arr.length; i++) { 1237 char ch = arr[i]; 1238 1239 if (ch == '-' || ch == '_') { 1240 answer.append("-"); 1241 } else { 1242 if (Character.isUpperCase(ch) && prev != 0) { 1243 char next; 1244 1245 if (i < arr.length - 1) { 1246 next = arr[i + 1]; 1247 } else { 1248 next = 0; 1249 } 1250 1251 if (!Character.isUpperCase(prev) || next != 0 && Character.isLowerCase(next)) { 1252 applyDashPrefix(prev, answer, ch); 1253 } else { 1254 answer.append(Character.toLowerCase(ch)); 1255 } 1256 } else { 1257 answer.append(Character.toLowerCase(ch)); 1258 } 1259 } 1260 prev = ch; 1261 } 1262 1263 return answer.toString(); 1264 } 1265 1266 private static void applyDashPrefix(char prev, StringBuilder answer, char ch) { 1267 if (prev != '-' && prev != '_') { 1268 answer.append("-"); 1269 } 1270 answer.append(Character.toLowerCase(ch)); 1271 } 1272 1273 /** 1274 * Does the string start with the given prefix (ignoring the case). 1275 * 1276 * @param text the string 1277 * @param prefix the prefix 1278 */ 1279 public static boolean startsWithIgnoreCase(String text, String prefix) { 1280 if (text != null && prefix != null) { 1281 return prefix.length() <= text.length() && text.regionMatches(true, 0, prefix, 0, prefix.length()); 1282 } else { 1283 return text == null && prefix == null; 1284 } 1285 } 1286 1287 /** 1288 * Converts the value to an enum constant value that is in the form of upper-cased with underscore. 1289 */ 1290 public static String asEnumConstantValue(final String value) { 1291 if (value == null || value.isEmpty()) { 1292 return value; 1293 } 1294 String ret = StringHelper.camelCaseToDash(value); 1295 // replace double dashes 1296 ret = ret.replaceAll("-+", "-"); 1297 // replace dash with underscore and upper case 1298 ret = ret.replace('-', '_').toUpperCase(Locale.ENGLISH); 1299 return ret; 1300 } 1301 1302 /** 1303 * Split the text on words, eg hello/world => becomes array with hello in index 0, and world in index 1. 1304 */ 1305 public static String[] splitWords(String text) { 1306 return text.split("[\\W]+"); 1307 } 1308 1309 /** 1310 * Creates a stream from the given input sequence around matches of the regex 1311 * 1312 * @param text the input 1313 * @param regex the expression used to split the input 1314 * @return the stream of strings computed by splitting the input with the given regex 1315 */ 1316 public static Stream<String> splitAsStream(CharSequence text, String regex) { 1317 if (text == null || regex == null) { 1318 return Stream.empty(); 1319 } 1320 1321 return Pattern.compile(regex).splitAsStream(text); 1322 } 1323 1324 /** 1325 * Returns the occurrence of a search string in to a string. 1326 * 1327 * @param text the text 1328 * @param search the string to search 1329 * @return an integer reporting the occurrences of the searched string in to the text 1330 */ 1331 public static int countOccurrence(String text, String search) { 1332 int lastIndex = 0; 1333 int count = 0; 1334 while (lastIndex != -1) { 1335 lastIndex = text.indexOf(search, lastIndex); 1336 if (lastIndex != -1) { 1337 count++; 1338 lastIndex += search.length(); 1339 } 1340 } 1341 return count; 1342 } 1343 1344 /** 1345 * Replaces a string in to a text starting from his second occurrence. 1346 * 1347 * @param text the text 1348 * @param search the string to search 1349 * @param replacement the replacement for the string 1350 * @return the string with the replacement 1351 */ 1352 public static String replaceFromSecondOccurrence(String text, String search, String replacement) { 1353 int index = text.indexOf(search); 1354 boolean replace = false; 1355 1356 while (index != -1) { 1357 String tempString = text.substring(index); 1358 if (replace) { 1359 tempString = tempString.replaceFirst(search, replacement); 1360 text = text.substring(0, index) + tempString; 1361 replace = false; 1362 } else { 1363 replace = true; 1364 } 1365 index = text.indexOf(search, index + 1); 1366 } 1367 return text; 1368 } 1369 1370 /** 1371 * Pad the string with leading spaces 1372 * 1373 * @param level level (2 blanks per level) 1374 */ 1375 public static String padString(int level) { 1376 return padString(level, 2); 1377 } 1378 1379 /** 1380 * Pad the string with leading spaces 1381 * 1382 * @param level level 1383 * @param blanks number of blanks per level 1384 */ 1385 public static String padString(int level, int blanks) { 1386 if (level == 0) { 1387 return ""; 1388 } else { 1389 return " ".repeat(level * blanks); 1390 } 1391 } 1392 1393 /** 1394 * Fills the string with repeating chars 1395 * 1396 * @param ch the char 1397 * @param count number of chars 1398 */ 1399 public static String fillChars(char ch, int count) { 1400 if (count <= 0) { 1401 return ""; 1402 } else { 1403 return Character.toString(ch).repeat(count); 1404 } 1405 } 1406 1407 public static boolean isDigit(String s) { 1408 for (char ch : s.toCharArray()) { 1409 if (!Character.isDigit(ch)) { 1410 return false; 1411 } 1412 } 1413 return true; 1414 } 1415 1416 public static String bytesToHex(byte[] hash) { 1417 StringBuilder sb = new StringBuilder(2 * hash.length); 1418 for (byte b : hash) { 1419 String hex = Integer.toHexString(0xff & b); 1420 if (hex.length() == 1) { 1421 sb.append('0'); 1422 } 1423 sb.append(hex); 1424 } 1425 return sb.toString(); 1426 } 1427 1428}