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