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        return dashToCamelCase(text, false);
536    }
537
538    /**
539     * Whether the string contains dashes or not
540     *
541     * @param  text the string to check
542     * @return      true if it contains dashes or false otherwise
543     */
544    public static boolean isDashed(String text) {
545        int length = text.length();
546        return length != 0 && text.indexOf('-') != -1;
547    }
548
549    /**
550     * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld)
551     *
552     * @param  text              the string
553     * @param  skipQuotedOrKeyed flag to skip converting within quoted or keyed text
554     * @return                   the string camel cased
555     */
556    public static String dashToCamelCase(final String text, boolean skipQuotedOrKeyed) {
557        if (text == null) {
558            return null;
559        }
560        if (!isDashed(text)) {
561            return text;
562        }
563
564        // there is at least 1 dash so the capacity can be shorter
565        int length = text.length();
566        StringBuilder sb = new StringBuilder(length - 1);
567        boolean upper = false;
568        int singleQuotes = 0;
569        int doubleQuotes = 0;
570        boolean skip = false;
571        for (int i = 0; i < length; i++) {
572            char c = text.charAt(i);
573
574            // special for skip mode where we should keep text inside quotes or keys as-is
575            if (skipQuotedOrKeyed) {
576                if (c == ']') {
577                    skip = false;
578                } else if (c == '[') {
579                    skip = true;
580                } else if (c == '\'') {
581                    singleQuotes++;
582                } else if (c == '"') {
583                    doubleQuotes++;
584                }
585                if (singleQuotes > 0) {
586                    skip = singleQuotes % 2 == 1;
587                }
588                if (doubleQuotes > 0) {
589                    skip = doubleQuotes % 2 == 1;
590                }
591                if (skip) {
592                    sb.append(c);
593                    continue;
594                }
595            }
596
597            if (c == '-') {
598                upper = true;
599            } else {
600                if (upper) {
601                    c = Character.toUpperCase(c);
602                }
603                sb.append(c);
604                upper = false;
605            }
606        }
607        return sb.toString();
608    }
609
610    /**
611     * Returns the string after the given token
612     *
613     * @param  text  the text
614     * @param  after the token
615     * @return       the text after the token, or <tt>null</tt> if text does not contain the token
616     */
617    public static String after(String text, String after) {
618        if (text == null) {
619            return null;
620        }
621        int pos = text.indexOf(after);
622        if (pos == -1) {
623            return null;
624        }
625        return text.substring(pos + after.length());
626    }
627
628    /**
629     * Returns the string after the given token, or the default value
630     *
631     * @param  text         the text
632     * @param  after        the token
633     * @param  defaultValue the value to return if text does not contain the token
634     * @return              the text after the token, or the supplied defaultValue if text does not contain the token
635     */
636    public static String after(String text, String after, String defaultValue) {
637        String answer = after(text, after);
638        return answer != null ? answer : defaultValue;
639    }
640
641    /**
642     * Returns an object after the given token
643     *
644     * @param  text   the text
645     * @param  after  the token
646     * @param  mapper a mapping function to convert the string after the token to type T
647     * @return        an Optional describing the result of applying a mapping function to the text after the token.
648     */
649    public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
650        String result = after(text, after);
651        if (result == null) {
652            return Optional.empty();
653        } else {
654            return Optional.ofNullable(mapper.apply(result));
655        }
656    }
657
658    /**
659     * Returns the string after the the last occurrence of the given token
660     *
661     * @param  text  the text
662     * @param  after the token
663     * @return       the text after the token, or <tt>null</tt> if text does not contain the token
664     */
665    public static String afterLast(String text, String after) {
666        if (text == null) {
667            return null;
668        }
669        int pos = text.lastIndexOf(after);
670        if (pos == -1) {
671            return null;
672        }
673        return text.substring(pos + after.length());
674    }
675
676    /**
677     * Returns the string after the the last occurrence of the given token, or the default value
678     *
679     * @param  text         the text
680     * @param  after        the token
681     * @param  defaultValue the value to return if text does not contain the token
682     * @return              the text after the token, or the supplied defaultValue if text does not contain the token
683     */
684    public static String afterLast(String text, String after, String defaultValue) {
685        String answer = afterLast(text, after);
686        return answer != null ? answer : defaultValue;
687    }
688
689    /**
690     * Returns the string before the given token
691     *
692     * @param  text   the text
693     * @param  before the token
694     * @return        the text before the token, or <tt>null</tt> if text does not contain the token
695     */
696    public static String before(String text, String before) {
697        if (text == null) {
698            return null;
699        }
700        int pos = text.indexOf(before);
701        return pos == -1 ? null : text.substring(0, pos);
702    }
703
704    /**
705     * Returns the string before 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 before(String text, String before, String defaultValue) {
713        if (text == null) {
714            return defaultValue;
715        }
716        int pos = text.indexOf(before);
717        return pos == -1 ? defaultValue : text.substring(0, pos);
718    }
719
720    /**
721     * Returns the string before the given token, or the default value
722     *
723     * @param  text         the text
724     * @param  before       the token
725     * @param  defaultValue the value to return if text does not contain the token
726     * @return              the text before the token, or the supplied defaultValue if text does not contain the token
727     */
728    public static String before(String text, char before, String defaultValue) {
729        if (text == null) {
730            return defaultValue;
731        }
732        int pos = text.indexOf(before);
733        return pos == -1 ? defaultValue : text.substring(0, pos);
734    }
735
736    /**
737     * Returns an object before the given token
738     *
739     * @param  text   the text
740     * @param  before the token
741     * @param  mapper a mapping function to convert the string before the token to type T
742     * @return        an Optional describing the result of applying a mapping function to the text before the token.
743     */
744    public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
745        String result = before(text, before);
746        if (result == null) {
747            return Optional.empty();
748        } else {
749            return Optional.ofNullable(mapper.apply(result));
750        }
751    }
752
753    /**
754     * Returns the string before the last occurrence of the given token
755     *
756     * @param  text   the text
757     * @param  before the token
758     * @return        the text before the token, or <tt>null</tt> if text does not contain the token
759     */
760    public static String beforeLast(String text, String before) {
761        if (text == null) {
762            return null;
763        }
764        int pos = text.lastIndexOf(before);
765        return pos == -1 ? null : text.substring(0, pos);
766    }
767
768    /**
769     * Returns the string before the last occurrence of the given token, or the default value
770     *
771     * @param  text         the text
772     * @param  before       the token
773     * @param  defaultValue the value to return if text does not contain the token
774     * @return              the text before the token, or the supplied defaultValue if text does not contain the token
775     */
776    public static String beforeLast(String text, String before, String defaultValue) {
777        String answer = beforeLast(text, before);
778        return answer != null ? answer : defaultValue;
779    }
780
781    /**
782     * Returns the string between the given tokens
783     *
784     * @param  text   the text
785     * @param  after  the before token
786     * @param  before the after token
787     * @return        the text between the tokens, or <tt>null</tt> if text does not contain the tokens
788     */
789    public static String between(final String text, String after, String before) {
790        String ret = after(text, after);
791        if (ret == null) {
792            return null;
793        }
794        return before(ret, before);
795    }
796
797    /**
798     * Returns an object between the given token
799     *
800     * @param  text   the text
801     * @param  after  the before token
802     * @param  before the after token
803     * @param  mapper a mapping function to convert the string between the token to type T
804     * @return        an Optional describing the result of applying a mapping function to the text between the token.
805     */
806    public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
807        String result = between(text, after, before);
808        if (result == null) {
809            return Optional.empty();
810        } else {
811            return Optional.ofNullable(mapper.apply(result));
812        }
813    }
814
815    /**
816     * Returns the string between the most outer pair of tokens
817     * <p/>
818     * The number of token pairs must be evenly, eg there must be same number of before and after tokens, otherwise
819     * <tt>null</tt> is returned
820     * <p/>
821     * This implementation skips matching when the text is either single or double quoted. For example:
822     * <tt>${body.matches("foo('bar')")</tt> Will not match the parenthesis from the quoted text.
823     *
824     * @param  text   the text
825     * @param  after  the before token
826     * @param  before the after token
827     * @return        the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
828     */
829    public static String betweenOuterPair(String text, char before, char after) {
830        if (text == null) {
831            return null;
832        }
833
834        int pos = -1;
835        int pos2 = -1;
836        int count = 0;
837        int count2 = 0;
838
839        boolean singleQuoted = false;
840        boolean doubleQuoted = false;
841        for (int i = 0; i < text.length(); i++) {
842            char ch = text.charAt(i);
843            if (!doubleQuoted && ch == '\'') {
844                singleQuoted = !singleQuoted;
845            } else if (!singleQuoted && ch == '\"') {
846                doubleQuoted = !doubleQuoted;
847            }
848            if (singleQuoted || doubleQuoted) {
849                continue;
850            }
851
852            if (ch == before) {
853                count++;
854            } else if (ch == after) {
855                count2++;
856            }
857
858            if (ch == before && pos == -1) {
859                pos = i;
860            } else if (ch == after) {
861                pos2 = i;
862            }
863        }
864
865        if (pos == -1 || pos2 == -1) {
866            return null;
867        }
868
869        // must be even paris
870        if (count != count2) {
871            return null;
872        }
873
874        return text.substring(pos + 1, pos2);
875    }
876
877    /**
878     * Returns an object between the most outer pair of tokens
879     *
880     * @param  text   the text
881     * @param  after  the before token
882     * @param  before the after token
883     * @param  mapper a mapping function to convert the string between the most outer pair of tokens to type T
884     * @return        an Optional describing the result of applying a mapping function to the text between the most
885     *                outer pair of tokens.
886     */
887    public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
888        String result = betweenOuterPair(text, before, after);
889        if (result == null) {
890            return Optional.empty();
891        } else {
892            return Optional.ofNullable(mapper.apply(result));
893        }
894    }
895
896    /**
897     * Returns true if the given name is a valid java identifier
898     */
899    public static boolean isJavaIdentifier(String name) {
900        if (name == null) {
901            return false;
902        }
903        int size = name.length();
904        if (size < 1) {
905            return false;
906        }
907        if (Character.isJavaIdentifierStart(name.charAt(0))) {
908            for (int i = 1; i < size; i++) {
909                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
910                    return false;
911                }
912            }
913            return true;
914        }
915        return false;
916    }
917
918    /**
919     * Cleans the string to a pure Java identifier so we can use it for loading class names.
920     * <p/>
921     * Especially from Spring DSL people can have \n \t or other characters that otherwise would result in
922     * ClassNotFoundException
923     *
924     * @param  name the class name
925     * @return      normalized classname that can be load by a class loader.
926     */
927    public static String normalizeClassName(String name) {
928        StringBuilder sb = new StringBuilder(name.length());
929        for (char ch : name.toCharArray()) {
930            if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
931                sb.append(ch);
932            }
933        }
934        return sb.toString();
935    }
936
937    /**
938     * Compares old and new text content and report back which lines are changed
939     *
940     * @param  oldText the old text
941     * @param  newText the new text
942     * @return         a list of line numbers that are changed in the new text
943     */
944    public static List<Integer> changedLines(String oldText, String newText) {
945        if (oldText == null || oldText.equals(newText)) {
946            return Collections.emptyList();
947        }
948
949        List<Integer> changed = new ArrayList<>();
950
951        String[] oldLines = oldText.split("\n");
952        String[] newLines = newText.split("\n");
953
954        for (int i = 0; i < newLines.length; i++) {
955            String newLine = newLines[i];
956            String oldLine = i < oldLines.length ? oldLines[i] : null;
957            if (oldLine == null) {
958                changed.add(i);
959            } else if (!newLine.equals(oldLine)) {
960                changed.add(i);
961            }
962        }
963
964        return changed;
965    }
966
967    /**
968     * Removes the leading and trailing whitespace and if the resulting string is empty returns {@code null}. Examples:
969     * <p>
970     * Examples: <blockquote>
971     *
972     * <pre>
973     * trimToNull("abc") -> "abc"
974     * trimToNull(" abc") -> "abc"
975     * trimToNull(" abc ") -> "abc"
976     * trimToNull(" ") -> null
977     * trimToNull("") -> null
978     * </pre>
979     *
980     * </blockquote>
981     */
982    public static String trimToNull(final String given) {
983        if (given == null) {
984            return null;
985        }
986
987        final String trimmed = given.trim();
988
989        if (trimmed.isEmpty()) {
990            return null;
991        }
992
993        return trimmed;
994    }
995
996    /**
997     * Checks if the src string contains what
998     *
999     * @param  src  is the source string to be checked
1000     * @param  what is the string which will be looked up in the src argument
1001     * @return      true/false
1002     */
1003    public static boolean containsIgnoreCase(String src, String what) {
1004        if (src == null || what == null) {
1005            return false;
1006        }
1007
1008        final int length = what.length();
1009        if (length == 0) {
1010            return true; // Empty string is contained
1011        }
1012
1013        final char firstLo = Character.toLowerCase(what.charAt(0));
1014        final char firstUp = Character.toUpperCase(what.charAt(0));
1015
1016        for (int i = src.length() - length; i >= 0; i--) {
1017            // Quick check before calling the more expensive regionMatches() method:
1018            final char ch = src.charAt(i);
1019            if (ch != firstLo && ch != firstUp) {
1020                continue;
1021            }
1022
1023            if (src.regionMatches(true, i, what, 0, length)) {
1024                return true;
1025            }
1026        }
1027
1028        return false;
1029    }
1030
1031    /**
1032     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
1033     *
1034     * @param  locale The locale to apply during formatting. If l is {@code null} then no localization is applied.
1035     * @param  bytes  number of bytes
1036     * @return        human readable output
1037     * @see           java.lang.String#format(Locale, String, Object...)
1038     */
1039    public static String humanReadableBytes(Locale locale, long bytes) {
1040        int unit = 1024;
1041        if (bytes < unit) {
1042            return bytes + " B";
1043        }
1044        int exp = (int) (Math.log(bytes) / Math.log(unit));
1045        String pre = String.valueOf("KMGTPE".charAt(exp - 1));
1046        return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
1047    }
1048
1049    /**
1050     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
1051     *
1052     * The locale always used is the one returned by {@link java.util.Locale#getDefault()}.
1053     *
1054     * @param  bytes number of bytes
1055     * @return       human readable output
1056     * @see          org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long)
1057     */
1058    public static String humanReadableBytes(long bytes) {
1059        return humanReadableBytes(Locale.getDefault(), bytes);
1060    }
1061
1062    /**
1063     * Check for string pattern matching with a number of strategies in the following order:
1064     *
1065     * - equals - null pattern always matches - * always matches - Ant style matching - Regexp
1066     *
1067     * @param  pattern the pattern
1068     * @param  target  the string to test
1069     * @return         true if target matches the pattern
1070     */
1071    public static boolean matches(String pattern, String target) {
1072        if (Objects.equals(pattern, target)) {
1073            return true;
1074        }
1075
1076        if (Objects.isNull(pattern)) {
1077            return true;
1078        }
1079
1080        if (Objects.equals("*", pattern)) {
1081            return true;
1082        }
1083
1084        if (AntPathMatcher.INSTANCE.match(pattern, target)) {
1085            return true;
1086        }
1087
1088        Pattern p = Pattern.compile(pattern);
1089        Matcher m = p.matcher(target);
1090
1091        return m.matches();
1092    }
1093
1094    /**
1095     * Converts the string from camel case into dash format (helloGreatWorld -> hello-great-world)
1096     *
1097     * @param  text the string
1098     * @return      the string camel cased
1099     */
1100    public static String camelCaseToDash(String text) {
1101        if (text == null || text.isEmpty()) {
1102            return text;
1103        }
1104        StringBuilder answer = new StringBuilder();
1105
1106        Character prev = null;
1107        Character next = null;
1108        char[] arr = text.toCharArray();
1109        for (int i = 0; i < arr.length; i++) {
1110            char ch = arr[i];
1111            if (i < arr.length - 1) {
1112                next = arr[i + 1];
1113            } else {
1114                next = null;
1115            }
1116            if (ch == '-' || ch == '_') {
1117                answer.append("-");
1118            } else if (Character.isUpperCase(ch) && prev != null && !Character.isUpperCase(prev)) {
1119                applyDashPrefix(prev, answer, ch);
1120            } else if (Character.isUpperCase(ch) && prev != null && next != null && Character.isLowerCase(next)) {
1121                applyDashPrefix(prev, answer, ch);
1122            } else {
1123                answer.append(Character.toLowerCase(ch));
1124            }
1125            prev = ch;
1126        }
1127
1128        return answer.toString();
1129    }
1130
1131    private static void applyDashPrefix(Character prev, StringBuilder answer, char ch) {
1132        if (prev != '-' && prev != '_') {
1133            answer.append("-");
1134        }
1135        answer.append(Character.toLowerCase(ch));
1136    }
1137
1138    /**
1139     * Does the string starts with the given prefix (ignore case).
1140     *
1141     * @param text   the string
1142     * @param prefix the prefix
1143     */
1144    public static boolean startsWithIgnoreCase(String text, String prefix) {
1145        if (text != null && prefix != null) {
1146            return prefix.length() <= text.length() && text.regionMatches(true, 0, prefix, 0, prefix.length());
1147        } else {
1148            return text == null && prefix == null;
1149        }
1150    }
1151
1152    /**
1153     * Converts the value to an enum constant value that is in the form of upper cased with underscore.
1154     */
1155    public static String asEnumConstantValue(final String value) {
1156        if (value == null || value.isEmpty()) {
1157            return value;
1158        }
1159        String ret = StringHelper.camelCaseToDash(value);
1160        // replace double dashes
1161        ret = ret.replaceAll("-+", "-");
1162        // replace dash with underscore and upper case
1163        ret = ret.replace('-', '_').toUpperCase(Locale.ENGLISH);
1164        return ret;
1165    }
1166
1167    /**
1168     * Split the text on words, eg hello/world => becomes array with hello in index 0, and world in index 1.
1169     */
1170    public static String[] splitWords(String text) {
1171        return text.split("[\\W]+");
1172    }
1173
1174    /**
1175     * Creates a stream from the given input sequence around matches of the regex
1176     *
1177     * @param  text  the input
1178     * @param  regex the expression used to split the input
1179     * @return       the stream of strings computed by splitting the input with the given regex
1180     */
1181    public static Stream<String> splitAsStream(CharSequence text, String regex) {
1182        if (text == null || regex == null) {
1183            return Stream.empty();
1184        }
1185
1186        return Pattern.compile(regex).splitAsStream(text);
1187    }
1188
1189    /**
1190     * Returns the occurrence of a search string in to a string.
1191     *
1192     * @param  text   the text
1193     * @param  search the string to search
1194     * @return        an integer reporting the number of occurrence of the searched string in to the text
1195     */
1196    public static int countOccurrence(String text, String search) {
1197        int lastIndex = 0;
1198        int count = 0;
1199        while (lastIndex != -1) {
1200            lastIndex = text.indexOf(search, lastIndex);
1201            if (lastIndex != -1) {
1202                count++;
1203                lastIndex += search.length();
1204            }
1205        }
1206        return count;
1207    }
1208
1209    /**
1210     * Replaces a string in to a text starting from his second occurrence.
1211     *
1212     * @param  text        the text
1213     * @param  search      the string to search
1214     * @param  replacement the replacement for the string
1215     * @return             the string with the replacement
1216     */
1217    public static String replaceFromSecondOccurrence(String text, String search, String replacement) {
1218        int index = text.indexOf(search);
1219        boolean replace = false;
1220
1221        while (index != -1) {
1222            String tempString = text.substring(index);
1223            if (replace) {
1224                tempString = tempString.replaceFirst(search, replacement);
1225                text = text.substring(0, index) + tempString;
1226                replace = false;
1227            } else {
1228                replace = true;
1229            }
1230            index = text.indexOf(search, index + 1);
1231        }
1232        return text;
1233    }
1234
1235    /**
1236     * Pad the string with leading spaces
1237     *
1238     * @param level level (2 blanks per level)
1239     */
1240    public static String padString(int level) {
1241        return padString(level, 2);
1242    }
1243
1244    /**
1245     * Pad the string with leading spaces
1246     *
1247     * @param level  level
1248     * @param blanks number of blanks per level
1249     */
1250    public static String padString(int level, int blanks) {
1251        if (level == 0) {
1252            return "";
1253        } else {
1254            return " ".repeat(level * blanks);
1255        }
1256    }
1257
1258    /**
1259     * Fills the string with repeating chars
1260     *
1261     * @param ch    the char
1262     * @param count number of chars
1263     */
1264    public static String fillChars(char ch, int count) {
1265        if (count <= 0) {
1266            return "";
1267        } else {
1268            return Character.toString(ch).repeat(count);
1269        }
1270    }
1271
1272    public static boolean isDigit(String s) {
1273        for (char ch : s.toCharArray()) {
1274            if (!Character.isDigit(ch)) {
1275                return false;
1276            }
1277        }
1278        return true;
1279    }
1280
1281    public static String bytesToHex(byte[] hash) {
1282        StringBuilder sb = new StringBuilder(2 * hash.length);
1283        for (byte b : hash) {
1284            String hex = Integer.toHexString(0xff & b);
1285            if (hex.length() == 1) {
1286                sb.append('0');
1287            }
1288            sb.append(hex);
1289        }
1290        return sb.toString();
1291    }
1292
1293}