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.List;
022import java.util.Locale;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.function.Function;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029/**
030 * Helper methods for working with Strings.
031 */
032public final class StringHelper {
033
034    /**
035     * Constructor of utility class should be private.
036     */
037    private StringHelper() {
038    }
039
040    /**
041     * Ensures that <code>s</code> is friendly for a URL or file system.
042     *
043     * @param s String to be sanitized.
044     * @return sanitized version of <code>s</code>.
045     * @throws NullPointerException if <code>s</code> is <code>null</code>.
046     */
047    public static String sanitize(String s) {
048        return s
049            .replace(':', '-')
050            .replace('_', '-')
051            .replace('.', '-')
052            .replace('/', '-')
053            .replace('\\', '-');
054    }
055
056    /**
057     * Remove carriage return and line feeds from a String, replacing them with an empty String.
058     * @param s String to be sanitized of carriage return / line feed characters
059     * @return sanitized version of <code>s</code>.
060     * @throws NullPointerException if <code>s</code> is <code>null</code>.
061     */
062    public static String removeCRLF(String s) {
063        return s
064            .replace("\r", "")
065            .replace("\n", "");
066    }
067
068    /**
069     * Counts the number of times the given char is in the string
070     *
071     * @param s  the string
072     * @param ch the char
073     * @return number of times char is located in the string
074     */
075    public static int countChar(String s, char ch) {
076        if (ObjectHelper.isEmpty(s)) {
077            return 0;
078        }
079
080        int matches = 0;
081        for (int i = 0; i < s.length(); i++) {
082            char c = s.charAt(i);
083            if (ch == c) {
084                matches++;
085            }
086        }
087
088        return matches;
089    }
090
091    /**
092     * Limits the length of a string
093     *
094     * @param s the string
095     * @param maxLength the maximum length of the returned string
096     * @return s if the length of s is less than maxLength or the first maxLength characters of s
097     */
098    public static String limitLength(String s, int maxLength) {
099        if (ObjectHelper.isEmpty(s)) {
100            return s;
101        }
102        return s.length() <= maxLength ? s : s.substring(0, maxLength);
103    }
104
105    /**
106     * Removes all quotes (single and double) from the string
107     *
108     * @param s  the string
109     * @return the string without quotes (single and double)
110     */
111    public static String removeQuotes(String s) {
112        if (ObjectHelper.isEmpty(s)) {
113            return s;
114        }
115
116        s = replaceAll(s, "'", "");
117        s = replaceAll(s, "\"", "");
118        return s;
119    }
120
121    /**
122     * Removes all leading and ending quotes (single and double) from the string
123     *
124     * @param s  the string
125     * @return the string without leading and ending quotes (single and double)
126     */
127    public static String removeLeadingAndEndingQuotes(String s) {
128        if (ObjectHelper.isEmpty(s)) {
129            return s;
130        }
131
132        String copy = s.trim();
133        if (copy.startsWith("'") && copy.endsWith("'")) {
134            return copy.substring(1, copy.length() - 1);
135        }
136        if (copy.startsWith("\"") && copy.endsWith("\"")) {
137            return copy.substring(1, copy.length() - 1);
138        }
139
140        // no quotes, so return as-is
141        return s;
142    }
143
144    /**
145     * Whether the string starts and ends with either single or double quotes.
146     *
147     * @param s the string
148     * @return <tt>true</tt> if the string starts and ends with either single or double quotes.
149     */
150    public static boolean isQuoted(String s) {
151        if (ObjectHelper.isEmpty(s)) {
152            return false;
153        }
154
155        if (s.startsWith("'") && s.endsWith("'")) {
156            return true;
157        }
158        if (s.startsWith("\"") && s.endsWith("\"")) {
159            return true;
160        }
161
162        return false;
163    }
164
165    /**
166     * Encodes the text into safe XML by replacing < > and & with XML tokens
167     *
168     * @param text  the text
169     * @return the encoded text
170     */
171    public static String xmlEncode(String text) {
172        if (text == null) {
173            return "";
174        }
175        // must replace amp first, so we dont replace &lt; to amp later
176        text = replaceAll(text, "&", "&amp;");
177        text = replaceAll(text, "\"", "&quot;");
178        text = replaceAll(text, "<", "&lt;");
179        text = replaceAll(text, ">", "&gt;");
180        return text;
181    }
182
183    /**
184     * Determines if the string has at least one letter in upper case
185     * @param text the text
186     * @return <tt>true</tt> if at least one letter is upper case, <tt>false</tt> otherwise
187     */
188    public static boolean hasUpperCase(String text) {
189        if (text == null) {
190            return false;
191        }
192
193        for (int i = 0; i < text.length(); i++) {
194            char ch = text.charAt(i);
195            if (Character.isUpperCase(ch)) {
196                return true;
197            }
198        }
199
200        return false;
201    }
202
203    /**
204     * Determines if the string is a fully qualified class name
205     */
206    public static boolean isClassName(String text) {
207        boolean result = false;
208        if (text != null) {
209            String[] split = text.split("\\.");
210            if (split.length > 0) {
211                String lastToken = split[split.length - 1];
212                if (lastToken.length() > 0) {
213                    result = Character.isUpperCase(lastToken.charAt(0));
214                }
215            }
216        }
217        return result;
218    }
219
220    /**
221     * Does the expression have the language start token?
222     *
223     * @param expression the expression
224     * @param language the name of the language, such as simple
225     * @return <tt>true</tt> if the expression contains the start token, <tt>false</tt> otherwise
226     */
227    public static boolean hasStartToken(String expression, String language) {
228        if (expression == null) {
229            return false;
230        }
231
232        // for the simple language the expression start token could be "${"
233        if ("simple".equalsIgnoreCase(language) && expression.contains("${")) {
234            return true;
235        }
236
237        if (language != null && expression.contains("$" + language + "{")) {
238            return true;
239        }
240
241        return false;
242    }
243
244    /**
245     * Replaces all the from tokens in the given input string.
246     * <p/>
247     * This implementation is not recursive, not does it check for tokens in the replacement string.
248     *
249     * @param input  the input string
250     * @param from   the from string, must <b>not</b> be <tt>null</tt> or empty
251     * @param to     the replacement string, must <b>not</b> be empty
252     * @return the replaced string, or the input string if no replacement was needed
253     * @throws IllegalArgumentException if the input arguments is invalid
254     */
255    public static String replaceAll(String input, String from, String to) {
256        // TODO: Use String.replace instead of this method when using JDK11 as minimum (as its much faster in JDK 11 onwards)
257
258        if (ObjectHelper.isEmpty(input)) {
259            return input;
260        }
261        if (from == null) {
262            throw new IllegalArgumentException("from cannot be null");
263        }
264        if (to == null) {
265            // to can be empty, so only check for null
266            throw new IllegalArgumentException("to cannot be null");
267        }
268
269        // fast check if there is any from at all
270        if (!input.contains(from)) {
271            return input;
272        }
273
274        final int len = from.length();
275        final int max = input.length();
276        StringBuilder sb = new StringBuilder(max);
277        for (int i = 0; i < max;) {
278            if (i + len <= max) {
279                String token = input.substring(i, i + len);
280                if (from.equals(token)) {
281                    sb.append(to);
282                    // fast forward
283                    i = i + len;
284                    continue;
285                }
286            }
287
288            // append single char
289            sb.append(input.charAt(i));
290            // forward to next
291            i++;
292        }
293        return sb.toString();
294    }
295
296    /**
297     * Creates a json tuple with the given name/value pair.
298     *
299     * @param name  the name
300     * @param value the value
301     * @param isMap whether the tuple should be map
302     * @return the json
303     */
304    public static String toJson(String name, String value, boolean isMap) {
305        if (isMap) {
306            return "{ " + StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value) + " }";
307        } else {
308            return StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value);
309        }
310    }
311
312    /**
313     * Asserts whether the string is <b>not</b> empty.
314     *
315     * @param value  the string to test
316     * @param name   the key that resolved the value
317     * @return the passed {@code value} as is
318     * @throws IllegalArgumentException is thrown if assertion fails
319     */
320    public static String notEmpty(String value, String name) {
321        if (ObjectHelper.isEmpty(value)) {
322            throw new IllegalArgumentException(name + " must be specified and not empty");
323        }
324
325        return value;
326    }
327
328    /**
329     * Asserts whether the string is <b>not</b> empty.
330     *
331     * @param value  the string to test
332     * @param on     additional description to indicate where this problem occurred (appended as toString())
333     * @param name   the key that resolved the value
334     * @return the passed {@code value} as is
335     * @throws IllegalArgumentException is thrown if assertion fails
336     */
337    public static String notEmpty(String value, String name, Object on) {
338        if (on == null) {
339            ObjectHelper.notNull(value, name);
340        } else if (ObjectHelper.isEmpty(value)) {
341            throw new IllegalArgumentException(name + " must be specified and not empty on: " + on);
342        }
343
344        return value;
345    }
346    
347    public static String[] splitOnCharacter(String value, String needle, int count) {
348        String[] rc = new String[count];
349        rc[0] = value;
350        for (int i = 1; i < count; i++) {
351            String v = rc[i - 1];
352            int p = v.indexOf(needle);
353            if (p < 0) {
354                return rc;
355            }
356            rc[i - 1] = v.substring(0, p);
357            rc[i] = v.substring(p + 1);
358        }
359        return rc;
360    }
361
362    /**
363     * Removes any starting characters on the given text which match the given
364     * character
365     *
366     * @param text the string
367     * @param ch the initial characters to remove
368     * @return either the original string or the new substring
369     */
370    public static String removeStartingCharacters(String text, char ch) {
371        int idx = 0;
372        while (text.charAt(idx) == ch) {
373            idx++;
374        }
375        if (idx > 0) {
376            return text.substring(idx);
377        }
378        return text;
379    }
380
381    /**
382     * Capitalize the string (upper case first character)
383     *
384     * @param text  the string
385     * @return the string capitalized (upper case first character)
386     */
387    public static String capitalize(String text) {
388        return capitalize(text, false);
389    }
390
391    /**
392     * Capitalize the string (upper case first character)
393     *
394     * @param text  the string
395     * @param dashToCamelCase whether to also convert dash format into camel case (hello-great-world -> helloGreatWorld)
396     * @return the string capitalized (upper case first character)
397     */
398    public static String capitalize(String text, boolean dashToCamelCase) {
399        if (dashToCamelCase) {
400            text = dashToCamelCase(text);
401        }
402        if (text == null) {
403            return null;
404        }
405        int length = text.length();
406        if (length == 0) {
407            return text;
408        }
409        String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH);
410        if (length > 1) {
411            answer += text.substring(1, length);
412        }
413        return answer;
414    }
415
416    /**
417     * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld)
418     *
419     * @param text  the string
420     * @return the string camel cased
421     */
422    public static String dashToCamelCase(String text) {
423        if (text == null) {
424            return null;
425        }
426        int length = text.length();
427        if (length == 0) {
428            return text;
429        }
430        if (text.indexOf('-') == -1) {
431            return text;
432        }
433
434        StringBuilder sb = new StringBuilder();
435
436        for (int i = 0; i < text.length(); i++) {
437            char c = text.charAt(i);
438            if (c == '-') {
439                i++;
440                sb.append(Character.toUpperCase(text.charAt(i)));
441            } else {
442                sb.append(c);
443            }
444        }
445        return sb.toString();
446    }
447
448    /**
449     * Returns the string after the given token
450     *
451     * @param text  the text
452     * @param after the token
453     * @return the text after the token, or <tt>null</tt> if text does not contain the token
454     */
455    public static String after(String text, String after) {
456        if (!text.contains(after)) {
457            return null;
458        }
459        return text.substring(text.indexOf(after) + after.length());
460    }
461
462    /**
463     * Returns an object after the given token
464     *
465     * @param text  the text
466     * @param after the token
467     * @param mapper a mapping function to convert the string after the token to type T
468     * @return an Optional describing the result of applying a mapping function to the text after the token.
469     */
470    public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
471        String result = after(text, after);
472        if (result == null) {
473            return Optional.empty();
474        } else {
475            return Optional.ofNullable(mapper.apply(result));
476        }
477    }
478
479    /**
480     * Returns the string before the given token
481     *
482     * @param text the text
483     * @param before the token
484     * @return the text before the token, or <tt>null</tt> if text does not
485     *         contain the token
486     */
487    public static String before(String text, String before) {
488        if (!text.contains(before)) {
489            return null;
490        }
491        return text.substring(0, text.indexOf(before));
492    }
493
494    /**
495     * Returns an object before the given token
496     *
497     * @param text  the text
498     * @param before the token
499     * @param mapper a mapping function to convert the string before the token to type T
500     * @return an Optional describing the result of applying a mapping function to the text before the token.
501     */
502    public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
503        String result = before(text, before);
504        if (result == null) {
505            return Optional.empty();
506        } else {
507            return Optional.ofNullable(mapper.apply(result));
508        }
509    }
510
511    /**
512     * Returns the string between the given tokens
513     *
514     * @param text  the text
515     * @param after the before token
516     * @param before the after token
517     * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens
518     */
519    public static String between(String text, String after, String before) {
520        text = after(text, after);
521        if (text == null) {
522            return null;
523        }
524        return before(text, before);
525    }
526
527    /**
528     * Returns an object between the given token
529     *
530     * @param text  the text
531     * @param after the before token
532     * @param before the after token
533     * @param mapper a mapping function to convert the string between the token to type T
534     * @return an Optional describing the result of applying a mapping function to the text between the token.
535     */
536    public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
537        String result = between(text, after, before);
538        if (result == null) {
539            return Optional.empty();
540        } else {
541            return Optional.ofNullable(mapper.apply(result));
542        }
543    }
544
545    /**
546     * Returns the string between the most outer pair of tokens
547     * <p/>
548     * The number of token pairs must be evenly, eg there must be same number of before and after tokens, otherwise <tt>null</tt> is returned
549     * <p/>
550     * This implementation skips matching when the text is either single or double quoted.
551     * For example:
552     * <tt>${body.matches("foo('bar')")</tt>
553     * Will not match the parenthesis from the quoted text.
554     *
555     * @param text  the text
556     * @param after the before token
557     * @param before the after token
558     * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
559     */
560    public static String betweenOuterPair(String text, char before, char after) {
561        if (text == null) {
562            return null;
563        }
564
565        int pos = -1;
566        int pos2 = -1;
567        int count = 0;
568        int count2 = 0;
569
570        boolean singleQuoted = false;
571        boolean doubleQuoted = false;
572        for (int i = 0; i < text.length(); i++) {
573            char ch = text.charAt(i);
574            if (!doubleQuoted && ch == '\'') {
575                singleQuoted = !singleQuoted;
576            } else if (!singleQuoted && ch == '\"') {
577                doubleQuoted = !doubleQuoted;
578            }
579            if (singleQuoted || doubleQuoted) {
580                continue;
581            }
582
583            if (ch == before) {
584                count++;
585            } else if (ch == after) {
586                count2++;
587            }
588
589            if (ch == before && pos == -1) {
590                pos = i;
591            } else if (ch == after) {
592                pos2 = i;
593            }
594        }
595
596        if (pos == -1 || pos2 == -1) {
597            return null;
598        }
599
600        // must be even paris
601        if (count != count2) {
602            return null;
603        }
604
605        return text.substring(pos + 1, pos2);
606    }
607
608    /**
609     * Returns an object between the most outer pair of tokens
610     *
611     * @param text  the text
612     * @param after the before token
613     * @param before the after token
614     * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T
615     * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens.
616     */
617    public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
618        String result = betweenOuterPair(text, before, after);
619        if (result == null) {
620            return Optional.empty();
621        } else {
622            return Optional.ofNullable(mapper.apply(result));
623        }
624    }
625
626    /**
627     * Returns true if the given name is a valid java identifier
628     */
629    public static boolean isJavaIdentifier(String name) {
630        if (name == null) {
631            return false;
632        }
633        int size = name.length();
634        if (size < 1) {
635            return false;
636        }
637        if (Character.isJavaIdentifierStart(name.charAt(0))) {
638            for (int i = 1; i < size; i++) {
639                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
640                    return false;
641                }
642            }
643            return true;
644        }
645        return false;
646    }
647
648    /**
649     * Cleans the string to a pure Java identifier so we can use it for loading class names.
650     * <p/>
651     * Especially from Spring DSL people can have \n \t or other characters that otherwise
652     * would result in ClassNotFoundException
653     *
654     * @param name the class name
655     * @return normalized classname that can be load by a class loader.
656     */
657    public static String normalizeClassName(String name) {
658        StringBuilder sb = new StringBuilder(name.length());
659        for (char ch : name.toCharArray()) {
660            if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
661                sb.append(ch);
662            }
663        }
664        return sb.toString();
665    }
666
667    /**
668     * Compares old and new text content and report back which lines are changed
669     *
670     * @param oldText  the old text
671     * @param newText  the new text
672     * @return a list of line numbers that are changed in the new text
673     */
674    public static List<Integer> changedLines(String oldText, String newText) {
675        if (oldText == null || oldText.equals(newText)) {
676            return Collections.emptyList();
677        }
678
679        List<Integer> changed = new ArrayList<>();
680
681        String[] oldLines = oldText.split("\n");
682        String[] newLines = newText.split("\n");
683
684        for (int i = 0; i < newLines.length; i++) {
685            String newLine = newLines[i];
686            String oldLine = i < oldLines.length ? oldLines[i] : null;
687            if (oldLine == null) {
688                changed.add(i);
689            } else if (!newLine.equals(oldLine)) {
690                changed.add(i);
691            }
692        }
693
694        return changed;
695    }
696
697    /**
698     * Removes the leading and trailing whitespace and if the resulting
699     * string is empty returns {@code null}. Examples:
700     * <p>
701     * Examples:
702     * <blockquote><pre>
703     * trimToNull("abc") -> "abc"
704     * trimToNull(" abc") -> "abc"
705     * trimToNull(" abc ") -> "abc"
706     * trimToNull(" ") -> null
707     * trimToNull("") -> null
708     * </pre></blockquote>
709     */
710    public static String trimToNull(final String given) {
711        if (given == null) {
712            return null;
713        }
714
715        final String trimmed = given.trim();
716
717        if (trimmed.isEmpty()) {
718            return null;
719        }
720
721        return trimmed;
722    }
723    
724    /**
725     * Checks if the src string contains what
726     *
727     * @param src  is the source string to be checked
728     * @param what is the string which will be looked up in the src argument 
729     * @return true/false
730     */
731    public static boolean containsIgnoreCase(String src, String what) {
732        if (src == null || what == null) {
733            return false;
734        }
735        
736        final int length = what.length();
737        if (length == 0) {
738            return true; // Empty string is contained
739        }
740
741        final char firstLo = Character.toLowerCase(what.charAt(0));
742        final char firstUp = Character.toUpperCase(what.charAt(0));
743
744        for (int i = src.length() - length; i >= 0; i--) {
745            // Quick check before calling the more expensive regionMatches() method:
746            final char ch = src.charAt(i);
747            if (ch != firstLo && ch != firstUp) {
748                continue;
749            }
750
751            if (src.regionMatches(true, i, what, 0, length)) {
752                return true;
753            }
754        }
755
756        return false;
757    }
758
759    /**
760     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
761     *
762     * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied.
763     * @param bytes number of bytes
764     * @return human readable output
765     * @see java.lang.String#format(Locale, String, Object...)
766     */
767    public static String humanReadableBytes(Locale locale, long bytes) {
768        int unit = 1024;
769        if (bytes < unit) {
770            return bytes + " B";
771        }
772        int exp = (int) (Math.log(bytes) / Math.log(unit));
773        String pre = "KMGTPE".charAt(exp - 1) + "";
774        return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
775    }
776
777    /**
778     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
779     *
780     * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 
781     *
782     * @param bytes number of bytes
783     * @return human readable output
784     * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long)
785     */
786    public static String humanReadableBytes(long bytes) {
787        return humanReadableBytes(Locale.getDefault(), bytes);
788    }
789
790    /**
791     * Check for string pattern matching with a number of strategies in the
792     * following order:
793     *
794     * - equals
795     * - null pattern always matches
796     * - * always matches
797     * - Ant style matching
798     * - Regexp
799     *
800     * @param pattern the pattern
801     * @param target the string to test
802     * @return true if target matches the pattern
803     */
804    public static boolean matches(String pattern, String target) {
805        if (Objects.equals(pattern, target)) {
806            return true;
807        }
808
809        if (Objects.isNull(pattern)) {
810            return true;
811        }
812
813        if (Objects.equals("*", pattern)) {
814            return true;
815        }
816
817        if (AntPathMatcher.INSTANCE.match(pattern, target)) {
818            return true;
819        }
820
821        Pattern p = Pattern.compile(pattern);
822        Matcher m = p.matcher(target);
823
824        return m.matches();
825    }
826
827    public static String camelCaseToDash(String text) {
828        StringBuilder answer = new StringBuilder();
829
830        Character prev = null;
831        Character next = null;
832        char[] arr = text.toCharArray();
833        for (int i = 0; i < arr.length; i++) {
834            char ch = arr[i];
835            if (i < arr.length - 1) {
836                next = arr[i + 1];
837            } else {
838                next = null;
839            }
840            if (ch == '-' || ch == '_') {
841                answer.append("-");
842            } else if (Character.isUpperCase(ch) && prev != null && !Character.isUpperCase(prev)) {
843                answer.append("-").append(ch);
844            } else if (Character.isUpperCase(ch) && prev != null && next != null && Character.isLowerCase(next)) {
845                answer.append("-").append(ch);
846            } else {
847                answer.append(ch);
848            }
849            prev = ch;
850        }
851        
852        return answer.toString().toLowerCase(Locale.US);
853    }
854
855}