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.List; 022import java.util.Locale; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.function.Function; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import static org.apache.camel.util.StringQuoteHelper.doubleQuote; 030 031/** 032 * Helper methods for working with Strings. 033 */ 034public final class StringHelper { 035 036 /** 037 * Constructor of utility class should be private. 038 */ 039 private StringHelper() { 040 } 041 042 /** 043 * Ensures that <code>s</code> is friendly for a URL or file system. 044 * 045 * @param s String to be sanitized. 046 * @return sanitized version of <code>s</code>. 047 * @throws NullPointerException if <code>s</code> is <code>null</code>. 048 */ 049 public static String sanitize(String s) { 050 return s 051 .replace(':', '-') 052 .replace('_', '-') 053 .replace('.', '-') 054 .replace('/', '-') 055 .replace('\\', '-'); 056 } 057 058 /** 059 * Counts the number of times the given char is in the string 060 * 061 * @param s the string 062 * @param ch the char 063 * @return number of times char is located in the string 064 */ 065 public static int countChar(String s, char ch) { 066 if (ObjectHelper.isEmpty(s)) { 067 return 0; 068 } 069 070 int matches = 0; 071 for (int i = 0; i < s.length(); i++) { 072 char c = s.charAt(i); 073 if (ch == c) { 074 matches++; 075 } 076 } 077 078 return matches; 079 } 080 081 /** 082 * Limits the length of a string 083 * 084 * @param s the string 085 * @param maxLength the maximum length of the returned string 086 * @return s if the length of s is less than maxLength or the first maxLength characters of s 087 * @deprecated use {@link #limitLength(String, int)} 088 */ 089 @Deprecated 090 public static String limitLenght(String s, int maxLength) { 091 return limitLength(s, maxLength); 092 } 093 094 /** 095 * Limits the length of a string 096 * 097 * @param s the string 098 * @param maxLength the maximum length of the returned string 099 * @return s if the length of s is less than maxLength or the first maxLength characters of s 100 */ 101 public static String limitLength(String s, int maxLength) { 102 if (ObjectHelper.isEmpty(s)) { 103 return s; 104 } 105 return s.length() <= maxLength ? s : s.substring(0, maxLength); 106 } 107 108 /** 109 * Removes all quotes (single and double) from the string 110 * 111 * @param s the string 112 * @return the string without quotes (single and double) 113 */ 114 public static String removeQuotes(String s) { 115 if (ObjectHelper.isEmpty(s)) { 116 return s; 117 } 118 119 s = replaceAll(s, "'", ""); 120 s = replaceAll(s, "\"", ""); 121 return s; 122 } 123 124 /** 125 * Removes all leading and ending quotes (single and double) from the string 126 * 127 * @param s the string 128 * @return the string without leading and ending quotes (single and double) 129 */ 130 public static String removeLeadingAndEndingQuotes(String s) { 131 if (ObjectHelper.isEmpty(s)) { 132 return s; 133 } 134 135 String copy = s.trim(); 136 if (copy.startsWith("'") && copy.endsWith("'")) { 137 return copy.substring(1, copy.length() - 1); 138 } 139 if (copy.startsWith("\"") && copy.endsWith("\"")) { 140 return copy.substring(1, copy.length() - 1); 141 } 142 143 // no quotes, so return as-is 144 return s; 145 } 146 147 /** 148 * Whether the string starts and ends with either single or double quotes. 149 * 150 * @param s the string 151 * @return <tt>true</tt> if the string starts and ends with either single or double quotes. 152 */ 153 public static boolean isQuoted(String s) { 154 if (ObjectHelper.isEmpty(s)) { 155 return false; 156 } 157 158 if (s.startsWith("'") && s.endsWith("'")) { 159 return true; 160 } 161 if (s.startsWith("\"") && s.endsWith("\"")) { 162 return true; 163 } 164 165 return false; 166 } 167 168 /** 169 * Encodes the text into safe XML by replacing < > and & with XML tokens 170 * 171 * @param text the text 172 * @return the encoded text 173 */ 174 public static String xmlEncode(String text) { 175 if (text == null) { 176 return ""; 177 } 178 // must replace amp first, so we dont replace < to amp later 179 text = replaceAll(text, "&", "&"); 180 text = replaceAll(text, "\"", """); 181 text = replaceAll(text, "<", "<"); 182 text = replaceAll(text, ">", ">"); 183 return text; 184 } 185 186 /** 187 * Determines if the string has at least one letter in upper case 188 * @param text the text 189 * @return <tt>true</tt> if at least one letter is upper case, <tt>false</tt> otherwise 190 */ 191 public static boolean hasUpperCase(String text) { 192 if (text == null) { 193 return false; 194 } 195 196 for (int i = 0; i < text.length(); i++) { 197 char ch = text.charAt(i); 198 if (Character.isUpperCase(ch)) { 199 return true; 200 } 201 } 202 203 return false; 204 } 205 206 /** 207 * Determines if the string is a fully qualified class name 208 */ 209 public static boolean isClassName(String text) { 210 boolean result = false; 211 if (text != null) { 212 String[] split = text.split("\\."); 213 if (split.length > 0) { 214 String lastToken = split[split.length - 1]; 215 if (lastToken.length() > 0) { 216 result = Character.isUpperCase(lastToken.charAt(0)); 217 } 218 } 219 } 220 return result; 221 } 222 223 /** 224 * Does the expression have the language start token? 225 * 226 * @param expression the expression 227 * @param language the name of the language, such as simple 228 * @return <tt>true</tt> if the expression contains the start token, <tt>false</tt> otherwise 229 */ 230 public static boolean hasStartToken(String expression, String language) { 231 if (expression == null) { 232 return false; 233 } 234 235 // for the simple language the expression start token could be "${" 236 if ("simple".equalsIgnoreCase(language) && expression.contains("${")) { 237 return true; 238 } 239 240 if (language != null && expression.contains("$" + language + "{")) { 241 return true; 242 } 243 244 return false; 245 } 246 247 /** 248 * Replaces all the from tokens in the given input string. 249 * <p/> 250 * This implementation is not recursive, not does it check for tokens in the replacement string. 251 * 252 * @param input the input string 253 * @param from the from string, must <b>not</b> be <tt>null</tt> or empty 254 * @param to the replacement string, must <b>not</b> be empty 255 * @return the replaced string, or the input string if no replacement was needed 256 * @throws IllegalArgumentException if the input arguments is invalid 257 */ 258 public static String replaceAll(String input, String from, String to) { 259 if (ObjectHelper.isEmpty(input)) { 260 return input; 261 } 262 if (from == null) { 263 throw new IllegalArgumentException("from cannot be null"); 264 } 265 if (to == null) { 266 // to can be empty, so only check for null 267 throw new IllegalArgumentException("to cannot be null"); 268 } 269 270 // fast check if there is any from at all 271 if (!input.contains(from)) { 272 return input; 273 } 274 275 final int len = from.length(); 276 final int max = input.length(); 277 StringBuilder sb = new StringBuilder(max); 278 for (int i = 0; i < max;) { 279 if (i + len <= max) { 280 String token = input.substring(i, i + len); 281 if (from.equals(token)) { 282 sb.append(to); 283 // fast forward 284 i = i + len; 285 continue; 286 } 287 } 288 289 // append single char 290 sb.append(input.charAt(i)); 291 // forward to next 292 i++; 293 } 294 return sb.toString(); 295 } 296 297 /** 298 * Creates a json tuple with the given name/value pair. 299 * 300 * @param name the name 301 * @param value the value 302 * @param isMap whether the tuple should be map 303 * @return the json 304 */ 305 public static String toJson(String name, String value, boolean isMap) { 306 if (isMap) { 307 return "{ " + doubleQuote(name) + ": " + doubleQuote(value) + " }"; 308 } else { 309 return doubleQuote(name) + ": " + doubleQuote(value); 310 } 311 } 312 313 /** 314 * Asserts whether the string is <b>not</b> empty. 315 * 316 * @param value the string to test 317 * @param name the key that resolved the value 318 * @return the passed {@code value} as is 319 * @throws IllegalArgumentException is thrown if assertion fails 320 */ 321 public static String notEmpty(String value, String name) { 322 if (ObjectHelper.isEmpty(value)) { 323 throw new IllegalArgumentException(name + " must be specified and not empty"); 324 } 325 326 return value; 327 } 328 329 /** 330 * Asserts whether the string is <b>not</b> empty. 331 * 332 * @param value the string to test 333 * @param on additional description to indicate where this problem occurred (appended as toString()) 334 * @param name the key that resolved the value 335 * @return the passed {@code value} as is 336 * @throws IllegalArgumentException is thrown if assertion fails 337 */ 338 public static String notEmpty(String value, String name, Object on) { 339 if (on == null) { 340 ObjectHelper.notNull(value, name); 341 } else if (ObjectHelper.isEmpty(value)) { 342 throw new IllegalArgumentException(name + " must be specified and not empty on: " + on); 343 } 344 345 return value; 346 } 347 348 public static String[] splitOnCharacter(String value, String needle, int count) { 349 String rc[] = new String[count]; 350 rc[0] = value; 351 for (int i = 1; i < count; i++) { 352 String v = rc[i - 1]; 353 int p = v.indexOf(needle); 354 if (p < 0) { 355 return rc; 356 } 357 rc[i - 1] = v.substring(0, p); 358 rc[i] = v.substring(p + 1); 359 } 360 return rc; 361 } 362 363 /** 364 * Removes any starting characters on the given text which match the given 365 * character 366 * 367 * @param text the string 368 * @param ch the initial characters to remove 369 * @return either the original string or the new substring 370 */ 371 public static String removeStartingCharacters(String text, char ch) { 372 int idx = 0; 373 while (text.charAt(idx) == ch) { 374 idx++; 375 } 376 if (idx > 0) { 377 return text.substring(idx); 378 } 379 return text; 380 } 381 382 /** 383 * Capitalize the string (upper case first character) 384 * 385 * @param text the string 386 * @return the string capitalized (upper case first character) 387 */ 388 public static String capitalize(String text) { 389 if (text == null) { 390 return null; 391 } 392 int length = text.length(); 393 if (length == 0) { 394 return text; 395 } 396 String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH); 397 if (length > 1) { 398 answer += text.substring(1, length); 399 } 400 return answer; 401 } 402 403 /** 404 * Returns the string after the given token 405 * 406 * @param text the text 407 * @param after the token 408 * @return the text after the token, or <tt>null</tt> if text does not contain the token 409 */ 410 public static String after(String text, String after) { 411 if (!text.contains(after)) { 412 return null; 413 } 414 return text.substring(text.indexOf(after) + after.length()); 415 } 416 417 /** 418 * Returns an object after the given token 419 * 420 * @param text the text 421 * @param after the token 422 * @param mapper a mapping function to convert the string after the token to type T 423 * @return an Optional describing the result of applying a mapping function to the text after the token. 424 */ 425 public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) { 426 String result = after(text, after); 427 if (result == null) { 428 return Optional.empty(); 429 } else { 430 return Optional.ofNullable(mapper.apply(result)); 431 } 432 } 433 434 /** 435 * Returns the string before the given token 436 * 437 * @param text the text 438 * @param before the token 439 * @return the text before the token, or <tt>null</tt> if text does not 440 * contain the token 441 */ 442 public static String before(String text, String before) { 443 if (!text.contains(before)) { 444 return null; 445 } 446 return text.substring(0, text.indexOf(before)); 447 } 448 449 /** 450 * Returns an object before the given token 451 * 452 * @param text the text 453 * @param before the token 454 * @param mapper a mapping function to convert the string before the token to type T 455 * @return an Optional describing the result of applying a mapping function to the text before the token. 456 */ 457 public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) { 458 String result = before(text, before); 459 if (result == null) { 460 return Optional.empty(); 461 } else { 462 return Optional.ofNullable(mapper.apply(result)); 463 } 464 } 465 466 /** 467 * Returns the string between the given tokens 468 * 469 * @param text the text 470 * @param after the before token 471 * @param before the after token 472 * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens 473 */ 474 public static String between(String text, String after, String before) { 475 text = after(text, after); 476 if (text == null) { 477 return null; 478 } 479 return before(text, before); 480 } 481 482 /** 483 * Returns an object between the given token 484 * 485 * @param text the text 486 * @param after the before token 487 * @param before the after token 488 * @param mapper a mapping function to convert the string between the token to type T 489 * @return an Optional describing the result of applying a mapping function to the text between the token. 490 */ 491 public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) { 492 String result = between(text, after, before); 493 if (result == null) { 494 return Optional.empty(); 495 } else { 496 return Optional.ofNullable(mapper.apply(result)); 497 } 498 } 499 500 /** 501 * Returns the string between the most outer pair of tokens 502 * <p/> 503 * The number of token pairs must be evenly, eg there must be same number of before and after tokens, otherwise <tt>null</tt> is returned 504 * <p/> 505 * This implementation skips matching when the text is either single or double quoted. 506 * For example: 507 * <tt>${body.matches("foo('bar')")</tt> 508 * Will not match the parenthesis from the quoted text. 509 * 510 * @param text the text 511 * @param after the before token 512 * @param before the after token 513 * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens 514 */ 515 public static String betweenOuterPair(String text, char before, char after) { 516 if (text == null) { 517 return null; 518 } 519 520 int pos = -1; 521 int pos2 = -1; 522 int count = 0; 523 int count2 = 0; 524 525 boolean singleQuoted = false; 526 boolean doubleQuoted = false; 527 for (int i = 0; i < text.length(); i++) { 528 char ch = text.charAt(i); 529 if (!doubleQuoted && ch == '\'') { 530 singleQuoted = !singleQuoted; 531 } else if (!singleQuoted && ch == '\"') { 532 doubleQuoted = !doubleQuoted; 533 } 534 if (singleQuoted || doubleQuoted) { 535 continue; 536 } 537 538 if (ch == before) { 539 count++; 540 } else if (ch == after) { 541 count2++; 542 } 543 544 if (ch == before && pos == -1) { 545 pos = i; 546 } else if (ch == after) { 547 pos2 = i; 548 } 549 } 550 551 if (pos == -1 || pos2 == -1) { 552 return null; 553 } 554 555 // must be even paris 556 if (count != count2) { 557 return null; 558 } 559 560 return text.substring(pos + 1, pos2); 561 } 562 563 /** 564 * Returns an object between the most outer pair of tokens 565 * 566 * @param text the text 567 * @param after the before token 568 * @param before the after token 569 * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T 570 * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens. 571 */ 572 public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) { 573 String result = betweenOuterPair(text, before, after); 574 if (result == null) { 575 return Optional.empty(); 576 } else { 577 return Optional.ofNullable(mapper.apply(result)); 578 } 579 } 580 581 /** 582 * Returns true if the given name is a valid java identifier 583 */ 584 public static boolean isJavaIdentifier(String name) { 585 if (name == null) { 586 return false; 587 } 588 int size = name.length(); 589 if (size < 1) { 590 return false; 591 } 592 if (Character.isJavaIdentifierStart(name.charAt(0))) { 593 for (int i = 1; i < size; i++) { 594 if (!Character.isJavaIdentifierPart(name.charAt(i))) { 595 return false; 596 } 597 } 598 return true; 599 } 600 return false; 601 } 602 603 /** 604 * Cleans the string to a pure Java identifier so we can use it for loading class names. 605 * <p/> 606 * Especially from Spring DSL people can have \n \t or other characters that otherwise 607 * would result in ClassNotFoundException 608 * 609 * @param name the class name 610 * @return normalized classname that can be load by a class loader. 611 */ 612 public static String normalizeClassName(String name) { 613 StringBuilder sb = new StringBuilder(name.length()); 614 for (char ch : name.toCharArray()) { 615 if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) { 616 sb.append(ch); 617 } 618 } 619 return sb.toString(); 620 } 621 622 /** 623 * Compares old and new text content and report back which lines are changed 624 * 625 * @param oldText the old text 626 * @param newText the new text 627 * @return a list of line numbers that are changed in the new text 628 */ 629 public static List<Integer> changedLines(String oldText, String newText) { 630 if (oldText == null || oldText.equals(newText)) { 631 return Collections.emptyList(); 632 } 633 634 List<Integer> changed = new ArrayList<>(); 635 636 String[] oldLines = oldText.split("\n"); 637 String[] newLines = newText.split("\n"); 638 639 for (int i = 0; i < newLines.length; i++) { 640 String newLine = newLines[i]; 641 String oldLine = i < oldLines.length ? oldLines[i] : null; 642 if (oldLine == null) { 643 changed.add(i); 644 } else if (!newLine.equals(oldLine)) { 645 changed.add(i); 646 } 647 } 648 649 return changed; 650 } 651 652 /** 653 * Removes the leading and trailing whitespace and if the resulting 654 * string is empty returns {@code null}. Examples: 655 * <p> 656 * Examples: 657 * <blockquote><pre> 658 * trimToNull("abc") -> "abc" 659 * trimToNull(" abc") -> "abc" 660 * trimToNull(" abc ") -> "abc" 661 * trimToNull(" ") -> null 662 * trimToNull("") -> null 663 * </pre></blockquote> 664 */ 665 public static String trimToNull(final String given) { 666 if (given == null) { 667 return null; 668 } 669 670 final String trimmed = given.trim(); 671 672 if (trimmed.isEmpty()) { 673 return null; 674 } 675 676 return trimmed; 677 } 678 679 /** 680 * Checks if the src string contains what 681 * 682 * @param src is the source string to be checked 683 * @param what is the string which will be looked up in the src argument 684 * @return true/false 685 */ 686 public static boolean containsIgnoreCase(String src, String what) { 687 if (src == null || what == null) { 688 return false; 689 } 690 691 final int length = what.length(); 692 if (length == 0) { 693 return true; // Empty string is contained 694 } 695 696 final char firstLo = Character.toLowerCase(what.charAt(0)); 697 final char firstUp = Character.toUpperCase(what.charAt(0)); 698 699 for (int i = src.length() - length; i >= 0; i--) { 700 // Quick check before calling the more expensive regionMatches() method: 701 final char ch = src.charAt(i); 702 if (ch != firstLo && ch != firstUp) { 703 continue; 704 } 705 706 if (src.regionMatches(true, i, what, 0, length)) { 707 return true; 708 } 709 } 710 711 return false; 712 } 713 714 /** 715 * Outputs the bytes in human readable format in units of KB,MB,GB etc. 716 * 717 * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied. 718 * @param bytes number of bytes 719 * @return human readable output 720 * @see java.lang.String#format(Locale, String, Object...) 721 */ 722 public static String humanReadableBytes(Locale locale, long bytes) { 723 int unit = 1024; 724 if (bytes < unit) { 725 return bytes + " B"; 726 } 727 int exp = (int) (Math.log(bytes) / Math.log(unit)); 728 String pre = "KMGTPE".charAt(exp - 1) + ""; 729 return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre); 730 } 731 732 /** 733 * Outputs the bytes in human readable format in units of KB,MB,GB etc. 734 * 735 * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 736 * 737 * @param bytes number of bytes 738 * @return human readable output 739 * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long) 740 */ 741 public static String humanReadableBytes(long bytes) { 742 return humanReadableBytes(Locale.getDefault(), bytes); 743 } 744 745 /** 746 * Check for string pattern matching with a number of strategies in the 747 * following order: 748 * 749 * - equals 750 * - null pattern always matches 751 * - * always matches 752 * - Ant style matching 753 * - Regexp 754 * 755 * @param patter the pattern 756 * @param target the string to test 757 * @return true if target matches the pattern 758 */ 759 public static boolean matches(String patter, String target) { 760 if (Objects.equals(patter, target)) { 761 return true; 762 } 763 764 if (Objects.isNull(patter)) { 765 return true; 766 } 767 768 if (Objects.equals("*", patter)) { 769 return true; 770 } 771 772 if (AntPathMatcher.INSTANCE.match(patter, target)) { 773 return true; 774 } 775 776 Pattern p = Pattern.compile(patter); 777 Matcher m = p.matcher(target); 778 779 return m.matches(); 780 } 781 782}