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.Optional;
024import java.util.function.Function;
025
026import static org.apache.camel.util.StringQuoteHelper.doubleQuote;
027
028/**
029 * Helper methods for working with Strings.
030 */
031public final class StringHelper {
032
033    /**
034     * Constructor of utility class should be private.
035     */
036    private StringHelper() {
037    }
038
039    /**
040     * Ensures that <code>s</code> is friendly for a URL or file system.
041     *
042     * @param s String to be sanitized.
043     * @return sanitized version of <code>s</code>.
044     * @throws NullPointerException if <code>s</code> is <code>null</code>.
045     */
046    public static String sanitize(String s) {
047        return s
048            .replace(':', '-')
049            .replace('_', '-')
050            .replace('.', '-')
051            .replace('/', '-')
052            .replace('\\', '-');
053    }
054
055    /**
056     * Counts the number of times the given char is in the string
057     *
058     * @param s  the string
059     * @param ch the char
060     * @return number of times char is located in the string
061     */
062    public static int countChar(String s, char ch) {
063        if (ObjectHelper.isEmpty(s)) {
064            return 0;
065        }
066
067        int matches = 0;
068        for (int i = 0; i < s.length(); i++) {
069            char c = s.charAt(i);
070            if (ch == c) {
071                matches++;
072            }
073        }
074
075        return matches;
076    }
077
078    /**
079     * Limits the length of a string
080     * 
081     * @param s the string
082     * @param maxLength the maximum length of the returned string
083     * @return s if the length of s is less than maxLength or the first maxLength characters of s
084     * @deprecated use {@link #limitLength(String, int)}
085     */
086    @Deprecated
087    public static String limitLenght(String s, int maxLength) {
088        return limitLength(s, maxLength);
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 "{ " + doubleQuote(name) + ": " + doubleQuote(value) + " }";
305        } else {
306            return doubleQuote(name) + ": " + 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        if (text == null) {
387            return null;
388        }
389        int length = text.length();
390        if (length == 0) {
391            return text;
392        }
393        String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH);
394        if (length > 1) {
395            answer += text.substring(1, length);
396        }
397        return answer;
398    }
399
400    /**
401     * Returns the string after the given token
402     *
403     * @param text  the text
404     * @param after the token
405     * @return the text after the token, or <tt>null</tt> if text does not contain the token
406     */
407    public static String after(String text, String after) {
408        if (!text.contains(after)) {
409            return null;
410        }
411        return text.substring(text.indexOf(after) + after.length());
412    }
413
414    /**
415     * Returns an object after the given token
416     *
417     * @param text  the text
418     * @param after the token
419     * @param mapper a mapping function to convert the string after the token to type T
420     * @return an Optional describing the result of applying a mapping function to the text after the token.
421     */
422    public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
423        String result = after(text, after);
424        if (result == null) {
425            return Optional.empty();            
426        } else {
427            return Optional.ofNullable(mapper.apply(result));
428        }
429    }
430
431    /**
432     * Returns the string before the given token
433     *
434     * @param text the text
435     * @param before the token
436     * @return the text before the token, or <tt>null</tt> if text does not
437     *         contain the token
438     */
439    public static String before(String text, String before) {
440        if (!text.contains(before)) {
441            return null;
442        }
443        return text.substring(0, text.indexOf(before));
444    }
445
446    /**
447     * Returns an object before the given token
448     *
449     * @param text  the text
450     * @param before the token
451     * @param mapper a mapping function to convert the string before the token to type T
452     * @return an Optional describing the result of applying a mapping function to the text before the token.
453     */
454    public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
455        String result = before(text, before);
456        if (result == null) {
457            return Optional.empty();            
458        } else {
459            return Optional.ofNullable(mapper.apply(result));
460        }
461    }
462
463    /**
464     * Returns the string between the given tokens
465     *
466     * @param text  the text
467     * @param after the before token
468     * @param before the after token
469     * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens
470     */
471    public static String between(String text, String after, String before) {
472        text = after(text, after);
473        if (text == null) {
474            return null;
475        }
476        return before(text, before);
477    }
478
479    /**
480     * Returns an object between the given token
481     *
482     * @param text  the text
483     * @param after the before token
484     * @param before the after token
485     * @param mapper a mapping function to convert the string between the token to type T
486     * @return an Optional describing the result of applying a mapping function to the text between the token.
487     */
488    public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
489        String result = between(text, after, before);
490        if (result == null) {
491            return Optional.empty();            
492        } else {
493            return Optional.ofNullable(mapper.apply(result));
494        }
495    }
496
497    /**
498     * Returns the string between the most outer pair of tokens
499     * <p/>
500     * 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
501     * <p/>
502     * This implementation skips matching when the text is either single or double quoted.
503     * For example:
504     * <tt>${body.matches("foo('bar')")</tt>
505     * Will not match the parenthesis from the quoted text.
506     *
507     * @param text  the text
508     * @param after the before token
509     * @param before the after token
510     * @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
511     */
512    public static String betweenOuterPair(String text, char before, char after) {
513        if (text == null) {
514            return null;
515        }
516
517        int pos = -1;
518        int pos2 = -1;
519        int count = 0;
520        int count2 = 0;
521
522        boolean singleQuoted = false;
523        boolean doubleQuoted = false;
524        for (int i = 0; i < text.length(); i++) {
525            char ch = text.charAt(i);
526            if (!doubleQuoted && ch == '\'') {
527                singleQuoted = !singleQuoted;
528            } else if (!singleQuoted && ch == '\"') {
529                doubleQuoted = !doubleQuoted;
530            }
531            if (singleQuoted || doubleQuoted) {
532                continue;
533            }
534
535            if (ch == before) {
536                count++;
537            } else if (ch == after) {
538                count2++;
539            }
540
541            if (ch == before && pos == -1) {
542                pos = i;
543            } else if (ch == after) {
544                pos2 = i;
545            }
546        }
547
548        if (pos == -1 || pos2 == -1) {
549            return null;
550        }
551
552        // must be even paris
553        if (count != count2) {
554            return null;
555        }
556
557        return text.substring(pos + 1, pos2);
558    }
559
560    /**
561     * Returns an object between the most outer pair of tokens
562     *
563     * @param text  the text
564     * @param after the before token
565     * @param before the after token
566     * @param mapper a mapping function to convert the string between the most outer pair of tokens to type T
567     * @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens.
568     */
569    public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
570        String result = betweenOuterPair(text, before, after);
571        if (result == null) {
572            return Optional.empty();            
573        } else {
574            return Optional.ofNullable(mapper.apply(result));
575        }
576    }
577
578    /**
579     * Returns true if the given name is a valid java identifier
580     */
581    public static boolean isJavaIdentifier(String name) {
582        if (name == null) {
583            return false;
584        }
585        int size = name.length();
586        if (size < 1) {
587            return false;
588        }
589        if (Character.isJavaIdentifierStart(name.charAt(0))) {
590            for (int i = 1; i < size; i++) {
591                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
592                    return false;
593                }
594            }
595            return true;
596        }
597        return false;
598    }
599
600    /**
601     * Cleans the string to a pure Java identifier so we can use it for loading class names.
602     * <p/>
603     * Especially from Spring DSL people can have \n \t or other characters that otherwise
604     * would result in ClassNotFoundException
605     *
606     * @param name the class name
607     * @return normalized classname that can be load by a class loader.
608     */
609    public static String normalizeClassName(String name) {
610        StringBuilder sb = new StringBuilder(name.length());
611        for (char ch : name.toCharArray()) {
612            if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
613                sb.append(ch);
614            }
615        }
616        return sb.toString();
617    }
618
619    /**
620     * Compares old and new text content and report back which lines are changed
621     *
622     * @param oldText  the old text
623     * @param newText  the new text
624     * @return a list of line numbers that are changed in the new text
625     */
626    public static List<Integer> changedLines(String oldText, String newText) {
627        if (oldText == null || oldText.equals(newText)) {
628            return Collections.emptyList();
629        }
630
631        List<Integer> changed = new ArrayList<>();
632
633        String[] oldLines = oldText.split("\n");
634        String[] newLines = newText.split("\n");
635
636        for (int i = 0; i < newLines.length; i++) {
637            String newLine = newLines[i];
638            String oldLine = i < oldLines.length ? oldLines[i] : null;
639            if (oldLine == null) {
640                changed.add(i);
641            } else if (!newLine.equals(oldLine)) {
642                changed.add(i);
643            }
644        }
645
646        return changed;
647    }
648
649    /**
650     * Removes the leading and trailing whitespace and if the resulting
651     * string is empty returns {@code null}. Examples:
652     * <p>
653     * Examples:
654     * <blockquote><pre>
655     * trimToNull("abc") -> "abc"
656     * trimToNull(" abc") -> "abc"
657     * trimToNull(" abc ") -> "abc"
658     * trimToNull(" ") -> null
659     * trimToNull("") -> null
660     * </pre></blockquote>
661     */
662    public static String trimToNull(final String given) {
663        if (given == null) {
664            return null;
665        }
666
667        final String trimmed = given.trim();
668
669        if (trimmed.isEmpty()) {
670            return null;
671        }
672
673        return trimmed;
674    }
675    
676    /**
677     * Checks if the src string contains what
678     *
679     * @param src  is the source string to be checked
680     * @param what is the string which will be looked up in the src argument 
681     * @return true/false
682     */
683    public static boolean containsIgnoreCase(String src, String what) {
684        if (src == null || what == null) {
685            return false;
686        }
687        
688        final int length = what.length();
689        if (length == 0) {
690            return true; // Empty string is contained
691        }
692
693        final char firstLo = Character.toLowerCase(what.charAt(0));
694        final char firstUp = Character.toUpperCase(what.charAt(0));
695
696        for (int i = src.length() - length; i >= 0; i--) {
697            // Quick check before calling the more expensive regionMatches() method:
698            final char ch = src.charAt(i);
699            if (ch != firstLo && ch != firstUp) {
700                continue;
701            }
702
703            if (src.regionMatches(true, i, what, 0, length)) {
704                return true;
705            }
706        }
707
708        return false;
709    }
710
711    /**
712     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
713     *
714     * @param locale The locale to apply during formatting. If l is {@code null} then no localization is applied.
715     * @param bytes number of bytes
716     * @return human readable output
717     * @see java.lang.String#format(Locale, String, Object...)
718     */
719    public static String humanReadableBytes(Locale locale, long bytes) {
720        int unit = 1024;
721        if (bytes < unit) {
722            return bytes + " B";
723        }
724        int exp = (int) (Math.log(bytes) / Math.log(unit));
725        String pre = "KMGTPE".charAt(exp - 1) + "";
726        return String.format(locale, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
727    }
728
729    /**
730     * Outputs the bytes in human readable format in units of KB,MB,GB etc.
731     *
732     * The locale always used is the one returned by {@link java.util.Locale#getDefault()}. 
733     *
734     * @param bytes number of bytes
735     * @return human readable output
736     * @see org.apache.camel.util.StringHelper#humanReadableBytes(Locale, long)
737     */
738    public static String humanReadableBytes(long bytes) {
739        return humanReadableBytes(Locale.getDefault(), bytes);
740    }
741
742}