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      private static final Pattern HEX_COLOR = Pattern.compile("#([\\da-fA-F]{3}|[\\da-fA-F]{6})");
37      private static final Pattern RGB_COLOR =
38          Pattern.compile("rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
39                              + "\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
40                              + "\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*\\)");
41      private static final Pattern RGBA_COLOR =
42              Pattern.compile("rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%?\\s*,"
43                                   + "\\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])|[01])\\s*\\)");
46      private static final Pattern HSL_COLOR =
47              Pattern.compile("hsl\\(\\s*((0|[1-9]\\d?|[12]\\d\\d?|3[0-5]\\d)(.\\d*)?)\\s*,"
48                                  + "\\s*((0|[1-9]\\d?|100)(.\\d*)?)%\\s*,"
49                                  + "\\s*((0|[1-9]\\d?|100)(.\\d*)?)%\\s*\\)");
50      private static final Pattern ILLEGAL_FILE_NAME_CHARS = Pattern.compile("\\\\|/|\\||:|\\?|\\*|\"|<|>|\\p{Cntrl}");
51  
52      private static final Map<String, String> CAMELIZE_CACHE = new ConcurrentHashMap<>();
53  
54      /**
55       * Disallow instantiation of this class.
56       */
57      private StringUtils() {
58          // Empty.
59      }
60  
61      /**
62       * Returns true if the param is not null and empty. This is different from
63       * {@link org.apache.commons.lang3.StringUtils#isEmpty(CharSequence)} because
64       * this returns false if the provided string is null.
65       *
66       * @param s the string to check
67       * @return true if the param is not null and empty
68       */
69      public static boolean isEmptyString(final CharSequence s) {
70          return s != null && s.length() == 0;
71      }
72  
73      /**
74       * Returns true if the param is null or empty.
75       *
76       * @param s the string to check
77       * @return true if the param is null or empty
78       */
79      public static boolean isEmptyOrNull(final CharSequence s) {
80          return s == null || s.length() == 0;
81      }
82  
83      /**
84       * Returns either the passed in CharSequence, or if the CharSequence is
85       * empty or {@code null}, the default value.
86       *
87       * @param <T> the kind of CharSequence
88       * @param s  the CharSequence to check
89       * @param defaultString the default to return if the input is empty or null
90       * @return the passed in CharSequence, or the defaultString
91       */
92      public static <T extends CharSequence> T defaultIfEmptyOrNull(final T s, final T defaultString) {
93          return isEmptyOrNull(s) ? defaultString : s;
94      }
95  
96      /**
97       * Tests if a CharSequence is null, empty, or contains only whitespace.
98       *
99       * @param s the CharSequence to check
100      * @return true if a CharSequence is null, empty, or contains only whitespace
101      */
102     public static boolean isBlank(final CharSequence s) {
103         if (s == null) {
104             return true;
105         }
106 
107         final int length = s.length();
108         if (length == 0) {
109             return true;
110         }
111 
112         for (int i = 0; i < length; i++) {
113             if (!Character.isWhitespace(s.charAt(i))) {
114                 return false;
115             }
116         }
117         return true;
118     }
119 
120     /**
121      * Tests if a CharSequence is NOT null, empty, or contains only whitespace.
122      *
123      * @param s the CharSequence to check
124      * @return false if a CharSequence is null, empty, or contains only whitespace
125      */
126     public static boolean isNotBlank(final CharSequence s) {
127         if (s == null) {
128             return false;
129         }
130 
131         final int length = s.length();
132         if (length == 0) {
133             return false;
134         }
135 
136         for (int i = 0; i < length; i++) {
137             if (!Character.isWhitespace(s.charAt(i))) {
138                 return true;
139             }
140         }
141         return false;
142     }
143 
144     /**
145      * @param expected the char that we expect
146      * @param s the string to check
147      * @return true if the provided string has only one char and this matches the expectation
148      */
149     public static boolean equalsChar(final char expected, final CharSequence s) {
150         return s != null && s.length() == 1 && expected == s.charAt(0);
151     }
152 
153     /**
154      * Tests if a CharSequence starts with a specified prefix.
155      *
156      * @param s the string to check
157      * @param expectedStart the string that we expect at the beginning (has to be not null and not empty)
158      * @return true if the provided string has only one char and this matches the expectation
159      */
160     public static boolean startsWithIgnoreCase(final String s, final String expectedStart) {
161         if (expectedStart == null || expectedStart.length() == 0) {
162             throw new IllegalArgumentException("Expected start string can't be null or empty");
163         }
164 
165         if (s == null) {
166             return false;
167         }
168         if (s == expectedStart) {
169             return true;
170         }
171 
172         return s.regionMatches(true, 0, expectedStart, 0, expectedStart.length());
173     }
174 
175     /**
176      * Tests if a CharSequence ends with a specified prefix.
177      *
178      * @param s the string to check
179      * @param expectedEnd the string that we expect at the end (has to be not null and not empty)
180      * @return true if the provided string has only one char and this matches the expectation
181      */
182     public static boolean endsWithIgnoreCase(final String s, final String expectedEnd) {
183         if (expectedEnd == null) {
184             throw new IllegalArgumentException("Expected end string can't be null or empty");
185         }
186 
187         final int expectedEndLength = expectedEnd.length();
188         if (expectedEndLength == 0) {
189             throw new IllegalArgumentException("Expected end string can't be null or empty");
190         }
191 
192         if (s == null) {
193             return false;
194         }
195         if (s == expectedEnd) {
196             return true;
197         }
198 
199         return s.regionMatches(true, s.length() - expectedEndLength, expectedEnd, 0, expectedEndLength);
200     }
201 
202     /**
203      * Tests if a CharSequence ends with a specified prefix.
204      *
205      * @param s the string to check
206      * @param expected the string that we expect to be a substring (has to be not null and not empty)
207      * @return true if the provided string has only one char and this matches the expectation
208      */
209     public static boolean containsIgnoreCase(final String s, final String expected) {
210         if (expected == null) {
211             throw new IllegalArgumentException("Expected string can't be null or empty");
212         }
213 
214         final int expectedLength = expected.length();
215         if (expectedLength == 0) {
216             throw new IllegalArgumentException("Expected string can't be null or empty");
217         }
218 
219         if (s == null) {
220             return false;
221         }
222         if (s == expected) {
223             return true;
224         }
225 
226         final int max = s.length() - expectedLength;
227         for (int i = 0; i <= max; i++) {
228             if (s.regionMatches(true, i, expected, 0, expectedLength)) {
229                 return true;
230             }
231         }
232         return false;
233     }
234 
235     /**
236      * Escapes the characters '&lt;', '&gt;' and '&amp;' into their XML entity equivalents.
237      *
238      * @param s the string to escape
239      * @return the escaped form of the specified string
240      */
241     public static String escapeXmlChars(final String s) {
242         return org.apache.commons.lang3.StringUtils.
243                 replaceEach(s, new String[] {"&", "<", ">"}, new String[] {"&amp;", "&lt;", "&gt;"});
244     }
245 
246     /**
247      * Escape the string to be used as xml 1.0 content be replacing the
248      * characters '&quot;', '&amp;', '&#39;', '&lt;', and '&gt;' into their XML entity equivalents.
249      * @param text the attribute value
250      * @return the escaped value
251      */
252     public static String escapeXml(final String text) {
253         if (text == null) {
254             return null;
255         }
256 
257         StringBuilder escaped = null;
258 
259         final int offset = 0;
260         final int max = text.length();
261 
262         int readOffset = offset;
263 
264         for (int i = offset; i < max; i++) {
265             final int codepoint = Character.codePointAt(text, i);
266             final boolean codepointValid = supportedByXML10(codepoint);
267 
268             if (!codepointValid
269                     || codepoint == '<'
270                     || codepoint == '>'
271                     || codepoint == '&'
272                     || codepoint == '\''
273                     || codepoint == '"') {
274 
275                 // replacement required
276                 if (escaped == null) {
277                     escaped = new StringBuilder(max);
278                 }
279 
280                 if (i > readOffset) {
281                     escaped.append(text, readOffset, i);
282                 }
283 
284                 if (Character.charCount(codepoint) > 1) {
285                     i++;
286                 }
287                 readOffset = i + 1;
288 
289                 // skip
290                 if (!codepointValid) {
291                     continue;
292                 }
293 
294                 if (codepoint == '<') {
295                     escaped.append("&lt;");
296                 }
297                 else if (codepoint == '>') {
298                     escaped.append("&gt;");
299                 }
300                 else if (codepoint == '&') {
301                     escaped.append("&amp;");
302                 }
303                 else if (codepoint == '\'') {
304                     escaped.append("&apos;");
305                 }
306                 else if (codepoint == '\"') {
307                     escaped.append("&quot;");
308                 }
309             }
310         }
311 
312         if (escaped == null) {
313             return text;
314         }
315 
316         if (max > readOffset) {
317             escaped.append(text, readOffset, max);
318         }
319 
320         return escaped.toString();
321     }
322 
323     /**
324      * Escape the string to be used as attribute value.
325      * Only {@code <}, {@code &} and {@code "} have to be escaped (see
326      * <a href="http://www.w3.org/TR/REC-xml/#d0e888">http://www.w3.org/TR/REC-xml/#d0e888</a>).
327      * @param attValue the attribute value
328      * @return the escaped value
329      */
330     public static String escapeXmlAttributeValue(final String attValue) {
331         if (attValue == null) {
332             return null;
333         }
334 
335         StringBuilder escaped = null;
336 
337         final int offset = 0;
338         final int max = attValue.length();
339 
340         int readOffset = offset;
341 
342         for (int i = offset; i < max; i++) {
343             final int codepoint = Character.codePointAt(attValue, i);
344             final boolean codepointValid = supportedByXML10(codepoint);
345 
346             if (!codepointValid
347                     || codepoint == '<'
348                     || codepoint == '&'
349                     || codepoint == '"') {
350 
351                 // replacement required
352                 if (escaped == null) {
353                     escaped = new StringBuilder(max);
354                 }
355 
356                 if (i > readOffset) {
357                     escaped.append(attValue, readOffset, i);
358                 }
359 
360                 if (Character.charCount(codepoint) > 1) {
361                     i++;
362                 }
363                 readOffset = i + 1;
364 
365                 // skip
366                 if (!codepointValid) {
367                     continue;
368                 }
369 
370                 if (codepoint == '<') {
371                     escaped.append("&lt;");
372                 }
373                 else if (codepoint == '&') {
374                     escaped.append("&amp;");
375                 }
376                 else if (codepoint == '\"') {
377                     escaped.append("&quot;");
378                 }
379             }
380         }
381 
382         if (escaped == null) {
383             return attValue;
384         }
385 
386         if (max > readOffset) {
387             escaped.append(attValue, readOffset, max);
388         }
389 
390         return escaped.toString();
391     }
392 
393     /*
394      * XML 1.0 does not allow control characters or unpaired Unicode surrogate codepoints.
395      * We will remove characters that do not fit in the following ranges:
396      * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
397      */
398     private static boolean supportedByXML10(final int codepoint) {
399         if (codepoint < 0x20) {
400             return codepoint == 0x9 || codepoint == 0xA || codepoint == 0xD;
401         }
402         if (codepoint <= 0xD7FF) {
403             return true;
404         }
405 
406         if (codepoint < 0xE000) {
407             return false;
408         }
409         if (codepoint <= 0xFFFD) {
410             return true;
411         }
412 
413         if (codepoint < 0x10000) {
414             return false;
415         }
416         if (codepoint <= 0x10FFFF) {
417             return true;
418         }
419 
420         return true;
421     }
422 
423     /**
424      * Returns the index within the specified string of the first occurrence of
425      * the specified search character.
426      *
427      * @param s the string to search
428      * @param searchChar the character to search for
429      * @param beginIndex the index at which to start the search
430      * @param endIndex the index at which to stop the search
431      * @return the index of the first occurrence of the character in the string or <code>-1</code>
432      */
433     public static int indexOf(final String s, final char searchChar, final int beginIndex, final int endIndex) {
434         for (int i = beginIndex; i < endIndex; i++) {
435             if (s.charAt(i) == searchChar) {
436                 return i;
437             }
438         }
439         return -1;
440     }
441 
442     /**
443      * Returns a Color parsed from the given RGB in hexadecimal notation.
444      * @param token the token to parse
445      * @return a Color whether the token is a color RGB in hexadecimal notation; otherwise null
446      */
447     public static Color asColorHexadecimal(final String token) {
448         if (token == null) {
449             return null;
450         }
451         final Matcher tmpMatcher = HEX_COLOR.matcher(token);
452         final boolean tmpFound = tmpMatcher.matches();
453         if (!tmpFound) {
454             return null;
455         }
456 
457         final String tmpHex = tmpMatcher.group(1);
458         if (tmpHex.length() == 6) {
459             final int tmpRed = Integer.parseInt(tmpHex.substring(0, 2), 16);
460             final int tmpGreen = Integer.parseInt(tmpHex.substring(2, 4), 16);
461             final int tmpBlue = Integer.parseInt(tmpHex.substring(4, 6), 16);
462             return new Color(tmpRed, tmpGreen, tmpBlue);
463         }
464 
465         final int tmpRed = Integer.parseInt(tmpHex.substring(0, 1) + tmpHex.substring(0, 1), 16);
466         final int tmpGreen = Integer.parseInt(tmpHex.substring(1, 2) + tmpHex.substring(1, 2), 16);
467         final int tmpBlue = Integer.parseInt(tmpHex.substring(2, 3) + tmpHex.substring(2, 3), 16);
468         return new Color(tmpRed, tmpGreen, tmpBlue);
469     }
470 
471     /**
472      * Returns a Color parsed from the given rgb notation if found inside the given string.
473      * @param token the token to parse
474      * @return a Color whether the token contains a color in RGB notation; otherwise null
475      */
476     public static Color findColorRGB(final String token) {
477         if (token == null) {
478             return null;
479         }
480         final Matcher tmpMatcher = RGB_COLOR.matcher(token);
481         if (!tmpMatcher.find()) {
482             return null;
483         }
484 
485         final int tmpRed = Integer.parseInt(tmpMatcher.group(1));
486         final int tmpGreen = Integer.parseInt(tmpMatcher.group(2));
487         final int tmpBlue = Integer.parseInt(tmpMatcher.group(3));
488         return new Color(tmpRed, tmpGreen, tmpBlue);
489     }
490 
491     /**
492      * Returns a Color parsed from the given rgb notation.
493      * @param token the token to parse
494      * @return a Color whether the token is a color in RGB notation; otherwise null
495      */
496     public static Color findColorRGBA(final String token) {
497         if (token == null) {
498             return null;
499         }
500         final Matcher tmpMatcher = RGBA_COLOR.matcher(token);
501         if (!tmpMatcher.find()) {
502             return null;
503         }
504 
505         final int tmpRed = Integer.parseInt(tmpMatcher.group(1));
506         final int tmpGreen = Integer.parseInt(tmpMatcher.group(2));
507         final int tmpBlue = Integer.parseInt(tmpMatcher.group(3));
508         final int tmpAlpha = (int) (Float.parseFloat(tmpMatcher.group(4)) * 255);
509         return new Color(tmpRed, tmpGreen, tmpBlue, tmpAlpha);
510     }
511 
512     /**
513      * Returns a Color parsed from the given hsl notation if found inside the given string.
514      * @param token the token to parse
515      * @return a Color whether the token contains a color in RGB notation; otherwise null
516      */
517     public static Color findColorHSL(final String token) {
518         if (token == null) {
519             return null;
520         }
521         final Matcher tmpMatcher = HSL_COLOR.matcher(token);
522         if (!tmpMatcher.find()) {
523             return null;
524         }
525 
526         final float tmpHue = Float.parseFloat(tmpMatcher.group(1)) / 360f;
527         final float tmpSaturation = Float.parseFloat(tmpMatcher.group(4)) / 100f;
528         final float tmpLightness = Float.parseFloat(tmpMatcher.group(7)) / 100f;
529         return hslToRgb(tmpHue, tmpSaturation, tmpLightness);
530     }
531 
532     /**
533      * Converts an HSL color value to RGB. Conversion formula
534      * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
535      * Assumes h, s, and l are contained in the set [0, 1]
536      *
537      * @param h the hue
538      * @param s the saturation
539      * @param l the lightness
540      * @return {@link Color}
541      */
542     private static Color hslToRgb(final float h, final float s, final float l) {
543         if (s == 0f) {
544             return new Color(to255(l), to255(l), to255(l));
545         }
546 
547         final float q = l < 0.5f ? l * (1 + s) : l + s - l * s;
548         final float p = 2 * l - q;
549         final float r = hueToRgb(p, q, h + 1f / 3f);
550         final float g = hueToRgb(p, q, h);
551         final float b = hueToRgb(p, q, h - 1f / 3f);
552 
553         return new Color(to255(r), to255(g), to255(b));
554     }
555 
556     private static float hueToRgb(final float p, final float q, float t) {
557         if (t < 0f) {
558             t += 1f;
559         }
560 
561         if (t > 1f) {
562             t -= 1f;
563         }
564 
565         if (t < 1f / 6f) {
566             return p + (q - p) * 6f * t;
567         }
568 
569         if (t < 1f / 2f) {
570             return q;
571         }
572 
573         if (t < 2f / 3f) {
574             return p + (q - p) * (2f / 3f - t) * 6f;
575         }
576 
577         return p;
578     }
579 
580     private static int to255(final float value) {
581         return (int) Math.min(255, 256 * value);
582     }
583 
584     /**
585      * Formats the specified color.
586      *
587      * @param color the color to format
588      * @return the specified color, formatted
589      */
590     public static String formatColor(final Color color) {
591         return "rgb(" + color.getRed() + ", " + color.getGreen() + ", " + color.getBlue() + ")";
592     }
593 
594     /**
595      * Sanitize a string for use in Matcher.appendReplacement.
596      * Replaces all \ with \\ and $ as \$ because they are used as control
597      * characters in appendReplacement.
598      *
599      * @param toSanitize the string to sanitize
600      * @return sanitized version of the given string
601      */
602     public static String sanitizeForAppendReplacement(final String toSanitize) {
603         return org.apache.commons.lang3.StringUtils.replaceEach(toSanitize,
604                                     new String[] {"\\", "$"}, new String[]{"\\\\", "\\$"});
605     }
606 
607     /**
608      * Sanitizes a string for use as filename.
609      * Replaces \, /, |, :, ?, *, &quot;, &lt;, &gt;, control chars by _ (underscore).
610      *
611      * @param toSanitize the string to sanitize
612      * @return sanitized version of the given string
613      */
614     public static String sanitizeForFileName(final String toSanitize) {
615         return ILLEGAL_FILE_NAME_CHARS.matcher(toSanitize).replaceAll("_");
616     }
617 
618     /**
619      * Transforms the specified string from delimiter-separated (e.g. <code>font-size</code>)
620      * to camel-cased (e.g. <code>fontSize</code>).
621      * @param string the string to camelize
622      * @return the transformed string
623      */
624     public static String cssCamelize(final String string) {
625         if (string == null) {
626             return null;
627         }
628 
629         String result = CAMELIZE_CACHE.get(string);
630         if (null != result) {
631             return result;
632         }
633 
634         // not found in CamelizeCache_; convert and store in cache
635         final int pos = string.indexOf('-');
636         if (pos == -1 || pos == string.length() - 1) {
637             // cache also this strings for performance
638             CAMELIZE_CACHE.put(string, string);
639             return string;
640         }
641 
642         final StringBuilder builder = new StringBuilder(string);
643         builder.deleteCharAt(pos);
644         builder.setCharAt(pos, Character.toUpperCase(builder.charAt(pos)));
645 
646         int i = pos + 1;
647         while (i < builder.length() - 1) {
648             if (builder.charAt(i) == '-') {
649                 builder.deleteCharAt(i);
650                 builder.setCharAt(i, Character.toUpperCase(builder.charAt(i)));
651             }
652             i++;
653         }
654         result = builder.toString();
655         CAMELIZE_CACHE.put(string, result);
656 
657         return result;
658     }
659 
660     /**
661      * Lowercases a string by checking and check for null first. There
662      * is no cache involved and the ROOT locale is used to convert it.
663      *
664      * @param s the string to lowercase
665      * @return the lowercased string
666      */
667     public static String toRootLowerCase(final String s) {
668         return s == null ? null : s.toLowerCase(Locale.ROOT);
669     }
670 
671     /**
672      * Transforms the specified string from camel-cased (e.g. <code>fontSize</code>)
673      * to delimiter-separated (e.g. <code>font-size</code>).
674      * to camel-cased .
675      * @param string the string to decamelize
676      * @return the transformed string
677      */
678     public static String cssDeCamelize(final String string) {
679         if (string == null || string.isEmpty()) {
680             return string;
681         }
682 
683         final StringBuilder builder = new StringBuilder();
684         for (int i = 0; i < string.length(); i++) {
685             final char ch = string.charAt(i);
686             if (Character.isUpperCase(ch)) {
687                 builder.append('-').append(Character.toLowerCase(ch));
688             }
689             else {
690                 builder.append(ch);
691             }
692         }
693         return builder.toString();
694     }
695 
696     /**
697      * Converts a string into a byte array using the specified encoding.
698      *
699      * @param charset the charset
700      * @param content the string to convert
701      * @return the String as a byte[]; if the specified encoding is not supported an empty byte[] will be returned
702      */
703     public static byte[] toByteArray(final String content, final Charset charset) {
704         if (content ==  null || content.isEmpty()) {
705             return new byte[0];
706         }
707 
708         return content.getBytes(charset);
709     }
710 
711     /**
712      * Splits the provided text into an array, using whitespace as the
713      * separator.
714      * Whitespace is defined by {@link Character#isWhitespace(char)}.
715      *
716      * @param str  the String to parse, may be null
717      * @return an array of parsed Strings, an empty array if null String input
718      */
719     public static String[] splitAtJavaWhitespace(final String str) {
720         final String[] parts = org.apache.commons.lang3.StringUtils.split(str);
721         if (parts == null) {
722             return new String[0];
723         }
724         return parts;
725     }
726 
727     /**
728      * Splits the provided text into an array, using blank as the
729      * separator.
730      *
731      * @param str  the String to parse, may be null
732      * @return an array of parsed Strings, an empty array if null String input
733      */
734     public static String[] splitAtBlank(final String str) {
735         final String[] parts = org.apache.commons.lang3.StringUtils.split(str, ' ');
736         if (parts == null) {
737             return new String[0];
738         }
739         return parts;
740     }
741 
742     /**
743      * Splits the provided text into an array, using blank as the
744      * separator.
745      *
746      * @param str  the String to parse, may be null
747      * @return an array of parsed Strings, an empty array if null String input
748      */
749     public static String[] splitAtComma(final String str) {
750         final String[] parts = org.apache.commons.lang3.StringUtils.split(str, ',');
751         if (parts == null) {
752             return new String[0];
753         }
754         return parts;
755     }
756 
757     /**
758      * Splits the provided text into an array, using comma or blank as the
759      * separator.
760      *
761      * @param str the String to parse, may be null
762      * @return an array of parsed Strings, an empty array if null String input
763      */
764     public static String[] splitAtCommaOrBlank(final String str) {
765         final String[] parts = org.apache.commons.lang3.StringUtils.split(str, ", ");
766         if (parts == null) {
767             return new String[0];
768         }
769         return parts;
770     }
771 }