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