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