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