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