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.common.utils;
018
019import static java.lang.Character.*;
020import static java.nio.charset.StandardCharsets.*;
021import static org.apache.juneau.common.utils.IOUtils.*;
022import static org.apache.juneau.common.utils.ThrowableUtils.*;
023import static org.apache.juneau.common.utils.Utils.*;
024
025import java.io.*;
026import java.lang.reflect.*;
027import java.math.*;
028import java.net.*;
029import java.nio.*;
030import java.text.*;
031import java.util.*;
032import java.util.concurrent.*;
033import java.util.concurrent.atomic.*;
034import java.util.function.*;
035import java.util.regex.*;
036import java.util.stream.*;
037import java.util.zip.*;
038
039import jakarta.xml.bind.*;
040
041/**
042 * Reusable string utility methods.
043 */
044public class StringUtils {
045
046   /**
047    * Predicate check to filter out null and empty strings.
048    */
049   public static final Predicate<String> NOT_EMPTY = Utils::isNotEmpty;
050
051   private static final AsciiSet numberChars = AsciiSet.of("-xX.+-#pP0123456789abcdefABCDEF");
052
053   private static final AsciiSet firstNumberChars =AsciiSet.of("+-.#0123456789");
054   private static final AsciiSet octChars = AsciiSet.of("01234567");
055   private static final AsciiSet decChars = AsciiSet.of("0123456789");
056   private static final AsciiSet hexChars = AsciiSet.of("0123456789abcdefABCDEF");
057   // Maps 6-bit nibbles to BASE64 characters.
058   private static final char[] base64m1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
059
060   // Characters that do not need to be URL-encoded
061   private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.!~*'()\\").build();
062
063   // Characters that really do not need to be URL-encoded
064   private static final AsciiSet unencodedCharsLax = unencodedChars.copy()
065      .chars(":@$,")  // reserved, but can't be confused in a query parameter.
066      .chars("{}|\\^[]`")  // unwise characters.
067      .build();
068
069   // Valid HTTP header characters (including quoted strings and comments).
070   private static final AsciiSet httpHeaderChars = AsciiSet
071      .create()
072      .chars("\t -")
073      .ranges("!-[","]-}")
074      .build();
075
076   // Maps BASE64 characters to 6-bit nibbles.
077   private static final byte[] base64m2 = new byte[128];
078
079   static {
080      for (var i = 0; i < 64; i++)
081         base64m2[base64m1[i]] = (byte)i;
082   }
083   private static final Random RANDOM = new Random();
084
085   private static final Pattern fpRegex = Pattern.compile(
086      "[+-]?(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]*"  // NOSONAR
087   );
088
089   static final Map<Character,AsciiSet> ESCAPE_SETS = new ConcurrentHashMap<>();
090
091   static final AsciiSet MAP_ESCAPE_SET = AsciiSet.of(",=\\");
092
093   static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.of("\"'\\");
094
095   private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
096
097   private static final Map<Class<?>,Function<Object,String>> PRIMITIVE_ARRAY_STRINGIFIERS = new HashMap<>();
098
099   static {
100      PRIMITIVE_ARRAY_STRINGIFIERS.put(boolean[].class, x -> Arrays.toString((boolean[])x));
101      PRIMITIVE_ARRAY_STRINGIFIERS.put(byte[].class, x -> Arrays.toString((byte[])x));
102      PRIMITIVE_ARRAY_STRINGIFIERS.put(char[].class, x -> Arrays.toString((char[])x));
103      PRIMITIVE_ARRAY_STRINGIFIERS.put(double[].class, x -> Arrays.toString((double[])x));
104      PRIMITIVE_ARRAY_STRINGIFIERS.put(float[].class, x -> Arrays.toString((float[])x));
105      PRIMITIVE_ARRAY_STRINGIFIERS.put(int[].class, x -> Arrays.toString((int[])x));
106      PRIMITIVE_ARRAY_STRINGIFIERS.put(long[].class, x -> Arrays.toString((long[])x));
107      PRIMITIVE_ARRAY_STRINGIFIERS.put(short[].class, x -> Arrays.toString((short[])x));
108   }
109
110   private static final char[] HEX = "0123456789ABCDEF".toCharArray();
111
112   private static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS =
113      AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.*/()").build();
114   private static final AsciiSet URI_CHARS = AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0','9').range('A','Z').range('a','z').build();
115
116   /**
117    * Abbreviates a String using ellipses.
118    *
119    * @param in The input string.
120    * @param length The max length of the resulting string.
121    * @return The abbreviated string.
122    */
123   public static String abbreviate(String in, int length) {
124      if (in == null || in.length() <= length || in.length() <= 3)
125         return in;
126      return in.substring(0, length-3) + "...";
127   }
128
129   /**
130    * Appends a string to a StringBuilder, creating a new one if null.
131    *
132    * @param sb The StringBuilder to append to, or <jk>null</jk> to create a new one.
133    * @param in The string to append.
134    * @return The StringBuilder with the string appended.
135    */
136   private static StringBuilder append(StringBuilder sb, String in) {
137      if (sb == null)
138         return new StringBuilder(in);
139      sb.append(in);
140      return sb;
141   }
142
143   /**
144    * Converts an array to a List, handling both primitive and object arrays.
145    *
146    * @param array The array to convert.
147    * @return A List containing the array elements.
148    */
149   private static List<Object> arrayAsList(Object array) {
150      if (array.getClass().getComponentType().isPrimitive()) {
151         var l = new ArrayList<>(Array.getLength(array));
152         for (var i = 0; i < Array.getLength(array); i++)
153            l.add(Array.get(array, i));
154         return l;
155      }
156      return Arrays.asList((Object[])array);
157   }
158
159   /**
160    * BASE64-decodes the specified string.
161    *
162    * @param in The BASE-64 encoded string.
163    * @return The decoded byte array, or null if the input was <jk>null</jk>.
164    */
165   public static byte[] base64Decode(String in) {
166      if (in == null)
167         return null;  // NOSONAR - Intentional.
168
169      var bIn = in.getBytes(IOUtils.UTF8);
170
171      Utils.assertArg(bIn.length % 4 == 0, "Invalid BASE64 string length.  Must be multiple of 4.");
172
173      // Strip out any trailing '=' filler characters.
174      var inLength = bIn.length;
175      while (inLength > 0 && bIn[inLength - 1] == '=')
176         inLength--;
177
178      var outLength = (inLength * 3) / 4;
179      var out = new byte[outLength];
180      var iIn = 0;
181      var iOut = 0;
182      while (iIn < inLength) {
183         var i0 = bIn[iIn++];
184         var i1 = bIn[iIn++];
185         var i2 = iIn < inLength ? bIn[iIn++] : 'A';
186         var i3 = iIn < inLength ? bIn[iIn++] : 'A';
187         var b0 = base64m2[i0];
188         var b1 = base64m2[i1];
189         var b2 = base64m2[i2];
190         int b3 = base64m2[i3];
191         var o0 = (b0 << 2) | (b1 >>> 4);
192         var o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);
193         var o2 = ((b2 & 3) << 6) | b3;
194         out[iOut++] = (byte)o0;
195         if (iOut < outLength)
196            out[iOut++] = (byte)o1;
197         if (iOut < outLength)
198            out[iOut++] = (byte)o2;
199      }
200      return out;
201   }
202
203   /**
204    * Shortcut for calling <c>base64Decode(String)</c> and converting the result to a UTF-8 encoded string.
205    *
206    * @param in The BASE-64 encoded string to decode.
207    * @return The decoded string.
208    */
209   public static String base64DecodeToString(String in) {
210      var b = base64Decode(in);
211      if (b == null)
212         return null;
213      return new String(b, IOUtils.UTF8);
214   }
215
216   /**
217    * BASE64-encodes the specified byte array.
218    *
219    * @param in The input byte array to convert.
220    * @return The byte array converted to a BASE-64 encoded string.
221    */
222   public static String base64Encode(byte[] in) {
223      if (in == null)
224         return null;
225      var outLength = (in.length * 4 + 2) / 3;   // Output length without padding
226      var out = new char[((in.length + 2) / 3) * 4];  // Length includes padding.
227      var iIn = 0;
228      var iOut = 0;
229      while (iIn < in.length) {
230         var i0 = in[iIn++] & 0xff;
231         var i1 = iIn < in.length ? in[iIn++] & 0xff : 0;
232         var i2 = iIn < in.length ? in[iIn++] & 0xff : 0;
233         var o0 = i0 >>> 2;
234         var o1 = ((i0 & 3) << 4) | (i1 >>> 4);
235         var o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
236         var o3 = i2 & 0x3F;
237         out[iOut++] = base64m1[o0];
238         out[iOut++] = base64m1[o1];
239         out[iOut] = iOut < outLength ? base64m1[o2] : '=';
240         iOut++;
241         out[iOut] = iOut < outLength ? base64m1[o3] : '=';
242         iOut++;
243      }
244      return new String(out);
245   }
246
247   /**
248    * Shortcut for calling <code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code>
249    *
250    * @param in The input string to convert.
251    * @return The string converted to BASE-64 encoding.
252    */
253   public static String base64EncodeToString(String in) {
254      if (in == null)
255         return null;
256      return base64Encode(in.getBytes(IOUtils.UTF8));
257   }
258
259   /**
260    * Returns the character at the specified index in the string without throwing exceptions.
261    *
262    * @param s The string.
263    * @param i The index position.
264    * @return
265    *    The character at the specified index, or <c>0</c> if the index is out-of-range or the string
266    *    is <jk>null</jk>.
267    */
268   public static char charAt(String s, int i) {
269      if (s == null || i < 0 || i >= s.length())
270         return 0;
271      return s.charAt(i);
272   }
273
274   /**
275    * Compares two strings, but gracefully handles <jk>nulls</jk>.
276    *
277    * @param s1 The first string.
278    * @param s2 The second string.
279    * @return The same as {@link String#compareTo(String)}.
280    */
281   public static int compare(String s1, String s2) {
282      if (s1 == null && s2 == null)
283         return 0;
284      if (s1 == null)
285         return Integer.MIN_VALUE;
286      if (s2 == null)
287         return Integer.MAX_VALUE;
288      return s1.compareTo(s2);
289   }
290
291   /**
292    * Converts string into a GZipped input stream.
293    *
294    * @param contents The contents to compress.
295    * @return The input stream converted to GZip.
296    * @throws Exception Exception occurred.
297    */
298   public static byte[] compress(String contents) throws Exception {
299      var baos = new ByteArrayOutputStream(contents.length()>>1);
300      try (var gos = new GZIPOutputStream(baos)) {
301         gos.write(contents.getBytes());
302         gos.finish();
303         gos.flush();
304      }
305      return baos.toByteArray();
306   }
307
308   /**
309    * Same as {@link String#contains(CharSequence)} except returns <jk>null</jk> if the value is null.
310    *
311    * @param value The string to check.
312    * @param substring The value to check for.
313    * @return <jk>true</jk> if the value contains the specified substring.
314    */
315   public static boolean contains(String value, CharSequence substring) {
316      return value != null && value.contains(substring);
317   }
318
319   /**
320    * Returns <jk>true</jk> if the specified string contains any of the specified characters.
321    *
322    * @param s The string to test.
323    * @param chars The characters to look for.
324    * @return
325    *    <jk>true</jk> if the specified string contains any of the specified characters.
326    *    <br><jk>false</jk> if the string is <jk>null</jk>.
327    */
328   public static boolean containsAny(String s, char...chars) {
329      if (s == null)
330         return false;
331      for (int i = 0, j = s.length(); i < j; i++) {
332         var c = s.charAt(i);
333         for (var c2 : chars)
334            if (c == c2)
335               return true;
336      }
337      return false;
338   }
339
340   /**
341    * Converts an object to a readable string representation for formatting.
342    *
343    * @param o The object to convert.
344    * @return A readable string representation of the object.
345    */
346   private static String convertToReadable(Object o) {
347      if (o == null)
348         return null;
349      if (o instanceof Class)
350         return ((Class<?>)o).getName();
351      if (o instanceof Method)
352         return Method.class.cast(o).getName();
353      if (isArray(o))
354         return arrayAsList(o).stream().map(StringUtils::convertToReadable).collect(Collectors.joining(", ", "[", "]"));
355      return o.toString();
356   }
357
358   /**
359    * Counts the number of the specified character in the specified string.
360    *
361    * @param s The string to check.
362    * @param c The character to check for.
363    * @return The number of those characters or zero if the string was <jk>null</jk>.
364    */
365   public static int countChars(String s, char c) {
366      var count = 0;
367      if (s == null)
368         return count;
369      for (var i = 0; i < s.length(); i++)
370         if (s.charAt(i) == c)
371            count++;
372      return count;
373   }
374
375   /**
376    * Debug method for rendering non-ASCII character sequences.
377    *
378    * @param s The string to decode.
379    * @return A string with non-ASCII characters converted to <js>"[hex]"</js> sequences.
380    */
381   public static String decodeHex(String s) {
382      if (s == null)
383         return null;
384      var sb = new StringBuilder();
385      for (var c : s.toCharArray()) {
386         if (c < ' ' || c > '~')
387            sb.append("[").append(Integer.toHexString(c)).append("]");
388         else
389            sb.append(c);
390      }
391      return sb.toString();
392   }
393
394   /**
395    * Converts a GZipped input stream into a string.
396    *
397    * @param is The contents to decompress.
398    * @return The string.
399    * @throws Exception Exception occurred.
400    */
401   public static String decompress(byte[] is) throws Exception {
402      return read(new GZIPInputStream(new ByteArrayInputStream(is)));
403   }
404
405   /**
406    * Finds the position where the two strings differ.
407    *
408    * @param s1 The first string.
409    * @param s2 The second string.
410    * @return The position where the two strings differ, or <c>-1</c> if they're equal.
411    */
412   public static int diffPosition(String s1, String s2) {
413      s1 = emptyIfNull(s1);
414      s2 = emptyIfNull(s2);
415      var i = 0;
416      var len = Math.min(s1.length(), s2.length());
417      while (i < len) {
418         var j = s1.charAt(i) - s2.charAt(i);
419         if (j != 0)
420            return i;
421         i++;
422      }
423      if (i == len && s1.length() == s2.length())
424         return -1;
425      return i;
426   }
427
428   /**
429    * Finds the position where the two strings differ ignoring case.
430    *
431    * @param s1 The first string.
432    * @param s2 The second string.
433    * @return The position where the two strings differ, or <c>-1</c> if they're equal.
434    */
435   public static int diffPositionIc(String s1, String s2) {
436      s1 = emptyIfNull(s1);
437      s2 = emptyIfNull(s2);
438      var i = 0;
439      var len = Math.min(s1.length(), s2.length());
440      while (i < len) {
441         var j = toLowerCase(s1.charAt(i)) - toLowerCase(s2.charAt(i));
442         if (j != 0)
443            return i;
444         i++;
445      }
446      if (i == len && s1.length() == s2.length())
447         return -1;
448      return i;
449   }
450
451   /**
452    * An efficient method for checking if a string ends with a character.
453    *
454    * @param s The string to check.  Can be <jk>null</jk>.
455    * @param c The character to check for.
456    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character.
457    */
458   public static boolean endsWith(String s, char c) {
459      if (s != null) {
460         var i = s.length();
461         if (i > 0)
462            return s.charAt(i-1) == c;
463      }
464      return false;
465   }
466
467   /**
468    * Same as {@link #endsWith(String, char)} except check for multiple characters.
469    *
470    * @param s The string to check.  Can be <jk>null</jk>.
471    * @param c The characters to check for.
472    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character.
473    */
474   public static boolean endsWith(String s, char...c) {
475      if (s != null) {
476         var i = s.length();
477         if (i > 0) {
478            var c2 = s.charAt(i-1);
479            for (var cc : c)
480               if (c2 == cc)
481                  return true;
482         }
483      }
484      return false;
485   }
486
487   /**
488    * Escapes the specified characters in the string.
489    *
490    * @param s The string with characters to escape.
491    * @param escaped The characters to escape.
492    * @return The string with characters escaped, or the same string if no escapable characters were found.
493    */
494   public static String escapeChars(String s, AsciiSet escaped) {
495      if (s == null || s.isEmpty())
496         return s;
497
498      var count = 0;
499      for (var i = 0; i < s.length(); i++)
500         if (escaped.contains(s.charAt(i)))
501            count++;
502      if (count == 0)
503         return s;
504
505      var sb = new StringBuffer(s.length() + count);
506      for (var i = 0; i < s.length(); i++) {
507         var c = s.charAt(i);
508         if (escaped.contains(c))
509            sb.append('\\');
510         sb.append(c);
511      }
512      return sb.toString();
513   }
514
515   /**
516    * Returns the first character in the specified string.
517    *
518    * @param s The string to check.
519    * @return The first character in the string, or <c>0</c> if the string is <jk>null</jk> or empty.
520    */
521   public static char firstChar(String s) {
522      if (s == null || s.isEmpty())
523         return 0;
524      return s.charAt(0);
525   }
526
527   /**
528    * Returns the first non-null, non-empty string in the list.
529    *
530    * @param s The strings to test.
531    * @return The first non-empty string in the list, or <jk>null</jk> if they were all <jk>null</jk> or empty.
532    */
533   public static String firstNonEmpty(String...s) {
534      for (var ss : s)
535         if (Utils.isNotEmpty(ss))
536            return ss;
537      return null;
538   }
539
540   /**
541    * Returns the first non-whitespace character in the string.
542    *
543    * @param s The string to check.
544    * @return
545    *    The first non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed
546    *    of only whitespace.
547    */
548   public static char firstNonWhitespaceChar(String s) {
549      if (s != null)
550         for (var i = 0; i < s.length(); i++)
551            if (! isWhitespace(s.charAt(i)))
552               return s.charAt(i);
553      return 0;
554   }
555
556   /**
557    * Finds the first non-whitespace, non-comment character in a string.
558    *
559    * @param s The string to analyze.
560    * @return The first real character, or <c>-1</c> if none found.
561    */
562   private static int firstRealCharacter(String s) {
563      try (var r = new StringReader(s)) {
564         var c = 0;
565         while ((c = r.read()) != -1) {
566            if (! isWhitespace(c)) {
567               if (c == '/') {
568                  skipComments(r);
569               } else {
570                  return c;
571               }
572            }
573         }
574         return -1;
575      } catch (Exception e) {
576         throw asRuntimeException(e);
577      }
578   }
579
580   /**
581    * Attempts to escape any invalid characters found in a URI.
582    *
583    * @param in The URI to fix.
584    * @return The fixed URI.
585    */
586   public static String fixUrl(String in) {
587
588      if (in == null)
589         return null;
590
591      StringBuilder sb = null;
592
593      var m = 0;
594
595      for (var i = 0; i < in.length(); i++) {
596         var c = in.charAt(i);
597         if (c <= 127 && ! URI_CHARS.contains(c)) {
598            sb = append(sb, in.substring(m, i));
599            if (c == ' ')
600               sb.append("+");
601            else
602               sb.append('%').append(toHex2(c));
603            m = i+1;
604         }
605      }
606      if (sb != null) {
607         sb.append(in.substring(m));
608         return sb.toString();
609      }
610      return in;
611
612   }
613
614   /**
615    * Similar to {@link MessageFormat#format(String, Object...)} except allows you to specify POJO arguments.
616    *
617    * @param pattern The string pattern.
618    * @param args The arguments.
619    * @return The formatted string.
620    */
621   public static String format(String pattern, Object...args) {
622      if (args == null || args.length == 0)
623         return pattern;
624      var args2 = new Object[args.length];
625      for (var i = 0; i < args.length; i++)
626         args2[i] = convertToReadable(args[i]);
627
628      var c = countChars(pattern, '\'');
629      if (c % 2 != 0)
630         throw new AssertionError("Dangling single quote found in pattern: " + pattern);
631
632      return MessageFormat.format(pattern, args2);
633   }
634
635   /**
636    * Converts a hexadecimal character string to a byte array.
637    *
638    * @param hex The string to convert to a byte array.
639    * @return A new byte array.
640    */
641   public static byte[] fromHex(String hex) {
642      var buff = ByteBuffer.allocate(hex.length()/2);
643      for (var i = 0; i < hex.length(); i+=2)
644         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
645      buff.rewind();
646      return buff.array();
647   }
648
649   /**
650    * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 encoded string.
651    *
652    * @param hex The hexadecimal string.
653    * @return The UTF-8 string.
654    */
655   public static String fromHexToUTF8(String hex) {
656      var buff = ByteBuffer.allocate(hex.length()/2);
657      for (var i = 0; i < hex.length(); i+=2)
658         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
659      buff.rewind();  // Fixes Java 11 issue.
660      return UTF_8.decode(buff).toString();
661   }
662
663   /**
664    * Same as {@link #fromHex(String)} except expects spaces between the byte strings.
665    *
666    * @param hex The string to convert to a byte array.
667    * @return A new byte array.
668    */
669   public static byte[] fromSpacedHex(String hex) {
670      var buff = ByteBuffer.allocate((hex.length()+1)/3);
671      for (var i = 0; i < hex.length(); i+=3)
672         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
673      buff.rewind();
674      return buff.array();
675   }
676
677   /**
678    * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 BC") into a UTF-8 encoded string.
679    *
680    * @param hex The hexadecimal string.
681    * @return The UTF-8 string.
682    */
683   public static String fromSpacedHexToUTF8(String hex) {
684      var buff = ByteBuffer.allocate((hex.length()+1)/3);
685      for (var i = 0; i < hex.length(); i+=3)
686         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
687      buff.rewind();  // Fixes Java 11 issue.
688      return UTF_8.decode(buff).toString();
689   }
690
691   /**
692    * Given an absolute URI, returns just the authority portion (e.g. <js>"http://hostname:port"</js>)
693    *
694    * @param s The URI string.
695    * @return Just the authority portion of the URI.
696    */
697   public static String getAuthorityUri(String s) {  // NOSONAR - False positive.
698
699      // Use a state machine for maximum performance.
700
701      final int
702         S1 = 1,  // Looking for http
703         S2 = 2,  // Found http, looking for :
704         S3 = 3,  // Found :, looking for /
705         S4 = 4,  // Found /, looking for /
706         S5 = 5,  // Found /, looking for x
707         S6 = 6;  // Found x, looking for /
708
709      var state = S1;
710
711      for (var i = 0; i < s.length(); i++) {
712         var c = s.charAt(i);
713         if (state == S1) {
714            if (c >= 'a' && c <= 'z')
715               state = S2;
716            else
717               return s;
718         } else if (state == S2) {
719            if (c == ':')
720               state = S3;
721            else if (c < 'a' || c > 'z')
722               return s;
723         } else if (state == S3) {  // NOSONAR - False positive.
724            if (c == '/')
725               state = S4;
726            else
727               return s;
728         } else if (state == S4) {
729            if (c == '/')
730               state = S5;
731            else
732               return s;
733         } else if (state == S5) {
734            if (c != '/')
735               state = S6;
736            else
737               return s;
738         } else if (state == S6) {
739            if (c == '/')  // NOSONAR - Intentional.
740               return s.substring(0, i);
741         }
742      }
743
744      return s;
745   }
746
747   /**
748    * Parses a duration string.
749    *
750    * <p>
751    * Examples:
752    * <ul>
753    *    <li><js>"1000"</js> - 1000 milliseconds.
754    *    <li><js>"10s"</js> - 10 seconds.
755    *    <li><js>"10 sec"</js> - 10 seconds.
756    *    <li><js>"10 seconds"</js> - 10 seconds.
757    * </ul>
758    *
759    * <p>
760    * Use any of the following suffixes:
761    * <ul>
762    *    <li>None (time in milliseconds).
763    *    <li><js>"s"</js>/<js>"sec"</js>/<js>"second"</js>/<js>"seconds"</js>
764    *    <li><js>"m"</js>/<js>"min"</js>/<js>"minutes"</js>/<js>"seconds"</js>
765    *    <li><js>"h"</js>/<js>"hour"</js>/<js>"hours"</js>
766    *    <li><js>"d"</js>/<js>"day"</js>/<js>"days"</js>
767    *    <li><js>"w"</js>/<js>"week"</js>/<js>"weeks"</js>
768    * </ul>
769    *
770    * <p>
771    * Suffixes are case-insensitive.
772    * <br>Whitespace is ignored.
773    *
774    * @param s The string to parse.
775    * @return
776    *    The time in milliseconds, or <c>-1</c> if the string is empty or <jk>null</jk>.
777    */
778   public static long getDuration(String s) {
779      s = trim(s);
780      if (Utils.isEmpty(s))
781         return -1;
782      int i;
783      for (i = 0; i < s.length(); i++) {
784         var c = s.charAt(i);
785         if (c < '0' || c > '9')
786            break;
787      }
788      long l;
789      if (i == s.length())
790         l = Long.parseLong(s);
791      else {
792         l = Long.parseLong(s.substring(0, i).trim());
793         var r = s.substring(i).trim().toLowerCase();
794         if (r.startsWith("s"))
795            l *= 1000;
796         else if (r.startsWith("m"))
797            l *= 1000 * 60;
798         else if (r.startsWith("h"))
799            l *= 1000 * 60 * 60;
800         else if (r.startsWith("d"))
801            l *= 1000 * 60 * 60 * 24;
802         else if (r.startsWith("w"))
803            l *= 1000 * 60 * 60 * 24 * 7;
804      }
805      return l;
806   }
807   /**
808    * Gets or creates an AsciiSet for escaping the specified character.
809    *
810    * @param c The character to create an escape set for.
811    * @return An AsciiSet containing the character and backslash.
812    */
813   static AsciiSet getEscapeSet(char c) {
814      return ESCAPE_SETS.computeIfAbsent(c, key -> AsciiSet.create().chars(key, '\\').build());
815   }
816
817   /**
818    * Takes in a string, splits it by lines, and then prepends each line with line numbers.
819    *
820    * @param s The string.
821    * @return The string with line numbers added.
822    */
823   public static String getNumberedLines(String s) {
824      return getNumberedLines(s, 1, Integer.MAX_VALUE);
825   }
826
827   /**
828    * Same as {@link #getNumberedLines(String)} except only returns the specified lines.
829    *
830    * <p>
831    * Out-of-bounds values are allowed and fixed.
832    *
833    * @param s The string.
834    * @param start The starting line (1-indexed).
835    * @param end The ending line (1-indexed).
836    * @return The string with line numbers added.
837    */
838   public static String getNumberedLines(String s, int start, int end) {
839      if (s == null)
840         return null;
841      var lines = s.split("[\r\n]+");
842      var digits = String.valueOf(lines.length).length();
843      if (start < 1)
844         start = 1;
845      if (end < 0)
846         end = Integer.MAX_VALUE;
847      if (end > lines.length)
848         end = lines.length;
849      var sb = new StringBuilder();
850      for (var l :  Arrays.asList(lines).subList(start-1, end))
851         sb.append(String.format("%0"+digits+"d", start++)).append(": ").append(l).append("\n");  // NOSONAR - Intentional.
852      return sb.toString();
853   }
854
855   /**
856    * Same as {@link String#indexOf(int)} except allows you to check for multiple characters.
857    *
858    * @param s The string to check.
859    * @param c The characters to check for.
860    * @return The index into the string that is one of the specified characters.
861    */
862   public static int indexOf(String s, char...c) {
863      if (s == null)
864         return -1;
865      for (var i = 0; i < s.length(); i++) {
866         var c2 = s.charAt(i);
867         for (var cc : c)
868            if (c2 == cc)
869               return i;
870      }
871      return -1;
872   }
873
874   /**
875    * Efficiently determines whether a URL is of the pattern "xxx://xxx"
876    *
877    * @param s The string to test.
878    * @return <jk>true</jk> if it's an absolute path.
879    */
880   public static boolean isAbsoluteUri(String s) {  // NOSONAR - False positive.
881
882      if (Utils.isEmpty(s))
883         return false;
884
885      // Use a state machine for maximum performance.
886
887      final int
888         S1 = 1,  // Looking for http
889         S2 = 2,  // Found http, looking for :
890         S3 = 3,  // Found :, looking for /
891         S4 = 4,  // Found /, looking for /
892         S5 = 5;  // Found /, looking for x
893
894      var state = S1;
895
896      for (var i = 0; i < s.length(); i++) {
897         var c = s.charAt(i);
898         if (state == S1) {
899            if (c >= 'a' && c <= 'z')
900               state = S2;
901            else
902               return false;
903         } else if (state == S2) {
904            if (c == ':')
905               state = S3;
906            else if (c < 'a' || c > 'z')
907               return false;
908         } else if (state == S3) {  // NOSONAR - False positive.
909            if (c == '/')
910               state = S4;
911            else
912               return false;
913         } else if (state == S4) {
914            if (c == '/')
915               state = S5;
916            else
917               return false;
918         } else if (state == S5) {
919            return true;
920         }
921      }
922      return false;
923   }
924
925   /**
926    * Returns <jk>true</jk> if the specified string is numeric.
927    *
928    * @param s The string to check.
929    * @return <jk>true</jk> if the specified string is numeric.
930    */
931   public static boolean isDecimal(String s) {
932      if (s == null || s.isEmpty() || ! firstNumberChars.contains(s.charAt(0)))
933         return false;
934      var i = 0;
935      var length = s.length();
936      var c = s.charAt(0);
937      var isPrefixed = false;
938      if (c == '+' || c == '-') {
939         isPrefixed = true;
940         i++;
941      }
942      if (i == length)
943         return false;
944      c = s.charAt(i++);
945      if (c == '0' && length > (isPrefixed ? 2 : 1)) {
946         c = s.charAt(i++);
947         if (c == 'x' || c == 'X') {
948            for (int j = i; j < length; j++) {
949               if (! hexChars.contains(s.charAt(j)))
950                  return false;
951            }
952         } else if (octChars.contains(c)) {
953            for (int j = i; j < length; j++)
954               if (! octChars.contains(s.charAt(j)))
955                  return false;
956         } else {
957            return false;
958         }
959      } else if (c == '#') {
960         for (int j = i; j < length; j++) {
961            if (! hexChars.contains(s.charAt(j)))
962               return false;
963         }
964      } else if (decChars.contains(c)) {
965         for (int j = i; j < length; j++)
966            if (! decChars.contains(s.charAt(j)))
967               return false;
968      } else {
969         return false;
970      }
971      return true;
972   }
973
974   /**
975    * Returns <jk>true</jk> if the specified character is a valid first character for a number.
976    *
977    * @param c The character to test.
978    * @return <jk>true</jk> if the specified character is a valid first character for a number.
979    */
980   public static boolean isFirstNumberChar(char c) {
981      return firstNumberChars.contains(c);
982   }
983
984   /**
985    * Returns <jk>true</jk> if the specified string is a floating point number.
986    *
987    * @param s The string to check.
988    * @return <jk>true</jk> if the specified string is a floating point number.
989    */
990   public static boolean isFloat(String s) {
991      if (s == null || s.isEmpty())
992         return false;
993      if (! firstNumberChars.contains(s.charAt(0)))
994         return (s.equals("NaN") || s.equals("Infinity"));
995      var i = 0;
996      var length = s.length();
997      var c = s.charAt(0);
998      if (c == '+' || c == '-')
999         i++;
1000      if (i == length)
1001         return false;
1002      c = s.charAt(i);
1003      if (c == '.' || decChars.contains(c)) {
1004         return fpRegex.matcher(s).matches();
1005      }
1006      return false;
1007   }
1008
1009   /**
1010    * Returns <jk>true</jk> if the specified string is valid JSON.
1011    *
1012    * <p>
1013    * Leading and trailing spaces are ignored.
1014    * <br>Leading and trailing comments are not allowed.
1015    *
1016    * @param s The string to test.
1017    * @return <jk>true</jk> if the specified string is valid JSON.
1018    */
1019   public static boolean isJson(String s) {
1020      if (s == null)
1021         return false;
1022      var c1 = firstNonWhitespaceChar(s);
1023      var c2 = lastNonWhitespaceChar(s);
1024      if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == '\'' && c2 == '\'')
1025         return true;
1026      return (isOneOf(s, "true","false","null") || isNumeric(s));
1027   }
1028
1029   /**
1030    * Returns <jk>true</jk> if the specified string appears to be an JSON array.
1031    *
1032    * @param o The object to test.
1033    * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored.
1034    * @return <jk>true</jk> if the specified string appears to be a JSON array.
1035    */
1036   public static boolean isJsonArray(Object o, boolean ignoreWhitespaceAndComments) {
1037      if (o instanceof CharSequence) {
1038         var s = o.toString();
1039         if (! ignoreWhitespaceAndComments)
1040            return (s.startsWith("[") && s.endsWith("]"));
1041         if (firstRealCharacter(s) != '[')
1042            return false;
1043         var i = s.lastIndexOf(']');
1044         if (i == -1)
1045            return false;
1046         s = s.substring(i+1);
1047         return firstRealCharacter(s) == -1;
1048      }
1049      return false;
1050   }
1051
1052   /**
1053    * Returns <jk>true</jk> if the specified string appears to be a JSON object.
1054    *
1055    * @param o The object to test.
1056    * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored.
1057    * @return <jk>true</jk> if the specified string appears to be a JSON object.
1058    */
1059   public static boolean isJsonObject(Object o, boolean ignoreWhitespaceAndComments) {
1060      if (o instanceof CharSequence) {
1061         var s = o.toString();
1062         if (! ignoreWhitespaceAndComments)
1063            return (s.startsWith("{") && s.endsWith("}"));
1064         if (firstRealCharacter(s) != '{')
1065            return false;
1066         var i = s.lastIndexOf('}');
1067         if (i == -1)
1068            return false;
1069         s = s.substring(i+1);
1070         return firstRealCharacter(s) == -1;
1071      }
1072      return false;
1073   }
1074
1075   /**
1076    * Returns <jk>true</jk> if the specified character is a valid number character.
1077    *
1078    * @param c The character to check.
1079    * @return <jk>true</jk> if the specified character is a valid number character.
1080    */
1081   public static boolean isNumberChar(char c) {
1082      return numberChars.contains(c);
1083   }
1084
1085   /**
1086    * Returns <jk>true</jk> if this string can be parsed by {@link #parseNumber(String, Class)}.
1087    *
1088    * @param s The string to check.
1089    * @return <jk>true</jk> if this string can be parsed without causing an exception.
1090    */
1091   public static boolean isNumeric(String s) {
1092      if (s == null || s.isEmpty() || ! isFirstNumberChar(s.charAt(0)))
1093         return false;
1094      return isDecimal(s) || isFloat(s);
1095   }
1096
1097   /**
1098    * Returns <jk>true</jk> if the specified string is one of the specified values.
1099    *
1100    * @param s
1101    *    The string to test.
1102    *    Can be <jk>null</jk>.
1103    * @param values
1104    *    The values to test.
1105    *    Can contain <jk>null</jk>.
1106    * @return <jk>true</jk> if the specified string is one of the specified values.
1107    */
1108   public static boolean isOneOf(String s, String...values) {
1109      for (var value : values)
1110         if (Utils.eq(s, value))
1111            return true;
1112      return false;
1113   }
1114
1115   /**
1116    * Efficiently determines whether a URL is of the pattern "xxx:/xxx".
1117    *
1118    * <p>
1119    * The pattern matched is: <c>[a-z]{2,}\:\/.*</c>
1120    *
1121    * <p>
1122    * Note that this excludes filesystem paths such as <js>"C:/temp"</js>.
1123    *
1124    * @param s The string to test.
1125    * @return <jk>true</jk> if it's an absolute path.
1126    */
1127   public static boolean isUri(String s) {  // NOSONAR - False positive.
1128
1129      if (Utils.isEmpty(s))
1130         return false;
1131
1132      // Use a state machine for maximum performance.
1133
1134      final int
1135         S1 = 1,  // Looking for protocol char 1
1136         S2 = 2,  // Found protocol char 1, looking for protocol char 2
1137         S3 = 3,  // Found protocol char 2, looking for :
1138         S4 = 4;  // Found :, looking for /
1139
1140      var state = S1;
1141
1142      for (var i = 0; i < s.length(); i++) {
1143         var c = s.charAt(i);
1144         if (state == S1) {
1145            if (c >= 'a' && c <= 'z')
1146               state = S2;
1147            else
1148               return false;
1149         } else if (state == S2) {
1150            if (c >= 'a' && c <= 'z')
1151               state = S3;
1152            else
1153               return false;
1154         } else if (state == S3) {  // NOSONAR - False positive.
1155            if (c == ':')
1156               state = S4;
1157            else if (c < 'a' || c > 'z')
1158               return false;
1159         } else if (state == S4) {
1160            return c == '/';
1161         }
1162      }
1163      return false;
1164   }
1165
1166   /**
1167    * Same as {@link Utils#join(Collection, char)} but escapes the delimiter if found in the tokens.
1168    *
1169    * @param tokens The tokens to join.
1170    * @param d The delimiter.
1171    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
1172    */
1173   public static String joine(List<?> tokens, char d) {
1174      if (tokens == null)
1175         return null;
1176      var as = getEscapeSet(d);
1177      var sb = new StringBuilder();
1178      for (int i = 0, j = tokens.size(); i < j; i++) {
1179         if (i > 0)
1180            sb.append(d);
1181         sb.append(escapeChars(Utils.s(tokens.get(i)), as));
1182      }
1183      return sb.toString();
1184   }
1185
1186   /**
1187    * Returns the last non-whitespace character in the string.
1188    *
1189    * @param s The string to check.
1190    * @return
1191    *    The last non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed
1192    *    of only whitespace.
1193    */
1194   public static char lastNonWhitespaceChar(String s) {
1195      if (s != null)
1196         for (var i = s.length()-1; i >= 0; i--)
1197            if (! isWhitespace(s.charAt(i)))
1198               return s.charAt(i);
1199      return 0;
1200   }
1201
1202   /**
1203    * Determines the multiplier value based on the suffix character in a string.
1204    *
1205    * @param s The string to analyze for multiplier suffix.
1206    * @return The multiplier value (1 if no valid suffix found).
1207    */
1208   private static int multiplier(String s) {
1209      char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1);  // NOSONAR - NPE not possible.
1210      if (c == 'G') return 1024*1024*1024;
1211      if (c == 'M') return 1024*1024;
1212      if (c == 'K') return 1024;
1213      if (c == 'g') return 1000*1000*1000;
1214      if (c == 'm') return 1000*1000;
1215      if (c == 'k') return 1000;
1216      return 1;
1217   }
1218
1219   /**
1220    * Determines the long multiplier value based on the suffix character in a string.
1221    *
1222    * @param s The string to analyze for multiplier suffix.
1223    * @return The multiplier value (1 if no valid suffix found).
1224    */
1225   private static long multiplier2(String s) {
1226      char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1);  // NOSONAR - NPE not possible.
1227      if (c == 'P') return 1024*1024*1024*1024*1024l;
1228      if (c == 'T') return 1024*1024*1024*1024l;
1229      if (c == 'G') return 1024*1024*1024l;
1230      if (c == 'M') return 1024*1024l;
1231      if (c == 'K') return 1024l;
1232      if (c == 'p') return 1000*1000*1000*1000*1000l;
1233      if (c == 't') return 1000*1000*1000*1000l;
1234      if (c == 'g') return 1000*1000*1000l;
1235      if (c == 'm') return 1000*1000l;
1236      if (c == 'k') return 1000l;
1237      return 1;
1238   }
1239
1240   /**
1241    * Converts a <c>String</c> to a <c>Character</c>
1242    *
1243    * @param o The string to convert.
1244    * @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.
1245    * @throws IllegalArgumentException If the string length is not 1.
1246    */
1247   public static Character parseCharacter(Object o) {
1248      if (o == null)
1249         return null;
1250      var s = o.toString();
1251      if (s.isEmpty())
1252         return null;
1253      if (s.length() == 1)
1254         return s.charAt(0);
1255      throw new IllegalArgumentException("Invalid character: '" + s + "'");
1256   }
1257
1258   /**
1259    * Converts a string containing a possible multiplier suffix to an integer.
1260    *
1261    * <p>
1262    * The string can contain any of the following multiplier suffixes:
1263    * <ul>
1264    *    <li><js>"K"</js> - x 1024
1265    *    <li><js>"M"</js> - x 1024*1024
1266    *    <li><js>"G"</js> - x 1024*1024*1024
1267    *    <li><js>"k"</js> - x 1000
1268    *    <li><js>"m"</js> - x 1000*1000
1269    *    <li><js>"g"</js> - x 1000*1000*1000
1270    * </ul>
1271    *
1272    * @param s The string to parse.
1273    * @return The parsed value.
1274    */
1275   public static int parseIntWithSuffix(String s) {
1276      Utils.assertArgNotNull("s", s);
1277      var m = multiplier(s);
1278      if (m == 1)
1279         return Integer.decode(s);
1280      return Integer.decode(s.substring(0, s.length()-1).trim()) * m;  // NOSONAR - NPE not possible here.
1281   }
1282
1283   /**
1284    * Parses an ISO8601 string into a calendar.
1285    *
1286    * <p>
1287    * Supports any of the following formats:
1288    * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c>
1289    *
1290    * @param date The date string.
1291    * @return The parsed calendar.
1292    * @throws IllegalArgumentException Value was not a valid date.
1293    */
1294   public static Calendar parseIsoCalendar(String date) throws IllegalArgumentException {
1295      if (Utils.isEmpty(date))
1296         return null;
1297      date = date.trim().replace(' ', 'T');  // Convert to 'standard' ISO8601
1298      if (date.indexOf(',') != -1)  // Trim milliseconds
1299         date = date.substring(0, date.indexOf(','));
1300      if (date.matches("\\d{4}"))
1301         date += "-01-01T00:00:00";
1302      else if (date.matches("\\d{4}\\-\\d{2}"))
1303         date += "-01T00:00:00";
1304      else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}"))
1305         date += "T00:00:00";
1306      else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}"))
1307         date += ":00:00";
1308      else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}"))
1309         date += ":00";
1310      return DatatypeConverter.parseDateTime(date);
1311   }
1312
1313   /**
1314    * Parses an ISO8601 string into a date.
1315    *
1316    * <p>
1317    * Supports any of the following formats:
1318    * <br><c>yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS</c>
1319    *
1320    * @param date The date string.
1321    * @return The parsed date.
1322    * @throws IllegalArgumentException Value was not a valid date.
1323    */
1324   public static Date parseIsoDate(String date) throws IllegalArgumentException {
1325      if (Utils.isEmpty(date))
1326         return null;
1327      return parseIsoCalendar(date).getTime();  // NOSONAR - NPE not possible.
1328   }
1329
1330   /**
1331    * Converts a string containing a possible multiplier suffix to a long.
1332    *
1333    * <p>
1334    * The string can contain any of the following multiplier suffixes:
1335    * <ul>
1336    *    <li><js>"K"</js> - x 1024
1337    *    <li><js>"M"</js> - x 1024*1024
1338    *    <li><js>"G"</js> - x 1024*1024*1024
1339    *    <li><js>"T"</js> - x 1024*1024*1024*1024
1340    *    <li><js>"P"</js> - x 1024*1024*1024*1024*1024
1341    *    <li><js>"k"</js> - x 1000
1342    *    <li><js>"m"</js> - x 1000*1000
1343    *    <li><js>"g"</js> - x 1000*1000*1000
1344    *    <li><js>"t"</js> - x 1000*1000*1000*1000
1345    *    <li><js>"p"</js> - x 1000*1000*1000*1000*1000
1346    * </ul>
1347    *
1348    * @param s The string to parse.
1349    * @return The parsed value.
1350    */
1351   public static long parseLongWithSuffix(String s) {
1352      Utils.assertArgNotNull("s", s);
1353      var m = multiplier2(s);
1354      if (m == 1)
1355         return Long.decode(s);
1356      return Long.decode(s.substring(0, s.length()-1).trim()) * m;  // NOSONAR - NPE not possible here.
1357   }
1358
1359   /**
1360    * Parses a number from the specified string.
1361    *
1362    * @param s The string to parse the number from.
1363    * @param type
1364    *    The number type to created.
1365    *    Can be any of the following:
1366    *    <ul>
1367    *       <li> Integer
1368    *       <li> Double
1369    *       <li> Float
1370    *       <li> Long
1371    *       <li> Short
1372    *       <li> Byte
1373    *       <li> BigInteger
1374    *       <li> BigDecimal
1375    *    </ul>
1376    *    If <jk>null</jk> or <c>Number</c>, uses the best guess.
1377    * @return The parsed number, or <jk>null</jk> if the string was null.
1378    */
1379   public static Number parseNumber(String s, Class<? extends Number> type) {
1380      if (s == null)
1381         return null;
1382      if (s.isEmpty())
1383         s = "0";
1384      if (type == null)
1385         type = Number.class;
1386
1387      // Determine the data type if it wasn't specified.
1388      var isAutoDetect = (type == Number.class);
1389      var isDecimal = false;
1390      if (isAutoDetect) {
1391         // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how
1392         // long the string is.
1393         // An integer range is -2,147,483,648 to 2,147,483,647
1394         // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
1395         isDecimal = isDecimal(s);
1396         if (isDecimal) {
1397            if (s.length() > 20)
1398               type = Double.class;
1399            else if (s.length() >= 10)
1400               type = Long.class;
1401            else
1402               type = Integer.class;
1403         }
1404         else if (isFloat(s))
1405            type = Double.class;
1406         else
1407            throw new NumberFormatException(s);
1408      }
1409
1410      if (type == Double.class || type == Double.TYPE) {
1411         var d = Double.valueOf(s);
1412         var f = Float.valueOf(s);
1413         if (isAutoDetect && (!isDecimal) && d.toString().equals(f.toString()))
1414            return f;
1415         return d;
1416      }
1417      if (type == Float.class || type == Float.TYPE)
1418         return Float.valueOf(s);
1419      if (type == BigDecimal.class)
1420         return new BigDecimal(s);
1421      if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) {
1422         try {
1423            var l = Long.decode(s);
1424            if (type == AtomicLong.class)
1425               return new AtomicLong(l);
1426            if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) {
1427               // This occurs if the string is 10 characters long but is still a valid integer value.
1428               return l.intValue();
1429            }
1430            return l;
1431         } catch (NumberFormatException e) {
1432            if (isAutoDetect) {
1433               // This occurs if the string is 20 characters long but still falls outside the range of a valid long.
1434               return Double.valueOf(s);
1435            }
1436            throw e;
1437         }
1438      }
1439      if (type == Integer.class || type == Integer.TYPE)
1440         return Integer.decode(s);
1441      if (type == Short.class || type == Short.TYPE)
1442         return Short.decode(s);
1443      if (type == Byte.class || type == Byte.TYPE)
1444         return Byte.decode(s);
1445      if (type == BigInteger.class)
1446         return new BigInteger(s);
1447      if (type == AtomicInteger.class)
1448         return new AtomicInteger(Integer.decode(s));
1449      throw new NumberFormatException("Unsupported Number type: "+type.getName());
1450   }
1451
1452   /**
1453    * Generated a random UUID with the specified number of characters.
1454    *
1455    * <p>
1456    * Characters are composed of lower-case ASCII letters and numbers only.
1457    *
1458    * <p>
1459    * This method conforms to the restrictions for hostnames as specified in <a class="doclink" href="https://tools.ietf.org/html/rfc952">RFC 952</a>
1460    * Since each character has 36 possible values, the square approximation formula for the number of generated IDs
1461    * that would produce a 50% chance of collision is:
1462    * <c>sqrt(36^N)</c>.
1463    * Dividing this number by 10 gives you an approximation of the number of generated IDs needed to produce a
1464    * &lt;1% chance of collision.
1465    *
1466    * <p>
1467    * For example, given 5 characters, the number of generated IDs need to produce a &lt;1% chance of collision would
1468    * be:
1469    * <c>sqrt(36^5)/10=777</c>
1470    *
1471    * @param numchars The number of characters in the generated UUID.
1472    * @return A new random UUID.
1473    */
1474   public static String random(int numchars) {
1475      var sb = new StringBuilder(numchars);
1476      for (var i = 0; i < numchars; i++) {
1477         var c = RANDOM.nextInt(36) + 97;
1478         if (c > 'z')
1479            c -= ('z'-'0'+1);
1480         sb.append((char)c);
1481      }
1482      return sb.toString();
1483   }
1484
1485   /**
1486    * Creates a repeated pattern.
1487    *
1488    * @param count The number of times to repeat the pattern.
1489    * @param pattern The pattern to repeat.
1490    * @return A new string consisting of the repeated pattern.
1491    */
1492   public static String repeat(int count, String pattern) {
1493      var sb = new StringBuilder(pattern.length() * count);
1494      for (var i = 0; i < count; i++)
1495         sb.append(pattern);
1496      return sb.toString();
1497   }
1498
1499   /**
1500    * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters.
1501    *
1502    * @param s The string to replace unicode sequences in.
1503    * @return A string with unicode sequences replaced.
1504    */
1505   public static String replaceUnicodeSequences(String s) {
1506
1507      if (s.indexOf('\\') == -1)
1508         return s;
1509
1510      var p = Pattern.compile("\\\\u(\\p{XDigit}{4})");
1511      var m = p.matcher(s);
1512      var sb = new StringBuffer(s.length());
1513
1514      while (m.find()) {
1515         var ch = String.valueOf((char) Integer.parseInt(m.group(1), 16));
1516         m.appendReplacement(sb, Matcher.quoteReplacement(ch));
1517      }
1518
1519      m.appendTail(sb);
1520      return sb.toString();
1521   }
1522
1523   /**
1524    * Simple utility for replacing variables of the form <js>"{key}"</js> with values in the specified map.
1525    *
1526    * <p>
1527    * Nested variables are supported in both the input string and map values.
1528    *
1529    * <p>
1530    * If the map does not contain the specified value, the variable is not replaced.
1531    *
1532    * <p>
1533    * <jk>null</jk> values in the map are treated as blank strings.
1534    *
1535    * @param s The string containing variables to replace.
1536    * @param m The map containing the variable values.
1537    * @return The new string with variables replaced, or the original string if it didn't have variables in it.
1538    */
1539   public static String replaceVars(String s, Map<String,Object> m) {
1540
1541      if (s == null)
1542         return null;
1543
1544      if (m == null || m.isEmpty() || s.indexOf('{') == -1)
1545         return s;
1546
1547      final int
1548         S1 = 1,     // Not in variable, looking for '{'
1549         S2 = 2;    // Found '{', Looking for '}'
1550
1551      var state = S1;
1552      var hasInternalVar = false;
1553      var x = 0;
1554      var depth = 0;
1555      var length = s.length();
1556      var out = new StringBuilder();
1557
1558      for (var i = 0; i < length; i++) {
1559         var c = s.charAt(i);
1560         if (state == S1) {
1561            if (c == '{') {
1562               state = S2;
1563               x = i;
1564            } else {
1565               out.append(c);
1566            }
1567         } else /* state == S2 */ {
1568            if (c == '{') {
1569               depth++;
1570               hasInternalVar = true;
1571            } else if (c == '}') {
1572               if (depth > 0) {
1573                  depth--;
1574               } else {
1575                  var key = s.substring(x+1, i);
1576                  key = (hasInternalVar ? replaceVars(key, m) : key);
1577                  hasInternalVar = false;
1578                  if (! m.containsKey(key))
1579                     out.append('{').append(key).append('}');
1580                  else {
1581                     var val = m.get(key);
1582                     if (val == null)
1583                        val = "";
1584                     var v = val.toString();
1585                     // If the replacement also contains variables, replace them now.
1586                     if (v.indexOf('{') != -1)
1587                        v = replaceVars(v, m);
1588                     out.append(v);
1589                  }
1590                  state = 1;
1591               }
1592            }
1593         }
1594      }
1595      return out.toString();
1596   }
1597
1598   /**
1599    * Skips over comment sequences in a StringReader.
1600    *
1601    * @param r The StringReader positioned at the start of a comment.
1602    * @throws IOException If an I/O error occurs.
1603    */
1604   private static void skipComments(StringReader r) throws IOException {
1605      var c = r.read();
1606      //  "/* */" style comments
1607      if (c == '*') {
1608         while (c != -1)
1609            if ((c = r.read()) == '*')
1610               if ((c = r.read()) == '/')  // NOSONAR - Intentional.
1611                  return;
1612      //  "//" style comments
1613      } else if (c == '/') {
1614         while (c != -1) {
1615            c = r.read();
1616            if (c == -1 || c == '\n')
1617               return;
1618         }
1619      }
1620   }
1621
1622   /**
1623    * An efficient method for checking if a string starts with a character.
1624    *
1625    * @param s The string to check.  Can be <jk>null</jk>.
1626    * @param c The character to check for.
1627    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character.
1628    */
1629   public static boolean startsWith(String s, char c) {
1630      if (s != null) {
1631         var i = s.length();
1632         if (i > 0)
1633            return s.charAt(0) == c;
1634      }
1635      return false;
1636   }
1637
1638   /**
1639    * Converts the specified array to a string.
1640    *
1641    * @param o The array to convert to a string.
1642    * @return The array converted to a string, or <jk>null</jk> if the object was null.
1643    */
1644   public static String stringifyDeep(Object o) {
1645      if (o == null)
1646         return null;
1647      if (! isArray(o))
1648         return o.toString();
1649      if (o.getClass().getComponentType().isPrimitive())
1650         return PRIMITIVE_ARRAY_STRINGIFIERS.get(o.getClass()).apply(o);
1651      return Arrays.deepToString((Object[])o);
1652   }
1653
1654   /**
1655    * Strips the first and last character from a string.
1656    *
1657    * @param s The string to strip.
1658    * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2.
1659    */
1660   public static String strip(String s) {
1661      if (s == null || s.length() <= 1)
1662         return s;
1663      return s.substring(1, s.length()-1);
1664   }
1665
1666   /**
1667    * Strips invalid characters such as CTRL characters from a string meant to be encoded
1668    * as an HTTP header value.
1669    *
1670    * @param s The string to strip chars from.
1671    * @return The string with invalid characters removed.
1672    */
1673   public static String stripInvalidHttpHeaderChars(String s) {
1674
1675      if (s == null)
1676         return null;
1677
1678      var needsReplace = false;
1679      for (var i = 0; i < s.length() && ! needsReplace; i++)
1680         needsReplace |= httpHeaderChars.contains(s.charAt(i));
1681
1682      if (! needsReplace)
1683         return s;
1684
1685      var sb = new StringBuilder(s.length());
1686      for (var i = 0; i < s.length(); i++) {
1687         var c = s.charAt(i);
1688         if (httpHeaderChars.contains(c))
1689            sb.append(c);
1690      }
1691
1692      return sb.toString();
1693   }
1694
1695   /**
1696    * Converts the specified object to a comma-delimited list.
1697    *
1698    * @param o The object to convert.
1699    * @return The specified object as a comma-delimited list.
1700    */
1701   public static String toCdl(Object o) {
1702      if (o == null)
1703         return null;
1704      if (isArray(o)) {
1705         var sb = new StringBuilder();
1706         for (int i = 0, j = Array.getLength(o); i < j; i++) {
1707            if (i > 0)
1708               sb.append(", ");
1709            sb.append(Array.get(o, i));
1710         }
1711         return sb.toString();
1712      }
1713      if (o instanceof Collection)
1714         return Utils.join((Collection<?>)o, ", ");
1715      return o.toString();
1716   }
1717
1718   /**
1719    * Converts the specified byte into a 2 hexadecimal characters.
1720    *
1721    * @param b The number to convert to hex.
1722    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
1723    */
1724   public static String toHex(byte b) {
1725      var c = new char[2];
1726      var v = b & 0xFF;
1727      c[0] = hexArray[v >>> 4];
1728      c[1] = hexArray[v & 0x0F];
1729      return new String(c);
1730   }
1731
1732   /**
1733    * Converts a byte array into a simple hexadecimal character string.
1734    *
1735    * @param bytes The bytes to convert to hexadecimal.
1736    * @return A new string consisting of hexadecimal characters.
1737    */
1738   public static String toHex(byte[] bytes) {
1739      var sb = new StringBuilder(bytes.length * 2);
1740      for (var element : bytes) {
1741         var v = element & 0xFF;
1742         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
1743      }
1744      return sb.toString();
1745   }
1746
1747   /**
1748    * Converts the contents of the specified input stream to a hex string.
1749    *
1750    * @param is The input stream to convert.
1751    * @return The hex string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>.
1752    */
1753   public static String toHex(InputStream is) {
1754      return safe(()->is == null ? null : toHex(readBytes(is)));
1755   }
1756
1757   /**
1758    * Converts the specified number into a 2 hexadecimal characters.
1759    *
1760    * @param num The number to convert to hex.
1761    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
1762    */
1763   public static char[] toHex2(int num) {
1764      if (num < 0 || num > 255)
1765         throw new NumberFormatException("toHex2 can only be used on numbers between 0 and 255");
1766      var n = new char[2];
1767      var a = num%16;
1768      n[1] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1769      a = (num/16)%16;
1770      n[0] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1771      return n;
1772   }
1773
1774   /**
1775    * Converts the specified number into a 4 hexadecimal characters.
1776    *
1777    * @param num The number to convert to hex.
1778    * @return A <code><jk>char</jk>[4]</code> containing the specified characters.
1779    */
1780   public static char[] toHex4(int num) {
1781      var n = new char[4];
1782      var a = num%16;
1783      n[3] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1784      var base = 16;
1785      for (var i = 1; i < 4; i++) {
1786         a = (num/base)%16;
1787         base <<= 4;
1788         n[3-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1789      }
1790      return n;
1791   }
1792
1793   /**
1794    * Converts the specified number into a 8 hexadecimal characters.
1795    *
1796    * @param num The number to convert to hex.
1797    * @return A <code><jk>char</jk>[8]</code> containing the specified characters.
1798    */
1799   public static char[] toHex8(long num) {
1800      var n = new char[8];
1801      var a = num%16;
1802      n[7] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1803      var base = 16;
1804      for (var i = 1; i < 8; i++) {
1805         a = (num/base)%16;
1806         base <<= 4;
1807         n[7-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1808      }
1809      return n;
1810   }
1811
1812   /**
1813    * Converts the specified object to an ISO8601 date string.
1814    *
1815    * @param c The object to convert.
1816    * @return The converted object.
1817    */
1818   public static String toIsoDate(Calendar c) {
1819      return DatatypeConverter.printDate(c);
1820   }
1821   /**
1822    * Converts the specified object to an ISO8601 date-time string.
1823    *
1824    * @param c The object to convert.
1825    * @return The converted object.
1826    */
1827   public static String toIsoDateTime(Calendar c) {
1828      return DatatypeConverter.printDateTime(c);
1829   }
1830
1831   /**
1832    * Converts the specified bytes into a readable string.
1833    *
1834    * @param b The number to convert to hex.
1835    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
1836    */
1837   public static String toReadableBytes(byte[] b) {
1838      var sb = new StringBuilder();
1839      for (var b2 : b)
1840         sb.append((b2 < ' ' || b2 > 'z') ? String.format("[%02X]", b2) : (char)b2 + "   ");
1841      sb.append("\n");
1842      for (var b2 : b)
1843         sb.append(String.format("[%02X]", b2));
1844      return sb.toString();
1845   }
1846
1847   /**
1848    * Same as {@link #toHex(byte[])} but puts spaces between the byte strings.
1849    *
1850    * @param bytes The bytes to convert to hexadecimal.
1851    * @return A new string consisting of hexadecimal characters.
1852    */
1853   public static String toSpacedHex(byte[] bytes) {
1854      var sb = new StringBuilder(bytes.length * 3);
1855      for (var j = 0; j < bytes.length; j++) {
1856         if (j > 0)
1857            sb.append(' ');
1858         var v = bytes[j] & 0xFF;
1859         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
1860      }
1861      return sb.toString();
1862   }
1863
1864   /**
1865    * Converts the specified object to a URI.
1866    *
1867    * @param o The object to convert to a URI.
1868    * @return A new URI, or the same object if the object was already a URI, or
1869    */
1870   public static URI toURI(Object o) {
1871      if (o == null || o instanceof URI)
1872         return (URI)o;
1873      try {
1874         return new URI(o.toString());
1875      } catch (URISyntaxException e) {
1876         throw asRuntimeException(e);
1877      }
1878   }
1879
1880   /**
1881    * Converts the specified byte array to a UTF-8 string.
1882    *
1883    * @param b The byte array to convert.
1884    * @return The UTF-8 string representation, or <jk>null</jk> if the array is <jk>null</jk>.
1885    */
1886   public static String toUtf8(byte[] b) {
1887      return b == null ? null : new String(b, IOUtils.UTF8);
1888   }
1889
1890   /**
1891    * Converts the contents of the specified input stream to a UTF-8 string.
1892    *
1893    * @param is The input stream to convert.
1894    * @return The UTF-8 string representation of the input stream contents, or <jk>null</jk> if the stream is <jk>null</jk>.
1895    */
1896   public static String toUtf8(InputStream is) {
1897      return safe(()->is == null ? null : new String(readBytes(is), IOUtils.UTF8));
1898   }
1899
1900   /**
1901    * Same as {@link String#trim()} but prevents <c>NullPointerExceptions</c>.
1902    *
1903    * @param s The string to trim.
1904    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
1905    */
1906   public static String trim(String s) {
1907      if (s == null)
1908         return null;
1909      return s.trim();
1910   }
1911
1912   /**
1913    * Trims whitespace characters from the end of the specified string.
1914    *
1915    * @param s The string to trim.
1916    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
1917    */
1918   public static String trimEnd(String s) {
1919      if (s != null)
1920         while (Utils.isNotEmpty(s) && isWhitespace(s.charAt(s.length()-1)))
1921            s = s.substring(0, s.length()-1);
1922      return s;
1923   }
1924
1925   /**
1926    * Trims <js>'/'</js> characters from the beginning of the specified string.
1927    *
1928    * @param s The string to trim.
1929    * @return A new trimmed string, or the same string if no trimming was necessary.
1930    */
1931   public static String trimLeadingSlashes(String s) {
1932      if (s == null)
1933         return null;
1934      while (Utils.isNotEmpty(s) && s.charAt(0) == '/')
1935         s = s.substring(1);
1936      return s;
1937   }
1938
1939   /**
1940    * Trims <js>'/'</js> characters from both the start and end of the specified string.
1941    *
1942    * @param s The string to trim.
1943    * @return A new trimmed string, or the same string if no trimming was necessary.
1944    */
1945   public static String trimSlashes(String s) {
1946      if (s == null)
1947         return null;
1948      if (s.isEmpty())
1949         return s;
1950      while (endsWith(s, '/'))
1951         s = s.substring(0, s.length()-1);
1952      while (Utils.isNotEmpty(s) && s.charAt(0) == '/')  // NOSONAR - NPE not possible here.
1953         s = s.substring(1);
1954      return s;
1955   }
1956
1957   /**
1958    * Trims <js>'/'</js> and space characters from both the start and end of the specified string.
1959    *
1960    * @param s The string to trim.
1961    * @return A new trimmed string, or the same string if no trimming was necessary.
1962    */
1963   public static String trimSlashesAndSpaces(String s) {
1964      if (s == null)
1965         return null;
1966      while (Utils.isNotEmpty(s) && (s.charAt(s.length()-1) == '/' || isWhitespace(s.charAt(s.length()-1))))
1967         s = s.substring(0, s.length()-1);
1968      while (Utils.isNotEmpty(s) && (s.charAt(0) == '/' || isWhitespace(s.charAt(0))))
1969         s = s.substring(1);
1970      return s;
1971   }
1972
1973   /**
1974    * Trims whitespace characters from the beginning of the specified string.
1975    *
1976    * @param s The string to trim.
1977    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
1978    */
1979   public static String trimStart(String s) {
1980      if (s != null)
1981         while (Utils.isNotEmpty(s) && isWhitespace(s.charAt(0)))
1982            s = s.substring(1);
1983      return s;
1984   }
1985
1986   /**
1987    * Trims <js>'/'</js> characters from the end of the specified string.
1988    *
1989    * @param s The string to trim.
1990    * @return A new trimmed string, or the same string if no trimming was necessary.
1991    */
1992   public static String trimTrailingSlashes(String s) {
1993      if (s == null)
1994         return null;
1995      while (endsWith(s, '/'))
1996         s = s.substring(0, s.length()-1);
1997      return s;
1998   }
1999
2000   /**
2001    * Removes escape characters from the specified characters.
2002    *
2003    * @param s The string to remove escape characters from.
2004    * @param escaped The characters escaped.
2005    * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>.
2006    */
2007   public static String unEscapeChars(String s, AsciiSet escaped) {
2008      if (s == null || s.isEmpty())
2009         return s;
2010      var count = 0;
2011      for (var i = 0; i < s.length(); i++)
2012         if (escaped.contains(s.charAt(i)))
2013            count++;
2014      if (count == 0)
2015         return s;
2016      var sb = new StringBuffer(s.length()-count);
2017      for (var i = 0; i < s.length(); i++) {
2018         var c = s.charAt(i);
2019
2020         if (c == '\\') {
2021            if (i+1 != s.length()) {  // NOSONAR - Intentional.
2022               var c2 = s.charAt(i+1);
2023               if (escaped.contains(c2)) {
2024                  i++;  // NOSONAR - Intentional.
2025               } else if (c2 == '\\') {
2026                  sb.append('\\');
2027                  i++;  // NOSONAR - Intentional.
2028               }
2029            }
2030         }
2031         sb.append(s.charAt(i));
2032      }
2033      return sb.toString();
2034   }
2035
2036   /**
2037    * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for the specified character.
2038    *
2039    * @param c The character to create a sequence for.
2040    * @return An escaped-unicode sequence.
2041    */
2042   public static String unicodeSequence(char c) {
2043      var sb = new StringBuilder(6);
2044      sb.append('\\').append('u');
2045      for (var cc : toHex4(c))
2046         sb.append(cc);
2047      return sb.toString();
2048   }
2049
2050   /**
2051    * Decodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
2052    *
2053    * @param s The string to decode.
2054    * @return The decoded string, or <jk>null</jk> if input is <jk>null</jk>.
2055    */
2056   public static String urlDecode(String s) {
2057
2058      if (s == null)
2059         return s;
2060
2061      var needsDecode = false;
2062      for (var i = 0; i < s.length() && ! needsDecode; i++) {
2063         var c = s.charAt(i);
2064         if (c == '+' || c == '%')
2065            needsDecode = true;
2066      }
2067
2068      if (needsDecode) {
2069         try {
2070            return URLDecoder.decode(s, "UTF-8");
2071         } catch (UnsupportedEncodingException e) {/* Won't happen */}
2072      }
2073      return s;
2074   }
2075
2076   /**
2077    * Encodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
2078    *
2079    * @param s The string to encode.
2080    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
2081    */
2082   public static String urlEncode(String s) {
2083
2084      if (s == null)
2085         return null;
2086
2087      var needsEncode = false;
2088
2089      for (var i = 0; i < s.length() && ! needsEncode; i++)
2090         needsEncode |= (! unencodedChars.contains(s.charAt(i)));
2091
2092      if (needsEncode) {
2093         try {
2094            return URLEncoder.encode(s, "UTF-8");
2095         } catch (UnsupportedEncodingException e) {/* Won't happen */}
2096      }
2097
2098      return s;
2099   }
2100
2101   /**
2102    * Same as {@link #urlEncode(String)} except only escapes characters that absolutely need to be escaped.
2103    *
2104    * @param s The string to escape.
2105    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
2106    */
2107   public static String urlEncodeLax(String s) {
2108      if (s == null)
2109         return null;
2110      var needsEncode = false;
2111      for (var i = 0; i < s.length() && ! needsEncode; i++)
2112         needsEncode |= (! unencodedCharsLax.contains(s.charAt(i)));
2113      if (needsEncode) {
2114         var sb = new StringBuilder(s.length()*2);
2115         for (var i = 0; i < s.length(); i++) {
2116            var c = s.charAt(i);
2117            if (unencodedCharsLax.contains(c))
2118               sb.append(c);
2119            else if (c == ' ')
2120               sb.append("+");
2121            else if (c <= 127)
2122               sb.append('%').append(toHex2(c));
2123            else
2124               try {
2125                  sb.append(URLEncoder.encode(""+c, "UTF-8"));  // Yuck.
2126               } catch (UnsupportedEncodingException e) {
2127                  // Not possible.
2128               }
2129         }
2130         s = sb.toString();
2131      }
2132      return s;
2133   }
2134
2135   /**
2136    * Similar to {@link URLEncoder#encode(String, String)} but doesn't encode <js>"/"</js> characters.
2137    *
2138    * @param o The object to encode.
2139    * @return The URL encoded string, or <jk>null</jk> if the object was null.
2140    */
2141   public static String urlEncodePath(Object o) {
2142
2143      if (o == null)
2144         return null;
2145
2146      var s = Utils.s(o);
2147
2148      var needsEncode = false;
2149      for (var i = 0; i < s.length() && ! needsEncode; i++)
2150         needsEncode = URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i));
2151      if (! needsEncode)
2152         return s;
2153
2154      var sb = new StringBuilder();
2155      var caw = new CharArrayWriter();
2156      var caseDiff = ('a' - 'A');
2157
2158      for (var i = 0; i < s.length();) {
2159         var c = s.charAt(i);
2160         if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) {
2161            sb.append(c);
2162            i++;  // NOSONAR - Intentional.
2163         } else {
2164            if (c == ' ') {
2165               sb.append('+');
2166               i++;  // NOSONAR - Intentional.
2167            } else {
2168               do {
2169                  caw.write(c);
2170                  if (c >= 0xD800 && c <= 0xDBFF) {
2171                     if ((i+1) < s.length()) {  // NOSONAR - Intentional.
2172                        int d = s.charAt(i+1);
2173                        if (d >= 0xDC00 && d <= 0xDFFF) {
2174                           caw.write(d);
2175                           i++;  // NOSONAR - Intentional.
2176                        }
2177                     }
2178                  }
2179                  i++;  // NOSONAR - Intentional.
2180               } while (i < s.length() && !URL_ENCODE_PATHINFO_VALIDCHARS.contains((c = s.charAt(i))));   // NOSONAR - Intentional.
2181
2182               caw.flush();
2183               var s2 = new String(caw.toCharArray());
2184               var ba = s2.getBytes(IOUtils.UTF8);
2185               for (var element : ba) {
2186                  sb.append('%');
2187                  var ch = forDigit((element >> 4) & 0xF, 16);
2188                  if (isLetter(ch)) {
2189                     ch -= caseDiff;
2190                  }
2191                  sb.append(ch);
2192                  ch = forDigit(element & 0xF, 16);
2193                  if (isLetter(ch)) {
2194                     ch -= caseDiff;
2195                  }
2196                  sb.append(ch);
2197               }
2198               caw.reset();
2199            }
2200         }
2201      }
2202      return sb.toString();
2203   }
2204
2205   /**
2206    * Constructor.
2207    */
2208   protected StringUtils() {}
2209}