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