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