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