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;
028import java.util.stream.Stream;
029
030/**
031 * Helper methods for working with Strings.
032 */
033public final class StringHelper {
034
035    /**
036     * Constructor of utility class should be private.
037     */
038    private StringHelper() {
039    }
040
041    /**
042     * Ensures that <code>s</code> is friendly for a URL or file system.
043     *
044     * @param  s                    String to be sanitized.
045     * @return                      sanitized version of <code>s</code>.
046     * @throws NullPointerException if <code>s</code> is <code>null</code>.
047     */
048    public static String sanitize(String s) {
049        return s.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     * Replaces the first from token in the given input string.
313     * <p/>
314     * This implementation is not recursive, not does it check for tokens in the replacement string.
315     *
316     * @param  input                    the input string
317     * @param  from                     the from string, must <b>not</b> be <tt>null</tt> or empty
318     * @param  to                       the replacement string, must <b>not</b> be empty
319     * @return                          the replaced string, or the input string if no replacement was needed
320     * @throws IllegalArgumentException if the input arguments is invalid
321     */
322    public static String replaceFirst(String input, String from, String to) {
323        int pos = input.indexOf(from);
324        if (pos != -1) {
325            int len = from.length();
326            return input.substring(0, pos) + to + input.substring(pos + len);
327        } else {
328            return input;
329        }
330    }
331
332    /**
333     * Creates a json tuple with the given name/value pair.
334     *
335     * @param  name  the name
336     * @param  value the value
337     * @param  isMap whether the tuple should be map
338     * @return       the json
339     */
340    public static String toJson(String name, String value, boolean isMap) {
341        if (isMap) {
342            return "{ " + StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value) + " }";
343        } else {
344            return StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value);
345        }
346    }
347
348    /**
349     * Asserts whether the string is <b>not</b> empty.
350     *
351     * @param  value                    the string to test
352     * @param  name                     the key that resolved the value
353     * @return                          the passed {@code value} as is
354     * @throws IllegalArgumentException is thrown if assertion fails
355     */
356    public static String notEmpty(String value, String name) {
357        if (ObjectHelper.isEmpty(value)) {
358            throw new IllegalArgumentException(name + " must be specified and not empty");
359        }
360
361        return value;
362    }
363
364    /**
365     * Asserts whether the string is <b>not</b> empty.
366     *
367     * @param  value                    the string to test
368     * @param  on                       additional description to indicate where this problem occurred (appended as
369     *                                  toString())
370     * @param  name                     the key that resolved the value
371     * @return                          the passed {@code value} as is
372     * @throws IllegalArgumentException is thrown if assertion fails
373     */
374    public static String notEmpty(String value, String name, Object on) {
375        if (on == null) {
376            ObjectHelper.notNull(value, name);
377        } else if (ObjectHelper.isEmpty(value)) {
378            throw new IllegalArgumentException(name + " must be specified and not empty on: " + on);
379        }
380
381        return value;
382    }
383
384    public static String[] splitOnCharacter(String value, String needle, int count) {
385        String[] rc = new String[count];
386        rc[0] = value;
387        for (int i = 1; i < count; i++) {
388            String v = rc[i - 1];
389            int p = v.indexOf(needle);
390            if (p < 0) {
391                return rc;
392            }
393            rc[i - 1] = v.substring(0, p);
394            rc[i] = v.substring(p + 1);
395        }
396        return rc;
397    }
398
399    /**
400     * Removes any starting characters on the given text which match the given character
401     *
402     * @param  text the string
403     * @param  ch   the initial characters to remove
404     * @return      either the original string or the new substring
405     */
406    public static String removeStartingCharacters(String text, char ch) {
407        int idx = 0;
408        while (text.charAt(idx) == ch) {
409            idx++;
410        }
411        if (idx > 0) {
412            return text.substring(idx);
413        }
414        return text;
415    }
416
417    /**
418     * Capitalize the string (upper case first character)
419     *
420     * @param  text the string
421     * @return      the string capitalized (upper case first character)
422     */
423    public static String capitalize(String text) {
424        return capitalize(text, false);
425    }
426
427    /**
428     * Capitalize the string (upper case first character)
429     *
430     * @param  text            the string
431     * @param  dashToCamelCase whether to also convert dash format into camel case (hello-great-world ->
432     *                         helloGreatWorld)
433     * @return                 the string capitalized (upper case first character)
434     */
435    public static String capitalize(String text, boolean dashToCamelCase) {
436        if (dashToCamelCase) {
437            text = dashToCamelCase(text);
438        }
439        if (text == null) {
440            return null;
441        }
442        int length = text.length();
443        if (length == 0) {
444            return text;
445        }
446        String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH);
447        if (length > 1) {
448            answer += text.substring(1, length);
449        }
450        return answer;
451    }
452
453    /**
454     * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld)
455     *
456     * @param  text the string
457     * @return      the string camel cased
458     */
459    public static String dashToCamelCase(String text) {
460        if (text == null) {
461            return null;
462        }
463        int length = text.length();
464        if (length == 0) {
465            return text;
466        }
467        if (text.indexOf('-') == -1) {
468            return text;
469        }
470
471        // there is at least 1 dash so the capacity can be shorter
472        StringBuilder sb = new StringBuilder(length - 1);
473        boolean upper = false;
474        for (int i = 0; i < length; i++) {
475            char c = text.charAt(i);
476            if (c == '-') {
477                upper = true;
478            } else {
479                if (upper) {
480                    c = Character.toUpperCase(c);
481                }
482                sb.append(c);
483                upper = false;
484            }
485        }
486        return sb.toString();
487    }
488
489    /**
490     * Returns the string after the given token
491     *
492     * @param  text  the text
493     * @param  after the token
494     * @return       the text after the token, or <tt>null</tt> if text does not contain the token
495     */
496    public static String after(String text, String after) {
497        int pos = text.indexOf(after);
498        if (pos == -1) {
499            return null;
500        }
501        return text.substring(pos + after.length());
502    }
503
504    /**
505     * Returns the string after the given token, or the default value
506     *
507     * @param  text         the text
508     * @param  after        the token
509     * @param  defaultValue the value to return if text does not contain the token
510     * @return              the text after the token, or the supplied defaultValue if text does not contain the token
511     */
512    public static String after(String text, String after, String defaultValue) {
513        String answer = after(text, after);
514        return answer != null ? answer : defaultValue;
515    }
516
517    /**
518     * Returns an object after the given token
519     *
520     * @param  text   the text
521     * @param  after  the token
522     * @param  mapper a mapping function to convert the string after the token to type T
523     * @return        an Optional describing the result of applying a mapping function to the text after the token.
524     */
525    public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
526        String result = after(text, after);
527        if (result == null) {
528            return Optional.empty();
529        } else {
530            return Optional.ofNullable(mapper.apply(result));
531        }
532    }
533
534    /**
535     * Returns the string after the the last occurrence of the given token
536     *
537     * @param  text  the text
538     * @param  after the token
539     * @return       the text after the token, or <tt>null</tt> if text does not contain the token
540     */
541    public static String afterLast(String text, String after) {
542        int pos = text.lastIndexOf(after);
543        if (pos == -1) {
544            return null;
545        }
546        return text.substring(pos + after.length());
547    }
548
549    /**
550     * Returns the string after the the last occurrence of the given token, or the default value
551     *
552     * @param  text         the text
553     * @param  after        the token
554     * @param  defaultValue the value to return if text does not contain the token
555     * @return              the text after the token, or the supplied defaultValue if text does not contain the token
556     */
557    public static String afterLast(String text, String after, String defaultValue) {
558        String answer = afterLast(text, after);
559        return answer != null ? answer : defaultValue;
560    }
561
562    /**
563     * Returns the string before the given token
564     *
565     * @param  text   the text
566     * @param  before the token
567     * @return        the text before the token, or <tt>null</tt> if text does not contain the token
568     */
569    public static String before(String text, String before) {
570        int pos = text.indexOf(before);
571        return pos == -1 ? null : text.substring(0, pos);
572    }
573
574    /**
575     * Returns the string before the given token, or the default value
576     *
577     * @param  text         the text
578     * @param  before       the token
579     * @param  defaultValue the value to return if text does not contain the token
580     * @return              the text before the token, or the supplied defaultValue if text does not contain the token
581     */
582    public static String before(String text, String before, String defaultValue) {
583        String answer = before(text, before);
584        return answer != null ? answer : defaultValue;
585    }
586
587    /**
588     * Returns an object before the given token
589     *
590     * @param  text   the text
591     * @param  before the token
592     * @param  mapper a mapping function to convert the string before the token to type T
593     * @return        an Optional describing the result of applying a mapping function to the text before the token.
594     */
595    public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
596        String result = before(text, before);
597        if (result == null) {
598            return Optional.empty();
599        } else {
600            return Optional.ofNullable(mapper.apply(result));
601        }
602    }
603
604    /**
605     * Returns the string before the last occurrence of the given token
606     *
607     * @param  text   the text
608     * @param  before the token
609     * @return        the text before the token, or <tt>null</tt> if text does not contain the token
610     */
611    public static String beforeLast(String text, String before) {
612        int pos = text.lastIndexOf(before);
613        return pos == -1 ? null : text.substring(0, pos);
614    }
615
616    /**
617     * Returns the string before the last occurrence of the given token, or the default value
618     *
619     * @param  text         the text
620     * @param  before       the token
621     * @param  defaultValue the value to return if text does not contain the token
622     * @return              the text before the token, or the supplied defaultValue if text does not contain the token
623     */
624    public static String beforeLast(String text, String before, String defaultValue) {
625        String answer = beforeLast(text, before);
626        return answer != null ? answer : defaultValue;
627    }
628
629    /**
630     * Returns the string between the given tokens
631     *
632     * @param  text   the text
633     * @param  after  the before token
634     * @param  before the after token
635     * @return        the text between the tokens, or <tt>null</tt> if text does not contain the tokens
636     */
637    public static String between(String text, String after, String before) {
638        text = after(text, after);
639        if (text == null) {
640            return null;
641        }
642        return before(text, before);
643    }
644
645    /**
646     * Returns an object between the given token
647     *
648     * @param  text   the text
649     * @param  after  the before token
650     * @param  before the after token
651     * @param  mapper a mapping function to convert the string between the token to type T
652     * @return        an Optional describing the result of applying a mapping function to the text between the token.
653     */
654    public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
655        String result = between(text, after, before);
656        if (result == null) {
657            return Optional.empty();
658        } else {
659            return Optional.ofNullable(mapper.apply(result));
660        }
661    }
662
663    /**
664     * Returns the string between the most outer pair of tokens
665     * <p/>
666     * The number of token pairs must be evenly, eg there must be same number of before and after tokens, otherwise
667     * <tt>null</tt> is returned
668     * <p/>
669     * This implementation skips matching when the text is either single or double quoted. For example:
670     * <tt>${body.matches("foo('bar')")</tt> Will not match the parenthesis from the quoted text.
671     *
672     * @param  text   the text
673     * @param  after  the before token
674     * @param  before the after token
675     * @return        the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
676     */
677    public static String betweenOuterPair(String text, char before, char after) {
678        if (text == null) {
679            return null;
680        }
681
682        int pos = -1;
683        int pos2 = -1;
684        int count = 0;
685        int count2 = 0;
686
687        boolean singleQuoted = false;
688        boolean doubleQuoted = false;
689        for (int i = 0; i < text.length(); i++) {
690            char ch = text.charAt(i);
691            if (!doubleQuoted && ch == '\'') {
692                singleQuoted = !singleQuoted;
693            } else if (!singleQuoted && ch == '\"') {
694                doubleQuoted = !doubleQuoted;
695            }
696            if (singleQuoted || doubleQuoted) {
697                continue;
698            }
699
700            if (ch == before) {
701                count++;
702            } else if (ch == after) {
703                count2++;
704            }
705
706            if (ch == before && pos == -1) {
707                pos = i;
708            } else if (ch == after) {
709                pos2 = i;
710            }
711        }
712
713        if (pos == -1 || pos2 == -1) {
714            return null;
715        }
716
717        // must be even paris
718        if (count != count2) {
719            return null;
720        }
721
722        return text.substring(pos + 1, pos2);
723    }
724
725    /**
726     * Returns an object between the most outer pair of tokens
727     *
728     * @param  text   the text
729     * @param  after  the before token
730     * @param  before the after token
731     * @param  mapper a mapping function to convert the string between the most outer pair of tokens to type T
732     * @return        an Optional describing the result of applying a mapping function to the text between the most
733     *                outer pair of tokens.
734     */
735    public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
736        String result = betweenOuterPair(text, before, after);
737        if (result == null) {
738            return Optional.empty();
739        } else {
740            return Optional.ofNullable(mapper.apply(result));
741        }
742    }
743
744    /**
745     * Returns true if the given name is a valid java identifier
746     */
747    public static boolean isJavaIdentifier(String name) {
748        if (name == null) {
749            return false;
750        }
751        int size = name.length();
752        if (size < 1) {
753            return false;
754        }
755        if (Character.isJavaIdentifierStart(name.charAt(0))) {
756            for (int i = 1; i < size; i++) {
757                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
758                    return false;
759                }
760            }
761            return true;
762        }
763        return false;
764    }
765
766    /**
767     * Cleans the string to a pure Java identifier so we can use it for loading class names.
768     * <p/>
769     * Especially from Spring DSL people can have \n \t or other characters that otherwise would result in
770     * ClassNotFoundException
771     *
772     * @param  name the class name
773     * @return      normalized classname that can be load by a class loader.
774     */
775    public static String normalizeClassName(String name) {
776        StringBuilder sb = new StringBuilder(name.length());
777        for (char ch : name.toCharArray()) {
778            if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
779                sb.append(ch);
780            }
781        }
782        return sb.toString();
783    }
784
785    /**
786     * Compares old and new text content and report back which lines are changed
787     *
788     * @param  oldText the old text
789     * @param  newText the new text
790     * @return         a list of line numbers that are changed in the new text
791     */
792    public static List<Integer> changedLines(String oldText, String newText) {
793        if (oldText == null || oldText.equals(newText)) {
794            return Collections.emptyList();
795        }
796
797        List<Integer> changed = new ArrayList<>();
798
799        String[] oldLines = oldText.split("\n");
800        String[] newLines = newText.split("\n");
801
802        for (int i = 0; i < newLines.length; i++) {
803            String newLine = newLines[i];
804            String oldLine = i < oldLines.length ? oldLines[i] : null;
805            if (oldLine == null) {
806                changed.add(i);
807            } else if (!newLine.equals(oldLine)) {
808                changed.add(i);
809            }
810        }
811
812        return changed;
813    }
814
815    /**
816     * Removes the leading and trailing whitespace and if the resulting string is empty returns {@code null}. Examples:
817     * <p>
818     * Examples: <blockquote>
819     *
820     * <pre>
821     * trimToNull("abc") -> "abc"
822     * trimToNull(" abc") -> "abc"
823     * trimToNull(" abc ") -> "abc"
824     * trimToNull(" ") -> null
825     * trimToNull("") -> null
826     * </pre>
827     *
828     * </blockquote>
829     */
830    public static String trimToNull(final String given) {
831        if (given == null) {
832            return null;
833        }
834
835        final String trimmed = given.trim();
836
837        if (trimmed.isEmpty()) {
838            return null;
839        }
840
841        return trimmed;
842    }
843
844    /**
845     * Checks if the src string contains what
846     *
847     * @param  src  is the source string to be checked
848     * @param  what is the string which will be looked up in the src argument
849     * @return      true/false
850     */
851    public static boolean containsIgnoreCase(String src, String what) {
852        if (src == null || what == null) {
853            return false;
854        }
855
856        final int length = what.length();
857        if (length == 0) {
858            return true; // Empty string is contained
859        }
860
861        final char firstLo = Character.toLowerCase(what.charAt(0));
862        final char firstUp = Character.toUpperCase(what.charAt(0));
863
864        for (int i = src.length() - length; i >= 0; i--) {
865            // Quick check before calling the more expensive regionMatches() method:
866            final char ch = src.charAt(i);
867            if (ch != firstLo && ch != firstUp) {
868                continue;
869            }
870
871            if (src.regionMatches(true, i, what, 0, length)) {
872                return true;
873            }
874        }
875
876        return false;
877    }
878
879    /**
880     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
881     *
882     * @param  locale The locale to apply during formatting. If l is {@code null} then no localization is applied.
883     * @param  bytes  number of bytes
884     * @return        human readable output
885     * @see           java.lang.String#format(Locale, String, Object...)
886     */
887    public static String humanReadableBytes(Locale locale, long bytes) {
888        int unit = 1024;
889        if (bytes < unit) {
890            return bytes + " B";
891        }
892        int exp = (int) (Math.log(bytes) / Math.log(unit));
893        String pre = "KMGTPE".charAt(exp - 1) + "";
894        return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
895    }
896
897    /**
898     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
899     *
900     * The locale always used is the one returned by {@link java.util.Locale#getDefault()}.
901     *
902     * @param  bytes number of bytes
903     * @return       human readable output
904     * @see          org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long)
905     */
906    public static String humanReadableBytes(long bytes) {
907        return humanReadableBytes(Locale.getDefault(), bytes);
908    }
909
910    /**
911     * Check for string pattern matching with a number of strategies in the following order:
912     *
913     * - equals - null pattern always matches - * always matches - Ant style matching - Regexp
914     *
915     * @param  pattern the pattern
916     * @param  target  the string to test
917     * @return         true if target matches the pattern
918     */
919    public static boolean matches(String pattern, String target) {
920        if (Objects.equals(pattern, target)) {
921            return true;
922        }
923
924        if (Objects.isNull(pattern)) {
925            return true;
926        }
927
928        if (Objects.equals("*", pattern)) {
929            return true;
930        }
931
932        if (AntPathMatcher.INSTANCE.match(pattern, target)) {
933            return true;
934        }
935
936        Pattern p = Pattern.compile(pattern);
937        Matcher m = p.matcher(target);
938
939        return m.matches();
940    }
941
942    /**
943     * Converts the string from camel case into dash format (helloGreatWorld -> hello-great-world)
944     *
945     * @param  text the string
946     * @return      the string camel cased
947     */
948    public static String camelCaseToDash(String text) {
949        if (text == null || text.isEmpty()) {
950            return text;
951        }
952        StringBuilder answer = new StringBuilder();
953
954        Character prev = null;
955        Character next = null;
956        char[] arr = text.toCharArray();
957        for (int i = 0; i < arr.length; i++) {
958            char ch = arr[i];
959            if (i < arr.length - 1) {
960                next = arr[i + 1];
961            } else {
962                next = null;
963            }
964            if (ch == '-' || ch == '_') {
965                answer.append("-");
966            } else if (Character.isUpperCase(ch) && prev != null && !Character.isUpperCase(prev)) {
967                if (prev != '-' && prev != '_') {
968                    answer.append("-");
969                }
970                answer.append(ch);
971            } else if (Character.isUpperCase(ch) && prev != null && next != null && Character.isLowerCase(next)) {
972                if (prev != '-' && prev != '_') {
973                    answer.append("-");
974                }
975                answer.append(ch);
976            } else {
977                answer.append(ch);
978            }
979            prev = ch;
980        }
981
982        return answer.toString().toLowerCase(Locale.ENGLISH);
983    }
984
985    /**
986     * Does the string starts with the given prefix (ignore case).
987     *
988     * @param text   the string
989     * @param prefix the prefix
990     */
991    public static boolean startsWithIgnoreCase(String text, String prefix) {
992        if (text != null && prefix != null) {
993            return prefix.length() > text.length() ? false : text.regionMatches(true, 0, prefix, 0, prefix.length());
994        } else {
995            return text == null && prefix == null;
996        }
997    }
998
999    /**
1000     * Converts the value to an enum constant value that is in the form of upper cased with underscore.
1001     */
1002    public static String asEnumConstantValue(String value) {
1003        if (value == null || value.isEmpty()) {
1004            return value;
1005        }
1006        value = StringHelper.camelCaseToDash(value);
1007        // replace double dashes
1008        value = value.replaceAll("-+", "-");
1009        // replace dash with underscore and upper case
1010        value = value.replace('-', '_').toUpperCase(Locale.ENGLISH);
1011        return value;
1012    }
1013
1014    /**
1015     * Split the text on words, eg hello/world => becomes array with hello in index 0, and world in index 1.
1016     */
1017    public static String[] splitWords(String text) {
1018        return text.split("[\\W]+");
1019    }
1020
1021    /**
1022     * Creates a stream from the given input sequence around matches of the regex
1023     *
1024     * @param  text  the input
1025     * @param  regex the expression used to split the input
1026     * @return       the stream of strings computed by splitting the input with the given regex
1027     */
1028    public static Stream<String> splitAsStream(CharSequence text, String regex) {
1029        if (text == null || regex == null) {
1030            return Stream.empty();
1031        }
1032
1033        return Pattern.compile(regex).splitAsStream(text);
1034    }
1035
1036    /**
1037     * Returns the occurrence of a search string in to a string.
1038     *
1039     * @param  text   the text
1040     * @param  search the string to search
1041     * @return        an integer reporting the number of occurrence of the searched string in to the text
1042     */
1043    public static int countOccurrence(String text, String search) {
1044        int lastIndex = 0;
1045        int count = 0;
1046        while (lastIndex != -1) {
1047            lastIndex = text.indexOf(search, lastIndex);
1048            if (lastIndex != -1) {
1049                count++;
1050                lastIndex += search.length();
1051            }
1052        }
1053        return count;
1054    }
1055
1056    /**
1057     * Replaces a string in to a text starting from his second occurrence.
1058     *
1059     * @param  text        the text
1060     * @param  search      the string to search
1061     * @param  replacement the replacement for the string
1062     * @return             the string with the replacement
1063     */
1064    public static String replaceFromSecondOccurrence(String text, String search, String replacement) {
1065        int index = text.indexOf(search);
1066        boolean replace = false;
1067
1068        while (index != -1) {
1069            String tempString = text.substring(index);
1070            if (replace) {
1071                tempString = tempString.replaceFirst(search, replacement);
1072                text = text.substring(0, index) + tempString;
1073                replace = false;
1074            } else {
1075                replace = true;
1076            }
1077            index = text.indexOf(search, index + 1);
1078        }
1079        return text;
1080    }
1081}