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