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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Escape mappings:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Injection vectors this defends against (do not "simplify" these away):</h5>
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    * <h5 class='section'>Why <c>&amp;</c> and {@code >} are deliberately NOT escaped:</h5>
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    * <h5 class='section'>Why the substitutions are safe:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Format Support:</h5>
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    * <h5 class='section'>Printf Format Specifiers:</h5>
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    * <h5 class='section'>Format Specifier Syntax:</h5>
2195    * <p>
2196    * Printf format specifiers follow this pattern: <c>%[argument_index$][flags][width][.precision]conversion</c>
2197    * </p>
2198    *
2199    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Comparison with mformat():</h5>
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    * <h5 class='section'>Null Handling:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Format Examples:</h5>
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    * <h5 class='section'>Supported Units:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Performance Note:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Examples:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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    * <h5 class='section'>Example:</h5>
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   /**
5502    * Parses a number from the specified string.
5503    *
5504    * <p>
5505    * Supports Java 7+ numeric literals with underscores (e.g., <js>"1_000_000"</js>).
5506    * The underscores are automatically removed before parsing.
5507    *
5508    * @param s The string to parse the number from.
5509    * @param type
5510    *    The number type to created.
5511    *    Can be any of the following:
5512    *    <ul>
5513    *       <li> Integer (or <c>int</c> primitive)
5514    *       <li> Long (or <c>long</c> primitive)
5515    *       <li> Short (or <c>short</c> primitive)
5516    *       <li> Byte (or <c>byte</c> primitive)
5517    *       <li> Float (or <c>float</c> primitive)
5518    *       <li> Double (or <c>double</c> primitive)
5519    *       <li> BigInteger
5520    *       <li> BigDecimal
5521    *       <li> AtomicInteger
5522    *       <li> AtomicLong
5523    *    </ul>
5524    *    If <jk>null</jk> or <c>Number</c>, uses the best guess.
5525    * @return The parsed number, or <jk>null</jk> if the string was null.
5526    */
5527   public static Number parseNumber(String s, Class<? extends Number> type) {
5528      if (s == null)
5529         return null;
5530      if (s.isEmpty())
5531         s = "0";
5532      if (type == null)
5533         type = Number.class;
5534
5535      // Remove underscores (Java 7+ numeric literal support) before parsing
5536      // Note: We do this before type detection to ensure clean parsing
5537      s = s.replace("_", "");
5538
5539      // Determine the data type if it wasn't specified.
5540      var isAutoDetect = (type == Number.class);
5541      var isDecimal = false;
5542      if (isAutoDetect) {
5543         // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how
5544         // long the string is.
5545         // An integer range is -2,147,483,648 to 2,147,483,647
5546         // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
5547         isDecimal = isDecimal(s);
5548         if (isDecimal) {
5549            if (s.length() > 20)
5550               type = Double.class;
5551            else if (s.length() >= 10)
5552               type = Long.class;
5553            else
5554               type = Integer.class;
5555         } else if (isFloat(s))
5556            type = Double.class;
5557         else
5558            throw new NumberFormatException(s);
5559      }
5560
5561      if (type == Double.class || type == Double.TYPE) {
5562         return Double.valueOf(s);
5563      }
5564      if (type == Float.class || type == Float.TYPE)
5565         return Float.valueOf(s);
5566      if (type == BigDecimal.class)
5567         return new BigDecimal(s);
5568      if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) {
5569         try {
5570            var l = parseLongWithSuffix(s);
5571            if (type == AtomicLong.class)
5572               return new AtomicLong(l);
5573            if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) {
5574               // This occurs if the string is 10 characters long but is still a valid integer value.
5575               return (int)l;
5576            }
5577            return l;
5578         } catch (NumberFormatException e) {
5579            if (isAutoDetect) {
5580               // This occurs if the string is 20 characters long but still falls outside the range of a valid long.
5581               return Double.valueOf(s);
5582            }
5583            throw e;
5584         }
5585      }
5586      if (type == Integer.class || type == Integer.TYPE)
5587         return Integer.decode(s);
5588      if (type == Short.class || type == Short.TYPE)
5589         return Short.decode(s);
5590      if (type == Byte.class || type == Byte.TYPE)
5591         return Byte.decode(s);
5592      if (type == BigInteger.class)
5593         return new BigInteger(s);
5594      if (type == AtomicInteger.class)
5595         return new AtomicInteger(Integer.decode(s));
5596      throw new NumberFormatException("Unsupported Number type: " + type.getName());
5597   }
5598
5599   /**
5600    * Converts a string to PascalCase format.
5601    *
5602    * <p>
5603    * Handles various input formats:
5604    * <ul>
5605    *   <li>Space-separated: "hello world" → "HelloWorld"</li>
5606    *   <li>CamelCase: "helloWorld" → "HelloWorld"</li>
5607    *   <li>Snake_case: "hello_world" → "HelloWorld"</li>
5608    *   <li>Kebab-case: "hello-world" → "HelloWorld"</li>
5609    * </ul>
5610    *
5611    * <h5 class='section'>Example:</h5>
5612    * <p class='bjava'>
5613    *    pascalCase(<jk>null</jk>);                    <jc>// null</jc>
5614    *    pascalCase(<js>""</js>);                      <jc>// ""</jc>
5615    *    pascalCase(<js>"hello world"</js>);           <jc>// "HelloWorld"</jc>
5616    *    pascalCase(<js>"helloWorld"</js>);            <jc>// "HelloWorld"</jc>
5617    *    pascalCase(<js>"hello_world"</js>);           <jc>// "HelloWorld"</jc>
5618    *    pascalCase(<js>"hello-world"</js>);           <jc>// "HelloWorld"</jc>
5619    * </p>
5620    *
5621    * @param str The string to convert.
5622    * @return The PascalCase string, or <jk>null</jk> if input is <jk>null</jk>.
5623    */
5624   public static String pascalCase(String str) {
5625      if (isEmpty(str))
5626         return str;
5627
5628      var words = splitWords(str);
5629      if (words.isEmpty())
5630         return "";
5631
5632      var result = new StringBuilder();
5633      for (var word : words) {
5634         result.append(capitalize(lowerCaseRoot(word)));
5635      }
5636
5637      return result.toString();
5638   }
5639
5640   /**
5641    * Pluralizes a word based on a count.
5642    *
5643    * <p>
5644    * Simple pluralization that adds "s" to most words, with basic rules for words ending in "s", "x", "z", "ch", "sh" (add "es"),
5645    * and words ending in "y" preceded by a consonant (replace "y" with "ies").
5646    *
5647    * <h5 class='section'>Example:</h5>
5648    * <p class='bjava'>
5649    *    pluralize(<js>"cat"</js>, <js>1</js>);        <jc>// "cat"</jc>
5650    *    pluralize(<js>"cat"</js>, <js>2</js>);        <jc>// "cats"</jc>
5651    *    pluralize(<js>"box"</js>, <js>2</js>);        <jc>// "boxes"</jc>
5652    *    pluralize(<js>"city"</js>, <js>2</js>);       <jc>// "cities"</jc>
5653    * </p>
5654    *
5655    * @param word The word to pluralize.  Can be <jk>null</jk> (returned as-is).
5656    * @param count The count to determine if pluralization is needed.
5657    * @return The pluralized word if count is not 1, otherwise the original word.
5658    */
5659   public static String pluralize(String word, int count) {
5660      if (word == null || word.isEmpty())
5661         return word;
5662      if (count == 1)
5663         return word;
5664
5665      var lower = lowerCaseRoot(word);
5666      var length = word.length();
5667
5668      // Words ending in s, x, z, ch, sh -> add "es"
5669      if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
5670         return word + "es";
5671      }
5672
5673      // Words ending in "y" preceded by a consonant -> replace "y" with "ies"
5674      if (length > 1 && lower.endsWith("y")) {
5675         var secondLast = lower.charAt(length - 2);
5676         if (! VOWEL.contains(secondLast)) {
5677            return word.substring(0, length - 1) + "ies";
5678         }
5679      }
5680
5681      // Words ending in "f" or "fe" -> replace with "ves" (basic rule)
5682      if (lower.endsWith("f")) {
5683         return word.substring(0, length - 1) + "ves";
5684      }
5685      if (lower.endsWith("fe")) {
5686         return word.substring(0, length - 2) + "ves";
5687      }
5688
5689      // Default: add "s"
5690      return word + "s";
5691   }
5692
5693   /**
5694    * Generated a random UUID with the specified number of characters.
5695    *
5696    * <p>
5697    * Characters are composed of lower-case ASCII letters and numbers only.
5698    *
5699    * <p>
5700    * This method conforms to the restrictions for hostnames as specified in <a class="doclink" href="https://tools.ietf.org/html/rfc952">RFC 952</a>
5701    * Since each character has 36 possible values, the square approximation formula for the number of generated IDs
5702    * that would produce a 50% chance of collision is:
5703    * <c>sqrt(36^N)</c>.
5704    * Dividing this number by 10 gives you an approximation of the number of generated IDs needed to produce a
5705    * &lt;1% chance of collision.
5706    *
5707    * <p>
5708    * For example, given 5 characters, the number of generated IDs need to produce a &lt;1% chance of collision would
5709    * be:
5710    * <c>sqrt(36^5)/10=777</c>
5711    *
5712    * @param numchars The number of characters in the generated UUID.
5713    * @return A new random UUID.
5714    */
5715   public static String random(int numchars) {
5716      var sb = new StringBuilder(numchars);
5717      for (var i = 0; i < numchars; i++) {
5718         var c = RANDOM.nextInt(36) + 97;
5719         if (c > 'z')
5720            c -= ('z' - '0' + 1);
5721         sb.append((char)c);
5722      }
5723      return sb.toString();
5724   }
5725
5726   /**
5727    * Generates a random alphabetic string of the specified length.
5728    *
5729    * <p>
5730    * Characters are composed of upper-case and lower-case ASCII letters (a-z, A-Z).
5731    *
5732    * <h5 class='section'>Example:</h5>
5733    * <p class='bjava'>
5734    *    randomAlphabetic(<jk>5</jk>);  <jc>// "aBcDe"</jc>
5735    * </p>
5736    *
5737    * @param length The length of the generated string.
5738    * @return A new random alphabetic string.
5739    */
5740   public static String randomAlphabetic(int length) {
5741      if (length < 0)
5742         throw iaex("Length must be non-negative: " + length);
5743      var sb = new StringBuilder(length);
5744      for (var i = 0; i < length; i++) {
5745         var c = RANDOM.nextInt(52);
5746         if (c < 26)
5747            sb.append((char)('a' + c));
5748         else
5749            sb.append((char)('A' + c - 26));
5750      }
5751      return sb.toString();
5752   }
5753
5754   /**
5755    * Generates a random alphanumeric string of the specified length.
5756    *
5757    * <p>
5758    * Characters are composed of upper-case and lower-case ASCII letters and digits (a-z, A-Z, 0-9).
5759    *
5760    * <h5 class='section'>Example:</h5>
5761    * <p class='bjava'>
5762    *    randomAlphanumeric(<jk>8</jk>);  <jc>// "aB3dE5fG"</jc>
5763    * </p>
5764    *
5765    * @param length The length of the generated string.
5766    * @return A new random alphanumeric string.
5767    */
5768   public static String randomAlphanumeric(int length) {
5769      if (length < 0)
5770         throw iaex("Length must be non-negative: " + length);
5771      var sb = new StringBuilder(length);
5772      for (var i = 0; i < length; i++) {
5773         var c = RANDOM.nextInt(62);
5774         if (c < 10)
5775            sb.append((char)('0' + c));
5776         else if (c < 36)
5777            sb.append((char)('a' + c - 10));
5778         else
5779            sb.append((char)('A' + c - 36));
5780      }
5781      return sb.toString();
5782   }
5783
5784   /**
5785    * Generates a random ASCII string of the specified length.
5786    *
5787    * <p>
5788    * Characters are composed of printable ASCII characters (32-126).
5789    *
5790    * <h5 class='section'>Example:</h5>
5791    * <p class='bjava'>
5792    *    randomAscii(<jk>10</jk>);  <jc>// "!@#$%^&amp;*()"</jc>
5793    * </p>
5794    *
5795    * @param length The length of the generated string.
5796    * @return A new random ASCII string.
5797    */
5798   public static String randomAscii(int length) {
5799      if (length < 0)
5800         throw iaex("Length must be non-negative: " + length);
5801      var sb = new StringBuilder(length);
5802      for (var i = 0; i < length; i++) {
5803         sb.append((char)(32 + RANDOM.nextInt(95))); // 95 printable ASCII chars (32-126)
5804      }
5805      return sb.toString();
5806   }
5807
5808   /**
5809    * Generates a random numeric string of the specified length.
5810    *
5811    * <p>
5812    * Characters are composed of digits (0-9).
5813    *
5814    * <h5 class='section'>Example:</h5>
5815    * <p class='bjava'>
5816    *    randomNumeric(<jk>6</jk>);  <jc>// "123456"</jc>
5817    * </p>
5818    *
5819    * @param length The length of the generated string.
5820    * @return A new random numeric string.
5821    */
5822   public static String randomNumeric(int length) {
5823      if (length < 0)
5824         throw iaex("Length must be non-negative: " + length);
5825      var sb = new StringBuilder(length);
5826      for (var i = 0; i < length; i++) {
5827         sb.append((char)('0' + RANDOM.nextInt(10)));
5828      }
5829      return sb.toString();
5830   }
5831
5832   /**
5833    * Generates a random string of the specified length using characters from the given character set.
5834    *
5835    * <p>
5836    * Each character in the generated string is randomly selected from the provided character set.
5837    *
5838    * <h5 class='section'>Example:</h5>
5839    * <p class='bjava'>
5840    *    randomString(<jk>5</jk>, <js>"ABC"</js>);  <jc>// "BACAB"</jc>
5841    * </p>
5842    *
5843    * @param length The length of the generated string.
5844    * @param chars The character set to use. Must not be null or empty.
5845    * @return A new random string.
5846    * @throws IllegalArgumentException If chars is null or empty, or length is negative.
5847    */
5848   public static String randomString(int length, String chars) {
5849      if (length < 0)
5850         throw iaex("Length must be non-negative: " + length);
5851      if (chars == null || chars.isEmpty())
5852         throw iaex("Character set must not be null or empty");
5853      var sb = new StringBuilder(length);
5854      var charsLen = chars.length();
5855      for (var i = 0; i < length; i++) {
5856         sb.append(chars.charAt(RANDOM.nextInt(charsLen)));
5857      }
5858      return sb.toString();
5859   }
5860
5861   /**
5862    * Calculates a simple readability score for a string.
5863    *
5864    * <p>
5865    * Uses a simplified Flesch Reading Ease-like formula based on:
5866    * <ul>
5867    *   <li>Average words per sentence</li>
5868    *   <li>Average syllables per word (estimated)</li>
5869    * </ul>
5870    * Returns a score from 0-100, where higher scores indicate easier reading.
5871    *
5872    * <h5 class='section'>Example:</h5>
5873    * <p class='bjava'>
5874    *    readabilityScore(<js>"The cat sat."</js>);      <jc>// Higher score (simple)</jc>
5875    *    readabilityScore(<js>"The sophisticated..."</js>); <jc>// Lower score (complex)</jc>
5876    * </p>
5877    *
5878    * @param str The string to analyze. Can be <jk>null</jk>.
5879    * @return A readability score from 0-100, or <c>0.0</c> if the string is <jk>null</jk> or empty.
5880    */
5881   public static double readabilityScore(String str) {
5882      if (isEmpty(str))
5883         return 0.0;
5884
5885      var words = extractWords(str);
5886      if (words.isEmpty())
5887         return 0.0;
5888
5889      // Count sentences (ending with . ! ?)
5890      var sentenceCount = 0;
5891      for (var i = 0; i < str.length(); i++) {
5892         var c = str.charAt(i);
5893         if (c == '.' || c == '!' || c == '?') {
5894            sentenceCount++;
5895         }
5896      }
5897      if (sentenceCount == 0)
5898         sentenceCount = 1; // At least one sentence
5899
5900      // Calculate average words per sentence
5901      var avgWordsPerSentence = (double)words.size() / sentenceCount;
5902
5903      // Estimate average syllables per word (simplified: count vowel groups)
5904      var totalSyllables = 0;
5905      for (var word : words) {
5906         totalSyllables += estimateSyllables(word);
5907      }
5908      var avgSyllablesPerWord = (double)totalSyllables / words.size();
5909
5910      // Simplified Flesch Reading Ease formula
5911      // Score = 206.835 - (1.015 * ASL) - (84.6 * ASW)
5912      // Where ASL = average sentence length (words), ASW = average syllables per word
5913      var score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord);
5914
5915      // Clamp to 0-100 range
5916      return Math.max(0.0, Math.min(100.0, score));
5917   }
5918
5919   /**
5920    * Converts an arbitrary object to a readable string format suitable for debugging and testing.
5921    *
5922    * <p>This method provides intelligent formatting for various Java types, recursively processing
5923    * nested structures to create human-readable representations. It's extensively used throughout
5924    * the Juneau framework for test assertions and debugging output.</p>
5925    *
5926    * <h5 class='section'>Type-Specific Formatting:</h5>
5927    * <ul>
5928    *    <li><b>null:</b> Returns <js>null</js></li>
5929    *    <li><b>Optional:</b> Recursively formats the contained value (or <js>null</js> if empty)</li>
5930    *    <li><b>Collections:</b> Formats as <js>"[item1,item2,item3]"</js> with comma-separated elements</li>
5931    *    <li><b>Maps:</b> Formats as <js>"{key1=value1,key2=value2}"</js> with comma-separated entries</li>
5932    *    <li><b>Map.Entry:</b> Formats as <js>"key=value"</js></li>
5933    *    <li><b>Arrays:</b> Converts to list format <js>"[item1,item2,item3]"</js></li>
5934    *    <li><b>Iterables/Iterators/Enumerations:</b> Converts to list and formats recursively</li>
5935    *    <li><b>GregorianCalendar:</b> Formats as ISO instant timestamp</li>
5936    *    <li><b>Date:</b> Formats as ISO instant string (e.g., <js>"2023-12-25T10:30:00Z"</js>)</li>
5937    *    <li><b>InputStream:</b> Converts to hexadecimal representation</li>
5938    *    <li><b>Reader:</b> Reads content and returns as string</li>
5939    *    <li><b>File:</b> Reads file content and returns as string</li>
5940    *    <li><b>byte[]:</b> Converts to hexadecimal representation</li>
5941    *    <li><b>Enum:</b> Returns the enum name via {@link Enum#name()}</li>
5942    *    <li><b>All other types:</b> Uses {@link Object#toString()}</li>
5943    * </ul>
5944    *
5945    * <h5 class='section'>Examples:</h5>
5946    * <p class='bjava'>
5947    *    <jc>// Collections</jc>
5948    *    readable(List.of("a", "b", "c")) <jc>// Returns: "[a,b,c]"</jc>
5949    *    readable(Set.of(1, 2, 3)) <jc>// Returns: "[1,2,3]" (order may vary)</jc>
5950    *
5951    *    <jc>// Maps</jc>
5952    *    readable(Map.of("foo", "bar", "baz", 123)) <jc>// Returns: "{foo=bar,baz=123}"</jc>
5953    *
5954    *    <jc>// Arrays</jc>
5955    *    readable(ints(1, 2, 3)) <jc>// Returns: "[1,2,3]"</jc>
5956    *    readable(new String[]{"a", "b"}) <jc>// Returns: "[a,b]"</jc>
5957    *
5958    *    <jc>// Nested structures</jc>
5959    *    readable(List.of(Map.of("x", 1), Set.of("a", "b"))) <jc>// Returns: "[{x=1},[a,b]]"</jc>
5960    *
5961    *    <jc>// Special types</jc>
5962    *    readable(Optional.of("test")) <jc>// Returns: "test"</jc>
5963    *    readable(Optional.empty()) <jc>// Returns: null</jc>
5964    *    readable(new Date(1640995200000L)) <jc>// Returns: "2022-01-01T00:00:00Z"</jc>
5965    *    readable(MyEnum.FOO) <jc>// Returns: "FOO"</jc>
5966    * </p>
5967    *
5968    * <h5 class='section'>Recursive Processing:</h5>
5969    * <p>The method recursively processes nested structures, so complex objects containing
5970    * collections, maps, and arrays are fully flattened into readable format. This makes it
5971    * ideal for test assertions where you need to compare complex object structures.</p>
5972    *
5973    * <h5 class='section'>Error Handling:</h5>
5974    * <p>IO operations (reading files, streams) are wrapped in safe() calls, converting
5975    * any exceptions to RuntimeExceptions. Binary data (InputStreams, byte arrays) is
5976    * converted to hexadecimal representation for readability.</p>
5977    *
5978    * @param o The object to convert to readable format. Can be <jk>null</jk>.
5979    * @return A readable string representation of the object, or <jk>null</jk> if the input was <jk>null</jk>.
5980    */
5981   public static String readable(Object o) {
5982      if (o == null)
5983         return null;
5984      var c = o.getClass();
5985
5986      // Special case for byte[] - must be handled before general array check
5987      if (c == byte[].class) {
5988         return toHex((byte[])o);
5989      }
5990
5991      // Check cache first
5992      var f = READIFIER_CACHE.get(c, () -> {
5993         // Find readifier from READIFIERS list
5994         // First try exact match, then isAssignableFrom
5995         var readifier = READIFIERS.stream()
5996            .filter(r -> r.forClass() == c || r.forClass().isAssignableFrom(c))
5997            .map(Readifier::toFunction)
5998            .findFirst()
5999            .orElse(null);
6000
6001         // If readifier found, use it
6002         if (readifier != null)
6003            return readifier;
6004
6005         // If no readifier found, check if it's an array
6006         if (c.isArray()) {
6007            return x -> {
6008               var l = list();
6009               for (var i = 0; i < Array.getLength(x); i++) {
6010                  l.add(Array.get(x, i));
6011               }
6012               return readable(l);
6013            };
6014         }
6015
6016         // If no readifier found, use toString() as fallback
6017         return Object::toString;
6018      });
6019
6020      return f.apply(o);
6021   }
6022
6023   /**
6024    * Removes all occurrences of a substring from a string.
6025    *
6026    * <h5 class='section'>Example:</h5>
6027    * <p class='bjava'>
6028    *    remove(<jk>null</jk>, <js>"x"</js>);              <jc>// null</jc>
6029    *    remove(<js>"hello"</js>, <jk>null</jk>);          <jc>// "hello"</jc>
6030    *    remove(<js>"hello world"</js>, <js>"o"</js>);     <jc>// "hell wrld"</jc>
6031    *    remove(<js>"hello world"</js>, <js>"xyz"</js>);   <jc>// "hello world"</jc>
6032    * </p>
6033    *
6034    * @param str The string to process.
6035    * @param remove The substring to remove.
6036    * @return The string with all occurrences of the substring removed, or <jk>null</jk> if input is <jk>null</jk>.
6037    */
6038   public static String remove(String str, String remove) {
6039      if (isEmpty(str) || isEmpty(remove))
6040         return str;
6041      return str.replace(remove, "");
6042   }
6043
6044   /**
6045    * Removes diacritical marks (accents) from characters in a string.
6046    *
6047    * <p>
6048    * Normalizes the string to NFD form and removes combining diacritical marks.
6049    *
6050    * <h5 class='section'>Example:</h5>
6051    * <p class='bjava'>
6052    *    removeAccents(<js>"café"</js>);     <jc>// "cafe"</jc>
6053    *    removeAccents(<js>"naïve"</js>);    <jc>// "naive"</jc>
6054    *    removeAccents(<js>"résumé"</js>);   <jc>// "resume"</jc>
6055    * </p>
6056    *
6057    * @param str The string to remove accents from. Can be <jk>null</jk>.
6058    * @return The string with accents removed, or <jk>null</jk> if input is <jk>null</jk>.
6059    */
6060   public static String removeAccents(String str) {
6061      if (str == null)
6062         return null;
6063
6064      // Normalize to NFD (decomposed form)
6065      var normalized = Normalizer.normalize(str, Normalizer.Form.NFD);
6066
6067      // Remove combining diacritical marks (Unicode category Mn)
6068      var sb = new StringBuilder(normalized.length());
6069      for (var i = 0; i < normalized.length(); i++) {
6070         var c = normalized.charAt(i);
6071         var type = Character.getType(c);
6072         // Mn = Nonspacing_Mark (combining marks)
6073         if (type != Character.NON_SPACING_MARK) {
6074            sb.append(c);
6075         }
6076      }
6077
6078      return sb.toString();
6079   }
6080
6081   /**
6082    * Removes multiple substrings from a string.
6083    *
6084    * <h5 class='section'>Example:</h5>
6085    * <p class='bjava'>
6086    *    removeAll(<js>"hello world test"</js>, <js>"hello"</js>, <js>"test"</js>);  <jc>// " world "</jc>
6087    *    removeAll(<jk>null</jk>, <js>"x"</js>);                                     <jc>// null</jc>
6088    * </p>
6089    *
6090    * @param str The string to process.
6091    * @param remove The substrings to remove.
6092    * @return The string with all specified substrings removed, or <jk>null</jk> if input is <jk>null</jk>.
6093    */
6094   public static String removeAll(String str, String...remove) {
6095      if (str == null)
6096         return null;
6097      if (isEmpty(str) || remove == null || remove.length == 0)
6098         return str;
6099      var result = str;
6100      for (var r : remove) {
6101         if (r != null)
6102            result = result.replace(r, "");
6103      }
6104      return result;
6105   }
6106
6107   /**
6108    * Removes control characters from a string, replacing them with spaces.
6109    *
6110    * <h5 class='section'>Example:</h5>
6111    * <p class='bjava'>
6112    *    removeControlChars(<js>"hello\u0000\u0001world"</js>);   <jc>// "hello  world"</jc>
6113    *    removeControlChars(<js>"hello\nworld"</js>);             <jc>// "hello\nworld"</jc>
6114    * </p>
6115    *
6116    * @param str The string to process.
6117    * @return The string with control characters replaced by spaces (except whitespace control chars), or <jk>null</jk> if input is <jk>null</jk>.
6118    */
6119   public static String removeControlChars(String str) {
6120      if (str == null)
6121         return null;
6122      var sb = new StringBuilder();
6123      for (var i = 0; i < str.length(); i++) {
6124         var c = str.charAt(i);
6125         if (Character.isISOControl(c) && ! Character.isWhitespace(c))
6126            sb.append(' ');
6127         else
6128            sb.append(c);
6129      }
6130      return sb.toString();
6131   }
6132
6133   /**
6134    * Removes a suffix from a string if present.
6135    *
6136    * <h5 class='section'>Example:</h5>
6137    * <p class='bjava'>
6138    *    removeEnd(<jk>null</jk>, <js>"x"</js>);              <jc>// null</jc>
6139    *    removeEnd(<js>"hello"</js>, <jk>null</jk>);          <jc>// "hello"</jc>
6140    *    removeEnd(<js>"hello world"</js>, <js>"world"</js>); <jc>// "hello "</jc>
6141    *    removeEnd(<js>"hello world"</js>, <js>"xyz"</js>);   <jc>// "hello world"</jc>
6142    * </p>
6143    *
6144    * @param str The string to process.
6145    * @param suffix The suffix to remove.
6146    * @return The string with the suffix removed if present, or <jk>null</jk> if input is <jk>null</jk>.
6147    */
6148   public static String removeEnd(String str, String suffix) {
6149      if (isEmpty(str) || isEmpty(suffix))
6150         return str;
6151      if (str.endsWith(suffix))
6152         return str.substring(0, str.length() - suffix.length());
6153      return str;
6154   }
6155
6156   /**
6157    * Removes non-printable characters from a string.
6158    *
6159    * <h5 class='section'>Example:</h5>
6160    * <p class='bjava'>
6161    *    removeNonPrintable(<js>"hello\u0000world"</js>);   <jc>// "helloworld"</jc>
6162    * </p>
6163    *
6164    * @param str The string to process.
6165    * @return The string with non-printable characters removed, or <jk>null</jk> if input is <jk>null</jk>.
6166    */
6167   public static String removeNonPrintable(String str) {
6168      if (str == null)
6169         return null;
6170      return str.replaceAll("\\p{C}", "");
6171   }
6172
6173   /**
6174    * Removes a prefix from a string if present.
6175    *
6176    * <h5 class='section'>Example:</h5>
6177    * <p class='bjava'>
6178    *    removeStart(<jk>null</jk>, <js>"x"</js>);              <jc>// null</jc>
6179    *    removeStart(<js>"hello"</js>, <jk>null</jk>);          <jc>// "hello"</jc>
6180    *    removeStart(<js>"hello world"</js>, <js>"hello"</js>); <jc>// " world"</jc>
6181    *    removeStart(<js>"hello world"</js>, <js>"xyz"</js>);   <jc>// "hello world"</jc>
6182    * </p>
6183    *
6184    * @param str The string to process.
6185    * @param prefix The prefix to remove.
6186    * @return The string with the prefix removed if present, or <jk>null</jk> if input is <jk>null</jk>.
6187    */
6188   public static String removeStart(String str, String prefix) {
6189      if (isEmpty(str) || isEmpty(prefix))
6190         return str;
6191      if (str.startsWith(prefix))
6192         return str.substring(prefix.length());
6193      return str;
6194   }
6195
6196   /**
6197    * Removes all underscore characters from a string.
6198    *
6199    * <p>
6200    * This method is commonly used to process numeric literals that may contain underscores for readability
6201    * (e.g., <js>"1_000_000"</js> becomes <js>"1000000"</js>), as Java allows underscores in numeric literals
6202    * but some parsing methods do not support them.
6203    *
6204    * <p>
6205    * If the string does not contain any underscores, the original string is returned (no new object created).
6206    *
6207    * <h5 class='section'>Example:</h5>
6208    * <p class='bjava'>
6209    *    removeUnderscores(<js>"1_000_000"</js>);        <jc>// "1000000"</jc>
6210    *    removeUnderscores(<js>"1_000.5"</js>);          <jc>// "1000.5"</jc>
6211    *    removeUnderscores(<js>"hello_world"</js>);      <jc>// "helloworld"</jc>
6212    *    removeUnderscores(<js>"no_underscores"</js>);   <jc>// "nounderscores"</jc>
6213    *    removeUnderscores(<js>"Hello"</js>);            <jc>// "Hello" (no change, same object returned)</jc>
6214    * </p>
6215    *
6216    * @param value The string from which to remove underscores. Must not be <jk>null</jk>.
6217    * @return A new string with all underscores removed, or the original string if it contains no underscores.
6218    * @throws IllegalArgumentException If <c>value</c> is <jk>null</jk>.
6219    * @see #parseInt(String)
6220    * @see #parseLong(String)
6221    * @see #parseFloat(String)
6222    * @see #parseNumber(String, Class)
6223    */
6224   public static String removeUnderscores(String value) {
6225      reqnn("value", value);
6226      return notContains(value, '_') ? value : value.replace("_", "");
6227   }
6228
6229   /**
6230    * Creates a repeated pattern.
6231    *
6232    * @param count The number of times to repeat the pattern.
6233    * @param pattern The pattern to repeat.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
6234    * @return A new string consisting of the repeated pattern.
6235    */
6236   public static String repeat(int count, String pattern) {
6237      var sb = new StringBuilder(pattern.length() * count);
6238      for (var i = 0; i < count; i++)
6239         sb.append(pattern);
6240      return sb.toString();
6241   }
6242
6243   /**
6244    * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters.
6245    *
6246    * @param s The string to replace unicode sequences in.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
6247    * @return A string with unicode sequences replaced.
6248    */
6249   public static String replaceUnicodeSequences(String s) {
6250
6251      if (s.indexOf('\\') == -1)
6252         return s;
6253
6254      var p = Pattern.compile("\\\\u(\\p{XDigit}{4})");
6255      var m = p.matcher(s);
6256      var sb = new StringBuffer(s.length());
6257
6258      while (m.find()) {
6259         var ch = String.valueOf((char)Integer.parseInt(m.group(1), 16));
6260         m.appendReplacement(sb, Matcher.quoteReplacement(ch));
6261      }
6262
6263      m.appendTail(sb);
6264      return sb.toString();
6265   }
6266
6267   /**
6268    * Reverses a string.
6269    *
6270    * <h5 class='section'>Example:</h5>
6271    * <p class='bjava'>
6272    *    reverse(<jk>null</jk>);          <jc>// null</jc>
6273    *    reverse(<js>""</js>);            <jc>// ""</jc>
6274    *    reverse(<js>"hello"</js>);       <jc>// "olleh"</jc>
6275    * </p>
6276    *
6277    * @param str The string to reverse.
6278    * @return The reversed string, or <jk>null</jk> if input is <jk>null</jk>.
6279    */
6280   public static String reverse(String str) {
6281      if (str == null)
6282         return null;
6283      return new StringBuilder(str).reverse().toString();
6284   }
6285
6286   /**
6287    * Returns the rightmost characters of a string.
6288    *
6289    * <h5 class='section'>Example:</h5>
6290    * <p class='bjava'>
6291    *    right(<jk>null</jk>, 3);          <jc>// null</jc>
6292    *    right(<js>""</js>, 3);            <jc>// ""</jc>
6293    *    right(<js>"hello"</js>, 3);       <jc>// "llo"</jc>
6294    *    right(<js>"hello"</js>, 10);      <jc>// "hello"</jc>
6295    * </p>
6296    *
6297    * @param str The string to get characters from.
6298    * @param len The number of characters to get.
6299    * @return The rightmost characters, or <jk>null</jk> if input is <jk>null</jk>.
6300    */
6301   public static String right(String str, int len) {
6302      if (str == null)
6303         return null;
6304      if (len < 0)
6305         return "";
6306      if (len >= str.length())
6307         return str;
6308      return str.substring(str.length() - len);
6309   }
6310
6311   /**
6312    * Basic HTML/XML sanitization - removes or escapes potentially dangerous content.
6313    *
6314    * <p>
6315    * Removes HTML/XML tags and escapes special characters to prevent XSS attacks.
6316    * This is a basic sanitization - for production use, consider a more robust library.
6317    *
6318    * <h5 class='section'>Example:</h5>
6319    * <p class='bjava'>
6320    *    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>
6321    *    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>
6322    * </p>
6323    *
6324    * @param str The string to sanitize.
6325    * @return The sanitized string with HTML/XML tags escaped, or <jk>null</jk> if input is <jk>null</jk>.
6326    */
6327   public static String sanitize(String str) {
6328      if (str == null)
6329         return null;
6330      // Escape HTML/XML special characters
6331      return escapeHtml(str);
6332   }
6333
6334   /**
6335    * Calculates the similarity percentage between two strings using Levenshtein distance.
6336    *
6337    * <p>
6338    * Returns a value between 0.0 (completely different) and 1.0 (identical).
6339    *
6340    * <h5 class='section'>Example:</h5>
6341    * <p class='bjava'>
6342    *    similarity(<js>"hello"</js>, <js>"hello"</js>);           <jc>// 1.0 (100%)</jc>
6343    *    similarity(<js>"kitten"</js>, <js>"sitting"</js>);        <jc>// ~0.57 (57%)</jc>
6344    *    similarity(<js>"abc"</js>, <js>"xyz"</js>);               <jc>// 0.0 (0%)</jc>
6345    * </p>
6346    *
6347    * @param str1 The first string.  Can be <jk>null</jk> (treated as empty).
6348    * @param str2 The second string.  Can be <jk>null</jk> (treated as empty).
6349    * @return A similarity value between 0.0 and 1.0, where 1.0 means identical.
6350    */
6351   public static double similarity(String str1, String str2) {
6352      if (str1 == null)
6353         str1 = "";
6354      if (str2 == null)
6355         str2 = "";
6356
6357      if (str1.equals(str2))
6358         return 1.0;
6359
6360      var maxLen = Math.max(str1.length(), str2.length());
6361
6362      var distance = levenshteinDistance(str1, str2);
6363      return 1.0 - ((double)distance / maxLen);
6364   }
6365
6366   /**
6367    * Converts a string to snake_case format.
6368    *
6369    * <p>
6370    * Handles various input formats:
6371    * <ul>
6372    *   <li>Space-separated: "hello world" → "hello_world"</li>
6373    *   <li>CamelCase: "helloWorld" → "hello_world"</li>
6374    *   <li>PascalCase: "HelloWorld" → "hello_world"</li>
6375    *   <li>Kebab-case: "hello-world" → "hello_world"</li>
6376    * </ul>
6377    *
6378    * <h5 class='section'>Example:</h5>
6379    * <p class='bjava'>
6380    *    snakeCase(<jk>null</jk>);                    <jc>// null</jc>
6381    *    snakeCase(<js>""</js>);                      <jc>// ""</jc>
6382    *    snakeCase(<js>"hello world"</js>);           <jc>// "hello_world"</jc>
6383    *    snakeCase(<js>"helloWorld"</js>);            <jc>// "hello_world"</jc>
6384    *    snakeCase(<js>"HelloWorld"</js>);            <jc>// "hello_world"</jc>
6385    *    snakeCase(<js>"hello-world"</js>);           <jc>// "hello_world"</jc>
6386    * </p>
6387    *
6388    * @param str The string to convert.
6389    * @return The snake_case string, or <jk>null</jk> if input is <jk>null</jk>.
6390    */
6391   public static String snakeCase(String str) {
6392      if (str == null)
6393         return null;
6394      if (isEmpty(str))
6395         return str;
6396
6397      var words = splitWords(str);
6398      if (words.isEmpty())
6399         return "";
6400
6401      var result = new StringBuilder();
6402      for (var i = 0; i < words.size(); i++) {
6403         if (i > 0)
6404            result.append('_');
6405         result.append(lowerCaseRoot(words.get(i)));
6406      }
6407
6408      return result.toString();
6409   }
6410
6411   /**
6412    * Sorts a string array in natural order.
6413    *
6414    * <p>
6415    * Returns <jk>null</jk> if the array is <jk>null</jk>.
6416    * This method creates a copy of the array and sorts it, leaving the original array unchanged.
6417    *
6418    * <h5 class='section'>Examples:</h5>
6419    * <p class='bjava'>
6420    *    String[] <jv>array</jv> = {<js>"zebra"</js>, <js>"apple"</js>, <js>"banana"</js>};
6421    *    String[] <jv>sorted</jv> = sort(<jv>array</jv>);
6422    *    <jc>// Returns: ["apple", "banana", "zebra"]</jc>
6423    * </p>
6424    *
6425    * @param array The array to sort. Can be <jk>null</jk>.
6426    * @return A new sorted array, or an empty array if the array was <jk>null</jk>.
6427    */
6428   public static String[] sort(String[] array) {
6429      if (array == null)
6430         return new String[0];
6431      var result = Arrays.copyOf(array, array.length);
6432      Arrays.sort(result);
6433      return result;
6434   }
6435
6436   /**
6437    * Sorts a string array in case-insensitive order.
6438    *
6439    * <p>
6440    * Returns <jk>null</jk> if the array is <jk>null</jk>.
6441    * This method creates a copy of the array and sorts it using case-insensitive comparison,
6442    * leaving the original array unchanged.
6443    *
6444    * <h5 class='section'>Examples:</h5>
6445    * <p class='bjava'>
6446    *    String[] <jv>array</jv> = {<js>"Zebra"</js>, <js>"apple"</js>, <js>"Banana"</js>};
6447    *    String[] <jv>sorted</jv> = sortIgnoreCase(<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 (case-insensitive), or an empty array if the array was <jk>null</jk>.
6453    */
6454   public static String[] sortIgnoreCase(String[] array) {
6455      if (array == null)
6456         return new String[0];
6457      var result = Arrays.copyOf(array, array.length);
6458      Arrays.sort(result, String.CASE_INSENSITIVE_ORDER);
6459      return result;
6460   }
6461
6462   /**
6463    * Generates a Soundex code for a string.
6464    *
6465    * <p>
6466    * Soundex is a phonetic algorithm for indexing names by sound. The code consists of
6467    * a letter followed by three digits. Similar-sounding names produce the same code.
6468    *
6469    * <h5 class='section'>Example:</h5>
6470    * <p class='bjava'>
6471    *    soundex(<js>"Smith"</js>);   <jc>// "S530"</jc>
6472    *    soundex(<js>"Smythe"</js>);  <jc>// "S530"</jc>
6473    *    soundex(<js>"Robert"</js>);  <jc>// "R163"</jc>
6474    * </p>
6475    *
6476    * @param str The string to generate a Soundex code for. Can be <jk>null</jk>.
6477    * @return The Soundex code (1 letter + 3 digits), or <jk>null</jk> if input is <jk>null</jk> or empty.
6478    */
6479   public static String soundex(String str) {
6480      if (isEmpty(str))
6481         return null;
6482
6483      var upper = str.toUpperCase(Locale.ROOT);
6484      var result = new StringBuilder(4);
6485      result.append(upper.charAt(0));
6486
6487      // Soundex mapping: 0 = AEIOUHWY, 1 = BFPV, 2 = CGJKQSXZ, 3 = DT, 4 = L, 5 = MN, 6 = R
6488      // H/W/Y don't get codes but don't break sequences either
6489      // Initialize lastCode to a value that won't match any real code
6490      var lastCode = '\0';
6491
6492      for (var i = 1; i < upper.length() && result.length() < 4; i++) {
6493         var c = upper.charAt(i);
6494         var code = getSoundexCode(c);
6495         if (code == '0') {
6496            // H/W/Y/vowels - don't add code, but don't update lastCode either
6497            // This allows sequences to continue across H/W/Y/vowels
6498            continue;
6499         }
6500         if (code != lastCode) {
6501            result.append(code);
6502            lastCode = code;
6503         }
6504         // If code == lastCode, skip it (consecutive same codes)
6505      }
6506
6507      // Pad with zeros if needed
6508      while (result.length() < 4) {
6509         result.append('0');
6510      }
6511
6512      return result.toString();
6513   }
6514
6515   /**
6516    * Splits a comma-delimited list into a list of strings.
6517    *
6518    * @param s The string to split.
6519    * @return A list of split strings, or an empty list if the input is <jk>null</jk>.
6520    */
6521   public static List<String> split(String s) {
6522      return s == null ? Collections.emptyList() : split(s, ',');
6523   }
6524
6525   /**
6526    * Splits a character-delimited string into a string array.
6527    *
6528    * <p>
6529    * Does not split on escaped-delimiters (e.g. "\,");
6530    * Resulting tokens are trimmed of whitespace.
6531    *
6532    * <p>
6533    * <b>NOTE:</b>  This behavior is different than the Jakarta equivalent.
6534    * split("a,b,c",',') -&gt; {"a","b","c"}
6535    * split("a, b ,c ",',') -&gt; {"a","b","c"}
6536    * split("a,,c",',') -&gt; {"a","","c"}
6537    * split(",,",',') -&gt; {"","",""}
6538    * split("",',') -&gt; {}
6539    * split(null,',') -&gt; {}
6540    * split("a,b\,c,d", ',', false) -&gt; {"a","b\,c","d"}
6541    * split("a,b\\,c,d", ',', false) -&gt; {"a","b\","c","d"}
6542    * split("a,b\,c,d", ',', true) -&gt; {"a","b,c","d"}
6543    *
6544    * @param s The string to split.  Can be <jk>null</jk>.
6545    * @param c The character to split on.
6546    * @return The tokens, or an empty list if the string was <jk>null</jk>.
6547    */
6548   public static List<String> split(String s, char c) {
6549      return split(s, c, Integer.MAX_VALUE);
6550   }
6551
6552   /**
6553    * Same as {@link #splita(String, char)} but consumes the tokens instead of creating an array.
6554    *
6555    * @param s The string to split.  Can be <jk>null</jk> (no tokens are consumed).
6556    * @param c The character to split on.
6557    * @param consumer The consumer of the tokens.  Must not be <jk>null</jk> when the string is non-empty.
6558    */
6559   public static void split(String s, char c, Consumer<String> consumer) {
6560      var escapeChars = getEscapeSet(c);
6561
6562      if (isEmpty(s))
6563         return;
6564      if (s.indexOf(c) == -1) {
6565         consumer.accept(s);
6566         return;
6567      }
6568
6569      var x1 = 0;
6570      var escapeCount = 0;
6571
6572      for (var i = 0; i < s.length(); i++) {
6573         if (s.charAt(i) == '\\')
6574            escapeCount++;
6575         else if (s.charAt(i) == c && escapeCount % 2 == 0) {
6576            var s2 = s.substring(x1, i);
6577            var s3 = unescapeChars(s2, escapeChars);
6578            consumer.accept(s3.trim());
6579            x1 = i + 1;
6580         }
6581         if (s.charAt(i) != '\\')
6582            escapeCount = 0;
6583      }
6584      var s2 = s.substring(x1);
6585      var s3 = unescapeChars(s2, escapeChars);
6586      consumer.accept(s3.trim());
6587   }
6588
6589   /**
6590    * Same as {@link #splita(String, char, int)} but returns a {@link List}.
6591    *
6592    * @param s The string to split.  Can be <jk>null</jk>.
6593    * @param c The character to split on.
6594    * @param limit The maximum number of tokens to return.
6595    * @return The tokens, or an empty list if the string was <jk>null</jk>.
6596    */
6597   public static List<String> split(String s, char c, int limit) {
6598
6599      var escapeChars = getEscapeSet(c);
6600
6601      if (s == null)
6602         return Collections.emptyList();
6603      if (isEmpty(s))
6604         return Collections.emptyList();
6605      if (s.indexOf(c) == -1)
6606         return Collections.singletonList(s);
6607
6608      var l = new LinkedList<String>();
6609      var sArray = s.toCharArray();
6610      var x1 = 0;
6611      var escapeCount = 0;
6612      limit--;
6613      for (var i = 0; i < sArray.length && limit > 0; i++) {
6614         if (sArray[i] == '\\')
6615            escapeCount++;
6616         else if (sArray[i] == c && escapeCount % 2 == 0) {
6617            var s2 = new String(sArray, x1, i - x1);
6618            var s3 = unescapeChars(s2, escapeChars);
6619            l.add(s3.trim());
6620            limit--;
6621            x1 = i + 1;
6622         }
6623         if (sArray[i] != '\\')
6624            escapeCount = 0;
6625      }
6626      var s2 = new String(sArray, x1, sArray.length - x1);
6627      var s3 = unescapeChars(s2, escapeChars);
6628      l.add(s3.trim());
6629
6630      return l;
6631   }
6632
6633   /**
6634    * Same as {@link #splita(String)} but consumes the tokens instead of creating an array.
6635    *
6636    * @param s The string to split.  Can be <jk>null</jk> (no tokens are consumed).
6637    * @param consumer The consumer of the tokens.  Must not be <jk>null</jk> when the string is non-empty.
6638    */
6639   public static void split(String s, Consumer<String> consumer) {
6640      StringUtils.split(s, ',', consumer);
6641   }
6642
6643   /**
6644    * Splits a comma-delimited list into an array of strings.
6645    *
6646    * @param s The string to split.  Can be <jk>null</jk> (returns <jk>null</jk>).
6647    * @return An array of split strings, or <jk>null</jk> if the string was <jk>null</jk>.
6648    */
6649   public static String[] splita(String s) {
6650      return splita(s, ',');
6651   }
6652
6653   /**
6654    * Splits a character-delimited string into a string array.
6655    *
6656    * <p>
6657    * Does not split on escaped-delimiters (e.g. "\,");
6658    * Resulting tokens are trimmed of whitespace.
6659    *
6660    * <p>
6661    * <b>NOTE:</b>  This behavior is different than the Jakarta equivalent.
6662    * split("a,b,c",',') -&gt; {"a","b","c"}
6663    * split("a, b ,c ",',') -&gt; {"a","b","c"}
6664    * split("a,,c",',') -&gt; {"a","","c"}
6665    * split(",,",',') -&gt; {"","",""}
6666    * split("",',') -&gt; {}
6667    * split(null,',') -&gt; null
6668    * split("a,b\,c,d", ',', false) -&gt; {"a","b\,c","d"}
6669    * split("a,b\\,c,d", ',', false) -&gt; {"a","b\","c","d"}
6670    * split("a,b\,c,d", ',', true) -&gt; {"a","b,c","d"}
6671    *
6672    * @param s The string to split.  Can be <jk>null</jk>.
6673    * @param c The character to split on.
6674    * @return The tokens, or <jk>null</jk> if the string was null.
6675    */
6676   public static String[] splita(String s, char c) {
6677      return splita(s, c, Integer.MAX_VALUE);
6678   }
6679
6680   /**
6681    * Same as {@link #splita(String, char)} but limits the number of tokens returned.
6682    *
6683    * @param s The string to split.  Can be <jk>null</jk>.
6684    * @param c The character to split on.
6685    * @param limit The maximum number of tokens to return.
6686    * @return The tokens, or <jk>null</jk> if the string was null.
6687    */
6688   public static String[] splita(String s, char c, int limit) {
6689      if (s == null)
6690         return null;
6691      var l = StringUtils.split(s, c, limit);
6692      return l.toArray(new String[l.size()]);
6693   }
6694
6695   /**
6696    * Same as {@link #splita(String, char)} except splits all strings in the input and returns a single result.
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 null if the input array was null
6701    */
6702   public static String[] splita(String[] s, char c) {
6703      if (s == null)
6704         return null;
6705      var l = new LinkedList<String>();
6706      for (var ss : s) {
6707         if (ss == null || ss.indexOf(c) == -1)
6708            l.add(ss);
6709         else
6710            Collections.addAll(l, splita(ss, c));
6711      }
6712      return l.toArray(new String[l.size()]);
6713   }
6714
6715   /**
6716    * Splits a list of key-value pairs into an ordered map.
6717    *
6718    * <p>
6719    * Example:
6720    * <p class='bjava'>
6721    *    String <jv>in</jv> = <js>"foo=1;bar=2"</js>;
6722    *    Map <jv>map</jv> = StringUtils.<jsm>splitMap</jsm>(in, <js>';'</js>, <js>'='</js>, <jk>true</jk>);
6723    * </p>
6724    *
6725    * @param s The string to split.
6726    * @param trim Trim strings after parsing.
6727    * @return The parsed map, or an empty map if the string was null.
6728    */
6729   public static Map<String,String> splitMap(String s, boolean trim) {
6730
6731      if (s == null)
6732         return emptyMap();
6733      if (isEmpty(s))
6734         return emptyMap();
6735
6736      var m = new LinkedHashMap<String,String>();
6737
6738      // S1: Found start of key, looking for equals.
6739      // S2: Found equals, looking for delimiter (or end).
6740
6741      var state = S1;
6742
6743      var sArray = s.toCharArray();
6744      var x1 = 0;
6745      var escapeCount = 0;
6746      String key = null;
6747      for (var i = 0; i < sArray.length + 1; i++) {
6748         var c = i == sArray.length ? ',' : sArray[i];
6749         if (c == '\\')
6750            escapeCount++;
6751         if (escapeCount % 2 == 0) {
6752            if (state == S1) {
6753               if (c == '=') {
6754                  key = s.substring(x1, i);
6755                  if (trim)
6756                     key = trim(key);
6757                  key = unescapeChars(key, MAP_ESCAPE_SET);
6758                  state = S2;
6759                  x1 = i + 1;
6760               } else if (c == ',') {
6761                  key = s.substring(x1, i);
6762                  if (trim)
6763                     key = trim(key);
6764                  key = unescapeChars(key, MAP_ESCAPE_SET);
6765                  m.put(key, "");
6766                  state = S1;
6767                  x1 = i + 1;
6768               }
6769            } else /* state == S2 */ {
6770               if (c == ',') {
6771                  var val = s.substring(x1, i);
6772                  if (trim)
6773                     val = trim(val);
6774                  val = unescapeChars(val, MAP_ESCAPE_SET);
6775                  m.put(key, val);
6776                  key = null;
6777                  x1 = i + 1;
6778                  state = S1;
6779               }
6780            }
6781         }
6782         if (c != '\\')
6783            escapeCount = 0;
6784      }
6785
6786      return m;
6787   }
6788
6789   /**
6790    * Splits the method arguments in the signature of a method.
6791    *
6792    * @param s The arguments to split.  Can be <jk>null</jk> (returns an empty array).
6793    * @return The split arguments, or an empty array if the input string is <jk>null</jk> or empty.
6794    */
6795   public static String[] splitMethodArgs(String s) {
6796
6797      if (s == null)
6798         return new String[0];
6799      if (isEmpty(s))
6800         return new String[0];
6801      if (s.indexOf(',') == -1)
6802         return array(s);
6803
6804      var l = new LinkedList<String>();
6805      var sArray = s.toCharArray();
6806      var x1 = 0;
6807      var paramDepth = 0;
6808
6809      for (var i = 0; i < sArray.length; i++) {
6810         var c = s.charAt(i);
6811         if (c == '>')
6812            paramDepth++;
6813         else if (c == '<')
6814            paramDepth--;
6815         else if (c == ',' && paramDepth == 0) {
6816            var s2 = new String(sArray, x1, i - x1);
6817            l.add(s2.trim());
6818            x1 = i + 1;
6819         }
6820      }
6821
6822      var s2 = new String(sArray, x1, sArray.length - x1);
6823      l.add(s2.trim());
6824
6825      return l.toArray(new String[l.size()]);
6826   }
6827
6828   /**
6829    * Splits a comma-delimited list containing "nesting constructs".
6830    *
6831    * Nesting constructs are simple embedded "{...}" comma-delimted lists.
6832    *
6833    * Example:
6834    *    "a{b,c},d" -> ["a{b,c}","d"]
6835    *
6836    * Handles escapes and trims whitespace from tokens.
6837    *
6838    * @param s The input string.
6839    * @return
6840    *    The results, or an empty list if the input was <jk>null</jk>.
6841    *    <br>An empty string results in an empty list.
6842    */
6843   public static List<String> splitNested(String s) {
6844      var escapeChars = getEscapeSet(',');
6845
6846      if (s == null)
6847         return Collections.emptyList();
6848      if (isEmpty(s))
6849         return Collections.emptyList();
6850      if (s.indexOf(',') == -1)
6851         return Collections.singletonList(trim(s));
6852
6853      var l = new LinkedList<String>();
6854
6855      var x1 = 0;
6856      var inEscape = false;
6857      var depthCount = 0;
6858
6859      for (var i = 0; i < s.length(); i++) {
6860         var c = s.charAt(i);
6861         if (inEscape) {
6862            if (c == '\\') {
6863               inEscape = false;
6864            }
6865         } else {
6866            if (c == '\\') {
6867               inEscape = true;
6868            } else if (c == '{') {
6869               depthCount++;
6870            } else if (c == '}') {
6871               depthCount--;
6872            } else if (c == ',' && depthCount == 0) {
6873               l.add(trim(unescapeChars(s.substring(x1, i), escapeChars)));
6874               x1 = i + 1;
6875            }
6876         }
6877      }
6878      l.add(trim(unescapeChars(s.substring(x1, s.length()), escapeChars)));
6879
6880      return l;
6881   }
6882
6883   /**
6884    * Splits a nested comma-delimited list.
6885    *
6886    * Nesting constructs are simple embedded "{...}" comma-delimted lists.
6887    *
6888    * Example:
6889    *    "a{b,c{d,e}}" -> ["b","c{d,e}"]
6890    *
6891    * Handles escapes and trims whitespace from tokens.
6892    *
6893    * @param s The input string.  Must not be <jk>null</jk> or empty (a <jk>null</jk> or empty argument throws {@link IllegalArgumentException}).
6894    * @return
6895    *    The results.
6896    * @throws IllegalArgumentException If the input was <jk>null</jk> or empty.
6897    */
6898   public static List<String> splitNestedInner(String s) {
6899      req(isNotNull(s), "String was null.");
6900      req(isNotEmpty(s), "String was empty.");
6901
6902      // S1: Looking for '{'
6903      // S2: Found '{', looking for '}'
6904
6905      var start = -1;
6906      var end = -1;
6907      var state = S1;
6908
6909      var depth = 0;
6910      var inEscape = false;
6911
6912      for (var i = 0; i < s.length(); i++) {
6913         var c = s.charAt(i);
6914         if (inEscape) {
6915            if (c == '\\') {
6916               inEscape = false;
6917            }
6918         } else {
6919            if (c == '\\') {
6920               inEscape = true;
6921            } else if (state == S1) {
6922               if (c == '{') {
6923                  start = i + 1;
6924                  state = S2;
6925               }
6926            } else /* state == S2 */ {
6927               if (c == '{') {
6928                  depth++;
6929               } else if (depth > 0 && c == '}') {
6930                  depth--;
6931               } else if (c == '}') {
6932                  end = i;
6933                  break;
6934               }
6935            }
6936         }
6937      }
6938
6939      if (start == -1)
6940         throw iaex("Start character { not found in string:  %s", s);
6941      if (end == -1)
6942         throw iaex("End character } not found in string  %s", s);
6943      return splitNested(s.substring(start, end));
6944   }
6945
6946   /**
6947    * Splits a space-delimited string with optionally quoted arguments.
6948    *
6949    * <p>
6950    * Examples:
6951    * <ul>
6952    *    <li><js>"foo"</js> =&gt; <c>["foo"]</c>
6953    *    <li><js>" foo "</js> =&gt; <c>["foo"]</c>
6954    *    <li><js>"foo bar baz"</js> =&gt; <c>["foo","bar","baz"]</c>
6955    *    <li><js>"foo 'bar baz'"</js> =&gt; <c>["foo","bar baz"]</c>
6956    *    <li><js>"foo \"bar baz\""</js> =&gt; <c>["foo","bar baz"]</c>
6957    *    <li><js>"foo 'bar\'baz'"</js> =&gt; <c>["foo","bar'baz"]</c>
6958    * </ul>
6959    *
6960    * @param s The input string.  Can be <jk>null</jk> (returns an empty array).
6961    * @return
6962    *    The results, or an empty array if the input was <jk>null</jk>.
6963    *    <br>An empty string results in an empty array.
6964    */
6965   public static String[] splitQuoted(String s) {
6966      return splitQuoted(s, false);
6967   }
6968
6969   /**
6970    * Same as {@link StringUtils#splitQuoted(String)} but allows you to optionally keep the quote characters.
6971    *
6972    * @param s The input string.  Can be <jk>null</jk> (returns an empty array).
6973    * @param keepQuotes If <jk>true</jk>, quote characters are kept on the tokens.
6974    * @return
6975    *    The results, or an empty array if the input was <jk>null</jk>.
6976    *    <br>An empty string results in an empty array.
6977    */
6978   @SuppressWarnings({
6979      "java:S2583" // State variables persist across loop iterations
6980   })
6981   public static String[] splitQuoted(String s, boolean keepQuotes) {
6982
6983      if (s == null)
6984         return new String[0];
6985
6986      s = s.trim();
6987
6988      if (isEmpty(s))
6989         return array();
6990
6991      if (! containsAny(s, ' ', '\t', '\'', '"'))
6992         return array(s);
6993
6994      // S1: Looking for start of token.
6995      // S2: Found ', looking for end '
6996      // S3: Found ", looking for end "
6997      // S4: Found non-whitespace, looking for end whitespace.
6998
6999      var state = S1;
7000
7001      var isInEscape = false;
7002      var needsUnescape = false;
7003      var mark = 0;
7004
7005      var l = new ArrayList<String>();
7006      for (var i = 0; i < s.length(); i++) {
7007         var c = s.charAt(i);
7008
7009         if (state == S1) {
7010            if (c == '\'') {
7011               state = S2;
7012               mark = keepQuotes ? i : i + 1;
7013            } else if (c == '"') {
7014               state = S3;
7015               mark = keepQuotes ? i : i + 1;
7016            } else if (c != ' ' && c != '\t') {
7017               state = S4;
7018               mark = i;
7019            }
7020         } else if (state == S2 || state == S3) {
7021            if (c == '\\') {
7022               isInEscape = ! isInEscape;
7023               if (isInEscape)
7024                  needsUnescape = ! keepQuotes;
7025            } else if (! isInEscape) {
7026               if (c == (state == S2 ? '\'' : '"')) {
7027                  var s2 = s.substring(mark, keepQuotes ? i + 1 : i);
7028                  if (needsUnescape)
7029                     s2 = unescapeChars(s2, QUOTE_ESCAPE_SET);
7030                  l.add(s2);
7031                  state = S1;
7032                  isInEscape = needsUnescape = false;
7033               }
7034            } else {
7035               isInEscape = false;
7036            }
7037         } else /* state == S4 */ {
7038            if (c == ' ' || c == '\t') {
7039               l.add(s.substring(mark, i));
7040               state = S1;
7041            }
7042         }
7043      }
7044      if (state == S4)
7045         l.add(s.substring(mark));
7046      else if (state == S2 || state == S3)
7047         throw iaex("Unmatched string quotes: %s", s);
7048      return l.toArray(new String[l.size()]);
7049   }
7050
7051   /**
7052    * An efficient method for checking if a string starts with a character.
7053    *
7054    * @param s The string to check.  Can be <jk>null</jk>.
7055    * @param c The character to check for.
7056    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character.
7057    */
7058   public static boolean startsWith(String s, char c) {
7059      if (isNotNull(s)) {
7060         var i = s.length();
7061         if (i > 0)
7062            return s.charAt(0) == c;
7063      }
7064      return false;
7065   }
7066
7067   /**
7068    * Checks if a string starts with a prefix, ignoring case.
7069    *
7070    * <h5 class='section'>Example:</h5>
7071    * <p class='bjava'>
7072    *    startsWithIgnoreCase(<js>"Hello World"</js>, <js>"hello"</js>);     <jc>// true</jc>
7073    *    startsWithIgnoreCase(<js>"Hello World"</js>, <js>"HELLO"</js>);     <jc>// true</jc>
7074    *    startsWithIgnoreCase(<js>"hello world"</js>, <js>"world"</js>);     <jc>// false</jc>
7075    * </p>
7076    *
7077    * @param str The string to check.
7078    * @param prefix The prefix to check for.
7079    * @return <jk>true</jk> if the string starts with the prefix (ignoring case), <jk>false</jk> otherwise.
7080    */
7081   public static boolean startsWithIgnoreCase(String str, String prefix) {
7082      if (str == null || prefix == null)
7083         return false;
7084      return str.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT));
7085   }
7086
7087   /**
7088    * Takes a supplier of any type and returns a {@link Supplier}{@code <String>}.
7089    *
7090    * <p>Useful when passing arguments to loggers.
7091    *
7092    * @param s The supplier.  Must not be <jk>null</jk> (the returned supplier throws {@link NullPointerException} when invoked).
7093    * @return A string supplier that calls {@link #readable(Object)} on the supplied value.
7094    */
7095   public static Supplier<String> stringSupplier(Supplier<?> s) {
7096      return () -> readable(s.get());
7097   }
7098
7099   /**
7100    * Strips the first and last character from a string.
7101    *
7102    * @param s The string to strip.
7103    * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2.
7104    */
7105   public static String strip(String s) {
7106      if (s == null || s.length() <= 1)
7107         return s;
7108      return s.substring(1, s.length() - 1);
7109   }
7110
7111   /**
7112    * Strips invalid characters such as CTRL characters from a string meant to be encoded
7113    * as an HTTP header value.
7114    *
7115    * @param s The string to strip chars from.
7116    * @return The string with invalid characters removed.
7117    */
7118   public static String stripInvalidHttpHeaderChars(String s) {
7119
7120      if (s == null)
7121         return null;
7122
7123      var needsReplace = false;
7124      for (var i = 0; i < s.length() && ! needsReplace; i++)
7125         needsReplace |= HTTP_HEADER_CHARS.contains(s.charAt(i));
7126
7127      if (! needsReplace)
7128         return s;
7129
7130      var sb = new StringBuilder(s.length());
7131      for (var i = 0; i < s.length(); i++) {
7132         var c = s.charAt(i);
7133         if (HTTP_HEADER_CHARS.contains(c))
7134            sb.append(c);
7135      }
7136
7137      return sb.toString();
7138   }
7139
7140   /**
7141    * Returns the substring after the first occurrence of a separator.
7142    *
7143    * <h5 class='section'>Example:</h5>
7144    * <p class='bjava'>
7145    *    substringAfter(<jk>null</jk>, <js>"."</js>);              <jc>// null</jc>
7146    *    substringAfter(<js>"hello.world"</js>, <jk>null</jk>);    <jc>// ""</jc>
7147    *    substringAfter(<js>"hello.world"</js>, <js>"."</js>);     <jc>// "world"</jc>
7148    *    substringAfter(<js>"hello.world"</js>, <js>"xyz"</js>);   <jc>// ""</jc>
7149    * </p>
7150    *
7151    * @param str The string to get a substring from.
7152    * @param separator The separator string.
7153    * @return The substring after the first occurrence of the separator, or empty string if separator not found.
7154    */
7155   public static String substringAfter(String str, String separator) {
7156      if (isEmpty(str))
7157         return str;
7158      if (separator == null)
7159         return "";
7160      var pos = str.indexOf(separator);
7161      if (pos == -1)
7162         return "";
7163      return str.substring(pos + separator.length());
7164   }
7165
7166   /**
7167    * Returns the substring before the first occurrence of a separator.
7168    *
7169    * <h5 class='section'>Example:</h5>
7170    * <p class='bjava'>
7171    *    substringBefore(<jk>null</jk>, <js>"."</js>);              <jc>// null</jc>
7172    *    substringBefore(<js>"hello.world"</js>, <jk>null</jk>);    <jc>// "hello.world"</jc>
7173    *    substringBefore(<js>"hello.world"</js>, <js>"."</js>);     <jc>// "hello"</jc>
7174    *    substringBefore(<js>"hello.world"</js>, <js>"xyz"</js>);   <jc>// "hello.world"</jc>
7175    * </p>
7176    *
7177    * @param str The string to get a substring from.
7178    * @param separator The separator string.
7179    * @return The substring before the first occurrence of the separator, or the original string if separator not found.
7180    */
7181   public static String substringBefore(String str, String separator) {
7182      if (isEmpty(str) || separator == null)
7183         return str;
7184      var pos = str.indexOf(separator);
7185      if (pos == -1)
7186         return str;
7187      return str.substring(0, pos);
7188   }
7189
7190   /**
7191    * Returns the substring between two delimiters.
7192    *
7193    * <h5 class='section'>Example:</h5>
7194    * <p class='bjava'>
7195    *    substringBetween(<jk>null</jk>, <js>"&lt;"</js>, <js>"&gt;"</js>);              <jc>// null</jc>
7196    *    substringBetween(<js>"&lt;hello&gt;"</js>, <js>"&lt;"</js>, <js>"&gt;"</js>);   <jc>// "hello"</jc>
7197    *    substringBetween(<js>"&lt;hello&gt;"</js>, <js>"["</js>, <js>"]"</js>);         <jc>// null</jc>
7198    * </p>
7199    *
7200    * @param str The string to get a substring from.
7201    * @param open The opening delimiter.
7202    * @param close The closing delimiter.
7203    * @return The substring between the delimiters, or <jk>null</jk> if delimiters not found.
7204    */
7205   public static String substringBetween(String str, String open, String close) {
7206      if (str == null || open == null || close == null)
7207         return null;
7208      var start = str.indexOf(open);
7209      if (start == -1)
7210         return null;
7211      var end = str.indexOf(close, start + open.length());
7212      if (end == -1)
7213         return null;
7214      return str.substring(start + open.length(), end);
7215   }
7216
7217   /**
7218    * Swaps the case of all characters in a string.
7219    *
7220    * <h5 class='section'>Example:</h5>
7221    * <p class='bjava'>
7222    *    swapCase(<js>"Hello World"</js>);   <jc>// "hELLO wORLD"</jc>
7223    *    swapCase(<js>"ABC123xyz"</js>);     <jc>// "abc123XYZ"</jc>
7224    * </p>
7225    *
7226    * @param str The string to process.
7227    * @return The string with case swapped, or <jk>null</jk> if input is <jk>null</jk>.
7228    */
7229   public static String swapCase(String str) {
7230      if (str == null)
7231         return null;
7232      var sb = new StringBuilder(str.length());
7233      for (var i = 0; i < str.length(); i++) {
7234         var c = str.charAt(i);
7235         if (LETTER_UC.contains(c))
7236            sb.append(Character.toLowerCase(c));
7237         else if (LETTER_LC.contains(c))
7238            sb.append(Character.toUpperCase(c));
7239         else
7240            sb.append(c);
7241      }
7242      return sb.toString();
7243   }
7244
7245   /**
7246    * Converts a string to Title Case format (first letter of each word capitalized, separated by spaces).
7247    *
7248    * <p>
7249    * Handles various input formats:
7250    * <ul>
7251    *   <li>CamelCase: "helloWorld" → "Hello World"</li>
7252    *   <li>PascalCase: "HelloWorld" → "Hello World"</li>
7253    *   <li>Snake_case: "hello_world" → "Hello World"</li>
7254    *   <li>Kebab-case: "hello-world" → "Hello World"</li>
7255    * </ul>
7256    *
7257    * <h5 class='section'>Example:</h5>
7258    * <p class='bjava'>
7259    *    titleCase(<jk>null</jk>);                    <jc>// null</jc>
7260    *    titleCase(<js>""</js>);                      <jc>// ""</jc>
7261    *    titleCase(<js>"hello world"</js>);           <jc>// "Hello World"</jc>
7262    *    titleCase(<js>"helloWorld"</js>);            <jc>// "Hello World"</jc>
7263    *    titleCase(<js>"hello_world"</js>);           <jc>// "Hello World"</jc>
7264    *    titleCase(<js>"hello-world"</js>);           <jc>// "Hello World"</jc>
7265    * </p>
7266    *
7267    * @param str The string to convert.
7268    * @return The Title Case string, or <jk>null</jk> if input is <jk>null</jk>.
7269    */
7270   public static String titleCase(String str) {
7271      if (str == null)
7272         return null;
7273      if (isEmpty(str))
7274         return str;
7275
7276      var words = splitWords(str);
7277      if (words.isEmpty())
7278         return "";
7279
7280      var result = new StringBuilder();
7281      for (var i = 0; i < words.size(); i++) {
7282         if (i > 0)
7283            result.append(' ');
7284         result.append(capitalize(words.get(i).toLowerCase()));
7285      }
7286
7287      return result.toString();
7288   }
7289
7290   /**
7291    * Converts the specified object to a comma-delimited list.
7292    *
7293    * @param o The object to convert.  Can be <jk>null</jk> (returns <jk>null</jk>).
7294    * @return The specified object as a comma-delimited list, or <jk>null</jk> if the input was <jk>null</jk>.
7295    */
7296   public static String toCdl(Object o) {
7297      if (o == null)
7298         return null;
7299      if (ClassUtils.isArray(o)) {
7300         var sb = new StringBuilder();
7301         for (int i = 0, j = Array.getLength(o); i < j; i++) {
7302            if (i > 0)
7303               sb.append(", ");
7304            sb.append(Array.get(o, i));
7305         }
7306         return sb.toString();
7307      }
7308      if (o instanceof Collection<?> o2)
7309         return join(o2, ", ");
7310      return o.toString();
7311   }
7312
7313   /**
7314    * Converts the specified byte into a 2 hexadecimal characters.
7315    *
7316    * @param b The number to convert to hex.
7317    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
7318    */
7319   public static String toHex(byte b) {
7320      var c = new char[2];
7321      var v = b & 0xFF;
7322      c[0] = HEX[v >>> 4];
7323      c[1] = HEX[v & 0x0F];
7324      return new String(c);
7325   }
7326
7327   /**
7328    * Converts a byte array into a simple hexadecimal character string.
7329    *
7330    * @param bytes The bytes to convert to hexadecimal.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
7331    * @return A new string consisting of hexadecimal characters.
7332    */
7333   public static String toHex(byte[] bytes) {
7334      var sb = new StringBuilder(bytes.length * 2);
7335      for (var element : bytes) {
7336         var v = element & 0xFF;
7337         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
7338      }
7339      return sb.toString();
7340   }
7341
7342   /**
7343    * Converts the contents of the specified input stream to a hex string.
7344    *
7345    * @param is The input stream to convert.
7346    * @return The hex string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>.
7347    */
7348   public static String toHex(InputStream is) {
7349      return safe(() -> is == null ? null : toHex(readBytes(is)));
7350   }
7351
7352   /**
7353    * Converts the specified number into a 2 hexadecimal characters.
7354    *
7355    * @param num The number to convert to hex.
7356    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
7357    */
7358   public static char[] toHex2(int num) {
7359      if (num < 0 || num > 255)
7360         throw new NumberFormatException("toHex2 can only be used on numbers between 0 and 255");
7361      var n = new char[2];
7362      var a = num % 16;
7363      n[1] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7364      a = (num / 16) % 16;
7365      n[0] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7366      return n;
7367   }
7368
7369   /**
7370    * Converts the specified number into a 4 hexadecimal characters.
7371    *
7372    * @param num The number to convert to hex.
7373    * @return A <code><jk>char</jk>[4]</code> containing the specified characters.
7374    * @throws NumberFormatException If the number is negative.
7375    */
7376   public static char[] toHex4(int num) {
7377      if (num < 0)
7378         throw new NumberFormatException("toHex4 can only be used on non-negative numbers");
7379      var n = new char[4];
7380      var a = num % 16;
7381      n[3] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7382      var base = 16;
7383      for (var i = 1; i < 4; i++) {
7384         a = (num / base) % 16;
7385         base <<= 4;
7386         n[3 - i] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7387      }
7388      return n;
7389   }
7390
7391   /**
7392    * Converts the specified number into a 8 hexadecimal characters.
7393    *
7394    * @param num The number to convert to hex.
7395    * @return A <code><jk>char</jk>[8]</code> containing the specified characters.
7396    * @throws NumberFormatException If the number is negative.
7397    */
7398   public static char[] toHex8(long num) {
7399      if (num < 0)
7400         throw new NumberFormatException("toHex8 can only be used on non-negative numbers");
7401      var n = new char[8];
7402      var a = num % 16;
7403      n[7] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7404      var base = 16;
7405      for (var i = 1; i < 8; i++) {
7406         a = (num / base) % 16;
7407         base <<= 4;
7408         n[7 - i] = (char)(a > 9 ? 'A' + a - 10 : '0' + a);
7409      }
7410      return n;
7411   }
7412
7413   /**
7414    * Converts the specified object to an ISO8601 date string.
7415    *
7416    * @param c The object to convert.  Can be <jk>null</jk> (returns <jk>null</jk>).
7417    * @return The converted object, or <jk>null</jk> if the input was <jk>null</jk>.
7418    */
7419   public static String toIsoDate(Calendar c) {
7420      if (c == null) {
7421         return null;
7422      }
7423      // Convert Calendar to ZonedDateTime and format as ISO8601 date (YYYY-MM-DD)
7424      ZonedDateTime zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
7425      return zdt.format(DateTimeFormatter.ISO_LOCAL_DATE);
7426   }
7427
7428   /**
7429    * Converts the specified object to an ISO8601 date-time string.
7430    *
7431    * @param c The object to convert.  Can be <jk>null</jk> (returns <jk>null</jk>).
7432    * @return The converted object, or <jk>null</jk> if the input was <jk>null</jk>.
7433    */
7434   public static String toIsoDateTime(Calendar c) {
7435      if (c == null) {
7436         return null;
7437      }
7438      // Convert Calendar to ZonedDateTime and format as ISO8601 date-time with timezone
7439      ZonedDateTime zdt = c.toInstant().atZone(c.getTimeZone().toZoneId());
7440      return zdt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
7441   }
7442
7443   /**
7444    * Converts the specified bytes into a readable string.
7445    *
7446    * @param b The number to convert to hex.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
7447    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
7448    */
7449   public static String toReadableBytes(byte[] b) {
7450      var sb = new StringBuilder();
7451      for (var b2 : b)
7452         sb.append((b2 < ' ' || b2 > 'z') ? String.format("[%02X]", b2) : (char)b2 + "   ");
7453      sb.append("\n");
7454      for (var b2 : b)
7455         sb.append(String.format("[%02X]", b2));
7456      return sb.toString();
7457   }
7458
7459   /**
7460    * Same as {@link #toHex(byte[])} but puts spaces between the byte strings.
7461    *
7462    * @param bytes The bytes to convert to hexadecimal.  Must not be <jk>null</jk> (a <jk>null</jk> argument throws {@link NullPointerException}).
7463    * @return A new string consisting of hexadecimal characters.
7464    */
7465   public static String toSpacedHex(byte[] bytes) {
7466      var sb = new StringBuilder(bytes.length * 3);
7467      for (var j = 0; j < bytes.length; j++) {
7468         if (j > 0)
7469            sb.append(' ');
7470         var v = bytes[j] & 0xFF;
7471         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
7472      }
7473      return sb.toString();
7474   }
7475
7476   /**
7477    * Safely converts an object to a string, returning <jk>null</jk> if the object is <jk>null</jk>.
7478    *
7479    * @param obj The object to convert to a string.
7480    * @return The string representation of the object, or <jk>null</jk> if the object is <jk>null</jk>.
7481    */
7482   public static String toString(Object obj) {
7483      return obj == null ? null : obj.toString();
7484   }
7485
7486   /**
7487    * Safely converts an object to a string, returning the default string if the object is <jk>null</jk>.
7488    *
7489    * @param obj The object to convert to a string.
7490    * @param defaultStr The default string to return if the object is <jk>null</jk>.
7491    * @return The string representation of the object, or the default string if the object is <jk>null</jk>.
7492    */
7493   public static String toString(Object obj, String defaultStr) {
7494      return obj == null ? defaultStr : obj.toString();
7495   }
7496
7497   /**
7498    * Converts a collection of strings to a string array.
7499    *
7500    * <p>
7501    * Returns an empty array if the collection is <jk>null</jk> or empty.
7502    *
7503    * @param collection The collection to convert. Can be <jk>null</jk> (returns an empty array).
7504    * @return A new string array containing the collection elements, or an empty array if the collection was <jk>null</jk>.
7505    */
7506   public static String[] toStringArray(Collection<String> collection) {
7507      if (collection == null)
7508         return new String[0];
7509      return collection.toArray(new String[collection.size()]);
7510   }
7511
7512   /**
7513    * Converts the specified object to a URI.
7514    *
7515    * @param o The object to convert to a URI.  Can be <jk>null</jk> (returns <jk>null</jk>).
7516    * @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>.
7517    */
7518   public static URI toUri(Object o) {
7519      if (o == null || o instanceof URI)
7520         return (URI)o;
7521      try {
7522         return new URI(o.toString());
7523      } catch (URISyntaxException e) {
7524         throw toRuntimeException(e);
7525      }
7526   }
7527
7528   /**
7529    * Converts the specified byte array to a UTF-8 string.
7530    *
7531    * @param b The byte array to convert.
7532    * @return The UTF-8 string representation, or <jk>null</jk> if the array is <jk>null</jk>.
7533    */
7534   public static String toUtf8(byte[] b) {
7535      return b == null ? null : new String(b, UTF8);
7536   }
7537
7538   /**
7539    * Converts the contents of the specified input stream to a UTF-8 string.
7540    *
7541    * @param is The input stream to convert.
7542    * @return The UTF-8 string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>.
7543    */
7544   public static String toUtf8(InputStream is) {
7545      return safe(() -> is == null ? null : new String(readBytes(is), UTF8));
7546   }
7547
7548   /**
7549    * Transliterates characters in a string by mapping characters from one set to another.
7550    *
7551    * <p>
7552    * Performs character-by-character translation. If a character is found in <c>fromChars</c>,
7553    * it is replaced with the corresponding character at the same position in <c>toChars</c>.
7554    * Characters not found in <c>fromChars</c> are left unchanged.
7555    *
7556    * <h5 class='section'>Example:</h5>
7557    * <p class='bjava'>
7558    *    transliterate(<js>"hello"</js>, <js>"aeiou"</js>, <js>"12345"</js>);
7559    *    <jc>// "h2ll4"</jc>
7560    *    transliterate(<js>"ABC"</js>, <js>"ABC"</js>, <js>"XYZ"</js>);
7561    *    <jc>// "XYZ"</jc>
7562    * </p>
7563    *
7564    * @param str The string to transliterate. Can be <jk>null</jk> (returns <jk>null</jk>).
7565    * @param fromChars The source character set. Can be <jk>null</jk> (returns <c>str</c> unchanged).
7566    * @param toChars The target character set. Can be <jk>null</jk> (returns <c>str</c> unchanged).
7567    * @return The transliterated string, or <jk>null</jk> if input is <jk>null</jk>.
7568    * @throws IllegalArgumentException If <c>fromChars</c> and <c>toChars</c> have different lengths.
7569    */
7570   public static String transliterate(String str, String fromChars, String toChars) {
7571      if (str == null)
7572         return null;
7573      if (fromChars == null || toChars == null || fromChars.isEmpty() || toChars.isEmpty())
7574         return str;
7575      if (fromChars.length() != toChars.length())
7576         throw iaex("fromChars and toChars must have the same length");
7577
7578      var sb = new StringBuilder(str.length());
7579      for (var i = 0; i < str.length(); i++) {
7580         var c = str.charAt(i);
7581         var index = fromChars.indexOf(c);
7582         if (index >= 0)
7583            sb.append(toChars.charAt(index));
7584         else
7585            sb.append(c);
7586      }
7587      return sb.toString();
7588   }
7589
7590   /**
7591    * Same as {@link String#trim()} but prevents <c>NullPointerExceptions</c>.
7592    *
7593    * @param s The string to trim.
7594    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
7595    */
7596   public static String trim(String s) {
7597      if (s == null)
7598         return null;
7599      return s.trim();
7600   }
7601
7602   /**
7603    * Trims whitespace characters from the end of the specified string.
7604    *
7605    * @param s The string to trim.
7606    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
7607    */
7608   public static String trimEnd(String s) {
7609      if (isNotNull(s))
7610         while (isNotEmpty(s) && isWhitespace(s.charAt(s.length() - 1)))
7611            s = s.substring(0, s.length() - 1);
7612      return s;
7613   }
7614
7615   /**
7616    * Trims <js>'/'</js> characters from the beginning of the specified string.
7617    *
7618    * @param s The string to trim.
7619    * @return A new trimmed string, or the same string if no trimming was necessary.
7620    */
7621   public static String trimLeadingSlashes(String s) {
7622      if (s == null)
7623         return null;
7624      while (isNotEmpty(s) && s.charAt(0) == '/')
7625         s = s.substring(1);
7626      return s;
7627   }
7628
7629   /**
7630    * Trims <js>'/'</js> characters from both the start and end of the specified string.
7631    *
7632    * @param s The string to trim.
7633    * @return A new trimmed string, or the same string if no trimming was necessary.
7634    */
7635   public static String trimSlashes(String s) {
7636      if (s == null)
7637         return null;
7638      if (s.isEmpty())
7639         return s;
7640      while (endsWith(s, '/'))
7641         s = s.substring(0, s.length() - 1);
7642      while (isNotEmpty(s) && s.charAt(0) == '/')
7643         s = s.substring(1);
7644      return s;
7645   }
7646
7647   /**
7648    * Trims <js>'/'</js> and space characters from both the start and end of the specified string.
7649    *
7650    * @param s The string to trim.
7651    * @return A new trimmed string, or the same string if no trimming was necessary.
7652    */
7653   public static String trimSlashesAndSpaces(String s) {
7654      if (s == null)
7655         return null;
7656      while (isNotEmpty(s) && (s.charAt(s.length() - 1) == '/' || isWhitespace(s.charAt(s.length() - 1))))
7657         s = s.substring(0, s.length() - 1);
7658      while (isNotEmpty(s) && (s.charAt(0) == '/' || isWhitespace(s.charAt(0))))
7659         s = s.substring(1);
7660      return s;
7661   }
7662
7663   /**
7664    * Trims whitespace characters from the beginning of the specified string.
7665    *
7666    * @param s The string to trim.
7667    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
7668    */
7669   public static String trimStart(String s) {
7670      if (isNotNull(s))
7671         while (isNotEmpty(s) && isWhitespace(s.charAt(0)))
7672            s = s.substring(1);
7673      return s;
7674   }
7675
7676   /**
7677    * Trims <js>'/'</js> characters from the end of the specified string.
7678    *
7679    * @param s The string to trim.
7680    * @return A new trimmed string, or the same string if no trimming was necessary.
7681    */
7682   public static String trimTrailingSlashes(String s) {
7683      if (s == null)
7684         return null;
7685      while (endsWith(s, '/'))
7686         s = s.substring(0, s.length() - 1);
7687      return s;
7688   }
7689
7690   /**
7691    * Uncapitalizes the first character of a string.
7692    *
7693    * <h5 class='section'>Example:</h5>
7694    * <p class='bjava'>
7695    *    uncapitalize(<jk>null</jk>);          <jc>// null</jc>
7696    *    uncapitalize(<js>""</js>);            <jc>// ""</jc>
7697    *    uncapitalize(<js>"Hello"</js>);       <jc>// "hello"</jc>
7698    *    uncapitalize(<js>"hello"</js>);       <jc>// "hello"</jc>
7699    *    uncapitalize(<js>"HELLO"</js>);       <jc>// "hELLO"</jc>
7700    * </p>
7701    *
7702    * @param str The string to uncapitalize.
7703    * @return The string with the first character uncapitalized, or <jk>null</jk> if input is <jk>null</jk>.
7704    */
7705   public static String uncapitalize(String str) {
7706      if (isEmpty(str))
7707         return str;
7708      return Character.toLowerCase(str.charAt(0)) + str.substring(1);
7709   }
7710
7711   /**
7712    * Removes escape characters from the specified characters.
7713    *
7714    * @param s The string to remove escape characters from.  Can be <jk>null</jk> (returns <jk>null</jk>).
7715    * @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}).
7716    * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>.
7717    */
7718   public static String unescapeChars(String s, AsciiSet escaped) {
7719      if (s == null || s.isEmpty())
7720         return s;
7721      var count = 0;
7722      for (var i = 0; i < s.length(); i++)
7723         if (escaped.contains(s.charAt(i)))
7724            count++;
7725      if (count == 0)
7726         return s;
7727      var sb = new StringBuffer(s.length() - count);
7728      for (var i = 0; i < s.length(); i++) {
7729         var c = s.charAt(i);
7730
7731         if (c == '\\' && i + 1 != s.length()) {
7732            var c2 = s.charAt(i + 1);
7733            if (escaped.contains(c2)) {
7734               i++;
7735            }
7736         }
7737         sb.append(s.charAt(i));
7738      }
7739      return sb.toString();
7740   }
7741
7742   /**
7743    * Unescapes HTML entities in a string.
7744    *
7745    * <p>
7746    * Unescapes the following HTML entities:
7747    * <ul>
7748    *   <li><js>"&amp;amp;"</js> → <js>'&amp;'</js></li>
7749    *   <li><js>"&amp;lt;"</js> → <js>'&lt;'</js></li>
7750    *   <li><js>"&amp;gt;"</js> → <js>'&gt;'</js></li>
7751    *   <li><js>"&amp;quot;"</js> → <js>'"'</js></li>
7752    *   <li><js>"&amp;#39;"</js> or <js>"&amp;apos;"</js> → <js>'\''</js></li>
7753    * </ul>
7754    *
7755    * <h5 class='section'>Example:</h5>
7756    * <p class='bjava'>
7757    *    unescapeHtml(<js>"&amp;lt;script&amp;gt;"</js>);     <jc>// Returns: "&lt;script&gt;"</jc>
7758    * </p>
7759    *
7760    * @param str The string to unescape.
7761    * @return The unescaped string, or <jk>null</jk> if input is <jk>null</jk>.
7762    */
7763   public static String unescapeHtml(String str) {
7764      if (str == null)
7765         return null;
7766      // Must unescape &amp; last to avoid interfering with other entities
7767      return str.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'").replace("&apos;", "'").replace("&amp;", "&");
7768   }
7769
7770   /**
7771    * Unescapes XML entities in a string.
7772    *
7773    * <p>
7774    * Unescapes the following XML entities:
7775    * <ul>
7776    *   <li><js>"&amp;amp;"</js> → <js>'&amp;'</js></li>
7777    *   <li><js>"&amp;lt;"</js> → <js>'&lt;'</js></li>
7778    *   <li><js>"&amp;gt;"</js> → <js>'&gt;'</js></li>
7779    *   <li><js>"&amp;quot;"</js> → <js>'"'</js></li>
7780    *   <li><js>"&amp;apos;"</js> → <js>'\''</js></li>
7781    * </ul>
7782    *
7783    * <h5 class='section'>Example:</h5>
7784    * <p class='bjava'>
7785    *    unescapeXml(<js>"&amp;lt;tag&amp;gt;"</js>);     <jc>// Returns: "&lt;tag&gt;"</jc>
7786    * </p>
7787    *
7788    * @param str The string to unescape.
7789    * @return The unescaped string, or <jk>null</jk> if input is <jk>null</jk>.
7790    */
7791   public static String unescapeXml(String str) {
7792      if (str == null)
7793         return null;
7794      // Must unescape &amp; last to avoid interfering with other entities
7795      return str.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&apos;", "'").replace("&amp;", "&");
7796   }
7797
7798   /**
7799    * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for the specified character.
7800    *
7801    * @param c The character to create a sequence for.
7802    * @return An escaped-unicode sequence.
7803    */
7804   public static String unicodeSequence(char c) {
7805      var sb = new StringBuilder(6);
7806      sb.append('\\').append('u');
7807      for (var cc : toHex4(c))
7808         sb.append(cc);
7809      return sb.toString();
7810   }
7811
7812   /**
7813    * Null-safe convenience method for {@link String#toUpperCase()}.
7814    *
7815    * <p>
7816    * Converts the string to uppercase if not null.
7817    *
7818    * @param s The string to convert.
7819    * @return The uppercase string, or <jk>null</jk> if the input was <jk>null</jk>.
7820    * @see #lowerCase(String)
7821    * @see #upperCase(String)
7822    */
7823   public static String upperCase(String s) {
7824      return s == null ? null : s.toUpperCase();
7825   }
7826
7827   /**
7828    * Null-safe convenience method for {@link String#toUpperCase(Locale)} using {@link Locale#ROOT}.
7829    *
7830    * <p>
7831    * Locale-independent upper-casing for parse/match/normalize/key-fold code paths (as opposed to
7832    * {@link #upperCase(String)}, which uses the default locale and is intended for display casing).
7833    *
7834    * @param s The string to convert.
7835    * @return The uppercase string, or <jk>null</jk> if the input was <jk>null</jk>.
7836    * @see #lowerCaseRoot(String)
7837    * @see #upperCase(String)
7838    */
7839   public static String upperCaseRoot(String s) {
7840      return s == null ? null : s.toUpperCase(Locale.ROOT);
7841   }
7842
7843   /**
7844    * Decodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
7845    *
7846    * @param s The string to decode.
7847    * @return The decoded string, or <jk>null</jk> if input is <jk>null</jk>.
7848    */
7849   public static String urlDecode(String s) {
7850
7851      if (s == null)
7852         return s;
7853
7854      var needsDecode = false;
7855      for (var i = 0; i < s.length() && ! needsDecode; i++) {
7856         var c = s.charAt(i);
7857         if (c == '+' || c == '%')
7858            needsDecode = true;
7859      }
7860
7861      if (needsDecode) {
7862         return safe(()->URLDecoder.decode(s, UTF_8.name()));
7863      }
7864      return s;
7865   }
7866
7867   /**
7868    * Encodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
7869    *
7870    * @param s The string to encode.
7871    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
7872    */
7873   public static String urlEncode(String s) {
7874
7875      if (s == null)
7876         return null;
7877
7878      var needsEncode = false;
7879
7880      for (var i = 0; i < s.length() && ! needsEncode; i++)
7881         needsEncode |= (! URL_UNENCODED_CHARS.contains(s.charAt(i)));
7882
7883      if (needsEncode) {
7884         return safe(()->URLEncoder.encode(s, UTF_8.name()));
7885      }
7886
7887      return s;
7888   }
7889
7890   /**
7891    * Same as {@link #urlEncode(String)} except only escapes characters that absolutely need to be escaped.
7892    *
7893    * @param s The string to escape.
7894    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
7895    */
7896   public static String urlEncodeLax(String s) {
7897      if (s == null)
7898         return null;
7899      var needsEncode = false;
7900      for (var i = 0; i < s.length() && ! needsEncode; i++)
7901         needsEncode |= (! URL_UNENCODED_LAX_CHARS.contains(s.charAt(i)));
7902      if (needsEncode) {
7903         var sb = new StringBuilder(s.length() * 2);
7904         for (var i = 0; i < s.length(); i++) {
7905            var c = s.charAt(i);
7906            if (URL_UNENCODED_LAX_CHARS.contains(c))
7907               sb.append(c);
7908            else if (c == ' ')
7909               sb.append("+");
7910            else if (c <= 127)
7911               sb.append('%').append(toHex2(c));
7912            else
7913               safe(()->sb.append(URLEncoder.encode("" + c, UTF_8.name())));  // Yuck.
7914         }
7915         s = sb.toString();
7916      }
7917      return s;
7918   }
7919
7920   /**
7921    * Similar to {@link URLEncoder#encode(String, String)} but doesn't encode <js>"/"</js> characters.
7922    *
7923    * @param o The object to encode.
7924    * @return The URL encoded string, or <jk>null</jk> if the object was null.
7925    */
7926   public static String urlEncodePath(Object o) {
7927
7928      if (o == null)
7929         return null;
7930
7931      var s = stringify(o);
7932
7933      var needsEncode = false;
7934      for (var i = 0; i < s.length() && ! needsEncode; i++)
7935         needsEncode = URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i));
7936      if (! needsEncode)
7937         return s;
7938
7939      var sb = new StringBuilder();
7940      var caw = new CharArrayWriter();
7941      var caseDiff = ('a' - 'A');
7942
7943      for (var i = 0; i < s.length();) {
7944         var c = s.charAt(i);
7945         if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) {
7946            sb.append(c);
7947            i++;
7948         } else {
7949            if (c == ' ') {
7950               sb.append('+');
7951               i++;
7952            } else {
7953               do {
7954                  caw.write(c);
7955                  if (c >= 0xD800 && c <= 0xDBFF && (i + 1) < s.length()) {
7956                     int d = s.charAt(i + 1);
7957                     if (d >= 0xDC00 && d <= 0xDFFF) {
7958                        caw.write(d);
7959                        i++;
7960                     }
7961                  }
7962                  i++;
7963                  if (i < s.length()) {
7964                     c = s.charAt(i);
7965                  }
7966               } while (i < s.length() && ! URL_ENCODE_PATHINFO_VALIDCHARS.contains(c));
7967
7968               caw.flush();
7969               var s2 = new String(caw.toCharArray());
7970               var ba = s2.getBytes(UTF8);
7971               for (var element : ba) {
7972                  sb.append('%');
7973                  var ch = forDigit((element >> 4) & 0xF, 16);
7974                  if (isLetter(ch)) {
7975                     ch -= caseDiff;
7976                  }
7977                  sb.append(ch);
7978                  ch = forDigit(element & 0xF, 16);
7979                  if (isLetter(ch)) {
7980                     ch -= caseDiff;
7981                  }
7982                  sb.append(ch);
7983               }
7984               caw.reset();
7985            }
7986         }
7987      }
7988      return sb.toString();
7989   }
7990
7991   /**
7992    * Counts the number of words in a string.
7993    *
7994    * <p>
7995    * A word is defined as a sequence of one or more word characters (letters, digits, underscores)
7996    * separated by non-word characters.
7997    *
7998    * <h5 class='section'>Example:</h5>
7999    * <p class='bjava'>
8000    *    wordCount(<js>"Hello world"</js>);              <jc>// 2</jc>
8001    *    wordCount(<js>"The quick brown fox"</js>);      <jc>// 4</jc>
8002    *    wordCount(<js>"Hello, world! How are you?"</js>); <jc>// 5</jc>
8003    * </p>
8004    *
8005    * @param str The string to count words in. Can be <jk>null</jk>.
8006    * @return The number of words, or <c>0</c> if the string is <jk>null</jk> or empty.
8007    */
8008   public static int wordCount(String str) {
8009      if (isEmpty(str))
8010         return 0;
8011
8012      var count = 0;
8013      var inWord = false;
8014
8015      for (var i = 0; i < str.length(); i++) {
8016         var c = str.charAt(i);
8017         if ((LETTER.contains(c) || DIGIT.contains(c)) || c == '_') {
8018            if (! inWord) {
8019               count++;
8020               inWord = true;
8021            }
8022         } else {
8023            inWord = false;
8024         }
8025      }
8026
8027      return count;
8028   }
8029
8030   /**
8031    * Wraps text to a specified line length.
8032    *
8033    * <p>
8034    * Wraps text by breaking at word boundaries (spaces). Words longer than the wrap length
8035    * will be broken at the wrap length. Existing newlines are preserved.
8036    *
8037    * <h5 class='section'>Example:</h5>
8038    * <p class='bjava'>
8039    *    wrap(<js>"hello world test"</js>, 10);  <jc>// "hello world\ntest"</jc>
8040    *    wrap(<jk>null</jk>, 10);                <jc>// null</jc>
8041    * </p>
8042    *
8043    * @param str The string to wrap.
8044    * @param wrapLength The maximum line length (must be &gt; 0).
8045    * @return The wrapped string, or <jk>null</jk> if input is <jk>null</jk>.
8046    * @throws IllegalArgumentException if wrapLength is &lt;= 0.
8047    */
8048   public static String wrap(String str, int wrapLength) {
8049      return wrap(str, wrapLength, "\n");
8050   }
8051
8052   /**
8053    * Wraps text to a specified line length with a custom newline string.
8054    *
8055    * <p>
8056    * Wraps text by breaking at word boundaries (spaces). Words longer than the wrap length
8057    * will be broken at the wrap length. Existing newlines are preserved.
8058    *
8059    * <h5 class='section'>Example:</h5>
8060    * <p class='bjava'>
8061    *    wrap(<js>"hello world test"</js>, 10, <js>"<br>"</js>);  <jc>// "hello world<br>test"</jc>
8062    *    wrap(<jk>null</jk>, 10, <js>"\n"</js>);                  <jc>// null</jc>
8063    * </p>
8064    *
8065    * @param str The string to wrap.
8066    * @param wrapLength The maximum line length (must be &gt; 0).
8067    * @param newline The string to use as line separator.
8068    * @return The wrapped string, or <jk>null</jk> if input is <jk>null</jk>.
8069    * @throws IllegalArgumentException if wrapLength is &lt;= 0 or newline is <jk>null</jk>.
8070    */
8071   public static String wrap(String str, int wrapLength, String newline) {
8072      if (str == null)
8073         return null;
8074      if (isEmpty(str))
8075         return str;
8076      if (wrapLength <= 0)
8077         throw iaex("wrapLength must be > 0: %s", wrapLength);
8078      if (newline == null)
8079         throw iaex("newline cannot be null");
8080
8081      var result = new StringBuilder();
8082      var lines = str.split("\r?\n", -1);  // Preserve empty lines
8083
8084      for (var lineIdx = 0; lineIdx < lines.length; lineIdx++) {
8085         var line = lines[lineIdx];
8086         if (line.isEmpty()) {
8087            if (lineIdx < lines.length - 1)
8088               result.append(newline);
8089            continue;
8090         }
8091
8092         // Split into words first, then combine words that fit
8093         var words = line.split(" +");  // Split on one or more spaces
8094         var currentLine = new StringBuilder();
8095
8096         for (var word : words) {
8097            if (word.isEmpty())
8098               continue;
8099
8100            var wordLength = word.length();
8101            var currentLength = currentLine.length();
8102
8103            if (currentLength == 0) {
8104               // First word on line
8105               // Only break single words if there are multiple words in the input
8106               // (single long words should not be broken for readability)
8107               if (wordLength > wrapLength && words.length > 1) {
8108                  // Word is too long and there are other words, break it
8109                  if (!result.isEmpty())
8110                     result.append(newline);
8111                  var wordPos = 0;
8112                  while (wordPos < wordLength) {
8113                     if (wordPos > 0)
8114                        result.append(newline);
8115                     var remaining = wordLength - wordPos;
8116                     if (remaining <= wrapLength) {
8117                        result.append(word.substring(wordPos));
8118                        break;
8119                     }
8120                     result.append(word.substring(wordPos, wordPos + wrapLength));
8121                     wordPos += wrapLength;
8122                  }
8123               } else {
8124                  currentLine.append(word);
8125               }
8126            } else {
8127               // Check if we can add this word to current line
8128               var neededLength = currentLength + 1 + wordLength;  // current + space + word
8129               // Break if it would fit exactly or exceed - prefer breaking for readability
8130               if (neededLength < wrapLength) {
8131                  // Fits with room to spare
8132                  currentLine.append(' ').append(word);
8133               } else {
8134                  // Doesn't fit or fits exactly - start new line
8135                  if (!result.isEmpty())
8136                     result.append(newline);
8137                  result.append(currentLine);
8138                  currentLine.setLength(0);
8139                  if (wordLength > wrapLength && words.length > 1) {
8140                     // Word is too long and there are other words, break it
8141                     result.append(newline);
8142                     var wordPos = 0;
8143                     while (wordPos < wordLength) {
8144                        if (wordPos > 0)
8145                           result.append(newline);
8146                        var remaining = wordLength - wordPos;
8147                        if (remaining <= wrapLength) {
8148                           result.append(word.substring(wordPos));
8149                           break;
8150                        }
8151                        result.append(word.substring(wordPos, wordPos + wrapLength));
8152                        wordPos += wrapLength;
8153                     }
8154                  } else {
8155                     currentLine.append(word);
8156                  }
8157               }
8158            }
8159         }
8160
8161         // Append any remaining line
8162         if (!currentLine.isEmpty()) {
8163            if (!result.isEmpty())
8164               result.append(newline);
8165            result.append(currentLine);
8166         }
8167      }
8168
8169      return result.toString();
8170   }
8171
8172   /**
8173    * Helper method to estimate the number of syllables in a word.
8174    */
8175   private static int estimateSyllables(String word) {
8176
8177      var lower = word.toLowerCase(Locale.ROOT);
8178      var count = 0;
8179      var prevWasVowel = false;
8180
8181      for (var i = 0; i < lower.length(); i++) {
8182         var c = lower.charAt(i);
8183         var isVowel = (VOWEL.contains(c) || c == 'y');
8184
8185         if (isVowel && ! prevWasVowel) {
8186            count++;
8187         }
8188         prevWasVowel = isVowel;
8189      }
8190
8191      // Handle silent 'e' at the end
8192      if (lower.endsWith("e") && count > 1) {
8193         count--;
8194      }
8195
8196      // At least one syllable
8197      return Math.max(1, count);
8198   }
8199
8200   /**
8201    * Finds the first non-whitespace, non-comment character in a string.
8202    *
8203    * @param s The string to analyze.
8204    * @return The first real character, or <c>-1</c> if none found.
8205    */
8206   private static int firstRealCharacter(String s) {
8207      return safe(()-> {
8208         var r = new StringReader(s);
8209         var c = 0;
8210         while ((c = r.read()) != -1) {
8211            if (! isWhitespace(c)) {
8212               if (c == '/') {
8213                  skipComments(r);
8214               } else {
8215                  return c;
8216               }
8217            }
8218         }
8219         return -1;
8220      });
8221   }
8222
8223   /**
8224    * Helper method to get Soundex code for a character.
8225    */
8226   private static char getSoundexCode(char c) {
8227      if (c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' || c == 'H' || c == 'W' || c == 'Y')
8228         return '0';
8229      if (c == 'B' || c == 'F' || c == 'P' || c == 'V')
8230         return '1';
8231      if (c == 'C' || c == 'G' || c == 'J' || c == 'K' || c == 'Q' || c == 'S' || c == 'X' || c == 'Z')
8232         return '2';
8233      if (c == 'D' || c == 'T')
8234         return '3';
8235      if (c == 'L')
8236         return '4';
8237      if (c == 'M' || c == 'N')
8238         return '5';
8239      if (c == 'R')
8240         return '6';
8241      return '0'; // Non-letter characters
8242   }
8243
8244   /**
8245    * Validates if a string is a valid IPv6 address format (without network operations).
8246    *
8247    * <p>
8248    * This method performs pure string-based validation and does not perform any DNS lookups
8249    * or network operations, making it fast and suitable for validation purposes.
8250    *
8251    * @param ip The IPv6 address string to validate.
8252    * @return <jk>true</jk> if the string is a valid IPv6 address format, <jk>false</jk> otherwise.
8253    */
8254   @SuppressWarnings({
8255      "java:S1313" // IPv6 validation intentionally compares against canonical literals like :: and ::ffff
8256   })
8257   public static boolean isValidIPv6Address(String ip) {
8258      if (ip == null || ip.isEmpty())
8259         return false;
8260
8261      // IPv6 addresses can be:
8262      // 1. Full format: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (8 groups of 4 hex digits)
8263      // 2. Compressed format: 2001:db8::1 (uses :: to represent consecutive zeros)
8264      // 3. IPv4-mapped: ::ffff:192.168.1.1 (last 32 bits as IPv4)
8265      // 4. Loopback: ::1
8266      // 5. Unspecified: ::
8267
8268      // Cannot start or end with a single colon (except ::)
8269      if (ip.startsWith(":") && !ip.startsWith("::"))
8270         return false;
8271      if (ip.endsWith(":") && !ip.endsWith("::"))
8272         return false;
8273
8274      // Check for IPv4-mapped format (contains both : and .)
8275      if (ip.contains(".")) {
8276         // Must be in format ::ffff:x.x.x.x or similar
8277         var lastColon = ip.lastIndexOf(":");
8278         if (lastColon < 0)
8279            return false;
8280         var ipv4Part = ip.substring(lastColon + 1);
8281         // Validate IPv4 part
8282         var ipv4Parts = ipv4Part.split("\\.");
8283         if (ipv4Parts.length != 4)
8284            return false;
8285         for (var part : ipv4Parts) {
8286            try {
8287               var num = Integer.parseInt(part);
8288               if (num < 0 || num > 255)
8289                  return false;
8290            } catch (@SuppressWarnings("unused") NumberFormatException e) {
8291               return false;
8292            }
8293         }
8294         // Validate IPv6 part before the IPv4
8295         var ipv6Part = ip.substring(0, lastColon);
8296         // Accept empty, ::, ::ffff, ::FFFF, or : (when string starts with ::)
8297         if (ipv6Part.isEmpty() || ipv6Part.equals("::") || ipv6Part.equals("::ffff") || ipv6Part.equals("::FFFF") || (ipv6Part.equals(":") && ip.startsWith("::")))
8298            return true;
8299         // More complex validation would be needed for other IPv4-mapped formats
8300         // For now, accept common formats
8301      }
8302
8303      // Check for :: (compression) - only one allowed
8304      var doubleColonCount = 0;
8305      for (var i = 1; i < ip.length(); i++) {
8306         if (ip.charAt(i) == ':' && ip.charAt(i - 1) == ':') {
8307            doubleColonCount++;
8308            if (doubleColonCount > 1)
8309               return false; // Only one :: allowed
8310         }
8311      }
8312
8313      // Split by ::
8314      var parts = ip.split("::", -1);
8315
8316      if (parts.length == 2) {
8317         // Compressed format
8318         var leftParts = parts[0].isEmpty() ? new String[0] : parts[0].split(":");
8319         var rightParts = parts[1].isEmpty() ? new String[0] : parts[1].split(":");
8320         var totalParts = leftParts.length + rightParts.length;
8321         if (totalParts > 7)
8322            return false; // Too many groups (max 8, but :: counts as one or more)
8323      } else {
8324         // Full format (no compression)
8325         var groups = ip.split(":");
8326         if (groups.length != 8)
8327            return false;
8328      }
8329
8330      // Validate each hex group
8331      var groups = ip.split("::");
8332      for (var groupSection : groups) {
8333         if (groupSection.isEmpty())
8334            continue; // Skip empty section from ::
8335         var groupParts = groupSection.split(":");
8336         for (var group : groupParts) {
8337            if (group.length() > 4)
8338               return false; // Each group is max 4 hex digits
8339            // Validate hex digits
8340            for (var i = 0; i < group.length(); i++) {
8341               var c = group.charAt(i);
8342               if (! HEXADECIMAL_CHARS.contains(c))
8343                  return false;
8344            }
8345         }
8346      }
8347
8348      return true;
8349   }
8350
8351
8352   private static class Readifier {
8353      private final Class<?> type;
8354      private final Function<Object,String> bridge;
8355
8356      private <T> Readifier(Class<T> type, Function<? super T,String> converter) {
8357         this.type = type;
8358         this.bridge = o -> converter.apply(type.cast(o));
8359      }
8360
8361      Class<?> forClass() {
8362         return type;
8363      }
8364
8365      Function<Object,String> toFunction() {
8366         return bridge;
8367      }
8368   }
8369
8370   private static <T> Readifier readifier(Class<T> type, Function<? super T,String> converter) {
8371      return new Readifier(type, converter);
8372   }
8373
8374   private static List<Readifier> loadReadifiers() {
8375      var list = new ArrayList<Readifier>();
8376
8377      // More specific types first - order matters!
8378      list.add(readifier(Map.Entry.class, x -> readable(x.getKey()) + '=' + readable(x.getValue())));
8379      list.add(readifier(Collection.class, x -> ((Collection<?>)x).stream().map(StringUtils::readable).collect(joining(",", "[", "]"))));
8380      list.add(readifier(Map.class, x -> ((Map<?,?>)x).entrySet().stream().map(StringUtils::readable).collect(joining(",", "{", "}"))));
8381      list.add(readifier(Iterable.class, x -> readable(toList(x))));
8382      list.add(readifier(Iterator.class, x -> readable(toList(x))));
8383      list.add(readifier(Enumeration.class, x -> readable(toList(x))));
8384      list.add(readifier(Optional.class, x -> readable(((Optional<?>)x).orElse(null))));
8385      list.add(readifier(GregorianCalendar.class, x -> x.toZonedDateTime().format(DateTimeFormatter.ISO_INSTANT)));
8386      list.add(readifier(Date.class, x -> x.toInstant().toString()));
8387      list.add(readifier(InputStream.class, x -> toHex(x)));
8388      list.add(readifier(Reader.class, (Reader x) -> safe(() -> read(x))));
8389      list.add(readifier(File.class, (File x) -> safe(() -> read(x))));
8390      list.add(readifier(byte[].class, x -> toHex(x)));
8391      list.add(readifier(Enum.class, x -> ((Enum<?>)x).name()));
8392      list.add(readifier(Class.class, ClassUtils::classNameSimple));
8393      list.add(readifier(Constructor.class, x -> ConstructorInfo.of(x).getNameFull()));
8394      list.add(readifier(Method.class, x -> MethodInfo.of(x).getNameFull()));
8395      list.add(readifier(Field.class, x -> FieldInfo.of(x).toString()));
8396      list.add(readifier(Parameter.class, x -> ParameterInfo.of(x).toString()));
8397      list.add(readifier(ClassInfo.class, ClassInfo::toString));
8398      list.add(readifier(MethodInfo.class, MethodInfo::toString));
8399      list.add(readifier(ConstructorInfo.class, ConstructorInfo::toString));
8400      list.add(readifier(FieldInfo.class, FieldInfo::toString));
8401      list.add(readifier(ParameterInfo.class, ParameterInfo::toString));
8402
8403      return Collections.unmodifiableList(list);
8404   }
8405
8406   /**
8407    * Determines the multiplier value based on the suffix character in a string.
8408    *
8409    * @param s The string to analyze for multiplier suffix.
8410    * @return The multiplier value (1 if no valid suffix found).
8411    */
8412   private static int multiplierInt(String s) {
8413      var c = isEmpty(s) ? 'z' : s.charAt(s.length() - 1);
8414      if (c == 'G')
8415         return 1024 * 1024 * 1024;
8416      if (c == 'M')
8417         return 1024 * 1024;
8418      if (c == 'K')
8419         return 1024;
8420      if (c == 'g')
8421         return 1000 * 1000 * 1000;
8422      if (c == 'm')
8423         return 1000 * 1000;
8424      if (c == 'k')
8425         return 1000;
8426      return 1;
8427   }
8428
8429   /**
8430    * Determines the long multiplier value based on the suffix character in a string.
8431    *
8432    * @param s The string to analyze for multiplier suffix.
8433    * @return The multiplier value (1 if no valid suffix found).
8434    */
8435   private static long multiplierLong(String s) {
8436      if (isEmpty(s))
8437         return 1;
8438      var c = s.charAt(s.length() - 1);
8439      if (c == 'P')
8440         return 1125899906842624L;  // 1024^5
8441      if (c == 'T')
8442         return 1099511627776L;  // 1024^4
8443      if (c == 'G')
8444         return 1073741824L;  // 1024^3
8445      if (c == 'M')
8446         return 1048576L;  // 1024^2
8447      if (c == 'K')
8448         return 1024L;
8449      if (c == 'p')
8450         return 1000000000000000L;  // 1000^5
8451      if (c == 't')
8452         return 1000000000000L;  // 1000^4
8453      if (c == 'g')
8454         return 1000000000L;  // 1000^3
8455      if (c == 'm')
8456         return 1000000L;  // 1000^2
8457      if (c == 'k')
8458         return 1000L;
8459      return 1;
8460   }
8461
8462   /**
8463    * Parses a unit string and converts the value to milliseconds.
8464    *
8465    * @param unit The unit string (case-insensitive, already lowercased).
8466    * @param value The numeric value.
8467    * @return The value in milliseconds, or <c>-1</c> if the unit is invalid.
8468    */
8469   private static long parseUnit(String unit, double value) {
8470      if (isEmpty(unit)) {
8471         // No unit means milliseconds
8472         return (long)value;
8473      }
8474
8475      // Check milliseconds first (before minutes) - must check exact "ms" before checking "m"
8476      if (unit.equals("ms") || unit.equals("millis") || unit.equals("milliseconds"))
8477         return (long)value;
8478
8479      // Seconds
8480      if (unit.startsWith("s") && !unit.startsWith("sec"))
8481         return (long)(value * 1000);
8482      if (unit.startsWith("sec") || unit.startsWith("second"))
8483         return (long)(value * 1000);
8484
8485      // Minutes (must check after milliseconds and months)
8486      if (unit.startsWith("m") && !unit.startsWith("mo") && !unit.startsWith("mill") && !unit.startsWith("ms"))
8487         return (long)(value * 1000 * 60);
8488      if (unit.startsWith("min") || unit.startsWith("minute"))
8489         return (long)(value * 1000 * 60);
8490
8491      // Hours
8492      if (unit.startsWith("h") || unit.startsWith("hour"))
8493         return (long)(value * 1000 * 60 * 60);
8494
8495      // Days
8496      if (unit.startsWith("d") && !unit.startsWith("da"))
8497         return (long)(value * 1000 * 60 * 60 * 24);
8498      if (unit.startsWith("day"))
8499         return (long)(value * 1000 * 60 * 60 * 24);
8500
8501      // Weeks
8502      if (unit.startsWith("w") || unit.startsWith("week"))
8503         return (long)(value * 1000 * 60 * 60 * 24 * 7);
8504
8505      // Months (30 days)
8506      if (unit.startsWith("mo") || unit.startsWith("month"))
8507         return (long)(value * 1000 * 60 * 60 * 24 * 30);
8508
8509      // Years (365 days)
8510      if (unit.startsWith("y") && !unit.startsWith("yr"))
8511         return (long)(value * 1000 * 60 * 60 * 24 * 365);
8512      if (unit.startsWith("yr") || unit.startsWith("year"))
8513         return (long)(value * 1000 * 60 * 60 * 24 * 365);
8514
8515      // Unknown unit
8516      return -1;
8517   }
8518
8519   /**
8520    * Skips over a single comment sequence in a StringReader.
8521    *
8522    * <p>
8523    * The reader must be positioned at the first <js>'/'</js> character of a comment.
8524    * This method will skip only the comment it's currently positioned on, not all comments in the reader.
8525    *
8526    * <p>
8527    * Supports both <js>"/* * /"</js> style block comments and <js>"//"</js> style line comments.
8528    *
8529    * @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}).
8530    * @throws IOException If an I/O error occurs.
8531    */
8532   public static void skipComments(StringReader r) throws IOException {
8533      var c = r.read();
8534      //  "/* */" style comments
8535      if (c == '*') {
8536         c = r.read();
8537         while (c != -1) {
8538            if (c == '*') {
8539               c = r.read();
8540               if (c == '/')
8541                  return;
8542               // If not '/', continue checking from this character
8543               // Don't read again, just continue the loop
8544            } else {
8545               c = r.read();
8546            }
8547         }
8548         //  "//" style comments
8549      } else if (c == '/') {
8550         while ((c = r.read()) != -1) {
8551            if (c == '\n')
8552               return;
8553         }
8554      }
8555   }
8556
8557   /**
8558    * Gets or creates an AsciiSet for escaping the specified character.
8559    *
8560    * @param c The character to create an escape set for.
8561    * @return An AsciiSet containing the character and backslash.
8562    */
8563   private static AsciiSet getEscapeSet(char c) {
8564      return ESCAPE_SETS.computeIfAbsent(c, key -> AsciiSet.create().chars(key, '\\').build());
8565   }
8566
8567   /**
8568    * Helper method to split a string into words.
8569    * Detects word boundaries from separators (spaces, underscores, hyphens) and case changes.
8570    *
8571    * @param str The string to split.
8572    * @return A list of words, or empty list if input is null or empty.
8573    */
8574   private static List<String> splitWords(String str) {
8575      if (str == null || isEmpty(str))
8576         return Collections.emptyList();
8577
8578      var words = new ArrayList<String>();
8579      var sb = new StringBuilder();
8580      var wasLowerCase = false;
8581      var wasUpperCase = false;
8582      var consecutiveUpperCount = 0;
8583
8584      for (var i = 0; i < str.length(); i++) {
8585         var c = str.charAt(i);
8586         var isSeparator = (c == ' ' || c == '_' || c == '-' || c == '\t');
8587         var isUpperCase = LETTER_UC.contains(c);
8588         var isLowerCase = LETTER_LC.contains(c);
8589         var isLetter = LETTER.contains(c);
8590
8591         if (isSeparator) {
8592            if (!sb.isEmpty()) {
8593               words.add(sb.toString());
8594               sb.setLength(0);
8595            }
8596            wasLowerCase = false;
8597            wasUpperCase = false;
8598            consecutiveUpperCount = 0;
8599         } else if (isLetter) {
8600            // Detect word boundary:
8601            // 1. Uppercase after lowercase (e.g., "helloWorld" → "hello", "World")
8602            // 2. Uppercase after consecutive uppercase when next is lowercase (e.g., "XMLHttp" → "XML", "Http")
8603            // 3. Lowercase after 2+ consecutive uppercase (e.g., "XMLHttp" → "XML", "Http")
8604            if (!sb.isEmpty()) {
8605               if (isUpperCase && wasLowerCase) {
8606                  // Case 1: uppercase after lowercase (e.g., "helloWorld" → "hello", "World")
8607                  words.add(sb.toString());
8608                  sb.setLength(0);
8609                  consecutiveUpperCount = 0;
8610               } else if (isUpperCase && wasUpperCase && consecutiveUpperCount >= 2) {
8611                  // Case 2: uppercase after uppercase - check if this starts a new word
8612                  // Look ahead to see if next character is lowercase
8613                  // This handles "XMLHttp" where 'H' starts "Http"
8614                  // We need at least 2 consecutive uppercase letters before this one to split
8615                  if (i + 1 < str.length()) {
8616                     var nextChar = str.charAt(i + 1);
8617                     if (LETTER_LC.contains(nextChar)) {
8618                        // This uppercase starts a new word, split before it
8619                        words.add(sb.toString());
8620                        sb.setLength(0);
8621                        consecutiveUpperCount = 0;
8622                     }
8623                  }
8624               } else if (isLowerCase && wasUpperCase && consecutiveUpperCount >= 2) {
8625                  // Case 3: lowercase after 2+ consecutive uppercase
8626                  // Split all but the last uppercase (e.g., "XMLH" → "XML" + "H")
8627                  var splitPoint = sb.length() - 1;
8628                  words.add(sb.substring(0, splitPoint));
8629                  sb.delete(0, splitPoint);
8630                  consecutiveUpperCount = 0;
8631               }
8632            }
8633            sb.append(c);
8634            // Update state AFTER appending
8635            wasLowerCase = isLowerCase;
8636            wasUpperCase = isUpperCase;
8637            if (isUpperCase) {
8638               consecutiveUpperCount++;
8639            } else {
8640               consecutiveUpperCount = 0;
8641            }
8642         } else {
8643            // Non-letter characters (digits, etc.) - treat as part of current word
8644            sb.append(c);
8645            wasLowerCase = false;
8646            wasUpperCase = false;
8647         consecutiveUpperCount = 0;
8648      }
8649   }
8650
8651   if (!sb.isEmpty())
8652      words.add(sb.toString());
8653
8654      return words;
8655   }
8656
8657   /**
8658    * Null-safe startsWith (both args null-checked).
8659    *
8660    * @param s The string to test.
8661    * @param prefix The prefix to test for.
8662    * @return <jk>true</jk> if s starts with prefix (both non-null).
8663    */
8664   public static boolean startsWith(String s, String prefix) {
8665      return s != null && prefix != null && s.startsWith(prefix);
8666   }
8667
8668   /**
8669    * Returns the string, or blank ("") if null.
8670    *
8671    * @param value The string value.
8672    * @return The value, or "" if null.
8673    */
8674   public static String blankIfNull(String value) { return coalesce(value, ""); }
8675
8676   /**
8677    * Creates a new {@link StringBuilder} initialized with the stringified form of the specified value.
8678    *
8679    * <p>
8680    * The value is rendered via {@link ObjectUtils#stringify(Object)} (a <jk>null</jk> value renders as
8681    * the literal string {@code "null"}). Using the stringified form avoids the
8682    * {@link StringBuilder#StringBuilder(int) StringBuilder(int capacity)} footgun that would otherwise
8683    * apply when passing a numeric value.
8684    *
8685    * @param value The initial value. Can be <jk>null</jk>.
8686    * @return A new StringBuilder containing the stringified value.
8687    */
8688   public static StringBuilder newStringBuilder(Object value) { return new StringBuilder(value == null ? "null" : stringify(value)); }
8689
8690   /**
8691    * Creates a new {@link StringBuilder} with the stringified form of each specified value appended in order.
8692    *
8693    * <p>
8694    * Each value is rendered via {@link ObjectUtils#stringify(Object)} (a <jk>null</jk> value renders as
8695    * the literal string {@code "null"}). With no arguments, an empty builder is returned.
8696    *
8697    * @param values The values to append in order. Individual values can be <jk>null</jk>.
8698    * @return A new StringBuilder containing the appended values.
8699    */
8700   public static StringBuilder newStringBuilder(Object...values) {
8701      var b = new StringBuilder();
8702      for (var v : values)
8703         b.append(stringify(v));
8704      return b;
8705   }
8706
8707   /**
8708    * Inverse of {@link #equalsIgnoreCase(String, String)}.
8709    *
8710    * @param s1 String 1.
8711    * @param s2 String 2.
8712    * @return <jk>true</jk> if the strings are not equal ignoring case.
8713    */
8714   public static boolean notEqualsIgnoreCase(String s1, String s2) { return ! equalsIgnoreCase(s1, s2); }
8715
8716   /**
8717    * Returns the specified string, or <jk>null</jk> if that string is <jk>null</jk> or empty.
8718    *
8719    * @param value The string value to check.
8720    * @return The string value, or <jk>null</jk> if the string is <jk>null</jk> or empty.
8721    */
8722   public static String nullIfEmpty(String value) {
8723      return isEmpty(value) ? null : value;
8724   }
8725
8726   /**
8727    * Constructor.
8728    */
8729   protected StringUtils() {}
8730}