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