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            .replaceAll("\r", "")
065            .replaceAll("\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        if (ObjectHelper.isEmpty(input)) {
257            return input;
258        }
259        if (from == null) {
260            throw new IllegalArgumentException("from cannot be null");
261        }
262        if (to == null) {
263            // to can be empty, so only check for null
264            throw new IllegalArgumentException("to cannot be null");
265        }
266
267        // fast check if there is any from at all
268        if (!input.contains(from)) {
269            return input;
270        }
271
272        final int len = from.length();
273        final int max = input.length();
274        StringBuilder sb = new StringBuilder(max);
275        for (int i = 0; i < max;) {
276            if (i + len <= max) {
277                String token = input.substring(i, i + len);
278                if (from.equals(token)) {
279                    sb.append(to);
280                    // fast forward
281                    i = i + len;
282                    continue;
283                }
284            }
285
286            // append single char
287            sb.append(input.charAt(i));
288            // forward to next
289            i++;
290        }
291        return sb.toString();
292    }
293
294    /**
295     * Creates a json tuple with the given name/value pair.
296     *
297     * @param name  the name
298     * @param value the value
299     * @param isMap whether the tuple should be map
300     * @return the json
301     */
302    public static String toJson(String name, String value, boolean isMap) {
303        if (isMap) {
304            return "{ " + StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value) + " }";
305        } else {
306            return StringQuoteHelper.doubleQuote(name) + ": " + StringQuoteHelper.doubleQuote(value);
307        }
308    }
309
310    /**
311     * Asserts whether the string is <b>not</b> empty.
312     *
313     * @param value  the string to test
314     * @param name   the key that resolved the value
315     * @return the passed {@code value} as is
316     * @throws IllegalArgumentException is thrown if assertion fails
317     */
318    public static String notEmpty(String value, String name) {
319        if (ObjectHelper.isEmpty(value)) {
320            throw new IllegalArgumentException(name + " must be specified and not empty");
321        }
322
323        return value;
324    }
325
326    /**
327     * Asserts whether the string is <b>not</b> empty.
328     *
329     * @param value  the string to test
330     * @param on     additional description to indicate where this problem occurred (appended as toString())
331     * @param name   the key that resolved the value
332     * @return the passed {@code value} as is
333     * @throws IllegalArgumentException is thrown if assertion fails
334     */
335    public static String notEmpty(String value, String name, Object on) {
336        if (on == null) {
337            ObjectHelper.notNull(value, name);
338        } else if (ObjectHelper.isEmpty(value)) {
339            throw new IllegalArgumentException(name + " must be specified and not empty on: " + on);
340        }
341
342        return value;
343    }
344    
345    public static String[] splitOnCharacter(String value, String needle, int count) {
346        String rc[] = new String[count];
347        rc[0] = value;
348        for (int i = 1; i < count; i++) {
349            String v = rc[i - 1];
350            int p = v.indexOf(needle);
351            if (p < 0) {
352                return rc;
353            }
354            rc[i - 1] = v.substring(0, p);
355            rc[i] = v.substring(p + 1);
356        }
357        return rc;
358    }
359
360    /**
361     * Removes any starting characters on the given text which match the given
362     * character
363     *
364     * @param text the string
365     * @param ch the initial characters to remove
366     * @return either the original string or the new substring
367     */
368    public static String removeStartingCharacters(String text, char ch) {
369        int idx = 0;
370        while (text.charAt(idx) == ch) {
371            idx++;
372        }
373        if (idx > 0) {
374            return text.substring(idx);
375        }
376        return text;
377    }
378
379    /**
380     * Capitalize the string (upper case first character)
381     *
382     * @param text  the string
383     * @return the string capitalized (upper case first character)
384     */
385    public static String capitalize(String text) {
386        return capitalize(text, false);
387    }
388
389    /**
390     * Capitalize the string (upper case first character)
391     *
392     * @param text  the string
393     * @param dashToCamelCase whether to also convert dash format into camel case (hello-great-world -> helloGreatWorld)
394     * @return the string capitalized (upper case first character)
395     */
396    public static String capitalize(String text, boolean dashToCamelCase) {
397        if (dashToCamelCase) {
398            text = dashToCamelCase(text);
399        }
400        if (text == null) {
401            return null;
402        }
403        int length = text.length();
404        if (length == 0) {
405            return text;
406        }
407        String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH);
408        if (length > 1) {
409            answer += text.substring(1, length);
410        }
411        return answer;
412    }
413
414    /**
415     * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld)
416     *
417     * @param text  the string
418     * @return the string camel cased
419     */
420    public static String dashToCamelCase(String text) {
421        if (text == null) {
422            return null;
423        }
424        int length = text.length();
425        if (length == 0) {
426            return text;
427        }
428        if (text.indexOf('-') == -1) {
429            return text;
430        }
431
432        StringBuilder sb = new StringBuilder();
433
434        for (int i = 0; i < text.length(); i++) {
435            char c = text.charAt(i);
436            if (c == '-') {
437                i++;
438                sb.append(Character.toUpperCase(text.charAt(i)));
439            } else {
440                sb.append(c);
441            }
442        }
443        return sb.toString();
444    }
445
446    /**
447     * Returns the string after the given token
448     *
449     * @param text  the text
450     * @param after the token
451     * @return the text after the token, or <tt>null</tt> if text does not contain the token
452     */
453    public static String after(String text, String after) {
454        if (!text.contains(after)) {
455            return null;
456        }
457        return text.substring(text.indexOf(after) + after.length());
458    }
459
460    /**
461     * Returns an object after the given token
462     *
463     * @param text  the text
464     * @param after the token
465     * @param mapper a mapping function to convert the string after the token to type T
466     * @return an Optional describing the result of applying a mapping function to the text after the token.
467     */
468    public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
469        String result = after(text, after);
470        if (result == null) {
471            return Optional.empty();            
472        } else {
473            return Optional.ofNullable(mapper.apply(result));
474        }
475    }
476
477    /**
478     * Returns the string before the given token
479     *
480     * @param text the text
481     * @param before the token
482     * @return the text before the token, or <tt>null</tt> if text does not
483     *         contain the token
484     */
485    public static String before(String text, String before) {
486        if (!text.contains(before)) {
487            return null;
488        }
489        return text.substring(0, text.indexOf(before));
490    }
491
492    /**
493     * Returns an object before the given token
494     *
495     * @param text  the text
496     * @param before the token
497     * @param mapper a mapping function to convert the string before the token to type T
498     * @return an Optional describing the result of applying a mapping function to the text before the token.
499     */
500    public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
501        String result = before(text, before);
502        if (result == null) {
503            return Optional.empty();            
504        } else {
505            return Optional.ofNullable(mapper.apply(result));
506        }
507    }
508
509    /**
510     * Returns the string between the given tokens
511     *
512     * @param text  the text
513     * @param after the before token
514     * @param before the after token
515     * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens
516     */
517    public static String between(String text, String after, String before) {
518        text = after(text, after);
519        if (text == null) {
520            return null;
521        }
522        return before(text, before);
523    }
524
525    /**
526     * Returns an object between the given token
527     *
528     * @param text  the text
529     * @param after the before token
530     * @param before the after token
531     * @param mapper a mapping function to convert the string between the token to type T
532     * @return an Optional describing the result of applying a mapping function to the text between the token.
533     */
534    public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
535        String result = between(text, after, before);
536        if (result == null) {
537            return Optional.empty();            
538        } else {
539            return Optional.ofNullable(mapper.apply(result));
540        }
541    }
542
543    /**
544     * Returns the string between the most outer pair of tokens
545     * <p/>
546     * 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
547     * <p/>
548     * This implementation skips matching when the text is either single or double quoted.
549     * For example:
550     * <tt>${body.matches("foo('bar')")</tt>
551     * Will not match the parenthesis from the quoted text.
552     *
553     * @param text  the text
554     * @param after the before token
555     * @param before the after token
556     * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
557     */
558    public static String betweenOuterPair(String text, char before, char after) {
559        if (text == null) {
560            return null;
561        }
562
563        int pos = -1;
564        int pos2 = -1;
565        int count = 0;
566        int count2 = 0;
567
568        boolean singleQuoted = false;
569        boolean doubleQuoted = false;
570        for (int i = 0; i < text.length(); i++) {
571            char ch = text.charAt(i);
572            if (!doubleQuoted && ch == '\'') {
573                singleQuoted = !singleQuoted;
574            } else if (!singleQuoted && ch == '\"') {
575                doubleQuoted = !doubleQuoted;
576            }
577            if (singleQuoted || doubleQuoted) {
578                continue;
579            }
580
581            if (ch == before) {
582                count++;
583            } else if (ch == after) {
584                count2++;
585            }
586
587            if (ch == before && pos == -1) {
588                pos = i;
589            } else if (ch == after) {
590                pos2 = i;
591            }
592        }
593
594        if (pos == -1 || pos2 == -1) {
595            return null;
596        }
597
598        // must be even paris
599        if (count != count2) {
600            return null;
601        }
602
603        return text.substring(pos + 1, pos2);
604    }
605
606    /**
607     * Returns an object between the most outer pair of tokens
608     *
609     * @param text  the text
610     * @param after the before token
611     * @param before the after token
612     * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T
613     * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens.
614     */
615    public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
616        String result = betweenOuterPair(text, before, after);
617        if (result == null) {
618            return Optional.empty();            
619        } else {
620            return Optional.ofNullable(mapper.apply(result));
621        }
622    }
623
624    /**
625     * Returns true if the given name is a valid java identifier
626     */
627    public static boolean isJavaIdentifier(String name) {
628        if (name == null) {
629            return false;
630        }
631        int size = name.length();
632        if (size < 1) {
633            return false;
634        }
635        if (Character.isJavaIdentifierStart(name.charAt(0))) {
636            for (int i = 1; i < size; i++) {
637                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
638                    return false;
639                }
640            }
641            return true;
642        }
643        return false;
644    }
645
646    /**
647     * Cleans the string to a pure Java identifier so we can use it for loading class names.
648     * <p/>
649     * Especially from Spring DSL people can have \n \t or other characters that otherwise
650     * would result in ClassNotFoundException
651     *
652     * @param name the class name
653     * @return normalized classname that can be load by a class loader.
654     */
655    public static String normalizeClassName(String name) {
656        StringBuilder sb = new StringBuilder(name.length());
657        for (char ch : name.toCharArray()) {
658            if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
659                sb.append(ch);
660            }
661        }
662        return sb.toString();
663    }
664
665    /**
666     * Compares old and new text content and report back which lines are changed
667     *
668     * @param oldText  the old text
669     * @param newText  the new text
670     * @return a list of line numbers that are changed in the new text
671     */
672    public static List<Integer> changedLines(String oldText, String newText) {
673        if (oldText == null || oldText.equals(newText)) {
674            return Collections.emptyList();
675        }
676
677        List<Integer> changed = new ArrayList<>();
678
679        String[] oldLines = oldText.split("\n");
680        String[] newLines = newText.split("\n");
681
682        for (int i = 0; i < newLines.length; i++) {
683            String newLine = newLines[i];
684            String oldLine = i < oldLines.length ? oldLines[i] : null;
685            if (oldLine == null) {
686                changed.add(i);
687            } else if (!newLine.equals(oldLine)) {
688                changed.add(i);
689            }
690        }
691
692        return changed;
693    }
694
695    /**
696     * Removes the leading and trailing whitespace and if the resulting
697     * string is empty returns {@code null}. Examples:
698     * <p>
699     * Examples:
700     * <blockquote><pre>
701     * trimToNull("abc") -> "abc"
702     * trimToNull(" abc") -> "abc"
703     * trimToNull(" abc ") -> "abc"
704     * trimToNull(" ") -> null
705     * trimToNull("") -> null
706     * </pre></blockquote>
707     */
708    public static String trimToNull(final String given) {
709        if (given == null) {
710            return null;
711        }
712
713        final String trimmed = given.trim();
714
715        if (trimmed.isEmpty()) {
716            return null;
717        }
718
719        return trimmed;
720    }
721    
722    /**
723     * Checks if the src string contains what
724     *
725     * @param src  is the source string to be checked
726     * @param what is the string which will be looked up in the src argument 
727     * @return true/false
728     */
729    public static boolean containsIgnoreCase(String src, String what) {
730        if (src == null || what == null) {
731            return false;
732        }
733        
734        final int length = what.length();
735        if (length == 0) {
736            return true; // Empty string is contained
737        }
738
739        final char firstLo = Character.toLowerCase(what.charAt(0));
740        final char firstUp = Character.toUpperCase(what.charAt(0));
741
742        for (int i = src.length() - length; i >= 0; i--) {
743            // Quick check before calling the more expensive regionMatches() method:
744            final char ch = src.charAt(i);
745            if (ch != firstLo && ch != firstUp) {
746                continue;
747            }
748
749            if (src.regionMatches(true, i, what, 0, length)) {
750                return true;
751            }
752        }
753
754        return false;
755    }
756
757    /**
758     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
759     *
760     * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied.
761     * @param bytes number of bytes
762     * @return human readable output
763     * @see java.lang.String#format(Locale, String, Object...)
764     */
765    public static String humanReadableBytes(Locale locale, long bytes) {
766        int unit = 1024;
767        if (bytes < unit) {
768            return bytes + " B";
769        }
770        int exp = (int) (Math.log(bytes) / Math.log(unit));
771        String pre = "KMGTPE".charAt(exp - 1) + "";
772        return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
773    }
774
775    /**
776     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
777     *
778     * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 
779     *
780     * @param bytes number of bytes
781     * @return human readable output
782     * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long)
783     */
784    public static String humanReadableBytes(long bytes) {
785        return humanReadableBytes(Locale.getDefault(), bytes);
786    }
787
788    /**
789     * Check for string pattern matching with a number of strategies in the
790     * following order:
791     *
792     * - equals
793     * - null pattern always matches
794     * - * always matches
795     * - Ant style matching
796     * - Regexp
797     *
798     * @param patter the pattern
799     * @param target the string to test
800     * @return true if target matches the pattern
801     */
802    public static boolean matches(String patter, String target) {
803        if (Objects.equals(patter, target)) {
804            return true;
805        }
806
807        if (Objects.isNull(patter)) {
808            return true;
809        }
810
811        if (Objects.equals("*", patter)) {
812            return true;
813        }
814
815        if (AntPathMatcher.INSTANCE.match(patter, target)) {
816            return true;
817        }
818
819        Pattern p = Pattern.compile(patter);
820        Matcher m = p.matcher(target);
821
822        return m.matches();
823    }
824
825}