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 &lt; to amp later
190        text = replaceAll(text, "&", "&amp;");
191        text = replaceAll(text, "\"", "&quot;");
192        text = replaceAll(text, "<", "&lt;");
193        text = replaceAll(text, ">", "&gt;");
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}