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