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