001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.utils;
018
019import static java.lang.Character.*;
020import static java.nio.charset.StandardCharsets.*;
021import static java.util.Collections.*;
022import static java.util.stream.Collectors.*;
023import static org.apache.juneau.commons.lang.StateEnum.*;
024import static org.apache.juneau.commons.utils.CollectionUtils.*;
025import static org.apache.juneau.commons.utils.CollectionUtils.list;
026import static org.apache.juneau.commons.utils.CollectionUtils.toList;
027import static org.apache.juneau.commons.utils.Exceptions.*;
028import static org.apache.juneau.commons.utils.IoUtils.*;
029import static org.apache.juneau.commons.utils.ObjectUtils.*;
030import static org.apache.juneau.commons.utils.Shorts.req;
031import static org.apache.juneau.commons.utils.Shorts.reqnn;
032import static org.apache.juneau.commons.utils.ThrowableUtils.*;
033
034import java.io.*;
035import java.lang.reflect.*;
036import java.math.*;
037import java.net.*;
038import java.nio.*;
039import java.text.*;
040import java.time.*;
041import java.time.format.*;
042import java.util.*;
043import java.util.concurrent.*;
044import java.util.concurrent.atomic.*;
045import java.util.function.*;
046import java.util.regex.*;
047import java.util.stream.*;
048import java.util.zip.*;
049
050import org.apache.juneau.commons.collections.*;
051import org.apache.juneau.commons.lang.*;
052import org.apache.juneau.commons.reflect.*;
053
054/**
055 * Reusable string utility methods.
056 */
057@SuppressWarnings({
058   "java:S127", // Scanning loops (e.g. the whitespace/token skipping parsers) advance the index inside the loop body
059   "java:S135", // Multiple break statements in mutually exclusive branches - necessary for early termination
060   "java:S1168", // Null-in/null-out string utilities (base64Encode, decodeHex, clean, escapeForJava, ...) return null for null input
061   "java:S1192", // Duplicated string literals (HTML entities) are intentional
062   "java:S3516", // Methods flagged as always returning the same value are valid utility implementations (e.g. null-safe wrappers)
063   "java:S3776", // Multi-branch scanners and parsers (tokenizers, escape handling, validators) are kept as single methods
064   "java:S5843", // FP_REGEX copied from JDK source for parsing consistency
065   "java:S6539", // Monster class; StringUtils is intentionally a single cohesive string-utility hub, not a set of unrelated responsibilities
066   "java:S6541", // Thread-safe singleton pattern acceptable
067   "java:S8786" // Possessive, overlap-free regex ((?:label\.)++TLD with a dot-less label class) does not backtrack; matching is linear. S8786 is a false positive here.
068})
069public class StringUtils {
070
071   /** Characters considered common separators (comma/semicolon/colon/pipe/tab). */
072   public static final AsciiSet COMMON_SEPARATORS = AsciiSet.of(",;:|\t");
073
074   /** Windows-style carriage-return/line-feed sequence. */
075   public static final String CRLF = "\r\n";
076
077   /** Digits 0-9 represented as an {@link AsciiSet}. */
078   public static final AsciiSet DECIMAL_CHARS = AsciiSet.of("0123456789");
079
080   /** Digits 0-9 represented as an {@link AsciiSet}. */
081   public static final AsciiSet DIGIT = AsciiSet.of("0123456789");
082
083   /** Zero-length string constant. */
084   public static final String EMPTY = "";
085
086   /** Zero-length string array constant. */
087   public static final String[] EMPTY_STRING_ARRAY = new String[0];
088
089   /** Characters allowed at the beginning of a numeric literal. */
090   public static final AsciiSet FIRST_NUMBER_CHARS = AsciiSet.of("+-.#0123456789");
091
092   /** Hexadecimal digit characters. */
093   public static final AsciiSet HEXADECIMAL_CHARS = AsciiSet.of("0123456789abcdefABCDEF");
094
095   /** Characters allowed in HTTP headers (including quoted strings and comments). */
096   public static final AsciiSet HTTP_HEADER_CHARS = AsciiSet.create().chars("\t -").ranges("!-[","]-}").build();
097
098   /** Letters a-z and A-Z represented as an {@link AsciiSet}. */
099   public static final AsciiSet LETTER = AsciiSet.create().ranges("a-z", "A-Z").build();
100
101   /** Lowercase letters a-z represented as an {@link AsciiSet}. */
102   public static final AsciiSet LETTER_LC = AsciiSet.create().range('a', 'z').build();
103
104   /** Uppercase letters A-Z represented as an {@link AsciiSet}. */
105   public static final AsciiSet LETTER_UC = AsciiSet.create().range('A', 'Z').build();
106
107   /** Characters escaped when parsing key/value pairs. */
108   public static final AsciiSet MAP_ESCAPE_SET = AsciiSet.of(",=\\");
109
110   /** Unix-style newline character. */
111   public static final String NEWLINE = "\n";
112
113   /** Predicate that filters out {@code null} and empty strings. */
114   public static final Predicate<String> NOT_EMPTY = ObjectUtils::isNotEmpty;
115
116   /** Characters that can appear anywhere in a numeric literal. */
117   public static final AsciiSet NUMBER_CHARS = AsciiSet.of("-xX.+-#pP0123456789abcdefABCDEF");
118
119   /** Octal digit characters. */
120   public static final AsciiSet OCTAL_CHARS = AsciiSet.of("01234567");
121
122   /** Characters escaped when parsing quoted strings. */
123   public static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.of("\"'\\");
124
125   /** Single-space character constant. */
126   public static final String SPACE = " ";
127
128   /** Horizontal tab character constant. */
129   public static final String TAB = "\t";
130
131   /** Characters considered part of a URI when encoding paths. */
132   public static final AsciiSet URI_CHARS = AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0', '9').range('A', 'Z').range('a', 'z').build();
133
134   /** Characters that may appear unescaped in path segments when URL-encoding. */
135   public static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars("-_.*/()").build();
136
137   /** Characters that never require URL encoding per RFC 3986. */
138   public static final AsciiSet URL_UNENCODED_CHARS = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars("-_.!~*'()\\").build();
139
140   /** Extended set of characters that are typically safe to leave unencoded. */
141   public static final AsciiSet URL_UNENCODED_LAX_CHARS = URL_UNENCODED_CHARS.copy().chars(":@$,").chars("{}|\\^[]`").build();
142
143   /** Vowel characters a, e, i, o, u (both uppercase and lowercase) represented as an {@link AsciiSet}. */
144   public static final AsciiSet VOWEL = AsciiSet.of("aeiouAEIOU");
145
146   /**
147    * All standard whitespace characters (space, tab, newline, carriage return, form feed, vertical tab).
148    */
149   public static final AsciiSet WHITESPACE_CHARS = AsciiSet.of(" \t\n\r\f\u000B");
150
151   // Maps 6-bit nibbles to BASE64 characters.
152   private static final char[] BASE64M1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
153
154   // Maps BASE64 characters to 6-bit nibbles.
155   private static final byte[] BASE64M2 = new byte[128];
156
157   static {
158      for (var i = 0; i < 64; i++)
159         BASE64M2[BASE64M1[i]] = (byte)i;
160   }
161   private static final Random RANDOM = new Random();
162
163   /**
164    * Floating-point literal validation pattern used by {@link Double#valueOf(String)}.
165    *
166    * <p>
167    * Matches decimal, hexadecimal, and scientific-notation literals, as well as {@code NaN} and {@code Infinity},
168    * with optional sign, exponent, and type suffixes. Copied from the JDK source to keep parsing logic consistent.
169    */
170   public static final Pattern FP_REGEX = Pattern.compile(
171      "[+-]?(NaN|Infinity|((((\\p{Digit}+)(\\.)?((\\p{Digit}+)?)([eE][+-]?(\\p{Digit}+))?)|(\\.((\\p{Digit}+))([eE][+-]?(\\p{Digit}+))?)|(((0[xX](\\p{XDigit}+)(\\.)?)|(0[xX](\\p{XDigit}+)?(\\.)(\\p{XDigit}+)))[pP][+-]?(\\p{Digit}+)))[fFdD]?))[\\x00-\\x20]*"
172   );
173
174   static final Map<Character,AsciiSet> ESCAPE_SETS = new ConcurrentHashMap<>();
175
176   private static final List<Readifier> READIFIERS = loadReadifiers();
177   private static final Cache<Class<?>,Function<Object,String>> READIFIER_CACHE = Cache.<Class<?>,Function<Object,String>>create().weak().build();
178
179   private static final char[] HEX = "0123456789ABCDEF".toCharArray();
180
181   /**
182    * Abbreviates a string using ellipses if it exceeds the specified length.
183    *
184    * <p>
185    * If the string is longer than the specified length, it is truncated and <js>"..."</js> is appended.
186    * The total length of the result will be exactly <c>length</c> characters (including the ellipses).
187    *
188    * <p>
189    * If the string is <jk>null</jk>, shorter than or equal to <c>length</c>, or has length 3 or less,
190    * the original string is returned unchanged.
191    *
192    * <h4 class='section'>Example:</h4>
193    * <p class='bjava'>
194    *    abbreviate(<js>"Hello World"</js>, 8);        <jc>// "Hello..."</jc>
195    *    abbreviate(<js>"Hello World"</js>, 20);       <jc>// "Hello World" (no change)</jc>
196    *    abbreviate(<js>"Hi"</js>, 5);                 <jc>// "Hi" (too short to abbreviate)</jc>
197    *    abbreviate(<jk>null</jk>, 10);                <jc>// null</jc>
198    * </p>
199    *
200    * @param in The input string. Can be <jk>null</jk> (returns <jk>null</jk>).
201    * @param length The maximum length of the resulting string (must be at least 4 for abbreviation to occur).
202    * @return The abbreviated string with ellipses, or the original string if no abbreviation is needed.
203    */
204   public static String abbreviate(String in, int length) {
205      if (in == null || in.length() <= length || in.length() <= 3)
206         return in;
207      return in.substring(0, length - 3) + "...";
208   }
209
210   /**
211    * Appends a string to a StringBuilder, creating a new one if null.
212    *
213    * @param sb The StringBuilder to append to, or <jk>null</jk> to create a new one.
214    * @param in The string to append.
215    *    <br>Can be <jk>null</jk> (appended as <js>"null"</js>), unless <c>sb</c> is also <jk>null</jk> in which case a {@link NullPointerException} is thrown.
216    * @return The StringBuilder with the string appended.
217    */
218   public static StringBuilder append(StringBuilder sb, String in) {
219      if (sb == null)
220         return new StringBuilder(in);
221      sb.append(in);
222      return sb;
223   }
224
225   /**
226    * Appends a string to a StringBuilder if the string is not blank.
227    *
228    * <p>
229    * Returns the same StringBuilder instance for method chaining.
230    * If the string is <jk>null</jk>, empty, or contains only whitespace, nothing is appended.
231    * If <c>sb</c> is <jk>null</jk> and an append is going to occur, a new StringBuilder is automatically created.
232    *
233    * <h4 class='section'>Examples:</h4>
234    * <p class='bjava'>
235    *    StringBuilder <jv>sb</jv> = <jk>new</jk> StringBuilder();
236    *    appendIfNotBlank(<jv>sb</jv>, <js>"hello"</js>);  <jc>// Appends "hello"</jc>
237    *    appendIfNotBlank(<jv>sb</jv>, <js>"   "</js>);    <jc>// Does nothing</jc>
238    *    appendIfNotBlank(<jv>sb</jv>, <jk>null</jk>);     <jc>// Does nothing</jc>
239    *    appendIfNotBlank(<jv>sb</jv>, <js>"world"</js>);  <jc>// Appends "world"</jc>
240    *    <jc>// Result: "helloworld"</jc>
241    *
242    *    <jc>// Auto-create StringBuilder if null and append occurs</jc>
243    *    StringBuilder <jv>sb2</jv> = appendIfNotBlank(<jk>null</jk>, <js>"test"</js>);  <jc>// Creates new StringBuilder with "test"</jc>
244    *    StringBuilder <jv>sb3</jv> = appendIfNotBlank(<jk>null</jk>, <js>"   "</js>);   <jc>// Returns null (no append occurred)</jc>
245    * </p>
246    *
247    * @param sb The StringBuilder to append to. Can be <jk>null</jk>.
248    * @param str The string to append if not blank. Can be <jk>null</jk>.
249    * @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.
250    */
251   public static StringBuilder appendIfNotBlank(StringBuilder sb, String str) {
252      if (isNotBlank(str)) {
253         if (sb == null)
254            sb = new StringBuilder();
255         sb.append(str);
256      }
257      return sb;
258   }
259
260   /**
261    * Appends a string to a StringBuilder if the string is not empty.
262    *
263    * <p>
264    * Returns the same StringBuilder instance for method chaining.
265    * If the string is <jk>null</jk> or empty, nothing is appended.
266    * If <c>sb</c> is <jk>null</jk> and an append is going to occur, a new StringBuilder is automatically created.
267    *
268    * <h4 class='section'>Examples:</h4>
269    * <p class='bjava'>
270    *    StringBuilder <jv>sb</jv> = <jk>new</jk> StringBuilder();
271    *    appendIfNotEmpty(<jv>sb</jv>, <js>"hello"</js>);  <jc>// Appends "hello"</jc>
272    *    appendIfNotEmpty(<jv>sb</jv>, <js>""</js>);       <jc>// Does nothing</jc>
273    *    appendIfNotEmpty(<jv>sb</jv>, <jk>null</jk>);     <jc>// Does nothing</jc>
274    *    appendIfNotEmpty(<jv>sb</jv>, <js>"world"</js>);  <jc>// Appends "world"</jc>
275    *    <jc>// Result: "helloworld"</jc>
276    *
277    *    <jc>// Auto-create StringBuilder if null and append occurs</jc>
278    *    StringBuilder <jv>sb2</jv> = appendIfNotEmpty(<jk>null</jk>, <js>"test"</js>);  <jc>// Creates new StringBuilder with "test"</jc>
279    *    StringBuilder <jv>sb3</jv> = appendIfNotEmpty(<jk>null</jk>, <jk>null</jk>);   <jc>// Returns null (no append occurred)</jc>
280    * </p>
281    *
282    * @param sb The StringBuilder to append to. Can be <jk>null</jk>.
283    * @param str The string to append if not empty. Can be <jk>null</jk>.
284    * @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.
285    */
286   public static StringBuilder appendIfNotEmpty(StringBuilder sb, String str) {
287      if (isNotEmpty(str)) {
288         if (sb == null)
289            sb = new StringBuilder();
290         sb.append(str);
291      }
292      return sb;
293   }
294
295   /**
296    * Appends a string to a StringBuilder with a separator, only adding the separator if the StringBuilder is not empty.
297    *
298    * <p>
299    * Returns the same StringBuilder instance for method chaining.
300    * If the StringBuilder is empty, only the string is appended (no separator).
301    * If the StringBuilder is not empty, the separator is appended first, then the string.
302    * If <c>sb</c> is <jk>null</jk> and an append is going to occur, a new StringBuilder is automatically created.
303    *
304    * <h4 class='section'>Examples:</h4>
305    * <p class='bjava'>
306    *    StringBuilder <jv>sb</jv> = <jk>new</jk> StringBuilder();
307    *    appendWithSeparator(<jv>sb</jv>, <js>"first"</js>, <js>", "</js>);   <jc>// Appends "first"</jc>
308    *    appendWithSeparator(<jv>sb</jv>, <js>"second"</js>, <js>", "</js>);  <jc>// Appends ", second"</jc>
309    *    appendWithSeparator(<jv>sb</jv>, <js>"third"</js>, <js>", "</js>);   <jc>// Appends ", third"</jc>
310    *    <jc>// Result: "first, second, third"</jc>
311    *
312    *    <jc>// Auto-create StringBuilder if null and append occurs</jc>
313    *    StringBuilder <jv>sb2</jv> = appendWithSeparator(<jk>null</jk>, <js>"test"</js>, <js>", "</js>);  <jc>// Creates new StringBuilder with "test"</jc>
314    *    StringBuilder <jv>sb3</jv> = appendWithSeparator(<jk>null</jk>, <jk>null</jk>, <js>", "</js>);   <jc>// Returns null (no append occurred)</jc>
315    * </p>
316    *
317    * @param sb The StringBuilder to append to. Can be <jk>null</jk>.
318    * @param str The string to append. Can be <jk>null</jk>.
319    * @param separator The separator to add before the string if the StringBuilder is not empty. Can be <jk>null</jk> (no separator is appended).
320    * @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.
321    */
322   public static StringBuilder appendWithSeparator(StringBuilder sb, String str, String separator) {
323      if (str != null) {
324         if (sb == null)
325            sb = new StringBuilder();
326         else if (!sb.isEmpty() && separator != null)
327            sb.append(separator);
328         sb.append(str);
329      }
330      return sb;
331   }
332
333   /**
334    * Adds the appropriate indefinite article ('a' or 'an') before a word.
335    *
336    * <p>Uses a simple vowel-based rule: 'an' if the word starts with a vowel, 'a' otherwise.
337    *
338    * @param subject The word to articlize.  Must not be <jk>null</jk> or empty (a <jk>null</jk> argument throws {@link NullPointerException}).
339    * @return The word with 'a' or 'an' prepended.
340    */
341   public static String articlized(String subject) {
342      return (VOWEL.contains(subject.charAt(0)) ? "an " : "a ") + subject;
343   }
344
345   /**
346    * BASE64-decodes the specified string.
347    *
348    * @param in The BASE-64 encoded string.
349    * @return The decoded byte array, or an empty array if the input was <jk>null</jk>.
350    */
351   public static byte[] base64Decode(String in) {
352      if (in == null)
353         return new byte[0];
354
355      var bIn = in.getBytes(UTF8);
356
357      req(bIn.length % 4 == 0, "Invalid BASE64 string length.  Must be multiple of 4.");
358
359      // Strip out any trailing '=' filler characters.
360      var inLength = bIn.length;
361      while (inLength > 0 && bIn[inLength - 1] == '=')
362         inLength--;
363
364      var outLength = (inLength * 3) / 4;
365      var out = new byte[outLength];
366      var iIn = 0;
367      var iOut = 0;
368      while (iIn < inLength) {
369         var i0 = bIn[iIn++];
370         var i1 = bIn[iIn++];
371         var i2 = iIn < inLength ? bIn[iIn++] : 'A';
372         var i3 = iIn < inLength ? bIn[iIn++] : 'A';
373         var b0 = BASE64M2[i0 & 0xff];
374         var b1 = BASE64M2[i1 & 0xff];
375         var b2 = BASE64M2[i2 & 0xff];
376         var b3 = BASE64M2[i3 & 0xff];
377         var o0 = (b0 << 2) | (b1 >>> 4);
378         var o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);
379         var o2 = ((b2 & 3) << 6) | (b3 & 0xff);
380         out[iOut++] = (byte)(o0 & 0xff);
381         if (iOut < outLength)
382            out[iOut++] = (byte)(o1 & 0xff);
383         if (iOut < outLength)
384            out[iOut++] = (byte)(o2 & 0xff);
385      }
386      return out;
387   }
388
389   /**
390    * Shortcut for calling <c>base64Decode(String)</c> and converting the result to a UTF-8 encoded string.
391    *
392    * @param in The BASE-64 encoded string to decode.
393    * @return The decoded string, or an empty string if the input was <jk>null</jk>.
394    */
395   public static String base64DecodeToString(String in) {
396      var b = base64Decode(in);
397      return new String(b, UTF8);
398   }
399
400   /**
401    * BASE64-encodes the specified byte array.
402    *
403    * @param in The input byte array to convert.
404    * @return The byte array converted to a BASE-64 encoded string.
405    */
406   public static String base64Encode(byte[] in) {
407      if (in == null)
408         return null;
409      var outLength = (in.length * 4 + 2) / 3;   // Output length without padding
410      var out = new char[((in.length + 2) / 3) * 4];  // Length includes padding.
411      var iIn = 0;
412      var iOut = 0;
413      while (iIn < in.length) {
414         var i0 = in[iIn++] & 0xff;
415         var i1 = iIn < in.length ? in[iIn++] & 0xff : 0;
416         var i2 = iIn < in.length ? in[iIn++] & 0xff : 0;
417         var o0 = i0 >>> 2;
418         var o1 = ((i0 & 3) << 4) | (i1 >>> 4);
419         var o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
420         var o3 = i2 & 0x3F;
421         out[iOut++] = BASE64M1[o0];
422         out[iOut++] = BASE64M1[o1];
423         out[iOut] = iOut < outLength ? BASE64M1[o2] : '=';
424         iOut++;
425         out[iOut] = iOut < outLength ? BASE64M1[o3] : '=';
426         iOut++;
427      }
428      return new String(out);
429   }
430
431   /**
432    * Shortcut for calling <code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code>
433    *
434    * @param in The input string to convert.
435    * @return The string converted to BASE-64 encoding.
436    */
437   public static String base64EncodeToString(String in) {
438      if (in == null)
439         return null;
440      return base64Encode(in.getBytes(UTF8));
441   }
442
443   /**
444    * Builds a string using a functional approach with a StringBuilder.
445    *
446    * <p>
447    * Creates a new StringBuilder, applies the consumer to it, and returns the resulting string.
448    * This provides a functional way to build strings without manually managing the StringBuilder.
449    *
450    * <h4 class='section'>Examples:</h4>
451    * <p class='bjava'>
452    *    String <jv>result</jv> = buildString(<jv>sb</jv> -&gt; {
453    *       <jv>sb</jv>.append(<js>"Hello"</js>);
454    *       <jv>sb</jv>.append(<js>" "</js>);
455    *       <jv>sb</jv>.append(<js>"World"</js>);
456    *    });
457    *    <jc>// Returns: "Hello World"</jc>
458    *
459    *    String <jv>joined</jv> = buildString(<jv>sb</jv> -&gt; {
460    *       appendWithSeparator(<jv>sb</jv>, <js>"a"</js>, <js>", "</js>);
461    *       appendWithSeparator(<jv>sb</jv>, <js>"b"</js>, <js>", "</js>);
462    *       appendWithSeparator(<jv>sb</jv>, <js>"c"</js>, <js>", "</js>);
463    *    });
464    *    <jc>// Returns: "a, b, c"</jc>
465    * </p>
466    *
467    * @param builder The consumer that builds the string using the provided StringBuilder.
468    * @return The built string.
469    * @throws IllegalArgumentException If <c>builder</c> is <jk>null</jk>.
470    */
471   public static String buildString(Consumer<StringBuilder> builder) {
472      reqnn("builder", builder);
473      var sb = new StringBuilder();
474      builder.accept(sb);
475      return sb.toString();
476   }
477
478   /**
479    * Converts a string to camelCase format.
480    *
481    * <p>
482    * Handles various input formats:
483    * <ul>
484    *   <li>Space-separated: "hello world" → "helloWorld"</li>
485    *   <li>Underscore-separated: "hello_world" → "helloWorld"</li>
486    *   <li>Hyphen-separated: "hello-world" → "helloWorld"</li>
487    *   <li>PascalCase: "HelloWorld" → "helloWorld"</li>
488    *   <li>Already camelCase: "helloWorld" → "helloWorld"</li>
489    *   <li>Mixed case: "Hello_World-Test" → "helloWorldTest"</li>
490    * </ul>
491    *
492    * <h4 class='section'>Example:</h4>
493    * <p class='bjava'>
494    *    camelCase(<jk>null</jk>);                    <jc>// null</jc>
495    *    camelCase(<js>""</js>);                      <jc>// ""</jc>
496    *    camelCase(<js>"hello world"</js>);           <jc>// "helloWorld"</jc>
497    *    camelCase(<js>"hello_world"</js>);           <jc>// "helloWorld"</jc>
498    *    camelCase(<js>"hello-world"</js>);           <jc>// "helloWorld"</jc>
499    *    camelCase(<js>"HelloWorld"</js>);            <jc>// "helloWorld"</jc>
500    *    camelCase(<js>"helloWorld"</js>);            <jc>// "helloWorld"</jc>
501    *    camelCase(<js>"  hello   world  "</js>);     <jc>// "helloWorld"</jc>
502    * </p>
503    *
504    * @param str The string to convert.
505    * @return The camelCase string, or <jk>null</jk> if input is <jk>null</jk>.
506    */
507   public static String camelCase(String str) {
508      if (isEmpty(str))
509         return str;
510
511      var words = splitWords(str);
512      if (words.isEmpty())
513         return "";
514
515      var result = new StringBuilder();
516      for (var i = 0; i < words.size(); i++) {
517         var word = words.get(i);
518         if (i == 0) {
519            result.append(uncapitalize(word));
520         } else {
521            result.append(capitalize(lowerCaseRoot(word)));
522         }
523      }
524
525      return result.toString();
526   }
527
528   /**
529    * Capitalizes the first character of a string.
530    *
531    * <h4 class='section'>Example:</h4>
532    * <p class='bjava'>
533    *    capitalize(<jk>null</jk>);          <jc>// null</jc>
534    *    capitalize(<js>""</js>);            <jc>// ""</jc>
535    *    capitalize(<js>"hello"</js>);       <jc>// "Hello"</jc>
536    *    capitalize(<js>"Hello"</js>);       <jc>// "Hello"</jc>
537    *    capitalize(<js>"HELLO"</js>);       <jc>// "HELLO"</jc>
538    * </p>
539    *
540    * @param str The string to capitalize.
541    * @return The string with the first character capitalized, or <jk>null</jk> if input is <jk>null</jk>.
542    */
543   public static String capitalize(String str) {
544      if (isEmpty(str))
545         return str;
546      return Character.toUpperCase(str.charAt(0)) + str.substring(1);
547   }
548
549   /**
550    * Converts a comma-delimited string to a list.
551    *
552    * @param s The comma-delimited string.
553    * @return A new modifiable list. Never <jk>null</jk>.
554    */
555   public static List<String> cdlToList(String s) {
556      return split(s);
557   }
558
559   /**
560    * Converts a comma-delimited string to a set.
561    *
562    * @param s The comma-delimited string.
563    * @return A new {@link LinkedHashSet}. Never <jk>null</jk>.
564    */
565   public static Set<String> cdlToSet(String s) {
566      return split(s).stream().collect(Collectors.toCollection(LinkedHashSet::new));
567   }
568
569   /**
570    * Returns the character at the specified index in the string without throwing exceptions.
571    *
572    * <p>
573    * This is a null-safe and bounds-safe version of {@link String#charAt(int)}.
574    * Returns <c>0</c> (null character) if:
575    * <ul>
576    *   <li>The string is <jk>null</jk></li>
577    *   <li>The index is negative</li>
578    *   <li>The index is greater than or equal to the string length</li>
579    * </ul>
580    *
581    * <h4 class='section'>Example:</h4>
582    * <p class='bjava'>
583    *    charAt(<js>"Hello"</js>, 0);     <jc>// 'H'</jc>
584    *    charAt(<js>"Hello"</js>, 4);     <jc>// 'o'</jc>
585    *    charAt(<js>"Hello"</js>, 5);     <jc>// 0 (out of bounds)</jc>
586    *    charAt(<js>"Hello"</js>, -1);    <jc>// 0 (out of bounds)</jc>
587    *    charAt(<jk>null</jk>, 0);        <jc>// 0 (null string)</jc>
588    * </p>
589    *
590    * @param s The string.
591    * @param i The index position.
592    * @return The character at the specified index, or <c>0</c> if the index is out-of-range or the string is <jk>null</jk>.
593    * @see String#charAt(int)
594    */
595   public static char charAt(String s, int i) {
596      if (s == null || i < 0 || i >= s.length())
597         return 0;
598      return s.charAt(i);
599   }
600
601   /**
602    * Cleans a string by removing control characters and normalizing whitespace.
603    *
604    * <h4 class='section'>Example:</h4>
605    * <p class='bjava'>
606    *    clean(<js>"hello\u0000\u0001world"</js>);     <jc>// "hello world"</jc>
607    *    clean(<js>"hello  \t\n  world"</js>);         <jc>// "hello world"</jc>
608    * </p>
609    *
610    * @param str The string to clean.
611    * @return The cleaned string, or <jk>null</jk> if input is <jk>null</jk>.
612    */
613   public static String clean(String str) {
614      if (str == null)
615         return null;
616      str = removeControlChars(str);
617      return normalizeWhitespace(str);
618   }
619
620   /**
621    * Compares two strings lexicographically, but gracefully handles <jk>null</jk> values.
622    *
623    * <p>
624    * Null handling:
625    * <ul>
626    *   <li>Both <jk>null</jk> → returns <c>0</c> (equal)</li>
627    *   <li>First <jk>null</jk> → returns {@link Integer#MIN_VALUE}</li>
628    *   <li>Second <jk>null</jk> → returns {@link Integer#MAX_VALUE}</li>
629    *   <li>Neither <jk>null</jk> → returns the same as {@link String#compareTo(String)}</li>
630    * </ul>
631    *
632    * <h4 class='section'>Example:</h4>
633    * <p class='bjava'>
634    *    compare(<js>"apple"</js>, <js>"banana"</js>);   <jc>// negative (apple &lt; banana)</jc>
635    *    compare(<js>"banana"</js>, <js>"apple"</js>);   <jc>// positive (banana &gt; apple)</jc>
636    *    compare(<js>"apple"</js>, <js>"apple"</js>);    <jc>// 0 (equal)</jc>
637    *    compare(<jk>null</jk>, <jk>null</jk>);          <jc>// 0 (equal)</jc>
638    *    compare(<jk>null</jk>, <js>"apple"</js>);       <jc>// Integer.MIN_VALUE</jc>
639    *    compare(<js>"apple"</js>, <jk>null</jk>);       <jc>// Integer.MAX_VALUE</jc>
640    * </p>
641    *
642    * @param s1 The first string.
643    * @param s2 The second string.
644    * @return A negative integer, zero, or a positive integer as the first string is less than, equal to, or greater than the second.
645    * @see String#compareTo(String)
646    */
647   public static int compare(String s1, String s2) {
648      if (s1 == null && s2 == null)
649         return 0;
650      if (s1 == null)
651         return Integer.MIN_VALUE;
652      if (s2 == null)
653         return Integer.MAX_VALUE;
654      return s1.compareTo(s2);
655   }
656
657   /**
658    * Compares two strings lexicographically, ignoring case.
659    *
660    * <p>
661    * Returns a negative integer, zero, or a positive integer as the first string is less than, equal to, or greater than the second string, ignoring case.
662    *
663    * <h4 class='section'>Example:</h4>
664    * <p class='bjava'>
665    *    compareIgnoreCase(<js>"apple"</js>, <js>"BANANA"</js>);   <jc>// negative (apple &lt; banana)</jc>
666    *    compareIgnoreCase(<js>"Hello"</js>, <js>"hello"</js>);    <jc>// 0 (equal)</jc>
667    *    compareIgnoreCase(<js>"Zebra"</js>, <js>"apple"</js>);    <jc>// positive (zebra &gt; apple)</jc>
668    * </p>
669    *
670    * @param str1 The first string.
671    * @param str2 The second string.
672    * @return A negative integer, zero, or a positive integer as the first string is less than, equal to, or greater than the second.
673    */
674   public static int compareIgnoreCase(String str1, String str2) {
675      if (Objects.equals(str1, str2))
676         return 0;
677      if (str1 == null)
678         return -1;
679      if (str2 == null)
680         return 1;
681      return str1.compareToIgnoreCase(str2);
682   }
683
684   /**
685    * Compresses a UTF-8 string into a GZIP-compressed byte array.
686    *
687    * <p>
688    * This method compresses the input string using GZIP compression. The string is first converted to
689    * UTF-8 bytes, then compressed. Use {@link #decompress(byte[])} to decompress the result.
690    *
691    * <h4 class='section'>Example:</h4>
692    * <p class='bjava'>
693    *    <jc>// Compress a string</jc>
694    *    byte[] <jv>compressed</jv> = compress(<js>"Hello World"</js>);
695    *
696    *    <jc>// Decompress it back</jc>
697    *    String <jv>decompressed</jv> = decompress(<jv>compressed</jv>);
698    *    <jc>// Returns: "Hello World"</jc>
699    * </p>
700    *
701    * @param contents The UTF-8 string to compress.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
702    * @return The GZIP-compressed byte array.
703    * @throws IOException If compression fails.
704    * @see #decompress(byte[])
705    */
706   public static byte[] compress(String contents) throws IOException {
707      var baos = new ByteArrayOutputStream(contents.length() >> 1);
708      try (var gos = new GZIPOutputStream(baos)) {
709         gos.write(contents.getBytes());
710         gos.finish();
711         gos.flush();
712         return baos.toByteArray();
713      }
714   }
715
716   /**
717    * Checks if a string contains the specified character.
718    *
719    * <p>
720    * This is a null-safe operation. Returns <jk>false</jk> if the string is <jk>null</jk>.
721    *
722    * <h4 class='section'>Example:</h4>
723    * <p class='bjava'>
724    *    contains(<js>"Hello World"</js>, <js>'o'</js>);   <jc>// true</jc>
725    *    contains(<js>"Hello World"</js>, <js>'x'</js>);   <jc>// false</jc>
726    *    contains(<jk>null</jk>, <js>'a'</js>);            <jc>// false</jc>
727    * </p>
728    *
729    * @param s The string to check.
730    * @param c The character to check for.
731    * @return <jk>true</jk> if the string contains the specified character.
732    * @see #contains(String, CharSequence)
733    * @see #contains(String, String)
734    * @see #containsAny(String, char...)
735    */
736   public static boolean contains(String s, char c) {
737      return s != null && s.indexOf(c) >= 0;
738   }
739
740   /**
741    * Null-safe {@link String#contains(CharSequence)} operation.
742    *
743    * <p>
744    * Returns <jk>false</jk> if the string is <jk>null</jk>, otherwise behaves the same as
745    * {@link String#contains(CharSequence)}.
746    *
747    * <h4 class='section'>Example:</h4>
748    * <p class='bjava'>
749    *    contains(<js>"Hello World"</js>, <js>"World"</js>);   <jc>// true</jc>
750    *    contains(<js>"Hello World"</js>, <js>"Foo"</js>);     <jc>// false</jc>
751    *    contains(<jk>null</jk>, <js>"Hello"</js>);            <jc>// false</jc>
752    * </p>
753    *
754    * @param value The string to check.
755    * @param substring The substring to check for.
756    * @return <jk>true</jk> if the value contains the specified substring, <jk>false</jk> if the string is <jk>null</jk>.
757    * @see #contains(String, char)
758    * @see #contains(String, String)
759    * @see #containsAny(String, CharSequence...)
760    */
761   public static boolean contains(String value, CharSequence substring) {
762      return isNotNull(value) && value.contains(substring);
763   }
764
765   /**
766    * Null-safe check if a string contains another string.
767    *
768    * <p>
769    * Returns <jk>false</jk> if the string is <jk>null</jk>, otherwise behaves the same as
770    * {@link String#contains(CharSequence)}.
771    *
772    * <h4 class='section'>Example:</h4>
773    * <p class='bjava'>
774    *    contains(<js>"Hello World"</js>, <js>"World"</js>);   <jc>// true</jc>
775    *    contains(<js>"Hello World"</js>, <js>"Foo"</js>);     <jc>// false</jc>
776    *    contains(<jk>null</jk>, <js>"Hello"</js>);            <jc>// false</jc>
777    * </p>
778    *
779    * @param s The string to check.
780    * @param substring The substring to check for.
781    * @return <jk>true</jk> if the string contains the specified substring, <jk>false</jk> if the string is <jk>null</jk>.
782    * @see #contains(String, CharSequence)
783    * @see #containsAny(String, String...)
784    */
785   public static boolean contains(String s, String substring) {
786      return s != null && substring != null && s.contains(substring);
787   }
788
789   /**
790    * Checks if a string contains all of the specified characters.
791    *
792    * <p>
793    * This is a null-safe operation that returns <jk>false</jk> if:
794    * <ul>
795    *   <li>The string is <jk>null</jk></li>
796    *   <li>The values array is <jk>null</jk> or empty</li>
797    *   <li>Any of the specified characters are not found in the string</li>
798    * </ul>
799    *
800    * <h4 class='section'>Example:</h4>
801    * <p class='bjava'>
802    *    containsAll(<js>"Hello World"</js>, <js>'H'</js>, <js>'e'</js>, <js>'l'</js>);   <jc>// true (contains all)</jc>
803    *    containsAll(<js>"Hello World"</js>, <js>'H'</js>, <js>'x'</js>);                <jc>// false (missing 'x')</jc>
804    *    containsAll(<jk>null</jk>, <js>'a'</js>);                                      <jc>// false</jc>
805    * </p>
806    *
807    * @param s The string to check.
808    * @param values The characters to check for.
809    * @return <jk>true</jk> if the string contains all of the specified characters.
810    * @see #containsAny(String, char...)
811    */
812   public static boolean containsAll(String s, char...values) {
813      if (s == null || values == null || values.length == 0)
814         return false;
815      for (var v : values) {
816         if (s.indexOf(v) < 0)
817            return false;
818      }
819      return true;
820   }
821
822   /**
823    * Checks if a string contains all of the specified substrings.
824    *
825    * <p>
826    * This is a null-safe operation that returns <jk>false</jk> if:
827    * <ul>
828    *   <li>The string is <jk>null</jk></li>
829    *   <li>The values array is <jk>null</jk> or empty</li>
830    *   <li>Any of the specified substrings are not found in the string</li>
831    * </ul>
832    *
833    * <h4 class='section'>Example:</h4>
834    * <p class='bjava'>
835    *    containsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"World"</js>);   <jc>// true (contains all)</jc>
836    *    containsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);    <jc>// false (missing "Foo")</jc>
837    *    containsAll(<jk>null</jk>, <js>"Hello"</js>);                              <jc>// false</jc>
838    * </p>
839    *
840    * @param s The string to check.
841    * @param values The substrings to check for.
842    * @return <jk>true</jk> if the string contains all of the specified substrings.
843    * @see #containsAny(String, CharSequence...)
844    */
845   public static boolean containsAll(String s, CharSequence...values) {
846      if (s == null || values == null || values.length == 0)
847         return false;
848      for (var v : values) {
849         if (! s.contains(v))
850            return false;
851      }
852      return true;
853   }
854
855   /**
856    * Checks if a string contains all of the specified substrings.
857    *
858    * <p>
859    * This is a null-safe operation that returns <jk>false</jk> if:
860    * <ul>
861    *   <li>The string is <jk>null</jk></li>
862    *   <li>The values array is <jk>null</jk> or empty</li>
863    *   <li>Any of the specified substrings are not found in the string</li>
864    * </ul>
865    *
866    * <h4 class='section'>Example:</h4>
867    * <p class='bjava'>
868    *    containsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"World"</js>);   <jc>// true (contains all)</jc>
869    *    containsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);    <jc>// false (missing "Foo")</jc>
870    *    containsAll(<jk>null</jk>, <js>"Hello"</js>);                              <jc>// false</jc>
871    * </p>
872    *
873    * @param s The string to check.
874    * @param values The substrings to check for.
875    * @return <jk>true</jk> if the string contains all of the specified substrings.
876    * @see #containsAny(String, String...)
877    */
878   public static boolean containsAll(String s, String...values) {
879      if (s == null || values == null || values.length == 0)
880         return false;
881      for (var v : values) {
882         if (! s.contains(v))
883            return false;
884      }
885      return true;
886   }
887
888   /**
889    * Checks if a string contains any of the specified characters.
890    *
891    * <p>
892    * This is a null-safe operation that returns <jk>false</jk> if:
893    * <ul>
894    *   <li>The string is <jk>null</jk></li>
895    *   <li>The values array is <jk>null</jk> or empty</li>
896    *   <li>None of the specified characters are found in the string</li>
897    * </ul>
898    *
899    * <h4 class='section'>Example:</h4>
900    * <p class='bjava'>
901    *    containsAny(<js>"Hello World"</js>, <js>'o'</js>, <js>'x'</js>);   <jc>// true (contains 'o')</jc>
902    *    containsAny(<js>"Hello World"</js>, <js>'x'</js>, <js>'y'</js>);   <jc>// false</jc>
903    *    containsAny(<jk>null</jk>, <js>'a'</js>);                          <jc>// false</jc>
904    * </p>
905    *
906    * @param s The string to check.
907    * @param values The characters to check for.
908    * @return <jk>true</jk> if the string contains any of the specified characters.
909    * @see #contains(String, char)
910    * @see #containsAll(String, char...)
911    */
912   public static boolean containsAny(String s, char...values) {
913      if (s == null || values == null || values.length == 0)
914         return false;
915      for (var v : values) {
916         if (s.indexOf(v) >= 0)
917            return true;
918      }
919      return false;
920   }
921
922   /**
923    * Checks if a string contains any of the specified substrings.
924    *
925    * <p>
926    * This is a null-safe operation that returns <jk>false</jk> if:
927    * <ul>
928    *   <li>The string is <jk>null</jk></li>
929    *   <li>The values array is <jk>null</jk> or empty</li>
930    *   <li>None of the specified substrings are found in the string</li>
931    * </ul>
932    *
933    * <h4 class='section'>Example:</h4>
934    * <p class='bjava'>
935    *    containsAny(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);   <jc>// true (contains "Hello")</jc>
936    *    containsAny(<js>"Hello World"</js>, <js>"Foo"</js>, <js>"Bar"</js>);    <jc>// false</jc>
937    *    containsAny(<jk>null</jk>, <js>"Hello"</js>);                            <jc>// false</jc>
938    * </p>
939    *
940    * @param s The string to check.
941    * @param values The substrings to check for.
942    * @return <jk>true</jk> if the string contains any of the specified substrings.
943    * @see #contains(String, CharSequence)
944    * @see #containsAll(String, CharSequence...)
945    */
946   public static boolean containsAny(String s, CharSequence...values) {
947      if (s == null || values == null || values.length == 0)
948         return false;
949      for (var v : values) {
950         if (s.contains(v))
951            return true;
952      }
953      return false;
954   }
955
956   /**
957    * Checks if a string contains any of the specified substrings.
958    *
959    * <p>
960    * This is a null-safe operation that returns <jk>false</jk> if:
961    * <ul>
962    *   <li>The string is <jk>null</jk></li>
963    *   <li>The values array is <jk>null</jk> or empty</li>
964    *   <li>None of the specified substrings are found in the string</li>
965    * </ul>
966    *
967    * <h4 class='section'>Example:</h4>
968    * <p class='bjava'>
969    *    containsAny(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);   <jc>// true (contains "Hello")</jc>
970    *    containsAny(<js>"Hello World"</js>, <js>"Foo"</js>, <js>"Bar"</js>);    <jc>// false</jc>
971    *    containsAny(<jk>null</jk>, <js>"Hello"</js>);                            <jc>// false</jc>
972    *    containsAny(<js>"Hello"</js>);                                          <jc>// false (no values to check)</jc>
973    * </p>
974    *
975    * @param s The string to check.
976    * @param values The substrings to check for.
977    * @return <jk>true</jk> if the string contains any of the specified substrings.
978    * @see #contains(String, String)
979    * @see #containsAll(String, String...)
980    */
981   public static boolean containsAny(String s, String...values) {
982      if (s == null || values == null || values.length == 0)
983         return false;
984      for (var v : values) {
985         if (s.contains(v))
986            return true;
987      }
988      return false;
989   }
990
991   /**
992    * Checks if a string contains a substring, ignoring case.
993    *
994    * <h4 class='section'>Example:</h4>
995    * <p class='bjava'>
996    *    containsIgnoreCase(<js>"Hello World"</js>, <js>"world"</js>);     <jc>// true</jc>
997    *    containsIgnoreCase(<js>"Hello World"</js>, <js>"WORLD"</js>);     <jc>// true</jc>
998    *    containsIgnoreCase(<js>"hello world"</js>, <js>"xyz"</js>);       <jc>// false</jc>
999    * </p>
1000    *
1001    * @param str The string to search in.
1002    * @param search The substring to search for.
1003    * @return <jk>true</jk> if the string contains the substring (ignoring case), <jk>false</jk> otherwise.
1004    */
1005   public static boolean containsIgnoreCase(String str, String search) {
1006      if (str == null || search == null)
1007         return false;
1008      return str.toLowerCase(Locale.ROOT).contains(search.toLowerCase(Locale.ROOT));
1009   }
1010
1011   /**
1012    * Counts the number of occurrences of the specified character in the specified string.
1013    *
1014    * <p>
1015    * Returns <c>0</c> if the string is <jk>null</jk>.
1016    *
1017    * <h4 class='section'>Example:</h4>
1018    * <p class='bjava'>
1019    *    countChars(<js>"Hello World"</js>, <js>'o'</js>);   <jc>// 2</jc>
1020    *    countChars(<js>"Hello World"</js>, <js>'x'</js>);   <jc>// 0</jc>
1021    *    countChars(<jk>null</jk>, <js>'a'</js>);            <jc>// 0</jc>
1022    * </p>
1023    *
1024    * @param s The string to check.
1025    * @param c The character to count.
1026    * @return The number of occurrences of the character, or <c>0</c> if the string was <jk>null</jk>.
1027    */
1028   public static int countChars(String s, char c) {
1029      var count = 0;
1030      if (s == null)
1031         return count;
1032      for (var i = 0; i < s.length(); i++)
1033         if (s.charAt(i) == c)
1034            count++;
1035      return count;
1036   }
1037
1038   /**
1039    * Counts the number of occurrences of a substring within a string.
1040    *
1041    * <h4 class='section'>Example:</h4>
1042    * <p class='bjava'>
1043    *    countMatches(<js>"hello world world"</js>, <js>"world"</js>);     <jc>// 2</jc>
1044    *    countMatches(<js>"ababab"</js>, <js>"ab"</js>);                    <jc>// 3</jc>
1045    *    countMatches(<js>"hello"</js>, <js>"xyz"</js>);                    <jc>// 0</jc>
1046    * </p>
1047    *
1048    * @param str The string to search in.
1049    * @param search The substring to count.
1050    * @return The number of occurrences, or <c>0</c> if not found or if either parameter is <jk>null</jk> or empty.
1051    */
1052   public static int countMatches(String str, String search) {
1053      if (isEmpty(str) || isEmpty(search))
1054         return 0;
1055      var count = 0;
1056      var index = 0;
1057      while ((index = str.indexOf(search, index)) != -1) {
1058         count++;
1059         index += search.length();
1060      }
1061      return count;
1062   }
1063
1064   /**
1065    * Debug method for rendering non-ASCII character sequences.
1066    *
1067    * <p>
1068    * Converts non-printable and non-ASCII characters (outside the range <c>0x20-0x7E</c>) to hexadecimal
1069    * sequences in the format <js>"[hex]"</js>. Printable ASCII characters are left unchanged.
1070    *
1071    * <h4 class='section'>Example:</h4>
1072    * <p class='bjava'>
1073    *    decodeHex(<js>"Hello"</js>);              <jc>// "Hello"</jc>
1074    *    decodeHex(<js>"Hello\u0000World"</js>);   <jc>// "Hello[0]World"</jc>
1075    *    decodeHex(<js>"Hello\u00A9"</js>);        <jc>// "Hello[a9]"</jc>
1076    *    decodeHex(<jk>null</jk>);                 <jc>// null</jc>
1077    * </p>
1078    *
1079    * @param s The string to decode.
1080    * @return A string with non-ASCII characters converted to <js>"[hex]"</js> sequences, or <jk>null</jk> if input is <jk>null</jk>.
1081    */
1082   public static String decodeHex(String s) {
1083      if (s == null)
1084         return null;
1085      var sb = new StringBuilder();
1086      for (var c : s.toCharArray()) {
1087         if (c < ' ' || c > '~')
1088            sb.append("[").append(Integer.toHexString(c)).append("]");
1089         else
1090            sb.append(c);
1091      }
1092      return sb.toString();
1093   }
1094
1095   /**
1096    * Decompresses a GZIP-compressed byte array into a UTF-8 string.
1097    *
1098    * <p>
1099    * This method is the inverse of {@link #compress(String)}. It takes a byte array that was compressed
1100    * using GZIP compression and decompresses it into a UTF-8 encoded string.
1101    *
1102    * <h4 class='section'>Example:</h4>
1103    * <p class='bjava'>
1104    *    <jc>// Compress a string</jc>
1105    *    byte[] <jv>compressed</jv> = compress(<js>"Hello World"</js>);
1106    *
1107    *    <jc>// Decompress it back</jc>
1108    *    String <jv>decompressed</jv> = decompress(<jv>compressed</jv>);
1109    *    <jc>// Returns: "Hello World"</jc>
1110    * </p>
1111    *
1112    * @param is The GZIP-compressed byte array to decompress.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
1113    * @return The decompressed UTF-8 string.
1114    * @throws IOException If decompression fails or the input is not valid GZIP data.
1115    * @see #compress(String)
1116    */
1117   public static String decompress(byte[] is) throws IOException {
1118      return read(new GZIPInputStream(new ByteArrayInputStream(is)));
1119   }
1120
1121   /**
1122    * Returns the specified string, or the default string if that string is <jk>null</jk> or blank.
1123    *
1124    * @param str The string value to check.
1125    * @param defaultStr The default string to return if the string is <jk>null</jk> or blank.
1126    * @return The string value, or the default string if the string is <jk>null</jk> or blank.
1127    */
1128   public static String defaultIfBlank(String str, String defaultStr) {
1129      return isBlank(str) ? defaultStr : str;
1130   }
1131
1132   /**
1133    * Returns the specified string, or the default string if that string is <jk>null</jk> or empty.
1134    *
1135    * @param str The string value to check.
1136    * @param defaultStr The default string to return if the string is <jk>null</jk> or empty.
1137    * @return The string value, or the default string if the string is <jk>null</jk> or empty.
1138    */
1139   public static String defaultIfEmpty(String str, String defaultStr) {
1140      return isEmpty(str) ? defaultStr : str;
1141   }
1142
1143   /**
1144    * Finds the position where the two strings first differ.
1145    *
1146    * <p>
1147    * This method compares strings character by character and returns the index of the first position
1148    * where they differ. If the strings are equal, returns <c>-1</c>. If one string is a prefix of the other,
1149    * returns the length of the shorter string.
1150    *
1151    * <h4 class='section'>Example:</h4>
1152    * <p class='bjava'>
1153    *    diffPosition(<js>"apple"</js>, <js>"apple"</js>);      <jc>// -1 (equal)</jc>
1154    *    diffPosition(<js>"apple"</js>, <js>"apricot"</js>);    <jc>// 2 (differs at 'p' vs 'r')</jc>
1155    *    diffPosition(<js>"apple"</js>, <js>"app"</js>);        <jc>// 3 (shorter string ends here)</jc>
1156    *    diffPosition(<js>"app"</js>, <js>"apple"</js>);        <jc>// 3 (shorter string ends here)</jc>
1157    * </p>
1158    *
1159    * @param s1 The first string.
1160    * @param s2 The second string.
1161    * @return The position where the two strings differ, or <c>-1</c> if they're equal.
1162    * @see #diffPositionIc(String, String)
1163    */
1164   public static int diffPosition(String s1, String s2) {
1165      s1 = emptyIfNull(s1);
1166      s2 = emptyIfNull(s2);
1167      var i = 0;
1168      var len = Math.min(s1.length(), s2.length());
1169      while (i < len) {
1170         var j = s1.charAt(i) - s2.charAt(i);
1171         if (j != 0)
1172            return i;
1173         i++;
1174      }
1175      if (equal(s1.length(), s2.length()))
1176         return -1;
1177      return i;
1178   }
1179
1180   /**
1181    * Finds the position where the two strings first differ, ignoring case.
1182    *
1183    * <p>
1184    * This method compares strings character by character (case-insensitive) and returns the index of the first position
1185    * where they differ. If the strings are equal (ignoring case), returns <c>-1</c>. If one string is a prefix of the other,
1186    * returns the length of the shorter string.
1187    *
1188    * <h4 class='section'>Example:</h4>
1189    * <p class='bjava'>
1190    *    diffPositionIc(<js>"Apple"</js>, <js>"apple"</js>);      <jc>// -1 (equal ignoring case)</jc>
1191    *    diffPositionIc(<js>"Apple"</js>, <js>"Apricot"</js>);    <jc>// 2 (differs at 'p' vs 'r')</jc>
1192    *    diffPositionIc(<js>"APPLE"</js>, <js>"app"</js>);        <jc>// 3 (shorter string ends here)</jc>
1193    * </p>
1194    *
1195    * @param s1 The first string.
1196    * @param s2 The second string.
1197    * @return The position where the two strings differ, or <c>-1</c> if they're equal (ignoring case).
1198    * @see #diffPosition(String, String)
1199    */
1200   public static int diffPositionIc(String s1, String s2) {
1201      s1 = emptyIfNull(s1);
1202      s2 = emptyIfNull(s2);
1203      var i = 0;
1204      var len = Math.min(s1.length(), s2.length());
1205      while (i < len) {
1206         var j = toLowerCase(s1.charAt(i)) - toLowerCase(s2.charAt(i));
1207         if (j != 0)
1208            return i;
1209         i++;
1210      }
1211      if (equal(s1.length(), s2.length()))
1212         return -1;
1213      return i;
1214   }
1215
1216   /**
1217    * Removes duplicate elements from a string array, preserving order.
1218    *
1219    * <p>
1220    * Returns <jk>null</jk> if the array is <jk>null</jk>.
1221    * Uses a {@link LinkedHashSet} to preserve insertion order while removing duplicates.
1222    *
1223    * <h4 class='section'>Examples:</h4>
1224    * <p class='bjava'>
1225    *    String[] <jv>array</jv> = {<js>"foo"</js>, <js>"bar"</js>, <js>"foo"</js>, <js>"baz"</js>, <js>"bar"</js>};
1226    *    String[] <jv>unique</jv> = distinct(<jv>array</jv>);
1227    *    <jc>// Returns: ["foo", "bar", "baz"]</jc>
1228    * </p>
1229    *
1230    * @param array The array to process. Can be <jk>null</jk>.
1231    * @return A new array with duplicate elements removed, or an empty array if the array was <jk>null</jk>.
1232    */
1233   public static String[] distinct(String[] array) {
1234      if (array == null)
1235         return new String[0];
1236      return Arrays.stream(array).collect(Collectors.toCollection(LinkedHashSet::new)).toArray(new String[0]);
1237   }
1238
1239   /**
1240    * Generates a Double Metaphone code for a string.
1241    *
1242    * <p>
1243    * Double Metaphone is an improved version of Metaphone that returns two codes:
1244    * a primary code and an alternate code. This handles more edge cases and variations.
1245    *
1246    * <h4 class='section'>Example:</h4>
1247    * <p class='bjava'>
1248    *    doubleMetaphone(<js>"Smith"</js>);   <jc>// "SM0"</jc>
1249    *    doubleMetaphone(<js>"Schmidt"</js>); <jc>// "XMT"</jc>
1250    * </p>
1251    *
1252    * @param str The string to generate a Double Metaphone code for. Can be <jk>null</jk>.
1253    * @return An array with two elements: [primary code, alternate code]. Returns an empty array if input is <jk>null</jk> or empty.
1254    */
1255   public static String[] doubleMetaphone(String str) {
1256      if (isEmpty(str))
1257         return new String[0];
1258
1259      // For simplicity, return the same code for both primary and alternate
1260      // A full Double Metaphone implementation would be much more complex
1261      var primary = metaphone(str);
1262
1263      // Generate alternate code (simplified - full implementation would have different rules)
1264      var alternate = primary;
1265
1266      return array(primary, alternate);
1267   }
1268
1269   /**
1270    * Returns the specified string, or an empty string if that string is <jk>null</jk>.
1271    *
1272    * @param str The string value to check.
1273    * @return The string value, or an empty string if the string is <jk>null</jk>.
1274    */
1275   public static String emptyIfNull(String str) {
1276      return coalesce(str, "");
1277   }
1278
1279   /** Returns the string representation of an object, or <jk>""</jk> if the object is <jk>null</jk>. */
1280   public static String emptyIfNull(Object value) {
1281      return value == null ? "" : value.toString();
1282   }
1283
1284   /**
1285    * Checks if a string ends with the specified character.
1286    *
1287    * <p>
1288    * This is a null-safe operation. Returns <jk>false</jk> if the string is <jk>null</jk> or empty.
1289    *
1290    * <h4 class='section'>Example:</h4>
1291    * <p class='bjava'>
1292    *    endsWith(<js>"Hello"</js>, <js>'o'</js>);     <jc>// true</jc>
1293    *    endsWith(<js>"Hello"</js>, <js>'H'</js>);     <jc>// false</jc>
1294    *    endsWith(<jk>null</jk>, <js>'o'</js>);        <jc>// false</jc>
1295    *    endsWith(<js>""</js>, <js>'o'</js>);          <jc>// false</jc>
1296    * </p>
1297    *
1298    * @param s The string to check. Can be <jk>null</jk>.
1299    * @param c The character to check for.
1300    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character.
1301    * @see #endsWith(String, String)
1302    * @see #endsWithAny(String, char...)
1303    * @see String#endsWith(String)
1304    */
1305   public static boolean endsWith(String s, char c) {
1306      if (isNotNull(s)) {
1307         var i = s.length();
1308         if (i > 0)
1309            return s.charAt(i - 1) == c;
1310      }
1311      return false;
1312   }
1313
1314   /**
1315    * Checks if a string ends with the specified string.
1316    *
1317    * <p>
1318    * This is a null-safe operation. Returns <jk>false</jk> if the string is <jk>null</jk>.
1319    * Otherwise behaves the same as {@link String#endsWith(String)}.
1320    *
1321    * <h4 class='section'>Example:</h4>
1322    * <p class='bjava'>
1323    *    endsWith(<js>"Hello World"</js>, <js>"World"</js>);   <jc>// true</jc>
1324    *    endsWith(<js>"Hello World"</js>, <js>"Hello"</js>);   <jc>// false</jc>
1325    *    endsWith(<jk>null</jk>, <js>"World"</js>);            <jc>// false</jc>
1326    * </p>
1327    *
1328    * @param s The string to check. Can be <jk>null</jk>.
1329    * @param suffix The suffix to check for.
1330    * @return <jk>true</jk> if the string ends with the specified suffix.
1331    * @see #endsWith(String, char)
1332    * @see #endsWithAny(String, String...)
1333    * @see String#endsWith(String)
1334    */
1335   public static boolean endsWith(String s, String suffix) {
1336      return s != null && suffix != null && s.endsWith(suffix);
1337   }
1338
1339   /**
1340    * Checks if a string ends with any of the specified characters.
1341    *
1342    * <p>
1343    * This is a null-safe operation. Returns <jk>false</jk> if:
1344    * <ul>
1345    *   <li>The string is <jk>null</jk> or empty</li>
1346    *   <li>The characters array is <jk>null</jk> or empty</li>
1347    *   <li>The string does not end with any of the specified characters</li>
1348    * </ul>
1349    *
1350    * <h4 class='section'>Example:</h4>
1351    * <p class='bjava'>
1352    *    endsWithAny(<js>"Hello"</js>, <js>'o'</js>, <js>'x'</js>);     <jc>// true (ends with 'o')</jc>
1353    *    endsWithAny(<js>"Hello"</js>, <js>'x'</js>, <js>'y'</js>);     <jc>// false</jc>
1354    *    endsWithAny(<jk>null</jk>, <js>'o'</js>);                      <jc>// false</jc>
1355    * </p>
1356    *
1357    * @param s The string to check. Can be <jk>null</jk>.
1358    * @param c The characters to check for.
1359    * @return <jk>true</jk> if the string ends with any of the specified characters.
1360    * @see #endsWith(String, char)
1361    * @see #endsWithAny(String, String...)
1362    */
1363   public static boolean endsWithAny(String s, char...c) {
1364      if (isNotNull(s)) {
1365         var i = s.length();
1366         if (i > 0) {
1367            var c2 = s.charAt(i - 1);
1368            for (var cc : c)
1369               if (c2 == cc)
1370                  return true;
1371         }
1372      }
1373      return false;
1374   }
1375
1376   /**
1377    * Checks if a string ends with any of the specified strings.
1378    *
1379    * <p>
1380    * This is a null-safe operation. Returns <jk>false</jk> if:
1381    * <ul>
1382    *   <li>The string is <jk>null</jk></li>
1383    *   <li>The suffixes array is <jk>null</jk> or empty</li>
1384    *   <li>The string does not end with any of the specified suffixes</li>
1385    * </ul>
1386    *
1387    * <h4 class='section'>Example:</h4>
1388    * <p class='bjava'>
1389    *    endsWithAny(<js>"Hello World"</js>, <js>"World"</js>, <js>"Foo"</js>);   <jc>// true (ends with "World")</jc>
1390    *    endsWithAny(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);   <jc>// false</jc>
1391    *    endsWithAny(<jk>null</jk>, <js>"World"</js>);                            <jc>// false</jc>
1392    * </p>
1393    *
1394    * @param s The string to check. Can be <jk>null</jk>.
1395    * @param suffixes The suffixes to check for.
1396    * @return <jk>true</jk> if the string ends with any of the specified suffixes.
1397    * @see #endsWith(String, String)
1398    * @see #endsWithAny(String, char...)
1399    * @see String#endsWith(String)
1400    */
1401   public static boolean endsWithAny(String s, String...suffixes) {
1402      if (s == null || suffixes == null || suffixes.length == 0)
1403         return false;
1404      for (var suffix : suffixes) {
1405         if (s.endsWith(suffix))
1406            return true;
1407      }
1408      return false;
1409   }
1410
1411   /**
1412    * Checks if a string ends with a suffix, ignoring case.
1413    *
1414    * <h4 class='section'>Example:</h4>
1415    * <p class='bjava'>
1416    *    endsWithIgnoreCase(<js>"Hello World"</js>, <js>"world"</js>);     <jc>// true</jc>
1417    *    endsWithIgnoreCase(<js>"Hello World"</js>, <js>"WORLD"</js>);     <jc>// true</jc>
1418    *    endsWithIgnoreCase(<js>"hello world"</js>, <js>"hello"</js>);     <jc>// false</jc>
1419    * </p>
1420    *
1421    * @param str The string to check.
1422    * @param suffix The suffix to check for.
1423    * @return <jk>true</jk> if the string ends with the suffix (ignoring case), <jk>false</jk> otherwise.
1424    */
1425   public static boolean endsWithIgnoreCase(String str, String suffix) {
1426      if (str == null || suffix == null)
1427         return false;
1428      return str.toLowerCase(Locale.ROOT).endsWith(suffix.toLowerCase(Locale.ROOT));
1429   }
1430
1431   /**
1432    * Calculates the entropy of a string.
1433    *
1434    * <p>
1435    * Entropy measures the randomness or information content of a string.
1436    * Higher entropy indicates more randomness. The formula used is:
1437    * <c>H(X) = -Σ P(x) * log2(P(x))</c>
1438    * where P(x) is the probability of character x.
1439    *
1440    * <h4 class='section'>Example:</h4>
1441    * <p class='bjava'>
1442    *    entropy(<js>"aaaa"</js>);        <jc>// 0.0 (no randomness)</jc>
1443    *    entropy(<js>"abcd"</js>);        <jc>// 2.0 (high randomness)</jc>
1444    *    entropy(<js>"hello"</js>);       <jc>// ~2.32</jc>
1445    * </p>
1446    *
1447    * @param str The string to calculate entropy for. Can be <jk>null</jk>.
1448    * @return The entropy value (0.0 or higher), or <c>0.0</c> if the string is <jk>null</jk> or empty.
1449    */
1450   public static double entropy(String str) {
1451      if (isEmpty(str))
1452         return 0.0;
1453
1454      var length = str.length();
1455
1456      // Count character frequencies
1457      var charCounts = new int[Character.MAX_VALUE + 1];
1458      for (var i = 0; i < length; i++) {
1459         charCounts[str.charAt(i)]++;
1460      }
1461
1462      // Calculate entropy
1463      var entropy = 0.0;
1464      for (var count : charCounts) {
1465         if (count > 0) {
1466            var probability = (double)count / length;
1467            entropy -= probability * (Math.log(probability) / Math.log(2.0));
1468         }
1469      }
1470
1471      return entropy;
1472   }
1473
1474   /**
1475    * Tests two objects for case-insensitive string equality.
1476    *
1477    * <p>
1478    * Converts both objects to strings using {@link Object#toString()} before comparison.
1479    * This method handles <jk>null</jk> values gracefully:
1480    * <ul>
1481    *   <li>Both <jk>null</jk> → returns <jk>true</jk></li>
1482    *   <li>One <jk>null</jk> → returns <jk>false</jk></li>
1483    *   <li>Neither <jk>null</jk> → compares string representations ignoring case</li>
1484    * </ul>
1485    *
1486    * <h4 class='section'>Example:</h4>
1487    * <p class='bjava'>
1488    *    equalsIgnoreCase(<js>"Hello"</js>, <js>"HELLO"</js>);     <jc>// true</jc>
1489    *    equalsIgnoreCase(<js>"Hello"</js>, <js>"World"</js>);     <jc>// false</jc>
1490    *    equalsIgnoreCase(<jk>null</jk>, <jk>null</jk>);            <jc>// true</jc>
1491    *    equalsIgnoreCase(<js>"Hello"</js>, <jk>null</jk>);         <jc>// false</jc>
1492    *    equalsIgnoreCase(123, <js>"123"</js>);                    <jc>// true (converts 123 to "123")</jc>
1493    * </p>
1494    *
1495    * @param a Object 1.
1496    * @param b Object 2.
1497    * @return <jk>true</jk> if both objects are equal ignoring case.
1498    * @see #equalsIgnoreCase(String, String)
1499    * @see ObjectUtils#equalIgnoreCase(Object, Object)
1500    */
1501   public static boolean equalsIgnoreCase(Object a, Object b) {
1502      if (a == null && b == null)
1503         return true;
1504      if (a == null || b == null)
1505         return false;
1506      return a.toString().equalsIgnoreCase(b.toString());
1507   }
1508
1509   /**
1510    * Tests two strings for case-insensitive equality, but gracefully handles nulls.
1511    *
1512    * <p>
1513    * This method handles <jk>null</jk> values gracefully:
1514    * <ul>
1515    *   <li>Both <jk>null</jk> → returns <jk>true</jk> (same reference check)</li>
1516    *   <li>One <jk>null</jk> → returns <jk>false</jk></li>
1517    *   <li>Neither <jk>null</jk> → compares strings ignoring case</li>
1518    * </ul>
1519    *
1520    * <h4 class='section'>Example:</h4>
1521    * <p class='bjava'>
1522    *    equalsIgnoreCase(<js>"Hello"</js>, <js>"hello"</js>);     <jc>// true</jc>
1523    *    equalsIgnoreCase(<js>"Hello"</js>, <js>"WORLD"</js>);     <jc>// false</jc>
1524    *    equalsIgnoreCase(<jk>null</jk>, <jk>null</jk>);           <jc>// true</jc>
1525    *    equalsIgnoreCase(<js>"Hello"</js>, <jk>null</jk>);        <jc>// false</jc>
1526    * </p>
1527    *
1528    * @param str1 The first string.
1529    * @param str2 The second string.
1530    * @return <jk>true</jk> if the strings are equal ignoring case, <jk>false</jk> otherwise.
1531    * @see #equalsIgnoreCase(Object, Object)
1532    * @see #equalsIgnoreCase(String, String)
1533    */
1534   public static boolean equalsIgnoreCase(String str1, String str2) {
1535      if (Objects.equals(str1, str2))
1536         return true;
1537      if (str1 == null || str2 == null)
1538         return false;
1539      return str1.equalsIgnoreCase(str2);
1540   }
1541
1542   /**
1543    * Escapes the specified characters in the string.
1544    *
1545    * @param s The string with characters to escape.  Can be <jk>null</jk> (returns <jk>null</jk>).
1546    * @param escaped The characters to escape.  Must not be <jk>null</jk> when the string is non-empty (a <jk>null</jk> argument throws {@link NullPointerException}).
1547    * @return The string with characters escaped, or the same string if no escapable characters were found.
1548    */
1549   public static String escapeChars(String s, AsciiSet escaped) {
1550      if (s == null || s.isEmpty())
1551         return s;
1552
1553      var count = 0;
1554      for (var i = 0; i < s.length(); i++)
1555         if (escaped.contains(s.charAt(i)))
1556            count++;
1557      if (count == 0)
1558         return s;
1559
1560      var sb = new StringBuffer(s.length() + count);
1561      for (var i = 0; i < s.length(); i++) {
1562         var c = s.charAt(i);
1563         if (escaped.contains(c))
1564            sb.append('\\');
1565         sb.append(c);
1566      }
1567      return sb.toString();
1568   }
1569
1570   /**
1571    * Escapes a string for safe inclusion in Java source code.
1572    *
1573    * <p>This method converts special characters to their Java escape sequences and
1574    * converts non-printable ASCII characters to Unicode escape sequences.
1575    *
1576    * <h4 class='section'>Escape mappings:</h4>
1577    * <ul>
1578    *   <li>{@code "} → {@code \"}</li>
1579    *   <li>{@code \} → {@code \\}</li>
1580    *   <li>{@code \n} → {@code \\n}</li>
1581    *   <li>{@code \r} → {@code \\r}</li>
1582    *   <li>{@code \t} → {@code \\t}</li>
1583    *   <li>{@code \f} → {@code \\f}</li>
1584    *   <li>{@code \b} → {@code \\b}</li>
1585    *   <li>Non-printable characters → {@code \\uXXXX}</li>
1586    * </ul>
1587    *
1588    * <h4 class='section'>Example:</h4>
1589    * <p class='bjava'>
1590    *   <jk>var</jk> <jv>escaped</jv> = <jsm>escapeForJava</jsm>(<js>"Hello\nWorld\"Test\""</js>);
1591    *   <jc>// Returns: "Hello\\nWorld\\\"Test\\\""</jc>
1592    * </p>
1593    *
1594    * @param s The string to escape.
1595    * @return The escaped string safe for Java source code, or <jk>null</jk> if input is <jk>null</jk>.
1596    */
1597   public static String escapeForJava(String s) {
1598      if (s == null)
1599         return null;
1600      var sb = new StringBuilder();
1601      for (var c : s.toCharArray()) {
1602         sb.append(switch (c) {
1603            case '\"' -> "\\\"";
1604            case '\\' -> "\\\\";
1605            case '\n' -> "\\n";
1606            case '\r' -> "\\r";
1607            case '\t' -> "\\t";
1608            case '\f' -> "\\f";
1609            case '\b' -> "\\b";
1610            default -> {
1611               if (c < 0x20 || c > 0x7E)
1612                  yield String.format("\\u%04x", (int)c);
1613               yield String.valueOf(c);
1614            }
1615         });
1616      }
1617      return sb.toString();
1618   }
1619
1620   /**
1621    * Escapes a serialized-JSON string so it can be safely embedded as the raw-text content of an HTML
1622    * {@code <script>} element.
1623    *
1624    * <p>
1625    * This is the escaper for a JSON <i>sidecar</i> &mdash; a payload handed to the browser as the content of a
1626    * {@code <script type="application/json" id="...">} element, or assigned to a variable inside a plain inline
1627    * {@code <script>}.  It substitutes only characters whose JSON encoding is unambiguous, so the result stays
1628    * valid, round-trippable JSON that parses back to the original value:
1629    *
1630    * <ul class='spaced-list'>
1631    *    <li>{@code <} &rarr; {@code \u005Cu003c}
1632    *    <li>{@code U+2028} (LINE SEPARATOR) &rarr; {@code \u005Cu2028}
1633    *    <li>{@code U+2029} (PARAGRAPH SEPARATOR) &rarr; {@code \u005Cu2029}
1634    * </ul>
1635    *
1636    * <h4 class='section'>Injection vectors this defends against (do not "simplify" these away):</h4>
1637    * <ul class='spaced-list'>
1638    *    <li><b>{@code </script>} break-out.</b>  A {@code <script>} element's content is <i>raw text</i>: the HTML
1639    *       parser scans it only for an end tag, which it matches ASCII-case-insensitively as {@code </script}
1640    *       followed by whitespace, {@code /}, or {@code >}.  A payload containing that sequence closes the element
1641    *       early and everything after it becomes live markup.  Escaping {@code <} neutralizes it at the source.  A
1642    *       targeted {@code replace("</script>", ...)} does <b>not</b>: it misses {@code </SCRIPT>},
1643    *       {@code </script foo>}, and {@code </script/}.
1644    *    <li><b>{@code <!--} / {@code <script} double-escape confusion.</b>  Inside script content, {@code <!--}
1645    *       switches the tokenizer to its "script data escaped" state, and a subsequent {@code <script} switches it
1646    *       to "script data double escaped" &mdash; a state in which the element's own real {@code </script>} no
1647    *       longer closes it, so the rest of the document is swallowed as script data and the page's structure is
1648    *       attacker-controlled.  Escaping {@code </} alone does <b>not</b> stop this; escaping {@code <} does.
1649    *    <li><b>{@code U+2028}/{@code U+2029} string-literal break-out.</b>  Both are legal raw characters inside a
1650    *       JSON string but were JavaScript <i>line terminators</i> before ES2019, so a payload embedded in a
1651    *       JavaScript expression context could terminate its string literal mid-value.  Escaped unconditionally so
1652    *       one helper is correct for both the {@code type="application/json"} and the inline-expression form.
1653    * </ul>
1654    *
1655    * <h4 class='section'>Why <c>&amp;</c> and {@code >} are deliberately NOT escaped:</h4>
1656    * <p>
1657    * Raw-text content is not entity-decoded by the browser, so HTML character references buy no safety here and
1658    * {@link #escapeHtml(String)} is the <b>wrong</b> tool for this context &mdash; it would rewrite <c>&amp;</c>
1659    * to <c>&amp;amp;</c> and corrupt the payload the moment {@code JSON.parse} reads it back verbatim.  For the
1660    * same reason {@code >} cannot terminate anything on its own once {@code <} is gone.
1661    *
1662    * <h4 class='section'>Why the substitutions are safe:</h4>
1663    * <p>
1664    * In valid JSON, {@code <}, {@code U+2028}, and {@code U+2029} can only occur inside a string literal (JSON's
1665    * inter-token whitespace is limited to space, tab, CR, and LF), and inside a string literal a backslash is
1666    * always itself escaped as {@code \u005C\u005C}.  The inserted backslash therefore always begins a fresh
1667    * escape sequence rather than extending a preceding one.
1668    *
1669    * <h4 class='section'>Example:</h4>
1670    * <p class='bjava'>
1671    *    <jc>// Serialize, escape, then insert as VERBATIM raw script content (never entity-encoded).</jc>
1672    *    String <jv>json</jv> = <jsm>escapeForScript</jsm>(Json.<jsm>of</jsm>(<jv>meta</jv>));
1673    *    <jc>// {"label":"x\u005Cu003c/script&gt;"} - parses back to the original "x&lt;/script&gt;"</jc>
1674    * </p>
1675    *
1676    * @param json The <b>already-serialized JSON</b> to escape.  Must be JSON: on arbitrary text these
1677    *    substitutions are not escape sequences and would alter the value.  Can be <jk>null</jk> (returns
1678    *    <jk>null</jk>).
1679    * @return The break-out-safe JSON, or <jk>null</jk> if input is <jk>null</jk>.
1680    * @see #escapeHtml(String)
1681    */
1682   public static String escapeForScript(String json) {
1683      if (json == null)
1684         return null;
1685      // The search terms are the RAW characters (a Java source \\u escape is pre-processed to the single character);
1686      // the replacements are their 6-character JSON escape sequences.
1687      return json
1688         .replace("<", "\\u003c")
1689         .replace("\u2028", "\\u2028")
1690         .replace("\u2029", "\\u2029");
1691   }
1692
1693   /**
1694    * Escapes HTML entities in a string.
1695    *
1696    * <p>
1697    * Use this for text interpolated into HTML <i>element content or an attribute value</i>.  It is <b>not</b> the
1698    * right escaper for the raw-text content of a {@code <script>} element &mdash; see
1699    * {@link #escapeForScript(String)} for that.
1700    *
1701    * <p>
1702    * Escapes the following characters:
1703    * <ul>
1704    *   <li><js>'&amp;'</js> → <js>"&amp;amp;"</js></li>
1705    *   <li><js>'&lt;'</js> → <js>"&amp;lt;"</js></li>
1706    *   <li><js>'&gt;'</js> → <js>"&amp;gt;"</js></li>
1707    *   <li><js>'"'</js> → <js>"&amp;quot;"</js></li>
1708    *   <li><js>'\''</js> → <js>"&amp;#39;"</js></li>
1709    * </ul>
1710    *
1711    * <h4 class='section'>Example:</h4>
1712    * <p class='bjava'>
1713    *    escapeHtml(<js>"&lt;script&gt;alert('xss')&lt;/script&gt;"</js>);
1714    *    <jc>// Returns: "&amp;lt;script&amp;gt;alert(&amp;#39;xss&amp;#39;)&amp;lt;/script&amp;gt;"</jc>
1715    * </p>
1716    *
1717    * @param str The string to escape.
1718    * @return The escaped string, or <jk>null</jk> if input is <jk>null</jk>.
1719    */
1720   public static String escapeHtml(String str) {
1721      if (str == null)
1722         return null;
1723      var sb = new StringBuilder(str.length() * 2);
1724      for (var i = 0; i < str.length(); i++) {
1725         var c = str.charAt(i);
1726         switch (c) {
1727            case '&' -> sb.append("&amp;");
1728            case '<' -> sb.append("&lt;");
1729            case '>' -> sb.append("&gt;");
1730            case '"' -> sb.append("&quot;");
1731            case '\'' -> sb.append("&#39;");
1732            default -> sb.append(c);
1733         }
1734      }
1735      return sb.toString();
1736   }
1737
1738   /**
1739    * Escapes regex special characters in a string.
1740    *
1741    * <p>
1742    * Escapes the following regex special characters: <js>\.*+?^${}()[]|\\</js>
1743    *
1744    * <h4 class='section'>Example:</h4>
1745    * <p class='bjava'>
1746    *    escapeRegex(<js>"file.txt"</js>);        <jc>// Returns: "file\\.txt"</jc>
1747    *    escapeRegex(<js>"price: $10.99"</js>);   <jc>// Returns: "price: \\$10\\.99"</jc>
1748    * </p>
1749    *
1750    * @param str The string to escape.
1751    * @return The escaped string, or <jk>null</jk> if input is <jk>null</jk>.
1752    */
1753   public static String escapeRegex(String str) {
1754      if (str == null)
1755         return null;
1756      // Escape regex special characters: . * + ? ^ $ { } ( ) [ ] | \
1757      return str.replace("\\", "\\\\").replace(".", "\\.").replace("*", "\\*").replace("+", "\\+").replace("?", "\\?").replace("^", "\\^").replace("$", "\\$").replace("{", "\\{").replace("}", "\\}")
1758         .replace("(", "\\(").replace(")", "\\)").replace("[", "\\[").replace("]", "\\]").replace("|", "\\|");
1759   }
1760
1761   /**
1762    * Escapes SQL string literals by doubling single quotes.
1763    *
1764    * <p>
1765    * Basic SQL escaping for string literals. Escapes single quotes by doubling them.
1766    * This is a basic implementation - for production use, consider using prepared statements.
1767    *
1768    * <h4 class='section'>Example:</h4>
1769    * <p class='bjava'>
1770    *    escapeSql(<js>"O'Brien"</js>);     <jc>// Returns: "O''Brien"</jc>
1771    *    escapeSql(<js>"It's a test"</js>); <jc>// Returns: "It''s a test"</jc>
1772    * </p>
1773    *
1774    * @param str The string to escape.
1775    * @return The escaped string, or <jk>null</jk> if input is <jk>null</jk>.
1776    */
1777   public static String escapeSql(String str) {
1778      if (str == null)
1779         return null;
1780      return str.replace("'", "''");
1781   }
1782
1783   /**
1784    * Escapes XML entities in a string.
1785    *
1786    * <p>
1787    * Escapes the following characters:
1788    * <ul>
1789    *   <li><js>'&amp;'</js> → <js>"&amp;amp;"</js></li>
1790    *   <li><js>'&lt;'</js> → <js>"&amp;lt;"</js></li>
1791    *   <li><js>'&gt;'</js> → <js>"&amp;gt;"</js></li>
1792    *   <li><js>'"'</js> → <js>"&amp;quot;"</js></li>
1793    *   <li><js>'\''</js> → <js>"&amp;apos;"</js></li>
1794    * </ul>
1795    *
1796    * <h4 class='section'>Example:</h4>
1797    * <p class='bjava'>
1798    *    escapeXml(<js>"&lt;tag attr='value'&gt;text&lt;/tag&gt;"</js>);
1799    *    <jc>// Returns: "&amp;lt;tag attr=&amp;apos;value&amp;apos;&amp;gt;text&amp;lt;/tag&amp;gt;"</jc>
1800    * </p>
1801    *
1802    * @param str The string to escape.
1803    * @return The escaped string, or <jk>null</jk> if input is <jk>null</jk>.
1804    */
1805   public static String escapeXml(String str) {
1806      if (str == null)
1807         return null;
1808      var sb = new StringBuilder(str.length() * 2);
1809      for (var i = 0; i < str.length(); i++) {
1810         var c = str.charAt(i);
1811         switch (c) {
1812            case '&' -> sb.append("&amp;");
1813            case '<' -> sb.append("&lt;");
1814            case '>' -> sb.append("&gt;");
1815            case '"' -> sb.append("&quot;");
1816            case '\'' -> sb.append("&apos;");
1817            default -> sb.append(c);
1818         }
1819      }
1820      return sb.toString();
1821   }
1822
1823   /**
1824    * Extracts all text segments between start and end markers.
1825    *
1826    * <p>
1827    * Finds all occurrences of text between the start and end markers (non-overlapping).
1828    *
1829    * <h4 class='section'>Example:</h4>
1830    * <p class='bjava'>
1831    *    extractBetween(<js>"&lt;tag&gt;content&lt;/tag&gt;"</js>, <js>"&lt;"</js>, <js>"&gt;"</js>);
1832    *    <jc>// ["tag", "/tag"]</jc>
1833    *    extractBetween(<js>"[one][two][three]"</js>, <js>"["</js>, <js>"]"</js>);
1834    *    <jc>// ["one", "two", "three"]</jc>
1835    * </p>
1836    *
1837    * @param str The string to extract from. Can be <jk>null</jk>.
1838    * @param start The start marker. Can be <jk>null</jk>.
1839    * @param end The end marker. Can be <jk>null</jk>.
1840    * @return A list of text segments found between the markers, or an empty list if any parameter is <jk>null</jk> or empty.
1841    */
1842   public static List<String> extractBetween(String str, String start, String end) {
1843      if (isEmpty(str) || isEmpty(start) || isEmpty(end))
1844         return Collections.emptyList();
1845
1846      var result = new ArrayList<String>();
1847      var startIndex = 0;
1848      while (true) {
1849         var startPos = str.indexOf(start, startIndex);
1850         if (startPos == -1)
1851            break;
1852         var endPos = str.indexOf(end, startPos + start.length());
1853         if (endPos == -1)
1854            break;
1855         result.add(str.substring(startPos + start.length(), endPos));
1856         startIndex = endPos + end.length();
1857      }
1858      return result;
1859   }
1860
1861   /**
1862    * Extracts all email addresses from a string.
1863    *
1864    * <p>
1865    * Uses a basic email regex pattern to find email addresses.
1866    *
1867    * <h4 class='section'>Example:</h4>
1868    * <p class='bjava'>
1869    *    extractEmails(<js>"Contact: user@example.com or admin@test.org"</js>);
1870    *    <jc>// ["user@example.com", "admin@test.org"]</jc>
1871    * </p>
1872    *
1873    * @param str The string to extract emails from. Can be <jk>null</jk>.
1874    * @return A list of email addresses found in the input, or an empty list if the string is <jk>null</jk> or empty.
1875    */
1876   public static List<String> extractEmails(String str) {
1877      if (isEmpty(str))
1878         return Collections.emptyList();
1879
1880      var result = new ArrayList<String>();
1881      // Email regex pattern (same as isEmail but without ^ and $ anchors)
1882      var pattern = Pattern.compile("[a-zA-Z0-9._%+-]+@(?:[a-zA-Z0-9-]+\\.)++[a-zA-Z]{2,}");
1883      var matcher = pattern.matcher(str);
1884      while (matcher.find()) {
1885         result.add(matcher.group());
1886      }
1887      return result;
1888   }
1889
1890   /**
1891    * Extracts all numeric sequences from a string.
1892    *
1893    * <p>
1894    * Finds all sequences of digits (including decimal numbers with dots).
1895    *
1896    * <h4 class='section'>Example:</h4>
1897    * <p class='bjava'>
1898    *    extractNumbers(<js>"Price: $19.99, Quantity: 5"</js>);
1899    *    <jc>// ["19.99", "5"]</jc>
1900    *    extractNumbers(<js>"Version 1.2.3"</js>);
1901    *    <jc>// ["1.2", "3"]</jc>
1902    * </p>
1903    *
1904    * @param str The string to extract numbers from. Can be <jk>null</jk>.
1905    * @return A list of numeric strings found in the input, or an empty list if the string is <jk>null</jk> or empty.
1906    */
1907   public static List<String> extractNumbers(String str) {
1908      if (isEmpty(str))
1909         return Collections.emptyList();
1910
1911      var result = new ArrayList<String>();
1912      var pattern = Pattern.compile("\\d+(?:\\.\\d+)?");
1913      var matcher = pattern.matcher(str);
1914      while (matcher.find()) {
1915         result.add(matcher.group());
1916      }
1917      return result;
1918   }
1919
1920   /**
1921    * Extracts all URLs from a string.
1922    *
1923    * <p>
1924    * Uses a basic URL regex pattern to find URLs (http, https, ftp).
1925    *
1926    * <h4 class='section'>Example:</h4>
1927    * <p class='bjava'>
1928    *    extractUrls(<js>"Visit https://example.com or http://test.org"</js>);
1929    *    <jc>// ["https://example.com", "http://test.org"]</jc>
1930    * </p>
1931    *
1932    * @param str The string to extract URLs from. Can be <jk>null</jk>.
1933    * @return A list of URLs found in the input, or an empty list if the string is <jk>null</jk> or empty.
1934    */
1935   public static List<String> extractUrls(String str) {
1936      if (isEmpty(str))
1937         return Collections.emptyList();
1938
1939      var result = new ArrayList<String>();
1940      // Basic URL pattern: protocol://domain/path
1941      var pattern = Pattern.compile("(?:https?|ftp)://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+", Pattern.CASE_INSENSITIVE);
1942      var matcher = pattern.matcher(str);
1943      while (matcher.find()) {
1944         result.add(matcher.group());
1945      }
1946      return result;
1947   }
1948
1949   /**
1950    * Extracts all words from a string.
1951    *
1952    * <p>
1953    * A word is defined as a sequence of letters, digits, and underscores.
1954    *
1955    * <h4 class='section'>Example:</h4>
1956    * <p class='bjava'>
1957    *    extractWords(<js>"Hello world! This is a test."</js>);
1958    *    <jc>// ["Hello", "world", "This", "is", "a", "test"]</jc>
1959    * </p>
1960    *
1961    * @param str The string to extract words from. Can be <jk>null</jk>.
1962    * @return A list of words found in the input, or an empty list if the string is <jk>null</jk> or empty.
1963    */
1964   public static List<String> extractWords(String str) {
1965      if (isEmpty(str))
1966         return Collections.emptyList();
1967
1968      var result = new ArrayList<String>();
1969      // Word pattern: sequence of word characters (letters, digits, underscore)
1970      var pattern = Pattern.compile("\\w+");
1971      var matcher = pattern.matcher(str);
1972      while (matcher.find()) {
1973         result.add(matcher.group());
1974      }
1975      return result;
1976   }
1977
1978   /**
1979    * Filters a string array using the specified predicate.
1980    *
1981    * <p>
1982    * Returns <jk>null</jk> if the array is <jk>null</jk>.
1983    * Returns an empty array if the predicate is <jk>null</jk> or no elements match.
1984    *
1985    * <h4 class='section'>Examples:</h4>
1986    * <p class='bjava'>
1987    *    String[] <jv>array</jv> = {<js>"foo"</js>, <js>""</js>, <js>"bar"</js>, <jk>null</jk>, <js>"baz"</js>};
1988    *    String[] <jv>filtered</jv> = filter(<jv>array</jv>, StringUtils.<jsf>NOT_EMPTY</jsf>);
1989    *    <jc>// Returns: ["foo", "bar", "baz"]</jc>
1990    *
1991    *    String[] <jv>longStrings</jv> = filter(<jv>array</jv>, s -&gt; s != <jk>null</jk> &amp;&amp; s.length() &gt; 3);
1992    *    <jc>// Returns: ["baz"]</jc>
1993    * </p>
1994    *
1995    * @param array The array to filter. Can be <jk>null</jk> (returns an empty array).
1996    * @param predicate The predicate to apply to each element. Can be <jk>null</jk> (returns an empty array).
1997    * @return A new array containing only the elements that match the predicate, or an empty array if the array was <jk>null</jk>.
1998    */
1999   public static String[] filter(String[] array, Predicate<String> predicate) {
2000      if (array == null)
2001         return new String[0];
2002      if (predicate == null)
2003         return new String[0];
2004      return Arrays.stream(array).filter(predicate).toArray(String[]::new);
2005   }
2006
2007   /**
2008    * Returns the first character in the specified string.
2009    *
2010    * <p>
2011    * This is a null-safe and bounds-safe operation. Returns <c>0</c> (null character) if:
2012    * <ul>
2013    *   <li>The string is <jk>null</jk></li>
2014    *   <li>The string is empty</li>
2015    * </ul>
2016    *
2017    * <h4 class='section'>Example:</h4>
2018    * <p class='bjava'>
2019    *    firstChar(<js>"Hello"</js>);     <jc>// 'H'</jc>
2020    *    firstChar(<js>"World"</js>);     <jc>// 'W'</jc>
2021    *    firstChar(<js>""</js>);          <jc>// 0 (empty string)</jc>
2022    *    firstChar(<jk>null</jk>);        <jc>// 0 (null string)</jc>
2023    * </p>
2024    *
2025    * @param s The string to check.
2026    * @return The first character in the string, or <c>0</c> if the string is <jk>null</jk> or empty.
2027    * @see #charAt(String, int)
2028    * @see #firstNonWhitespaceChar(String)
2029    */
2030   public static char firstChar(String s) {
2031      if (s == null || s.isEmpty())
2032         return 0;
2033      return s.charAt(0);
2034   }
2035
2036   /**
2037    * Returns the first non-blank string in the array.
2038    *
2039    * @param vals The strings to check.
2040    * @return The first non-blank string, or <jk>null</jk> if all values were blank or <jk>null</jk>.
2041    */
2042   public static String firstNonBlank(String...vals) {
2043      for (var v : vals) {
2044         if (isNotBlank(v))
2045            return v;
2046      }
2047      return null;
2048   }
2049
2050   /**
2051    * Returns the first non-null, non-empty string in the list.
2052    *
2053    * <p>
2054    * This method iterates through the provided strings and returns the first one that is not <jk>null</jk>
2055    * and not empty (as determined by {@link #isNotEmpty(CharSequence)}).
2056    *
2057    * <h4 class='section'>Example:</h4>
2058    * <p class='bjava'>
2059    *    firstNonEmpty(<jk>null</jk>, <js>""</js>, <js>"Hello"</js>, <js>"World"</js>);   <jc>// "Hello"</jc>
2060    *    firstNonEmpty(<js>"Hello"</js>, <js>"World"</js>);                              <jc>// "Hello"</jc>
2061    *    firstNonEmpty(<jk>null</jk>, <js>""</js>);                                      <jc>// null</jc>
2062    *    firstNonEmpty();                                                                <jc>// null</jc>
2063    * </p>
2064    *
2065    * @param s The strings to test.
2066    * @return The first non-empty string in the list, or <jk>null</jk> if they were all <jk>null</jk> or empty.
2067    * @see #firstNonBlank(String...)
2068    * @see #isNotEmpty(CharSequence)
2069    */
2070   public static String firstNonEmpty(String...s) {
2071      for (var ss : s)
2072         if (isNotEmpty(ss))
2073            return ss;
2074      return null;
2075   }
2076
2077   /**
2078    * Returns the first non-whitespace character in the string.
2079    *
2080    * <p>
2081    * This method scans the string from the beginning and returns the first character that is not
2082    * a whitespace character (as determined by {@link Character#isWhitespace(char)}).
2083    *
2084    * <h4 class='section'>Example:</h4>
2085    * <p class='bjava'>
2086    *    firstNonWhitespaceChar(<js>"Hello"</js>);          <jc>// 'H'</jc>
2087    *    firstNonWhitespaceChar(<js>"  Hello"</js>);        <jc>// 'H'</jc>
2088    *    firstNonWhitespaceChar(<js>"\t\nWorld"</js>);      <jc>// 'W'</jc>
2089    *    firstNonWhitespaceChar(<js>"   "</js>);            <jc>// 0 (only whitespace)</jc>
2090    *    firstNonWhitespaceChar(<jk>null</jk>);            <jc>// 0 (null string)</jc>
2091    * </p>
2092    *
2093    * @param s The string to check.
2094    * @return The first non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed of only whitespace.
2095    * @see #firstChar(String)
2096    * @see Character#isWhitespace(char)
2097    */
2098   public static char firstNonWhitespaceChar(String s) {
2099      if (isNotNull(s))
2100         for (var i = 0; i < s.length(); i++)
2101            if (! isWhitespace(s.charAt(i)))
2102               return s.charAt(i);
2103      return 0;
2104   }
2105
2106   /**
2107    * URL-encodes invalid characters in a URI string.
2108    *
2109    * <p>
2110    * This method escapes characters that are not valid in URIs by converting them to percent-encoded
2111    * format. Spaces are converted to <js>"+"</js> characters, and other invalid characters are
2112    * percent-encoded (e.g., <js>"hello world"</js> becomes <js>"hello+world"</js>).
2113    *
2114    * <p>
2115    * Only ASCII characters (0-127) that are not in the valid URI character set are encoded.
2116    * If the string contains no invalid characters, the original string is returned.
2117    *
2118    * <h4 class='section'>Example:</h4>
2119    * <p class='bjava'>
2120    *    fixUrl(<js>"hello world"</js>);              <jc>// "hello+world"</jc>
2121    *    fixUrl(<js>"file://path/to file.txt"</js>);  <jc>// "file://path/to+file.txt"</jc>
2122    *    fixUrl(<js>"valid-url"</js>);                <jc>// "valid-url" (no change)</jc>
2123    *    fixUrl(<jk>null</jk>);                       <jc>// null</jc>
2124    * </p>
2125    *
2126    * @param in The URI string to encode. Can be <jk>null</jk>.
2127    * @return The URI with invalid characters encoded, or <jk>null</jk> if input is <jk>null</jk>.
2128    * @see #urlEncode(String)
2129    */
2130   @SuppressWarnings({
2131      "null" // Null analysis false positive - method handles null input
2132   })
2133   public static String fixUrl(String in) {
2134
2135      if (in == null)
2136         return null;
2137
2138      StringBuilder sb = null;
2139
2140      var m = 0;
2141
2142      for (var i = 0; i < in.length(); i++) {
2143         var c = in.charAt(i);
2144         if (! URI_CHARS.contains(c)) {
2145            sb = append(sb, in.substring(m, i));
2146            if (c == ' ')
2147               sb.append("+");
2148            else
2149               sb.append('%').append(toHex2(c));
2150            m = i + 1;
2151         }
2152      }
2153      if (isNotNull(sb)) {
2154         sb.append(in.substring(m));
2155         return sb.toString();
2156      }
2157      return in;
2158
2159   }
2160
2161   /**
2162    * Formats a string using printf-style format specifiers.
2163    *
2164    * <p>
2165    * This method provides printf-style formatting (similar to C's <c>printf()</c> function and Java's
2166    * {@link String#format(String, Object...)}). As of 10.0.0 it is <b>printf-only</b>: <c>{</c> and
2167    * <c>'</c> are ordinary literal characters (no MessageFormat <c>{0}</c> placeholders, no single-quote
2168    * escaping). For MessageFormat-style (<js>"{0}"</js>) patterns — including externally-authored i18n
2169    * <c>.properties</c> strings — use {@link #mformat(String, Object...)} instead.
2170    *
2171    * <h4 class='section'>Format Support:</h4>
2172    * <ul>
2173    *   <li><b>Printf-style:</b> <js>"%s"</js>, <js>"%d"</js>, <js>"%.2f"</js>, <js>"%1$s"</js>, etc.</li>
2174    * </ul>
2175    *
2176    * <h4 class='section'>Printf Format Specifiers:</h4>
2177    * <ul>
2178    *   <li><b>%s</b> - String</li>
2179    *   <li><b>%d</b> - Decimal integer</li>
2180    *   <li><b>%f</b> - Floating point</li>
2181    *   <li><b>%x</b> - Hexadecimal (lowercase)</li>
2182    *   <li><b>%X</b> - Hexadecimal (uppercase)</li>
2183    *   <li><b>%o</b> - Octal</li>
2184    *   <li><b>%b</b> - Boolean</li>
2185    *   <li><b>%c</b> - Character</li>
2186    *   <li><b>%e</b> - Scientific notation (lowercase)</li>
2187    *   <li><b>%E</b> - Scientific notation (uppercase)</li>
2188    *   <li><b>%g</b> - General format (lowercase)</li>
2189    *   <li><b>%G</b> - General format (uppercase)</li>
2190    *   <li><b>%n</b> - Platform-specific line separator</li>
2191    *   <li><b>%%</b> - Literal percent sign</li>
2192    * </ul>
2193    *
2194    * <h4 class='section'>Format Specifier Syntax:</h4>
2195    * <p>
2196    * Printf format specifiers follow this pattern: <c>%[argument_index$][flags][width][.precision]conversion</c>
2197    * </p>
2198    *
2199    * <h4 class='section'>Examples:</h4>
2200    * <p class='bjava'>
2201    *    <jc>// Printf-style formatting</jc>
2202    *    format(<js>"Hello %s, you have %d items"</js>, <js>"John"</js>, 5);
2203    *    <jc>// Returns: "Hello John, you have 5 items"</jc>
2204    *
2205    *    <jc>// Floating point with precision</jc>
2206    *    format(<js>"Price: $%.2f"</js>, 19.99);
2207    *    <jc>// Returns: "Price: $19.99"</jc>
2208    *
2209    *    <jc>// Width and alignment (printf)</jc>
2210    *    format(<js>"Name: %-20s Age: %3d"</js>, <js>"John"</js>, 25);
2211    *    <jc>// Returns: "Name: John                 Age:  25"</jc>
2212    *
2213    *    <jc>// Hexadecimal (printf)</jc>
2214    *    format(<js>"Color: #%06X"</js>, 0xFF5733);
2215    *    <jc>// Returns: "Color: #FF5733"</jc>
2216    *
2217    *    <jc>// Argument index (reuse arguments)</jc>
2218    *    format(<js>"%1$s loves %2$s, and %1$s also loves %3$s"</js>, <js>"Alice"</js>, <js>"Bob"</js>, <js>"Charlie"</js>);
2219    *    <jc>// Returns: "Alice loves Bob, and Alice also loves Charlie"</jc>
2220    * </p>
2221    *
2222    * <h4 class='section'>Comparison with mformat():</h4>
2223    * <p class='bjava'>
2224    *    <jc>// Printf style (this method)</jc>
2225    *    format(<js>"Hello %s, you have %d items"</js>, <js>"John"</js>, 5);
2226    *
2227    *    <jc>// MessageFormat style</jc>
2228    *    mformat(<js>"Hello {0}, you have {1} items"</js>, <js>"John"</js>, 5);
2229    * </p>
2230    *
2231    * <h4 class='section'>Null Handling:</h4>
2232    * <p>
2233    * Null arguments are formatted as the string <js>"null"</js> for string conversions,
2234    * or cause a {@link NullPointerException} for numeric conversions (consistent with {@link String#format(String, Object...)}).
2235    * </p>
2236    *
2237    * @param pattern The printf-style format string.
2238    * @param args The arguments to format.
2239    * @return The formatted string.
2240    * @throws IllegalFormatException If the format string is invalid or arguments don't match the format specifiers.
2241    * @see String#format(String, Object...)
2242    * @see #mformat(String, Object...)
2243    * @see StringFormat for detailed format specification
2244    */
2245   public static String format(String pattern, Object...args) {
2246      return StringFormat.formatPrintf(pattern, args);
2247   }
2248
2249   /**
2250    * Formats a pattern string using {@link java.text.MessageFormat}-style placeholders only.
2251    *
2252    * <p>
2253    * This is the MessageFormat counterpart to {@link #format(String, Object...)}: it is intended for
2254    * patterns that use MessageFormat syntax (<js>"{0}"</js>, <js>"{1,number}"</js>, <js>"{2,date}"</js>,
2255    * <js>"''"</js>-quoting) — most importantly externally-authored patterns such as i18n
2256    * <c>.properties</c> resource-bundle strings. Use {@link #format(String, Object...)} for printf-style
2257    * (<js>"%s"</js>/<js>"%d"</js>) patterns.
2258    *
2259    * <p>
2260    * Rendering is delegated to {@link java.text.MessageFormat}. As a convenience, when no arguments are
2261    * supplied the pattern is returned verbatim (so <js>"''"</js>-quoting is left untouched for placeholder-free
2262    * strings).
2263    *
2264    * <h4 class='section'>Examples:</h4>
2265    * <p class='bjava'>
2266    *    mformat(<js>"Hello {0}, you have {1} items"</js>, <js>"John"</js>, 5);
2267    *    <jc>// Returns: "Hello John, you have 5 items"</jc>
2268    * </p>
2269    *
2270    * @param pattern The MessageFormat-style pattern.
2271    * @param args The arguments to format.
2272    * @return The formatted string.
2273    * @throws IllegalArgumentException If the pattern is <jk>null</jk> or contains invalid placeholders.
2274    * @see #format(String, Object...)
2275    * @see java.text.MessageFormat
2276    */
2277   public static String mformat(String pattern, Object...args) {
2278      reqnn("pattern", pattern);
2279      if (args.length == 0)
2280         return pattern;
2281      return MessageFormat.format(pattern, args);
2282   }
2283
2284   /**
2285    * Simple utility for replacing variables of the form <js>"{key}"</js> with values from a function.
2286    *
2287    * <p>
2288    * Supports named MessageFormat-style variables: <js>"{key}"</js> where <c>key</c> is resolved by the function.
2289    * For numbered MessageFormat placeholders <js>"{0}"</js>, use {@link #mformat(String, Object...)} instead.
2290    *
2291    * <p>
2292    * Variable values are converted to strings using {@link #readable(Object)} to ensure consistent,
2293    * readable formatting (e.g., byte arrays are converted to hex, collections are formatted without spaces).
2294    *
2295    * <p>
2296    * Nested variables are supported in both the input string and returned values.
2297    *
2298    * <p>
2299    * If the function returns <jk>null</jk> for a key, the variable is not replaced.
2300    *
2301    * <p>
2302    * <jk>null</jk> values returned by the function are treated as blank strings.
2303    *
2304    * @param s The string containing variables to replace.  Can be <jk>null</jk> (returns <jk>null</jk>).
2305    * @param resolver The function that resolves variable names to values.  Can be <jk>null</jk> (the string is returned unchanged).
2306    * @return The new string with variables replaced, or the original string if it didn't have variables in it.
2307    */
2308   public static String formatNamed(String s, Function<String,Object> resolver) {
2309
2310      if (s == null)
2311         return null;
2312
2313      if (resolver == null || s.indexOf('{') == -1)
2314         return s;
2315
2316      // S1: Not in variable, looking for '{'
2317      // S2: Found '{', Looking for '}'
2318
2319      var state = S1;
2320      var hasInternalVar = false;
2321      var x = 0;
2322      var depth = 0;
2323      var length = s.length();
2324      var out = new StringBuilder();
2325
2326      for (var i = 0; i < length; i++) {
2327         var c = s.charAt(i);
2328         if (state == S1) {
2329            if (c == '{') {
2330               state = S2;
2331               x = i;
2332            } else {
2333               out.append(c);
2334            }
2335         } else /* state == S2 */ {
2336            if (c == '{') {
2337               depth++;
2338               hasInternalVar = true;
2339            } else if (c == '}') {
2340               if (depth > 0) {
2341                  depth--;
2342               } else {
2343                  var key = s.substring(x + 1, i);
2344                  key = (hasInternalVar ? formatNamed(key, resolver) : key);
2345                  hasInternalVar = false;
2346                  var val = resolver.apply(key);
2347                  if (val == null)
2348                     out.append('{').append(key).append('}');
2349                  else {
2350                     var v = readable(val);
2351                     // If the replacement also contains variables, replace them now.
2352                     if (v.indexOf('{') != -1)
2353                        v = formatNamed(v, resolver);
2354                     out.append(v);
2355                  }
2356                  state = S1;
2357               }
2358            }
2359         }
2360      }
2361      return out.toString();
2362   }
2363
2364   /**
2365    * Simple utility for replacing variables of the form <js>"{key}"</js> with values in the specified map.
2366    *
2367    * <p>
2368    * Supports named MessageFormat-style variables: <js>"{key}"</js> where <c>key</c> is a map key.
2369    * For numbered MessageFormat placeholders <js>"{0}"</js>, use {@link #mformat(String, Object...)} instead.
2370    *
2371    * <p>
2372    * Variable values are converted to strings using {@link #readable(Object)} to ensure consistent,
2373    * readable formatting (e.g., byte arrays are converted to hex, collections are formatted without spaces).
2374    *
2375    * <p>
2376    * Nested variables are supported in both the input string and map values.
2377    *
2378    * <p>
2379    * If the map does not contain the specified value, the variable is not replaced.
2380    *
2381    * <p>
2382    * <jk>null</jk> values in the map are treated as blank strings.
2383    *
2384    * @param s The string containing variables to replace.  Can be <jk>null</jk> (returns <jk>null</jk>).
2385    * @param m The map containing the variable values.  Can be <jk>null</jk> (the string is returned unchanged).
2386    * @return The new string with variables replaced, or the original string if it didn't have variables in it.
2387    */
2388   public static String formatNamed(String s, Map<String,Object> m) {
2389      if (s == null)
2390         return null;
2391
2392      if (m == null || m.isEmpty())
2393         return s;
2394
2395      // Delegate to Function-based version
2396      return formatNamed(s, key -> {
2397         // JUNEAU-248: Check if key exists in map by attempting to get it
2398         // For regular maps: use containsKey() OR nn(get()) check
2399         // For BeanMaps: get() returns non-null for accessible properties (including hidden ones)
2400         var val = m.get(key);
2401         // Check if key actually exists: either containsKey is true, or val is non-null
2402         // This handles both regular maps and BeanMaps correctly
2403         var keyExists = m.containsKey(key) || isNotNull(val);
2404         return keyExists ? val : null;
2405      });
2406   }
2407
2408   /**
2409    * Converts a hexadecimal character string to a byte array.
2410    *
2411    * @param hex The string to convert to a byte array.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
2412    * @return A new byte array.
2413    */
2414   public static byte[] fromHex(String hex) {
2415      var buff = ByteBuffer.allocate(hex.length() / 2);
2416      for (var i = 0; i < hex.length(); i += 2)
2417         buff.put((byte)Integer.parseInt(hex.substring(i, i + 2), 16));
2418      buff.rewind();
2419      return buff.array();
2420   }
2421
2422   /**
2423    * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 encoded string.
2424    *
2425    * @param hex The hexadecimal string.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
2426    * @return The UTF-8 string.
2427    */
2428   public static String fromHexToUTF8(String hex) {
2429      var buff = ByteBuffer.allocate(hex.length() / 2);
2430      for (var i = 0; i < hex.length(); i += 2)
2431         buff.put((byte)Integer.parseInt(hex.substring(i, i + 2), 16));
2432      buff.rewind();  // Fixes Java 11 issue.
2433      return UTF_8.decode(buff).toString();
2434   }
2435
2436   /**
2437    * Same as {@link #fromHex(String)} except expects spaces between the byte strings.
2438    *
2439    * @param hex The string to convert to a byte array.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
2440    * @return A new byte array.
2441    */
2442   public static byte[] fromSpacedHex(String hex) {
2443      var buff = ByteBuffer.allocate((hex.length() + 1) / 3);
2444      for (var i = 0; i < hex.length(); i += 3)
2445         buff.put((byte)Integer.parseInt(hex.substring(i, i + 2), 16));
2446      buff.rewind();
2447      return buff.array();
2448   }
2449
2450   /**
2451    * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 BC") into a UTF-8 encoded string.
2452    *
2453    * @param hex The hexadecimal string.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
2454    * @return The UTF-8 string.
2455    */
2456   public static String fromSpacedHexToUTF8(String hex) {
2457      var buff = ByteBuffer.allocate((hex.length() + 1) / 3);
2458      for (var i = 0; i < hex.length(); i += 3)
2459         buff.put((byte)Integer.parseInt(hex.substring(i, i + 2), 16));
2460      buff.rewind();  // Fixes Java 11 issue.
2461      return UTF_8.decode(buff).toString();
2462   }
2463
2464   /**
2465    * Generates a random UUID string in standard format.
2466    *
2467    * <p>
2468    * Returns a UUID in the format: <c>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</c>
2469    *
2470    * <h4 class='section'>Example:</h4>
2471    * <p class='bjava'>
2472    *    generateUUID();  <jc>// "550e8400-e29b-41d4-a716-446655440000"</jc>
2473    * </p>
2474    *
2475    * @return A new random UUID string.
2476    */
2477   public static String generateUUID() {
2478      return UUID.randomUUID().toString();
2479   }
2480
2481   /**
2482    * Given an absolute URI, returns just the authority portion (e.g. <js>"http://hostname:port"</js>)
2483    *
2484    * @param s The URI string.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
2485    * @return Just the authority portion of the URI.
2486    */
2487   public static String getAuthorityUri(String s) {
2488
2489      // Use a state machine for maximum performance.
2490
2491      // S1: Looking for http
2492      // S2: Found http, looking for :
2493      // S3: Found :, looking for /
2494      // S4: Found /, looking for /
2495      // S5: Found /, looking for x
2496      // S6: Found x, looking for /
2497
2498      var state = S1;
2499
2500      for (var i = 0; i < s.length(); i++) {
2501         var c = s.charAt(i);
2502         if (state == S1) {
2503            if (isLowerCaseLetter(c))
2504               state = S2;
2505            else
2506               return s;
2507         } else if (state == S2) {
2508            if (c == ':')
2509               state = S3;
2510            else if (! isLowerCaseLetter(c))
2511               return s;
2512            // else: continue in S2 (lowercase letter)
2513         } else if (state == S3) {  // NOSONAR - State check necessary for state machine
2514            if (c == '/')
2515               state = S4;
2516            else
2517               return s;
2518         } else if (state == S4) {
2519            if (c == '/')
2520               state = S5;
2521            else
2522               return s;
2523         } else if (state == S5) {
2524            if (c != '/')
2525               state = S6;
2526            else
2527               return s;
2528         } else /* state == S6 */ {
2529            if (c == '/')
2530               return s.substring(0, i);
2531         }
2532      }
2533
2534      return s;
2535   }
2536
2537   /**
2538    * Parses a duration string.
2539    *
2540    * <p>
2541    * Supports simple duration formats:
2542    *
2543    * <h4 class='section'>Format Examples:</h4>
2544    * <ul>
2545    *    <li><js>"1000"</js> - 1000 milliseconds (no suffix)
2546    *    <li><js>"10s"</js> - 10 seconds
2547    *    <li><js>"10 sec"</js> - 10 seconds
2548    *    <li><js>"10 seconds"</js> - 10 seconds
2549    *    <li><js>"1.5h"</js> - 1.5 hours (5400000 ms)
2550    *    <li><js>"1h30m"</js> - 1 hour 30 minutes (5400000 ms)
2551    *    <li><js>"1h 30m"</js> - 1 hour 30 minutes (with spaces)
2552    * </ul>
2553    *
2554    * <h4 class='section'>Supported Units:</h4>
2555    * <ul>
2556    *    <li><b>Milliseconds:</b> <js>"ms"</js>, <js>"millis"</js>, <js>"milliseconds"</js> (or no suffix)
2557    *    <li><b>Seconds:</b> <js>"s"</js>, <js>"sec"</js>, <js>"second"</js>, <js>"seconds"</js>
2558    *    <li><b>Minutes:</b> <js>"m"</js>, <js>"min"</js>, <js>"minute"</js>, <js>"minutes"</js>
2559    *    <li><b>Hours:</b> <js>"h"</js>, <js>"hour"</js>, <js>"hours"</js>
2560    *    <li><b>Days:</b> <js>"d"</js>, <js>"day"</js>, <js>"days"</js>
2561    *    <li><b>Weeks:</b> <js>"w"</js>, <js>"week"</js>, <js>"weeks"</js>
2562    *    <li><b>Months:</b> <js>"mo"</js>, <js>"month"</js>, <js>"months"</js> (30 days)
2563    *    <li><b>Years:</b> <js>"y"</js>, <js>"yr"</js>, <js>"year"</js>, <js>"years"</js> (365 days)
2564    * </ul>
2565    *
2566    * <p>
2567    * Suffixes are case-insensitive.
2568    * <br>Whitespace is ignored.
2569    * <br>Decimal values are supported (e.g., <js>"1.5h"</js>).
2570    * <br>Combined formats are supported (e.g., <js>"1h30m"</js>).
2571    *
2572    * @param s The string to parse.
2573    * @return
2574    *    The time in milliseconds, or <c>-1</c> if the string is empty or <jk>null</jk>.
2575    */
2576   public static long getDuration(String s) {
2577      s = trim(s);
2578      if (isEmpty(s))
2579         return -1;
2580
2581      // Parse simple format (number + unit or combined format)
2582      var totalMs = 0L;
2583      var i = 0;
2584      var len = s.length();
2585
2586      while (i < len) {
2587         // Skip whitespace
2588         while (i < len && Character.isWhitespace(s.charAt(i)))
2589            i++;
2590
2591         // Parse number (including decimal)
2592         var numStart = i;
2593         var hasDecimal = false;
2594         while (i < len) {
2595         var c = s.charAt(i);
2596            if (c >= '0' && c <= '9') {
2597               i++;
2598            } else if (c == '.' && !hasDecimal) {
2599               hasDecimal = true;
2600               i++;
2601            } else {
2602            break;
2603      }
2604         }
2605
2606         if (i == numStart) {
2607            // No number found, invalid format
2608            return -1;
2609         }
2610
2611         var numStr = s.substring(numStart, i).trim();
2612         var value = Double.parseDouble(numStr);
2613
2614         // Skip whitespace
2615         while (i < len && Character.isWhitespace(s.charAt(i)))
2616            i++;
2617
2618      // Parse unit (read all letters until we hit a digit or whitespace)
2619      var unitStart = i;
2620      while (i < len && LETTER.contains(s.charAt(i)))
2621         i++;
2622      var unit = s.substring(unitStart, i).trim().toLowerCase(Locale.ROOT);
2623
2624         // Convert to milliseconds
2625         var ms = parseUnit(unit, value);
2626         if (ms < 0)
2627            return -1;
2628         totalMs += ms;
2629      }
2630
2631      return totalMs;
2632   }
2633
2634   /**
2635    * Converts a string containing glob-style wildcard characters to a regular expression {@link Pattern}.
2636    *
2637    * <p>This method converts glob-style patterns to regular expressions with the following mappings:
2638    * <ul>
2639    *   <li>{@code *} matches any sequence of characters (including none)</li>
2640    *   <li>{@code ?} matches exactly one character</li>
2641    *   <li>All other characters are treated literally</li>
2642    * </ul>
2643    *
2644    * <h4 class='section'>Example:</h4>
2645    * <p class='bjava'>
2646    *   <jk>var</jk> <jv>pattern</jv> = <jsm>getGlobMatchPattern</jsm>(<js>"user_*_temp"</js>);
2647    *   <jk>boolean</jk> <jv>matches</jv> = <jv>pattern</jv>.matcher(<js>"user_alice_temp"</js>).matches();  <jc>// true</jc>
2648    *   <jv>matches</jv> = <jv>pattern</jv>.matcher(<js>"user_bob_temp"</js>).matches();    <jc>// true</jc>
2649    *   <jv>matches</jv> = <jv>pattern</jv>.matcher(<js>"admin_alice_temp"</js>).matches(); <jc>// false</jc>
2650    * </p>
2651    *
2652    * @param s The glob-style wildcard pattern string.
2653    * @return A compiled {@link Pattern} object, or <jk>null</jk> if the input string is <jk>null</jk>.
2654    */
2655   public static Pattern getGlobMatchPattern(String s) {
2656      return getGlobMatchPattern(s, 0);
2657   }
2658
2659   /**
2660    * Converts a string containing glob-style wildcard characters to a regular expression {@link Pattern} with flags.
2661    *
2662    * <p>This method converts glob-style patterns to regular expressions with the following mappings:
2663    * <ul>
2664    *   <li>{@code *} matches any sequence of characters (including none)</li>
2665    *   <li>{@code ?} matches exactly one character</li>
2666    *   <li>All other characters are treated literally</li>
2667    * </ul>
2668    *
2669    * <h4 class='section'>Example:</h4>
2670    * <p class='bjava'>
2671    *   <jc>// Case-insensitive matching</jc>
2672    *   <jk>var</jk> <jv>pattern</jv> = <jsm>getGlobMatchPattern</jsm>(<js>"USER_*"</js>, Pattern.<jsf>CASE_INSENSITIVE</jsf>);
2673    *   <jk>boolean</jk> <jv>matches</jv> = <jv>pattern</jv>.matcher(<js>"user_alice"</js>).matches();  <jc>// true</jc>
2674    * </p>
2675    *
2676    * @param s The glob-style wildcard pattern string.
2677    * @param flags Regular expression flags (see {@link Pattern} constants).
2678    * @return A compiled {@link Pattern} object, or <jk>null</jk> if the input string is <jk>null</jk>.
2679    */
2680   public static Pattern getGlobMatchPattern(String s, int flags) {
2681      if (s == null)
2682         return null;
2683      var sb = new StringBuilder();
2684      sb.append("\\Q");
2685      for (var i = 0; i < s.length(); i++) {
2686         var c = s.charAt(i);
2687         if (c == '*')
2688            sb.append("\\E").append(".*").append("\\Q");
2689         else if (c == '?')
2690            sb.append("\\E").append(".").append("\\Q");
2691         else
2692            sb.append(c);
2693      }
2694      sb.append("\\E");
2695      return Pattern.compile(sb.toString(), flags);
2696   }
2697
2698   /**
2699    * Converts a string containing <js>"*"</js> meta characters with a regular expression pattern.
2700    *
2701    * @param s The string to create a pattern from.  Can be <jk>null</jk> (returns <jk>null</jk>).
2702    * @return A regular expression pattern, or <jk>null</jk> if the input string is <jk>null</jk>.
2703    */
2704   public static Pattern getMatchPattern(String s) {
2705      return getMatchPattern(s, 0);
2706   }
2707
2708   /**
2709    * Converts a string containing <js>"*"</js> meta characters with a regular expression pattern.
2710    *
2711    * @param s The string to create a pattern from.  Can be <jk>null</jk> (returns <jk>null</jk>).
2712    * @param flags Regular expression flags.
2713    * @return A regular expression pattern, or <jk>null</jk> if the input string is <jk>null</jk>.
2714    */
2715   public static Pattern getMatchPattern(String s, int flags) {
2716      return getGlobMatchPattern(s, flags);
2717   }
2718
2719   /**
2720    * Takes in a string, splits it by lines, and then prepends each line with line numbers.
2721    *
2722    * @param s The string.  Can be <jk>null</jk> (returns <jk>null</jk>).
2723    * @return The string with line numbers added, or <jk>null</jk> if the input was <jk>null</jk>.
2724    */
2725   public static String getNumberedLines(String s) {
2726      return getNumberedLines(s, 1, Integer.MAX_VALUE);
2727   }
2728
2729   /**
2730    * Same as {@link #getNumberedLines(String)} except only returns the specified lines.
2731    *
2732    * <p>
2733    * Out-of-bounds values are allowed and fixed.
2734    *
2735    * @param s The string.  Can be <jk>null</jk> (returns <jk>null</jk>).
2736    * @param start The starting line (1-indexed).
2737    * @param end The ending line (1-indexed).
2738    * @return The string with line numbers added, or <jk>null</jk> if the input was <jk>null</jk>.
2739    */
2740   public static String getNumberedLines(String s, int start, int end) {
2741      if (s == null)
2742         return null;
2743      var lines = s.split("[\r\n]+");
2744      var digits = String.valueOf(lines.length).length();
2745      if (start < 1)
2746         start = 1;
2747      if (end < 0)
2748         end = Integer.MAX_VALUE;
2749      if (end > lines.length)
2750         end = lines.length;
2751      var sb = new StringBuilder();
2752      var format = String.format(Locale.ROOT, "%%0%dd", digits);
2753      for (var l : fixedSizeList(lines).subList(start - 1, end))
2754         sb.append(String.format(Locale.ROOT, format, start++)).append(": ").append(l).append("\n");
2755      return sb.toString();
2756   }
2757
2758   /**
2759    * Calculates the approximate memory size of a string in bytes.
2760    *
2761    * <p>
2762    * Returns <c>0</c> if the input string is <jk>null</jk>.
2763    * This method provides an estimate based on typical JVM object layout:
2764    * <ul>
2765    *   <li>String object overhead: ~24 bytes (object header + fields)</li>
2766    *   <li>char[] array overhead: ~16 bytes (array header)</li>
2767    *   <li>Character data: 2 bytes per character</li>
2768    * </ul>
2769    *
2770    * <p>
2771    * <b>Note:</b> Actual memory usage may vary based on JVM implementation, object alignment,
2772    * and whether compressed OOPs are enabled. This is an approximation for informational purposes.
2773    *
2774    * <h4 class='section'>Examples:</h4>
2775    * <p class='bjava'>
2776    *    getStringSize(<jk>null</jk>);        <jc>// Returns: 0</jc>
2777    *    getStringSize(<js>""</js>);          <jc>// Returns: ~40 bytes</jc>
2778    *    getStringSize(<js>"hello"</js>);     <jc>// Returns: ~50 bytes (40 + 10)</jc>
2779    *    getStringSize(<js>"test"</js>);      <jc>// Returns: ~48 bytes (40 + 8)</jc>
2780    * </p>
2781    *
2782    * @param str The string to measure. Can be <jk>null</jk>.
2783    * @return The approximate memory size in bytes, or <c>0</c> if the input was <jk>null</jk>.
2784    */
2785   public static long getStringSize(String str) {
2786      if (str == null)
2787         return 0;
2788      // String object overhead: ~24 bytes (object header + fields: value, hash, coder)
2789      // char[] array overhead: ~16 bytes (array header)
2790      // Character data: 2 bytes per character
2791      return 24L + 16L + (2L * str.length());
2792   }
2793
2794   /**
2795    * Checks if a string has text (not null, not empty, and contains at least one non-whitespace character).
2796    *
2797    * <h4 class='section'>Example:</h4>
2798    * <p class='bjava'>
2799    *    hasText(<jk>null</jk>);       <jc>// false</jc>
2800    *    hasText(<js>""</js>);         <jc>// false</jc>
2801    *    hasText(<js>"   "</js>);      <jc>// false</jc>
2802    *    hasText(<js>"hello"</js>);    <jc>// true</jc>
2803    * </p>
2804    *
2805    * @param str The string to check.
2806    * @return <jk>true</jk> if the string is not null, not empty, and contains at least one non-whitespace character.
2807    */
2808   public static boolean hasText(String str) {
2809      return isNotBlank(str);
2810   }
2811
2812   /**
2813    * Same as {@link String#indexOf(int)} except allows you to check for multiple characters.
2814    *
2815    * @param s The string to check.  Can be <jk>null</jk> (returns <c>-1</c>).
2816    * @param c The characters to check for.
2817    * @return The index into the string that is one of the specified characters, or <c>-1</c> if none found or the string is <jk>null</jk>.
2818    */
2819   public static int indexOf(String s, char...c) {
2820      if (s == null)
2821         return -1;
2822      for (var i = 0; i < s.length(); i++) {
2823         var c2 = s.charAt(i);
2824         for (var cc : c)
2825            if (c2 == cc)
2826               return i;
2827      }
2828      return -1;
2829   }
2830
2831   /**
2832    * Finds the index of the first occurrence of a substring within a string.
2833    *
2834    * <h4 class='section'>Example:</h4>
2835    * <p class='bjava'>
2836    *    indexOf(<js>"hello world"</js>, <js>"world"</js>);     <jc>// 6</jc>
2837    *    indexOf(<js>"hello world"</js>, <js>"xyz"</js>);       <jc>// -1</jc>
2838    *    indexOf(<jk>null</jk>, <js>"test"</js>);               <jc>// -1</jc>
2839    * </p>
2840    *
2841    * @param str The string to search in.
2842    * @param search The substring to search for.
2843    * @return The index of the first occurrence, or <c>-1</c> if not found or if either parameter is <jk>null</jk>.
2844    */
2845   public static int indexOf(String str, String search) {
2846      if (str == null || search == null)
2847         return -1;
2848      return str.indexOf(search);
2849   }
2850
2851   /**
2852    * Finds the index of the first occurrence of a substring within a string, ignoring case.
2853    *
2854    * <h4 class='section'>Example:</h4>
2855    * <p class='bjava'>
2856    *    indexOfIgnoreCase(<js>"Hello World"</js>, <js>"world"</js>);     <jc>// 6</jc>
2857    *    indexOfIgnoreCase(<js>"Hello World"</js>, <js>"WORLD"</js>);     <jc>// 6</jc>
2858    *    indexOfIgnoreCase(<js>"hello world"</js>, <js>"xyz"</js>);       <jc>// -1</jc>
2859    * </p>
2860    *
2861    * @param str The string to search in.
2862    * @param search The substring to search for.
2863    * @return The index of the first occurrence, or <c>-1</c> if not found or if either parameter is <jk>null</jk>.
2864    */
2865   public static int indexOfIgnoreCase(String str, String search) {
2866      if (str == null || search == null)
2867         return -1;
2868      return str.toLowerCase(Locale.ROOT).indexOf(search.toLowerCase(Locale.ROOT));
2869   }
2870
2871   /**
2872    * Interns a string, returning the canonical representation.
2873    *
2874    * <p>
2875    * Returns <jk>null</jk> if the input string is <jk>null</jk>.
2876    * This method provides a null-safe wrapper around {@link String#intern()}.
2877    *
2878    * <h4 class='section'>Examples:</h4>
2879    * <p class='bjava'>
2880    *    String <jv>s1</jv> = <jk>new</jk> String(<js>"test"</js>);
2881    *    String <jv>s2</jv> = <jk>new</jk> String(<js>"test"</js>);
2882    *    assertTrue(<jv>s1</jv> != <jv>s2</jv>);  <jc>// Different objects</jc>
2883    *
2884    *    String <jv>i1</jv> = intern(<jv>s1</jv>);
2885    *    String <jv>i2</jv> = intern(<jv>s2</jv>);
2886    *    assertTrue(<jv>i1</jv> == <jv>i2</jv>);  <jc>// Same interned object</jc>
2887    * </p>
2888    *
2889    * <h4 class='section'>Performance Note:</h4>
2890    * <p>String interning stores strings in a special pool, which can save memory when the same string
2891    * values are used repeatedly. However, the intern pool has limited size and interning can be slow,
2892    * so use judiciously for strings that are known to be repeated frequently.</p>
2893    *
2894    * @param str The string to intern. Can be <jk>null</jk>.
2895    * @return The interned string, or <jk>null</jk> if the input was <jk>null</jk>.
2896    */
2897   public static String intern(String str) {
2898      return str == null ? null : str.intern();
2899   }
2900
2901   /**
2902    * Interpolates variables in a template string using <js>"${name}"</js> syntax.
2903    *
2904    * <p>
2905    * Replaces variables of the form <js>"${name}"</js> with values from the map.
2906    * This is similar to shell variable interpolation syntax.
2907    *
2908    * <h4 class='section'>Example:</h4>
2909    * <p class='bjava'>
2910    *    var vars = Map.of(<js>"name"</js>, <js>"John"</js>, <js>"city"</js>, <js>"New York"</js>);
2911    *    interpolate(<js>"Hello ${name}, welcome to ${city}"</js>, vars);
2912    *    <jc>// Returns: "Hello John, welcome to New York"</jc>
2913    * </p>
2914    *
2915    * @param template The template string with <js>"${name}"</js> variables.  Can be <jk>null</jk> (returns <jk>null</jk>).
2916    * @param variables The map containing the variable values.  Can be <jk>null</jk> (the template is returned unchanged).
2917    * @return The interpolated string with variables replaced, or the original template if variables is null or empty.
2918    */
2919   public static String interpolate(String template, Map<String,Object> variables) {
2920      if (template == null)
2921         return null;
2922      if (variables == null || variables.isEmpty())
2923         return template;
2924
2925      var result = new StringBuilder();
2926      var i = 0;
2927      var length = template.length();
2928
2929      while (i < length) {
2930         var dollarIndex = template.indexOf("${", i);
2931         if (dollarIndex == -1) {
2932            // No more variables, append the rest
2933            result.append(template.substring(i));
2934            break;
2935         }
2936
2937         // Append text before the variable
2938         result.append(template.substring(i, dollarIndex));
2939
2940         // Find the closing brace
2941         var braceIndex = template.indexOf('}', dollarIndex + 2);
2942         if (braceIndex == -1) {
2943            // No closing brace, append the rest as-is
2944            result.append(template.substring(dollarIndex));
2945            break;
2946         }
2947
2948         // Extract variable name
2949         var varName = template.substring(dollarIndex + 2, braceIndex);
2950         var value = variables.get(varName);
2951
2952         if (variables.containsKey(varName)) {
2953            // Variable exists in map (even if null)
2954            result.append(value != null ? value.toString() : "null");
2955         } else {
2956            // Variable not found, keep the original placeholder
2957            result.append("${").append(varName).append("}");
2958         }
2959
2960         i = braceIndex + 1;
2961      }
2962
2963      return result.toString();
2964   }
2965
2966   /**
2967    * Efficiently determines whether a URL is of the pattern "xxx://xxx"
2968    *
2969    * @param s The string to test.  Can be <jk>null</jk> (returns <jk>false</jk>).
2970    * @return <jk>true</jk> if it's an absolute path.
2971    */
2972   public static boolean isAbsoluteUri(String s) {
2973
2974      if (isEmpty(s))
2975         return false;
2976
2977      // Use a state machine for maximum performance.
2978
2979      // S1: Looking for http
2980      // S2: Found http, looking for :
2981      // S3: Found :, looking for /
2982      // S4: Found /, looking for /
2983      // S5: Found /, looking for x
2984
2985      var state = S1;
2986
2987      for (var i = 0; i < s.length(); i++) {
2988         var c = s.charAt(i);
2989         if (state == S1) {
2990            if (isLowerCaseLetter(c))
2991               state = S2;
2992            else
2993               return false;
2994         } else if (state == S2) {
2995            if (c == ':')
2996               state = S3;
2997            else if (! isLowerCaseLetter(c))
2998               return false;
2999         } else if (state == S3) {  // NOSONAR - State check necessary for state machine
3000            if (c == '/')
3001               state = S4;
3002            else
3003               return false;
3004         } else if (state == S4) {
3005            if (c == '/')
3006               state = S5;
3007            else
3008               return false;
3009         } else /* state == S5 */ {
3010            return true;
3011         }
3012      }
3013      return false;
3014   }
3015
3016   /**
3017    * Checks if all of the provided strings are not blank (not null, not empty, and not whitespace only).
3018    *
3019    * <p>
3020    * Returns <jk>true</jk> only if all strings are not null, not empty, and contain non-whitespace characters.
3021    * Returns <jk>false</jk> if the array is null or empty, or if any string is null, empty, or whitespace only.
3022    *
3023    * <h4 class='section'>Example:</h4>
3024    * <p class='bjava'>
3025    *    isAllNotBlank();                                     <jc>// false</jc>
3026    *    isAllNotBlank(<jk>null</jk>);                        <jc>// false</jc>
3027    *    isAllNotBlank(<jk>null</jk>, <jk>null</jk>);         <jc>// false</jc>
3028    *    isAllNotBlank(<js>""</js>, <js>""</js>);             <jc>// false</jc>
3029    *    isAllNotBlank(<js>"   "</js>, <js>"   "</js>);       <jc>// false</jc>
3030    *    isAllNotBlank(<jk>null</jk>, <js>"hello"</js>);      <jc>// false</jc>
3031    *    isAllNotBlank(<js>""</js>, <js>"   "</js>);          <jc>// false</jc>
3032    *    isAllNotBlank(<js>"hello"</js>, <js>"   "</js>);     <jc>// false</jc>
3033    *    isAllNotBlank(<js>"hello"</js>);                     <jc>// true</jc>
3034    *    isAllNotBlank(<js>"hello"</js>, <js>"world"</js>);   <jc>// true</jc>
3035    * </p>
3036    *
3037    * @param values The strings to check.
3038    * @return <jk>true</jk> if all strings are not null, not empty, and not whitespace only, <jk>false</jk> otherwise.
3039    */
3040   public static boolean isAllNotBlank(CharSequence...values) {
3041      if (values == null || values.length == 0)
3042         return false;
3043      for (CharSequence value : values)
3044         if (! isNotBlank(value))
3045            return false;
3046      return true;
3047   }
3048
3049   /**
3050    * Checks if all of the provided strings are not empty (not null and not zero-length).
3051    *
3052    * <p>
3053    * Returns <jk>true</jk> only if all strings are not null and have a length greater than zero.
3054    * Returns <jk>false</jk> if the array is null or empty, or if any string is null or empty.
3055    *
3056    * <h4 class='section'>Example:</h4>
3057    * <p class='bjava'>
3058    *    isAllNotEmpty();                                     <jc>// false</jc>
3059    *    isAllNotEmpty(<jk>null</jk>);                        <jc>// false</jc>
3060    *    isAllNotEmpty(<jk>null</jk>, <jk>null</jk>);         <jc>// false</jc>
3061    *    isAllNotEmpty(<js>""</js>, <js>""</js>);             <jc>// false</jc>
3062    *    isAllNotEmpty(<jk>null</jk>, <js>"hello"</js>);      <jc>// false</jc>
3063    *    isAllNotEmpty(<js>""</js>, <js>"   "</js>);          <jc>// false</jc>
3064    *    isAllNotEmpty(<js>"hello"</js>);                     <jc>// true</jc>
3065    *    isAllNotEmpty(<js>"hello"</js>, <js>"world"</js>);   <jc>// true</jc>
3066    *    isAllNotEmpty(<js>"hello"</js>, <js>"   "</js>);     <jc>// true</jc>
3067    * </p>
3068    *
3069    * @param values The strings to check.
3070    * @return <jk>true</jk> if all strings are not null and not empty, <jk>false</jk> otherwise.
3071    */
3072   public static boolean isAllNotEmpty(CharSequence...values) {
3073      if (values == null || values.length == 0)
3074         return false;
3075      for (CharSequence value : values)
3076         if (value == null || value.isEmpty())
3077            return false;
3078      return true;
3079   }
3080
3081   /**
3082    * Checks if a string contains only alphabetic characters (a-z, A-Z).
3083    *
3084    * <h4 class='section'>Example:</h4>
3085    * <p class='bjava'>
3086    *    isAlpha(<jk>null</jk>);         <jc>// false</jc>
3087    *    isAlpha(<js>""</js>);           <jc>// false</jc>
3088    *    isAlpha(<js>"abc"</js>);        <jc>// true</jc>
3089    *    isAlpha(<js>"abc123"</js>);     <jc>// false</jc>
3090    *    isAlpha(<js>"abc def"</js>);    <jc>// false</jc>
3091    * </p>
3092    *
3093    * @param str The string to check.
3094    * @return <jk>true</jk> if the string is not null, not empty, and contains only alphabetic characters.
3095    */
3096   public static boolean isAlpha(String str) {
3097      if (isEmpty(str))
3098         return false;
3099      for (var i = 0; i < str.length(); i++) {
3100         if (! LETTER.contains(str.charAt(i)))
3101            return false;
3102      }
3103      return true;
3104   }
3105
3106   /**
3107    * Checks if a string contains only alphanumeric characters (a-z, A-Z, 0-9).
3108    *
3109    * <h4 class='section'>Example:</h4>
3110    * <p class='bjava'>
3111    *    isAlphaNumeric(<jk>null</jk>);         <jc>// false</jc>
3112    *    isAlphaNumeric(<js>""</js>);           <jc>// false</jc>
3113    *    isAlphaNumeric(<js>"abc"</js>);        <jc>// true</jc>
3114    *    isAlphaNumeric(<js>"abc123"</js>);     <jc>// true</jc>
3115    *    isAlphaNumeric(<js>"abc def"</js>);    <jc>// false</jc>
3116    *    isAlphaNumeric(<js>"abc-123"</js>);    <jc>// false</jc>
3117    * </p>
3118    *
3119    * @param str The string to check.
3120    * @return <jk>true</jk> if the string is not null, not empty, and contains only alphanumeric characters.
3121    */
3122   public static boolean isAlphaNumeric(String str) {
3123      if (isEmpty(str))
3124         return false;
3125      for (var i = 0; i < str.length(); i++) {
3126         if (! (LETTER.contains(str.charAt(i)) || DIGIT.contains(str.charAt(i))))
3127            return false;
3128      }
3129      return true;
3130   }
3131
3132   /**
3133    * Checks if any of the provided strings are not blank (not null, not empty, and not whitespace only).
3134    *
3135    * <p>
3136    * Returns <jk>true</jk> if at least one string is not null, not empty, and contains non-whitespace characters.
3137    *
3138    * <h4 class='section'>Example:</h4>
3139    * <p class='bjava'>
3140    *    isAnyNotBlank(<jk>null</jk>, <jk>null</jk>);             <jc>// false</jc>
3141    *    isAnyNotBlank(<js>""</js>, <js>""</js>);                 <jc>// false</jc>
3142    *    isAnyNotBlank(<js>"   "</js>, <js>"   "</js>);           <jc>// false</jc>
3143    *    isAnyNotBlank(<jk>null</jk>, <js>"hello"</js>);          <jc>// true</jc>
3144    *    isAnyNotBlank(<js>""</js>, <js>"   "</js>, <js>"x"</js>);<jc>// true</jc>
3145    *    isAnyNotBlank(<js>"hello"</js>, <js>"world"</js>);       <jc>// true</jc>
3146    * </p>
3147    *
3148    * @param values The strings to check.
3149    * @return <jk>true</jk> if at least one string is not null, not empty, and contains non-whitespace characters.
3150    */
3151   public static boolean isAnyNotBlank(CharSequence...values) {
3152      if (values == null)
3153         return false;
3154      for (CharSequence value : values)
3155         if (isNotBlank(value))
3156            return true;
3157      return false;
3158   }
3159
3160   /**
3161    * Checks if any of the provided strings are not empty (not null and not zero-length).
3162    *
3163    * <p>
3164    * Returns <jk>true</jk> if at least one string is not null and has a length greater than zero.
3165    *
3166    * <h4 class='section'>Example:</h4>
3167    * <p class='bjava'>
3168    *    isAnyNotEmpty(<jk>null</jk>, <jk>null</jk>);             <jc>// false</jc>
3169    *    isAnyNotEmpty(<js>""</js>, <js>""</js>);                 <jc>// false</jc>
3170    *    isAnyNotEmpty(<jk>null</jk>, <js>"hello"</js>);          <jc>// true</jc>
3171    *    isAnyNotEmpty(<js>""</js>, <js>"   "</js>);              <jc>// true</jc>
3172    *    isAnyNotEmpty(<js>"hello"</js>, <js>"world"</js>);       <jc>// true</jc>
3173    * </p>
3174    *
3175    * @param values The strings to check.
3176    * @return <jk>true</jk> if at least one string is not null and not empty.
3177    */
3178   public static boolean isAnyNotEmpty(CharSequence...values) {
3179      if (values == null)
3180         return false;
3181      for (CharSequence value : values)
3182         if (value != null && ! value.isEmpty())
3183            return true;
3184      return false;
3185   }
3186
3187   /**
3188    * Checks if a string is blank (null, empty, or whitespace only).
3189    *
3190    * <h4 class='section'>Example:</h4>
3191    * <p class='bjava'>
3192    *    isBlank(<jk>null</jk>);       <jc>// true</jc>
3193    *    isBlank(<js>""</js>);         <jc>// true</jc>
3194    *    isBlank(<js>"   "</js>);      <jc>// true</jc>
3195    *    isBlank(<js>"hello"</js>);    <jc>// false</jc>
3196    * </p>
3197    *
3198    * @param str The string to check.
3199    * @return <jk>true</jk> if the string is null, empty, or contains only whitespace characters.
3200    */
3201   public static boolean isBlank(CharSequence str) {
3202      return str == null || str.toString().isBlank();
3203   }
3204
3205   /**
3206    * Checks if a string is a valid credit card number using the Luhn algorithm.
3207    *
3208    * <p>
3209    * Validates credit card numbers by:
3210    * <ul>
3211    *   <li>Removing spaces and hyphens</li>
3212    *   <li>Checking that all remaining characters are digits</li>
3213    *   <li>Verifying the number passes the Luhn algorithm check</li>
3214    *   <li>Ensuring the number is between 13-19 digits (standard credit card length)</li>
3215    * </ul>
3216    *
3217    * <h4 class='section'>Example:</h4>
3218    * <p class='bjava'>
3219    *    isCreditCard(<jk>null</jk>);                    <jc>// false</jc>
3220    *    isCreditCard(<js>""</js>);                      <jc>// false</jc>
3221    *    isCreditCard(<js>"4532015112830366"</js>);      <jc>// true (Visa test card)</jc>
3222    *    isCreditCard(<js>"4532-0151-1283-0366"</js>);   <jc>// true (with separators)</jc>
3223    *    isCreditCard(<js>"1234567890"</js>);            <jc>// false (invalid Luhn)</jc>
3224    * </p>
3225    *
3226    * @param str The string to check.
3227    * @return <jk>true</jk> if the string is a valid credit card number.
3228    */
3229   public static boolean isCreditCard(String str) {
3230      if (isEmpty(str))
3231         return false;
3232      // Remove spaces and hyphens
3233      var cleaned = str.replaceAll("[\\s\\-]", "");
3234      // Must be all digits and 13-19 digits long
3235      if (! cleaned.matches("^\\d{13,19}$"))
3236         return false;
3237      // Apply Luhn algorithm
3238      var sum = 0;
3239      var alternate = false;
3240      for (var i = cleaned.length() - 1; i >= 0; i--) {
3241         var digit = Character.getNumericValue(cleaned.charAt(i));
3242         if (alternate) {
3243            digit *= 2;
3244            if (digit > 9)
3245               digit = (digit % 10) + 1;
3246         }
3247         sum += digit;
3248         alternate = ! alternate;
3249      }
3250      return (sum % 10) == 0;
3251   }
3252
3253   /**
3254    * Returns <jk>true</jk> if the specified string is numeric.
3255    *
3256    * @param s The string to check.
3257    * @return <jk>true</jk> if the specified string is numeric.
3258    */
3259   public static boolean isDecimal(String s) {
3260      if (s == null || s.isEmpty() || ! FIRST_NUMBER_CHARS.contains(s.charAt(0)))
3261         return false;
3262      var i = 0;
3263      var length = s.length();
3264      var c = s.charAt(0);
3265      var isPrefixed = false;
3266      if (c == '+' || c == '-') {
3267         isPrefixed = true;
3268         i++;
3269      }
3270      if (i == length)
3271         return false;
3272      c = s.charAt(i++);
3273      if (c == '0' && length > (isPrefixed ? 2 : 1)) {
3274         c = s.charAt(i++);
3275         if (c == 'x' || c == 'X') {
3276            for (var j = i; j < length; j++) {
3277               if (! HEXADECIMAL_CHARS.contains(s.charAt(j)))
3278                  return false;
3279            }
3280         } else if (OCTAL_CHARS.contains(c)) {
3281            for (var j = i; j < length; j++)
3282               if (! OCTAL_CHARS.contains(s.charAt(j)))
3283                  return false;
3284         } else {
3285            return false;
3286         }
3287      } else if (c == '#') {
3288         for (var j = i; j < length; j++) {
3289            if (! HEXADECIMAL_CHARS.contains(s.charAt(j)))
3290               return false;
3291         }
3292      } else if (DECIMAL_CHARS.contains(c)) {
3293         for (var j = i; j < length; j++)
3294            if (! DECIMAL_CHARS.contains(s.charAt(j)))
3295               return false;
3296      } else {
3297         return false;
3298      }
3299      return true;
3300   }
3301
3302   /**
3303    * Scans the longest prefix of the specified string that matches the JSON-style number grammar.
3304    *
3305    * <p>
3306    * Hand-written equivalent (greedy, anchored at the start of the string) of one of:
3307    * <ul>
3308    *    <li>{@code -?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?} when {@code allowLeadingZeros} is <jk>false</jk>
3309    *       (integer part restricted to {@code 0} or a non-zero-leading run), or
3310    *    <li>{@code -?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?} when {@code allowLeadingZeros} is <jk>true</jk>
3311    *       (integer part is any run of one or more digits).
3312    * </ul>
3313    *
3314    * <p>
3315    * {@code \d} matches ASCII {@code [0-9]} only. To emulate a {@code matches()} (whole-string) check, test the
3316    * return value against {@code s.length()}.
3317    *
3318    * @param s The string to scan. Must not be <jk>null</jk>.
3319    * @param allowLeadingZeros Whether the integer part may contain leading zeros.
3320    * @return The length of the matched prefix, or {@code -1} if the string does not start with a valid number.
3321    */
3322   public static int matchNumberPrefix(String s, boolean allowLeadingZeros) {
3323      reqnn("s", s);
3324      var len = s.length();
3325      var i = 0;
3326      if (i < len && s.charAt(i) == '-')
3327         i++;
3328      // Integer part (required).
3329      if (i >= len)
3330         return -1;
3331      var c = s.charAt(i);
3332      if (allowLeadingZeros) {
3333         if (c < '0' || c > '9')
3334            return -1;
3335         i++;
3336         while (i < len && s.charAt(i) >= '0' && s.charAt(i) <= '9')
3337            i++;
3338      } else if (c == '0') {
3339         i++;
3340      } else if (c >= '1' && c <= '9') {
3341         i++;
3342         while (i < len && s.charAt(i) >= '0' && s.charAt(i) <= '9')
3343            i++;
3344      } else {
3345         return -1;
3346      }
3347      // Optional fraction (?:\.\d+)? - the '.' requires at least one following digit, else not consumed.
3348      if (i < len && s.charAt(i) == '.' && i + 1 < len && s.charAt(i + 1) >= '0' && s.charAt(i + 1) <= '9') {
3349         i += 2;
3350         while (i < len && s.charAt(i) >= '0' && s.charAt(i) <= '9')
3351            i++;
3352      }
3353      // Optional exponent (?:[eE][+-]?\d+)? - consumed only when a digit follows e/E and the optional sign.
3354      if (i < len && (s.charAt(i) == 'e' || s.charAt(i) == 'E')) {
3355         var j = i + 1;
3356         if (j < len && (s.charAt(j) == '+' || s.charAt(j) == '-'))
3357            j++;
3358         if (j < len && s.charAt(j) >= '0' && s.charAt(j) <= '9') {
3359            j++;
3360            while (j < len && s.charAt(j) >= '0' && s.charAt(j) <= '9')
3361               j++;
3362            i = j;
3363         }
3364      }
3365      return i;
3366   }
3367
3368   /**
3369    * Checks if a string contains only digit characters (0-9).
3370    *
3371    * <h4 class='section'>Example:</h4>
3372    * <p class='bjava'>
3373    *    isDigit(<jk>null</jk>);         <jc>// false</jc>
3374    *    isDigit(<js>""</js>);           <jc>// false</jc>
3375    *    isDigit(<js>"123"</js>);        <jc>// true</jc>
3376    *    isDigit(<js>"abc123"</js>);     <jc>// false</jc>
3377    *    isDigit(<js>"12.3"</js>);       <jc>// false</jc>
3378    * </p>
3379    *
3380    * @param str The string to check.
3381    * @return <jk>true</jk> if the string is not null, not empty, and contains only digit characters.
3382    */
3383   public static boolean isDigit(String str) {
3384      if (isEmpty(str))
3385         return false;
3386      for (var i = 0; i < str.length(); i++) {
3387         if (! DIGIT.contains(str.charAt(i)))
3388            return false;
3389      }
3390      return true;
3391   }
3392
3393   /**
3394    * Checks if a string is a valid email address.
3395    *
3396    * <p>
3397    * Performs basic email validation using a simple regex pattern.
3398    * This is not a complete RFC 5321/5322 validation, but covers most common email formats.
3399    *
3400    * <h4 class='section'>Example:</h4>
3401    * <p class='bjava'>
3402    *    isEmail(<jk>null</jk>);                    <jc>// false</jc>
3403    *    isEmail(<js>""</js>);                      <jc>// false</jc>
3404    *    isEmail(<js>"user@example.com"</js>);      <jc>// true</jc>
3405    *    isEmail(<js>"invalid.email"</js>);         <jc>// false</jc>
3406    * </p>
3407    *
3408    * @param str The string to check.
3409    * @return <jk>true</jk> if the string is a valid email address.
3410    */
3411   public static boolean isEmail(String str) {
3412      if (isEmpty(str))
3413         return false;
3414      // Basic email regex: local@domain
3415      // Allows letters, digits, dots, underscores, hyphens, and plus signs in local part
3416      // Domain must have at least one dot and valid TLD
3417      return str.matches("^[a-zA-Z0-9._%+-]+@(?:[a-zA-Z0-9-]+\\.)++[a-zA-Z]{2,}$");
3418   }
3419
3420   /**
3421    * Checks if a string is null or empty.
3422    *
3423    * <h4 class='section'>Example:</h4>
3424    * <p class='bjava'>
3425    *    isEmpty(<jk>null</jk>);    <jc>// true</jc>
3426    *    isEmpty(<js>""</js>);      <jc>// true</jc>
3427    *    isEmpty(<js>"abc"</js>);   <jc>// false</jc>
3428    * </p>
3429    *
3430    * @param str The string to check.
3431    * @return <jk>true</jk> if the string is null or empty.
3432    */
3433   public static boolean isEmpty(String str) {
3434      return str == null || str.isEmpty();
3435   }
3436
3437   /**
3438    * Checks if a character sequence is null or empty.
3439    *
3440    * @param str The character sequence to check.
3441    * @return <jk>true</jk> if the character sequence is null or empty.
3442    */
3443   public static boolean isEmpty(CharSequence str) {
3444      return str == null || str.isEmpty();
3445   }
3446
3447   /**
3448    * Checks if a string is non-null and not empty.
3449    *
3450    * <h4 class='section'>Example:</h4>
3451    * <p class='bjava'>
3452    *    isNotEmpty(<jk>null</jk>);    <jc>// false</jc>
3453    *    isNotEmpty(<js>""</js>);      <jc>// false</jc>
3454    *    isNotEmpty(<js>"abc"</js>);   <jc>// true</jc>
3455    * </p>
3456    *
3457    * @param str The string to check.
3458    * @return <jk>true</jk> if the string is non-null and not empty.
3459    */
3460   public static boolean isNotEmpty(String str) {
3461      return !isEmpty(str);
3462   }
3463
3464   /**
3465    * Checks if a character sequence is non-null and not empty.
3466    *
3467    * @param str The character sequence to check.
3468    * @return <jk>true</jk> if the character sequence is non-null and not empty.
3469    */
3470   public static boolean isNotEmpty(CharSequence str) {
3471      return !isEmpty(str);
3472   }
3473
3474   /**
3475    * Returns <jk>true</jk> if the specified character is a valid first character for a number.
3476    *
3477    * @param c The character to test.
3478    * @return <jk>true</jk> if the specified character is a valid first character for a number.
3479    */
3480   public static boolean isFirstNumberChar(char c) {
3481      return FIRST_NUMBER_CHARS.contains(c);
3482   }
3483
3484   /**
3485    * Returns <jk>true</jk> if the specified string is a floating point number.
3486    *
3487    * @param s The string to check.
3488    * @return <jk>true</jk> if the specified string is a floating point number.
3489    */
3490   public static boolean isFloat(String s) {
3491      if (s == null || s.isEmpty())
3492         return false;
3493      if (! FIRST_NUMBER_CHARS.contains(s.charAt(0)))
3494         return (s.equals("NaN") || s.equals("Infinity"));
3495      var i = 0;
3496      var length = s.length();
3497      var c = s.charAt(0);
3498      if (c == '+' || c == '-')
3499         i++;
3500      if (i == length)
3501         return false;
3502      c = s.charAt(i);
3503      if (c == '.' || DECIMAL_CHARS.contains(c)) {
3504         return FP_REGEX.matcher(s).matches();
3505      }
3506      return false;
3507   }
3508
3509   /**
3510    * Checks if a string is already interned.
3511    *
3512    * <p>
3513    * Returns <jk>false</jk> if the input string is <jk>null</jk>.
3514    * A string is considered interned if it is the same object reference as its interned version.
3515    *
3516    * <h4 class='section'>Examples:</h4>
3517    * <p class='bjava'>
3518    *    String <jv>s1</jv> = <js>"test"</js>;  <jc>// String literal is automatically interned</jc>
3519    *    assertTrue(isInterned(<jv>s1</jv>));
3520    *
3521    *    String <jv>s2</jv> = <jk>new</jk> String(<js>"test"</js>);  <jc>// New object, not interned</jc>
3522    *    assertFalse(isInterned(<jv>s2</jv>));
3523    *
3524    *    String <jv>s3</jv> = intern(<jv>s2</jv>);  <jc>// Now interned</jc>
3525    *    assertTrue(isInterned(<jv>s3</jv>));
3526    * </p>
3527    *
3528    * @param str The string to check. Can be <jk>null</jk>.
3529    * @return <jk>true</jk> if the string is interned, <jk>false</jk> otherwise.
3530    */
3531   public static boolean isInterned(String str) {
3532      if (str == null)
3533         return false;
3534      // Use == for reference equality check to determine if string is interned
3535      return str == str.intern(); // NOSONAR - Reference equality required to check if interned
3536   }
3537
3538   /**
3539    * Returns <jk>true</jk> if the specified string appears to be valid JSON.
3540    *
3541    * <p>
3542    * This method performs a simple heuristic check and does not strictly validate JSON syntax.
3543    * Leading and trailing spaces are ignored.
3544    * <br>Leading and trailing comments are not allowed.
3545    *
3546    * @param s The string to test.
3547    * @return <jk>true</jk> if the specified string appears to be valid JSON.
3548    */
3549   public static boolean isProbablyJson(String s) {
3550      if (s == null)
3551         return false;
3552      var c1 = firstNonWhitespaceChar(s);
3553      var c2 = lastNonWhitespaceChar(s);
3554      if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == '\'' && c2 == '\'')
3555         return true;
3556      return (isOneOf(s, "true", "false", "null") || isNumeric(s));
3557   }
3558
3559   /**
3560    * Returns <jk>true</jk> if the specified string appears to be a JSON array.
3561    *
3562    * <p>
3563    * This method performs a simple heuristic check and does not strictly validate JSON syntax.
3564    *
3565    * @param o The object to test.
3566    * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored.
3567    * @return <jk>true</jk> if the specified string appears to be a JSON array.
3568    */
3569   public static boolean isProbablyJsonArray(Object o, boolean ignoreWhitespaceAndComments) {
3570      if (o instanceof CharSequence o2) {
3571         var s = o2.toString();
3572         if (! ignoreWhitespaceAndComments)
3573            return (s.startsWith("[") && s.endsWith("]"));
3574         if (firstRealCharacter(s) != '[')
3575            return false;
3576         var i = s.lastIndexOf(']');
3577         if (i == -1)
3578            return false;
3579         s = s.substring(i + 1);
3580         return firstRealCharacter(s) == -1;
3581      }
3582      return false;
3583   }
3584
3585   /**
3586    * Returns <jk>true</jk> if the specified string appears to be a JSON object.
3587    *
3588    * <p>
3589    * This method performs a simple heuristic check and does not strictly validate JSON syntax.
3590    *
3591    * @param o The object to test.
3592    * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored.
3593    * @return <jk>true</jk> if the specified string appears to be a JSON object.
3594    */
3595   public static boolean isProbablyJsonObject(Object o, boolean ignoreWhitespaceAndComments) {
3596      if (o instanceof CharSequence o2) {
3597         var s = o2.toString();
3598         if (! ignoreWhitespaceAndComments)
3599            return (s.startsWith("{") && s.endsWith("}"));
3600         if (firstRealCharacter(s) != '{')
3601            return false;
3602         var i = s.lastIndexOf('}');
3603         if (i == -1)
3604            return false;
3605         s = s.substring(i + 1);
3606         return firstRealCharacter(s) == -1;
3607      }
3608      return false;
3609   }
3610
3611   /**
3612    * Checks if a string is not blank (not null, not empty, and not whitespace only).
3613    *
3614    * <h4 class='section'>Example:</h4>
3615    * <p class='bjava'>
3616    *    isNotBlank(<jk>null</jk>);       <jc>// false</jc>
3617    *    isNotBlank(<js>""</js>);         <jc>// false</jc>
3618    *    isNotBlank(<js>"   "</js>);      <jc>// false</jc>
3619    *    isNotBlank(<js>"hello"</js>);    <jc>// true</jc>
3620    * </p>
3621    *
3622    * @param str The string to check.
3623    * @return <jk>true</jk> if the string is not null, not empty, and contains non-whitespace characters.
3624    */
3625   public static boolean isNotBlank(CharSequence str) {
3626      return ! isBlank(str);
3627   }
3628
3629   /**
3630    * Returns <jk>true</jk> if the specified character is a valid number character.
3631    *
3632    * @param c The character to check.
3633    * @return <jk>true</jk> if the specified character is a valid number character.
3634    */
3635   public static boolean isNumberChar(char c) {
3636      return NUMBER_CHARS.contains(c);
3637   }
3638
3639   /**
3640    * Returns <jk>true</jk> if this string can be parsed by {@link #parseNumber(String, Class)}.
3641    *
3642    * @param s The string to check.
3643    * @return <jk>true</jk> if this string can be parsed without causing an exception.
3644    */
3645   public static boolean isNumeric(String s) {
3646      if (s == null || s.isEmpty() || ! isFirstNumberChar(s.charAt(0)))
3647         return false;
3648      return isDecimal(s) || isFloat(s);
3649   }
3650
3651   /**
3652    * Returns <jk>true</jk> if the specified string is one of the specified values.
3653    *
3654    * @param s
3655    *    The string to test.
3656    *    Can be <jk>null</jk>.
3657    * @param values
3658    *    The values to test.
3659    *    Can contain <jk>null</jk>.
3660    * @return <jk>true</jk> if the specified string is one of the specified values.
3661    */
3662   public static boolean isOneOf(String s, String...values) {
3663      reqnn("values", values);
3664      for (var value : values)
3665         if (equal(s, value))
3666            return true;
3667      return false;
3668   }
3669
3670   /**
3671    * Checks if a string is a valid phone number.
3672    *
3673    * <p>
3674    * Performs basic phone number validation.
3675    * Accepts various formats including:
3676    * <ul>
3677    *   <li>Digits only: <js>"1234567890"</js></li>
3678    *   <li>With separators: <js>"(123) 456-7890"</js>, <js>"123-456-7890"</js>, <js>"123.456.7890"</js></li>
3679    *   <li>With country code: <js>"+1 123-456-7890"</js></li>
3680    * </ul>
3681    *
3682    * <h4 class='section'>Example:</h4>
3683    * <p class='bjava'>
3684    *    isPhoneNumber(<jk>null</jk>);              <jc>// false</jc>
3685    *    isPhoneNumber(<js>""</js>);                <jc>// false</jc>
3686    *    isPhoneNumber(<js>"1234567890"</js>);      <jc>// true</jc>
3687    *    isPhoneNumber(<js>"(123) 456-7890"</js>);  <jc>// true</jc>
3688    *    isPhoneNumber(<js>"123"</js>);             <jc>// false</jc>
3689    * </p>
3690    *
3691    * @param str The string to check.
3692    * @return <jk>true</jk> if the string is a valid phone number.
3693    */
3694   public static boolean isPhoneNumber(String str) {
3695      if (isEmpty(str))
3696         return false;
3697      // Remove common phone number separators and check if remaining is 10-15 digits
3698      // Allows: digits, spaces, parentheses, hyphens, dots, plus sign (for country code)
3699      var cleaned = str.replaceAll("[\\s()\\-\\.]", "");
3700      if (cleaned.startsWith("+"))
3701         cleaned = cleaned.substring(1);
3702      // Phone numbers should have 10-15 digits (10 for US, up to 15 for international)
3703      return cleaned.matches("^\\d{10,15}$");
3704   }
3705
3706   /**
3707    * Checks if two strings are similar based on a similarity threshold.
3708    *
3709    * <p>
3710    * Uses the {@link #similarity(String, String)} method to calculate similarity and compares it to the threshold.
3711    *
3712    * <h4 class='section'>Example:</h4>
3713    * <p class='bjava'>
3714    *    isSimilar(<js>"hello"</js>, <js>"hello"</js>, <js>0.8</js>);        <jc>// true</jc>
3715    *    isSimilar(<js>"kitten"</js>, <js>"sitting"</js>, <js>0.8</js>);     <jc>// false</jc>
3716    *    isSimilar(<js>"kitten"</js>, <js>"sitting"</js>, <js>0.5</js>);     <jc>// true</jc>
3717    * </p>
3718    *
3719    * @param str1 The first string.
3720    * @param str2 The second string.
3721    * @param threshold The similarity threshold (0.0 to 1.0).
3722    * @return <jk>true</jk> if the similarity is greater than or equal to the threshold, <jk>false</jk> otherwise.
3723    */
3724   public static boolean isSimilar(String str1, String str2, double threshold) {
3725      return similarity(str1, str2) >= threshold;
3726   }
3727
3728   /**
3729    * Efficiently determines whether a URL is of the pattern "xxx:/xxx".
3730    *
3731    * <p>
3732    * The pattern matched is: <c>[a-z]{2,}\:\/.*</c>
3733    *
3734    * <p>
3735    * Note that this excludes filesystem paths such as <js>"C:/temp"</js>.
3736    *
3737    * @param s The string to test.  Can be <jk>null</jk> (returns <jk>false</jk>).
3738    * @return <jk>true</jk> if it's an absolute path.
3739    */
3740   @SuppressWarnings({
3741      "java:S1126" // State machine requires if-then-else structure
3742   })
3743   public static boolean isUri(String s) {
3744
3745      if (isEmpty(s))
3746         return false;
3747
3748      // Use a state machine for maximum performance.
3749
3750      // S1: Looking for protocol char 1
3751      // S2: Found protocol char 1, looking for protocol char 2
3752      // S3: Found protocol char 2, looking for :
3753      // S4: Found :, looking for /
3754
3755      var state = S1;
3756
3757      for (var i = 0; i < s.length(); i++) {
3758         var c = s.charAt(i);
3759         if (state == S1) {
3760            if (isLowerCaseLetter(c))
3761               state = S2;
3762            else
3763               return false;
3764         } else if (state == S2) {
3765            if (! isLowerCaseLetter(c))
3766               return false;
3767            state = S3;
3768         } else if (state == S3) {  // NOSONAR - State check necessary for state machine
3769            if (c == ':')
3770               state = S4;
3771            else if (! isLowerCaseLetter(c))
3772               return false;
3773         } else /* state == S4 */ {
3774            return c == '/';
3775         }
3776      }
3777      return false;
3778   }
3779
3780   private static boolean isLowerCaseLetter(char c) {
3781      return c >= 'a' && c <= 'z';
3782   }
3783
3784   /**
3785    * Validates if a date string matches the specified date format.
3786    *
3787    * <p>
3788    * Uses {@link SimpleDateFormat} to parse the date string according to the format pattern.
3789    *
3790    * <h4 class='section'>Example:</h4>
3791    * <p class='bjava'>
3792    *    isValidDateFormat(<js>"2023-12-25"</js>, <js>"yyyy-MM-dd"</js>);  <jc>// true</jc>
3793    *    isValidDateFormat(<js>"25/12/2023"</js>, <js>"dd/MM/yyyy"</js>);  <jc>// true</jc>
3794    *    isValidDateFormat(<js>"2023-13-25"</js>, <js>"yyyy-MM-dd"</js>);  <jc>// false (invalid month)</jc>
3795    * </p>
3796    *
3797    * @param dateStr The date string to validate. Can be <jk>null</jk>.
3798    * @param format The date format pattern (e.g., "yyyy-MM-dd"). Can be <jk>null</jk>.
3799    * @return <jk>true</jk> if the date string matches the format, <jk>false</jk> otherwise.
3800    */
3801   public static boolean isValidDateFormat(String dateStr, String format) {
3802      if (isEmpty(dateStr) || isEmpty(format))
3803         return false;
3804      try {
3805         var sdf = new SimpleDateFormat(format);
3806         sdf.setLenient(false); // Strict parsing
3807         sdf.parse(dateStr);
3808         return true;
3809      } catch (@SuppressWarnings("unused") ParseException | IllegalArgumentException e) {
3810         // IllegalArgumentException thrown for invalid format patterns
3811         return false;
3812      }
3813   }
3814
3815   /**
3816    * Validates if a string is a valid hostname.
3817    *
3818    * <p>
3819    * Validates hostnames according to RFC 1123. A valid hostname:
3820    * <ul>
3821    *   <li>Can contain letters, digits, and hyphens</li>
3822    *   <li>Cannot start or end with a hyphen</li>
3823    *   <li>Each label (dot-separated part) can be up to 63 characters</li>
3824    *   <li>Total length can be up to 253 characters</li>
3825    *   <li>Labels cannot be empty</li>
3826    * </ul>
3827    *
3828    * <h4 class='section'>Example:</h4>
3829    * <p class='bjava'>
3830    *    isValidHostname(<js>"example.com"</js>);        <jc>// true</jc>
3831    *    isValidHostname(<js>"sub.example.com"</js>);    <jc>// true</jc>
3832    *    isValidHostname(<js>"-invalid.com"</js>);       <jc>// false (starts with hyphen)</jc>
3833    *    isValidHostname(<js>"example..com"</js>);       <jc>// false (empty label)</jc>
3834    * </p>
3835    *
3836    * @param hostname The hostname string to validate. Can be <jk>null</jk>.
3837    * @return <jk>true</jk> if the string is a valid hostname, <jk>false</jk> otherwise.
3838    */
3839   public static boolean isValidHostname(String hostname) {
3840      if (isEmpty(hostname))
3841         return false;
3842
3843      // Cannot start or end with a dot
3844      if (hostname.startsWith(".") || hostname.endsWith("."))
3845         return false;
3846
3847      // Total length cannot exceed 253 characters
3848      if (hostname.length() > 253)
3849         return false;
3850
3851      // Split by dots (use -1 to preserve trailing empty strings)
3852      var labels = hostname.split("\\.", -1);
3853
3854      // Check each label
3855      for (var label : labels) {
3856         // Label cannot be empty
3857         if (label.isEmpty())
3858            return false;
3859
3860         // Label cannot exceed 63 characters
3861         if (label.length() > 63)
3862            return false;
3863
3864         // Label cannot start or end with hyphen
3865         if (label.startsWith("-") || label.endsWith("-"))
3866            return false;
3867
3868         // Label can only contain letters, digits, and hyphens
3869         if (! label.matches("^[a-zA-Z0-9-]+$"))
3870            return false;
3871      }
3872
3873      return true;
3874   }
3875
3876   /**
3877    * Validates if a string is a valid IP address (IPv4 or IPv6).
3878    *
3879    * <p>
3880    * Supports both IPv4 (e.g., "192.168.1.1") and IPv6 (e.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334") formats.
3881    *
3882    * <h4 class='section'>Example:</h4>
3883    * <p class='bjava'>
3884    *    isValidIpAddress(<js>"192.168.1.1"</js>);                    <jc>// true</jc>
3885    *    isValidIpAddress(<js>"2001:0db8:85a3::8a2e:0370:7334"</js>); <jc>// true</jc>
3886    *    isValidIpAddress(<js>"256.1.1.1"</js>);                      <jc>// false</jc>
3887    *    isValidIpAddress(<js>"not.an.ip"</js>);                      <jc>// false</jc>
3888    * </p>
3889    *
3890    * @param ip The IP address string to validate. Can be <jk>null</jk>.
3891    * @return <jk>true</jk> if the string is a valid IP address, <jk>false</jk> otherwise.
3892    */
3893   public static boolean isValidIpAddress(String ip) {
3894      if (isEmpty(ip))
3895         return false;
3896      try {
3897         // Try IPv4 first
3898         if (ip.contains(".") && ! ip.contains(":")) {
3899            var parts = ip.split("\\.");
3900            if (parts.length != 4)
3901               return false;
3902            for (var part : parts) {
3903               var num = Integer.parseInt(part);
3904               if (num < 0 || num > 255)
3905                  return false;
3906            }
3907            return true;
3908         }
3909         // Try IPv6 - validate format without network operations
3910         if (ip.contains(":")) {
3911            return isValidIPv6Address(ip);
3912         }
3913         return false;
3914      } catch (@SuppressWarnings("unused") NumberFormatException e) {
3915         return false;
3916      }
3917   }
3918
3919   /**
3920    * Validates if a string is a valid MAC address.
3921    *
3922    * <p>
3923    * Supports common MAC address formats:
3924    * <ul>
3925    *   <li>Colon-separated: <js>"00:1B:44:11:3A:B7"</js></li>
3926    *   <li>Hyphen-separated: <js>"00-1B-44-11-3A-B7"</js></li>
3927    *   <li>No separators: <js>"001B44113AB7"</js></li>
3928    * </ul>
3929    *
3930    * <h4 class='section'>Example:</h4>
3931    * <p class='bjava'>
3932    *    isValidMacAddress(<js>"00:1B:44:11:3A:B7"</js>);  <jc>// true</jc>
3933    *    isValidMacAddress(<js>"00-1B-44-11-3A-B7"</js>);  <jc>// true</jc>
3934    *    isValidMacAddress(<js>"001B44113AB7"</js>);       <jc>// true</jc>
3935    *    isValidMacAddress(<js>"00:1B:44:11:3A"</js>);     <jc>// false (too short)</jc>
3936    * </p>
3937    *
3938    * @param mac The MAC address string to validate. Can be <jk>null</jk>.
3939    * @return <jk>true</jk> if the string is a valid MAC address, <jk>false</jk> otherwise.
3940    */
3941   public static boolean isValidMacAddress(String mac) {
3942      if (isEmpty(mac))
3943         return false;
3944
3945      // Remove separators and check if it's 12 hex digits
3946      var cleaned = mac.replaceAll("[:-]", "").toUpperCase(Locale.ROOT);
3947      if (cleaned.length() != 12)
3948         return false;
3949
3950      // Check if all characters are valid hex digits
3951      return cleaned.matches("^[0-9A-F]{12}$");
3952   }
3953
3954   /**
3955    * Validates if a string is a valid regular expression pattern.
3956    *
3957    * <p>
3958    * Attempts to compile the regex pattern to verify it's syntactically correct.
3959    *
3960    * <h4 class='section'>Example:</h4>
3961    * <p class='bjava'>
3962    *    isValidRegex(<js>"[a-z]+"</js>);        <jc>// true</jc>
3963    *    isValidRegex(<js>"[a-z"</js>);          <jc>// false (unclosed bracket)</jc>
3964    *    isValidRegex(<js>"(test"</js>);         <jc>// false (unclosed parenthesis)</jc>
3965    * </p>
3966    *
3967    * @param regex The regex pattern to validate. Can be <jk>null</jk>.
3968    * @return <jk>true</jk> if the string is a valid regex pattern, <jk>false</jk> otherwise.
3969    */
3970   public static boolean isValidRegex(String regex) {
3971      if (isEmpty(regex))
3972         return false;
3973      try {
3974         Pattern.compile(regex);
3975         return true;
3976      } catch (@SuppressWarnings("unused") PatternSyntaxException e) {
3977         return false;
3978      }
3979   }
3980
3981   /**
3982    * Validates if a time string matches the specified time format.
3983    *
3984    * <p>
3985    * Uses {@link SimpleDateFormat} to parse the time string according to the format pattern.
3986    *
3987    * <h4 class='section'>Example:</h4>
3988    * <p class='bjava'>
3989    *    isValidTimeFormat(<js>"14:30:00"</js>, <js>"HH:mm:ss"</js>);  <jc>// true</jc>
3990    *    isValidTimeFormat(<js>"2:30 PM"</js>, <js>"h:mm a"</js>);     <jc>// true</jc>
3991    *    isValidTimeFormat(<js>"25:00:00"</js>, <js>"HH:mm:ss"</js>);  <jc>// false (invalid hour)</jc>
3992    * </p>
3993    *
3994    * @param timeStr The time string to validate. Can be <jk>null</jk>.
3995    * @param format The time format pattern (e.g., "HH:mm:ss"). Can be <jk>null</jk>.
3996    * @return <jk>true</jk> if the time string matches the format, <jk>false</jk> otherwise.
3997    */
3998   public static boolean isValidTimeFormat(String timeStr, String format) {
3999      if (isEmpty(timeStr) || isEmpty(format))
4000         return false;
4001      try {
4002         var sdf = new SimpleDateFormat(format);
4003         sdf.setLenient(false); // Strict parsing
4004         sdf.parse(timeStr);
4005         return true;
4006      } catch (@SuppressWarnings("unused") ParseException | IllegalArgumentException e) {
4007         // IllegalArgumentException thrown for invalid format patterns
4008         return false;
4009      }
4010   }
4011
4012   /**
4013    * Checks if a character is whitespace.
4014    *
4015    * @param c The character to check.
4016    * @return <jk>true</jk> if the character is whitespace.
4017    */
4018   public static boolean isWhitespace(int c) {
4019      return Character.isWhitespace(c);
4020   }
4021
4022   /**
4023    * Checks if a string contains only whitespace characters.
4024    *
4025    * <h4 class='section'>Example:</h4>
4026    * <p class='bjava'>
4027    *    isWhitespace(<jk>null</jk>);         <jc>// false</jc>
4028    *    isWhitespace(<js>""</js>);           <jc>// true</jc>
4029    *    isWhitespace(<js>"   "</js>);        <jc>// true</jc>
4030    *    isWhitespace(<js>"\t\n"</js>);       <jc>// true</jc>
4031    *    isWhitespace(<js>" a "</js>);        <jc>// false</jc>
4032    * </p>
4033    *
4034    * @param str The string to check.
4035    * @return <jk>true</jk> if the string is not null and contains only whitespace characters (or is empty).
4036    */
4037   public static boolean isWhitespace(String str) {
4038      if (str == null)
4039         return false;
4040      if (str.isEmpty())
4041         return true;
4042      for (var i = 0; i < str.length(); i++) {
4043         if (! isWhitespace(str.charAt(i)))
4044            return false;
4045      }
4046      return true;
4047   }
4048
4049   /**
4050    * Combines collection values into a simple comma-delimited string.
4051    *
4052    * @param values The values to join.
4053    * @return A comma-delimited string, or <jk>null</jk> if the input is <jk>null</jk>.
4054    */
4055   public static String join(Collection<?> values) {
4056      if (values == null)
4057         return null;
4058      return joine(toList(values), ',');
4059   }
4060
4061   /**
4062    * Join the specified tokens into a delimited string.
4063    *
4064    * @param tokens The tokens to join.
4065    * @param d The delimiter.
4066    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
4067    */
4068   public static String join(Collection<?> tokens, char d) {
4069      if (tokens == null)
4070         return null;
4071      var sb = new StringBuilder();
4072      for (var iter = tokens.iterator(); iter.hasNext();) {
4073         sb.append(iter.next());
4074         if (iter.hasNext())
4075            sb.append(d);
4076      }
4077      return sb.toString();
4078   }
4079
4080   /**
4081    * Join the specified tokens into a delimited string.
4082    *
4083    * @param tokens The tokens to join.
4084    * @param d The delimiter.
4085    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
4086    */
4087   public static String join(Collection<?> tokens, String d) {
4088      if (tokens == null)
4089         return null;
4090      return StringUtils.join(tokens, d, new StringBuilder()).toString();
4091   }
4092
4093   /**
4094    * Joins the specified tokens into a delimited string and writes the output to the specified string builder.
4095    *
4096    * @param tokens The tokens to join.
4097    * @param d The delimiter.
4098    * @param sb The string builder to append the response to.  Must not be <jk>null</jk> when <c>tokens</c> is non-<jk>null</jk>.
4099    * @return The same string builder passed in as <c>sb</c>.
4100    */
4101   public static StringBuilder join(Collection<?> tokens, String d, StringBuilder sb) {
4102      if (tokens == null)
4103         return sb;
4104      for (var iter = tokens.iterator(); iter.hasNext();) {
4105         sb.append(iter.next());
4106         if (iter.hasNext())
4107            sb.append(d);
4108      }
4109      return sb;
4110   }
4111
4112   /**
4113    * Join the specified tokens into a delimited string.
4114    *
4115    * @param tokens The tokens to join.
4116    * @param d The delimiter.
4117    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
4118    */
4119   public static String join(int[] tokens, char d) {
4120      if (tokens == null)
4121         return null;
4122      var sb = new StringBuilder();
4123      for (var i = 0; i < tokens.length; i++) {
4124         if (i > 0)
4125            sb.append(d);
4126         sb.append(tokens[i]);
4127      }
4128      return sb.toString();
4129   }
4130
4131   /**
4132    * Joins an array of integers with a delimiter.
4133    *
4134    * <h4 class='section'>Example:</h4>
4135    * <p class='bjava'>
4136    *    join(<jk>new int</jk>[]{1, 2, 3}, <js>","</js>);   <jc>// "1,2,3"</jc>
4137    *    join(<jk>new int</jk>[]{}, <js>","</js>);          <jc>// ""</jc>
4138    * </p>
4139    *
4140    * @param array The array to join.  Can be <jk>null</jk> (returns an empty string).
4141    * @param delimiter The delimiter string.  Can be <jk>null</jk> (treated as empty).
4142    * @return The joined string.
4143    */
4144   public static String join(int[] array, String delimiter) {
4145      if (array == null || array.length == 0)
4146         return "";
4147      if (delimiter == null)
4148         delimiter = "";
4149      return Arrays.stream(array).mapToObj(String::valueOf).collect(Collectors.joining(delimiter));
4150   }
4151
4152
4153   /**
4154    * Joins the specified tokens into a delimited string.
4155    *
4156    * @param tokens The tokens to join.
4157    * @param d The delimiter.
4158    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
4159    */
4160   public static String join(Object[] tokens, char d) {
4161      if (tokens == null)
4162         return null;
4163      if (tokens.length == 1)
4164         return emptyIfNull(stringify(tokens[0]));
4165      return StringUtils.join(tokens, d, new StringBuilder()).toString();
4166   }
4167
4168   /**
4169    * Join the specified tokens into a delimited string and writes the output to the specified string builder.
4170    *
4171    * @param tokens The tokens to join.
4172    * @param d The delimiter.
4173    * @param sb The string builder to append the response to.  Must not be <jk>null</jk> when <c>tokens</c> is non-<jk>null</jk>.
4174    * @return The same string builder passed in as <c>sb</c>.
4175    */
4176   public static StringBuilder join(Object[] tokens, char d, StringBuilder sb) {
4177      if (tokens == null)
4178         return sb;
4179      for (var i = 0; i < tokens.length; i++) {
4180         if (i > 0)
4181            sb.append(d);
4182         sb.append(tokens[i]);
4183      }
4184      return sb;
4185   }
4186
4187   /**
4188    * Join the specified tokens into a delimited string.
4189    *
4190    * @param tokens The tokens to join.
4191    * @param separator The delimiter.
4192    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
4193    */
4194   public static String join(Object[] tokens, String separator) {
4195      if (tokens == null)
4196         return null;
4197      var sb = new StringBuilder();
4198      for (var i = 0; i < tokens.length; i++) {
4199         if (i > 0)
4200            sb.append(separator);
4201         sb.append(tokens[i]);
4202      }
4203      return sb.toString();
4204   }
4205
4206   /**
4207    * Combines values into a simple comma-delimited string.
4208    *
4209    * @param values The values to join.
4210    * @return A comma-delimited string.
4211    */
4212   public static String join(String...values) {
4213      return join(values, ',');
4214   }
4215
4216   /**
4217    * Same as {@link StringUtils#join(Collection, char)} but escapes the delimiter if found in the tokens.
4218    *
4219    * @param tokens The tokens to join.
4220    * @param d The delimiter.
4221    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
4222    */
4223   public static String joine(List<?> tokens, char d) {
4224      if (tokens == null)
4225         return null;
4226      var as = getEscapeSet(d);
4227      var sb = new StringBuilder();
4228      for (int i = 0, j = tokens.size(); i < j; i++) {
4229         if (i > 0)
4230            sb.append(d);
4231         sb.append(escapeChars(stringify(tokens.get(i)), as));
4232      }
4233      return sb.toString();
4234   }
4235
4236   /**
4237    * Joins tokens with newlines.
4238    *
4239    * @param tokens The tokens to concatenate.  Can be <jk>null</jk> (returns <jk>null</jk>).
4240    * @return A string with the specified tokens contatenated with newlines, or <jk>null</jk> if the input was <jk>null</jk>.
4241    */
4242   public static String joinnl(Object[] tokens) {
4243      return join(tokens, '\n');
4244   }
4245
4246   /**
4247    * Converts a string to kebab-case format.
4248    *
4249    * <p>
4250    * Handles various input formats:
4251    * <ul>
4252    *   <li>Space-separated: "hello world" → "hello-world"</li>
4253    *   <li>CamelCase: "helloWorld" → "hello-world"</li>
4254    *   <li>PascalCase: "HelloWorld" → "hello-world"</li>
4255    *   <li>Snake_case: "hello_world" → "hello-world"</li>
4256    * </ul>
4257    *
4258    * <h4 class='section'>Example:</h4>
4259    * <p class='bjava'>
4260    *    kebabCase(<jk>null</jk>);                    <jc>// null</jc>
4261    *    kebabCase(<js>""</js>);                      <jc>// ""</jc>
4262    *    kebabCase(<js>"hello world"</js>);           <jc>// "hello-world"</jc>
4263    *    kebabCase(<js>"helloWorld"</js>);            <jc>// "hello-world"</jc>
4264    *    kebabCase(<js>"HelloWorld"</js>);            <jc>// "hello-world"</jc>
4265    *    kebabCase(<js>"hello_world"</js>);           <jc>// "hello-world"</jc>
4266    * </p>
4267    *
4268    * @param str The string to convert.
4269    * @return The kebab-case string, or <jk>null</jk> if input is <jk>null</jk>.
4270    */
4271   public static String kebabCase(String str) {
4272      if (isEmpty(str))
4273         return str;
4274
4275      var words = splitWords(str);
4276      if (words.isEmpty())
4277         return "";
4278
4279      var result = new StringBuilder();
4280      for (var i = 0; i < words.size(); i++) {
4281         if (i > 0)
4282            result.append('-');
4283         result.append(lowerCaseRoot(words.get(i)));
4284      }
4285
4286      return result.toString();
4287   }
4288
4289   /**
4290    * Finds the index of the last occurrence of a substring within a string.
4291    *
4292    * <h4 class='section'>Example:</h4>
4293    * <p class='bjava'>
4294    *    lastIndexOf(<js>"hello world world"</js>, <js>"world"</js>);     <jc>// 12</jc>
4295    *    lastIndexOf(<js>"hello world"</js>, <js>"xyz"</js>);             <jc>// -1</jc>
4296    *    lastIndexOf(<jk>null</jk>, <js>"test"</js>);                     <jc>// -1</jc>
4297    * </p>
4298    *
4299    * @param str The string to search in.
4300    * @param search The substring to search for.
4301    * @return The index of the last occurrence, or <c>-1</c> if not found or if either parameter is <jk>null</jk>.
4302    */
4303   public static int lastIndexOf(String str, String search) {
4304      if (str == null || search == null)
4305         return -1;
4306      return str.lastIndexOf(search);
4307   }
4308
4309   /**
4310    * Finds the index of the last occurrence of a substring within a string, ignoring case.
4311    *
4312    * <h4 class='section'>Example:</h4>
4313    * <p class='bjava'>
4314    *    lastIndexOfIgnoreCase(<js>"Hello World World"</js>, <js>"world"</js>);     <jc>// 12</jc>
4315    *    lastIndexOfIgnoreCase(<js>"Hello World"</js>, <js>"WORLD"</js>);           <jc>// 6</jc>
4316    *    lastIndexOfIgnoreCase(<js>"hello world"</js>, <js>"xyz"</js>);             <jc>// -1</jc>
4317    * </p>
4318    *
4319    * @param str The string to search in.
4320    * @param search The substring to search for.
4321    * @return The index of the last occurrence, or <c>-1</c> if not found or if either parameter is <jk>null</jk>.
4322    */
4323   public static int lastIndexOfIgnoreCase(String str, String search) {
4324      if (str == null || search == null)
4325         return -1;
4326      return str.toLowerCase(Locale.ROOT).lastIndexOf(search.toLowerCase(Locale.ROOT));
4327   }
4328
4329   /**
4330    * Returns the last non-whitespace character in the string.
4331    *
4332    * @param s The string to check.
4333    * @return
4334    *    The last non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed
4335    *    of only whitespace.
4336    */
4337   public static char lastNonWhitespaceChar(String s) {
4338      if (isNotNull(s))
4339         for (var i = s.length() - 1; i >= 0; i--)
4340            if (! isWhitespace(s.charAt(i)))
4341               return s.charAt(i);
4342      return 0;
4343   }
4344
4345   /**
4346    * Returns the leftmost characters of a string.
4347    *
4348    * <h4 class='section'>Example:</h4>
4349    * <p class='bjava'>
4350    *    left(<jk>null</jk>, 3);          <jc>// null</jc>
4351    *    left(<js>""</js>, 3);            <jc>// ""</jc>
4352    *    left(<js>"hello"</js>, 3);       <jc>// "hel"</jc>
4353    *    left(<js>"hello"</js>, 10);      <jc>// "hello"</jc>
4354    * </p>
4355    *
4356    * @param str The string to get characters from.
4357    * @param len The number of characters to get.
4358    * @return The leftmost characters, or <jk>null</jk> if input is <jk>null</jk>.
4359    */
4360   public static String left(String str, int len) {
4361      if (str == null)
4362         return null;
4363      if (len < 0)
4364         return "";
4365      if (len >= str.length())
4366         return str;
4367      return str.substring(0, len);
4368   }
4369
4370   /**
4371    * Calculates the Levenshtein distance (edit distance) between two strings.
4372    *
4373    * <p>
4374    * The Levenshtein distance is the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one string into another.
4375    *
4376    * <h4 class='section'>Example:</h4>
4377    * <p class='bjava'>
4378    *    levenshteinDistance(<js>"kitten"</js>, <js>"sitting"</js>);     <jc>// 3</jc>
4379    *    levenshteinDistance(<js>"hello"</js>, <js>"hello"</js>);        <jc>// 0</jc>
4380    *    levenshteinDistance(<js>"abc"</js>, <js>""</js>);               <jc>// 3</jc>
4381    * </p>
4382    *
4383    * @param str1 The first string.  Can be <jk>null</jk> (treated as empty).
4384    * @param str2 The second string.  Can be <jk>null</jk> (treated as empty).
4385    * @return The Levenshtein distance between the two strings.
4386    */
4387   public static int levenshteinDistance(String str1, String str2) {
4388      if (str1 == null)
4389         str1 = "";
4390      if (str2 == null)
4391         str2 = "";
4392
4393      var len1 = str1.length();
4394      var len2 = str2.length();
4395
4396      // Use dynamic programming with optimized space (only need previous row)
4397      var prev = new int[len2 + 1];
4398      var curr = new int[len2 + 1];
4399
4400      // Initialize first row
4401      for (var j = 0; j <= len2; j++)
4402         prev[j] = j;
4403
4404      for (var i = 1; i <= len1; i++) {
4405         curr[0] = i;
4406         for (var j = 1; j <= len2; j++) {
4407            if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
4408               curr[j] = prev[j - 1];
4409            } else {
4410               curr[j] = 1 + Math.min(Math.min(prev[j], curr[j - 1]), prev[j - 1]);
4411            }
4412         }
4413         // Swap arrays
4414         var temp = prev;
4415         prev = curr;
4416         curr = temp;
4417      }
4418
4419      return prev[len2];
4420   }
4421
4422   /**
4423    * Counts the number of lines in a string.
4424    *
4425    * <p>
4426    * Counts newline characters. A string ending without a newline is counted as one line.
4427    *
4428    * <h4 class='section'>Example:</h4>
4429    * <p class='bjava'>
4430    *    lineCount(<js>"line1\nline2\nline3"</js>);      <jc>// 3</jc>
4431    *    lineCount(<js>"single line"</js>);              <jc>// 1</jc>
4432    *    lineCount(<js>"line1\r\nline2"</js>);          <jc>// 2</jc>
4433    * </p>
4434    *
4435    * @param str The string to count lines in. Can be <jk>null</jk>.
4436    * @return The number of lines, or <c>0</c> if the string is <jk>null</jk> or empty.
4437    */
4438   public static int lineCount(String str) {
4439      if (isEmpty(str))
4440         return 0;
4441
4442      var count = 1; // At least one line
4443      for (var i = 0; i < str.length(); i++) {
4444         var c = str.charAt(i);
4445         if (c == '\n') {
4446            count++;
4447         } else if (c == '\r') {
4448            // Handle \r\n as a single line break
4449            if (i + 1 < str.length() && str.charAt(i + 1) == '\n')
4450               i++;
4451            count++;
4452         }
4453      }
4454
4455      return count;
4456   }
4457
4458   /**
4459    * Null-safe convenience method for {@link String#toLowerCase()}.
4460    *
4461    * <p>
4462    * Converts the string to lowercase if not null.
4463    *
4464    * @param s The string to convert.
4465    * @return The lowercase string, or <jk>null</jk> if the input was <jk>null</jk>.
4466    * @see #upperCase(String)
4467    * @see #lowerCase(String)
4468    */
4469   public static String lowerCase(String s) {
4470      return s == null ? null : s.toLowerCase();
4471   }
4472
4473   /**
4474    * Null-safe convenience method for {@link String#toLowerCase(Locale)} using {@link Locale#ROOT}.
4475    *
4476    * <p>
4477    * Locale-independent lower-casing for parse/match/normalize/key-fold code paths (as opposed to
4478    * {@link #lowerCase(String)}, which uses the default locale and is intended for display casing).
4479    *
4480    * @param s The string to convert.
4481    * @return The lowercase string, or <jk>null</jk> if the input was <jk>null</jk>.
4482    * @see #upperCaseRoot(String)
4483    * @see #lowerCase(String)
4484    */
4485   public static String lowerCaseRoot(String s) {
4486      return s == null ? null : s.toLowerCase(Locale.ROOT);
4487   }
4488
4489   /**
4490    * Maps each element of a string array using the specified function.
4491    *
4492    * <p>
4493    * Returns <jk>null</jk> if the array is <jk>null</jk>.
4494    * Returns an array with <jk>null</jk> elements if the function is <jk>null</jk> or returns <jk>null</jk>.
4495    *
4496    * <h4 class='section'>Examples:</h4>
4497    * <p class='bjava'>
4498    *    String[] <jv>array</jv> = {<js>"foo"</js>, <js>"bar"</js>, <js>"baz"</js>};
4499    *    String[] <jv>uppercased</jv> = map(<jv>array</jv>, String::toUpperCase);
4500    *    <jc>// Returns: ["FOO", "BAR", "BAZ"]</jc>
4501    *
4502    *    String[] <jv>prefixed</jv> = map(<jv>array</jv>, s -&gt; <js>"prefix-"</js> + s);
4503    *    <jc>// Returns: ["prefix-foo", "prefix-bar", "prefix-baz"]</jc>
4504    * </p>
4505    *
4506    * @param array The array to map. Can be <jk>null</jk> (returns an empty array).
4507    * @param mapper The function to apply to each element. Can be <jk>null</jk> (returns an unmapped copy of the array).
4508    * @return A new array with the mapped elements, or an empty array if the array was <jk>null</jk>.
4509    */
4510   public static String[] mapped(String[] array, UnaryOperator<String> mapper) {
4511      if (array == null)
4512         return new String[0];
4513      if (mapper == null)
4514         return Arrays.copyOf(array, array.length);
4515      return Arrays.stream(array).map(mapper).toArray(String[]::new);
4516   }
4517
4518   /**
4519    * Checks if a string matches a regular expression pattern.
4520    *
4521    * <h4 class='section'>Example:</h4>
4522    * <p class='bjava'>
4523    *    matches(<js>"12345"</js>, <js>"\\d+"</js>);              <jc>// true</jc>
4524    *    matches(<js>"abc123"</js>, <js>"^[a-z]+\\d+$"</js>);     <jc>// true</jc>
4525    *    matches(<js>"abc"</js>, <js>"\\d+"</js>);                <jc>// false</jc>
4526    * </p>
4527    *
4528    * @param str The string to check.
4529    * @param regex The regular expression pattern.
4530    * @return <jk>true</jk> if the string matches the pattern, <jk>false</jk> otherwise.
4531    * @throws PatternSyntaxException If the regex pattern is invalid.
4532    */
4533   public static boolean matches(String str, String regex) {
4534      if (str == null || regex == null)
4535         return false;
4536      return str.matches(regex);
4537   }
4538
4539   /**
4540    * Generates a Metaphone code for a string.
4541    *
4542    * <p>
4543    * Metaphone is a phonetic algorithm that produces codes representing how words sound.
4544    * It's more accurate than Soundex for English words.
4545    *
4546    * <h4 class='section'>Example:</h4>
4547    * <p class='bjava'>
4548    *    metaphone(<js>"Smith"</js>);   <jc>// "SM0"</jc>
4549    *    metaphone(<js>"Smythe"</js>);  <jc>// "SM0"</jc>
4550    *    metaphone(<js>"Robert"</js>);  <jc>// "RBRT"</jc>
4551    * </p>
4552    *
4553    * @param str The string to generate a Metaphone code for. Can be <jk>null</jk>.
4554    * @return The Metaphone code, or <jk>null</jk> if input is <jk>null</jk> or empty.
4555    */
4556   @SuppressWarnings({
4557      "java:S1871" // Multiple cases in phonetic algorithm intentionally map to the same code (e.g. 'Q' -> 'K')
4558   })
4559   public static String metaphone(String str) {
4560      if (isEmpty(str))
4561         return null;
4562
4563      var upper = str.toUpperCase(Locale.ROOT).replaceAll("[^A-Z]", "");
4564      if (upper.isEmpty())
4565         return "";
4566
4567      var result = new StringBuilder();
4568      var i = 0;
4569      var len = upper.length();
4570
4571      // Handle initial characters
4572      if (upper.startsWith("KN") || upper.startsWith("GN") || upper.startsWith("PN") || upper.startsWith("AE") || upper.startsWith("WR")) {
4573         i = 1;
4574      } else if (upper.startsWith("X")) {
4575         result.append('S');
4576         i = 1;
4577      } else if (upper.startsWith("WH")) {
4578         result.append('W');
4579         i = 2;
4580      }
4581
4582      // Process remaining characters
4583      while (i < len && result.length() < 4) {
4584         var c = upper.charAt(i);
4585         var prev = i > 0 ? upper.charAt(i - 1) : '\0';
4586         var next = i < len - 1 ? upper.charAt(i + 1) : '\0';
4587         var next2 = i < len - 2 ? upper.charAt(i + 2) : '\0';
4588
4589         // Skip duplicates (except C)
4590         if (c == prev && c != 'C') {
4591            i++;
4592            continue;
4593         }
4594
4595         switch (c) {
4596            case 'B':
4597               if (prev != 'M' || next != '\0')
4598                  result.append('B');
4599               break;
4600            case 'C':
4601               if (next == 'H') {
4602                  if (prev == 'S')
4603                     result.append('K');
4604                  else
4605                     result.append('X');
4606                  i++;
4607               } else if (next == 'I' || next == 'E' || next == 'Y') {
4608                  result.append('S');
4609               } else {
4610                  result.append('K');
4611               }
4612               break;
4613            case 'D':
4614               if (next == 'G' && (next2 == 'E' || next2 == 'I' || next2 == 'Y')) {
4615                  result.append('J');
4616                  i++;
4617               } else {
4618                  result.append('T');
4619               }
4620               break;
4621            case 'F', 'J', 'L', 'M', 'N', 'R':
4622               result.append(c);
4623               break;
4624            case 'G':
4625               if (next == 'H' && (next2 == 'A' || next2 == 'E' || next2 == 'I' || next2 == 'O' || next2 == 'U')) {
4626                  // Silent GH
4627               } else if (next == 'N' && (next2 == 'E' || next2 == 'D')) {
4628                  // Silent GN
4629               } else if ((next == 'E' || next == 'I' || next == 'Y') && prev != 'G') {
4630                  result.append('J');
4631               } else {
4632                  result.append('K');
4633               }
4634               break;
4635            case 'H':
4636               if (! VOWEL.contains(prev) || ! VOWEL.contains(next))
4637                  result.append('H');
4638               break;
4639            case 'K':
4640               if (prev != 'C')
4641                  result.append('K');
4642               break;
4643            case 'P':
4644               if (next == 'H') {
4645                  result.append('F');
4646                  i++;
4647               } else {
4648                  result.append('P');
4649               }
4650               break;
4651            case 'Q':
4652               result.append('K');
4653               break;
4654            case 'S':
4655               if (next == 'H') {
4656                  result.append('X');
4657                  i++;
4658               } else if (next == 'I' && (next2 == 'O' || next2 == 'A')) {
4659                  result.append('X');
4660                  i++;
4661               } else {
4662                  result.append('S');
4663               }
4664               break;
4665            case 'T':
4666               if (next == 'H') {
4667                  result.append('0'); // TH sound
4668                  i++;
4669               } else if (next == 'I' && (next2 == 'O' || next2 == 'A')) {
4670                  result.append('X');
4671                  i++;
4672               } else {
4673                  result.append('T');
4674               }
4675               break;
4676            case 'V':
4677               result.append('F');
4678               break;
4679            case 'W', 'Y':
4680               if (VOWEL.contains(next))
4681                  result.append(c);
4682               break;
4683            case 'X':
4684               // X at start is handled in initial section (line 4346-4348), so i is never 0 here
4685               result.append("KS");
4686               break;
4687            case 'Z':
4688               result.append('S');
4689               break;
4690            default:
4691               break;
4692         }
4693         i++;
4694      }
4695
4696      return result.isEmpty() ? upper.substring(0, Math.min(1, upper.length())) : result.toString();
4697   }
4698
4699   /**
4700    * Returns the middle characters of a string.
4701    *
4702    * <h4 class='section'>Example:</h4>
4703    * <p class='bjava'>
4704    *    mid(<jk>null</jk>, 1, 3);          <jc>// null</jc>
4705    *    mid(<js>""</js>, 1, 3);            <jc>// ""</jc>
4706    *    mid(<js>"hello"</js>, 1, 3);       <jc>// "ell"</jc>
4707    *    mid(<js>"hello"</js>, 1, 10);      <jc>// "ello"</jc>
4708    * </p>
4709    *
4710    * @param str The string to get characters from.
4711    * @param pos The starting position (0-based).
4712    * @param len The number of characters to get.
4713    * @return The middle characters, or <jk>null</jk> if input is <jk>null</jk>.
4714    */
4715   public static String mid(String str, int pos, int len) {
4716      if (str == null)
4717         return null;
4718      if (pos < 0 || len < 0)
4719         return "";
4720      if (pos >= str.length())
4721         return "";
4722      int end = Math.min(pos + len, str.length());
4723      return str.substring(pos, end);
4724   }
4725
4726   /**
4727    * Finds the most frequent character in a string.
4728    *
4729    * <p>
4730    * Returns the character that appears most often. If multiple characters have the same
4731    * frequency, returns the first one encountered.
4732    *
4733    * <h4 class='section'>Example:</h4>
4734    * <p class='bjava'>
4735    *    mostFrequentChar(<js>"hello"</js>);     <jc>// 'l'</jc>
4736    *    mostFrequentChar(<js>"aabbcc"</js>);    <jc>// 'a' (first encountered)</jc>
4737    * </p>
4738    *
4739    * @param str The string to analyze. Can be <jk>null</jk>.
4740    * @return The most frequent character, or <c>'\0'</c> if the string is <jk>null</jk> or empty.
4741    */
4742   public static char mostFrequentChar(String str) {
4743      if (isEmpty(str))
4744         return '\0';
4745
4746      var charCounts = new int[Character.MAX_VALUE + 1];
4747      var maxCount = 0;
4748      var maxChar = '\0';
4749
4750      // Count occurrences of each character
4751      for (var i = 0; i < str.length(); i++) {
4752         var c = str.charAt(i);
4753         charCounts[c]++;
4754         if (charCounts[c] > maxCount) {
4755            maxCount = charCounts[c];
4756            maxChar = c;
4757         }
4758      }
4759
4760      return maxChar;
4761   }
4762
4763   /**
4764    * Performs natural string comparison that handles numbers correctly.
4765    *
4766    * <p>
4767    * Compares strings in a way that numbers are compared numerically rather than lexicographically.
4768    * For example, "file2.txt" comes before "file10.txt" in natural order.
4769    *
4770    * <h4 class='section'>Example:</h4>
4771    * <p class='bjava'>
4772    *    naturalCompare(<js>"file2.txt"</js>, <js>"file10.txt"</js>);   <jc>// negative (2 &lt; 10)</jc>
4773    *    naturalCompare(<js>"file10.txt"</js>, <js>"file2.txt"</js>);   <jc>// positive (10 &gt; 2)</jc>
4774    *    naturalCompare(<js>"file1.txt"</js>, <js>"file1.txt"</js>);    <jc>// 0 (equal)</jc>
4775    * </p>
4776    *
4777    * @param str1 The first string.  Can be <jk>null</jk> (a <jk>null</jk> str1 sorts before a non-<jk>null</jk> str2).
4778    * @param str2 The second string.  Can be <jk>null</jk> (a non-<jk>null</jk> str1 sorts after a <jk>null</jk> str2).
4779    * @return A negative integer, zero, or a positive integer as the first string is less than, equal to, or greater than the second.
4780    */
4781   public static int naturalCompare(String str1, String str2) {
4782      if (Objects.equals(str1, str2))
4783         return 0;
4784      if (str1 == null)
4785         return -1;
4786      if (str2 == null)
4787         return 1;
4788
4789      var len1 = str1.length();
4790      var len2 = str2.length();
4791      var i1 = 0;
4792      var i2 = 0;
4793
4794      while (i1 < len1 && i2 < len2) {
4795         var c1 = str1.charAt(i1);
4796         var c2 = str2.charAt(i2);
4797
4798         // If both are digits, compare numerically
4799         if (DIGIT.contains(c1) && DIGIT.contains(c2)) {
4800            // Skip leading zeros
4801            while (i1 < len1 && str1.charAt(i1) == '0')
4802               i1++;
4803            while (i2 < len2 && str2.charAt(i2) == '0')
4804               i2++;
4805
4806            // Find end of number sequences
4807            var end1 = i1;
4808            var end2 = i2;
4809            while (end1 < len1 && DIGIT.contains(str1.charAt(end1)))
4810               end1++;
4811            while (end2 < len2 && DIGIT.contains(str2.charAt(end2)))
4812               end2++;
4813
4814            // Compare lengths first (longer number is larger)
4815            var lenNum1 = end1 - i1;
4816            var lenNum2 = end2 - i2;
4817            if (lenNum1 != lenNum2)
4818               return lenNum1 - lenNum2;
4819
4820            // Same length, compare digit by digit
4821            for (var j = 0; j < lenNum1; j++) {
4822               var d1 = str1.charAt(i1 + j);
4823               var d2 = str2.charAt(i2 + j);
4824               if (d1 != d2)
4825                  return d1 - d2;
4826            }
4827
4828            i1 = end1;
4829            i2 = end2;
4830         } else {
4831            // Compare characters (case-insensitive)
4832            var cmp = Character.toLowerCase(c1) - Character.toLowerCase(c2);
4833            if (cmp != 0)
4834               return cmp;
4835            i1++;
4836            i2++;
4837         }
4838      }
4839
4840      return len1 - len2;
4841   }
4842
4843   /**
4844    * Normalizes Unicode characters in a string.
4845    *
4846    * <p>
4847    * Uses Unicode normalization form NFD (Canonical Decomposition).
4848    *
4849    * <h4 class='section'>Example:</h4>
4850    * <p class='bjava'>
4851    *    normalizeUnicode(<js>"café"</js>);
4852    *    <jc>// Normalized form</jc>
4853    * </p>
4854    *
4855    * @param str The string to normalize. Can be <jk>null</jk>.
4856    * @return The normalized string, or <jk>null</jk> if input is <jk>null</jk>.
4857    */
4858   public static String normalizeUnicode(String str) {
4859      if (str == null)
4860         return null;
4861      return Normalizer.normalize(str, Normalizer.Form.NFD);
4862   }
4863
4864   /**
4865    * Normalizes all whitespace in a string to single spaces.
4866    *
4867    * <h4 class='section'>Example:</h4>
4868    * <p class='bjava'>
4869    *    normalizeWhitespace(<js>"hello  \t\n  world"</js>);   <jc>// "hello world"</jc>
4870    *    normalizeWhitespace(<js>"  hello  world  "</js>);     <jc>// "hello world"</jc>
4871    * </p>
4872    *
4873    * @param str The string to normalize.
4874    * @return The normalized string, or <jk>null</jk> if input is <jk>null</jk>.
4875    */
4876   public static String normalizeWhitespace(String str) {
4877      if (str == null)
4878         return null;
4879      return str.replaceAll("\\s+", " ").trim();
4880   }
4881
4882   /**
4883    * Checks if a string does not contain the specified character.
4884    *
4885    * <p>
4886    * This is the inverse of {@link #contains(String, char)}.
4887    * Returns <jk>true</jk> if the string is <jk>null</jk> or does not contain the character.
4888    *
4889    * <h4 class='section'>Example:</h4>
4890    * <p class='bjava'>
4891    *    notContains(<js>"Hello World"</js>, <js>'x'</js>);   <jc>// true</jc>
4892    *    notContains(<js>"Hello World"</js>, <js>'o'</js>);   <jc>// false</jc>
4893    *    notContains(<jk>null</jk>, <js>'a'</js>);            <jc>// true</jc>
4894    * </p>
4895    *
4896    * @param s The string to check.
4897    * @param c The character to check for.
4898    * @return <jk>true</jk> if the string does not contain the specified character.
4899    * @see #contains(String, char)
4900    * @see #notContainsAny(String, char...)
4901    */
4902   public static boolean notContains(String s, char c) {
4903      return ! contains(s, c);
4904   }
4905
4906   /**
4907    * Checks if a string does not contain the specified substring.
4908    *
4909    * <p>
4910    * This is the inverse of {@link #contains(String, CharSequence)}.
4911    * Returns <jk>true</jk> if the string is <jk>null</jk> or does not contain the substring.
4912    *
4913    * <h4 class='section'>Example:</h4>
4914    * <p class='bjava'>
4915    *    notContains(<js>"Hello World"</js>, <js>"Foo"</js>);     <jc>// true</jc>
4916    *    notContains(<js>"Hello World"</js>, <js>"World"</js>);   <jc>// false</jc>
4917    *    notContains(<jk>null</jk>, <js>"Hello"</js>);            <jc>// true</jc>
4918    * </p>
4919    *
4920    * @param s The string to check.
4921    * @param substring The substring to check for.
4922    * @return <jk>true</jk> if the string does not contain the specified substring.
4923    * @see #contains(String, CharSequence)
4924    * @see #notContainsAny(String, CharSequence...)
4925    */
4926   public static boolean notContains(String s, CharSequence substring) {
4927      return ! contains(s, substring);
4928   }
4929
4930   /**
4931    * Checks if a string does not contain the specified substring.
4932    *
4933    * <p>
4934    * This is the inverse of {@link #contains(String, String)}.
4935    * Returns <jk>true</jk> if the string is <jk>null</jk> or does not contain the substring.
4936    *
4937    * <h4 class='section'>Example:</h4>
4938    * <p class='bjava'>
4939    *    notContains(<js>"Hello World"</js>, <js>"Foo"</js>);     <jc>// true</jc>
4940    *    notContains(<js>"Hello World"</js>, <js>"World"</js>);   <jc>// false</jc>
4941    *    notContains(<jk>null</jk>, <js>"Hello"</js>);            <jc>// true</jc>
4942    * </p>
4943    *
4944    * @param s The string to check.
4945    * @param substring The substring to check for.
4946    * @return <jk>true</jk> if the string does not contain the specified substring.
4947    * @see #contains(String, String)
4948    * @see #notContainsAny(String, String...)
4949    */
4950   public static boolean notContains(String s, String substring) {
4951      return ! contains(s, substring);
4952   }
4953
4954   /**
4955    * Checks if a string does not contain all of the specified characters.
4956    *
4957    * <p>
4958    * This is the inverse of {@link #containsAll(String, char...)}.
4959    * Returns <jk>true</jk> if:
4960    * <ul>
4961    *   <li>The string is <jk>null</jk></li>
4962    *   <li>The values array is <jk>null</jk> or empty</li>
4963    *   <li>Any of the specified characters are not found in the string</li>
4964    * </ul>
4965    *
4966    * <h4 class='section'>Example:</h4>
4967    * <p class='bjava'>
4968    *    notContainsAll(<js>"Hello World"</js>, <js>'H'</js>, <js>'x'</js>);                <jc>// true (missing 'x')</jc>
4969    *    notContainsAll(<js>"Hello World"</js>, <js>'H'</js>, <js>'e'</js>, <js>'l'</js>);   <jc>// false (contains all)</jc>
4970    *    notContainsAll(<jk>null</jk>, <js>'a'</js>);                                      <jc>// true</jc>
4971    * </p>
4972    *
4973    * @param s The string to check.
4974    * @param values The characters to check for.
4975    * @return <jk>true</jk> if the string does not contain all of the specified characters.
4976    * @see #containsAll(String, char...)
4977    */
4978   public static boolean notContainsAll(String s, char...values) {
4979      return ! containsAll(s, values);
4980   }
4981
4982   /**
4983    * Checks if a string does not contain all of the specified substrings.
4984    *
4985    * <p>
4986    * This is the inverse of {@link #containsAll(String, CharSequence...)}.
4987    * Returns <jk>true</jk> if:
4988    * <ul>
4989    *   <li>The string is <jk>null</jk></li>
4990    *   <li>The values array is <jk>null</jk> or empty</li>
4991    *   <li>Any of the specified substrings are not found in the string</li>
4992    * </ul>
4993    *
4994    * <h4 class='section'>Example:</h4>
4995    * <p class='bjava'>
4996    *    notContainsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);    <jc>// true (missing "Foo")</jc>
4997    *    notContainsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"World"</js>);   <jc>// false (contains all)</jc>
4998    *    notContainsAll(<jk>null</jk>, <js>"Hello"</js>);                              <jc>// true</jc>
4999    * </p>
5000    *
5001    * @param s The string to check.
5002    * @param values The substrings to check for.
5003    * @return <jk>true</jk> if the string does not contain all of the specified substrings.
5004    * @see #containsAll(String, CharSequence...)
5005    */
5006   public static boolean notContainsAll(String s, CharSequence...values) {
5007      return ! containsAll(s, values);
5008   }
5009
5010   /**
5011    * Checks if a string does not contain all of the specified substrings.
5012    *
5013    * <p>
5014    * This is the inverse of {@link #containsAll(String, String...)}.
5015    * Returns <jk>true</jk> if:
5016    * <ul>
5017    *   <li>The string is <jk>null</jk></li>
5018    *   <li>The values array is <jk>null</jk> or empty</li>
5019    *   <li>Any of the specified substrings are not found in the string</li>
5020    * </ul>
5021    *
5022    * <h4 class='section'>Example:</h4>
5023    * <p class='bjava'>
5024    *    notContainsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);    <jc>// true (missing "Foo")</jc>
5025    *    notContainsAll(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"World"</js>);   <jc>// false (contains all)</jc>
5026    *    notContainsAll(<jk>null</jk>, <js>"Hello"</js>);                              <jc>// true</jc>
5027    * </p>
5028    *
5029    * @param s The string to check.
5030    * @param values The substrings to check for.
5031    * @return <jk>true</jk> if the string does not contain all of the specified substrings.
5032    * @see #containsAll(String, String...)
5033    */
5034   public static boolean notContainsAll(String s, String...values) {
5035      return ! containsAll(s, values);
5036   }
5037
5038   /**
5039    * Checks if a string does not contain any of the specified characters.
5040    *
5041    * <p>
5042    * This is the inverse of {@link #containsAny(String, char...)}.
5043    * Returns <jk>true</jk> if:
5044    * <ul>
5045    *   <li>The string is <jk>null</jk></li>
5046    *   <li>The values array is <jk>null</jk> or empty</li>
5047    *   <li>None of the specified characters are found in the string</li>
5048    * </ul>
5049    *
5050    * <h4 class='section'>Example:</h4>
5051    * <p class='bjava'>
5052    *    notContainsAny(<js>"Hello World"</js>, <js>'x'</js>, <js>'y'</js>);   <jc>// true</jc>
5053    *    notContainsAny(<js>"Hello World"</js>, <js>'o'</js>, <js>'x'</js>);   <jc>// false (contains 'o')</jc>
5054    *    notContainsAny(<jk>null</jk>, <js>'a'</js>);                          <jc>// true</jc>
5055    * </p>
5056    *
5057    * @param s The string to check.
5058    * @param values The characters to check for.
5059    * @return <jk>true</jk> if the string does not contain any of the specified characters.
5060    * @see #containsAny(String, char...)
5061    * @see #notContainsAll(String, char...)
5062    */
5063   public static boolean notContainsAny(String s, char...values) {
5064      return ! containsAny(s, values);
5065   }
5066
5067   /**
5068    * Checks if a string does not contain any of the specified substrings.
5069    *
5070    * <p>
5071    * This is the inverse of {@link #containsAny(String, CharSequence...)}.
5072    * Returns <jk>true</jk> if:
5073    * <ul>
5074    *   <li>The string is <jk>null</jk></li>
5075    *   <li>The values array is <jk>null</jk> or empty</li>
5076    *   <li>None of the specified substrings are found in the string</li>
5077    * </ul>
5078    *
5079    * <h4 class='section'>Example:</h4>
5080    * <p class='bjava'>
5081    *    notContainsAny(<js>"Hello World"</js>, <js>"Foo"</js>, <js>"Bar"</js>);    <jc>// true</jc>
5082    *    notContainsAny(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);  <jc>// false (contains "Hello")</jc>
5083    *    notContainsAny(<jk>null</jk>, <js>"Hello"</js>);                            <jc>// true</jc>
5084    * </p>
5085    *
5086    * @param s The string to check.
5087    * @param values The substrings to check for.
5088    * @return <jk>true</jk> if the string does not contain any of the specified substrings.
5089    * @see #containsAny(String, CharSequence...)
5090    * @see #notContainsAll(String, CharSequence...)
5091    */
5092   public static boolean notContainsAny(String s, CharSequence...values) {
5093      return ! containsAny(s, values);
5094   }
5095
5096   /**
5097    * Checks if a string does not contain any of the specified substrings.
5098    *
5099    * <p>
5100    * This is the inverse of {@link #containsAny(String, String...)}.
5101    * Returns <jk>true</jk> if:
5102    * <ul>
5103    *   <li>The string is <jk>null</jk></li>
5104    *   <li>The values array is <jk>null</jk> or empty</li>
5105    *   <li>None of the specified substrings are found in the string</li>
5106    * </ul>
5107    *
5108    * <h4 class='section'>Example:</h4>
5109    * <p class='bjava'>
5110    *    notContainsAny(<js>"Hello World"</js>, <js>"Foo"</js>, <js>"Bar"</js>);    <jc>// true</jc>
5111    *    notContainsAny(<js>"Hello World"</js>, <js>"Hello"</js>, <js>"Foo"</js>);  <jc>// false (contains "Hello")</jc>
5112    *    notContainsAny(<jk>null</jk>, <js>"Hello"</js>);                            <jc>// true</jc>
5113    * </p>
5114    *
5115    * @param s The string to check.
5116    * @param values The substrings to check for.
5117    * @return <jk>true</jk> if the string does not contain any of the specified substrings.
5118    * @see #containsAny(String, String...)
5119    * @see #notContainsAll(String, String...)
5120    */
5121   public static boolean notContainsAny(String s, String...values) {
5122      return ! containsAny(s, values);
5123   }
5124
5125   /**
5126    * Returns an obfuscated version of the specified string.
5127    *
5128    * @param s The string to obfuscate.  Can be <jk>null</jk> (returns <js>"*"</js>).
5129    * @return The obfuscated string with most characters replaced by asterisks.
5130    */
5131   public static String obfuscate(String s) {
5132      if (s == null || s.length() < 2)
5133         return "*";
5134      return s.substring(0, 1) + s.substring(1).replaceAll(".", "*");  // NOSONAR - Dot matches any character, which is intended
5135   }
5136
5137   /**
5138    * Converts a number to its ordinal form (1st, 2nd, 3rd, 4th, etc.).
5139    *
5140    * <h4 class='section'>Example:</h4>
5141    * <p class='bjava'>
5142    *    ordinal(<js>1</js>);     <jc>// "1st"</jc>
5143    *    ordinal(<js>2</js>);     <jc>// "2nd"</jc>
5144    *    ordinal(<js>3</js>);     <jc>// "3rd"</jc>
5145    *    ordinal(<js>4</js>);     <jc>// "4th"</jc>
5146    *    ordinal(<js>11</js>);    <jc>// "11th"</jc>
5147    *    ordinal(<js>21</js>);    <jc>// "21st"</jc>
5148    * </p>
5149    *
5150    * @param number The number to convert.
5151    * @return The ordinal string representation of the number.
5152    */
5153   public static String ordinal(int number) {
5154      var abs = Math.abs(number);
5155      var suffix = "th";
5156
5157      // Special cases for 11, 12, 13 (all use "th")
5158      if (abs % 100 != 11 && abs % 100 != 12 && abs % 100 != 13) {
5159         var lastDigit = abs % 10;
5160         if (lastDigit == 1)
5161            suffix = "st";
5162         else if (lastDigit == 2)
5163            suffix = "nd";
5164         else if (lastDigit == 3)
5165            suffix = "rd";
5166      }
5167
5168      return number + suffix;
5169   }
5170
5171   /**
5172    * Center pads a string with a specified character.
5173    *
5174    * <h4 class='section'>Example:</h4>
5175    * <p class='bjava'>
5176    *    padCenter(<jk>null</jk>, 5, <js>' '</js>);          <jc>// "     "</jc>
5177    *    padCenter(<js>""</js>, 5, <js>' '</js>);            <jc>// "     "</jc>
5178    *    padCenter(<js>"hi"</js>, 6, <js>' '</js>);          <jc>// "  hi  "</jc>
5179    *    padCenter(<js>"hi"</js>, 7, <js>' '</js>);          <jc>// "   hi  "</jc>
5180    *    padCenter(<js>"hello"</js>, 3, <js>' '</js>);       <jc>// "hello"</jc>
5181    * </p>
5182    *
5183    * @param str The string to pad.
5184    * @param size The desired total string length.
5185    * @param padChar The character to pad with.
5186    * @return The center-padded string.
5187    */
5188   public static String padCenter(String str, int size, char padChar) {
5189      if (str == null)
5190         str = "";
5191      int pads = size - str.length();
5192      if (pads <= 0)
5193         return str;
5194      int rightPads = pads / 2;
5195      int leftPads = pads - rightPads;
5196      return String.valueOf(padChar).repeat(leftPads) + str + String.valueOf(padChar).repeat(rightPads);
5197   }
5198
5199   /**
5200    * Left pads a string with a specified character.
5201    *
5202    * <h4 class='section'>Example:</h4>
5203    * <p class='bjava'>
5204    *    padLeft(<jk>null</jk>, 5, <js>' '</js>);          <jc>// "     "</jc>
5205    *    padLeft(<js>""</js>, 5, <js>' '</js>);            <jc>// "     "</jc>
5206    *    padLeft(<js>"hello"</js>, 8, <js>' '</js>);       <jc>// "   hello"</jc>
5207    *    padLeft(<js>"hello"</js>, 3, <js>' '</js>);       <jc>// "hello"</jc>
5208    *    padLeft(<js>"123"</js>, 5, <js>'0'</js>);         <jc>// "00123"</jc>
5209    * </p>
5210    *
5211    * @param str The string to pad.
5212    * @param size The desired total string length.
5213    * @param padChar The character to pad with.
5214    * @return The left-padded string.
5215    */
5216   public static String padLeft(String str, int size, char padChar) {
5217      if (str == null)
5218         str = "";
5219      int pads = size - str.length();
5220      if (pads <= 0)
5221         return str;
5222      return String.valueOf(padChar).repeat(pads) + str;
5223   }
5224
5225   /**
5226    * Right pads a string with a specified character.
5227    *
5228    * <h4 class='section'>Example:</h4>
5229    * <p class='bjava'>
5230    *    padRight(<jk>null</jk>, 5, <js>' '</js>);          <jc>// "     "</jc>
5231    *    padRight(<js>""</js>, 5, <js>' '</js>);            <jc>// "     "</jc>
5232    *    padRight(<js>"hello"</js>, 8, <js>' '</js>);       <jc>// "hello   "</jc>
5233    *    padRight(<js>"hello"</js>, 3, <js>' '</js>);       <jc>// "hello"</jc>
5234    *    padRight(<js>"123"</js>, 5, <js>'0'</js>);         <jc>// "12300"</jc>
5235    * </p>
5236    *
5237    * @param str The string to pad.
5238    * @param size The desired total string length.
5239    * @param padChar The character to pad with.
5240    * @return The right-padded string.
5241    */
5242   public static String padRight(String str, int size, char padChar) {
5243      if (str == null)
5244         str = "";
5245      int pads = size - str.length();
5246      if (pads <= 0)
5247         return str;
5248      return str + String.valueOf(padChar).repeat(pads);
5249   }
5250
5251   /**
5252    * Converts a <c>String</c> to a <c>Character</c>
5253    *
5254    * @param o The string to convert.
5255    * @return The first character of the string if the string is of length 1, or <jk>null</jk> if the string is <jk>null</jk> or empty.
5256    * @throws IllegalArgumentException If the string length is not 1.
5257    */
5258   public static Character parseCharacter(Object o) {
5259      if (o == null)
5260         return null;
5261      var s = o.toString();
5262      if (s.isEmpty())
5263         return null;
5264      if (s.length() == 1)
5265         return s.charAt(0);
5266      throw iaex("Invalid character: '%s'", s);
5267   }
5268
5269   /**
5270    * Same as {@link Float#parseFloat(String)} but removes any underscore characters first.
5271    *
5272    * <p>Allows for better readability of numeric literals (e.g., <js>"1_000.5"</js>).
5273    *
5274    * @param value The string to parse.
5275    * @return The parsed float value.
5276    * @throws NumberFormatException If the string cannot be parsed.
5277    * @throws NullPointerException If the string is <jk>null</jk>.
5278    */
5279   public static float parseFloat(String value) {
5280      return Float.parseFloat(StringUtils.removeUnderscores(value));
5281   }
5282
5283   /**
5284    * Same as {@link Integer#parseInt(String)} but removes any underscore characters first.
5285    *
5286    * <p>Allows for better readability of numeric literals (e.g., <js>"1_000_000"</js>).
5287    *
5288    * @param value The string to parse.
5289    * @return The parsed integer value.
5290    * @throws NumberFormatException If the string cannot be parsed.
5291    * @throws NullPointerException If the string is <jk>null</jk>.
5292    */
5293   public static int parseInt(String value) {
5294      return Integer.parseInt(StringUtils.removeUnderscores(value));
5295   }
5296
5297   /**
5298    * Same as {@link #parseInt(String)} but converts a {@link NumberFormatException} into a caller-supplied exception.
5299    *
5300    * <p>
5301    * Lets callers wrap the parse failure with a domain-specific error (typically an {@link IllegalArgumentException})
5302    * without having to write a {@code try}/{@code catch} block at the call site.
5303    *
5304    * <h4 class='section'>Example:</h4>
5305    * <p class='bjava'>
5306    *    <jk>int</jk> <jv>port</jv> = StringUtils.<jsm>parseInt</jsm>(<jv>value</jv>,
5307    *       ()-&gt; <jsm>illegalArg</jsm>(<js>"Invalid port: {0}"</js>, <jv>value</jv>));
5308    * </p>
5309    *
5310    * @param <T> The exception type produced by the supplier.
5311    * @param value The string to parse.
5312    * @param errorSupplier Supplier of the exception to throw if the string cannot be parsed.
5313    * @return The parsed integer value.
5314    * @throws T If the string cannot be parsed.
5315    * @throws NullPointerException If the string is <jk>null</jk>.
5316    */
5317   public static <T extends RuntimeException> int parseInt(String value, Supplier<T> errorSupplier) {
5318      try {
5319         return parseInt(value);
5320      } catch (@SuppressWarnings("unused") NumberFormatException e) {
5321         throw errorSupplier.get();
5322      }
5323   }
5324
5325   /**
5326    * Converts a string containing a possible multiplier suffix to an integer.
5327    *
5328    * <p>
5329    * The string can contain any of the following multiplier suffixes:
5330    * <ul>
5331    *    <li><js>"K"</js> - x 1024
5332    *    <li><js>"M"</js> - x 1024*1024
5333    *    <li><js>"G"</js> - x 1024*1024*1024
5334    *    <li><js>"k"</js> - x 1000
5335    *    <li><js>"m"</js> - x 1000*1000
5336    *    <li><js>"g"</js> - x 1000*1000*1000
5337    * </ul>
5338    *
5339    * @param s The string to parse.  Must not be <jk>null</jk>.
5340    * @return The parsed value.
5341    * @throws IllegalArgumentException If the string is <jk>null</jk>.
5342    */
5343   public static int parseIntWithSuffix(String s) {
5344      reqnn("s", s);
5345      var m = multiplierInt(s);
5346      if (m == 1)
5347         return Integer.decode(s);
5348      return Integer.decode(s.substring(0, s.length() - 1).trim()) * m;
5349   }
5350
5351   /**
5352    * Same as {@link #parseIntWithSuffix(String)} but converts a {@link NumberFormatException} into a caller-supplied exception.
5353    *
5354    * <p>
5355    * Lets callers wrap the parse failure with a domain-specific error (typically an {@link IllegalArgumentException})
5356    * without having to write a {@code try}/{@code catch} block at the call site.
5357    *
5358    * @param <T> The exception type produced by the supplier.
5359    * @param value The string to parse.
5360    * @param errorSupplier Supplier of the exception to throw if the string cannot be parsed.
5361    * @return The parsed integer value.
5362    * @throws T If the string cannot be parsed.
5363    * @throws IllegalArgumentException If the string is <jk>null</jk>.
5364    */
5365   public static <T extends RuntimeException> int parseIntWithSuffix(String value, Supplier<T> errorSupplier) {
5366      try {
5367         return parseIntWithSuffix(value);
5368      } catch (@SuppressWarnings("unused") NumberFormatException e) {
5369         throw errorSupplier.get();
5370      }
5371   }
5372
5373   /**
5374    * Same as {@link Long#parseLong(String)} but removes any underscore characters first.
5375    *
5376    * <p>Allows for better readability of numeric literals (e.g., <js>"1_000_000"</js>).
5377    *
5378    * @param value The string to parse.
5379    * @return The parsed long value.
5380    * @throws NumberFormatException If the string cannot be parsed.
5381    * @throws NullPointerException If the string is <jk>null</jk>.
5382    */
5383   public static long parseLong(String value) {
5384      return Long.parseLong(StringUtils.removeUnderscores(value));
5385   }
5386
5387   /**
5388    * Same as {@link #parseLong(String)} but converts a {@link NumberFormatException} into a caller-supplied exception.
5389    *
5390    * <p>
5391    * Lets callers wrap the parse failure with a domain-specific error (typically an {@link IllegalArgumentException})
5392    * without having to write a {@code try}/{@code catch} block at the call site.
5393    *
5394    * <h4 class='section'>Example:</h4>
5395    * <p class='bjava'>
5396    *    <jk>long</jk> <jv>size</jv> = StringUtils.<jsm>parseLong</jsm>(<jv>value</jv>,
5397    *       ()-&gt; <jsm>illegalArg</jsm>(<js>"Invalid size: {0}"</js>, <jv>value</jv>));
5398    * </p>
5399    *
5400    * @param <T> The exception type produced by the supplier.
5401    * @param value The string to parse.
5402    * @param errorSupplier Supplier of the exception to throw if the string cannot be parsed.
5403    * @return The parsed long value.
5404    * @throws T If the string cannot be parsed.
5405    * @throws NullPointerException If the string is <jk>null</jk>.
5406    */
5407   public static <T extends RuntimeException> long parseLong(String value, Supplier<T> errorSupplier) {
5408      try {
5409         return parseLong(value);
5410      } catch (@SuppressWarnings("unused") NumberFormatException e) {
5411         throw errorSupplier.get();
5412      }
5413   }
5414
5415   /**
5416    * Converts a string containing a possible multiplier suffix to a long.
5417    *
5418    * <p>
5419    * The string can contain any of the following multiplier suffixes:
5420    * <ul>
5421    *    <li><js>"K"</js> - x 1024
5422    *    <li><js>"M"</js> - x 1024*1024
5423    *    <li><js>"G"</js> - x 1024*1024*1024
5424    *    <li><js>"T"</js> - x 1024*1024*1024*1024
5425    *    <li><js>"P"</js> - x 1024*1024*1024*1024*1024
5426    *    <li><js>"k"</js> - x 1000
5427    *    <li><js>"m"</js> - x 1000*1000
5428    *    <li><js>"g"</js> - x 1000*1000*1000
5429    *    <li><js>"t"</js> - x 1000*1000*1000*1000
5430    *    <li><js>"p"</js> - x 1000*1000*1000*1000*1000
5431    * </ul>
5432    *
5433    * @param s The string to parse.  Must not be <jk>null</jk>.
5434    * @return The parsed value.
5435    * @throws IllegalArgumentException If the string is <jk>null</jk>.
5436    */
5437   public static long parseLongWithSuffix(String s) {
5438      reqnn("s", s);
5439      var m = multiplierLong(s);
5440      if (m == 1) {
5441         // If multiplier is 1, try to decode the whole string
5442         // This will throw NumberFormatException if it contains a suffix character
5443         return Long.decode(s);
5444      }
5445      var baseStr = s.substring(0, s.length() - 1).trim();
5446      var base = Long.decode(baseStr);
5447      try {
5448         return Math.multiplyExact(base, m);
5449      } catch (@SuppressWarnings("unused") ArithmeticException e) {
5450         throw new NumberFormatException("Value " + s + " exceeds Long.MAX_VALUE");
5451      }
5452   }
5453
5454   /**
5455    * Parses a string containing key-value pairs into a map.
5456    *
5457    * <p>
5458    * Splits the string by the entry delimiter, then splits each entry by the key-value delimiter.
5459    *
5460    * <h4 class='section'>Example:</h4>
5461    * <p class='bjava'>
5462    *    parseMap(<js>"key1=value1,key2=value2"</js>, <js>'='</js>, <js>','</js>, <jk>false</jk>);
5463    *    <jc>// {"key1":"value1","key2":"value2"}</jc>
5464    *    parseMap(<js>" key1 = value1 ; key2 = value2 "</js>, <js>'='</js>, <js>';'</js>, <jk>true</jk>);
5465    *    <jc>// {"key1":"value1","key2":"value2"}</jc>
5466    * </p>
5467    *
5468    * @param str The string to parse. Can be <jk>null</jk>.
5469    * @param keyValueDelimiter The character that separates keys from values.
5470    * @param entryDelimiter The character that separates entries.
5471    * @param trimKeys If <jk>true</jk>, trims whitespace from keys and values.
5472    * @return A map containing the parsed key-value pairs, or an empty map if the string is <jk>null</jk> or empty.
5473    */
5474   public static Map<String,String> parseMap(String str, char keyValueDelimiter, char entryDelimiter, boolean trimKeys) {
5475      var result = new LinkedHashMap<String,String>();
5476      if (isEmpty(str))
5477         return result;
5478
5479      var entries = split(str, entryDelimiter);
5480      for (var entry : entries) {
5481         if (isEmpty(entry))
5482            continue;
5483         var delimiterIndex = entry.indexOf(keyValueDelimiter);
5484         if (delimiterIndex == -1) {
5485            // No delimiter found, treat entire entry as key with empty value
5486            var key = trimKeys ? entry.trim() : entry;
5487            result.put(key, "");
5488         } else {
5489            var key = entry.substring(0, delimiterIndex);
5490            var value = entry.substring(delimiterIndex + 1);
5491            if (trimKeys) {
5492               key = key.trim();
5493               value = value.trim();
5494            }
5495            result.put(key, value);
5496         }
5497      }
5498      return result;
5499   }
5500
5501   /** Maximum length of a decimal string accepted by {@link #parseBigDecimal(String)}. */
5502   private static final int MAX_BIG_DECIMAL_LENGTH = 1000;
5503
5504   /** Maximum magnitude of the decimal exponent accepted by {@link #parseBigDecimal(String)}. */
5505   private static final int MAX_BIG_DECIMAL_EXPONENT = 10_000;
5506
5507   /**
5508    * Parses a decimal string into a {@link BigDecimal}, rejecting values that would be expensive to process.
5509    *
5510    * <p>
5511    * Strings longer than 1,000 characters, and values whose decimal exponent is larger than 10,000 in magnitude
5512    * (e.g. <js>"1e100000"</js>), are rejected.
5513    *
5514    * @param s The string to parse.  Must not be <jk>null</jk>.
5515    * @return The parsed value.
5516    * @throws NumberFormatException If the string is not a valid decimal, is too long, or has an exponent that is too large.
5517    */
5518   public static BigDecimal parseBigDecimal(String s) {
5519      if (s.length() > MAX_BIG_DECIMAL_LENGTH)
5520         throw new NumberFormatException("Decimal value is too long.");
5521      var bd = new BigDecimal(s);
5522      if (Math.abs((long)bd.precision() - bd.scale() - 1) > MAX_BIG_DECIMAL_EXPONENT)
5523         throw new NumberFormatException("Decimal exponent is out of range.");
5524      return bd;
5525   }
5526
5527   /**
5528    * Parses a number from the specified string.
5529    *
5530    * <p>
5531    * Supports Java 7+ numeric literals with underscores (e.g., <js>"1_000_000"</js>).
5532    * The underscores are automatically removed before parsing.
5533    *
5534    * @param s The string to parse the number from.
5535    * @param type
5536    *    The number type to created.
5537    *    Can be any of the following:
5538    *    <ul>
5539    *       <li> Integer (or <c>int</c> primitive)
5540    *       <li> Long (or <c>long</c> primitive)
5541    *       <li> Short (or <c>short</c> primitive)
5542    *       <li> Byte (or <c>byte</c> primitive)
5543    *       <li> Float (or <c>float</c> primitive)
5544    *       <li> Double (or <c>double</c> primitive)
5545    *       <li> BigInteger
5546    *       <li> BigDecimal
5547    *       <li> AtomicInteger
5548    *       <li> AtomicLong
5549    *    </ul>
5550    *    If <jk>null</jk> or <c>Number</c>, uses the best guess.
5551    * @return The parsed number, or <jk>null</jk> if the string was null.
5552    */
5553   public static Number parseNumber(String s, Class<? extends Number> type) {
5554      if (s == null)
5555         return null;
5556      if (s.isEmpty())
5557         s = "0";
5558      if (type == null)
5559         type = Number.class;
5560
5561      // Remove underscores (Java 7+ numeric literal support) before parsing
5562      // Note: We do this before type detection to ensure clean parsing
5563      s = s.replace("_", "");
5564
5565      // Determine the data type if it wasn't specified.
5566      var isAutoDetect = (type == Number.class);
5567      var isDecimal = false;
5568      if (isAutoDetect) {
5569         // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how
5570         // long the string is.
5571         // An integer range is -2,147,483,648 to 2,147,483,647
5572         // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
5573         isDecimal = isDecimal(s);
5574         if (isDecimal) {
5575            if (s.length() > 20)
5576               type = Double.class;
5577            else if (s.length() >= 10)
5578               type = Long.class;
5579            else
5580               type = Integer.class;
5581         } else if (isFloat(s))
5582            type = Double.class;
5583         else
5584            throw new NumberFormatException(s);
5585      }
5586
5587      if (type == Double.class || type == Double.TYPE) {
5588         return Double.valueOf(s);
5589      }
5590      if (type == Float.class || type == Float.TYPE)
5591         return Float.valueOf(s);
5592      if (type == BigDecimal.class)
5593         return parseBigDecimal(s);
5594      if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) {
5595         try {
5596            var l = parseLongWithSuffix(s);
5597            if (type == AtomicLong.class)
5598               return new AtomicLong(l);
5599            if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) {
5600               // This occurs if the string is 10 characters long but is still a valid integer value.
5601               return (int)l;
5602            }
5603            return l;
5604         } catch (NumberFormatException e) {
5605            if (isAutoDetect) {
5606               // This occurs if the string is 20 characters long but still falls outside the range of a valid long.
5607               return Double.valueOf(s);
5608            }
5609            throw e;
5610         }
5611      }
5612      if (type == Integer.class || type == Integer.TYPE)
5613         return Integer.decode(s);
5614      if (type == Short.class || type == Short.TYPE)
5615         return Short.decode(s);
5616      if (type == Byte.class || type == Byte.TYPE)
5617         return Byte.decode(s);
5618      if (type == BigInteger.class)
5619         return new BigInteger(s);
5620      if (type == AtomicInteger.class)
5621         return new AtomicInteger(Integer.decode(s));
5622      throw new NumberFormatException("Unsupported Number type: " + type.getName());
5623   }
5624
5625   /**
5626    * Converts a string to PascalCase format.
5627    *
5628    * <p>
5629    * Handles various input formats:
5630    * <ul>
5631    *   <li>Space-separated: "hello world" → "HelloWorld"</li>
5632    *   <li>CamelCase: "helloWorld" → "HelloWorld"</li>
5633    *   <li>Snake_case: "hello_world" → "HelloWorld"</li>
5634    *   <li>Kebab-case: "hello-world" → "HelloWorld"</li>
5635    * </ul>
5636    *
5637    * <h4 class='section'>Example:</h4>
5638    * <p class='bjava'>
5639    *    pascalCase(<jk>null</jk>);                    <jc>// null</jc>
5640    *    pascalCase(<js>""</js>);                      <jc>// ""</jc>
5641    *    pascalCase(<js>"hello world"</js>);           <jc>// "HelloWorld"</jc>
5642    *    pascalCase(<js>"helloWorld"</js>);            <jc>// "HelloWorld"</jc>
5643    *    pascalCase(<js>"hello_world"</js>);           <jc>// "HelloWorld"</jc>
5644    *    pascalCase(<js>"hello-world"</js>);           <jc>// "HelloWorld"</jc>
5645    * </p>
5646    *
5647    * @param str The string to convert.
5648    * @return The PascalCase string, or <jk>null</jk> if input is <jk>null</jk>.
5649    */
5650   public static String pascalCase(String str) {
5651      if (isEmpty(str))
5652         return str;
5653
5654      var words = splitWords(str);
5655      if (words.isEmpty())
5656         return "";
5657
5658      var result = new StringBuilder();
5659      for (var word : words) {
5660         result.append(capitalize(lowerCaseRoot(word)));
5661      }
5662
5663      return result.toString();
5664   }
5665
5666   /**
5667    * Pluralizes a word based on a count.
5668    *
5669    * <p>
5670    * Simple pluralization that adds "s" to most words, with basic rules for words ending in "s", "x", "z", "ch", "sh" (add "es"),
5671    * and words ending in "y" preceded by a consonant (replace "y" with "ies").
5672    *
5673    * <h4 class='section'>Example:</h4>
5674    * <p class='bjava'>
5675    *    pluralize(<js>"cat"</js>, <js>1</js>);        <jc>// "cat"</jc>
5676    *    pluralize(<js>"cat"</js>, <js>2</js>);        <jc>// "cats"</jc>
5677    *    pluralize(<js>"box"</js>, <js>2</js>);        <jc>// "boxes"</jc>
5678    *    pluralize(<js>"city"</js>, <js>2</js>);       <jc>// "cities"</jc>
5679    * </p>
5680    *
5681    * @param word The word to pluralize.  Can be <jk>null</jk> (returned as-is).
5682    * @param count The count to determine if pluralization is needed.
5683    * @return The pluralized word if count is not 1, otherwise the original word.
5684    */
5685   public static String pluralize(String word, int count) {
5686      if (word == null || word.isEmpty())
5687         return word;
5688      if (count == 1)
5689         return word;
5690
5691      var lower = lowerCaseRoot(word);
5692      var length = word.length();
5693
5694      // Words ending in s, x, z, ch, sh -> add "es"
5695      if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
5696         return word + "es";
5697      }
5698
5699      // Words ending in "y" preceded by a consonant -> replace "y" with "ies"
5700      if (length > 1 && lower.endsWith("y")) {
5701         var secondLast = lower.charAt(length - 2);
5702         if (! VOWEL.contains(secondLast)) {
5703            return word.substring(0, length - 1) + "ies";
5704         }
5705      }
5706
5707      // Words ending in "f" or "fe" -> replace with "ves" (basic rule)
5708      if (lower.endsWith("f")) {
5709         return word.substring(0, length - 1) + "ves";
5710      }
5711      if (lower.endsWith("fe")) {
5712         return word.substring(0, length - 2) + "ves";
5713      }
5714
5715      // Default: add "s"
5716      return word + "s";
5717   }
5718
5719   /**
5720    * Generated a random UUID with the specified number of characters.
5721    *
5722    * <p>
5723    * Characters are composed of lower-case ASCII letters and numbers only.
5724    *
5725    * <p>
5726    * This method conforms to the restrictions for hostnames as specified in <a class="doclink" href="https://tools.ietf.org/html/rfc952">RFC 952</a>
5727    * Since each character has 36 possible values, the square approximation formula for the number of generated IDs
5728    * that would produce a 50% chance of collision is:
5729    * <c>sqrt(36^N)</c>.
5730    * Dividing this number by 10 gives you an approximation of the number of generated IDs needed to produce a
5731    * &lt;1% chance of collision.
5732    *
5733    * <p>
5734    * For example, given 5 characters, the number of generated IDs need to produce a &lt;1% chance of collision would
5735    * be:
5736    * <c>sqrt(36^5)/10=777</c>
5737    *
5738    * @param numchars The number of characters in the generated UUID.
5739    * @return A new random UUID.
5740    */
5741   public static String random(int numchars) {
5742      var sb = new StringBuilder(numchars);
5743      for (var i = 0; i < numchars; i++) {
5744         var c = RANDOM.nextInt(36) + 97;
5745         if (c > 'z')
5746            c -= ('z' - '0' + 1);
5747         sb.append((char)c);
5748      }
5749      return sb.toString();
5750   }
5751
5752   /**
5753    * Generates a random alphabetic string of the specified length.
5754    *
5755    * <p>
5756    * Characters are composed of upper-case and lower-case ASCII letters (a-z, A-Z).
5757    *
5758    * <h4 class='section'>Example:</h4>
5759    * <p class='bjava'>
5760    *    randomAlphabetic(<jk>5</jk>);  <jc>// "aBcDe"</jc>
5761    * </p>
5762    *
5763    * @param length The length of the generated string.
5764    * @return A new random alphabetic string.
5765    */
5766   public static String randomAlphabetic(int length) {
5767      if (length < 0)
5768         throw iaex("Length must be non-negative: " + length);
5769      var sb = new StringBuilder(length);
5770      for (var i = 0; i < length; i++) {
5771         var c = RANDOM.nextInt(52);
5772         if (c < 26)
5773            sb.append((char)('a' + c));
5774         else
5775            sb.append((char)('A' + c - 26));
5776      }
5777      return sb.toString();
5778   }
5779
5780   /**
5781    * Generates a random alphanumeric string of the specified length.
5782    *
5783    * <p>
5784    * Characters are composed of upper-case and lower-case ASCII letters and digits (a-z, A-Z, 0-9).
5785    *
5786    * <h4 class='section'>Example:</h4>
5787    * <p class='bjava'>
5788    *    randomAlphanumeric(<jk>8</jk>);  <jc>// "aB3dE5fG"</jc>
5789    * </p>
5790    *
5791    * @param length The length of the generated string.
5792    * @return A new random alphanumeric string.
5793    */
5794   public static String randomAlphanumeric(int length) {
5795      if (length < 0)
5796         throw iaex("Length must be non-negative: " + length);
5797      var sb = new StringBuilder(length);
5798      for (var i = 0; i < length; i++) {
5799         var c = RANDOM.nextInt(62);
5800         if (c < 10)
5801            sb.append((char)('0' + c));
5802         else if (c < 36)
5803            sb.append((char)('a' + c - 10));
5804         else
5805            sb.append((char)('A' + c - 36));
5806      }
5807      return sb.toString();
5808   }
5809
5810   /**
5811    * Generates a random ASCII string of the specified length.
5812    *
5813    * <p>
5814    * Characters are composed of printable ASCII characters (32-126).
5815    *
5816    * <h4 class='section'>Example:</h4>
5817    * <p class='bjava'>
5818    *    randomAscii(<jk>10</jk>);  <jc>// "!@#$%^&amp;*()"</jc>
5819    * </p>
5820    *
5821    * @param length The length of the generated string.
5822    * @return A new random ASCII string.
5823    */
5824   public static String randomAscii(int length) {
5825      if (length < 0)
5826         throw iaex("Length must be non-negative: " + length);
5827      var sb = new StringBuilder(length);
5828      for (var i = 0; i < length; i++) {
5829         sb.append((char)(32 + RANDOM.nextInt(95))); // 95 printable ASCII chars (32-126)
5830      }
5831      return sb.toString();
5832   }
5833
5834   /**
5835    * Generates a random numeric string of the specified length.
5836    *
5837    * <p>
5838    * Characters are composed of digits (0-9).
5839    *
5840    * <h4 class='section'>Example:</h4>
5841    * <p class='bjava'>
5842    *    randomNumeric(<jk>6</jk>);  <jc>// "123456"</jc>
5843    * </p>
5844    *
5845    * @param length The length of the generated string.
5846    * @return A new random numeric string.
5847    */
5848   public static String randomNumeric(int length) {
5849      if (length < 0)
5850         throw iaex("Length must be non-negative: " + length);
5851      var sb = new StringBuilder(length);
5852      for (var i = 0; i < length; i++) {
5853         sb.append((char)('0' + RANDOM.nextInt(10)));
5854      }
5855      return sb.toString();
5856   }
5857
5858   /**
5859    * Generates a random string of the specified length using characters from the given character set.
5860    *
5861    * <p>
5862    * Each character in the generated string is randomly selected from the provided character set.
5863    *
5864    * <h4 class='section'>Example:</h4>
5865    * <p class='bjava'>
5866    *    randomString(<jk>5</jk>, <js>"ABC"</js>);  <jc>// "BACAB"</jc>
5867    * </p>
5868    *
5869    * @param length The length of the generated string.
5870    * @param chars The character set to use. Must not be null or empty.
5871    * @return A new random string.
5872    * @throws IllegalArgumentException If chars is null or empty, or length is negative.
5873    */
5874   public static String randomString(int length, String chars) {
5875      if (length < 0)
5876         throw iaex("Length must be non-negative: " + length);
5877      if (chars == null || chars.isEmpty())
5878         throw iaex("Character set must not be null or empty");
5879      var sb = new StringBuilder(length);
5880      var charsLen = chars.length();
5881      for (var i = 0; i < length; i++) {
5882         sb.append(chars.charAt(RANDOM.nextInt(charsLen)));
5883      }
5884      return sb.toString();
5885   }
5886
5887   /**
5888    * Calculates a simple readability score for a string.
5889    *
5890    * <p>
5891    * Uses a simplified Flesch Reading Ease-like formula based on:
5892    * <ul>
5893    *   <li>Average words per sentence</li>
5894    *   <li>Average syllables per word (estimated)</li>
5895    * </ul>
5896    * Returns a score from 0-100, where higher scores indicate easier reading.
5897    *
5898    * <h4 class='section'>Example:</h4>
5899    * <p class='bjava'>
5900    *    readabilityScore(<js>"The cat sat."</js>);      <jc>// Higher score (simple)</jc>
5901    *    readabilityScore(<js>"The sophisticated..."</js>); <jc>// Lower score (complex)</jc>
5902    * </p>
5903    *
5904    * @param str The string to analyze. Can be <jk>null</jk>.
5905    * @return A readability score from 0-100, or <c>0.0</c> if the string is <jk>null</jk> or empty.
5906    */
5907   public static double readabilityScore(String str) {
5908      if (isEmpty(str))
5909         return 0.0;
5910
5911      var words = extractWords(str);
5912      if (words.isEmpty())
5913         return 0.0;
5914
5915      // Count sentences (ending with . ! ?)
5916      var sentenceCount = 0;
5917      for (var i = 0; i < str.length(); i++) {
5918         var c = str.charAt(i);
5919         if (c == '.' || c == '!' || c == '?') {
5920            sentenceCount++;
5921         }
5922      }
5923      if (sentenceCount == 0)
5924         sentenceCount = 1; // At least one sentence
5925
5926      // Calculate average words per sentence
5927      var avgWordsPerSentence = (double)words.size() / sentenceCount;
5928
5929      // Estimate average syllables per word (simplified: count vowel groups)
5930      var totalSyllables = 0;
5931      for (var word : words) {
5932         totalSyllables += estimateSyllables(word);
5933      }
5934      var avgSyllablesPerWord = (double)totalSyllables / words.size();
5935
5936      // Simplified Flesch Reading Ease formula
5937      // Score = 206.835 - (1.015 * ASL) - (84.6 * ASW)
5938      // Where ASL = average sentence length (words), ASW = average syllables per word
5939      var score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
5940
5941      // Clamp to 0-100 range
5942      return Math.max(0.0, Math.min(100.0, score));
5943   }
5944
5945   /**
5946    * Converts an arbitrary object to a readable string format suitable for debugging and testing.
5947    *
5948    * <p>This method provides intelligent formatting for various Java types, recursively processing
5949    * nested structures to create human-readable representations. It's extensively used throughout
5950    * the Juneau framework for test assertions and debugging output.</p>
5951    *
5952    * <h4 class='section'>Type-Specific Formatting:</h4>
5953    * <ul>
5954    *    <li><b>null:</b> Returns <js>null</js></li>
5955    *    <li><b>Optional:</b> Recursively formats the contained value (or <js>null</js> if empty)</li>
5956    *    <li><b>Collections:</b> Formats as <js>"[item1,item2,item3]"</js> with comma-separated elements</li>
5957    *    <li><b>Maps:</b> Formats as <js>"{key1=value1,key2=value2}"</js> with comma-separated entries</li>
5958    *    <li><b>Map.Entry:</b> Formats as <js>"key=value"</js></li>
5959    *    <li><b>Arrays:</b> Converts to list format <js>"[item1,item2,item3]"</js></li>
5960    *    <li><b>Iterables/Iterators/Enumerations:</b> Converts to list and formats recursively</li>
5961    *    <li><b>GregorianCalendar:</b> Formats as ISO instant timestamp</li>
5962    *    <li><b>Date:</b> Formats as ISO instant string (e.g., <js>"2023-12-25T10:30:00Z"</js>)</li>
5963    *    <li><b>InputStream:</b> Converts to hexadecimal representation</li>
5964    *    <li><b>Reader:</b> Reads content and returns as string</li>
5965    *    <li><b>File:</b> Reads file content and returns as string</li>
5966    *    <li><b>byte[]:</b> Converts to hexadecimal representation</li>
5967    *    <li><b>Enum:</b> Returns the enum name via {@link Enum#name()}</li>
5968    *    <li><b>All other types:</b> Uses {@link Object#toString()}</li>
5969    * </ul>
5970    *
5971    * <h4 class='section'>Examples:</h4>
5972    * <p class='bjava'>
5973    *    <jc>// Collections</jc>
5974    *    readable(List.of("a", "b", "c")) <jc>// Returns: "[a,b,c]"</jc>
5975    *    readable(Set.of(1, 2, 3)) <jc>// Returns: "[1,2,3]" (order may vary)</jc>
5976    *
5977    *    <jc>// Maps</jc>
5978    *    readable(Map.of("foo", "bar", "baz", 123)) <jc>// Returns: "{foo=bar,baz=123}"</jc>
5979    *
5980    *    <jc>// Arrays</jc>
5981    *    readable(ints(1, 2, 3)) <jc>// Returns: "[1,2,3]"</jc>
5982    *    readable(new String[]{"a", "b"}) <jc>// Returns: "[a,b]"</jc>
5983    *
5984    *    <jc>// Nested structures</jc>
5985    *    readable(List.of(Map.of("x", 1), Set.of("a", "b"))) <jc>// Returns: "[{x=1},[a,b]]"</jc>
5986    *
5987    *    <jc>// Special types</jc>
5988    *    readable(Optional.of("test")) <jc>// Returns: "test"</jc>
5989    *    readable(Optional.empty()) <jc>// Returns: null</jc>
5990    *    readable(new Date(1640995200000L)) <jc>// Returns: "2022-01-01T00:00:00Z"</jc>
5991    *    readable(MyEnum.FOO) <jc>// Returns: "FOO"</jc>
5992    * </p>
5993    *
5994    * <h4 class='section'>Recursive Processing:</h4>
5995    * <p>The method recursively processes nested structures, so complex objects containing
5996    * collections, maps, and arrays are fully flattened into readable format. This makes it
5997    * ideal for test assertions where you need to compare complex object structures.</p>
5998    *
5999    * <h4 class='section'>Error Handling:</h4>
6000    * <p>IO operations (reading files, streams) are wrapped in safe() calls, converting
6001    * any exceptions to RuntimeExceptions. Binary data (InputStreams, byte arrays) is
6002    * converted to hexadecimal representation for readability.</p>
6003    *
6004    * @param o The object to convert to readable format. Can be <jk>null</jk>.
6005    * @return A readable string representation of the object, or <jk>null</jk> if the input was <jk>null</jk>.
6006    */
6007   public static String readable(Object o) {
6008      if (o == null)
6009         return null;
6010      var c = o.getClass();
6011
6012      // Special case for byte[] - must be handled before general array check
6013      if (c == byte[].class) {
6014         return toHex((byte[])o);
6015      }
6016
6017      // Check cache first
6018      var f = READIFIER_CACHE.get(c, () -> {
6019         // Find readifier from READIFIERS list
6020         // First try exact match, then isAssignableFrom
6021         var readifier = READIFIERS.stream()
6022            .filter(r -> r.forClass() == c || r.forClass().isAssignableFrom(c))
6023            .map(Readifier::toFunction)
6024            .findFirst()
6025            .orElse(null);
6026
6027         // If readifier found, use it
6028         if (readifier != null)
6029            return readifier;
6030
6031         // If no readifier found, check if it's an array
6032         if (c.isArray()) {
6033            return x -> {
6034               var l = list();
6035               for (var i = 0; i < Array.getLength(x); i++) {
6036                  l.add(Array.get(x, i));
6037               }
6038               return readable(l);
6039            };
6040         }
6041
6042         // If no readifier found, use toString() as fallback
6043         return Object::toString;
6044      });
6045
6046      return f.apply(o);
6047   }
6048
6049   /**
6050    * Removes all occurrences of a substring from a string.
6051    *
6052    * <h4 class='section'>Example:</h4>
6053    * <p class='bjava'>
6054    *    remove(<jk>null</jk>, <js>"x"</js>);              <jc>// null</jc>
6055    *    remove(<js>"hello"</js>, <jk>null</jk>);          <jc>// "hello"</jc>
6056    *    remove(<js>"hello world"</js>, <js>"o"</js>);     <jc>// "hell wrld"</jc>
6057    *    remove(<js>"hello world"</js>, <js>"xyz"</js>);   <jc>// "hello world"</jc>
6058    * </p>
6059    *
6060    * @param str The string to process.
6061    * @param remove The substring to remove.
6062    * @return The string with all occurrences of the substring removed, or <jk>null</jk> if input is <jk>null</jk>.
6063    */
6064   public static String remove(String str, String remove) {
6065      if (isEmpty(str) || isEmpty(remove))
6066         return str;
6067      return str.replace(remove, "");
6068   }
6069
6070   /**
6071    * Removes diacritical marks (accents) from characters in a string.
6072    *
6073    * <p>
6074    * Normalizes the string to NFD form and removes combining diacritical marks.
6075    *
6076    * <h4 class='section'>Example:</h4>
6077    * <p class='bjava'>
6078    *    removeAccents(<js>"café"</js>);     <jc>// "cafe"</jc>
6079    *    removeAccents(<js>"naïve"</js>);    <jc>// "naive"</jc>
6080    *    removeAccents(<js>"résumé"</js>);   <jc>// "resume"</jc>
6081    * </p>
6082    *
6083    * @param str The string to remove accents from. Can be <jk>null</jk>.
6084    * @return The string with accents removed, or <jk>null</jk> if input is <jk>null</jk>.
6085    */
6086   public static String removeAccents(String str) {
6087      if (str == null)
6088         return null;
6089
6090      // Normalize to NFD (decomposed form)
6091      var normalized = Normalizer.normalize(str, Normalizer.Form.NFD);
6092
6093      // Remove combining diacritical marks (Unicode category Mn)
6094      var sb = new StringBuilder(normalized.length());
6095      for (var i = 0; i < normalized.length(); i++) {
6096         var c = normalized.charAt(i);
6097         var type = Character.getType(c);
6098         // Mn = Nonspacing_Mark (combining marks)
6099         if (type != Character.NON_SPACING_MARK) {
6100            sb.append(c);
6101         }
6102      }
6103
6104      return sb.toString();
6105   }
6106
6107   /**
6108    * Removes multiple substrings from a string.
6109    *
6110    * <h4 class='section'>Example:</h4>
6111    * <p class='bjava'>
6112    *    removeAll(<js>"hello world test"</js>, <js>"hello"</js>, <js>"test"</js>);  <jc>// " world "</jc>
6113    *    removeAll(<jk>null</jk>, <js>"x"</js>);                                     <jc>// null</jc>
6114    * </p>
6115    *
6116    * @param str The string to process.
6117    * @param remove The substrings to remove.
6118    * @return The string with all specified substrings removed, or <jk>null</jk> if input is <jk>null</jk>.
6119    */
6120   public static String removeAll(String str, String...remove) {
6121      if (str == null)
6122         return null;
6123      if (isEmpty(str) || remove == null || remove.length == 0)
6124         return str;
6125      var result = str;
6126      for (var r : remove) {
6127         if (r != null)
6128            result = result.replace(r, "");
6129      }
6130      return result;
6131   }
6132
6133   /**
6134    * Removes control characters from a string, replacing them with spaces.
6135    *
6136    * <h4 class='section'>Example:</h4>
6137    * <p class='bjava'>
6138    *    removeControlChars(<js>"hello\u0000\u0001world"</js>);   <jc>// "hello  world"</jc>
6139    *    removeControlChars(<js>"hello\nworld"</js>);             <jc>// "hello\nworld"</jc>
6140    * </p>
6141    *
6142    * @param str The string to process.
6143    * @return The string with control characters replaced by spaces (except whitespace control chars), or <jk>null</jk> if input is <jk>null</jk>.
6144    */
6145   public static String removeControlChars(String str) {
6146      if (str == null)
6147         return null;
6148      var sb = new StringBuilder();
6149      for (var i = 0; i < str.length(); i++) {
6150         var c = str.charAt(i);
6151         if (Character.isISOControl(c) && ! Character.isWhitespace(c))
6152            sb.append(' ');
6153         else
6154            sb.append(c);
6155      }
6156      return sb.toString();
6157   }
6158
6159   /**
6160    * Removes a suffix from a string if present.
6161    *
6162    * <h4 class='section'>Example:</h4>
6163    * <p class='bjava'>
6164    *    removeEnd(<jk>null</jk>, <js>"x"</js>);              <jc>// null</jc>
6165    *    removeEnd(<js>"hello"</js>, <jk>null</jk>);          <jc>// "hello"</jc>
6166    *    removeEnd(<js>"hello world"</js>, <js>"world"</js>); <jc>// "hello "</jc>
6167    *    removeEnd(<js>"hello world"</js>, <js>"xyz"</js>);   <jc>// "hello world"</jc>
6168    * </p>
6169    *
6170    * @param str The string to process.
6171    * @param suffix The suffix to remove.
6172    * @return The string with the suffix removed if present, or <jk>null</jk> if input is <jk>null</jk>.
6173    */
6174   public static String removeEnd(String str, String suffix) {
6175      if (isEmpty(str) || isEmpty(suffix))
6176         return str;
6177      if (str.endsWith(suffix))
6178         return str.substring(0, str.length() - suffix.length());
6179      return str;
6180   }
6181
6182   /**
6183    * Removes non-printable characters from a string.
6184    *
6185    * <h4 class='section'>Example:</h4>
6186    * <p class='bjava'>
6187    *    removeNonPrintable(<js>"hello\u0000world"</js>);   <jc>// "helloworld"</jc>
6188    * </p>
6189    *
6190    * @param str The string to process.
6191    * @return The string with non-printable characters removed, or <jk>null</jk> if input is <jk>null</jk>.
6192    */
6193   public static String removeNonPrintable(String str) {
6194      if (str == null)
6195         return null;
6196      return str.replaceAll("\\p{C}", "");
6197   }
6198
6199   /**
6200    * Removes a prefix from a string if present.
6201    *
6202    * <h4 class='section'>Example:</h4>
6203    * <p class='bjava'>
6204    *    removeStart(<jk>null</jk>, <js>"x"</js>);              <jc>// null</jc>
6205    *    removeStart(<js>"hello"</js>, <jk>null</jk>);          <jc>// "hello"</jc>
6206    *    removeStart(<js>"hello world"</js>, <js>"hello"</js>); <jc>// " world"</jc>
6207    *    removeStart(<js>"hello world"</js>, <js>"xyz"</js>);   <jc>// "hello world"</jc>
6208    * </p>
6209    *
6210    * @param str The string to process.
6211    * @param prefix The prefix to remove.
6212    * @return The string with the prefix removed if present, or <jk>null</jk> if input is <jk>null</jk>.
6213    */
6214   public static String removeStart(String str, String prefix) {
6215      if (isEmpty(str) || isEmpty(prefix))
6216         return str;
6217      if (str.startsWith(prefix))
6218         return str.substring(prefix.length());
6219      return str;
6220   }
6221
6222   /**
6223    * Removes all underscore characters from a string.
6224    *
6225    * <p>
6226    * This method is commonly used to process numeric literals that may contain underscores for readability
6227    * (e.g., <js>"1_000_000"</js> becomes <js>"1000000"</js>), as Java allows underscores in numeric literals
6228    * but some parsing methods do not support them.
6229    *
6230    * <p>
6231    * If the string does not contain any underscores, the original string is returned (no new object created).
6232    *
6233    * <h4 class='section'>Example:</h4>
6234    * <p class='bjava'>
6235    *    removeUnderscores(<js>"1_000_000"</js>);        <jc>// "1000000"</jc>
6236    *    removeUnderscores(<js>"1_000.5"</js>);          <jc>// "1000.5"</jc>
6237    *    removeUnderscores(<js>"hello_world"</js>);      <jc>// "helloworld"</jc>
6238    *    removeUnderscores(<js>"no_underscores"</js>);   <jc>// "nounderscores"</jc>
6239    *    removeUnderscores(<js>"Hello"</js>);            <jc>// "Hello" (no change, same object returned)</jc>
6240    * </p>
6241    *
6242    * @param value The string from which to remove underscores. Must not be <jk>null</jk>.
6243    * @return A new string with all underscores removed, or the original string if it contains no underscores.
6244    * @throws IllegalArgumentException If <c>value</c> is <jk>null</jk>.
6245    * @see #parseInt(String)
6246    * @see #parseLong(String)
6247    * @see #parseFloat(String)
6248    * @see #parseNumber(String, Class)
6249    */
6250   public static String removeUnderscores(String value) {
6251      reqnn("value", value);
6252      return notContains(value, '_') ? value : value.replace("_", "");
6253   }
6254
6255   /**
6256    * Creates a repeated pattern.
6257    *
6258    * @param count The number of times to repeat the pattern.
6259    * @param pattern The pattern to repeat.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
6260    * @return A new string consisting of the repeated pattern.
6261    */
6262   public static String repeat(int count, String pattern) {
6263      var sb = new StringBuilder(pattern.length() * count);
6264      for (var i = 0; i < count; i++)
6265         sb.append(pattern);
6266      return sb.toString();
6267   }
6268
6269   /**
6270    * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters.
6271    *
6272    * @param s The string to replace unicode sequences in.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
6273    * @return A string with unicode sequences replaced.
6274    */
6275   public static String replaceUnicodeSequences(String s) {
6276
6277      if (s.indexOf('\\') == -1)
6278         return s;
6279
6280      var p = Pattern.compile("\\\\u(\\p{XDigit}{4})");
6281      var m = p.matcher(s);
6282      var sb = new StringBuffer(s.length());
6283
6284      while (m.find()) {
6285         var ch = String.valueOf((char)Integer.parseInt(m.group(1), 16));
6286         m.appendReplacement(sb, Matcher.quoteReplacement(ch));
6287      }
6288
6289      m.appendTail(sb);
6290      return sb.toString();
6291   }
6292
6293   /**
6294    * Reverses a string.
6295    *
6296    * <h4 class='section'>Example:</h4>
6297    * <p class='bjava'>
6298    *    reverse(<jk>null</jk>);          <jc>// null</jc>
6299    *    reverse(<js>""</js>);            <jc>// ""</jc>
6300    *    reverse(<js>"hello"</js>);       <jc>// "olleh"</jc>
6301    * </p>
6302    *
6303    * @param str The string to reverse.
6304    * @return The reversed string, or <jk>null</jk> if input is <jk>null</jk>.
6305    */
6306   public static String reverse(String str) {
6307      if (str == null)
6308         return null;
6309      return new StringBuilder(str).reverse().toString();
6310   }
6311
6312   /**
6313    * Returns the rightmost characters of a string.
6314    *
6315    * <h4 class='section'>Example:</h4>
6316    * <p class='bjava'>
6317    *    right(<jk>null</jk>, 3);          <jc>// null</jc>
6318    *    right(<js>""</js>, 3);            <jc>// ""</jc>
6319    *    right(<js>"hello"</js>, 3);       <jc>// "llo"</jc>
6320    *    right(<js>"hello"</js>, 10);      <jc>// "hello"</jc>
6321    * </p>
6322    *
6323    * @param str The string to get characters from.
6324    * @param len The number of characters to get.
6325    * @return The rightmost characters, or <jk>null</jk> if input is <jk>null</jk>.
6326    */
6327   public static String right(String str, int len) {
6328      if (str == null)
6329         return null;
6330      if (len < 0)
6331         return "";
6332      if (len >= str.length())
6333         return str;
6334      return str.substring(str.length() - len);
6335   }
6336
6337   /**
6338    * Basic HTML/XML sanitization - removes or escapes potentially dangerous content.
6339    *
6340    * <p>
6341    * Removes HTML/XML tags and escapes special characters to prevent XSS attacks.
6342    * This is a basic sanitization - for production use, consider a more robust library.
6343    *
6344    * <h4 class='section'>Example:</h4>
6345    * <p class='bjava'>
6346    *    sanitize(<js>"&lt;script&gt;alert('xss')&lt;/script&gt;"</js>);     <jc>// "&amp;lt;script&amp;gt;alert('xss')&amp;lt;/script&amp;gt;"</jc>
6347    *    sanitize(<js>"Hello &lt;b&gt;World&lt;/b&gt;"</js>);                <jc>// "Hello &amp;lt;b&amp;gt;World&amp;lt;/b&amp;gt;"</jc>
6348    * </p>
6349    *
6350    * @param str The string to sanitize.
6351    * @return The sanitized string with HTML/XML tags escaped, or <jk>null</jk> if input is <jk>null</jk>.
6352    */
6353   public static String sanitize(String str) {
6354      if (str == null)
6355         return null;
6356      // Escape HTML/XML special characters
6357      return escapeHtml(str);
6358   }
6359
6360   /**
6361    * Calculates the similarity percentage between two strings using Levenshtein distance.
6362    *
6363    * <p>
6364    * Returns a value between 0.0 (completely different) and 1.0 (identical).
6365    *
6366    * <h4 class='section'>Example:</h4>
6367    * <p class='bjava'>
6368    *    similarity(<js>"hello"</js>, <js>"hello"</js>);           <jc>// 1.0 (100%)</jc>
6369    *    similarity(<js>"kitten"</js>, <js>"sitting"</js>);        <jc>// ~0.57 (57%)</jc>
6370    *    similarity(<js>"abc"</js>, <js>"xyz"</js>);               <jc>// 0.0 (0%)</jc>
6371    * </p>
6372    *
6373    * @param str1 The first string.  Can be <jk>null</jk> (treated as empty).
6374    * @param str2 The second string.  Can be <jk>null</jk> (treated as empty).
6375    * @return A similarity value between 0.0 and 1.0, where 1.0 means identical.
6376    */
6377   public static double similarity(String str1, String str2) {
6378      if (str1 == null)
6379         str1 = "";
6380      if (str2 == null)
6381         str2 = "";
6382
6383      if (str1.equals(str2))
6384         return 1.0;
6385
6386      var maxLen = Math.max(str1.length(), str2.length());
6387
6388      var distance = levenshteinDistance(str1, str2);
6389      return 1.0 - ((double)distance / maxLen);
6390   }
6391
6392   /**
6393    * Converts a string to snake_case format.
6394    *
6395    * <p>
6396    * Handles various input formats:
6397    * <ul>
6398    *   <li>Space-separated: "hello world" → "hello_world"</li>
6399    *   <li>CamelCase: "helloWorld" → "hello_world"</li>
6400    *   <li>PascalCase: "HelloWorld" → "hello_world"</li>
6401    *   <li>Kebab-case: "hello-world" → "hello_world"</li>
6402    * </ul>
6403    *
6404    * <h4 class='section'>Example:</h4>
6405    * <p class='bjava'>
6406    *    snakeCase(<jk>null</jk>);                    <jc>// null</jc>
6407    *    snakeCase(<js>""</js>);                      <jc>// ""</jc>
6408    *    snakeCase(<js>"hello world"</js>);           <jc>// "hello_world"</jc>
6409    *    snakeCase(<js>"helloWorld"</js>);            <jc>// "hello_world"</jc>
6410    *    snakeCase(<js>"HelloWorld"</js>);            <jc>// "hello_world"</jc>
6411    *    snakeCase(<js>"hello-world"</js>);           <jc>// "hello_world"</jc>
6412    * </p>
6413    *
6414    * @param str The string to convert.
6415    * @return The snake_case string, or <jk>null</jk> if input is <jk>null</jk>.
6416    */
6417   public static String snakeCase(String str) {
6418      if (str == null)
6419         return null;
6420      if (isEmpty(str))
6421         return str;
6422
6423      var words = splitWords(str);
6424      if (words.isEmpty())
6425         return "";
6426
6427      var result = new StringBuilder();
6428      for (var i = 0; i < words.size(); i++) {
6429         if (i > 0)
6430            result.append('_');
6431         result.append(lowerCaseRoot(words.get(i)));
6432      }
6433
6434      return result.toString();
6435   }
6436
6437   /**
6438    * Sorts a string array in natural order.
6439    *
6440    * <p>
6441    * Returns <jk>null</jk> if the array is <jk>null</jk>.
6442    * This method creates a copy of the array and sorts it, leaving the original array unchanged.
6443    *
6444    * <h4 class='section'>Examples:</h4>
6445    * <p class='bjava'>
6446    *    String[] <jv>array</jv> = {<js>"zebra"</js>, <js>"apple"</js>, <js>"banana"</js>};
6447    *    String[] <jv>sorted</jv> = sort(<jv>array</jv>);
6448    *    <jc>// Returns: ["apple", "banana", "zebra"]</jc>
6449    * </p>
6450    *
6451    * @param array The array to sort. Can be <jk>null</jk>.
6452    * @return A new sorted array, or an empty array if the array was <jk>null</jk>.
6453    */
6454   public static String[] sort(String[] array) {
6455      if (array == null)
6456         return new String[0];
6457      var result = Arrays.copyOf(array, array.length);
6458      Arrays.sort(result);
6459      return result;
6460   }
6461
6462   /**
6463    * Sorts a string array in case-insensitive order.
6464    *
6465    * <p>
6466    * Returns <jk>null</jk> if the array is <jk>null</jk>.
6467    * This method creates a copy of the array and sorts it using case-insensitive comparison,
6468    * leaving the original array unchanged.
6469    *
6470    * <h4 class='section'>Examples:</h4>
6471    * <p class='bjava'>
6472    *    String[] <jv>array</jv> = {<js>"Zebra"</js>, <js>"apple"</js>, <js>"Banana"</js>};
6473    *    String[] <jv>sorted</jv> = sortIgnoreCase(<jv>array</jv>);
6474    *    <jc>// Returns: ["apple", "Banana", "Zebra"]</jc>
6475    * </p>
6476    *
6477    * @param array The array to sort. Can be <jk>null</jk>.
6478    * @return A new sorted array (case-insensitive), or an empty array if the array was <jk>null</jk>.
6479    */
6480   public static String[] sortIgnoreCase(String[] array) {
6481      if (array == null)
6482         return new String[0];
6483      var result = Arrays.copyOf(array, array.length);
6484      Arrays.sort(result, String.CASE_INSENSITIVE_ORDER);
6485      return result;
6486   }
6487
6488   /**
6489    * Generates a Soundex code for a string.
6490    *
6491    * <p>
6492    * Soundex is a phonetic algorithm for indexing names by sound. The code consists of
6493    * a letter followed by three digits. Similar-sounding names produce the same code.
6494    *
6495    * <h4 class='section'>Example:</h4>
6496    * <p class='bjava'>
6497    *    soundex(<js>"Smith"</js>);   <jc>// "S530"</jc>
6498    *    soundex(<js>"Smythe"</js>);  <jc>// "S530"</jc>
6499    *    soundex(<js>"Robert"</js>);  <jc>// "R163"</jc>
6500    * </p>
6501    *
6502    * @param str The string to generate a Soundex code for. Can be <jk>null</jk>.
6503    * @return The Soundex code (1 letter + 3 digits), or <jk>null</jk> if input is <jk>null</jk> or empty.
6504    */
6505   public static String soundex(String str) {
6506      if (isEmpty(str))
6507         return null;
6508
6509      var upper = str.toUpperCase(Locale.ROOT);
6510      var result = new StringBuilder(4);
6511      result.append(upper.charAt(0));
6512
6513      // Soundex mapping: 0 = AEIOUHWY, 1 = BFPV, 2 = CGJKQSXZ, 3 = DT, 4 = L, 5 = MN, 6 = R
6514      // H/W/Y don't get codes but don't break sequences either
6515      // Initialize lastCode to a value that won't match any real code
6516      var lastCode = '\0';
6517
6518      for (var i = 1; i < upper.length() && result.length() < 4; i++) {
6519         var c = upper.charAt(i);
6520         var code = getSoundexCode(c);
6521         if (code == '0') {
6522            // H/W/Y/vowels - don't add code, but don't update lastCode either
6523            // This allows sequences to continue across H/W/Y/vowels
6524            continue;
6525         }
6526         if (code != lastCode) {
6527            result.append(code);
6528            lastCode = code;
6529         }
6530         // If code == lastCode, skip it (consecutive same codes)
6531      }
6532
6533      // Pad with zeros if needed
6534      while (result.length() < 4) {
6535         result.append('0');
6536      }
6537
6538      return result.toString();
6539   }
6540
6541   /**
6542    * Splits a comma-delimited list into a list of strings.
6543    *
6544    * @param s The string to split.
6545    * @return A list of split strings, or an empty list if the input is <jk>null</jk>.
6546    */
6547   public static List<String> split(String s) {
6548      return s == null ? Collections.emptyList() : split(s, ',');
6549   }
6550
6551   /**
6552    * Splits a character-delimited string into a string array.
6553    *
6554    * <p>
6555    * Does not split on escaped-delimiters (e.g. "\,");
6556    * Resulting tokens are trimmed of whitespace.
6557    *
6558    * <p>
6559    * <b>NOTE:</b>  This behavior is different than the Jakarta equivalent.
6560    * split("a,b,c",',') -&gt; {"a","b","c"}
6561    * split("a, b ,c ",',') -&gt; {"a","b","c"}
6562    * split("a,,c",',') -&gt; {"a","","c"}
6563    * split(",,",',') -&gt; {"","",""}
6564    * split("",',') -&gt; {}
6565    * split(null,',') -&gt; {}
6566    * split("a,b\,c,d", ',', false) -&gt; {"a","b\,c","d"}
6567    * split("a,b\\,c,d", ',', false) -&gt; {"a","b\","c","d"}
6568    * split("a,b\,c,d", ',', true) -&gt; {"a","b,c","d"}
6569    *
6570    * @param s The string to split.  Can be <jk>null</jk>.
6571    * @param c The character to split on.
6572    * @return The tokens, or an empty list if the string was <jk>null</jk>.
6573    */
6574   public static List<String> split(String s, char c) {
6575      return split(s, c, Integer.MAX_VALUE);
6576   }
6577
6578   /**
6579    * Same as {@link #splita(String, char)} but consumes the tokens instead of creating an array.
6580    *
6581    * @param s The string to split.  Can be <jk>null</jk> (no tokens are consumed).
6582    * @param c The character to split on.
6583    * @param consumer The consumer of the tokens.  Must not be <jk>null</jk> when the string is non-empty.
6584    */
6585   public static void split(String s, char c, Consumer<String> consumer) {
6586      var escapeChars = getEscapeSet(c);
6587
6588      if (isEmpty(s))
6589         return;
6590      if (s.indexOf(c) == -1) {
6591         consumer.accept(s);
6592         return;
6593      }
6594
6595      var x1 = 0;
6596      var escapeCount = 0;
6597
6598      for (var i = 0; i < s.length(); i++) {
6599         if (s.charAt(i) == '\\')
6600            escapeCount++;
6601         else if (s.charAt(i) == c && escapeCount % 2 == 0) {
6602            var s2 = s.substring(x1, i);
6603            var s3 = unescapeChars(s2, escapeChars);
6604            consumer.accept(s3.trim());
6605            x1 = i + 1;
6606         }
6607         if (s.charAt(i) != '\\')
6608            escapeCount = 0;
6609      }
6610      var s2 = s.substring(x1);
6611      var s3 = unescapeChars(s2, escapeChars);
6612      consumer.accept(s3.trim());
6613   }
6614
6615   /**
6616    * Same as {@link #splita(String, char, int)} but returns a {@link List}.
6617    *
6618    * @param s The string to split.  Can be <jk>null</jk>.
6619    * @param c The character to split on.
6620    * @param limit The maximum number of tokens to return.
6621    * @return The tokens, or an empty list if the string was <jk>null</jk>.
6622    */
6623   public static List<String> split(String s, char c, int limit) {
6624
6625      var escapeChars = getEscapeSet(c);
6626
6627      if (s == null)
6628         return Collections.emptyList();
6629      if (isEmpty(s))
6630         return Collections.emptyList();
6631      if (s.indexOf(c) == -1)
6632         return Collections.singletonList(s);
6633
6634      var l = new LinkedList<String>();
6635      var sArray = s.toCharArray();
6636      var x1 = 0;
6637      var escapeCount = 0;
6638      limit--;
6639      for (var i = 0; i < sArray.length && limit > 0; i++) {
6640         if (sArray[i] == '\\')
6641            escapeCount++;
6642         else if (sArray[i] == c && escapeCount % 2 == 0) {
6643            var s2 = new String(sArray, x1, i - x1);
6644            var s3 = unescapeChars(s2, escapeChars);
6645            l.add(s3.trim());
6646            limit--;
6647            x1 = i + 1;
6648         }
6649         if (sArray[i] != '\\')
6650            escapeCount = 0;
6651      }
6652      var s2 = new String(sArray, x1, sArray.length - x1);
6653      var s3 = unescapeChars(s2, escapeChars);
6654      l.add(s3.trim());
6655
6656      return l;
6657   }
6658
6659   /**
6660    * Same as {@link #splita(String)} but consumes the tokens instead of creating an array.
6661    *
6662    * @param s The string to split.  Can be <jk>null</jk> (no tokens are consumed).
6663    * @param consumer The consumer of the tokens.  Must not be <jk>null</jk> when the string is non-empty.
6664    */
6665   public static void split(String s, Consumer<String> consumer) {
6666      StringUtils.split(s, ',', consumer);
6667   }
6668
6669   /**
6670    * Splits a comma-delimited list into an array of strings.
6671    *
6672    * @param s The string to split.  Can be <jk>null</jk> (returns <jk>null</jk>).
6673    * @return An array of split strings, or <jk>null</jk> if the string was <jk>null</jk>.
6674    */
6675   public static String[] splita(String s) {
6676      return splita(s, ',');
6677   }
6678
6679   /**
6680    * Splits a character-delimited string into a string array.
6681    *
6682    * <p>
6683    * Does not split on escaped-delimiters (e.g. "\,");
6684    * Resulting tokens are trimmed of whitespace.
6685    *
6686    * <p>
6687    * <b>NOTE:</b>  This behavior is different than the Jakarta equivalent.
6688    * split("a,b,c",',') -&gt; {"a","b","c"}
6689    * split("a, b ,c ",',') -&gt; {"a","b","c"}
6690    * split("a,,c",',') -&gt; {"a","","c"}
6691    * split(",,",',') -&gt; {"","",""}
6692    * split("",',') -&gt; {}
6693    * split(null,',') -&gt; null
6694    * split("a,b\,c,d", ',', false) -&gt; {"a","b\,c","d"}
6695    * split("a,b\\,c,d", ',', false) -&gt; {"a","b\","c","d"}
6696    * split("a,b\,c,d", ',', true) -&gt; {"a","b,c","d"}
6697    *
6698    * @param s The string to split.  Can be <jk>null</jk>.
6699    * @param c The character to split on.
6700    * @return The tokens, or <jk>null</jk> if the string was null.
6701    */
6702   public static String[] splita(String s, char c) {
6703      return splita(s, c, Integer.MAX_VALUE);
6704   }
6705
6706   /**
6707    * Same as {@link #splita(String, char)} but limits the number of tokens returned.
6708    *
6709    * @param s The string to split.  Can be <jk>null</jk>.
6710    * @param c The character to split on.
6711    * @param limit The maximum number of tokens to return.
6712    * @return The tokens, or <jk>null</jk> if the string was null.
6713    */
6714   public static String[] splita(String s, char c, int limit) {
6715      if (s == null)
6716         return null;
6717      var l = StringUtils.split(s, c, limit);
6718      return l.toArray(new String[l.size()]);
6719   }
6720
6721   /**
6722    * Same as {@link #splita(String, char)} except splits all strings in the input and returns a single result.
6723    *
6724    * @param s The string to split.  Can be <jk>null</jk>.
6725    * @param c The character to split on.
6726    * @return The tokens, or null if the input array was null
6727    */
6728   public static String[] splita(String[] s, char c) {
6729      if (s == null)
6730         return null;
6731      var l = new LinkedList<String>();
6732      for (var ss : s) {
6733         if (ss == null || ss.indexOf(c) == -1)
6734            l.add(ss);
6735         else
6736            Collections.addAll(l, splita(ss, c));
6737      }
6738      return l.toArray(new String[l.size()]);
6739   }
6740
6741   /**
6742    * Splits a list of key-value pairs into an ordered map.
6743    *
6744    * <p>
6745    * Example:
6746    * <p class='bjava'>
6747    *    String <jv>in</jv> = <js>"foo=1;bar=2"</js>;
6748    *    Map <jv>map</jv> = StringUtils.<jsm>splitMap</jsm>(in, <js>';'</js>, <js>'='</js>, <jk>true</jk>);
6749    * </p>
6750    *
6751    * @param s The string to split.
6752    * @param trim Trim strings after parsing.
6753    * @return The parsed map, or an empty map if the string was null.
6754    */
6755   public static Map<String,String> splitMap(String s, boolean trim) {
6756
6757      if (s == null)
6758         return emptyMap();
6759      if (isEmpty(s))
6760         return emptyMap();
6761
6762      var m = new LinkedHashMap<String,String>();
6763
6764      // S1: Found start of key, looking for equals.
6765      // S2: Found equals, looking for delimiter (or end).
6766
6767      var state = S1;
6768
6769      var sArray = s.toCharArray();
6770      var x1 = 0;
6771      var escapeCount = 0;
6772      String key = null;
6773      for (var i = 0; i < sArray.length + 1; i++) {
6774         var c = i == sArray.length ? ',' : sArray[i];
6775         if (c == '\\')
6776            escapeCount++;
6777         if (escapeCount % 2 == 0) {
6778            if (state == S1) {
6779               if (c == '=') {
6780                  key = s.substring(x1, i);
6781                  if (trim)
6782                     key = trim(key);
6783                  key = unescapeChars(key, MAP_ESCAPE_SET);
6784                  state = S2;
6785                  x1 = i + 1;
6786               } else if (c == ',') {
6787                  key = s.substring(x1, i);
6788                  if (trim)
6789                     key = trim(key);
6790                  key = unescapeChars(key, MAP_ESCAPE_SET);
6791                  m.put(key, "");
6792                  state = S1;
6793                  x1 = i + 1;
6794               }
6795            } else /* state == S2 */ {
6796               if (c == ',') {
6797                  var val = s.substring(x1, i);
6798                  if (trim)
6799                     val = trim(val);
6800                  val = unescapeChars(val, MAP_ESCAPE_SET);
6801                  m.put(key, val);
6802                  key = null;
6803                  x1 = i + 1;
6804                  state = S1;
6805               }
6806            }
6807         }
6808         if (c != '\\')
6809            escapeCount = 0;
6810      }
6811
6812      return m;
6813   }
6814
6815   /**
6816    * Splits the method arguments in the signature of a method.
6817    *
6818    * @param s The arguments to split.  Can be <jk>null</jk> (returns an empty array).
6819    * @return The split arguments, or an empty array if the input string is <jk>null</jk> or empty.
6820    */
6821   public static String[] splitMethodArgs(String s) {
6822
6823      if (s == null)
6824         return new String[0];
6825      if (isEmpty(s))
6826         return new String[0];
6827      if (s.indexOf(',') == -1)
6828         return array(s);
6829
6830      var l = new LinkedList<String>();
6831      var sArray = s.toCharArray();
6832      var x1 = 0;
6833      var paramDepth = 0;
6834
6835      for (var i = 0; i < sArray.length; i++) {
6836         var c = s.charAt(i);
6837         if (c == '>')
6838            paramDepth++;
6839         else if (c == '<')
6840            paramDepth--;
6841         else if (c == ',' && paramDepth == 0) {
6842            var s2 = new String(sArray, x1, i - x1);
6843            l.add(s2.trim());
6844            x1 = i + 1;
6845         }
6846      }
6847
6848      var s2 = new String(sArray, x1, sArray.length - x1);
6849      l.add(s2.trim());
6850
6851      return l.toArray(new String[l.size()]);
6852   }
6853
6854   /**
6855    * Splits a comma-delimited list containing "nesting constructs".
6856    *
6857    * Nesting constructs are simple embedded "{...}" comma-delimted lists.
6858    *
6859    * Example:
6860    *    "a{b,c},d" -> ["a{b,c}","d"]
6861    *
6862    * Handles escapes and trims whitespace from tokens.
6863    *
6864    * @param s The input string.
6865    * @return
6866    *    The results, or an empty list if the input was <jk>null</jk>.
6867    *    <br>An empty string results in an empty list.
6868    */
6869   public static List<String> splitNested(String s) {
6870      var escapeChars = getEscapeSet(',');
6871
6872      if (s == null)
6873         return Collections.emptyList();
6874      if (isEmpty(s))
6875         return Collections.emptyList();
6876      if (s.indexOf(',') == -1)
6877         return Collections.singletonList(trim(s));
6878
6879      var l = new LinkedList<String>();
6880
6881      var x1 = 0;
6882      var inEscape = false;
6883      var depthCount = 0;
6884
6885      for (var i = 0; i < s.length(); i++) {
6886         var c = s.charAt(i);
6887         if (inEscape) {
6888            if (c == '\\') {
6889               inEscape = false;
6890            }
6891         } else {
6892            if (c == '\\') {
6893               inEscape = true;
6894            } else if (c == '{') {
6895               depthCount++;
6896            } else if (c == '}') {
6897               depthCount--;
6898            } else if (c == ',' && depthCount == 0) {
6899               l.add(trim(unescapeChars(s.substring(x1, i), escapeChars)));
6900               x1 = i + 1;
6901            }
6902         }
6903      }
6904      l.add(trim(unescapeChars(s.substring(x1, s.length()), escapeChars)));
6905
6906      return l;
6907   }
6908
6909   /**
6910    * Splits a nested comma-delimited list.
6911    *
6912    * Nesting constructs are simple embedded "{...}" comma-delimted lists.
6913    *
6914    * Example:
6915    *    "a{b,c{d,e}}" -> ["b","c{d,e}"]
6916    *
6917    * Handles escapes and trims whitespace from tokens.
6918    *
6919    * @param s The input string.  Must not be <jk>null</jk> or empty (a <jk>null</jk> or empty argument throws {@link IllegalArgumentException}).
6920    * @return
6921    *    The results.
6922    * @throws IllegalArgumentException If the input was <jk>null</jk> or empty.
6923    */
6924   public static List<String> splitNestedInner(String s) {
6925      req(isNotNull(s), "String was null.");
6926      req(isNotEmpty(s), "String was empty.");
6927
6928      // S1: Looking for '{'
6929      // S2: Found '{', looking for '}'
6930
6931      var start = -1;
6932      var end = -1;
6933      var state = S1;
6934
6935      var depth = 0;
6936      var inEscape = false;
6937
6938      for (var i = 0; i < s.length(); i++) {
6939         var c = s.charAt(i);
6940         if (inEscape) {
6941            if (c == '\\') {
6942               inEscape = false;
6943            }
6944         } else {
6945            if (c == '\\') {
6946               inEscape = true;
6947            } else if (state == S1) {
6948               if (c == '{') {
6949                  start = i + 1;
6950                  state = S2;
6951               }
6952            } else /* state == S2 */ {
6953               if (c == '{') {
6954                  depth++;
6955               } else if (depth > 0 && c == '}') {
6956                  depth--;
6957               } else if (c == '}') {
6958                  end = i;
6959                  break;
6960               }
6961            }
6962         }
6963      }
6964
6965      if (start == -1)
6966         throw iaex("Start character { not found in string:  %s", s);
6967      if (end == -1)
6968         throw iaex("End character } not found in string  %s", s);
6969      return splitNested(s.substring(start, end));
6970   }
6971
6972   /**
6973    * Splits a space-delimited string with optionally quoted arguments.
6974    *
6975    * <p>
6976    * Examples:
6977    * <ul>
6978    *    <li><js>"foo"</js> =&gt; <c>["foo"]</c>
6979    *    <li><js>" foo "</js> =&gt; <c>["foo"]</c>
6980    *    <li><js>"foo bar baz"</js> =&gt; <c>["foo","bar","baz"]</c>
6981    *    <li><js>"foo 'bar baz'"</js> =&gt; <c>["foo","bar baz"]</c>
6982    *    <li><js>"foo \"bar baz\""</js> =&gt; <c>["foo","bar baz"]</c>
6983    *    <li><js>"foo 'bar\'baz'"</js> =&gt; <c>["foo","bar'baz"]</c>
6984    * </ul>
6985    *
6986    * @param s The input string.  Can be <jk>null</jk> (returns an empty array).
6987    * @return
6988    *    The results, or an empty array if the input was <jk>null</jk>.
6989    *    <br>An empty string results in an empty array.
6990    */
6991   public static String[] splitQuoted(String s) {
6992      return splitQuoted(s, false);
6993   }
6994
6995   /**
6996    * Same as {@link StringUtils#splitQuoted(String)} but allows you to optionally keep the quote characters.
6997    *
6998    * @param s The input string.  Can be <jk>null</jk> (returns an empty array).
6999    * @param keepQuotes If <jk>true</jk>, quote characters are kept on the tokens.
7000    * @return
7001    *    The results, or an empty array if the input was <jk>null</jk>.
7002    *    <br>An empty string results in an empty array.
7003    */
7004   @SuppressWarnings({
7005      "java:S2583" // State variables persist across loop iterations
7006   })
7007   public static String[] splitQuoted(String s, boolean keepQuotes) {
7008
7009      if (s == null)
7010         return new String[0];
7011
7012      s = s.trim();
7013
7014      if (isEmpty(s))
7015         return array();
7016
7017      if (! containsAny(s, ' ', '\t', '\'', '"'))
7018         return array(s);
7019
7020      // S1: Looking for start of token.
7021      // S2: Found ', looking for end '
7022      // S3: Found ", looking for end "
7023      // S4: Found non-whitespace, looking for end whitespace.
7024
7025      var state = S1;
7026
7027      var isInEscape = false;
7028      var needsUnescape = false;
7029      var mark = 0;
7030
7031      var l = new ArrayList<String>();
7032      for (var i = 0; i < s.length(); i++) {
7033         var c = s.charAt(i);
7034
7035         if (state == S1) {
7036            if (c == '\'') {
7037               state = S2;
7038               mark = keepQuotes ? i : i + 1;
7039            } else if (c == '"') {
7040               state = S3;
7041               mark = keepQuotes ? i : i + 1;
7042            } else if (c != ' ' && c != '\t') {
7043               state = S4;
7044               mark = i;
7045            }
7046         } else if (state == S2 || state == S3) {
7047            if (c == '\\') {
7048               isInEscape = ! isInEscape;
7049               if (isInEscape)
7050                  needsUnescape = ! keepQuotes;
7051            } else if (! isInEscape) {
7052               if (c == (state == S2 ? '\'' : '"')) {
7053                  var s2 = s.substring(mark, keepQuotes ? i + 1 : i);
7054                  if (needsUnescape)
7055                     s2 = unescapeChars(s2, QUOTE_ESCAPE_SET);
7056                  l.add(s2);
7057                  state = S1;
7058                  isInEscape = needsUnescape = false;
7059               }
7060            } else {
7061               isInEscape = false;
7062            }
7063         } else /* state == S4 */ {
7064            if (c == ' ' || c == '\t') {
7065               l.add(s.substring(mark, i));
7066               state = S1;
7067            }
7068         }
7069      }
7070      if (state == S4)
7071         l.add(s.substring(mark));
7072      else if (state == S2 || state == S3)
7073         throw iaex("Unmatched string quotes: %s", s);
7074      return l.toArray(new String[l.size()]);
7075   }
7076
7077   /**
7078    * An efficient method for checking if a string starts with a character.
7079    *
7080    * @param s The string to check.  Can be <jk>null</jk>.
7081    * @param c The character to check for.
7082    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character.
7083    */
7084   public static boolean startsWith(String s, char c) {
7085      if (isNotNull(s)) {
7086         var i = s.length();
7087         if (i > 0)
7088            return s.charAt(0) == c;
7089      }
7090      return false;
7091   }
7092
7093   /**
7094    * Checks if a string starts with a prefix, ignoring case.
7095    *
7096    * <h4 class='section'>Example:</h4>
7097    * <p class='bjava'>
7098    *    startsWithIgnoreCase(<js>"Hello World"</js>, <js>"hello"</js>);     <jc>// true</jc>
7099    *    startsWithIgnoreCase(<js>"Hello World"</js>, <js>"HELLO"</js>);     <jc>// true</jc>
7100    *    startsWithIgnoreCase(<js>"hello world"</js>, <js>"world"</js>);     <jc>// false</jc>
7101    * </p>
7102    *
7103    * @param str The string to check.
7104    * @param prefix The prefix to check for.
7105    * @return <jk>true</jk> if the string starts with the prefix (ignoring case), <jk>false</jk> otherwise.
7106    */
7107   public static boolean startsWithIgnoreCase(String str, String prefix) {
7108      if (str == null || prefix == null)
7109         return false;
7110      return str.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT));
7111   }
7112
7113   /**
7114    * Takes a supplier of any type and returns a {@link Supplier}{@code <String>}.
7115    *
7116    * <p>Useful when passing arguments to loggers.
7117    *
7118    * @param s The supplier.  Must not be <jk>null</jk> (the returned supplier throws {@link NullPointerException} when invoked).
7119    * @return A string supplier that calls {@link #readable(Object)} on the supplied value.
7120    */
7121   public static Supplier<String> stringSupplier(Supplier<?> s) {
7122      return () -> readable(s.get());
7123   }
7124
7125   /**
7126    * Strips the first and last character from a string.
7127    *
7128    * @param s The string to strip.
7129    * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2.
7130    */
7131   public static String strip(String s) {
7132      if (s == null || s.length() <= 1)
7133         return s;
7134      return s.substring(1, s.length() - 1);
7135   }
7136
7137   /**
7138    * Strips invalid characters such as CTRL characters from a string meant to be encoded
7139    * as an HTTP header value.
7140    *
7141    * @param s The string to strip chars from.
7142    * @return The string with invalid characters removed.
7143    */
7144   public static String stripInvalidHttpHeaderChars(String s) {
7145
7146      if (s == null)
7147         return null;
7148
7149      var needsReplace = false;
7150      for (var i = 0; i < s.length() && ! needsReplace; i++)
7151         needsReplace |= HTTP_HEADER_CHARS.contains(s.charAt(i));
7152
7153      if (! needsReplace)
7154         return s;
7155
7156      var sb = new StringBuilder(s.length());
7157      for (var i = 0; i < s.length(); i++) {
7158         var c = s.charAt(i);
7159         if (HTTP_HEADER_CHARS.contains(c))
7160            sb.append(c);
7161      }
7162
7163      return sb.toString();
7164   }
7165
7166   /**
7167    * Returns the substring after the first occurrence of a separator.
7168    *
7169    * <h4 class='section'>Example:</h4>
7170    * <p class='bjava'>
7171    *    substringAfter(<jk>null</jk>, <js>"."</js>);              <jc>// null</jc>
7172    *    substringAfter(<js>"hello.world"</js>, <jk>null</jk>);    <jc>// ""</jc>
7173    *    substringAfter(<js>"hello.world"</js>, <js>"."</js>);     <jc>// "world"</jc>
7174    *    substringAfter(<js>"hello.world"</js>, <js>"xyz"</js>);   <jc>// ""</jc>
7175    * </p>
7176    *
7177    * @param str The string to get a substring from.
7178    * @param separator The separator string.
7179    * @return The substring after the first occurrence of the separator, or empty string if separator not found.
7180    */
7181   public static String substringAfter(String str, String separator) {
7182      if (isEmpty(str))
7183         return str;
7184      if (separator == null)
7185         return "";
7186      var pos = str.indexOf(separator);
7187      if (pos == -1)
7188         return "";
7189      return str.substring(pos + separator.length());
7190   }
7191
7192   /**
7193    * Returns the substring before the first occurrence of a separator.
7194    *
7195    * <h4 class='section'>Example:</h4>
7196    * <p class='bjava'>
7197    *    substringBefore(<jk>null</jk>, <js>"."</js>);              <jc>// null</jc>
7198    *    substringBefore(<js>"hello.world"</js>, <jk>null</jk>);    <jc>// "hello.world"</jc>
7199    *    substringBefore(<js>"hello.world"</js>, <js>"."</js>);     <jc>// "hello"</jc>
7200    *    substringBefore(<js>"hello.world"</js>, <js>"xyz"</js>);   <jc>// "hello.world"</jc>
7201    * </p>
7202    *
7203    * @param str The string to get a substring from.
7204    * @param separator The separator string.
7205    * @return The substring before the first occurrence of the separator, or the original string if separator not found.
7206    */
7207   public static String substringBefore(String str, String separator) {
7208      if (isEmpty(str) || separator == null)
7209         return str;
7210      var pos = str.indexOf(separator);
7211      if (pos == -1)
7212         return str;
7213      return str.substring(0, pos);
7214   }
7215
7216   /**
7217    * Returns the substring between two delimiters.
7218    *
7219    * <h4 class='section'>Example:</h4>
7220    * <p class='bjava'>
7221    *    substringBetween(<jk>null</jk>, <js>"&lt;"</js>, <js>"&gt;"</js>);              <jc>// null</jc>
7222    *    substringBetween(<js>"&lt;hello&gt;"</js>, <js>"&lt;"</js>, <js>"&gt;"</js>);   <jc>// "hello"</jc>
7223    *    substringBetween(<js>"&lt;hello&gt;"</js>, <js>"["</js>, <js>"]"</js>);         <jc>// null</jc>
7224    * </p>
7225    *
7226    * @param str The string to get a substring from.
7227    * @param open The opening delimiter.
7228    * @param close The closing delimiter.
7229    * @return The substring between the delimiters, or <jk>null</jk> if delimiters not found.
7230    */
7231   public static String substringBetween(String str, String open, String close) {
7232      if (str == null || open == null || close == null)
7233         return null;
7234      var start = str.indexOf(open);
7235      if (start == -1)
7236         return null;
7237      var end = str.indexOf(close, start + open.length());
7238      if (end == -1)
7239         return null;
7240      return str.substring(start + open.length(), end);
7241   }
7242
7243   /**
7244    * Swaps the case of all characters in a string.
7245    *
7246    * <h4 class='section'>Example:</h4>
7247    * <p class='bjava'>
7248    *    swapCase(<js>"Hello World"</js>);   <jc>// "hELLO wORLD"</jc>
7249    *    swapCase(<js>"ABC123xyz"</js>);     <jc>// "abc123XYZ"</jc>
7250    * </p>
7251    *
7252    * @param str The string to process.
7253    * @return The string with case swapped, or <jk>null</jk> if input is <jk>null</jk>.
7254    */
7255   public static String swapCase(String str) {
7256      if (str == null)
7257         return null;
7258      var sb = new StringBuilder(str.length());
7259      for (var i = 0; i < str.length(); i++) {
7260         var c = str.charAt(i);
7261         if (LETTER_UC.contains(c))
7262            sb.append(Character.toLowerCase(c));
7263         else if (LETTER_LC.contains(c))
7264            sb.append(Character.toUpperCase(c));
7265         else
7266            sb.append(c);
7267      }
7268      return sb.toString();
7269   }
7270
7271   /**
7272    * Converts a string to Title Case format (first letter of each word capitalized, separated by spaces).
7273    *
7274    * <p>
7275    * Handles various input formats:
7276    * <ul>
7277    *   <li>CamelCase: "helloWorld" → "Hello World"</li>
7278    *   <li>PascalCase: "HelloWorld" → "Hello World"</li>
7279    *   <li>Snake_case: "hello_world" → "Hello World"</li>
7280    *   <li>Kebab-case: "hello-world" → "Hello World"</li>
7281    * </ul>
7282    *
7283    * <h4 class='section'>Example:</h4>
7284    * <p class='bjava'>
7285    *    titleCase(<jk>null</jk>);                    <jc>// null</jc>
7286    *    titleCase(<js>""</js>);                      <jc>// ""</jc>
7287    *    titleCase(<js>"hello world"</js>);           <jc>// "Hello World"</jc>
7288    *    titleCase(<js>"helloWorld"</js>);            <jc>// "Hello World"</jc>
7289    *    titleCase(<js>"hello_world"</js>);           <jc>// "Hello World"</jc>
7290    *    titleCase(<js>"hello-world"</js>);           <jc>// "Hello World"</jc>
7291    * </p>
7292    *
7293    * @param str The string to convert.
7294    * @return The Title Case string, or <jk>null</jk> if input is <jk>null</jk>.
7295    */
7296   public static String titleCase(String str) {
7297      if (str == null)
7298         return null;
7299      if (isEmpty(str))
7300         return str;
7301
7302      var words = splitWords(str);
7303      if (words.isEmpty())
7304         return "";
7305
7306      var result = new StringBuilder();
7307      for (var i = 0; i < words.size(); i++) {
7308         if (i > 0)
7309            result.append(' ');
7310         result.append(capitalize(words.get(i).toLowerCase()));
7311      }
7312
7313      return result.toString();
7314   }
7315
7316   /**
7317    * Converts the specified object to a comma-delimited list.
7318    *
7319    * @param o The object to convert.  Can be <jk>null</jk> (returns <jk>null</jk>).
7320    * @return The specified object as a comma-delimited list, or <jk>null</jk> if the input was <jk>null</jk>.
7321    */
7322   public static String toCdl(Object o) {
7323      if (o == null)
7324         return null;
7325      if (ClassUtils.isArray(o)) {
7326         var sb = new StringBuilder();
7327         for (int i = 0, j = Array.getLength(o); i < j; i++) {
7328            if (i > 0)
7329               sb.append(", ");
7330            sb.append(Array.get(o, i));
7331         }
7332         return sb.toString();
7333      }
7334      if (o instanceof Collection<?> o2)
7335         return join(o2, ", ");
7336      return o.toString();
7337   }
7338
7339   /**
7340    * Converts the specified byte into a 2 hexadecimal characters.
7341    *
7342    * @param b The number to convert to hex.
7343    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
7344    */
7345   public static String toHex(byte b) {
7346      var c = new char[2];
7347      var v = b & 0xFF;
7348      c[0] = HEX[v >>> 4];
7349      c[1] = HEX[v & 0x0F];
7350      return new String(c);
7351   }
7352
7353   /**
7354    * Converts a byte array into a simple hexadecimal character string.
7355    *
7356    * @param bytes The bytes to convert to hexadecimal.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
7357    * @return A new string consisting of hexadecimal characters.
7358    */
7359   public static String toHex(byte[] bytes) {
7360      var sb = new StringBuilder(bytes.length * 2);
7361      for (var element : bytes) {
7362         var v = element & 0xFF;
7363         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
7364      }
7365      return sb.toString();
7366   }
7367
7368   /**
7369    * Converts the contents of the specified input stream to a hex string.
7370    *
7371    * @param is The input stream to convert.
7372    * @return The hex string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>.
7373    */
7374   public static String toHex(InputStream is) {
7375      return safe(() -> is == null ? null : toHex(readBytes(is)));
7376   }
7377
7378   /**
7379    * Converts the specified number into a 2 hexadecimal characters.
7380    *
7381    * @param num The number to convert to hex.
7382    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
7383    */
7384   public static char[] toHex2(int num) {
7385      if (num < 0 || num > 255)
7386         throw new NumberFormatException("toHex2 can only be used on numbers between 0 and 255");
7387      var n = new char[2];
7388      var a = num % 16;
7389      n[1] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7390      a = (num / 16) % 16;
7391      n[0] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7392      return n;
7393   }
7394
7395   /**
7396    * Converts the specified number into a 4 hexadecimal characters.
7397    *
7398    * @param num The number to convert to hex.
7399    * @return A <code><jk>char</jk>[4]</code> containing the specified characters.
7400    * @throws NumberFormatException If the number is negative.
7401    */
7402   public static char[] toHex4(int num) {
7403      if (num < 0)
7404         throw new NumberFormatException("toHex4 can only be used on non-negative numbers");
7405      var n = new char[4];
7406      var a = num % 16;
7407      n[3] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7408      var base = 16;
7409      for (var i = 1; i < 4; i++) {
7410         a = (num / base) % 16;
7411         base <<= 4;
7412         n[3 - i] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7413      }
7414      return n;
7415   }
7416
7417   /**
7418    * Converts the specified number into a 8 hexadecimal characters.
7419    *
7420    * @param num The number to convert to hex.
7421    * @return A <code><jk>char</jk>[8]</code> containing the specified characters.
7422    * @throws NumberFormatException If the number is negative.
7423    */
7424   public static char[] toHex8(long num) {
7425      if (num < 0)
7426         throw new NumberFormatException("toHex8 can only be used on non-negative numbers");
7427      var n = new char[8];
7428      var a = num % 16;
7429      n[7] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7430      var base = 16;
7431      for (var i = 1; i < 8; i++) {
7432         a = (num / base) % 16;
7433         base <<= 4;
7434         n[7 - i] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7435      }
7436      return n;
7437   }
7438
7439   /**
7440    * Converts the specified object to an ISO8601 date string.
7441    *
7442    * @param c The object to convert.  Can be <jk>null</jk> (returns <jk>null</jk>).
7443    * @return The converted object, or <jk>null</jk> if the input was <jk>null</jk>.
7444    */
7445   public static String toIsoDate(Calendar c) {
7446      if (c == null) {
7447         return null;
7448      }
7449      // Convert Calendar to ZonedDateTime and format as ISO8601 date (YYYY-MM-DD)
7450      ZonedDateTime zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
7451      return zdt.format(DateTimeFormatter.ISO_LOCAL_DATE);
7452   }
7453
7454   /**
7455    * Converts the specified object to an ISO8601 date-time string.
7456    *
7457    * @param c The object to convert.  Can be <jk>null</jk> (returns <jk>null</jk>).
7458    * @return The converted object, or <jk>null</jk> if the input was <jk>null</jk>.
7459    */
7460   public static String toIsoDateTime(Calendar c) {
7461      if (c == null) {
7462         return null;
7463      }
7464      // Convert Calendar to ZonedDateTime and format as ISO8601 date-time with timezone
7465      ZonedDateTime zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
7466      return zdt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
7467   }
7468
7469   /**
7470    * Converts the specified bytes into a readable string.
7471    *
7472    * @param b The number to convert to hex.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
7473    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
7474    */
7475   public static String toReadableBytes(byte[] b) {
7476      var sb = new StringBuilder();
7477      for (var b2 : b)
7478         sb.append((b2 < ' ' || b2 > 'z') ? String.format("[%02X]", b2) : (char)b2 + "   ");
7479      sb.append("\n");
7480      for (var b2 : b)
7481         sb.append(String.format("[%02X]", b2));
7482      return sb.toString();
7483   }
7484
7485   /**
7486    * Same as {@link #toHex(byte[])} but puts spaces between the byte strings.
7487    *
7488    * @param bytes The bytes to convert to hexadecimal.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
7489    * @return A new string consisting of hexadecimal characters.
7490    */
7491   public static String toSpacedHex(byte[] bytes) {
7492      var sb = new StringBuilder(bytes.length * 3);
7493      for (var j = 0; j < bytes.length; j++) {
7494         if (j > 0)
7495            sb.append(' ');
7496         var v = bytes[j] & 0xFF;
7497         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
7498      }
7499      return sb.toString();
7500   }
7501
7502   /**
7503    * Safely converts an object to a string, returning <jk>null</jk> if the object is <jk>null</jk>.
7504    *
7505    * @param obj The object to convert to a string.
7506    * @return The string representation of the object, or <jk>null</jk> if the object is <jk>null</jk>.
7507    */
7508   public static String toString(Object obj) {
7509      return obj == null ? null : obj.toString();
7510   }
7511
7512   /**
7513    * Safely converts an object to a string, returning the default string if the object is <jk>null</jk>.
7514    *
7515    * @param obj The object to convert to a string.
7516    * @param defaultStr The default string to return if the object is <jk>null</jk>.
7517    * @return The string representation of the object, or the default string if the object is <jk>null</jk>.
7518    */
7519   public static String toString(Object obj, String defaultStr) {
7520      return obj == null ? defaultStr : obj.toString();
7521   }
7522
7523   /**
7524    * Converts a collection of strings to a string array.
7525    *
7526    * <p>
7527    * Returns an empty array if the collection is <jk>null</jk> or empty.
7528    *
7529    * @param collection The collection to convert. Can be <jk>null</jk> (returns an empty array).
7530    * @return A new string array containing the collection elements, or an empty array if the collection was <jk>null</jk>.
7531    */
7532   public static String[] toStringArray(Collection<String> collection) {
7533      if (collection == null)
7534         return new String[0];
7535      return collection.toArray(new String[collection.size()]);
7536   }
7537
7538   /**
7539    * Converts the specified object to a URI.
7540    *
7541    * @param o The object to convert to a URI.  Can be <jk>null</jk> (returns <jk>null</jk>).
7542    * @return A new URI, the same object if the object was already a URI, or <jk>null</jk> if the input was <jk>null</jk>.
7543    */
7544   public static URI toUri(Object o) {
7545      if (o == null || o instanceof URI)
7546         return (URI)o;
7547      try {
7548         return new URI(o.toString());
7549      } catch (URISyntaxException e) {
7550         throw toRuntimeException(e);
7551      }
7552   }
7553
7554   /**
7555    * Converts the specified byte array to a UTF-8 string.
7556    *
7557    * @param b The byte array to convert.
7558    * @return The UTF-8 string representation, or <jk>null</jk> if the array is <jk>null</jk>.
7559    */
7560   public static String toUtf8(byte[] b) {
7561      return b == null ? null : new String(b, UTF8);
7562   }
7563
7564   /**
7565    * Converts the contents of the specified input stream to a UTF-8 string.
7566    *
7567    * @param is The input stream to convert.
7568    * @return The UTF-8 string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>.
7569    */
7570   public static String toUtf8(InputStream is) {
7571      return safe(() -> is == null ? null : new String(readBytes(is), UTF8));
7572   }
7573
7574   /**
7575    * Transliterates characters in a string by mapping characters from one set to another.
7576    *
7577    * <p>
7578    * Performs character-by-character translation. If a character is found in <c>fromChars</c>,
7579    * it is replaced with the corresponding character at the same position in <c>toChars</c>.
7580    * Characters not found in <c>fromChars</c> are left unchanged.
7581    *
7582    * <h4 class='section'>Example:</h4>
7583    * <p class='bjava'>
7584    *    transliterate(<js>"hello"</js>, <js>"aeiou"</js>, <js>"12345"</js>);
7585    *    <jc>// "h2ll4"</jc>
7586    *    transliterate(<js>"ABC"</js>, <js>"ABC"</js>, <js>"XYZ"</js>);
7587    *    <jc>// "XYZ"</jc>
7588    * </p>
7589    *
7590    * @param str The string to transliterate. Can be <jk>null</jk> (returns <jk>null</jk>).
7591    * @param fromChars The source character set. Can be <jk>null</jk> (returns <c>str</c> unchanged).
7592    * @param toChars The target character set. Can be <jk>null</jk> (returns <c>str</c> unchanged).
7593    * @return The transliterated string, or <jk>null</jk> if input is <jk>null</jk>.
7594    * @throws IllegalArgumentException If <c>fromChars</c> and <c>toChars</c> have different lengths.
7595    */
7596   public static String transliterate(String str, String fromChars, String toChars) {
7597      if (str == null)
7598         return null;
7599      if (fromChars == null || toChars == null || fromChars.isEmpty() || toChars.isEmpty())
7600         return str;
7601      if (fromChars.length() != toChars.length())
7602         throw iaex("fromChars and toChars must have the same length");
7603
7604      var sb = new StringBuilder(str.length());
7605      for (var i = 0; i < str.length(); i++) {
7606         var c = str.charAt(i);
7607         var index = fromChars.indexOf(c);
7608         if (index >= 0)
7609            sb.append(toChars.charAt(index));
7610         else
7611            sb.append(c);
7612      }
7613      return sb.toString();
7614   }
7615
7616   /**
7617    * Same as {@link String#trim()} but prevents <c>NullPointerExceptions</c>.
7618    *
7619    * @param s The string to trim.
7620    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
7621    */
7622   public static String trim(String s) {
7623      if (s == null)
7624         return null;
7625      return s.trim();
7626   }
7627
7628   /**
7629    * Trims whitespace characters from the end of the specified string.
7630    *
7631    * @param s The string to trim.
7632    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
7633    */
7634   public static String trimEnd(String s) {
7635      if (isNotNull(s))
7636         while (isNotEmpty(s) && isWhitespace(s.charAt(s.length() - 1)))
7637            s = s.substring(0, s.length() - 1);
7638      return s;
7639   }
7640
7641   /**
7642    * Trims <js>'/'</js> characters from the beginning of the specified string.
7643    *
7644    * @param s The string to trim.
7645    * @return A new trimmed string, or the same string if no trimming was necessary.
7646    */
7647   public static String trimLeadingSlashes(String s) {
7648      if (s == null)
7649         return null;
7650      while (isNotEmpty(s) && s.charAt(0) == '/')
7651         s = s.substring(1);
7652      return s;
7653   }
7654
7655   /**
7656    * Trims <js>'/'</js> characters from both the start and end of the specified string.
7657    *
7658    * @param s The string to trim.
7659    * @return A new trimmed string, or the same string if no trimming was necessary.
7660    */
7661   public static String trimSlashes(String s) {
7662      if (s == null)
7663         return null;
7664      if (s.isEmpty())
7665         return s;
7666      while (endsWith(s, '/'))
7667         s = s.substring(0, s.length() - 1);
7668      while (isNotEmpty(s) && s.charAt(0) == '/')
7669         s = s.substring(1);
7670      return s;
7671   }
7672
7673   /**
7674    * Trims <js>'/'</js> and space characters from both the start and end of the specified string.
7675    *
7676    * @param s The string to trim.
7677    * @return A new trimmed string, or the same string if no trimming was necessary.
7678    */
7679   public static String trimSlashesAndSpaces(String s) {
7680      if (s == null)
7681         return null;
7682      while (isNotEmpty(s) && (s.charAt(s.length() - 1) == '/' || isWhitespace(s.charAt(s.length() - 1))))
7683         s = s.substring(0, s.length() - 1);
7684      while (isNotEmpty(s) && (s.charAt(0) == '/' || isWhitespace(s.charAt(0))))
7685         s = s.substring(1);
7686      return s;
7687   }
7688
7689   /**
7690    * Trims whitespace characters from the beginning of the specified string.
7691    *
7692    * @param s The string to trim.
7693    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
7694    */
7695   public static String trimStart(String s) {
7696      if (isNotNull(s))
7697         while (isNotEmpty(s) && isWhitespace(s.charAt(0)))
7698            s = s.substring(1);
7699      return s;
7700   }
7701
7702   /**
7703    * Trims <js>'/'</js> characters from the end of the specified string.
7704    *
7705    * @param s The string to trim.
7706    * @return A new trimmed string, or the same string if no trimming was necessary.
7707    */
7708   public static String trimTrailingSlashes(String s) {
7709      if (s == null)
7710         return null;
7711      while (endsWith(s, '/'))
7712         s = s.substring(0, s.length() - 1);
7713      return s;
7714   }
7715
7716   /**
7717    * Uncapitalizes the first character of a string.
7718    *
7719    * <h4 class='section'>Example:</h4>
7720    * <p class='bjava'>
7721    *    uncapitalize(<jk>null</jk>);          <jc>// null</jc>
7722    *    uncapitalize(<js>""</js>);            <jc>// ""</jc>
7723    *    uncapitalize(<js>"Hello"</js>);       <jc>// "hello"</jc>
7724    *    uncapitalize(<js>"hello"</js>);       <jc>// "hello"</jc>
7725    *    uncapitalize(<js>"HELLO"</js>);       <jc>// "hELLO"</jc>
7726    * </p>
7727    *
7728    * @param str The string to uncapitalize.
7729    * @return The string with the first character uncapitalized, or <jk>null</jk> if input is <jk>null</jk>.
7730    */
7731   public static String uncapitalize(String str) {
7732      if (isEmpty(str))
7733         return str;
7734      return Character.toLowerCase(str.charAt(0)) + str.substring(1);
7735   }
7736
7737   /**
7738    * Removes escape characters from the specified characters.
7739    *
7740    * @param s The string to remove escape characters from.  Can be <jk>null</jk> (returns <jk>null</jk>).
7741    * @param escaped The characters escaped.  Must not be <jk>null</jk> when the string is non-empty (a <jk>null</jk> argument throws {@link NullPointerException}).
7742    * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>.
7743    */
7744   public static String unescapeChars(String s, AsciiSet escaped) {
7745      if (s == null || s.isEmpty())
7746         return s;
7747      var count = 0;
7748      for (var i = 0; i < s.length(); i++)
7749         if (escaped.contains(s.charAt(i)))
7750            count++;
7751      if (count == 0)
7752         return s;
7753      var sb = new StringBuffer(s.length() - count);
7754      for (var i = 0; i < s.length(); i++) {
7755         var c = s.charAt(i);
7756
7757         if (c == '\\' && i + 1 != s.length()) {
7758            var c2 = s.charAt(i + 1);
7759            if (escaped.contains(c2)) {
7760               i++;
7761            }
7762         }
7763         sb.append(s.charAt(i));
7764      }
7765      return sb.toString();
7766   }
7767
7768   /**
7769    * Unescapes HTML entities in a string.
7770    *
7771    * <p>
7772    * Unescapes the following HTML entities:
7773    * <ul>
7774    *   <li><js>"&amp;amp;"</js> → <js>'&amp;'</js></li>
7775    *   <li><js>"&amp;lt;"</js> → <js>'&lt;'</js></li>
7776    *   <li><js>"&amp;gt;"</js> → <js>'&gt;'</js></li>
7777    *   <li><js>"&amp;quot;"</js> → <js>'"'</js></li>
7778    *   <li><js>"&amp;#39;"</js> or <js>"&amp;apos;"</js> → <js>'\''</js></li>
7779    * </ul>
7780    *
7781    * <h4 class='section'>Example:</h4>
7782    * <p class='bjava'>
7783    *    unescapeHtml(<js>"&amp;lt;script&amp;gt;"</js>);     <jc>// Returns: "&lt;script&gt;"</jc>
7784    * </p>
7785    *
7786    * @param str The string to unescape.
7787    * @return The unescaped string, or <jk>null</jk> if input is <jk>null</jk>.
7788    */
7789   public static String unescapeHtml(String str) {
7790      if (str == null)
7791         return null;
7792      // Must unescape &amp; last to avoid interfering with other entities
7793      return str.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'").replace("&apos;", "'").replace("&amp;", "&");
7794   }
7795
7796   /**
7797    * Unescapes XML entities in a string.
7798    *
7799    * <p>
7800    * Unescapes the following XML entities:
7801    * <ul>
7802    *   <li><js>"&amp;amp;"</js> → <js>'&amp;'</js></li>
7803    *   <li><js>"&amp;lt;"</js> → <js>'&lt;'</js></li>
7804    *   <li><js>"&amp;gt;"</js> → <js>'&gt;'</js></li>
7805    *   <li><js>"&amp;quot;"</js> → <js>'"'</js></li>
7806    *   <li><js>"&amp;apos;"</js> → <js>'\''</js></li>
7807    * </ul>
7808    *
7809    * <h4 class='section'>Example:</h4>
7810    * <p class='bjava'>
7811    *    unescapeXml(<js>"&amp;lt;tag&amp;gt;"</js>);     <jc>// Returns: "&lt;tag&gt;"</jc>
7812    * </p>
7813    *
7814    * @param str The string to unescape.
7815    * @return The unescaped string, or <jk>null</jk> if input is <jk>null</jk>.
7816    */
7817   public static String unescapeXml(String str) {
7818      if (str == null)
7819         return null;
7820      // Must unescape &amp; last to avoid interfering with other entities
7821      return str.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&apos;", "'").replace("&amp;", "&");
7822   }
7823
7824   /**
7825    * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for the specified character.
7826    *
7827    * @param c The character to create a sequence for.
7828    * @return An escaped-unicode sequence.
7829    */
7830   public static String unicodeSequence(char c) {
7831      var sb = new StringBuilder(6);
7832      sb.append('\\').append('u');
7833      for (var cc : toHex4(c))
7834         sb.append(cc);
7835      return sb.toString();
7836   }
7837
7838   /**
7839    * Null-safe convenience method for {@link String#toUpperCase()}.
7840    *
7841    * <p>
7842    * Converts the string to uppercase if not null.
7843    *
7844    * @param s The string to convert.
7845    * @return The uppercase string, or <jk>null</jk> if the input was <jk>null</jk>.
7846    * @see #lowerCase(String)
7847    * @see #upperCase(String)
7848    */
7849   public static String upperCase(String s) {
7850      return s == null ? null : s.toUpperCase();
7851   }
7852
7853   /**
7854    * Null-safe convenience method for {@link String#toUpperCase(Locale)} using {@link Locale#ROOT}.
7855    *
7856    * <p>
7857    * Locale-independent upper-casing for parse/match/normalize/key-fold code paths (as opposed to
7858    * {@link #upperCase(String)}, which uses the default locale and is intended for display casing).
7859    *
7860    * @param s The string to convert.
7861    * @return The uppercase string, or <jk>null</jk> if the input was <jk>null</jk>.
7862    * @see #lowerCaseRoot(String)
7863    * @see #upperCase(String)
7864    */
7865   public static String upperCaseRoot(String s) {
7866      return s == null ? null : s.toUpperCase(Locale.ROOT);
7867   }
7868
7869   /**
7870    * Decodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
7871    *
7872    * @param s The string to decode.
7873    * @return The decoded string, or <jk>null</jk> if input is <jk>null</jk>.
7874    */
7875   public static String urlDecode(String s) {
7876
7877      if (s == null)
7878         return s;
7879
7880      var needsDecode = false;
7881      for (var i = 0; i < s.length() && ! needsDecode; i++) {
7882         var c = s.charAt(i);
7883         if (c == '+' || c == '%')
7884            needsDecode = true;
7885      }
7886
7887      if (needsDecode) {
7888         return safe(()->URLDecoder.decode(s, UTF_8.name()));
7889      }
7890      return s;
7891   }
7892
7893   /**
7894    * Encodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
7895    *
7896    * @param s The string to encode.
7897    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
7898    */
7899   public static String urlEncode(String s) {
7900
7901      if (s == null)
7902         return null;
7903
7904      var needsEncode = false;
7905
7906      for (var i = 0; i < s.length() && ! needsEncode; i++)
7907         needsEncode |= (! URL_UNENCODED_CHARS.contains(s.charAt(i)));
7908
7909      if (needsEncode) {
7910         return safe(()->URLEncoder.encode(s, UTF_8.name()));
7911      }
7912
7913      return s;
7914   }
7915
7916   /**
7917    * Same as {@link #urlEncode(String)} except only escapes characters that absolutely need to be escaped.
7918    *
7919    * @param s The string to escape.
7920    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
7921    */
7922   public static String urlEncodeLax(String s) {
7923      if (s == null)
7924         return null;
7925      var needsEncode = false;
7926      for (var i = 0; i < s.length() && ! needsEncode; i++)
7927         needsEncode |= (! URL_UNENCODED_LAX_CHARS.contains(s.charAt(i)));
7928      if (needsEncode) {
7929         var sb = new StringBuilder(s.length() * 2);
7930         for (var i = 0; i < s.length(); i++) {
7931            var c = s.charAt(i);
7932            if (URL_UNENCODED_LAX_CHARS.contains(c))
7933               sb.append(c);
7934            else if (c == ' ')
7935               sb.append("+");
7936            else if (c <= 127)
7937               sb.append('%').append(toHex2(c));
7938            else
7939               safe(()->sb.append(URLEncoder.encode("" + c, UTF_8.name())));  // Yuck.
7940         }
7941         s = sb.toString();
7942      }
7943      return s;
7944   }
7945
7946   /**
7947    * Similar to {@link URLEncoder#encode(String, String)} but doesn't encode <js>"/"</js> characters.
7948    *
7949    * @param o The object to encode.
7950    * @return The URL encoded string, or <jk>null</jk> if the object was null.
7951    */
7952   public static String urlEncodePath(Object o) {
7953
7954      if (o == null)
7955         return null;
7956
7957      var s = stringify(o);
7958
7959      var needsEncode = false;
7960      for (var i = 0; i < s.length() && ! needsEncode; i++)
7961         needsEncode = URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i));
7962      if (! needsEncode)
7963         return s;
7964
7965      var sb = new StringBuilder();
7966      var caw = new CharArrayWriter();
7967      var caseDiff = ('a' - 'A');
7968
7969      for (var i = 0; i < s.length();) {
7970         var c = s.charAt(i);
7971         if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) {
7972            sb.append(c);
7973            i++;
7974         } else {
7975            if (c == ' ') {
7976               sb.append('+');
7977               i++;
7978            } else {
7979               do {
7980                  caw.write(c);
7981                  if (c >= 0xD800 && c <= 0xDBFF && (i + 1) < s.length()) {
7982                     int d = s.charAt(i + 1);
7983                     if (d >= 0xDC00 && d <= 0xDFFF) {
7984                        caw.write(d);
7985                        i++;
7986                     }
7987                  }
7988                  i++;
7989                  if (i < s.length()) {
7990                     c = s.charAt(i);
7991                  }
7992               } while (i < s.length() && ! URL_ENCODE_PATHINFO_VALIDCHARS.contains(c));
7993
7994               caw.flush();
7995               var s2 = new String(caw.toCharArray());
7996               var ba = s2.getBytes(UTF8);
7997               for (var element : ba) {
7998                  sb.append('%');
7999                  var ch = forDigit((element >> 4) & 0xF, 16);
8000                  if (isLetter(ch)) {
8001                     ch -= caseDiff;
8002                  }
8003                  sb.append(ch);
8004                  ch = forDigit(element & 0xF, 16);
8005                  if (isLetter(ch)) {
8006                     ch -= caseDiff;
8007                  }
8008                  sb.append(ch);
8009               }
8010               caw.reset();
8011            }
8012         }
8013      }
8014      return sb.toString();
8015   }
8016
8017   /**
8018    * Counts the number of words in a string.
8019    *
8020    * <p>
8021    * A word is defined as a sequence of one or more word characters (letters, digits, underscores)
8022    * separated by non-word characters.
8023    *
8024    * <h4 class='section'>Example:</h4>
8025    * <p class='bjava'>
8026    *    wordCount(<js>"Hello world"</js>);              <jc>// 2</jc>
8027    *    wordCount(<js>"The quick brown fox"</js>);      <jc>// 4</jc>
8028    *    wordCount(<js>"Hello, world! How are you?"</js>); <jc>// 5</jc>
8029    * </p>
8030    *
8031    * @param str The string to count words in. Can be <jk>null</jk>.
8032    * @return The number of words, or <c>0</c> if the string is <jk>null</jk> or empty.
8033    */
8034   public static int wordCount(String str) {
8035      if (isEmpty(str))
8036         return 0;
8037
8038      var count = 0;
8039      var inWord = false;
8040
8041      for (var i = 0; i < str.length(); i++) {
8042         var c = str.charAt(i);
8043         if ((LETTER.contains(c) || DIGIT.contains(c)) || c == '_') {
8044            if (! inWord) {
8045               count++;
8046               inWord = true;
8047            }
8048         } else {
8049            inWord = false;
8050         }
8051      }
8052
8053      return count;
8054   }
8055
8056   /**
8057    * Wraps text to a specified line length.
8058    *
8059    * <p>
8060    * Wraps text by breaking at word boundaries (spaces). Words longer than the wrap length
8061    * will be broken at the wrap length. Existing newlines are preserved.
8062    *
8063    * <h4 class='section'>Example:</h4>
8064    * <p class='bjava'>
8065    *    wrap(<js>"hello world test"</js>, 10);  <jc>// "hello world\ntest"</jc>
8066    *    wrap(<jk>null</jk>, 10);                <jc>// null</jc>
8067    * </p>
8068    *
8069    * @param str The string to wrap.
8070    * @param wrapLength The maximum line length (must be &gt; 0).
8071    * @return The wrapped string, or <jk>null</jk> if input is <jk>null</jk>.
8072    * @throws IllegalArgumentException if wrapLength is &lt;= 0.
8073    */
8074   public static String wrap(String str, int wrapLength) {
8075      return wrap(str, wrapLength, "\n");
8076   }
8077
8078   /**
8079    * Wraps text to a specified line length with a custom newline string.
8080    *
8081    * <p>
8082    * Wraps text by breaking at word boundaries (spaces). Words longer than the wrap length
8083    * will be broken at the wrap length. Existing newlines are preserved.
8084    *
8085    * <h4 class='section'>Example:</h4>
8086    * <p class='bjava'>
8087    *    wrap(<js>"hello world test"</js>, 10, <js>"<br>"</js>);  <jc>// "hello world<br>test"</jc>
8088    *    wrap(<jk>null</jk>, 10, <js>"\n"</js>);                  <jc>// null</jc>
8089    * </p>
8090    *
8091    * @param str The string to wrap.
8092    * @param wrapLength The maximum line length (must be &gt; 0).
8093    * @param newline The string to use as line separator.
8094    * @return The wrapped string, or <jk>null</jk> if input is <jk>null</jk>.
8095    * @throws IllegalArgumentException if wrapLength is &lt;= 0 or newline is <jk>null</jk>.
8096    */
8097   public static String wrap(String str, int wrapLength, String newline) {
8098      if (str == null)
8099         return null;
8100      if (isEmpty(str))
8101         return str;
8102      if (wrapLength <= 0)
8103         throw iaex("wrapLength must be > 0: %s", wrapLength);
8104      if (newline == null)
8105         throw iaex("newline cannot be null");
8106
8107      var result = new StringBuilder();
8108      var lines = str.split("\r?\n", -1);  // Preserve empty lines
8109
8110      for (var lineIdx = 0; lineIdx < lines.length; lineIdx++) {
8111         var line = lines[lineIdx];
8112         if (line.isEmpty()) {
8113            if (lineIdx < lines.length - 1)
8114               result.append(newline);
8115            continue;
8116         }
8117
8118         // Split into words first, then combine words that fit
8119         var words = line.split(" +");  // Split on one or more spaces
8120         var currentLine = new StringBuilder();
8121
8122         for (var word : words) {
8123            if (word.isEmpty())
8124               continue;
8125
8126            var wordLength = word.length();
8127            var currentLength = currentLine.length();
8128
8129            if (currentLength == 0) {
8130               // First word on line
8131               // Only break single words if there are multiple words in the input
8132               // (single long words should not be broken for readability)
8133               if (wordLength > wrapLength && words.length > 1) {
8134                  // Word is too long and there are other words, break it
8135                  if (!result.isEmpty())
8136                     result.append(newline);
8137                  var wordPos = 0;
8138                  while (wordPos < wordLength) {
8139                     if (wordPos > 0)
8140                        result.append(newline);
8141                     var remaining = wordLength - wordPos;
8142                     if (remaining <= wrapLength) {
8143                        result.append(word.substring(wordPos));
8144                        break;
8145                     }
8146                     result.append(word.substring(wordPos, wordPos + wrapLength));
8147                     wordPos += wrapLength;
8148                  }
8149               } else {
8150                  currentLine.append(word);
8151               }
8152            } else {
8153               // Check if we can add this word to current line
8154               var neededLength = currentLength + 1 + wordLength;  // current + space + word
8155               // Break if it would fit exactly or exceed - prefer breaking for readability
8156               if (neededLength < wrapLength) {
8157                  // Fits with room to spare
8158                  currentLine.append(' ').append(word);
8159               } else {
8160                  // Doesn't fit or fits exactly - start new line
8161                  if (!result.isEmpty())
8162                     result.append(newline);
8163                  result.append(currentLine);
8164                  currentLine.setLength(0);
8165                  if (wordLength > wrapLength && words.length > 1) {
8166                     // Word is too long and there are other words, break it
8167                     result.append(newline);
8168                     var wordPos = 0;
8169                     while (wordPos < wordLength) {
8170                        if (wordPos > 0)
8171                           result.append(newline);
8172                        var remaining = wordLength - wordPos;
8173                        if (remaining <= wrapLength) {
8174                           result.append(word.substring(wordPos));
8175                           break;
8176                        }
8177                        result.append(word.substring(wordPos, wordPos + wrapLength));
8178                        wordPos += wrapLength;
8179                     }
8180                  } else {
8181                     currentLine.append(word);
8182                  }
8183               }
8184            }
8185         }
8186
8187         // Append any remaining line
8188         if (!currentLine.isEmpty()) {
8189            if (!result.isEmpty())
8190               result.append(newline);
8191            result.append(currentLine);
8192         }
8193      }
8194
8195      return result.toString();
8196   }
8197
8198   /**
8199    * Helper method to estimate the number of syllables in a word.
8200    */
8201   private static int estimateSyllables(String word) {
8202
8203      var lower = word.toLowerCase(Locale.ROOT);
8204      var count = 0;
8205      var prevWasVowel = false;
8206
8207      for (var i = 0; i < lower.length(); i++) {
8208         var c = lower.charAt(i);
8209         var isVowel = (VOWEL.contains(c) || c == 'y');
8210
8211         if (isVowel && ! prevWasVowel) {
8212            count++;
8213         }
8214         prevWasVowel = isVowel;
8215      }
8216
8217      // Handle silent 'e' at the end
8218      if (lower.endsWith("e") && count > 1) {
8219         count--;
8220      }
8221
8222      // At least one syllable
8223      return Math.max(1, count);
8224   }
8225
8226   /**
8227    * Finds the first non-whitespace, non-comment character in a string.
8228    *
8229    * @param s The string to analyze.
8230    * @return The first real character, or <c>-1</c> if none found.
8231    */
8232   private static int firstRealCharacter(String s) {
8233      return safe(()-> {
8234         var r = new StringReader(s);
8235         var c = 0;
8236         while ((c = r.read()) != -1) {
8237            if (! isWhitespace(c)) {
8238               if (c == '/') {
8239                  skipComments(r);
8240               } else {
8241                  return c;
8242               }
8243            }
8244         }
8245         return -1;
8246      });
8247   }
8248
8249   /**
8250    * Helper method to get Soundex code for a character.
8251    */
8252   private static char getSoundexCode(char c) {
8253      if (c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' || c == 'H' || c == 'W' || c == 'Y')
8254         return '0';
8255      if (c == 'B' || c == 'F' || c == 'P' || c == 'V')
8256         return '1';
8257      if (c == 'C' || c == 'G' || c == 'J' || c == 'K' || c == 'Q' || c == 'S' || c == 'X' || c == 'Z')
8258         return '2';
8259      if (c == 'D' || c == 'T')
8260         return '3';
8261      if (c == 'L')
8262         return '4';
8263      if (c == 'M' || c == 'N')
8264         return '5';
8265      if (c == 'R')
8266         return '6';
8267      return '0'; // Non-letter characters
8268   }
8269
8270   /**
8271    * Validates if a string is a valid IPv6 address format (without network operations).
8272    *
8273    * <p>
8274    * This method performs pure string-based validation and does not perform any DNS lookups
8275    * or network operations, making it fast and suitable for validation purposes.
8276    *
8277    * @param ip The IPv6 address string to validate.
8278    * @return <jk>true</jk> if the string is a valid IPv6 address format, <jk>false</jk> otherwise.
8279    */
8280   @SuppressWarnings({
8281      "java:S1313" // IPv6 validation intentionally compares against canonical literals like :: and ::ffff
8282   })
8283   public static boolean isValidIPv6Address(String ip) {
8284      if (ip == null || ip.isEmpty())
8285         return false;
8286
8287      // IPv6 addresses can be:
8288      // 1. Full format: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (8 groups of 4 hex digits)
8289      // 2. Compressed format: 2001:db8::1 (uses :: to represent consecutive zeros)
8290      // 3. IPv4-mapped: ::ffff:192.168.1.1 (last 32 bits as IPv4)
8291      // 4. Loopback: ::1
8292      // 5. Unspecified: ::
8293
8294      // Cannot start or end with a single colon (except ::)
8295      if (ip.startsWith(":") && !ip.startsWith("::"))
8296         return false;
8297      if (ip.endsWith(":") && !ip.endsWith("::"))
8298         return false;
8299
8300      // Check for IPv4-mapped format (contains both : and .)
8301      if (ip.contains(".")) {
8302         // Must be in format ::ffff:x.x.x.x or similar
8303         var lastColon = ip.lastIndexOf(":");
8304         if (lastColon < 0)
8305            return false;
8306         var ipv4Part = ip.substring(lastColon + 1);
8307         // Validate IPv4 part
8308         var ipv4Parts = ipv4Part.split("\\.");
8309         if (ipv4Parts.length != 4)
8310            return false;
8311         for (var part : ipv4Parts) {
8312            try {
8313               var num = Integer.parseInt(part);
8314               if (num < 0 || num > 255)
8315                  return false;
8316            } catch (@SuppressWarnings("unused") NumberFormatException e) {
8317               return false;
8318            }
8319         }
8320         // Validate IPv6 part before the IPv4
8321         var ipv6Part = ip.substring(0, lastColon);
8322         // Accept empty, ::, ::ffff, ::FFFF, or : (when string starts with ::)
8323         if (ipv6Part.isEmpty() || ipv6Part.equals("::") || ipv6Part.equals("::ffff") || ipv6Part.equals("::FFFF") || (ipv6Part.equals(":") && ip.startsWith("::")))
8324            return true;
8325         // More complex validation would be needed for other IPv4-mapped formats
8326         // For now, accept common formats
8327      }
8328
8329      // Check for :: (compression) - only one allowed
8330      var doubleColonCount = 0;
8331      for (var i = 1; i < ip.length(); i++) {
8332         if (ip.charAt(i) == ':' && ip.charAt(i - 1) == ':') {
8333            doubleColonCount++;
8334            if (doubleColonCount > 1)
8335               return false; // Only one :: allowed
8336         }
8337      }
8338
8339      // Split by ::
8340      var parts = ip.split("::", -1);
8341
8342      if (parts.length == 2) {
8343         // Compressed format
8344         var leftParts = parts[0].isEmpty() ? new String[0] : parts[0].split(":");
8345         var rightParts = parts[1].isEmpty() ? new String[0] : parts[1].split(":");
8346         var totalParts = leftParts.length + rightParts.length;
8347         if (totalParts > 7)
8348            return false; // Too many groups (max 8, but :: counts as one or more)
8349      } else {
8350         // Full format (no compression)
8351         var groups = ip.split(":");
8352         if (groups.length != 8)
8353            return false;
8354      }
8355
8356      // Validate each hex group
8357      var groups = ip.split("::");
8358      for (var groupSection : groups) {
8359         if (groupSection.isEmpty())
8360            continue; // Skip empty section from ::
8361         var groupParts = groupSection.split(":");
8362         for (var group : groupParts) {
8363            if (group.length() > 4)
8364               return false; // Each group is max 4 hex digits
8365            // Validate hex digits
8366            for (var i = 0; i < group.length(); i++) {
8367               var c = group.charAt(i);
8368               if (! HEXADECIMAL_CHARS.contains(c))
8369                  return false;
8370            }
8371         }
8372      }
8373
8374      return true;
8375   }
8376
8377
8378   private static class Readifier {
8379      private final Class<?> type;
8380      private final Function<Object,String> bridge;
8381
8382      private <T> Readifier(Class<T> type, Function<? super T,String> converter) {
8383         this.type = type;
8384         this.bridge = o -> converter.apply(type.cast(o));
8385      }
8386
8387      Class<?> forClass() {
8388         return type;
8389      }
8390
8391      Function<Object,String> toFunction() {
8392         return bridge;
8393      }
8394   }
8395
8396   private static <T> Readifier readifier(Class<T> type, Function<? super T,String> converter) {
8397      return new Readifier(type, converter);
8398   }
8399
8400   private static List<Readifier> loadReadifiers() {
8401      var list = new ArrayList<Readifier>();
8402
8403      // More specific types first - order matters!
8404      list.add(readifier(Map.Entry.class, x -> readable(x.getKey()) + '=' + readable(x.getValue())));
8405      list.add(readifier(Collection.class, x -> ((Collection<?>)x).stream().map(StringUtils::readable).collect(joining(",", "[", "]"))));
8406      list.add(readifier(Map.class, x -> ((Map<?,?>)x).entrySet().stream().map(StringUtils::readable).collect(joining(",", "{", "}"))));
8407      list.add(readifier(Iterable.class, x -> readable(toList(x))));
8408      list.add(readifier(Iterator.class, x -> readable(toList(x))));
8409      list.add(readifier(Enumeration.class, x -> readable(toList(x))));
8410      list.add(readifier(Optional.class, x -> readable(((Optional<?>)x).orElse(null))));
8411      list.add(readifier(GregorianCalendar.class, x -> x.toZonedDateTime().format(DateTimeFormatter.ISO_INSTANT)));
8412      list.add(readifier(Date.class, x -> x.toInstant().toString()));
8413      list.add(readifier(InputStream.class, x -> toHex(x)));
8414      list.add(readifier(Reader.class, (Reader x) -> safe(() -> read(x))));
8415      list.add(readifier(File.class, (File x) -> safe(() -> read(x))));
8416      list.add(readifier(byte[].class, x -> toHex(x)));
8417      list.add(readifier(Enum.class, x -> ((Enum<?>)x).name()));
8418      list.add(readifier(Class.class, ClassUtils::classNameSimple));
8419      list.add(readifier(Constructor.class, x -> ConstructorInfo.of(x).getNameFull()));
8420      list.add(readifier(Method.class, x -> MethodInfo.of(x).getNameFull()));
8421      list.add(readifier(Field.class, x -> FieldInfo.of(x).toString()));
8422      list.add(readifier(Parameter.class, x -> ParameterInfo.of(x).toString()));
8423      list.add(readifier(ClassInfo.class, ClassInfo::toString));
8424      list.add(readifier(MethodInfo.class, MethodInfo::toString));
8425      list.add(readifier(ConstructorInfo.class, ConstructorInfo::toString));
8426      list.add(readifier(FieldInfo.class, FieldInfo::toString));
8427      list.add(readifier(ParameterInfo.class, ParameterInfo::toString));
8428
8429      return Collections.unmodifiableList(list);
8430   }
8431
8432   /**
8433    * Determines the multiplier value based on the suffix character in a string.
8434    *
8435    * @param s The string to analyze for multiplier suffix.
8436    * @return The multiplier value (1 if no valid suffix found).
8437    */
8438   private static int multiplierInt(String s) {
8439      var c = isEmpty(s) ? 'z' : s.charAt(s.length() - 1);
8440      if (c == 'G')
8441         return 1024 * 1024 * 1024;
8442      if (c == 'M')
8443         return 1024 * 1024;
8444      if (c == 'K')
8445         return 1024;
8446      if (c == 'g')
8447         return 1000 * 1000 * 1000;
8448      if (c == 'm')
8449         return 1000 * 1000;
8450      if (c == 'k')
8451         return 1000;
8452      return 1;
8453   }
8454
8455   /**
8456    * Determines the long multiplier value based on the suffix character in a string.
8457    *
8458    * @param s The string to analyze for multiplier suffix.
8459    * @return The multiplier value (1 if no valid suffix found).
8460    */
8461   private static long multiplierLong(String s) {
8462      if (isEmpty(s))
8463         return 1;
8464      var c = s.charAt(s.length() - 1);
8465      if (c == 'P')
8466         return 1125899906842624L;  // 1024^5
8467      if (c == 'T')
8468         return 1099511627776L;  // 1024^4
8469      if (c == 'G')
8470         return 1073741824L;  // 1024^3
8471      if (c == 'M')
8472         return 1048576L;  // 1024^2
8473      if (c == 'K')
8474         return 1024L;
8475      if (c == 'p')
8476         return 1000000000000000L;  // 1000^5
8477      if (c == 't')
8478         return 1000000000000L;  // 1000^4
8479      if (c == 'g')
8480         return 1000000000L;  // 1000^3
8481      if (c == 'm')
8482         return 1000000L;  // 1000^2
8483      if (c == 'k')
8484         return 1000L;
8485      return 1;
8486   }
8487
8488   /**
8489    * Parses a unit string and converts the value to milliseconds.
8490    *
8491    * @param unit The unit string (case-insensitive, already lowercased).
8492    * @param value The numeric value.
8493    * @return The value in milliseconds, or <c>-1</c> if the unit is invalid.
8494    */
8495   private static long parseUnit(String unit, double value) {
8496      if (isEmpty(unit)) {
8497         // No unit means milliseconds
8498         return (long)value;
8499      }
8500
8501      // Check milliseconds first (before minutes) - must check exact "ms" before checking "m"
8502      if (unit.equals("ms") || unit.equals("millis") || unit.equals("milliseconds"))
8503         return (long)value;
8504
8505      // Seconds
8506      if (unit.startsWith("s") && !unit.startsWith("sec"))
8507         return (long)(value * 1000);
8508      if (unit.startsWith("sec") || unit.startsWith("second"))
8509         return (long)(value * 1000);
8510
8511      // Minutes (must check after milliseconds and months)
8512      if (unit.startsWith("m") && !unit.startsWith("mo") && !unit.startsWith("mill") && !unit.startsWith("ms"))
8513         return (long)(value * 1000 * 60);
8514      if (unit.startsWith("min") || unit.startsWith("minute"))
8515         return (long)(value * 1000 * 60);
8516
8517      // Hours
8518      if (unit.startsWith("h") || unit.startsWith("hour"))
8519         return (long)(value * 1000 * 60 * 60);
8520
8521      // Days
8522      if (unit.startsWith("d") && !unit.startsWith("da"))
8523         return (long)(value * 1000 * 60 * 60 * 24);
8524      if (unit.startsWith("day"))
8525         return (long)(value * 1000 * 60 * 60 * 24);
8526
8527      // Weeks
8528      if (unit.startsWith("w") || unit.startsWith("week"))
8529         return (long)(value * 1000 * 60 * 60 * 24 * 7);
8530
8531      // Months (30 days)
8532      if (unit.startsWith("mo") || unit.startsWith("month"))
8533         return (long)(value * 1000 * 60 * 60 * 24 * 30);
8534
8535      // Years (365 days)
8536      if (unit.startsWith("y") && !unit.startsWith("yr"))
8537         return (long)(value * 1000 * 60 * 60 * 24 * 365);
8538      if (unit.startsWith("yr") || unit.startsWith("year"))
8539         return (long)(value * 1000 * 60 * 60 * 24 * 365);
8540
8541      // Unknown unit
8542      return -1;
8543   }
8544
8545   /**
8546    * Skips over a single comment sequence in a StringReader.
8547    *
8548    * <p>
8549    * The reader must be positioned at the first <js>'/'</js> character of a comment.
8550    * This method will skip only the comment it's currently positioned on, not all comments in the reader.
8551    *
8552    * <p>
8553    * Supports both <js>"/* * /"</js> style block comments and <js>"//"</js> style line comments.
8554    *
8555    * @param r The StringReader positioned at the start of a comment (at the first <js>'/'</js>).  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
8556    * @throws IOException If an I/O error occurs.
8557    */
8558   public static void skipComments(StringReader r) throws IOException {
8559      var c = r.read();
8560      //  "/* */" style comments
8561      if (c == '*') {
8562         c = r.read();
8563         while (c != -1) {
8564            if (c == '*') {
8565               c = r.read();
8566               if (c == '/')
8567                  return;
8568               // If not '/', continue checking from this character
8569               // Don't read again, just continue the loop
8570            } else {
8571               c = r.read();
8572            }
8573         }
8574         //  "//" style comments
8575      } else if (c == '/') {
8576         while ((c = r.read()) != -1) {
8577            if (c == '\n')
8578               return;
8579         }
8580      }
8581   }
8582
8583   /**
8584    * Gets or creates an AsciiSet for escaping the specified character.
8585    *
8586    * @param c The character to create an escape set for.
8587    * @return An AsciiSet containing the character and backslash.
8588    */
8589   private static AsciiSet getEscapeSet(char c) {
8590      return ESCAPE_SETS.computeIfAbsent(c, key -> AsciiSet.create().chars(key, '\\').build());
8591   }
8592
8593   /**
8594    * Helper method to split a string into words.
8595    * Detects word boundaries from separators (spaces, underscores, hyphens) and case changes.
8596    *
8597    * @param str The string to split.
8598    * @return A list of words, or empty list if input is null or empty.
8599    */
8600   private static List<String> splitWords(String str) {
8601      if (str == null || isEmpty(str))
8602         return Collections.emptyList();
8603
8604      var words = new ArrayList<String>();
8605      var sb = new StringBuilder();
8606      var wasLowerCase = false;
8607      var wasUpperCase = false;
8608      var consecutiveUpperCount = 0;
8609
8610      for (var i = 0; i < str.length(); i++) {
8611         var c = str.charAt(i);
8612         var isSeparator = (c == ' ' || c == '_' || c == '-' || c == '\t');
8613         var isUpperCase = LETTER_UC.contains(c);
8614         var isLowerCase = LETTER_LC.contains(c);
8615         var isLetter = LETTER.contains(c);
8616
8617         if (isSeparator) {
8618            if (!sb.isEmpty()) {
8619               words.add(sb.toString());
8620               sb.setLength(0);
8621            }
8622            wasLowerCase = false;
8623            wasUpperCase = false;
8624            consecutiveUpperCount = 0;
8625         } else if (isLetter) {
8626            // Detect word boundary:
8627            // 1. Uppercase after lowercase (e.g., "helloWorld" → "hello", "World")
8628            // 2. Uppercase after consecutive uppercase when next is lowercase (e.g., "XMLHttp" → "XML", "Http")
8629            // 3. Lowercase after 2+ consecutive uppercase (e.g., "XMLHttp" → "XML", "Http")
8630            if (!sb.isEmpty()) {
8631               if (isUpperCase && wasLowerCase) {
8632                  // Case 1: uppercase after lowercase (e.g., "helloWorld" → "hello", "World")
8633                  words.add(sb.toString());
8634                  sb.setLength(0);
8635                  consecutiveUpperCount = 0;
8636               } else if (isUpperCase && wasUpperCase && consecutiveUpperCount >= 2) {
8637                  // Case 2: uppercase after uppercase - check if this starts a new word
8638                  // Look ahead to see if next character is lowercase
8639                  // This handles "XMLHttp" where 'H' starts "Http"
8640                  // We need at least 2 consecutive uppercase letters before this one to split
8641                  if (i + 1 < str.length()) {
8642                     var nextChar = str.charAt(i + 1);
8643                     if (LETTER_LC.contains(nextChar)) {
8644                        // This uppercase starts a new word, split before it
8645                        words.add(sb.toString());
8646                        sb.setLength(0);
8647                        consecutiveUpperCount = 0;
8648                     }
8649                  }
8650               } else if (isLowerCase && wasUpperCase && consecutiveUpperCount >= 2) {
8651                  // Case 3: lowercase after 2+ consecutive uppercase
8652                  // Split all but the last uppercase (e.g., "XMLH" → "XML" + "H")
8653                  var splitPoint = sb.length() - 1;
8654                  words.add(sb.substring(0, splitPoint));
8655                  sb.delete(0, splitPoint);
8656                  consecutiveUpperCount = 0;
8657               }
8658            }
8659            sb.append(c);
8660            // Update state AFTER appending
8661            wasLowerCase = isLowerCase;
8662            wasUpperCase = isUpperCase;
8663            if (isUpperCase) {
8664               consecutiveUpperCount++;
8665            } else {
8666               consecutiveUpperCount = 0;
8667            }
8668         } else {
8669            // Non-letter characters (digits, etc.) - treat as part of current word
8670            sb.append(c);
8671            wasLowerCase = false;
8672            wasUpperCase = false;
8673         consecutiveUpperCount = 0;
8674      }
8675   }
8676
8677   if (!sb.isEmpty())
8678      words.add(sb.toString());
8679
8680      return words;
8681   }
8682
8683   /**
8684    * Null-safe startsWith (both args null-checked).
8685    *
8686    * @param s The string to test.
8687    * @param prefix The prefix to test for.
8688    * @return <jk>true</jk> if s starts with prefix (both non-null).
8689    */
8690   public static boolean startsWith(String s, String prefix) {
8691      return s != null && prefix != null && s.startsWith(prefix);
8692   }
8693
8694   /**
8695    * Returns the string, or blank ("") if null.
8696    *
8697    * @param value The string value.
8698    * @return The value, or "" if null.
8699    */
8700   public static String blankIfNull(String value) { return coalesce(value, ""); }
8701
8702   /**
8703    * Creates a new {@link StringBuilder} initialized with the stringified form of the specified value.
8704    *
8705    * <p>
8706    * The value is rendered via {@link ObjectUtils#stringify(Object)} (a <jk>null</jk> value renders as
8707    * the literal string {@code "null"}). Using the stringified form avoids the
8708    * {@link StringBuilder#StringBuilder(int) StringBuilder(int capacity)} footgun that would otherwise
8709    * apply when passing a numeric value.
8710    *
8711    * @param value The initial value. Can be <jk>null</jk>.
8712    * @return A new StringBuilder containing the stringified value.
8713    */
8714   public static StringBuilder newStringBuilder(Object value) { return new StringBuilder(value == null ? "null" : stringify(value)); }
8715
8716   /**
8717    * Creates a new {@link StringBuilder} with the stringified form of each specified value appended in order.
8718    *
8719    * <p>
8720    * Each value is rendered via {@link ObjectUtils#stringify(Object)} (a <jk>null</jk> value renders as
8721    * the literal string {@code "null"}). With no arguments, an empty builder is returned.
8722    *
8723    * @param values The values to append in order. Individual values can be <jk>null</jk>.
8724    * @return A new StringBuilder containing the appended values.
8725    */
8726   public static StringBuilder newStringBuilder(Object...values) {
8727      var b = new StringBuilder();
8728      for (var v : values)
8729         b.append(stringify(v));
8730      return b;
8731   }
8732
8733   /**
8734    * Inverse of {@link #equalsIgnoreCase(String, String)}.
8735    *
8736    * @param s1 String 1.
8737    * @param s2 String 2.
8738    * @return <jk>true</jk> if the strings are not equal ignoring case.
8739    */
8740   public static boolean notEqualsIgnoreCase(String s1, String s2) { return ! equalsIgnoreCase(s1, s2); }
8741
8742   /**
8743    * Returns the specified string, or <jk>null</jk> if that string is <jk>null</jk> or empty.
8744    *
8745    * @param value The string value to check.
8746    * @return The string value, or <jk>null</jk> if the string is <jk>null</jk> or empty.
8747    */
8748   public static String nullIfEmpty(String value) {
8749      return isEmpty(value) ? null : value;
8750   }
8751
8752   /**
8753    * Constructor.
8754    */
8755   protected StringUtils() {}
8756}