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        if (text == null) {
390            return null;
391        }
392        int length = text.length();
393        if (length == 0) {
394            return text;
395        }
396        String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH);
397        if (length > 1) {
398            answer += text.substring(1, length);
399        }
400        return answer;
401    }
402
403    /**
404     * Returns the string after the given token
405     *
406     * @param text  the text
407     * @param after the token
408     * @return the text after the token, or <tt>null</tt> if text does not contain the token
409     */
410    public static String after(String text, String after) {
411        if (!text.contains(after)) {
412            return null;
413        }
414        return text.substring(text.indexOf(after) + after.length());
415    }
416
417    /**
418     * Returns an object after the given token
419     *
420     * @param text  the text
421     * @param after the token
422     * @param mapper a mapping function to convert the string after the token to type T
423     * @return an Optional describing the result of applying a mapping function to the text after the token.
424     */
425    public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
426        String result = after(text, after);
427        if (result == null) {
428            return Optional.empty();            
429        } else {
430            return Optional.ofNullable(mapper.apply(result));
431        }
432    }
433
434    /**
435     * Returns the string before the given token
436     *
437     * @param text the text
438     * @param before the token
439     * @return the text before the token, or <tt>null</tt> if text does not
440     *         contain the token
441     */
442    public static String before(String text, String before) {
443        if (!text.contains(before)) {
444            return null;
445        }
446        return text.substring(0, text.indexOf(before));
447    }
448
449    /**
450     * Returns an object before the given token
451     *
452     * @param text  the text
453     * @param before the token
454     * @param mapper a mapping function to convert the string before the token to type T
455     * @return an Optional describing the result of applying a mapping function to the text before the token.
456     */
457    public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
458        String result = before(text, before);
459        if (result == null) {
460            return Optional.empty();            
461        } else {
462            return Optional.ofNullable(mapper.apply(result));
463        }
464    }
465
466    /**
467     * Returns the string between the given tokens
468     *
469     * @param text  the text
470     * @param after the before token
471     * @param before the after token
472     * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens
473     */
474    public static String between(String text, String after, String before) {
475        text = after(text, after);
476        if (text == null) {
477            return null;
478        }
479        return before(text, before);
480    }
481
482    /**
483     * Returns an object between the given token
484     *
485     * @param text  the text
486     * @param after the before token
487     * @param before the after token
488     * @param mapper a mapping function to convert the string between the token to type T
489     * @return an Optional describing the result of applying a mapping function to the text between the token.
490     */
491    public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
492        String result = between(text, after, before);
493        if (result == null) {
494            return Optional.empty();            
495        } else {
496            return Optional.ofNullable(mapper.apply(result));
497        }
498    }
499
500    /**
501     * Returns the string between the most outer pair of tokens
502     * <p/>
503     * 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
504     * <p/>
505     * This implementation skips matching when the text is either single or double quoted.
506     * For example:
507     * <tt>${body.matches("foo('bar')")</tt>
508     * Will not match the parenthesis from the quoted text.
509     *
510     * @param text  the text
511     * @param after the before token
512     * @param before the after token
513     * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
514     */
515    public static String betweenOuterPair(String text, char before, char after) {
516        if (text == null) {
517            return null;
518        }
519
520        int pos = -1;
521        int pos2 = -1;
522        int count = 0;
523        int count2 = 0;
524
525        boolean singleQuoted = false;
526        boolean doubleQuoted = false;
527        for (int i = 0; i < text.length(); i++) {
528            char ch = text.charAt(i);
529            if (!doubleQuoted && ch == '\'') {
530                singleQuoted = !singleQuoted;
531            } else if (!singleQuoted && ch == '\"') {
532                doubleQuoted = !doubleQuoted;
533            }
534            if (singleQuoted || doubleQuoted) {
535                continue;
536            }
537
538            if (ch == before) {
539                count++;
540            } else if (ch == after) {
541                count2++;
542            }
543
544            if (ch == before && pos == -1) {
545                pos = i;
546            } else if (ch == after) {
547                pos2 = i;
548            }
549        }
550
551        if (pos == -1 || pos2 == -1) {
552            return null;
553        }
554
555        // must be even paris
556        if (count != count2) {
557            return null;
558        }
559
560        return text.substring(pos + 1, pos2);
561    }
562
563    /**
564     * Returns an object between the most outer pair of tokens
565     *
566     * @param text  the text
567     * @param after the before token
568     * @param before the after token
569     * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T
570     * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens.
571     */
572    public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
573        String result = betweenOuterPair(text, before, after);
574        if (result == null) {
575            return Optional.empty();            
576        } else {
577            return Optional.ofNullable(mapper.apply(result));
578        }
579    }
580
581    /**
582     * Returns true if the given name is a valid java identifier
583     */
584    public static boolean isJavaIdentifier(String name) {
585        if (name == null) {
586            return false;
587        }
588        int size = name.length();
589        if (size < 1) {
590            return false;
591        }
592        if (Character.isJavaIdentifierStart(name.charAt(0))) {
593            for (int i = 1; i < size; i++) {
594                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
595                    return false;
596                }
597            }
598            return true;
599        }
600        return false;
601    }
602
603    /**
604     * Cleans the string to a pure Java identifier so we can use it for loading class names.
605     * <p/>
606     * Especially from Spring DSL people can have \n \t or other characters that otherwise
607     * would result in ClassNotFoundException
608     *
609     * @param name the class name
610     * @return normalized classname that can be load by a class loader.
611     */
612    public static String normalizeClassName(String name) {
613        StringBuilder sb = new StringBuilder(name.length());
614        for (char ch : name.toCharArray()) {
615            if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
616                sb.append(ch);
617            }
618        }
619        return sb.toString();
620    }
621
622    /**
623     * Compares old and new text content and report back which lines are changed
624     *
625     * @param oldText  the old text
626     * @param newText  the new text
627     * @return a list of line numbers that are changed in the new text
628     */
629    public static List<Integer> changedLines(String oldText, String newText) {
630        if (oldText == null || oldText.equals(newText)) {
631            return Collections.emptyList();
632        }
633
634        List<Integer> changed = new ArrayList<>();
635
636        String[] oldLines = oldText.split("\n");
637        String[] newLines = newText.split("\n");
638
639        for (int i = 0; i < newLines.length; i++) {
640            String newLine = newLines[i];
641            String oldLine = i < oldLines.length ? oldLines[i] : null;
642            if (oldLine == null) {
643                changed.add(i);
644            } else if (!newLine.equals(oldLine)) {
645                changed.add(i);
646            }
647        }
648
649        return changed;
650    }
651
652    /**
653     * Removes the leading and trailing whitespace and if the resulting
654     * string is empty returns {@code null}. Examples:
655     * <p>
656     * Examples:
657     * <blockquote><pre>
658     * trimToNull("abc") -> "abc"
659     * trimToNull(" abc") -> "abc"
660     * trimToNull(" abc ") -> "abc"
661     * trimToNull(" ") -> null
662     * trimToNull("") -> null
663     * </pre></blockquote>
664     */
665    public static String trimToNull(final String given) {
666        if (given == null) {
667            return null;
668        }
669
670        final String trimmed = given.trim();
671
672        if (trimmed.isEmpty()) {
673            return null;
674        }
675
676        return trimmed;
677    }
678    
679    /**
680     * Checks if the src string contains what
681     *
682     * @param src  is the source string to be checked
683     * @param what is the string which will be looked up in the src argument 
684     * @return true/false
685     */
686    public static boolean containsIgnoreCase(String src, String what) {
687        if (src == null || what == null) {
688            return false;
689        }
690        
691        final int length = what.length();
692        if (length == 0) {
693            return true; // Empty string is contained
694        }
695
696        final char firstLo = Character.toLowerCase(what.charAt(0));
697        final char firstUp = Character.toUpperCase(what.charAt(0));
698
699        for (int i = src.length() - length; i >= 0; i--) {
700            // Quick check before calling the more expensive regionMatches() method:
701            final char ch = src.charAt(i);
702            if (ch != firstLo && ch != firstUp) {
703                continue;
704            }
705
706            if (src.regionMatches(true, i, what, 0, length)) {
707                return true;
708            }
709        }
710
711        return false;
712    }
713
714    /**
715     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
716     *
717     * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied.
718     * @param bytes number of bytes
719     * @return human readable output
720     * @see java.lang.String#format(Locale, String, Object...)
721     */
722    public static String humanReadableBytes(Locale locale, long bytes) {
723        int unit = 1024;
724        if (bytes < unit) {
725            return bytes + " B";
726        }
727        int exp = (int) (Math.log(bytes) / Math.log(unit));
728        String pre = "KMGTPE".charAt(exp - 1) + "";
729        return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
730    }
731
732    /**
733     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
734     *
735     * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 
736     *
737     * @param bytes number of bytes
738     * @return human readable output
739     * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long)
740     */
741    public static String humanReadableBytes(long bytes) {
742        return humanReadableBytes(Locale.getDefault(), bytes);
743    }
744
745    /**
746     * Check for string pattern matching with a number of strategies in the
747     * following order:
748     *
749     * - equals
750     * - null pattern always matches
751     * - * always matches
752     * - Ant style matching
753     * - Regexp
754     *
755     * @param patter the pattern
756     * @param target the string to test
757     * @return true if target matches the pattern
758     */
759    public static boolean matches(String patter, String target) {
760        if (Objects.equals(patter, target)) {
761            return true;
762        }
763
764        if (Objects.isNull(patter)) {
765            return true;
766        }
767
768        if (Objects.equals("*", patter)) {
769            return true;
770        }
771
772        if (AntPathMatcher.INSTANCE.match(patter, target)) {
773            return true;
774        }
775
776        Pattern p = Pattern.compile(patter);
777        Matcher m = p.matcher(target);
778
779        return m.matches();
780    }
781
782}