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