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> -> { 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> -> { 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 < banana)</jc> 635 * compare(<js>"banana"</js>, <js>"apple"</js>); <jc>// positive (banana > 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 < 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 > 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> — 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 <} → {@code \u005Cu003c} 1632 * <li>{@code U+2028} (LINE SEPARATOR) → {@code \u005Cu2028} 1633 * <li>{@code U+2029} (PARAGRAPH SEPARATOR) → {@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" — 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>&</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 — it would rewrite <c>&</c> 1659 * to <c>&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>"} - parses back to the original "x</script>"</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 — see 1699 * {@link #escapeForScript(String)} for that. 1700 * 1701 * <p> 1702 * Escapes the following characters: 1703 * <ul> 1704 * <li><js>'&'</js> → <js>"&amp;"</js></li> 1705 * <li><js>'<'</js> → <js>"&lt;"</js></li> 1706 * <li><js>'>'</js> → <js>"&gt;"</js></li> 1707 * <li><js>'"'</js> → <js>"&quot;"</js></li> 1708 * <li><js>'\''</js> → <js>"&#39;"</js></li> 1709 * </ul> 1710 * 1711 * <h5 class='section'>Example:</h5> 1712 * <p class='bjava'> 1713 * escapeHtml(<js>"<script>alert('xss')</script>"</js>); 1714 * <jc>// Returns: "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&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("&"); 1728 case '<' -> sb.append("<"); 1729 case '>' -> sb.append(">"); 1730 case '"' -> sb.append("""); 1731 case '\'' -> sb.append("'"); 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>'&'</js> → <js>"&amp;"</js></li> 1790 * <li><js>'<'</js> → <js>"&lt;"</js></li> 1791 * <li><js>'>'</js> → <js>"&gt;"</js></li> 1792 * <li><js>'"'</js> → <js>"&quot;"</js></li> 1793 * <li><js>'\''</js> → <js>"&apos;"</js></li> 1794 * </ul> 1795 * 1796 * <h5 class='section'>Example:</h5> 1797 * <p class='bjava'> 1798 * escapeXml(<js>"<tag attr='value'>text</tag>"</js>); 1799 * <jc>// Returns: "&lt;tag attr=&apos;value&apos;&gt;text&lt;/tag&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("&"); 1813 case '<' -> sb.append("<"); 1814 case '>' -> sb.append(">"); 1815 case '"' -> sb.append("""); 1816 case '\'' -> sb.append("'"); 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>"<tag>content</tag>"</js>, <js>"<"</js>, <js>">"</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 -> s != <jk>null</jk> && s.length() > 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 -> <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 < 10)</jc> 4773 * naturalCompare(<js>"file10.txt"</js>, <js>"file2.txt"</js>); <jc>// positive (10 > 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 * ()-> <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 * ()-> <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 * <1% chance of collision. 5706 * 5707 * <p> 5708 * For example, given 5 characters, the number of generated IDs need to produce a <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>// "!@#$%^&*()"</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>"<script>alert('xss')</script>"</js>); <jc>// "&lt;script&gt;alert('xss')&lt;/script&gt;"</jc> 6321 * sanitize(<js>"Hello <b>World</b>"</js>); <jc>// "Hello &lt;b&gt;World&lt;/b&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",',') -> {"a","b","c"} 6535 * split("a, b ,c ",',') -> {"a","b","c"} 6536 * split("a,,c",',') -> {"a","","c"} 6537 * split(",,",',') -> {"","",""} 6538 * split("",',') -> {} 6539 * split(null,',') -> {} 6540 * split("a,b\,c,d", ',', false) -> {"a","b\,c","d"} 6541 * split("a,b\\,c,d", ',', false) -> {"a","b\","c","d"} 6542 * split("a,b\,c,d", ',', true) -> {"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",',') -> {"a","b","c"} 6663 * split("a, b ,c ",',') -> {"a","b","c"} 6664 * split("a,,c",',') -> {"a","","c"} 6665 * split(",,",',') -> {"","",""} 6666 * split("",',') -> {} 6667 * split(null,',') -> null 6668 * split("a,b\,c,d", ',', false) -> {"a","b\,c","d"} 6669 * split("a,b\\,c,d", ',', false) -> {"a","b\","c","d"} 6670 * split("a,b\,c,d", ',', true) -> {"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> => <c>["foo"]</c> 6953 * <li><js>" foo "</js> => <c>["foo"]</c> 6954 * <li><js>"foo bar baz"</js> => <c>["foo","bar","baz"]</c> 6955 * <li><js>"foo 'bar baz'"</js> => <c>["foo","bar baz"]</c> 6956 * <li><js>"foo \"bar baz\""</js> => <c>["foo","bar baz"]</c> 6957 * <li><js>"foo 'bar\'baz'"</js> => <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>"<"</js>, <js>">"</js>); <jc>// null</jc> 7196 * substringBetween(<js>"<hello>"</js>, <js>"<"</js>, <js>">"</js>); <jc>// "hello"</jc> 7197 * substringBetween(<js>"<hello>"</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;"</js> → <js>'&'</js></li> 7749 * <li><js>"&lt;"</js> → <js>'<'</js></li> 7750 * <li><js>"&gt;"</js> → <js>'>'</js></li> 7751 * <li><js>"&quot;"</js> → <js>'"'</js></li> 7752 * <li><js>"&#39;"</js> or <js>"&apos;"</js> → <js>'\''</js></li> 7753 * </ul> 7754 * 7755 * <h5 class='section'>Example:</h5> 7756 * <p class='bjava'> 7757 * unescapeHtml(<js>"&lt;script&gt;"</js>); <jc>// Returns: "<script>"</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 & last to avoid interfering with other entities 7767 return str.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("'", "'").replace("&", "&"); 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;"</js> → <js>'&'</js></li> 7777 * <li><js>"&lt;"</js> → <js>'<'</js></li> 7778 * <li><js>"&gt;"</js> → <js>'>'</js></li> 7779 * <li><js>"&quot;"</js> → <js>'"'</js></li> 7780 * <li><js>"&apos;"</js> → <js>'\''</js></li> 7781 * </ul> 7782 * 7783 * <h5 class='section'>Example:</h5> 7784 * <p class='bjava'> 7785 * unescapeXml(<js>"&lt;tag&gt;"</js>); <jc>// Returns: "<tag>"</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 & last to avoid interfering with other entities 7795 return str.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("&", "&"); 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 > 0). 8045 * @return The wrapped string, or <jk>null</jk> if input is <jk>null</jk>. 8046 * @throws IllegalArgumentException if wrapLength is <= 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 > 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 <= 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}