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