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