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