This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new a3c24376ac StringUtils improvements
a3c24376ac is described below

commit a3c24376ac639a425d4195de29f88f544a2ff6b3
Author: James Bognar <[email protected]>
AuthorDate: Sun Nov 30 10:49:31 2025 -0500

    StringUtils improvements
---
 .../juneau/junit/bct/BasicBeanConverter.java       |  24 +-
 .../org/apache/juneau/junit/bct/BctAssertions.java |   2 +-
 .../juneau/junit/bct/PropertyExtractors.java       |   4 +-
 .../apache/juneau/common/utils/StringUtils.java    | 366 ++++++++++++++-------
 .../java/org/apache/juneau/common/utils/Utils.java | 252 ++------------
 .../java/org/apache/juneau/rest/RestRequest.java   |   3 +-
 .../juneau/common/utils/StringUtils_Test.java      |  48 ++-
 7 files changed, 349 insertions(+), 350 deletions(-)

diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
index 0f2585df6c..fd41d95eca 100644
--- 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
@@ -899,11 +899,31 @@ public class BasicBeanConverter implements BeanConverter {
                var c = o.getClass();
                var stringifier = stringifierMap.computeIfAbsent(c, 
this::findStringifier);
                if (stringifier.isEmpty()) {
-                       stringifier = of(canListify(o) ? (bc, o2) -> 
bc.stringify(bc.listify(o2)) : (bc, o2) -> safeToString(o2));
+                       stringifier = of(canListify(o) ? (bc, o2) -> 
bc.stringify(bc.listify(o2)) : (bc, o2) -> this.safeToString(o2));
                        stringifierMap.putIfAbsent(c, stringifier);
                }
                var o2 = o;
-               return stringifier.map(x -> (Stringifier)x).map(x -> 
x.apply(this, o2)).map(Utils::safeToString).orElse(null);
+               return stringifier.map(x -> (Stringifier)x).map(x -> 
x.apply(this, o2)).map(this::safeToString).orElse(null);
+       }
+
+       /**
+        * Safely converts an object to a string, catching any exceptions 
thrown by toString().
+        *
+        * <p>
+        * This helper method ensures that exceptions thrown by problematic 
{@code toString()} implementations
+        * don't propagate up the call stack. Instead, it returns a descriptive 
error message containing
+        * the exception type and message.
+        *
+        * @param o The object to convert to a string. May be any object 
including <jk>null</jk>.
+        * @return The string representation of the object, or a formatted 
error message if toString() throws an exception.
+        *    Returns <js>"null"</js> if the object is <jk>null</jk>.
+        */
+       private String safeToString(Object o) {
+               try {
+                       return o.toString();
+               } catch (Throwable t) { // NOSONAR
+                       return cns(t) + ": " + t.getMessage();
+               }
        }
 
        /**
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
index b0b87c52be..bb3d14b7e7 100644
--- 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
@@ -912,7 +912,7 @@ public class BctAssertions {
                assertNotNull(value, "Value was null.");
 
                var v = 
args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(value);
-               var m = getGlobMatchPattern(pattern).matcher(v);
+               var m = StringUtils.getGlobMatchPattern(pattern).matcher(v);
                assertTrue(m.matches(), args.getMessage("Pattern didn''t match. 
==> pattern: <{0}> but was: <{1}>", pattern, v));
        }
 
diff --git 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/PropertyExtractors.java
 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/PropertyExtractors.java
index 828ceea51c..0ba796948a 100644
--- 
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/PropertyExtractors.java
+++ 
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/PropertyExtractors.java
@@ -21,6 +21,8 @@ import static org.apache.juneau.common.utils.Utils.*;
 import java.lang.reflect.*;
 import java.util.*;
 
+import org.apache.juneau.common.utils.*;
+
 /**
  * Collection of standard property extractor implementations for the 
Bean-Centric Testing framework.
  *
@@ -105,7 +107,7 @@ public class PropertyExtractors {
                public Object extract(BeanConverter converter, Object o, String 
name) {
                        var l = converter.listify(o);
                        if (name.matches("-?\\d+")) {
-                               var index = parseInt(name);
+                               var index = StringUtils.parseInt(name);
                                if (index < 0) {
                                        index = l.size() + index; // Convert 
negative index to positive
                                }
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
index f59b9fbda8..1116d03605 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/StringUtils.java
@@ -2044,12 +2044,12 @@ public class StringUtils {
        public static boolean isValidMacAddress(String mac) {
                if (isEmpty(mac))
                        return false;
-               
+
                // Remove separators and check if it's 12 hex digits
                var cleaned = mac.replaceAll("[:-]", "").toUpperCase();
                if (cleaned.length() != 12)
                        return false;
-               
+
                // Check if all characters are valid hex digits
                return cleaned.matches("^[0-9A-F]{12}$");
        }
@@ -2081,41 +2081,41 @@ public class StringUtils {
        public static boolean isValidHostname(String hostname) {
                if (isEmpty(hostname))
                        return false;
-               
+
                // Cannot start or end with a dot
                if (hostname.startsWith(".") || hostname.endsWith("."))
                        return false;
-               
+
                // Total length cannot exceed 253 characters
                if (hostname.length() > 253)
                        return false;
-               
+
                // Split by dots (use -1 to preserve trailing empty strings)
                var labels = hostname.split("\\.", -1);
-               
+
                // Must have at least one label
                if (labels.length == 0)
                        return false;
-               
+
                // Check each label
                for (var label : labels) {
                        // Label cannot be empty
                        if (label.isEmpty())
                                return false;
-                       
+
                        // Label cannot exceed 63 characters
                        if (label.length() > 63)
                                return false;
-                       
+
                        // Label cannot start or end with hyphen
                        if (label.startsWith("-") || label.endsWith("-"))
                                return false;
-                       
+
                        // Label can only contain letters, digits, and hyphens
                        if (! label.matches("^[a-zA-Z0-9-]+$"))
                                return false;
                }
-               
+
                return true;
        }
 
@@ -2139,10 +2139,10 @@ public class StringUtils {
        public static int wordCount(String str) {
                if (isEmpty(str))
                        return 0;
-               
+
                var count = 0;
                var inWord = false;
-               
+
                for (var i = 0; i < str.length(); i++) {
                        var c = str.charAt(i);
                        if (Character.isLetterOrDigit(c) || c == '_') {
@@ -2154,7 +2154,7 @@ public class StringUtils {
                                inWord = false;
                        }
                }
-               
+
                return count;
        }
 
@@ -2177,7 +2177,7 @@ public class StringUtils {
        public static int lineCount(String str) {
                if (isEmpty(str))
                        return 0;
-               
+
                var count = 1; // At least one line
                for (var i = 0; i < str.length(); i++) {
                        var c = str.charAt(i);
@@ -2191,7 +2191,7 @@ public class StringUtils {
                                count++;
                        }
                }
-               
+
                return count;
        }
 
@@ -2214,11 +2214,11 @@ public class StringUtils {
        public static char mostFrequentChar(String str) {
                if (isEmpty(str))
                        return '\0';
-               
+
                var charCounts = new int[Character.MAX_VALUE + 1];
                var maxCount = 0;
                var maxChar = '\0';
-               
+
                // Count occurrences of each character
                for (var i = 0; i < str.length(); i++) {
                        var c = str.charAt(i);
@@ -2228,7 +2228,7 @@ public class StringUtils {
                                maxChar = c;
                        }
                }
-               
+
                return maxChar;
        }
 
@@ -2254,17 +2254,17 @@ public class StringUtils {
        public static double entropy(String str) {
                if (isEmpty(str))
                        return 0.0;
-               
+
                var length = str.length();
                if (length == 0)
                        return 0.0;
-               
+
                // Count character frequencies
                var charCounts = new int[Character.MAX_VALUE + 1];
                for (var i = 0; i < length; i++) {
                        charCounts[str.charAt(i)]++;
                }
-               
+
                // Calculate entropy
                var entropy = 0.0;
                for (var count : charCounts) {
@@ -2273,7 +2273,7 @@ public class StringUtils {
                                entropy -= probability * (Math.log(probability) 
/ Math.log(2.0));
                        }
                }
-               
+
                return entropy;
        }
 
@@ -2300,11 +2300,11 @@ public class StringUtils {
        public static double readabilityScore(String str) {
                if (isEmpty(str))
                        return 0.0;
-               
+
                var words = extractWords(str);
                if (words.isEmpty())
                        return 0.0;
-               
+
                // Count sentences (ending with . ! ?)
                var sentenceCount = 0;
                for (var i = 0; i < str.length(); i++) {
@@ -2315,22 +2315,22 @@ public class StringUtils {
                }
                if (sentenceCount == 0)
                        sentenceCount = 1; // At least one sentence
-               
+
                // Calculate average words per sentence
                var avgWordsPerSentence = (double)words.size() / sentenceCount;
-               
+
                // Estimate average syllables per word (simplified: count vowel 
groups)
                var totalSyllables = 0;
                for (var word : words) {
                        totalSyllables += estimateSyllables(word);
                }
                var avgSyllablesPerWord = (double)totalSyllables / words.size();
-               
+
                // Simplified Flesch Reading Ease formula
                // Score = 206.835 - (1.015 * ASL) - (84.6 * ASW)
                // Where ASL = average sentence length (words), ASW = average 
syllables per word
                var score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * 
avgSyllablesPerWord);
-               
+
                // Clamp to 0-100 range
                return Math.max(0.0, Math.min(100.0, score));
        }
@@ -2341,26 +2341,26 @@ public class StringUtils {
        private static int estimateSyllables(String word) {
                if (word == null || word.isEmpty())
                        return 1;
-               
+
                var lower = word.toLowerCase();
                var count = 0;
                var prevWasVowel = false;
-               
+
                for (var i = 0; i < lower.length(); i++) {
                        var c = lower.charAt(i);
                        var isVowel = (c == 'a' || c == 'e' || c == 'i' || c == 
'o' || c == 'u' || c == 'y');
-                       
+
                        if (isVowel && ! prevWasVowel) {
                                count++;
                        }
                        prevWasVowel = isVowel;
                }
-               
+
                // Handle silent 'e' at the end
                if (lower.endsWith("e") && count > 1) {
                        count--;
                }
-               
+
                // At least one syllable
                return Math.max(1, count);
        }
@@ -2575,11 +2575,11 @@ public class StringUtils {
                        return null;
                if (variables == null || variables.isEmpty())
                        return template;
-               
+
                var result = new StringBuilder();
                var i = 0;
                var length = template.length();
-               
+
                while (i < length) {
                        var dollarIndex = template.indexOf("${", i);
                        if (dollarIndex == -1) {
@@ -2587,10 +2587,10 @@ public class StringUtils {
                                result.append(template.substring(i));
                                break;
                        }
-                       
+
                        // Append text before the variable
                        result.append(template.substring(i, dollarIndex));
-                       
+
                        // Find the closing brace
                        var braceIndex = template.indexOf('}', dollarIndex + 2);
                        if (braceIndex == -1) {
@@ -2598,11 +2598,11 @@ public class StringUtils {
                                result.append(template.substring(dollarIndex));
                                break;
                        }
-                       
+
                        // Extract variable name
                        var varName = template.substring(dollarIndex + 2, 
braceIndex);
                        var value = variables.get(varName);
-                       
+
                        if (variables.containsKey(varName)) {
                                // Variable exists in map (even if null)
                                result.append(value != null ? value.toString() 
: "null");
@@ -2610,10 +2610,10 @@ public class StringUtils {
                                // Variable not found, keep the original 
placeholder
                                result.append("${").append(varName).append("}");
                        }
-                       
+
                        i = braceIndex + 1;
                }
-               
+
                return result.toString();
        }
 
@@ -2641,15 +2641,15 @@ public class StringUtils {
                        return word;
                if (count == 1)
                        return word;
-               
+
                var lower = word.toLowerCase();
                var length = word.length();
-               
+
                // Words ending in s, x, z, ch, sh -> add "es"
                if (lower.endsWith("s") || lower.endsWith("x") || 
lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
                        return word + "es";
                }
-               
+
                // Words ending in "y" preceded by a consonant -> replace "y" 
with "ies"
                if (length > 1 && lower.endsWith("y")) {
                        var secondLast = lower.charAt(length - 2);
@@ -2657,7 +2657,7 @@ public class StringUtils {
                                return word.substring(0, length - 1) + "ies";
                        }
                }
-               
+
                // Words ending in "f" or "fe" -> replace with "ves" (basic 
rule)
                if (lower.endsWith("f")) {
                        return word.substring(0, length - 1) + "ves";
@@ -2665,7 +2665,7 @@ public class StringUtils {
                if (lower.endsWith("fe")) {
                        return word.substring(0, length - 2) + "ves";
                }
-               
+
                // Default: add "s"
                return word + "s";
        }
@@ -2689,7 +2689,7 @@ public class StringUtils {
        public static String ordinal(int number) {
                var abs = Math.abs(number);
                var suffix = "th";
-               
+
                // Special cases for 11, 12, 13 (all use "th")
                if (abs % 100 != 11 && abs % 100 != 12 && abs % 100 != 13) {
                        var lastDigit = abs % 10;
@@ -2700,7 +2700,7 @@ public class StringUtils {
                        else if (lastDigit == 3)
                                suffix = "rd";
                }
-               
+
                return number + suffix;
        }
 
@@ -2982,16 +2982,16 @@ public class StringUtils {
                        return -1;
                if (str2 == null)
                        return 1;
-               
+
                var len1 = str1.length();
                var len2 = str2.length();
                var i1 = 0;
                var i2 = 0;
-               
+
                while (i1 < len1 && i2 < len2) {
                        var c1 = str1.charAt(i1);
                        var c2 = str2.charAt(i2);
-                       
+
                        // If both are digits, compare numerically
                        if (Character.isDigit(c1) && Character.isDigit(c2)) {
                                // Skip leading zeros
@@ -2999,7 +2999,7 @@ public class StringUtils {
                                        i1++;
                                while (i2 < len2 && str2.charAt(i2) == '0')
                                        i2++;
-                               
+
                                // Find end of number sequences
                                var end1 = i1;
                                var end2 = i2;
@@ -3007,13 +3007,13 @@ public class StringUtils {
                                        end1++;
                                while (end2 < len2 && 
Character.isDigit(str2.charAt(end2)))
                                        end2++;
-                               
+
                                // Compare lengths first (longer number is 
larger)
                                var lenNum1 = end1 - i1;
                                var lenNum2 = end2 - i2;
                                if (lenNum1 != lenNum2)
                                        return lenNum1 - lenNum2;
-                               
+
                                // Same length, compare digit by digit
                                for (var j = 0; j < lenNum1; j++) {
                                        var d1 = str1.charAt(i1 + j);
@@ -3021,7 +3021,7 @@ public class StringUtils {
                                        if (d1 != d2)
                                                return d1 - d2;
                                }
-                               
+
                                i1 = end1;
                                i2 = end2;
                        } else {
@@ -3033,7 +3033,7 @@ public class StringUtils {
                                i2++;
                        }
                }
-               
+
                return len1 - len2;
        }
 
@@ -3059,18 +3059,18 @@ public class StringUtils {
                        str1 = "";
                if (str2 == null)
                        str2 = "";
-               
+
                var len1 = str1.length();
                var len2 = str2.length();
-               
+
                // Use dynamic programming with optimized space (only need 
previous row)
                var prev = new int[len2 + 1];
                var curr = new int[len2 + 1];
-               
+
                // Initialize first row
                for (var j = 0; j <= len2; j++)
                        prev[j] = j;
-               
+
                for (var i = 1; i <= len1; i++) {
                        curr[0] = i;
                        for (var j = 1; j <= len2; j++) {
@@ -3085,7 +3085,7 @@ public class StringUtils {
                        prev = curr;
                        curr = temp;
                }
-               
+
                return prev[len2];
        }
 
@@ -3111,14 +3111,14 @@ public class StringUtils {
                        str1 = "";
                if (str2 == null)
                        str2 = "";
-               
+
                if (str1.equals(str2))
                        return 1.0;
-               
+
                var maxLen = Math.max(str1.length(), str2.length());
                if (maxLen == 0)
                        return 1.0;
-               
+
                var distance = levenshteinDistance(str1, str2);
                return 1.0 - ((double)distance / maxLen);
        }
@@ -3659,7 +3659,7 @@ public class StringUtils {
         */
        public static int parseIntWithSuffix(String s) {
                assertArgNotNull("s", s);
-               var m = multiplier(s);
+               var m = multiplierInt(s);
                if (m == 1)
                        return Integer.decode(s);
                return Integer.decode(s.substring(0, s.length() - 1).trim()) * 
m;  // NOSONAR - NPE not possible here.
@@ -3741,7 +3741,7 @@ public class StringUtils {
         */
        public static long parseLongWithSuffix(String s) {
                assertArgNotNull("s", s);
-               var m = multiplier2(s);
+               var m = multiplierLong(s);
                if (m == 1)
                        return Long.decode(s);
                return Long.decode(s.substring(0, s.length() - 1).trim()) * m;  
// NOSONAR - NPE not possible here.
@@ -4249,14 +4249,14 @@ public class StringUtils {
                        // Split into words first, then combine words that fit
                        var words = line.split(" +");  // Split on one or more 
spaces
                        var currentLine = new StringBuilder();
-                       
+
                        for (var word : words) {
                                if (word.isEmpty())
                                        continue;
-                                       
+
                                var wordLength = word.length();
                                var currentLength = currentLine.length();
-                               
+
                                if (currentLength == 0) {
                                        // First word on line
                                        // Only break single words if there are 
multiple words in the input
@@ -4314,7 +4314,7 @@ public class StringUtils {
                                        }
                                }
                        }
-                       
+
                        // Append any remaining line
                        if (currentLine.length() > 0) {
                                if (result.length() > 0)
@@ -4362,6 +4362,10 @@ public class StringUtils {
         * For un-numbered sequential placeholders <js>"{}"</js>, use {@link 
#format(String, Object...)} instead.
         *
         * <p>
+        * Variable values are converted to strings using {@link 
#readable(Object)} to ensure consistent,
+        * readable formatting (e.g., byte arrays are converted to hex, 
collections are formatted without spaces).
+        *
+        * <p>
         * Nested variables are supported in both the input string and map 
values.
         *
         * <p>
@@ -4424,7 +4428,7 @@ public class StringUtils {
                                                else {
                                                        if (val == null)
                                                                val = "";
-                                                       var v = val.toString();
+                                                       var v = r(val);
                                                        // If the replacement 
also contains variables, replace them now.
                                                        if (v.indexOf('{') != 
-1)
                                                                v = 
formatNamed(v, m);
@@ -5182,7 +5186,7 @@ public class StringUtils {
                var result = new LinkedHashMap<String,String>();
                if (isEmpty(str))
                        return result;
-               
+
                var entries = split(str, entryDelimiter);
                for (var entry : entries) {
                        if (isEmpty(entry))
@@ -5225,7 +5229,7 @@ public class StringUtils {
        public static List<String> extractNumbers(String str) {
                if (isEmpty(str))
                        return Collections.emptyList();
-               
+
                var result = new ArrayList<String>();
                var pattern = Pattern.compile("\\d+(?:\\.\\d+)?");
                var matcher = pattern.matcher(str);
@@ -5253,7 +5257,7 @@ public class StringUtils {
        public static List<String> extractEmails(String str) {
                if (isEmpty(str))
                        return Collections.emptyList();
-               
+
                var result = new ArrayList<String>();
                // Email regex pattern (same as isEmail but without ^ and $ 
anchors)
                var pattern = 
Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
@@ -5282,7 +5286,7 @@ public class StringUtils {
        public static List<String> extractUrls(String str) {
                if (isEmpty(str))
                        return Collections.emptyList();
-               
+
                var result = new ArrayList<String>();
                // Basic URL pattern: protocol://domain/path
                var pattern = 
Pattern.compile("(?:https?|ftp)://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+", 
Pattern.CASE_INSENSITIVE);
@@ -5311,7 +5315,7 @@ public class StringUtils {
        public static List<String> extractWords(String str) {
                if (isEmpty(str))
                        return Collections.emptyList();
-               
+
                var result = new ArrayList<String>();
                // Word pattern: sequence of word characters (letters, digits, 
underscore)
                var pattern = Pattern.compile("\\w+");
@@ -5344,7 +5348,7 @@ public class StringUtils {
        public static List<String> extractBetween(String str, String start, 
String end) {
                if (isEmpty(str) || isEmpty(start) || isEmpty(end))
                        return Collections.emptyList();
-               
+
                var result = new ArrayList<String>();
                var startIndex = 0;
                while (true) {
@@ -5389,7 +5393,7 @@ public class StringUtils {
                        return str;
                if (fromChars.length() != toChars.length())
                        throw new IllegalArgumentException("fromChars and 
toChars must have the same length");
-               
+
                var sb = new StringBuilder(str.length());
                for (var i = 0; i < str.length(); i++) {
                        var c = str.charAt(i);
@@ -5422,16 +5426,16 @@ public class StringUtils {
        public static String soundex(String str) {
                if (isEmpty(str))
                        return null;
-               
+
                var upper = str.toUpperCase();
                var result = new StringBuilder(4);
                result.append(upper.charAt(0));
-               
+
                // Soundex mapping: 0 = AEIOUHWY, 1 = BFPV, 2 = CGJKQSXZ, 3 = 
DT, 4 = L, 5 = MN, 6 = R
                // H/W/Y don't get codes but don't break sequences either
                // Initialize lastCode to a value that won't match any real code
                var lastCode = '\0';
-               
+
                for (var i = 1; i < upper.length() && result.length() < 4; i++) 
{
                        var c = upper.charAt(i);
                        var code = getSoundexCode(c);
@@ -5446,12 +5450,12 @@ public class StringUtils {
                        }
                        // If code == lastCode, skip it (consecutive same codes)
                }
-               
+
                // Pad with zeros if needed
                while (result.length() < 4) {
                        result.append('0');
                }
-               
+
                return result.toString();
        }
 
@@ -5496,15 +5500,15 @@ public class StringUtils {
        public static String metaphone(String str) {
                if (isEmpty(str))
                        return null;
-               
+
                var upper = str.toUpperCase().replaceAll("[^A-Z]", "");
                if (upper.isEmpty())
                        return "";
-               
+
                var result = new StringBuilder();
                var i = 0;
                var len = upper.length();
-               
+
                // Handle initial characters
                if (upper.startsWith("KN") || upper.startsWith("GN") || 
upper.startsWith("PN") || upper.startsWith("AE") || upper.startsWith("WR")) {
                        i = 1;
@@ -5515,20 +5519,20 @@ public class StringUtils {
                        result.append('W');
                        i = 2;
                }
-               
+
                // Process remaining characters
                while (i < len && result.length() < 4) {
                        var c = upper.charAt(i);
                        var prev = i > 0 ? upper.charAt(i - 1) : '\0';
                        var next = i < len - 1 ? upper.charAt(i + 1) : '\0';
                        var next2 = i < len - 2 ? upper.charAt(i + 2) : '\0';
-                       
+
                        // Skip duplicates (except C)
                        if (c == prev && c != 'C') {
                                i++;
                                continue;
                        }
-                       
+
                        switch (c) {
                                case 'B':
                                        if (prev != 'M' || next != '\0')
@@ -5636,7 +5640,7 @@ public class StringUtils {
                        }
                        i++;
                }
-               
+
                return result.length() > 0 ? result.toString() : 
upper.substring(0, Math.min(1, upper.length()));
        }
 
@@ -5666,16 +5670,16 @@ public class StringUtils {
        public static String[] doubleMetaphone(String str) {
                if (isEmpty(str))
                        return null;
-               
+
                // For simplicity, return the same code for both primary and 
alternate
                // A full Double Metaphone implementation would be much more 
complex
                var primary = metaphone(str);
                if (primary == null)
                        return null;
-               
+
                // Generate alternate code (simplified - full implementation 
would have different rules)
                var alternate = primary;
-               
+
                return new String[] { primary, alternate };
        }
 
@@ -5719,10 +5723,10 @@ public class StringUtils {
        public static String removeAccents(String str) {
                if (str == null)
                        return null;
-               
+
                // Normalize to NFD (decomposed form)
                var normalized = Normalizer.normalize(str, Normalizer.Form.NFD);
-               
+
                // Remove combining diacritical marks (Unicode category Mn)
                var sb = new StringBuilder(normalized.length());
                for (var i = 0; i < normalized.length(); i++) {
@@ -5733,7 +5737,7 @@ public class StringUtils {
                                sb.append(c);
                        }
                }
-               
+
                return sb.toString();
        }
 
@@ -6640,7 +6644,7 @@ public class StringUtils {
         * @param in The string to append.
         * @return The StringBuilder with the string appended.
         */
-       private static StringBuilder append(StringBuilder sb, String in) {
+       public static StringBuilder append(StringBuilder sb, String in) {
                if (sb == null)
                        return new StringBuilder(in);
                sb.append(in);
@@ -6677,7 +6681,7 @@ public class StringUtils {
         * @param s The string to analyze for multiplier suffix.
         * @return The multiplier value (1 if no valid suffix found).
         */
-       private static int multiplier(String s) {
+       private static int multiplierInt(String s) {
                var c = isEmpty(s) ? 'z' : s.charAt(s.length() - 1);
                if (c == 'G')
                        return 1024 * 1024 * 1024;
@@ -6700,7 +6704,7 @@ public class StringUtils {
         * @param s The string to analyze for multiplier suffix.
         * @return The multiplier value (1 if no valid suffix found).
         */
-       private static long multiplier2(String s) {
+       private static long multiplierLong(String s) {
                var c = isEmpty(s) ? 'z' : s.charAt(s.length() - 1);
                if (c == 'P')
                        return 1024 * 1024 * 1024 * 1024 * 1024l;
@@ -6769,7 +6773,7 @@ public class StringUtils {
         * @param s The string to convert.
         * @return The lowercase string, or <jk>null</jk> if the input was 
<jk>null</jk>.
         */
-       public static String lc(String s) {
+       public static String lowerCase(String s) {
                return s == null ? null : s.toLowerCase();
        }
 
@@ -6779,7 +6783,7 @@ public class StringUtils {
         * @param s The string to convert.
         * @return The uppercase string, or <jk>null</jk> if the input was 
<jk>null</jk>.
         */
-       public static String uc(String s) {
+       public static String upperCase(String s) {
                return s == null ? null : s.toUpperCase();
        }
 
@@ -7169,6 +7173,7 @@ public class StringUtils {
         * <p>
         * Returns the same StringBuilder instance for method chaining.
         * If the string is <jk>null</jk> or empty, nothing is appended.
+        * If <c>sb</c> is <jk>null</jk> and an append is going to occur, a new 
StringBuilder is automatically created.
         *
         * <h5 class='section'>Examples:</h5>
         * <p class='bjava'>
@@ -7178,17 +7183,22 @@ public class StringUtils {
         *      appendIfNotEmpty(<jv>sb</jv>, <jk>null</jk>);     <jc>// Does 
nothing</jc>
         *      appendIfNotEmpty(<jv>sb</jv>, <js>"world"</js>);  <jc>// 
Appends "world"</jc>
         *      <jc>// Result: "helloworld"</jc>
+        *
+        *      <jc>// Auto-create StringBuilder if null and append occurs</jc>
+        *      StringBuilder <jv>sb2</jv> = appendIfNotEmpty(<jk>null</jk>, 
<js>"test"</js>);  <jc>// Creates new StringBuilder with "test"</jc>
+        *      StringBuilder <jv>sb3</jv> = appendIfNotEmpty(<jk>null</jk>, 
<jk>null</jk>);   <jc>// Returns null (no append occurred)</jc>
         * </p>
         *
-        * @param sb The StringBuilder to append to. Must not be <jk>null</jk>.
+        * @param sb The StringBuilder to append to. Can be <jk>null</jk>.
         * @param str The string to append if not empty. Can be <jk>null</jk>.
-        * @return The same StringBuilder instance for method chaining.
-        * @throws IllegalArgumentException If <c>sb</c> is <jk>null</jk>.
+        * @return The same StringBuilder instance for method chaining, or a 
new StringBuilder if <c>sb</c> was <jk>null</jk> and an append occurred, or 
<jk>null</jk> if <c>sb</c> was <jk>null</jk> and no append occurred.
         */
        public static StringBuilder appendIfNotEmpty(StringBuilder sb, String 
str) {
-               assertArgNotNull("sb", sb);
-               if (isNotEmpty(str))
+               if (isNotEmpty(str)) {
+                       if (sb == null)
+                               sb = new StringBuilder();
                        sb.append(str);
+               }
                return sb;
        }
 
@@ -7198,6 +7208,7 @@ public class StringUtils {
         * <p>
         * Returns the same StringBuilder instance for method chaining.
         * If the string is <jk>null</jk>, empty, or contains only whitespace, 
nothing is appended.
+        * If <c>sb</c> is <jk>null</jk> and an append is going to occur, a new 
StringBuilder is automatically created.
         *
         * <h5 class='section'>Examples:</h5>
         * <p class='bjava'>
@@ -7207,17 +7218,22 @@ public class StringUtils {
         *      appendIfNotBlank(<jv>sb</jv>, <jk>null</jk>);     <jc>// Does 
nothing</jc>
         *      appendIfNotBlank(<jv>sb</jv>, <js>"world"</js>);  <jc>// 
Appends "world"</jc>
         *      <jc>// Result: "helloworld"</jc>
+        *
+        *      <jc>// Auto-create StringBuilder if null and append occurs</jc>
+        *      StringBuilder <jv>sb2</jv> = appendIfNotBlank(<jk>null</jk>, 
<js>"test"</js>);  <jc>// Creates new StringBuilder with "test"</jc>
+        *      StringBuilder <jv>sb3</jv> = appendIfNotBlank(<jk>null</jk>, 
<js>"   "</js>);   <jc>// Returns null (no append occurred)</jc>
         * </p>
         *
-        * @param sb The StringBuilder to append to. Must not be <jk>null</jk>.
+        * @param sb The StringBuilder to append to. Can be <jk>null</jk>.
         * @param str The string to append if not blank. Can be <jk>null</jk>.
-        * @return The same StringBuilder instance for method chaining.
-        * @throws IllegalArgumentException If <c>sb</c> is <jk>null</jk>.
+        * @return The same StringBuilder instance for method chaining, or a 
new StringBuilder if <c>sb</c> was <jk>null</jk> and an append occurred, or 
<jk>null</jk> if <c>sb</c> was <jk>null</jk> and no append occurred.
         */
        public static StringBuilder appendIfNotBlank(StringBuilder sb, String 
str) {
-               assertArgNotNull("sb", sb);
-               if (isNotBlank(str))
+               if (isNotBlank(str)) {
+                       if (sb == null)
+                               sb = new StringBuilder();
                        sb.append(str);
+               }
                return sb;
        }
 
@@ -7228,6 +7244,7 @@ public class StringUtils {
         * Returns the same StringBuilder instance for method chaining.
         * If the StringBuilder is empty, only the string is appended (no 
separator).
         * If the StringBuilder is not empty, the separator is appended first, 
then the string.
+        * If <c>sb</c> is <jk>null</jk> and an append is going to occur, a new 
StringBuilder is automatically created.
         *
         * <h5 class='section'>Examples:</h5>
         * <p class='bjava'>
@@ -7236,18 +7253,22 @@ public class StringUtils {
         *      appendWithSeparator(<jv>sb</jv>, <js>"second"</js>, <js>", 
"</js>);  <jc>// Appends ", second"</jc>
         *      appendWithSeparator(<jv>sb</jv>, <js>"third"</js>, <js>", 
"</js>);   <jc>// Appends ", third"</jc>
         *      <jc>// Result: "first, second, third"</jc>
+        *
+        *      <jc>// Auto-create StringBuilder if null and append occurs</jc>
+        *      StringBuilder <jv>sb2</jv> = appendWithSeparator(<jk>null</jk>, 
<js>"test"</js>, <js>", "</js>);  <jc>// Creates new StringBuilder with 
"test"</jc>
+        *      StringBuilder <jv>sb3</jv> = appendWithSeparator(<jk>null</jk>, 
<jk>null</jk>, <js>", "</js>);   <jc>// Returns null (no append occurred)</jc>
         * </p>
         *
-        * @param sb The StringBuilder to append to. Must not be <jk>null</jk>.
+        * @param sb The StringBuilder to append to. Can be <jk>null</jk>.
         * @param str The string to append. Can be <jk>null</jk>.
         * @param separator The separator to add before the string if the 
StringBuilder is not empty. Can be <jk>null</jk>.
-        * @return The same StringBuilder instance for method chaining.
-        * @throws IllegalArgumentException If <c>sb</c> is <jk>null</jk>.
+        * @return The same StringBuilder instance for method chaining, or a 
new StringBuilder if <c>sb</c> was <jk>null</jk> and an append occurred, or 
<jk>null</jk> if <c>sb</c> was <jk>null</jk> and no append occurred.
         */
        public static StringBuilder appendWithSeparator(StringBuilder sb, 
String str, String separator) {
-               assertArgNotNull("sb", sb);
                if (str != null) {
-                       if (sb.length() > 0 && separator != null)
+                       if (sb == null)
+                               sb = new StringBuilder();
+                       else if (sb.length() > 0 && separator != null)
                                sb.append(separator);
                        sb.append(str);
                }
@@ -7445,4 +7466,115 @@ public class StringUtils {
         * Constructor.
         */
        protected StringUtils() {}
+
+       /**
+        * Same as {@link Float#parseFloat(String)} but removes any underscore 
characters first.
+        *
+        * <p>Allows for better readability of numeric literals (e.g., 
<js>"1_000.5"</js>).
+        *
+        * @param value The string to parse.
+        * @return The parsed float value.
+        * @throws NumberFormatException If the string cannot be parsed.
+        * @throws NullPointerException If the string is <jk>null</jk>.
+        */
+       public static float parseFloat(String value) {
+               return Float.parseFloat(StringUtils.removeUnderscores(value));
+       }
+
+       /**
+        * Same as {@link Integer#parseInt(String)} but removes any underscore 
characters first.
+        *
+        * <p>Allows for better readability of numeric literals (e.g., 
<js>"1_000_000"</js>).
+        *
+        * @param value The string to parse.
+        * @return The parsed integer value.
+        * @throws NumberFormatException If the string cannot be parsed.
+        * @throws NullPointerException If the string is <jk>null</jk>.
+        */
+       public static int parseInt(String value) {
+               return Integer.parseInt(StringUtils.removeUnderscores(value));
+       }
+
+       /**
+        * Same as {@link Long#parseLong(String)} but removes any underscore 
characters first.
+        *
+        * <p>Allows for better readability of numeric literals (e.g., 
<js>"1_000_000"</js>).
+        *
+        * @param value The string to parse.
+        * @return The parsed long value.
+        * @throws NumberFormatException If the string cannot be parsed.
+        * @throws NullPointerException If the string is <jk>null</jk>.
+        */
+       public static long parseLong(String value) {
+               return Long.parseLong(StringUtils.removeUnderscores(value));
+       }
+
+       /**
+        * Converts a string containing glob-style wildcard characters to a 
regular expression {@link java.util.regex.Pattern}.
+        *
+        * <p>This method converts glob-style patterns to regular expressions 
with the following mappings:
+        * <ul>
+        *   <li>{@code *} matches any sequence of characters (including 
none)</li>
+        *   <li>{@code ?} matches exactly one character</li>
+        *   <li>All other characters are treated literally</li>
+        * </ul>
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *   <jk>var</jk> <jv>pattern</jv> = 
<jsm>getGlobMatchPattern</jsm>(<js>"user_*_temp"</js>);
+        *   <jk>boolean</jk> <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"user_alice_temp"</js>).matches();  <jc>// 
true</jc>
+        *   <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"user_bob_temp"</js>).matches();    <jc>// 
true</jc>
+        *   <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"admin_alice_temp"</js>).matches(); <jc>// 
false</jc>
+        * </p>
+        *
+        * @param s The glob-style wildcard pattern string.
+        * @return A compiled {@link java.util.regex.Pattern} object, or 
<jk>null</jk> if the input string is <jk>null</jk>.
+        */
+       public static java.util.regex.Pattern getGlobMatchPattern(String s) {
+               return getGlobMatchPattern(s, 0);
+       }
+
+       /**
+        * Converts a string containing glob-style wildcard characters to a 
regular expression {@link java.util.regex.Pattern} with flags.
+        *
+        * <p>This method converts glob-style patterns to regular expressions 
with the following mappings:
+        * <ul>
+        *   <li>{@code *} matches any sequence of characters (including 
none)</li>
+        *   <li>{@code ?} matches exactly one character</li>
+        *   <li>All other characters are treated literally</li>
+        * </ul>
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *   <jc>// Case-insensitive matching</jc>
+        *   <jk>var</jk> <jv>pattern</jv> = 
<jsm>getGlobMatchPattern</jsm>(<js>"USER_*"</js>, 
Pattern.<jsf>CASE_INSENSITIVE</jsf>);
+        *   <jk>boolean</jk> <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"user_alice"</js>).matches();  <jc>// true</jc>
+        * </p>
+        *
+        * @param s The glob-style wildcard pattern string.
+        * @param flags Regular expression flags (see {@link 
java.util.regex.Pattern} constants).
+        * @return A compiled {@link java.util.regex.Pattern} object, or 
<jk>null</jk> if the input string is <jk>null</jk>.
+        */
+       public static java.util.regex.Pattern getGlobMatchPattern(String s, int 
flags) {
+               if (s == null)
+                       return null;
+               var sb = new StringBuilder();
+               sb.append("\\Q");
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (c == '*')
+                               sb.append("\\E").append(".*").append("\\Q");
+                       else if (c == '?')
+                               sb.append("\\E").append(".").append("\\Q");
+                       else
+                               sb.append(c);
+               }
+               sb.append("\\E");
+               return java.util.regex.Pattern.compile(sb.toString(), flags);
+       }
+
+       public static String removeUnderscores(String value) {
+               assertArgNotNull("value", value);
+               return notContains(value, '_') ? value : value.replace("_", "");
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
index 3a10862138..9fa7ecc373 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/utils/Utils.java
@@ -190,12 +190,21 @@ public class Utils {
        /**
         * Tests two objects for equality, gracefully handling nulls and arrays.
         *
+        * <p>
+        * This method handles annotations specially by delegating to {@link 
AnnotationUtils#equals(Annotation, Annotation)}
+        * to ensure proper annotation comparison according to the annotation 
equality contract.
+        *
         * @param <T> The value types.
         * @param o1 Object 1.
         * @param o2 Object 2.
         * @return <jk>true</jk> if both objects are equal based on the {@link 
Object#equals(Object)} method.
+        * @see AnnotationUtils#equals(Annotation, Annotation)
         */
        public static <T> boolean eq(T o1, T o2) {
+               // Handle annotations specially
+               if (o1 instanceof java.lang.annotation.Annotation o1a && o2 
instanceof java.lang.annotation.Annotation o2a)
+                       return AnnotationUtils.equals(o1a, o2a);
+
                if (isArray(o1) && isArray(o2)) {
                        int l1 = Array.getLength(o1), l2 = Array.getLength(o2);
                        if (l1 != l2)
@@ -240,39 +249,6 @@ public class Utils {
                return test.test(o1, o2);
        }
 
-       /**
-        * Tests two annotations for equality using the criteria presented in 
the {@link java.lang.annotation.Annotation#equals(Object)} API docs.
-        *
-        * <p>
-        * This method delegates to {@link 
AnnotationUtils#equals(java.lang.annotation.Annotation, 
java.lang.annotation.Annotation)}
-        * to ensure proper annotation comparison according to the annotation 
equality contract.
-        *
-        * @param a1 Annotation 1.
-        * @param a2 Annotation 2.
-        * @return <jk>true</jk> if the two annotations are equal or both are 
<jk>null</jk>.
-        * @see AnnotationUtils#equals(java.lang.annotation.Annotation, 
java.lang.annotation.Annotation)
-        */
-       public static boolean eq(java.lang.annotation.Annotation a1, 
java.lang.annotation.Annotation a2) {
-               return AnnotationUtils.equals(a1, a2);
-       }
-
-       /**
-        * Tests two annotations for inequality using the criteria presented in 
the {@link java.lang.annotation.Annotation#equals(Object)} API docs.
-        *
-        * <p>
-        * This method is the negation of {@link 
#eq(java.lang.annotation.Annotation, java.lang.annotation.Annotation)},
-        * which delegates to {@link 
AnnotationUtils#equals(java.lang.annotation.Annotation, 
java.lang.annotation.Annotation)}.
-        *
-        * @param a1 Annotation 1.
-        * @param a2 Annotation 2.
-        * @return <jk>true</jk> if the two annotations are not equal.
-        * @see #eq(java.lang.annotation.Annotation, 
java.lang.annotation.Annotation)
-        * @see AnnotationUtils#equals(java.lang.annotation.Annotation, 
java.lang.annotation.Annotation)
-        */
-       public static boolean ne(java.lang.annotation.Annotation a1, 
java.lang.annotation.Annotation a2) {
-               return ! eq(a1, a2);
-       }
-
        /**
         * Tests two strings for case-insensitive equality, but gracefully 
handles nulls.
         *
@@ -304,70 +280,6 @@ public class Utils {
                return null;
        }
 
-       /**
-        * Converts a string containing glob-style wildcard characters to a 
regular expression {@link java.util.regex.Pattern}.
-        *
-        * <p>This method converts glob-style patterns to regular expressions 
with the following mappings:
-        * <ul>
-        *   <li>{@code *} matches any sequence of characters (including 
none)</li>
-        *   <li>{@code ?} matches exactly one character</li>
-        *   <li>All other characters are treated literally</li>
-        * </ul>
-        *
-        * <h5 class='section'>Example:</h5>
-        * <p class='bjava'>
-        *   <jk>var</jk> <jv>pattern</jv> = 
<jsm>getGlobMatchPattern</jsm>(<js>"user_*_temp"</js>);
-        *   <jk>boolean</jk> <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"user_alice_temp"</js>).matches();  <jc>// 
true</jc>
-        *   <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"user_bob_temp"</js>).matches();    <jc>// 
true</jc>
-        *   <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"admin_alice_temp"</js>).matches(); <jc>// 
false</jc>
-        * </p>
-        *
-        * @param s The glob-style wildcard pattern string.
-        * @return A compiled {@link java.util.regex.Pattern} object, or 
<jk>null</jk> if the input string is <jk>null</jk>.
-        */
-       public static java.util.regex.Pattern getGlobMatchPattern(String s) {
-               return getGlobMatchPattern(s, 0);
-       }
-
-       /**
-        * Converts a string containing glob-style wildcard characters to a 
regular expression {@link java.util.regex.Pattern} with flags.
-        *
-        * <p>This method converts glob-style patterns to regular expressions 
with the following mappings:
-        * <ul>
-        *   <li>{@code *} matches any sequence of characters (including 
none)</li>
-        *   <li>{@code ?} matches exactly one character</li>
-        *   <li>All other characters are treated literally</li>
-        * </ul>
-        *
-        * <h5 class='section'>Example:</h5>
-        * <p class='bjava'>
-        *   <jc>// Case-insensitive matching</jc>
-        *   <jk>var</jk> <jv>pattern</jv> = 
<jsm>getGlobMatchPattern</jsm>(<js>"USER_*"</js>, 
Pattern.<jsf>CASE_INSENSITIVE</jsf>);
-        *   <jk>boolean</jk> <jv>matches</jv> = 
<jv>pattern</jv>.matcher(<js>"user_alice"</js>).matches();  <jc>// true</jc>
-        * </p>
-        *
-        * @param s The glob-style wildcard pattern string.
-        * @param flags Regular expression flags (see {@link 
java.util.regex.Pattern} constants).
-        * @return A compiled {@link java.util.regex.Pattern} object, or 
<jk>null</jk> if the input string is <jk>null</jk>.
-        */
-       public static java.util.regex.Pattern getGlobMatchPattern(String s, int 
flags) {
-               if (s == null)
-                       return null;
-               var sb = new StringBuilder();
-               sb.append("\\Q");
-               for (var i = 0; i < s.length(); i++) {
-                       var c = s.charAt(i);
-                       if (c == '*')
-                               sb.append("\\E").append(".*").append("\\Q");
-                       else if (c == '?')
-                               sb.append("\\E").append(".").append("\\Q");
-                       else
-                               sb.append(c);
-               }
-               sb.append("\\E");
-               return java.util.regex.Pattern.compile(sb.toString(), flags);
-       }
-
        /**
         * Shortcut for calling {@link StringUtils#format(String, Object...)}.
         *
@@ -488,30 +400,6 @@ public class Utils {
                return nn(o) && o.getClass().isArray();
        }
 
-       /**
-        * Returns <jk>true</jk> if the specified object can be converted to a 
list.
-        *
-        * <p>
-        * The following types are considered convertible:
-        * <ul>
-        *      <li>Collections
-        *      <li>Iterables
-        *      <li>Iterators
-        *      <li>Enumerations
-        *      <li>Streams
-        *      <li>Maps
-        *      <li>Optional
-        *      <li>Arrays
-        * </ul>
-        *
-        * @param o The object to check.
-        * @return <jk>true</jk> if the object can be converted to a list.
-        */
-       public static boolean isConvertibleToList(Object o) {
-               return nn(o) && (o instanceof Collection || o instanceof 
Iterable || o instanceof Iterator || o instanceof Enumeration || o instanceof 
Stream || o instanceof Map || o instanceof Optional
-                       || isArray(o));
-       }
-
        /**
         * Returns <jk>true</jk> if the specified object is empty.
         *
@@ -850,54 +738,6 @@ public class Utils {
                }
        }
 
-       /**
-        * Safely converts an object to its string representation, handling any 
exceptions that may occur.
-        *
-        * <p>This method provides a fail-safe way to call {@code toString()} 
on any object, ensuring that
-        * exceptions thrown by problematic {@code toString()} implementations 
don't propagate up the call stack.
-        * Instead, it returns a descriptive error message containing the 
exception type and message.</p>
-        *
-        * <h5 class='section'>Exception Handling:</h5>
-        * <p>If the object's {@code toString()} method throws any {@code 
Throwable}, this method catches it
-        * and returns a formatted string in the form: {@code "<ExceptionType>: 
<exception message>"}.</p>
-        *
-        * <h5 class='section'>Examples:</h5>
-        * <p class='bjava'>
-        *    <jc>// Normal case - returns object's toString() result</jc>
-        *    String <jv>result</jv> = 
<jsm>safeToString</jsm>(<js>"Hello"</js>);
-        *    <jc>// result = "Hello"</jc>
-        *
-        *    <jc>// Exception case - returns formatted error message</jc>
-        *    Object <jv>problematic</jv> = <jk>new</jk> Object() {
-        *       <ja>@Override</ja>
-        *       <jk>public</jk> String toString() {
-        *          <jk>throw new</jk> RuntimeException(<js>"Cannot 
convert"</js>);
-        *       }
-        *    };
-        *    String <jv>result</jv> = 
<jsm>safeToString</jsm>(<jv>problematic</jv>);
-        *    <jc>// result = "RuntimeException: Cannot convert"</jc>
-        * </p>
-        *
-        * <h5 class='section'>Use Cases:</h5>
-        * <ul>
-        *    <li><b>Object stringification in converters:</b> Safe conversion 
of arbitrary objects to strings</li>
-        *    <li><b>Debugging and logging:</b> Ensures log statements never 
fail due to toString() exceptions</li>
-        *    <li><b>Error handling:</b> Graceful degradation when objects have 
problematic string representations</li>
-        *    <li><b>Third-party object integration:</b> Safe handling of 
objects from external libraries</li>
-        * </ul>
-        *
-        * @param o The object to convert to a string. May be any object 
including <jk>null</jk>.
-        * @return The string representation of the object, or a formatted 
error message if toString() throws an exception.
-        *    Returns <js>"null"</js> if the object is <jk>null</jk>.
-        */
-       public static String safeToString(Object o) {
-               try {
-                       return o.toString();
-               } catch (Throwable t) { // NOSONAR
-                       return cns(t) + ": " + t.getMessage();
-               }
-       }
-
        /**
         * Helper method for creating StringBuilder objects.
         *
@@ -1104,54 +944,6 @@ public class Utils {
                return n >= lower && n <= higher;
        }
 
-       /**
-        * Same as {@link Integer#parseInt(String)} but removes any underscore 
characters first.
-        *
-        * <p>Allows for better readability of numeric literals (e.g., 
<js>"1_000_000"</js>).
-        *
-        * @param value The string to parse.
-        * @return The parsed integer value.
-        * @throws NumberFormatException If the string cannot be parsed.
-        * @throws NullPointerException If the string is <jk>null</jk>.
-        */
-       public static int parseInt(String value) {
-               return Integer.parseInt(removeUnderscores(value));
-       }
-
-       /**
-        * Same as {@link Long#parseLong(String)} but removes any underscore 
characters first.
-        *
-        * <p>Allows for better readability of numeric literals (e.g., 
<js>"1_000_000"</js>).
-        *
-        * @param value The string to parse.
-        * @return The parsed long value.
-        * @throws NumberFormatException If the string cannot be parsed.
-        * @throws NullPointerException If the string is <jk>null</jk>.
-        */
-       public static long parseLong(String value) {
-               return Long.parseLong(removeUnderscores(value));
-       }
-
-       /**
-        * Same as {@link Float#parseFloat(String)} but removes any underscore 
characters first.
-        *
-        * <p>Allows for better readability of numeric literals (e.g., 
<js>"1_000.5"</js>).
-        *
-        * @param value The string to parse.
-        * @return The parsed float value.
-        * @throws NumberFormatException If the string cannot be parsed.
-        * @throws NullPointerException If the string is <jk>null</jk>.
-        */
-       public static float parseFloat(String value) {
-               return Float.parseFloat(removeUnderscores(value));
-       }
-
-       private static String removeUnderscores(String value) {
-               if (value == null)
-                       throw new NullPointerException("Trying to parse null 
string.");
-               return notContains(value, '_') ? value : value.replace("_", "");
-       }
-
        /**
         * Shortcut for calling {@link StringUtils#stringSupplier(Supplier)}.
         *
@@ -1277,4 +1069,30 @@ public class Utils {
                        return null;
                return cnsq(o) + "@" + System.identityHashCode(o);
        }
+
+       /**
+        * Convenience method for calling {@link StringUtils#upperCase(String)}.
+        *
+        * <p>
+        * Converts the string to uppercase if not null.
+        *
+        * @param value The string to convert.
+        * @return The uppercase string, or <jk>null</jk> if the input was 
<jk>null</jk>.
+        */
+       public static String uc(String value) {
+               return StringUtils.upperCase(value);
+       }
+
+       /**
+        * Convenience method for calling {@link StringUtils#lowerCase(String)}.
+        *
+        * <p>
+        * Converts the string to lowercase if not null.
+        *
+        * @param value The string to convert.
+        * @return The lowercase string, or <jk>null</jk> if the input was 
<jk>null</jk>.
+        */
+       public static String lc(String value) {
+               return StringUtils.lowerCase(value);
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index 7f421ce086..3b59f9d5e9 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -38,6 +38,7 @@ import org.apache.juneau.*;
 import org.apache.juneau.assertions.*;
 import org.apache.juneau.bean.swagger.*;
 import org.apache.juneau.bean.swagger.Swagger;
+import org.apache.juneau.common.utils.*;
 import org.apache.juneau.config.*;
 import org.apache.juneau.cp.*;
 import org.apache.juneau.http.annotation.*;
@@ -1195,7 +1196,7 @@ public class RestRequest extends 
HttpServletRequestWrapper {
                var x = inner.getProtocol();
                var i = x.indexOf('/');
                var j = x.indexOf('.', i);
-               var pv = new ProtocolVersion(x.substring(0, i), 
parseInt(x.substring(i + 1, j)), parseInt(x.substring(j + 1)));
+               var pv = new ProtocolVersion(x.substring(0, i), 
StringUtils.parseInt(x.substring(i + 1, j)), StringUtils.parseInt(x.substring(j 
+ 1)));
                return new BasicRequestLine(inner.getMethod(), 
inner.getRequestURI(), pv);
        }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
index 0422f64e62..e0c62dd1b6 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/common/utils/StringUtils_Test.java
@@ -3100,18 +3100,18 @@ class StringUtils_Test extends TestBase {
        
//====================================================================================================
        @Test
        void a77_lc() {
-               assertNull(lc(null));
-               assertEquals("", lc(""));
-               assertEquals("hello", lc("HELLO"));
-               assertEquals("hello world", lc("Hello World"));
+               assertNull(lowerCase(null));
+               assertEquals("", lowerCase(""));
+               assertEquals("hello", lowerCase("HELLO"));
+               assertEquals("hello world", lowerCase("Hello World"));
        }
 
        @Test
        void a78_uc() {
-               assertNull(uc(null));
-               assertEquals("", uc(""));
-               assertEquals("HELLO", uc("hello"));
-               assertEquals("HELLO WORLD", uc("Hello World"));
+               assertNull(upperCase(null));
+               assertEquals("", upperCase(""));
+               assertEquals("HELLO", upperCase("hello"));
+               assertEquals("HELLO WORLD", upperCase("Hello World"));
        }
 
        
//====================================================================================================
@@ -3574,7 +3574,14 @@ class StringUtils_Test extends TestBase {
                appendIfNotEmpty(sb2, "suffix");
                assertEquals("prefixsuffix", sb2.toString());
 
-               assertThrows(IllegalArgumentException.class, () -> 
appendIfNotEmpty(null, "test"));
+               // Auto-create StringBuilder if null and append occurs
+               var sb3 = appendIfNotEmpty(null, "test");
+               assertNotNull(sb3);
+               assertEquals("test", sb3.toString());
+
+               // Return null if null and no append occurs
+               assertNull(appendIfNotEmpty(null, null));
+               assertNull(appendIfNotEmpty(null, ""));
        }
 
        
//====================================================================================================
@@ -3605,7 +3612,16 @@ class StringUtils_Test extends TestBase {
                appendIfNotBlank(sb2, "suffix");
                assertEquals("prefixsuffix", sb2.toString());
 
-               assertThrows(IllegalArgumentException.class, () -> 
appendIfNotBlank(null, "test"));
+               // Auto-create StringBuilder if null and append occurs
+               var sb3 = appendIfNotBlank(null, "test");
+               assertNotNull(sb3);
+               assertEquals("test", sb3.toString());
+
+               // Return null if null and no append occurs
+               assertNull(appendIfNotBlank(null, null));
+               assertNull(appendIfNotBlank(null, ""));
+               assertNull(appendIfNotBlank(null, "   "));
+               assertNull(appendIfNotBlank(null, "\t\n"));
        }
 
        
//====================================================================================================
@@ -3641,7 +3657,17 @@ class StringUtils_Test extends TestBase {
                appendWithSeparator(sb4, "test", ", ");
                assertEquals("test", sb4.toString());
 
-               assertThrows(IllegalArgumentException.class, () -> 
appendWithSeparator(null, "test", ", "));
+               // Auto-create StringBuilder if null and append occurs
+               var sb5 = appendWithSeparator(null, "test", ", ");
+               assertNotNull(sb5);
+               assertEquals("test", sb5.toString());
+
+               var sb6 = appendWithSeparator(null, "first", ", ");
+               appendWithSeparator(sb6, "second", ", ");
+               assertEquals("first, second", sb6.toString());
+
+               // Return null if null and no append occurs
+               assertNull(appendWithSeparator(null, null, ", "));
        }
 
        
//====================================================================================================

Reply via email to