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