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