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