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