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[] {"&", "<", ">"});
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("<");
387 }
388 else if (codepoint == '>') {
389 escaped.append(">");
390 }
391 else if (codepoint == '&') {
392 escaped.append("&");
393 }
394 else if (codepoint == '\'') {
395 escaped.append("'");
396 }
397 else if (codepoint == '\"') {
398 escaped.append(""");
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("<");
465 }
466 else if (codepoint == '&') {
467 escaped.append("&");
468 }
469 else if (codepoint == '\"') {
470 escaped.append(""");
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 }