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