View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.util;
16  
17  import java.nio.charset.Charset;
18  import java.util.Locale;
19  import java.util.Map;
20  import java.util.concurrent.ConcurrentHashMap;
21  import java.util.regex.Matcher;
22  import java.util.regex.Pattern;
23  
24  import org.htmlunit.html.impl.Color;
25  
26  /**
27   * String utility class for functions not covered by third-party libraries.
28   *
29   * @author Daniel Gredler
30   * @author Ahmed Ashour
31   * @author Martin Tamme
32   * @author Ronald Brill
33   */
34  public final class StringUtils {
35  
36      /**
37       * The empty String {@code ""}.
38       */
39      public static final String EMPTY_STRING = "";
40  
41      private static final Pattern HEX_COLOR = Pattern.compile("#([\\da-fA-F]{3}|[\\da-fA-F]{6})");
42      private static final Pattern RGB_COLOR =
43          Pattern.compile("rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
44                              + "\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
45                              + "\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*\\)");
46      private static final Pattern RGBA_COLOR =
47              Pattern.compile("rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
48                                   + "\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
49                                   + "\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
50                                   + "\\s*((0?.[1-9])|[01])\\s*\\)");
51      private static final Pattern HSL_COLOR =
52              Pattern.compile("hsl\\(\\s*((0|[1-9]\\d?|[12]\\d\\d?|3[0-5]\\d)(.\\d*)?)\\s*,"
53                                  + "\\s*((0|[1-9]\\d?|100)(.\\d*)?)%\\s*,"
54                                  + "\\s*((0|[1-9]\\d?|100)(.\\d*)?)%\\s*\\)");
55      private static final Pattern ILLEGAL_FILE_NAME_CHARS = Pattern.compile("\\\\|/|\\||:|\\?|\\*|\"|<|>|\\p{Cntrl}");
56  
57      private static final Map<String, String> CAMELIZE_CACHE = new ConcurrentHashMap<>();
58  
59      /**
60       * Disallow instantiation of this class.
61       */
62      private StringUtils() {
63          // Empty.
64      }
65  
66      /**
67       * Returns {@code true} if the given sequence is not {@code null} and is empty.
68       * Unlike {@link org.apache.commons.lang3.StringUtils#isEmpty(CharSequence)}, this returns
69       * {@code false} if the sequence is {@code null}.
70       *
71       * @param s the string to check
72       * @return {@code true} if the string is not {@code null} and has length 0
73       */
74      public static boolean isEmptyString(final CharSequence s) {
75          return s != null && s.length() == 0;
76      }
77  
78      /**
79       * Returns {@code true} if the given sequence is {@code null} or empty.
80       *
81       * @param s the string to check
82       * @return {@code true} if the string is {@code null} or has length 0
83       */
84      public static boolean isEmptyOrNull(final CharSequence s) {
85          return s == null || s.length() == 0;
86      }
87  
88      /**
89       * Returns the given sequence, or the default value if it is empty or {@code null}.
90       *
91       * @param <T> the kind of {@link CharSequence}
92       * @param s the sequence to check
93       * @param defaultString the default value to return if the input is empty or {@code null}
94       * @return the given sequence, or {@code defaultString}
95       */
96      public static <T extends CharSequence> T defaultIfEmptyOrNull(final T s, final T defaultString) {
97          return isEmptyOrNull(s) ? defaultString : s;
98      }
99  
100     /**
101      * Returns {@code true} if the given sequence is {@code null}, empty, or contains only whitespace.
102      *
103      * @param s the sequence to check
104      * @return {@code true} if the sequence is blank
105      */
106     public static boolean isBlank(final CharSequence s) {
107         if (s == null) {
108             return true;
109         }
110 
111         final int length = s.length();
112         if (length == 0) {
113             return true;
114         }
115 
116         for (int i = 0; i < length; i++) {
117             if (!Character.isWhitespace(s.charAt(i))) {
118                 return false;
119             }
120         }
121         return true;
122     }
123 
124     /**
125      * Returns {@code true} if the given sequence is not {@code null}, not empty, and contains
126      * at least one non-whitespace character.
127      *
128      * @param s the sequence to check
129      * @return {@code true} if the sequence is not blank
130      */
131     public static boolean isNotBlank(final CharSequence s) {
132         if (s == null) {
133             return false;
134         }
135 
136         final int length = s.length();
137         if (length == 0) {
138             return false;
139         }
140 
141         for (int i = 0; i < length; i++) {
142             if (!Character.isWhitespace(s.charAt(i))) {
143                 return true;
144             }
145         }
146         return false;
147     }
148 
149     /**
150      * Returns {@code true} if the given sequence has exactly one character and it equals the expected char.
151      *
152      * @param expected the char to compare
153      * @param s the string to check
154      * @return {@code true} if the string has exactly one character matching {@code expected}
155      */
156     public static boolean equalsChar(final char expected, final CharSequence s) {
157         return s != null && s.length() == 1 && expected == s.charAt(0);
158     }
159 
160     /**
161      * Returns {@code true} if the given string starts with the specified prefix, ignoring case.
162      *
163      * @param s the string to check
164      * @param expectedStart the expected prefix (must not be {@code null} or empty)
165      * @return {@code true} if the string starts with the given prefix, ignoring case
166      */
167     public static boolean startsWithIgnoreCase(final String s, final String expectedStart) {
168         if (expectedStart == null || expectedStart.length() == 0) {
169             throw new IllegalArgumentException("Expected start string can't be null or empty");
170         }
171 
172         if (s == null) {
173             return false;
174         }
175         if (s == expectedStart) {
176             return true;
177         }
178 
179         return s.regionMatches(true, 0, expectedStart, 0, expectedStart.length());
180     }
181 
182     /**
183      * Returns {@code true} if the given string ends with the specified suffix, ignoring case.
184      *
185      * @param s the string to check
186      * @param expectedEnd the expected suffix (must not be {@code null} or empty)
187      * @return {@code true} if the string ends with the given suffix, ignoring case
188      */
189     public static boolean endsWithIgnoreCase(final String s, final String expectedEnd) {
190         if (expectedEnd == null) {
191             throw new IllegalArgumentException("Expected end string can't be null or empty");
192         }
193 
194         final int expectedEndLength = expectedEnd.length();
195         if (expectedEndLength == 0) {
196             throw new IllegalArgumentException("Expected end string can't be null or empty");
197         }
198 
199         if (s == null) {
200             return false;
201         }
202         if (s == expectedEnd) {
203             return true;
204         }
205 
206         return s.regionMatches(true, s.length() - expectedEndLength, expectedEnd, 0, expectedEndLength);
207     }
208 
209     /**
210      * Returns {@code true} if the given string contains the specified substring, ignoring case.
211      *
212      * @param s the string to check
213      * @param expected the substring to look for (must not be {@code null} or empty)
214      * @return {@code true} if the string contains the substring, ignoring case
215      */
216     public static boolean containsIgnoreCase(final String s, final String expected) {
217         if (expected == null) {
218             throw new IllegalArgumentException("Expected string can't be null or empty");
219         }
220 
221         final int expectedLength = expected.length();
222         if (expectedLength == 0) {
223             throw new IllegalArgumentException("Expected string can't be null or empty");
224         }
225 
226         if (s == null) {
227             return false;
228         }
229         if (s == expected) {
230             return true;
231         }
232 
233         final int max = s.length() - expectedLength;
234         for (int i = 0; i <= max; i++) {
235             if (s.regionMatches(true, i, expected, 0, expectedLength)) {
236                 return true;
237             }
238         }
239         return false;
240     }
241 
242     /**
243      * Replaces multiple characters in a string in a single pass.
244      * This method can also be used to delete characters by omitting them from {@code replaceChars}.
245      *
246      * @param str the string to process; may be {@code null}
247      * @param searchChars the set of characters to search for; may be {@code null}
248      * @param replaceChars the replacement characters; may be {@code null}
249      * @return the modified string, or the original if no replacement was performed
250      */
251     @SuppressWarnings("null")
252     public static String replaceChars(final String str, final String searchChars, final String replaceChars) {
253         if (isEmptyOrNull(str) || isEmptyOrNull(searchChars)) {
254             return str;
255         }
256 
257         final int replaceCharsLength = replaceChars == null ? 0 : replaceChars.length();
258         final int strLength = str.length();
259 
260         StringBuilder buf = null;
261         int i = 0;
262         for ( ; i < strLength; i++) {
263             final char ch = str.charAt(i);
264             final int index = searchChars.indexOf(ch);
265             if (index != -1) {
266                 buf = new StringBuilder(strLength);
267                 buf.append(str, 0, i);
268                 if (index < replaceCharsLength) {
269                     buf.append(replaceChars.charAt(index));
270                 }
271                 break;
272             }
273         }
274 
275         if (buf == null) {
276             return str;
277         }
278 
279         i++;
280         for ( ; i < strLength; i++) {
281             final char ch = str.charAt(i);
282             final int index = searchChars.indexOf(ch);
283             if (index != -1) {
284                 if (index < replaceCharsLength) {
285                     buf.append(replaceChars.charAt(index));
286                 }
287             }
288             else {
289                 buf.append(ch);
290             }
291         }
292 
293         return buf.toString();
294     }
295 
296     /**
297      * Returns the substring after the first occurrence of the specified separator.
298      * The separator itself is not included.
299      * <p>
300      * Returns {@code null} for a {@code null} input string.
301      * Returns an empty string for an empty input string.
302      * Returns an empty string if the separator is not found.
303      * Returns an empty string if the separator is {@code null} and the input is not {@code null}.
304      * </p>
305      *
306      * @param str the string to search; may be {@code null}
307      * @param find the separator to find; may be {@code null}
308      * @return the substring after the first occurrence of the separator, or {@code null} if input is {@code null}
309      */
310     public static String substringAfter(final String str, final String find) {
311         if (isEmptyOrNull(str)) {
312             return str;
313         }
314         if (find == null) {
315             return EMPTY_STRING;
316         }
317         final int pos = str.indexOf(find);
318         if (pos == -1) {
319             return EMPTY_STRING;
320         }
321         return str.substring(pos + find.length());
322     }
323 
324     /**
325      * Escapes the characters {@code <}, {@code >} and {@code &} into their XML entity equivalents.
326      *
327      * @param s the string to escape
328      * @return the escaped form of the specified string
329      */
330     public static String escapeXmlChars(final String s) {
331         return org.apache.commons.lang3.StringUtils.
332                 replaceEach(s, new String[] {"&", "<", ">"}, new String[] {"&amp;", "&lt;", "&gt;"});
333     }
334 
335     /**
336      * Escapes a string for use as XML 1.0 content, replacing
337      * {@code "}, {@code &}, {@code '}, {@code <}, and {@code >} with their XML entity equivalents.
338      * Invalid XML 1.0 code points are removed.
339      *
340      * @param text the text to escape
341      * @return the escaped value, or {@code null} if the input is {@code null}
342      */
343     public static String escapeXml(final String text) {
344         if (text == null) {
345             return null;
346         }
347 
348         StringBuilder escaped = null;
349 
350         final int offset = 0;
351         final int max = text.length();
352 
353         int readOffset = offset;
354 
355         for (int i = offset; i < max; i++) {
356             final int codepoint = Character.codePointAt(text, i);
357             final boolean codepointValid = supportedByXML10(codepoint);
358 
359             if (!codepointValid
360                     || codepoint == '<'
361                     || codepoint == '>'
362                     || codepoint == '&'
363                     || codepoint == '\''
364                     || codepoint == '"') {
365 
366                 // replacement required
367                 if (escaped == null) {
368                     escaped = new StringBuilder(max);
369                 }
370 
371                 if (i > readOffset) {
372                     escaped.append(text, readOffset, i);
373                 }
374 
375                 if (Character.charCount(codepoint) > 1) {
376                     i++;
377                 }
378                 readOffset = i + 1;
379 
380                 // skip
381                 if (!codepointValid) {
382                     continue;
383                 }
384 
385                 if (codepoint == '<') {
386                     escaped.append("&lt;");
387                 }
388                 else if (codepoint == '>') {
389                     escaped.append("&gt;");
390                 }
391                 else if (codepoint == '&') {
392                     escaped.append("&amp;");
393                 }
394                 else if (codepoint == '\'') {
395                     escaped.append("&apos;");
396                 }
397                 else if (codepoint == '\"') {
398                     escaped.append("&quot;");
399                 }
400             }
401         }
402 
403         if (escaped == null) {
404             return text;
405         }
406 
407         if (max > readOffset) {
408             escaped.append(text, readOffset, max);
409         }
410 
411         return escaped.toString();
412     }
413 
414     /**
415      * Escapes a string for use as an XML attribute value.
416      * Only {@code <}, {@code &}, and {@code "} are escaped, as required by the
417      * <a href="https://www.w3.org/TR/REC-xml/#d0e888">XML specification</a>.
418      * Invalid XML 1.0 code points are removed.
419      *
420      * @param attValue the attribute value to escape
421      * @return the escaped value, or {@code null} if the input is {@code null}
422      */
423     public static String escapeXmlAttributeValue(final String attValue) {
424         if (attValue == null) {
425             return null;
426         }
427 
428         StringBuilder escaped = null;
429 
430         final int offset = 0;
431         final int max = attValue.length();
432 
433         int readOffset = offset;
434 
435         for (int i = offset; i < max; i++) {
436             final int codepoint = Character.codePointAt(attValue, i);
437             final boolean codepointValid = supportedByXML10(codepoint);
438 
439             if (!codepointValid
440                     || codepoint == '<'
441                     || codepoint == '&'
442                     || codepoint == '"') {
443 
444                 // replacement required
445                 if (escaped == null) {
446                     escaped = new StringBuilder(max);
447                 }
448 
449                 if (i > readOffset) {
450                     escaped.append(attValue, readOffset, i);
451                 }
452 
453                 if (Character.charCount(codepoint) > 1) {
454                     i++;
455                 }
456                 readOffset = i + 1;
457 
458                 // skip
459                 if (!codepointValid) {
460                     continue;
461                 }
462 
463                 if (codepoint == '<') {
464                     escaped.append("&lt;");
465                 }
466                 else if (codepoint == '&') {
467                     escaped.append("&amp;");
468                 }
469                 else if (codepoint == '\"') {
470                     escaped.append("&quot;");
471                 }
472             }
473         }
474 
475         if (escaped == null) {
476             return attValue;
477         }
478 
479         if (max > readOffset) {
480             escaped.append(attValue, readOffset, max);
481         }
482 
483         return escaped.toString();
484     }
485 
486     /*
487      * XML 1.0 does not allow control characters or unpaired Unicode surrogate codepoints.
488      * We will remove characters that do not fit in the following ranges:
489      * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
490      */
491     private static boolean supportedByXML10(final int codepoint) {
492         if (codepoint < 0x20) {
493             return codepoint == 0x9 || codepoint == 0xA || codepoint == 0xD;
494         }
495         if (codepoint <= 0xD7FF) {
496             return true;
497         }
498 
499         if (codepoint < 0xE000) {
500             return false;
501         }
502         if (codepoint <= 0xFFFD) {
503             return true;
504         }
505 
506         if (codepoint < 0x10000) {
507             return false;
508         }
509         if (codepoint <= 0x10FFFF) {
510             return true;
511         }
512 
513         return true;
514     }
515 
516     /**
517      * Returns the index within the specified string of the first occurrence of
518      * the specified search character within the given range.
519      *
520      * @param s the string to search
521      * @param searchChar the character to search for
522      * @param beginIndex the index at which to start the search
523      * @param endIndex the index at which to stop the search (exclusive)
524      * @return the index of the first occurrence, or {@code -1} if not found
525      */
526     public static int indexOf(final String s, final char searchChar, final int beginIndex, final int endIndex) {
527         for (int i = beginIndex; i < endIndex; i++) {
528             if (s.charAt(i) == searchChar) {
529                 return i;
530             }
531         }
532         return -1;
533     }
534 
535     /**
536      * Returns a {@link Color} parsed from the given RGB hexadecimal notation,
537      * or {@code null} if the token is not a valid hex color.
538      *
539      * @param token the token to parse
540      * @return a {@link Color} if the token is a valid hex color; otherwise {@code null}
541      */
542     public static Color asColorHexadecimal(final String token) {
543         if (token == null) {
544             return null;
545         }
546         final Matcher tmpMatcher = HEX_COLOR.matcher(token);
547         final boolean tmpFound = tmpMatcher.matches();
548         if (!tmpFound) {
549             return null;
550         }
551 
552         final String tmpHex = tmpMatcher.group(1);
553         if (tmpHex.length() == 6) {
554             final int tmpRed = Integer.parseInt(tmpHex.substring(0, 2), 16);
555             final int tmpGreen = Integer.parseInt(tmpHex.substring(2, 4), 16);
556             final int tmpBlue = Integer.parseInt(tmpHex.substring(4, 6), 16);
557             return new Color(tmpRed, tmpGreen, tmpBlue);
558         }
559 
560         final int tmpRed = Integer.parseInt(tmpHex.substring(0, 1) + tmpHex.substring(0, 1), 16);
561         final int tmpGreen = Integer.parseInt(tmpHex.substring(1, 2) + tmpHex.substring(1, 2), 16);
562         final int tmpBlue = Integer.parseInt(tmpHex.substring(2, 3) + tmpHex.substring(2, 3), 16);
563         return new Color(tmpRed, tmpGreen, tmpBlue);
564     }
565 
566     /**
567      * Returns a {@link Color} parsed from the first {@code rgb(...)} notation found within the token,
568      * or {@code null} if none is found.
569      *
570      * @param token the token to parse
571      * @return a {@link Color} if an RGB color is found; otherwise {@code null}
572      */
573     public static Color findColorRGB(final String token) {
574         if (token == null) {
575             return null;
576         }
577         final Matcher tmpMatcher = RGB_COLOR.matcher(token);
578         if (!tmpMatcher.find()) {
579             return null;
580         }
581 
582         final int tmpRed = Integer.parseInt(tmpMatcher.group(1));
583         final int tmpGreen = Integer.parseInt(tmpMatcher.group(2));
584         final int tmpBlue = Integer.parseInt(tmpMatcher.group(3));
585         return new Color(tmpRed, tmpGreen, tmpBlue);
586     }
587 
588     /**
589      * Returns a {@link Color} parsed from the first {@code rgba(...)} notation found within the token,
590      * or {@code null} if none is found.
591      *
592      * @param token the token to parse
593      * @return a {@link Color} if an RGBA color is found; otherwise {@code null}
594      */
595     public static Color findColorRGBA(final String token) {
596         if (token == null) {
597             return null;
598         }
599         final Matcher tmpMatcher = RGBA_COLOR.matcher(token);
600         if (!tmpMatcher.find()) {
601             return null;
602         }
603 
604         final int tmpRed = Integer.parseInt(tmpMatcher.group(1));
605         final int tmpGreen = Integer.parseInt(tmpMatcher.group(2));
606         final int tmpBlue = Integer.parseInt(tmpMatcher.group(3));
607         final int tmpAlpha = (int) (Float.parseFloat(tmpMatcher.group(4)) * 255);
608         return new Color(tmpRed, tmpGreen, tmpBlue, tmpAlpha);
609     }
610 
611     /**
612      * Returns a {@link Color} parsed from the first {@code hsl(...)} notation found within the token,
613      * or {@code null} if none is found.
614      *
615      * @param token the token to parse
616      * @return a {@link Color} if an HSL color is found; otherwise {@code null}
617      */
618     public static Color findColorHSL(final String token) {
619         if (token == null) {
620             return null;
621         }
622         final Matcher tmpMatcher = HSL_COLOR.matcher(token);
623         if (!tmpMatcher.find()) {
624             return null;
625         }
626 
627         final float tmpHue = Float.parseFloat(tmpMatcher.group(1)) / 360f;
628         final float tmpSaturation = Float.parseFloat(tmpMatcher.group(4)) / 100f;
629         final float tmpLightness = Float.parseFloat(tmpMatcher.group(7)) / 100f;
630         return hslToRgb(tmpHue, tmpSaturation, tmpLightness);
631     }
632 
633     /**
634      * Converts an HSL color value to an RGB {@link Color}.
635      * Conversion formula adapted from
636      * <a href="https://en.wikipedia.org/wiki/HSL_color_space">Wikipedia: HSL color space</a>.
637      * Assumes {@code h}, {@code s}, and {@code l} are in the range [0, 1].
638      *
639      * @param h the hue
640      * @param s the saturation
641      * @param l the lightness
642      * @return the resulting {@link Color}
643      */
644     private static Color hslToRgb(final float h, final float s, final float l) {
645         if (s == 0f) {
646             return new Color(to255(l), to255(l), to255(l));
647         }
648 
649         final float q = l < 0.5f ? l * (1 + s) : l + s - l * s;
650         final float p = 2 * l - q;
651         final float r = hueToRgb(p, q, h + 1f / 3f);
652         final float g = hueToRgb(p, q, h);
653         final float b = hueToRgb(p, q, h - 1f / 3f);
654 
655         return new Color(to255(r), to255(g), to255(b));
656     }
657 
658     private static float hueToRgb(final float p, final float q, float t) {
659         if (t < 0f) {
660             t += 1f;
661         }
662 
663         if (t > 1f) {
664             t -= 1f;
665         }
666 
667         if (t < 1f / 6f) {
668             return p + (q - p) * 6f * t;
669         }
670 
671         if (t < 1f / 2f) {
672             return q;
673         }
674 
675         if (t < 2f / 3f) {
676             return p + (q - p) * (2f / 3f - t) * 6f;
677         }
678 
679         return p;
680     }
681 
682     private static int to255(final float value) {
683         return (int) Math.min(255, 256 * value);
684     }
685 
686     /**
687      * Formats the specified color as an {@code rgb(...)} string.
688      *
689      * @param color the color to format
690      * @return the specified color formatted as {@code rgb(r, g, b)}
691      */
692     public static String formatColor(final Color color) {
693         return "rgb(" + color.getRed() + ", " + color.getGreen() + ", " + color.getBlue() + ")";
694     }
695 
696     /**
697      * Sanitizes a string for use with {@link java.util.regex.Matcher#appendReplacement}.
698      * Replaces all {@code \} with {@code \\} and {@code $} with {@code \$},
699      * as these are control characters in {@code appendReplacement}.
700      *
701      * @param toSanitize the string to sanitize
702      * @return the sanitized version of the string
703      */
704     public static String sanitizeForAppendReplacement(final String toSanitize) {
705         return org.apache.commons.lang3.StringUtils.replaceEach(toSanitize,
706                                     new String[] {"\\", "$"}, new String[]{"\\\\", "\\$"});
707     }
708 
709     /**
710      * Sanitizes a string for use as a filename by replacing illegal characters
711      * ({@code \}, {@code /}, {@code |}, {@code :}, {@code ?}, {@code *},
712      * {@code "}, {@code <}, {@code >}, control characters) with {@code _}.
713      *
714      * @param toSanitize the string to sanitize
715      * @return the sanitized version of the string
716      */
717     public static String sanitizeForFileName(final String toSanitize) {
718         return ILLEGAL_FILE_NAME_CHARS.matcher(toSanitize).replaceAll("_");
719     }
720 
721     /**
722      * Transforms a delimiter-separated CSS property name (e.g. {@code font-size})
723      * into camel case (e.g. {@code fontSize}).
724      *
725      * @param string the string to camelize
726      * @return the camelized string
727      */
728     public static String cssCamelize(final String string) {
729         if (string == null) {
730             return null;
731         }
732 
733         String result = CAMELIZE_CACHE.get(string);
734         if (null != result) {
735             return result;
736         }
737 
738         // not found in CamelizeCache_; convert and store in cache
739         final int pos = string.indexOf('-');
740         if (pos == -1 || pos == string.length() - 1) {
741             // cache also this strings for performance
742             CAMELIZE_CACHE.put(string, string);
743             return string;
744         }
745 
746         final StringBuilder builder = new StringBuilder(string);
747         builder.deleteCharAt(pos);
748         builder.setCharAt(pos, Character.toUpperCase(builder.charAt(pos)));
749 
750         int i = pos + 1;
751         while (i < builder.length() - 1) {
752             if (builder.charAt(i) == '-') {
753                 builder.deleteCharAt(i);
754                 builder.setCharAt(i, Character.toUpperCase(builder.charAt(i)));
755             }
756             i++;
757         }
758         result = builder.toString();
759         CAMELIZE_CACHE.put(string, result);
760 
761         return result;
762     }
763 
764     /**
765      * Converts the given string to lowercase using the ROOT locale.
766      * Returns {@code null} if the input is {@code null}.
767      *
768      * @param s the string to lowercase
769      * @return the lowercased string, or {@code null}
770      */
771     public static String toRootLowerCase(final String s) {
772         return s == null ? null : s.toLowerCase(Locale.ROOT);
773     }
774 
775     /**
776      * Converts the given string to a byte array using the specified charset.
777      * Returns an empty byte array if the string is {@code null} or empty, or if the charset is unsupported.
778      *
779      * @param charset the charset to use for encoding
780      * @param content the string to convert
781      * @return the string as a byte array, or an empty array if the string is {@code null} or empty
782      */
783     public static byte[] toByteArray(final String content, final Charset charset) {
784         if (content ==  null || content.isEmpty()) {
785             return new byte[0];
786         }
787 
788         return content.getBytes(charset);
789     }
790 
791     /**
792      * Splits the given text on whitespace as defined by {@link Character#isWhitespace(char)}.
793      *
794      * @param str the string to split; may be {@code null}
795      * @return an array of parsed strings, or an empty array if the input is {@code null}
796      */
797     public static String[] splitAtJavaWhitespace(final String str) {
798         final String[] parts = org.apache.commons.lang3.StringUtils.split(str);
799         if (parts == null) {
800             return new String[0];
801         }
802         return parts;
803     }
804 
805     /**
806      * Splits the given text on blank (space) characters.
807      *
808      * @param str the string to split; may be {@code null}
809      * @return an array of parsed strings, or an empty array if the input is {@code null}
810      */
811     public static String[] splitAtBlank(final String str) {
812         final String[] parts = org.apache.commons.lang3.StringUtils.split(str, ' ');
813         if (parts == null) {
814             return new String[0];
815         }
816         return parts;
817     }
818 
819     /**
820      * Splits the given text on comma characters.
821      *
822      * @param str the string to split; may be {@code null}
823      * @return an array of parsed strings, or an empty array if the input is {@code null}
824      */
825     public static String[] splitAtComma(final String str) {
826         final String[] parts = org.apache.commons.lang3.StringUtils.split(str, ',');
827         if (parts == null) {
828             return new String[0];
829         }
830         return parts;
831     }
832 
833     /**
834      * Splits the given text on comma or blank (space) characters.
835      *
836      * @param str the string to split; may be {@code null}
837      * @return an array of parsed strings, or an empty array if the input is {@code null}
838      */
839     public static String[] splitAtCommaOrBlank(final String str) {
840         final String[] parts = org.apache.commons.lang3.StringUtils.split(str, ", ");
841         if (parts == null) {
842             return new String[0];
843         }
844         return parts;
845     }
846 
847     /**
848      * Returns the substring before the first occurrence of the specified separator.
849      * The separator itself is not included in the result.
850      * {@code null} input returns {@code null}; empty input returns an empty string.
851      *
852      * @param str the string to search; may be {@code null}
853      * @param find the separator to find; must not be {@code null} or empty
854      * @return the substring before the first occurrence of the separator,
855      *         or the full string if the separator is not found
856      */
857     public static String substringBefore(final String str, final String find) {
858         if (isEmptyOrNull(find)) {
859             throw new IllegalArgumentException("'find' string parameter has to be not empty and not null");
860         }
861 
862         if (isEmptyString(str)) {
863             return str;
864         }
865 
866         final int pos = str.indexOf(find);
867         if (pos == -1) {
868             return str;
869         }
870         return str.substring(0, pos);
871     }
872 
873     /**
874      * Parses an integer from the given string, returning the default value if parsing fails
875      * or the string is {@code null}.
876      *
877      * @param str the string to parse; may be {@code null}
878      * @param defaultValue the value to return if parsing fails
879      * @return the parsed integer, or {@code defaultValue}
880      */
881     public static int toInt(final String str, final int defaultValue) {
882         try {
883             return Integer.parseInt(str);
884         }
885         catch (final RuntimeException e) {
886             return defaultValue;
887         }
888     }
889 
890     /**
891      * Parses a float from the given string, returning the default value if parsing fails
892      * or the string is {@code null}.
893      *
894      * @param str the string to parse; may be {@code null}
895      * @param defaultValue the value to return if parsing fails
896      * @return the parsed float, or {@code defaultValue}
897      */
898     public static float toFloat(final String str, final float defaultValue) {
899         try {
900             return Float.parseFloat(str);
901         }
902         catch (final RuntimeException e) {
903             return defaultValue;
904         }
905     }
906 
907     /**
908      * Strips trailing whitespace from the given string.
909      * Returns {@code null} for a {@code null} input; returns the string unchanged if it has no trailing whitespace.
910      *
911      * @param str the string to trim; may be {@code null}
912      * @return the trimmed string, or {@code null} if input is {@code null}
913      */
914     public static String trimRight(final String str) {
915         if (isEmptyOrNull(str)) {
916             return str;
917         }
918 
919         int end = str.length();
920         while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) {
921             end--;
922         }
923 
924         if (end == str.length()) {
925             return str;
926         }
927 
928         return str.substring(0, end);
929     }
930 
931     /**
932      * Returns {@code true} if the given sequence is non-{@code null} and contains only characters
933      * from the provided set of valid characters.
934      *
935      * @param cs the sequence to check; may be {@code null}
936      * @param valid the array of valid characters; must not be {@code null} or empty
937      * @return {@code true} if all characters in the sequence are valid
938      */
939     public static boolean containsOnly(final CharSequence cs, final char... valid) {
940         if (valid == null || valid.length == 0) {
941             throw new IllegalArgumentException("Expected valid char[] can't be null or empty");
942         }
943         if (isEmptyOrNull(cs)) {
944             return false;
945         }
946 
947         final int csLength = cs.length();
948         final int validLength = valid.length;
949         for (int i = 0; i < csLength; i++) {
950             final char testChar = cs.charAt(i);
951             int j = 0;
952             for ( ; j < validLength; j++) {
953                 final char validChar = valid[j];
954                 if (validChar == testChar) {
955                     break;
956                 }
957             }
958             if (j == validLength) {
959                 return false;
960             }
961         }
962 
963         return true;
964     }
965 }