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

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


The following commit(s) were added to refs/heads/master by this push:
     new 7921b94c5 Test modernization, clean up utility classes.
7921b94c5 is described below

commit 7921b94c53dad4fd9350c9c422546762556da83d
Author: James Bognar <[email protected]>
AuthorDate: Mon Aug 25 16:37:41 2025 -0400

    Test modernization, clean up utility classes.
---
 .../apache/juneau/common/internal/ArgUtils.java    |   48 +-
 .../org/apache/juneau/common/internal/IOUtils.java |   25 +-
 .../juneau/common/internal/PathReaderBuilder.java  |   11 +-
 .../apache/juneau/common/internal/StringUtils.java | 3100 ++++++++++----------
 .../apache/juneau/common/internal/SystemUtils.java |   12 +-
 .../juneau/common/internal/ThrowableUtils.java     |   96 +-
 .../org/apache/juneau/common/internal/Utils.java   |    1 -
 7 files changed, 1637 insertions(+), 1656 deletions(-)

diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ArgUtils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ArgUtils.java
index 300209edb..0a7c79d7b 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ArgUtils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ArgUtils.java
@@ -20,55 +20,50 @@ import java.text.*;
 public class ArgUtils {
 
        /**
-        * Constructor.
-        */
-       protected ArgUtils() {}
-
-       /**
-        * Throws an {@link IllegalArgumentException} if the specified argument 
is <jk>null</jk>.
+        * Throws an {@link IllegalArgumentException} if the specified 
expression is <jk>false</jk>.
         *
         * <h5 class='section'>Example:</h5>
         * <p class='bjava'>
         *      <jk>import static</jk> org.apache.juneau.internal.ArgUtils.*;
         *
-        *      <jk>public</jk> String setFoo(String <jv>foo</jv>) {
-        *              <jsm>assertArgNotNull</jsm>(<js>"foo"</js>, 
<jv>foo</jv>);
+        *      <jk>public</jk> String setFoo(List&lt;String&gt; <jv>foo</jv>) {
+        *              <jsm>assertArg</jsm>(<jv>foo</jv> != <jk>null</jk> 
&amp;&amp; ! <jv>foo</jv>.isEmpty(), <js>"'foo' cannot be null or empty."</js>);
         *              ...
         *      }
         * </p>
         *
-        * @param <T> The argument data type.
-        * @param name The argument name.
-        * @param o The object to check.
-        * @return The same argument.
+        * @param expression The boolean expression to check.
+        * @param msg The exception message.
+        * @param args The exception message args.
         * @throws IllegalArgumentException Constructed exception.
         */
-       public static final <T> T assertArgNotNull(String name, T o) throws 
IllegalArgumentException {
-               assertArg(o != null, "Argument ''{0}'' cannot be null.", name);
-               return o;
+       public static final void assertArg(boolean expression, String msg, 
Object...args) throws IllegalArgumentException {
+               if (! expression)
+                       throw new 
IllegalArgumentException(MessageFormat.format(msg, args));
        }
 
        /**
-        * Throws an {@link IllegalArgumentException} if the specified 
expression is <jk>false</jk>.
+        * Throws an {@link IllegalArgumentException} if the specified argument 
is <jk>null</jk>.
         *
         * <h5 class='section'>Example:</h5>
         * <p class='bjava'>
         *      <jk>import static</jk> org.apache.juneau.internal.ArgUtils.*;
         *
-        *      <jk>public</jk> String setFoo(List&lt;String&gt; <jv>foo</jv>) {
-        *              <jsm>assertArg</jsm>(<jv>foo</jv> != <jk>null</jk> 
&amp;&amp; ! <jv>foo</jv>.isEmpty(), <js>"'foo' cannot be null or empty."</js>);
+        *      <jk>public</jk> String setFoo(String <jv>foo</jv>) {
+        *              <jsm>assertArgNotNull</jsm>(<js>"foo"</js>, 
<jv>foo</jv>);
         *              ...
         *      }
         * </p>
         *
-        * @param expression The boolean expression to check.
-        * @param msg The exception message.
-        * @param args The exception message args.
+        * @param <T> The argument data type.
+        * @param name The argument name.
+        * @param o The object to check.
+        * @return The same argument.
         * @throws IllegalArgumentException Constructed exception.
         */
-       public static final void assertArg(boolean expression, String msg, 
Object...args) throws IllegalArgumentException {
-               if (! expression)
-                       throw new 
IllegalArgumentException(MessageFormat.format(msg, args));
+       public static final <T> T assertArgNotNull(String name, T o) throws 
IllegalArgumentException {
+               assertArg(o != null, "Argument ''{0}'' cannot be null.", name);
+               return o;
        }
 
        /**
@@ -87,4 +82,9 @@ public class ArgUtils {
                                throw new IllegalArgumentException("Arg 
"+name+" did not have arg of type "+type.getName()+" at index "+i+": 
"+value[i].getName());
                return (Class<E>[])value;
        }
+
+       /**
+        * Constructor.
+        */
+       protected ArgUtils() {}
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/IOUtils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/IOUtils.java
index cf2770d9c..bd342d473 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/IOUtils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/IOUtils.java
@@ -12,25 +12,12 @@
 // 
***************************************************************************************************************************
 package org.apache.juneau.common.internal;
 
-import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.StringReader;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Scanner;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
+import java.io.*;
+import java.nio.charset.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.atomic.*;
+import java.util.function.*;
 
 /**
  * Various I/O related utility methods.
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/PathReaderBuilder.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/PathReaderBuilder.java
index 2811d954f..bab412bf3 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/PathReaderBuilder.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/PathReaderBuilder.java
@@ -14,14 +14,9 @@ package org.apache.juneau.common.internal;
 
 import static java.util.Optional.*;
 
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringReader;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.io.*;
+import java.nio.charset.*;
+import java.nio.file.*;
 
 /**
  * Utility class for creating {@link Path}-based {@link Reader} objects.
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/StringUtils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/StringUtils.java
index f6c8b6343..4be75bb47 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/StringUtils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/StringUtils.java
@@ -12,11 +12,11 @@
 // 
***************************************************************************************************************************
 package org.apache.juneau.common.internal;
 
+import static java.lang.Character.*;
+import static java.nio.charset.StandardCharsets.*;
 import static org.apache.juneau.common.internal.ArgUtils.*;
 import static org.apache.juneau.common.internal.IOUtils.*;
 import static org.apache.juneau.common.internal.ThrowableUtils.*;
-import static java.nio.charset.StandardCharsets.*;
-import static java.lang.Character.*;
 import static org.apache.juneau.common.internal.Utils.*;
 
 import java.io.*;
@@ -40,22 +40,17 @@ import jakarta.xml.bind.*;
  */
 public final class StringUtils {
 
-       /**
-        * Constructor.
-        */
-       protected StringUtils() {}
-
        /**
         * Predicate check to filter out null and empty strings.
         */
        public static final Predicate<String> NOT_EMPTY = Utils::isNotEmpty;
 
        private static final AsciiSet numberChars = 
AsciiSet.of("-xX.+-#pP0123456789abcdefABCDEF");
+
        private static final AsciiSet firstNumberChars 
=AsciiSet.of("+-.#0123456789");
        private static final AsciiSet octChars = AsciiSet.of("01234567");
        private static final AsciiSet decChars = AsciiSet.of("0123456789");
        private static final AsciiSet hexChars = 
AsciiSet.of("0123456789abcdefABCDEF");
-
        // Maps 6-bit nibbles to BASE64 characters.
        private static final char[] base64m1 = 
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
 
@@ -77,229 +72,233 @@ public final class StringUtils {
 
        // Maps BASE64 characters to 6-bit nibbles.
        private static final byte[] base64m2 = new byte[128];
+
        static {
                for (var i = 0; i < 64; i++)
                        base64m2[base64m1[i]] = (byte)i;
        }
-
        private static final Random RANDOM = new Random();
 
-       /**
-        * Parses a number from the specified string.
-        *
-        * @param s The string to parse the number from.
-        * @param type
-        *      The number type to created.
-        *      Can be any of the following:
-        *      <ul>
-        *              <li> Integer
-        *              <li> Double
-        *              <li> Float
-        *              <li> Long
-        *              <li> Short
-        *              <li> Byte
-        *              <li> BigInteger
-        *              <li> BigDecimal
-        *      </ul>
-        *      If <jk>null</jk> or <c>Number</c>, uses the best guess.
-        * @return The parsed number, or <jk>null</jk> if the string was null.
-        */
-       public static Number parseNumber(String s, Class<? extends Number> 
type) {
-               if (s == null)
-                       return null;
-               if (s.isEmpty())
-                       s = "0";
-               if (type == null)
-                       type = Number.class;
+       private static final Pattern fpRegex = Pattern.compile(
+               
"[+-]?(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
+       );
 
-               // Determine the data type if it wasn't specified.
-               var isAutoDetect = (type == Number.class);
-               var isDecimal = false;
-               if (isAutoDetect) {
-                       // If we're auto-detecting, then we use either an 
Integer, Long, or Double depending on how
-                       // long the string is.
-                       // An integer range is -2,147,483,648 to 2,147,483,647
-                       // An long range is -9,223,372,036,854,775,808 to 
+9,223,372,036,854,775,807
-                       isDecimal = isDecimal(s);
-                       if (isDecimal) {
-                               if (s.length() > 20)
-                                       type = Double.class;
-                               else if (s.length() >= 10)
-                                       type = Long.class;
-                               else
-                                       type = Integer.class;
-                       }
-                       else if (isFloat(s))
-                               type = Double.class;
-                       else
-                               throw new NumberFormatException(s);
-               }
+       static final Map<Character,AsciiSet> ESCAPE_SETS = new 
ConcurrentHashMap<>();
 
-               if (type == Double.class || type == Double.TYPE) {
-                       var d = Double.valueOf(s);
-                       var f = Float.valueOf(s);
-                       if (isAutoDetect && (!isDecimal) && 
d.toString().equals(f.toString()))
-                               return f;
-                       return d;
-               }
-               if (type == Float.class || type == Float.TYPE)
-                       return Float.valueOf(s);
-               if (type == BigDecimal.class)
-                       return new BigDecimal(s);
-               if (type == Long.class || type == Long.TYPE || type == 
AtomicLong.class) {
-                       try {
-                               var l = Long.decode(s);
-                               if (type == AtomicLong.class)
-                                       return new AtomicLong(l);
-                               if (isAutoDetect && l >= Integer.MIN_VALUE && l 
<= Integer.MAX_VALUE) {
-                                       // This occurs if the string is 10 
characters long but is still a valid integer value.
-                                       return l.intValue();
-                               }
-                               return l;
-                       } catch (NumberFormatException e) {
-                               if (isAutoDetect) {
-                                       // This occurs if the string is 20 
characters long but still falls outside the range of a valid long.
-                                       return Double.valueOf(s);
-                               }
-                               throw e;
-                       }
-               }
-               if (type == Integer.class || type == Integer.TYPE)
-                       return Integer.decode(s);
-               if (type == Short.class || type == Short.TYPE)
-                       return Short.decode(s);
-               if (type == Byte.class || type == Byte.TYPE)
-                       return Byte.decode(s);
-               if (type == BigInteger.class)
-                       return new BigInteger(s);
-               if (type == AtomicInteger.class)
-                       return new AtomicInteger(Integer.decode(s));
-               throw new NumberFormatException("Unsupported Number type: 
"+type.getName());
+       static final AsciiSet MAP_ESCAPE_SET = AsciiSet.of(",=\\");
+
+       static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.of("\"'\\");
+
+       private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+       private static final Map<Class<?>,Function<Object,String>> 
PRIMITIVE_ARRAY_STRINGIFIERS = new HashMap<>();
+
+       static {
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(boolean[].class, x -> 
Arrays.toString((boolean[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(byte[].class, x -> 
Arrays.toString((byte[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(char[].class, x -> 
Arrays.toString((char[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(double[].class, x -> 
Arrays.toString((double[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(float[].class, x -> 
Arrays.toString((float[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(int[].class, x -> 
Arrays.toString((int[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(long[].class, x -> 
Arrays.toString((long[])x));
+               PRIMITIVE_ARRAY_STRINGIFIERS.put(short[].class, x -> 
Arrays.toString((short[])x));
        }
 
-       private static final Pattern fpRegex = Pattern.compile(
-               
"[+-]?(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
-       );
+       private static final char[] HEX = "0123456789ABCDEF".toCharArray();
+
+       private static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS =
+               
AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.*/()").build();
+       private static final AsciiSet URI_CHARS = 
AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0','9').range('A','Z').range('a','z').build();
 
        /**
-        * Converts a <c>String</c> to a <c>Character</c>
+        * Abbreviates a String using ellipses.
         *
-        * @param o The string to convert.
-        * @return The first character of the string if the string is of length 
0, or <jk>null</jk> if the string is <jk>null</jk> or empty.
+        * @param in The input string.
+        * @param length The max length of the resulting string.
+        * @return The abbreviated string.
         */
-       public static Character parseCharacter(Object o) {
-               if (o == null)
-                       return null;
-               var s = o.toString();
-               if (s.isEmpty())
-                       return null;
-               if (s.length() == 1)
-                       return s.charAt(0);
-               throw new IllegalArgumentException("Invalid character: '" + s + 
"'");
+       public static String abbreviate(String in, int length) {
+               if (in == null || in.length() <= length || in.length() <= 3)
+                       return in;
+               return in.substring(0, length-3) + "...";
        }
 
-       /**
-        * Returns <jk>true</jk> if this string can be parsed by {@link 
#parseNumber(String, Class)}.
-        *
-        * @param s The string to check.
-        * @return <jk>true</jk> if this string can be parsed without causing 
an exception.
-        */
-       public static boolean isNumeric(String s) {
-               if (s == null || s.isEmpty() || ! 
isFirstNumberChar(s.charAt(0)))
-                       return false;
-               return isDecimal(s) || isFloat(s);
+       private static StringBuilder append(StringBuilder sb, String in) {
+               if (sb == null)
+                       return new StringBuilder(in);
+               sb.append(in);
+               return sb;
+       }
+
+       private static List<Object> arrayAsList(Object array) {
+               if (array.getClass().getComponentType().isPrimitive()) {
+                       var l = new ArrayList<>(Array.getLength(array));
+                       for (var i = 0; i < Array.getLength(array); i++)
+                               l.add(Array.get(array, i));
+                       return l;
+               }
+               return Arrays.asList((Object[])array);
        }
 
        /**
-        * Returns <jk>true</jk> if the specified character is a valid first 
character for a number.
+        * BASE64-decodes the specified string.
         *
-        * @param c The character to test.
-        * @return <jk>true</jk> if the specified character is a valid first 
character for a number.
+        * @param in The BASE-64 encoded string.
+        * @return The decoded byte array, or null if the input was 
<jk>null</jk>.
         */
-       public static boolean isFirstNumberChar(char c) {
-               return firstNumberChars.contains(c);
+       public static byte[] base64Decode(String in) {
+               if (in == null)
+                       return null;  // NOSONAR - Intentional.
+
+               var bIn = in.getBytes(IOUtils.UTF8);
+
+               assertArg(bIn.length % 4 == 0, "Invalid BASE64 string length.  
Must be multiple of 4.");
+
+               // Strip out any trailing '=' filler characters.
+               var inLength = bIn.length;
+               while (inLength > 0 && bIn[inLength - 1] == '=')
+                       inLength--;
+
+               var outLength = (inLength * 3) / 4;
+               var out = new byte[outLength];
+               var iIn = 0;
+               var iOut = 0;
+               while (iIn < inLength) {
+                       var i0 = bIn[iIn++];
+                       var i1 = bIn[iIn++];
+                       var i2 = iIn < inLength ? bIn[iIn++] : 'A';
+                       var i3 = iIn < inLength ? bIn[iIn++] : 'A';
+                       var b0 = base64m2[i0];
+                       var b1 = base64m2[i1];
+                       var b2 = base64m2[i2];
+                       int b3 = base64m2[i3];
+                       var o0 = (b0 << 2) | (b1 >>> 4);
+                       var o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);
+                       var o2 = ((b2 & 3) << 6) | b3;
+                       out[iOut++] = (byte)o0;
+                       if (iOut < outLength)
+                               out[iOut++] = (byte)o1;
+                       if (iOut < outLength)
+                               out[iOut++] = (byte)o2;
+               }
+               return out;
        }
 
        /**
-        * Returns <jk>true</jk> if the specified string is a floating point 
number.
+        * Shortcut for calling <c>base64Decode(String)</c> and converting the 
result to a UTF-8 encoded string.
         *
-        * @param s The string to check.
-        * @return <jk>true</jk> if the specified string is a floating point 
number.
+        * @param in The BASE-64 encoded string to decode.
+        * @return The decoded string.
         */
-       public static boolean isFloat(String s) {
-               if (s == null || s.isEmpty())
-                       return false;
-               if (! firstNumberChars.contains(s.charAt(0)))
-                       return (s.equals("NaN") || s.equals("Infinity"));
-               var i = 0;
-               var length = s.length();
-               var c = s.charAt(0);
-               if (c == '+' || c == '-')
-                       i++;
-               if (i == length)
-                       return false;
-               c = s.charAt(i);
-               if (c == '.' || decChars.contains(c)) {
-                       return fpRegex.matcher(s).matches();
-               }
-               return false;
+       public static String base64DecodeToString(String in) {
+               var b = base64Decode(in);
+               if (b == null)
+                       return null;
+               return new String(b, IOUtils.UTF8);
        }
 
        /**
-        * Returns <jk>true</jk> if the specified string is numeric.
+        * BASE64-encodes the specified byte array.
         *
-        * @param s The string to check.
-        * @return <jk>true</jk> if the specified string is numeric.
+        * @param in The input byte array to convert.
+        * @return The byte array converted to a BASE-64 encoded string.
         */
-       public static boolean isDecimal(String s) {
-               if (s == null || s.isEmpty() || ! 
firstNumberChars.contains(s.charAt(0)))
-                       return false;
-               var i = 0;
-               var length = s.length();
-               var c = s.charAt(0);
-               var isPrefixed = false;
-               if (c == '+' || c == '-') {
-                       isPrefixed = true;
-                       i++;
-               }
-               if (i == length)
-                       return false;
-               c = s.charAt(i++);
-               if (c == '0' && length > (isPrefixed ? 2 : 1)) {
-                       c = s.charAt(i++);
-                       if (c == 'x' || c == 'X') {
-                               for (int j = i; j < length; j++) {
-                                       if (! hexChars.contains(s.charAt(j)))
-                                               return false;
-                               }
-                       } else if (octChars.contains(c)) {
-                               for (int j = i; j < length; j++)
-                                       if (! octChars.contains(s.charAt(j)))
-                                               return false;
-                       } else {
-                               return false;
-                       }
-               } else if (c == '#') {
-                       for (int j = i; j < length; j++) {
-                               if (! hexChars.contains(s.charAt(j)))
-                                       return false;
-                       }
-               } else if (decChars.contains(c)) {
-                       for (int j = i; j < length; j++)
-                               if (! decChars.contains(s.charAt(j)))
-                                       return false;
-               } else {
-                       return false;
+       public static String base64Encode(byte[] in) {
+               if (in == null)
+                       return null;
+               var outLength = (in.length * 4 + 2) / 3;   // Output length 
without padding
+               var out = new char[((in.length + 2) / 3) * 4];  // Length 
includes padding.
+               var iIn = 0;
+               var iOut = 0;
+               while (iIn < in.length) {
+                       var i0 = in[iIn++] & 0xff;
+                       var i1 = iIn < in.length ? in[iIn++] & 0xff : 0;
+                       var i2 = iIn < in.length ? in[iIn++] & 0xff : 0;
+                       var o0 = i0 >>> 2;
+                       var o1 = ((i0 & 3) << 4) | (i1 >>> 4);
+                       var o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
+                       var o3 = i2 & 0x3F;
+                       out[iOut++] = base64m1[o0];
+                       out[iOut++] = base64m1[o1];
+                       out[iOut] = iOut < outLength ? base64m1[o2] : '=';
+                       iOut++;
+                       out[iOut] = iOut < outLength ? base64m1[o3] : '=';
+                       iOut++;
                }
-               return true;
+               return new String(out);
        }
 
-       static AsciiSet getEscapeSet(char c) {
-               return ESCAPE_SETS.computeIfAbsent(c, key -> 
AsciiSet.create().chars(key, '\\').build());
+       /**
+        * Shortcut for calling 
<code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code>
+        *
+        * @param in The input string to convert.
+        * @return The string converted to BASE-64 encoding.
+        */
+       public static String base64EncodeToString(String in) {
+               if (in == null)
+                       return null;
+               return base64Encode(in.getBytes(IOUtils.UTF8));
        }
-       static final Map<Character,AsciiSet> ESCAPE_SETS = new 
ConcurrentHashMap<>();
 
-       static final AsciiSet MAP_ESCAPE_SET = AsciiSet.of(",=\\");
+       /**
+        * Returns the character at the specified index in the string without 
throwing exceptions.
+        *
+        * @param s The string.
+        * @param i The index position.
+        * @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>.
+        */
+       public static char charAt(String s, int i) {
+               if (s == null || i < 0 || i >= s.length())
+                       return 0;
+               return s.charAt(i);
+       }
+
+       /**
+        * Compares two strings, but gracefully handles <jk>nulls</jk>.
+        *
+        * @param s1 The first string.
+        * @param s2 The second string.
+        * @return The same as {@link String#compareTo(String)}.
+        */
+       public static int compare(String s1, String s2) {
+               if (s1 == null && s2 == null)
+                       return 0;
+               if (s1 == null)
+                       return Integer.MIN_VALUE;
+               if (s2 == null)
+                       return Integer.MAX_VALUE;
+               return s1.compareTo(s2);
+       }
+
+       /**
+        * Converts string into a GZipped input stream.
+        *
+        * @param contents The contents to compress.
+        * @return The input stream converted to GZip.
+        * @throws Exception Exception occurred.
+        */
+       public static byte[] compress(String contents) throws Exception {
+               var baos = new ByteArrayOutputStream(contents.length()>>1);
+               try (var gos = new GZIPOutputStream(baos)) {
+                       gos.write(contents.getBytes());
+                       gos.finish();
+                       gos.flush();
+               }
+               return baos.toByteArray();
+       }
+
+       /**
+        * Same as {@link String#contains(CharSequence)} except returns 
<jk>null</jk> if the value is null.
+        *
+        * @param value The string to check.
+        * @param substring The value to check for.
+        * @return <jk>true</jk> if the value contains the specified substring.
+        */
+       public static boolean contains(String value, CharSequence substring) {
+               return value != null && value.contains(substring);
+       }
 
        /**
         * Returns <jk>true</jk> if the specified string contains any of the 
specified characters.
@@ -322,70 +321,33 @@ public final class StringUtils {
                return false;
        }
 
-       static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.of("\"'\\");
-
-       /**
-        * Removes escape characters from the specified characters.
-        *
-        * @param s The string to remove escape characters from.
-        * @param escaped The characters escaped.
-        * @return A new string if characters were removed, or the same string 
if not or if the input was <jk>null</jk>.
-        */
-       public static String unEscapeChars(String s, AsciiSet escaped) {
-               if (s == null || s.isEmpty())
-                       return s;
-               var count = 0;
-               for (var i = 0; i < s.length(); i++)
-                       if (escaped.contains(s.charAt(i)))
-                               count++;
-               if (count == 0)
-                       return s;
-               var sb = new StringBuffer(s.length()-count);
-               for (var i = 0; i < s.length(); i++) {
-                       var c = s.charAt(i);
-
-                       if (c == '\\') {
-                               if (i+1 != s.length()) {  // NOSONAR - 
Intentional.
-                                       var c2 = s.charAt(i+1);
-                                       if (escaped.contains(c2)) {
-                                               i++;  // NOSONAR - Intentional.
-                                       } else if (c2 == '\\') {
-                                               sb.append('\\');
-                                               i++;  // NOSONAR - Intentional.
-                                       }
-                               }
-                       }
-                       sb.append(s.charAt(i));
-               }
-               return sb.toString();
+       private static String convertToReadable(Object o) {
+               if (o == null)
+                       return null;
+               if (o instanceof Class)
+                       return ((Class<?>)o).getName();
+               if (o instanceof Method)
+                       return Method.class.cast(o).getName();
+               if (o.getClass().isArray())
+                       return 
arrayAsList(o).stream().map(StringUtils::convertToReadable).collect(Collectors.joining(",
 ", "[", "]"));
+               return o.toString();
        }
 
        /**
-        * Escapes the specified characters in the string.
+        * Counts the number of the specified character in the specified string.
         *
-        * @param s The string with characters to escape.
-        * @param escaped The characters to escape.
-        * @return The string with characters escaped, or the same string if no 
escapable characters were found.
+        * @param s The string to check.
+        * @param c The character to check for.
+        * @return The number of those characters or zero if the string was 
<jk>null</jk>.
         */
-       public static String escapeChars(String s, AsciiSet escaped) {
-               if (s == null || s.isEmpty())
-                       return s;
-
+       public static int countChars(String s, char c) {
                var count = 0;
+               if (s == null)
+                       return count;
                for (var i = 0; i < s.length(); i++)
-                       if (escaped.contains(s.charAt(i)))
+                       if (s.charAt(i) == c)
                                count++;
-               if (count == 0)
-                       return s;
-
-               var sb = new StringBuffer(s.length() + count);
-               for (var i = 0; i < s.length(); i++) {
-                       var c = s.charAt(i);
-                       if (escaped.contains(c))
-                               sb.append('\\');
-                       sb.append(c);
-               }
-               return sb.toString();
+               return count;
        }
 
        /**
@@ -408,42 +370,83 @@ public final class StringUtils {
        }
 
        /**
-        * An efficient method for checking if a string starts with a character.
+        * Converts a GZipped input stream into a string.
         *
-        * @param s The string to check.  Can be <jk>null</jk>.
-        * @param c The character to check for.
-        * @return <jk>true</jk> if the specified string is not <jk>null</jk> 
and starts with the specified character.
+        * @param is The contents to decompress.
+        * @return The string.
+        * @throws Exception Exception occurred.
         */
-       public static boolean startsWith(String s, char c) {
-               if (s != null) {
-                       var i = s.length();
-                       if (i > 0)
-                               return s.charAt(0) == c;
-               }
-               return false;
+       public static String decompress(byte[] is) throws Exception {
+               return read(new GZIPInputStream(new ByteArrayInputStream(is)));
        }
 
        /**
-        * An efficient method for checking if a string ends with a character.
+        * Finds the position where the two strings differ.
         *
-        * @param s The string to check.  Can be <jk>null</jk>.
-        * @param c The character to check for.
-        * @return <jk>true</jk> if the specified string is not <jk>null</jk> 
and ends with the specified character.
+        * @param s1 The first string.
+        * @param s2 The second string.
+        * @return The position where the two strings differ, or <c>-1</c> if 
they're equal.
         */
-       public static boolean endsWith(String s, char c) {
-               if (s != null) {
-                       var i = s.length();
-                       if (i > 0)
-                               return s.charAt(i-1) == c;
+       public static int diffPosition(String s1, String s2) {
+               s1 = emptyIfNull(s1);
+               s2 = emptyIfNull(s2);
+               var i = 0;
+               var len = Math.min(s1.length(), s2.length());
+               while (i < len) {
+                       var j = s1.charAt(i) - s2.charAt(i);
+                       if (j != 0)
+                               return i;
+                       i++;
                }
-               return false;
+               if (i == len && s1.length() == s2.length())
+                       return -1;
+               return i;
        }
 
        /**
-        * Same as {@link #endsWith(String, char)} except check for multiple 
characters.
+        * Finds the position where the two strings differ ignoring case.
         *
-        * @param s The string to check.  Can be <jk>null</jk>.
-        * @param c The characters to check for.
+        * @param s1 The first string.
+        * @param s2 The second string.
+        * @return The position where the two strings differ, or <c>-1</c> if 
they're equal.
+        */
+       public static int diffPositionIc(String s1, String s2) {
+               s1 = emptyIfNull(s1);
+               s2 = emptyIfNull(s2);
+               var i = 0;
+               var len = Math.min(s1.length(), s2.length());
+               while (i < len) {
+                       var j = toLowerCase(s1.charAt(i)) - 
toLowerCase(s2.charAt(i));
+                       if (j != 0)
+                               return i;
+                       i++;
+               }
+               if (i == len && s1.length() == s2.length())
+                       return -1;
+               return i;
+       }
+
+       /**
+        * An efficient method for checking if a string ends with a character.
+        *
+        * @param s The string to check.  Can be <jk>null</jk>.
+        * @param c The character to check for.
+        * @return <jk>true</jk> if the specified string is not <jk>null</jk> 
and ends with the specified character.
+        */
+       public static boolean endsWith(String s, char c) {
+               if (s != null) {
+                       var i = s.length();
+                       if (i > 0)
+                               return s.charAt(i-1) == c;
+               }
+               return false;
+       }
+
+       /**
+        * Same as {@link #endsWith(String, char)} except check for multiple 
characters.
+        *
+        * @param s The string to check.  Can be <jk>null</jk>.
+        * @param c The characters to check for.
         * @return <jk>true</jk> if the specified string is not <jk>null</jk> 
and ends with the specified character.
         */
        public static boolean endsWith(String s, char...c) {
@@ -460,631 +463,601 @@ public final class StringUtils {
        }
 
        /**
-        * Converts the specified number into a 2 hexadecimal characters.
+        * Escapes the specified characters in the string.
         *
-        * @param num The number to convert to hex.
-        * @return A <code><jk>char</jk>[2]</code> containing the specified 
characters.
+        * @param s The string with characters to escape.
+        * @param escaped The characters to escape.
+        * @return The string with characters escaped, or the same string if no 
escapable characters were found.
         */
-       public static char[] toHex2(int num) {
-               if (num < 0 || num > 255)
-                       throw new NumberFormatException("toHex2 can only be 
used on numbers between 0 and 255");
-               var n = new char[2];
-               var a = num%16;
-               n[1] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
-               a = (num/16)%16;
-               n[0] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
-               return n;
-       }
+       public static String escapeChars(String s, AsciiSet escaped) {
+               if (s == null || s.isEmpty())
+                       return s;
 
-       private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+               var count = 0;
+               for (var i = 0; i < s.length(); i++)
+                       if (escaped.contains(s.charAt(i)))
+                               count++;
+               if (count == 0)
+                       return s;
+
+               var sb = new StringBuffer(s.length() + count);
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (escaped.contains(c))
+                               sb.append('\\');
+                       sb.append(c);
+               }
+               return sb.toString();
+       }
 
        /**
-        * Converts the specified byte into a 2 hexadecimal characters.
+        * Returns the first character in the specified string.
         *
-        * @param b The number to convert to hex.
-        * @return A <code><jk>char</jk>[2]</code> containing the specified 
characters.
+        * @param s The string to check.
+        * @return The first character in the string, or <c>0</c> if the string 
is <jk>null</jk> or empty.
         */
-       public static String toHex(byte b) {
-               var c = new char[2];
-               var v = b & 0xFF;
-               c[0] = hexArray[v >>> 4];
-               c[1] = hexArray[v & 0x0F];
-               return new String(c);
+       public static char firstChar(String s) {
+               if (s == null || s.isEmpty())
+                       return 0;
+               return s.charAt(0);
        }
 
        /**
-        * Converts the specified bytes into a readable string.
+        * Returns the first non-null, non-empty string in the list.
         *
-        * @param b The number to convert to hex.
-        * @return A <code><jk>char</jk>[2]</code> containing the specified 
characters.
+        * @param s The strings to test.
+        * @return The first non-empty string in the list, or <jk>null</jk> if 
they were all <jk>null</jk> or empty.
         */
-       public static String toReadableBytes(byte[] b) {
-               var sb = new StringBuilder();
-               for (var b2 : b)
-                       sb.append((b2 < ' ' || b2 > 'z') ? 
String.format("[%02X]", b2) : (char)b2 + "   ");
-               sb.append("\n");
-               for (var b2 : b)
-                       sb.append(String.format("[%02X]", b2));
-               return sb.toString();
+       public static String firstNonEmpty(String...s) {
+               for (var ss : s)
+                       if (Utils.isNotEmpty(ss))
+                               return ss;
+               return null;
        }
 
        /**
-        * Converts the specified number into a 4 hexadecimal characters.
+        * Returns the first non-whitespace character in the string.
         *
-        * @param num The number to convert to hex.
-        * @return A <code><jk>char</jk>[4]</code> containing the specified 
characters.
+        * @param s The string to check.
+        * @return
+        *      The first non-whitespace character, or <c>0</c> if the string 
is <jk>null</jk>, empty, or composed
+        *      of only whitespace.
         */
-       public static char[] toHex4(int num) {
-               var n = new char[4];
-               var a = num%16;
-               n[3] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
-               var base = 16;
-               for (var i = 1; i < 4; i++) {
-                       a = (num/base)%16;
-                       base <<= 4;
-                       n[3-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+       public static char firstNonWhitespaceChar(String s) {
+               if (s != null)
+                       for (var i = 0; i < s.length(); i++)
+                               if (! isWhitespace(s.charAt(i)))
+                                       return s.charAt(i);
+               return 0;
+       }
+
+       private static int firstRealCharacter(String s) {
+               try (var r = new StringReader(s)) {
+                       var c = 0;
+                       while ((c = r.read()) != -1) {
+                               if (! isWhitespace(c)) {
+                                       if (c == '/') {
+                                               skipComments(r);
+                                       } else {
+                                               return c;
+                                       }
+                               }
+                       }
+                       return -1;
+               } catch (Exception e) {
+                       throw asRuntimeException(e);
                }
-               return n;
        }
 
        /**
-        * Converts the specified number into a 8 hexadecimal characters.
+        * Attempts to escape any invalid characters found in a URI.
         *
-        * @param num The number to convert to hex.
-        * @return A <code><jk>char</jk>[8]</code> containing the specified 
characters.
+        * @param in The URI to fix.
+        * @return The fixed URI.
         */
-       public static char[] toHex8(long num) {
-               var n = new char[8];
-               var a = num%16;
-               n[7] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
-               var base = 16;
-               for (var i = 1; i < 8; i++) {
-                       a = (num/base)%16;
-                       base <<= 4;
-                       n[7-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+       public static String fixUrl(String in) {
+
+               if (in == null)
+                       return null;
+
+               StringBuilder sb = null;
+
+               var m = 0;
+
+               for (var i = 0; i < in.length(); i++) {
+                       var c = in.charAt(i);
+                       if (c <= 127 && ! URI_CHARS.contains(c)) {
+                               sb = append(sb, in.substring(m, i));
+                               if (c == ' ')
+                                       sb.append("+");
+                               else
+                                       sb.append('%').append(toHex2(c));
+                               m = i+1;
+                       }
                }
-               return n;
+               if (sb != null) {
+                       sb.append(in.substring(m));
+                       return sb.toString();
+               }
+               return in;
+
        }
 
        /**
-        * Finds the position where the two strings differ.
+        * Similar to {@link MessageFormat#format(String, Object...)} except 
allows you to specify POJO arguments.
         *
-        * @param s1 The first string.
-        * @param s2 The second string.
-        * @return The position where the two strings differ, or <c>-1</c> if 
they're equal.
+        * @param pattern The string pattern.
+        * @param args The arguments.
+        * @return The formatted string.
         */
-       public static int diffPosition(String s1, String s2) {
-               s1 = emptyIfNull(s1);
-               s2 = emptyIfNull(s2);
-               var i = 0;
-               var len = Math.min(s1.length(), s2.length());
-               while (i < len) {
-                       var j = s1.charAt(i) - s2.charAt(i);
-                       if (j != 0)
-                               return i;
-                       i++;
-               }
-               if (i == len && s1.length() == s2.length())
-                       return -1;
-               return i;
+       public static String format(String pattern, Object...args) {
+               if (args == null || args.length == 0)
+                       return pattern;
+               var args2 = new Object[args.length];
+               for (var i = 0; i < args.length; i++)
+                       args2[i] = convertToReadable(args[i]);
+
+               var c = countChars(pattern, '\'');
+               if (c % 2 != 0)
+                       throw new AssertionError("Dangling single quote found 
in pattern: " + pattern);
+
+               return MessageFormat.format(pattern, args2);
        }
 
        /**
-        * Finds the position where the two strings differ ignoring case.
+        * Converts a hexadecimal character string to a byte array.
         *
-        * @param s1 The first string.
-        * @param s2 The second string.
-        * @return The position where the two strings differ, or <c>-1</c> if 
they're equal.
+        * @param hex The string to convert to a byte array.
+        * @return A new byte array.
         */
-       public static int diffPositionIc(String s1, String s2) {
-               s1 = emptyIfNull(s1);
-               s2 = emptyIfNull(s2);
-               var i = 0;
-               var len = Math.min(s1.length(), s2.length());
-               while (i < len) {
-                       var j = toLowerCase(s1.charAt(i)) - 
toLowerCase(s2.charAt(i));
-                       if (j != 0)
-                               return i;
-                       i++;
-               }
-               if (i == len && s1.length() == s2.length())
-                       return -1;
-               return i;
+       public static byte[] fromHex(String hex) {
+               var buff = ByteBuffer.allocate(hex.length()/2);
+               for (var i = 0; i < hex.length(); i+=2)
+                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
+               buff.rewind();
+               return buff.array();
        }
 
        /**
-        * Shortcut for calling 
<code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code>
+        * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 
encoded string.
         *
-        * @param in The input string to convert.
-        * @return The string converted to BASE-64 encoding.
+        * @param hex The hexadecimal string.
+        * @return The UTF-8 string.
         */
-       public static String base64EncodeToString(String in) {
-               if (in == null)
-                       return null;
-               return base64Encode(in.getBytes(IOUtils.UTF8));
+       public static String fromHexToUTF8(String hex) {
+               var buff = ByteBuffer.allocate(hex.length()/2);
+               for (var i = 0; i < hex.length(); i+=2)
+                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
+               buff.rewind();  // Fixes Java 11 issue.
+               return UTF_8.decode(buff).toString();
        }
 
        /**
-        * BASE64-encodes the specified byte array.
+        * Same as {@link #fromHex(String)} except expects spaces between the 
byte strings.
         *
-        * @param in The input byte array to convert.
-        * @return The byte array converted to a BASE-64 encoded string.
+        * @param hex The string to convert to a byte array.
+        * @return A new byte array.
         */
-       public static String base64Encode(byte[] in) {
-               if (in == null)
-                       return null;
-               var outLength = (in.length * 4 + 2) / 3;   // Output length 
without padding
-               var out = new char[((in.length + 2) / 3) * 4];  // Length 
includes padding.
-               var iIn = 0;
-               var iOut = 0;
-               while (iIn < in.length) {
-                       var i0 = in[iIn++] & 0xff;
-                       var i1 = iIn < in.length ? in[iIn++] & 0xff : 0;
-                       var i2 = iIn < in.length ? in[iIn++] & 0xff : 0;
-                       var o0 = i0 >>> 2;
-                       var o1 = ((i0 & 3) << 4) | (i1 >>> 4);
-                       var o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
-                       var o3 = i2 & 0x3F;
-                       out[iOut++] = base64m1[o0];
-                       out[iOut++] = base64m1[o1];
-                       out[iOut] = iOut < outLength ? base64m1[o2] : '=';
-                       iOut++;
-                       out[iOut] = iOut < outLength ? base64m1[o3] : '=';
-                       iOut++;
-               }
-               return new String(out);
+       public static byte[] fromSpacedHex(String hex) {
+               var buff = ByteBuffer.allocate((hex.length()+1)/3);
+               for (var i = 0; i < hex.length(); i+=3)
+                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
+               buff.rewind();
+               return buff.array();
        }
 
        /**
-        * Shortcut for calling <c>base64Decode(String)</c> and converting the 
result to a UTF-8 encoded string.
+        * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 
BC") into a UTF-8 encoded string.
         *
-        * @param in The BASE-64 encoded string to decode.
-        * @return The decoded string.
+        * @param hex The hexadecimal string.
+        * @return The UTF-8 string.
         */
-       public static String base64DecodeToString(String in) {
-               var b = base64Decode(in);
-               if (b == null)
-                       return null;
-               return new String(b, IOUtils.UTF8);
+       public static String fromSpacedHexToUTF8(String hex) {
+               var buff = ByteBuffer.allocate((hex.length()+1)/3);
+               for (var i = 0; i < hex.length(); i+=3)
+                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
+               buff.rewind();  // Fixes Java 11 issue.
+               return UTF_8.decode(buff).toString();
        }
 
        /**
-        * BASE64-decodes the specified string.
+        * Given an absolute URI, returns just the authority portion (e.g. 
<js>"http://hostname:port";</js>)
         *
-        * @param in The BASE-64 encoded string.
-        * @return The decoded byte array, or null if the input was 
<jk>null</jk>.
+        * @param s The URI string.
+        * @return Just the authority portion of the URI.
         */
-       public static byte[] base64Decode(String in) {
-               if (in == null)
-                       return null;  // NOSONAR - Intentional.
+       public static String getAuthorityUri(String s) {  // NOSONAR - False 
positive.
 
-               var bIn = in.getBytes(IOUtils.UTF8);
+               // Use a state machine for maximum performance.
 
-               assertArg(bIn.length % 4 == 0, "Invalid BASE64 string length.  
Must be multiple of 4.");
+               final int
+                       S1 = 1,  // Looking for http
+                       S2 = 2,  // Found http, looking for :
+                       S3 = 3,  // Found :, looking for /
+                       S4 = 4,  // Found /, looking for /
+                       S5 = 5,  // Found /, looking for x
+                       S6 = 6;  // Found x, looking for /
 
-               // Strip out any trailing '=' filler characters.
-               var inLength = bIn.length;
-               while (inLength > 0 && bIn[inLength - 1] == '=')
-                       inLength--;
+               var state = S1;
 
-               var outLength = (inLength * 3) / 4;
-               var out = new byte[outLength];
-               var iIn = 0;
-               var iOut = 0;
-               while (iIn < inLength) {
-                       var i0 = bIn[iIn++];
-                       var i1 = bIn[iIn++];
-                       var i2 = iIn < inLength ? bIn[iIn++] : 'A';
-                       var i3 = iIn < inLength ? bIn[iIn++] : 'A';
-                       var b0 = base64m2[i0];
-                       var b1 = base64m2[i1];
-                       var b2 = base64m2[i2];
-                       int b3 = base64m2[i3];
-                       var o0 = (b0 << 2) | (b1 >>> 4);
-                       var o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);
-                       var o2 = ((b2 & 3) << 6) | b3;
-                       out[iOut++] = (byte)o0;
-                       if (iOut < outLength)
-                               out[iOut++] = (byte)o1;
-                       if (iOut < outLength)
-                               out[iOut++] = (byte)o2;
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (state == S1) {
+                               if (c >= 'a' && c <= 'z')
+                                       state = S2;
+                               else
+                                       return s;
+                       } else if (state == S2) {
+                               if (c == ':')
+                                       state = S3;
+                               else if (c < 'a' || c > 'z')
+                                       return s;
+                       } else if (state == S3) {  // NOSONAR - False positive.
+                               if (c == '/')
+                                       state = S4;
+                               else
+                                       return s;
+                       } else if (state == S4) {
+                               if (c == '/')
+                                       state = S5;
+                               else
+                                       return s;
+                       } else if (state == S5) {
+                               if (c != '/')
+                                       state = S6;
+                               else
+                                       return s;
+                       } else if (state == S6) {
+                               if (c == '/')  // NOSONAR - Intentional.
+                                       return s.substring(0, i);
+                       }
                }
-               return out;
+
+               return s;
        }
 
        /**
-        * Generated a random UUID with the specified number of characters.
+        * Parses a duration string.
         *
         * <p>
-        * Characters are composed of lower-case ASCII letters and numbers only.
+        * Examples:
+        * <ul>
+        *      <li><js>"1000"</js> - 1000 milliseconds.
+        *      <li><js>"10s"</js> - 10 seconds.
+        *      <li><js>"10 sec"</js> - 10 seconds.
+        *      <li><js>"10 seconds"</js> - 10 seconds.
+        * </ul>
         *
         * <p>
-        * This method conforms to the restrictions for hostnames as specified 
in <a class="doclink" href="https://tools.ietf.org/html/rfc952";>RFC 952</a>
-        * Since each character has 36 possible values, the square 
approximation formula for the number of generated IDs
-        * that would produce a 50% chance of collision is:
-        * <c>sqrt(36^N)</c>.
-        * Dividing this number by 10 gives you an approximation of the number 
of generated IDs needed to produce a
-        * &lt;1% chance of collision.
+        * Use any of the following suffixes:
+        * <ul>
+        *      <li>None (time in milliseconds).
+        *      
<li><js>"s"</js>/<js>"sec"</js>/<js>"second"</js>/<js>"seconds"</js>
+        *      
<li><js>"m"</js>/<js>"min"</js>/<js>"minutes"</js>/<js>"seconds"</js>
+        *      <li><js>"h"</js>/<js>"hour"</js>/<js>"hours"</js>
+        *      <li><js>"d"</js>/<js>"day"</js>/<js>"days"</js>
+        *      <li><js>"w"</js>/<js>"week"</js>/<js>"weeks"</js>
+        * </ul>
         *
         * <p>
-        * For example, given 5 characters, the number of generated IDs need to 
produce a &lt;1% chance of collision would
-        * be:
-        * <c>sqrt(36^5)/10=777</c>
+        * Suffixes are case-insensitive.
+        * <br>Whitespace is ignored.
         *
-        * @param numchars The number of characters in the generated UUID.
-        * @return A new random UUID.
+        * @param s The string to parse.
+        * @return
+        *      The time in milliseconds, or <c>-1</c> if the string is empty 
or <jk>null</jk>.
         */
-       public static String random(int numchars) {
-               var sb = new StringBuilder(numchars);
-               for (var i = 0; i < numchars; i++) {
-                       var c = RANDOM.nextInt(36) + 97;
-                       if (c > 'z')
-                               c -= ('z'-'0'+1);
-                       sb.append((char)c);
+       public static long getDuration(String s) {
+               s = trim(s);
+               if (Utils.isEmpty(s))
+                       return -1;
+               int i;
+               for (i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (c < '0' || c > '9')
+                               break;
                }
-               return sb.toString();
+               long l;
+               if (i == s.length())
+                       l = Long.parseLong(s);
+               else {
+                       l = Long.parseLong(s.substring(0, i).trim());
+                       var r = s.substring(i).trim().toLowerCase();
+                       if (r.startsWith("s"))
+                               l *= 1000;
+                       else if (r.startsWith("m"))
+                               l *= 1000 * 60;
+                       else if (r.startsWith("h"))
+                               l *= 1000 * 60 * 60;
+                       else if (r.startsWith("d"))
+                               l *= 1000 * 60 * 60 * 24;
+                       else if (r.startsWith("w"))
+                               l *= 1000 * 60 * 60 * 24 * 7;
+               }
+               return l;
        }
-
-       /**
-        * Same as {@link String#trim()} but prevents 
<c>NullPointerExceptions</c>.
-        *
-        * @param s The string to trim.
-        * @return The trimmed string, or <jk>null</jk> if the string was 
<jk>null</jk>.
-        */
-       public static String trim(String s) {
-               if (s == null)
-                       return null;
-               return s.trim();
+       static AsciiSet getEscapeSet(char c) {
+               return ESCAPE_SETS.computeIfAbsent(c, key -> 
AsciiSet.create().chars(key, '\\').build());
        }
 
        /**
-        * Strips the first and last character from a string.
+        * Takes in a string, splits it by lines, and then prepends each line 
with line numbers.
         *
-        * @param s The string to strip.
-        * @return The striped string, or the same string if the input was 
<jk>null</jk> or less than length 2.
+        * @param s The string.
+        * @return The string with line numbers added.
         */
-       public static String strip(String s) {
-               if (s == null || s.length() <= 1)
-                       return s;
-               return s.substring(1, s.length()-1);
+       public static String getNumberedLines(String s) {
+               return getNumberedLines(s, 1, Integer.MAX_VALUE);
        }
 
        /**
-        * Parses an ISO8601 string into a date.
+        * Same as {@link #getNumberedLines(String)} except only returns the 
specified lines.
         *
         * <p>
-        * Supports any of the following formats:
-        * <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>
+        * Out-of-bounds values are allowed and fixed.
         *
-        * @param date The date string.
-        * @return The parsed date.
-        * @throws IllegalArgumentException Value was not a valid date.
+        * @param s The string.
+        * @param start The starting line (1-indexed).
+        * @param end The ending line (1-indexed).
+        * @return The string with line numbers added.
         */
-       public static Date parseIsoDate(String date) throws 
IllegalArgumentException {
-               if (Utils.isEmpty(date))
-                       return null;
-               return parseIsoCalendar(date).getTime();  // NOSONAR - NPE not 
possible.
-       }
-
-       /**
-        * Parses an ISO8601 string into a calendar.
-        *
-        * <p>
-        * Supports any of the following formats:
-        * <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>
-        *
-        * @param date The date string.
-        * @return The parsed calendar.
-        * @throws IllegalArgumentException Value was not a valid date.
-        */
-       public static Calendar parseIsoCalendar(String date) throws 
IllegalArgumentException {
-               if (Utils.isEmpty(date))
+       public static String getNumberedLines(String s, int start, int end) {
+               if (s == null)
                        return null;
-               date = date.trim().replace(' ', 'T');  // Convert to 'standard' 
ISO8601
-               if (date.indexOf(',') != -1)  // Trim milliseconds
-                       date = date.substring(0, date.indexOf(','));
-               if (date.matches("\\d{4}"))
-                       date += "-01-01T00:00:00";
-               else if (date.matches("\\d{4}\\-\\d{2}"))
-                       date += "-01T00:00:00";
-               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}"))
-                       date += "T00:00:00";
-               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}"))
-                       date += ":00:00";
-               else if 
(date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}"))
-                       date += ":00";
-               return DatatypeConverter.parseDateTime(date);
-       }
-
-       /**
-        * Converts the specified object to an ISO8601 date string.
-        *
-        * @param c The object to convert.
-        * @return The converted object.
-        */
-       public static String toIsoDate(Calendar c) {
-               return DatatypeConverter.printDate(c);
+               var lines = s.split("[\r\n]+");
+               var digits = String.valueOf(lines.length).length();
+               if (start < 1)
+                       start = 1;
+               if (end < 0)
+                       end = Integer.MAX_VALUE;
+               if (end > lines.length)
+                       end = lines.length;
+               var sb = new StringBuilder();
+               for (var l :  Arrays.asList(lines).subList(start-1, end))
+                       sb.append(String.format("%0"+digits+"d", 
start++)).append(": ").append(l).append("\n");  // NOSONAR - Intentional.
+               return sb.toString();
        }
 
        /**
-        * Converts the specified object to an ISO8601 date-time string.
+        * Same as {@link String#indexOf(int)} except allows you to check for 
multiple characters.
         *
-        * @param c The object to convert.
-        * @return The converted object.
+        * @param s The string to check.
+        * @param c The characters to check for.
+        * @return The index into the string that is one of the specified 
characters.
         */
-       public static String toIsoDateTime(Calendar c) {
-               return DatatypeConverter.printDateTime(c);
+       public static int indexOf(String s, char...c) {
+               if (s == null)
+                       return -1;
+               for (var i = 0; i < s.length(); i++) {
+                       var c2 = s.charAt(i);
+                       for (var cc : c)
+                               if (c2 == cc)
+                                       return i;
+               }
+               return -1;
        }
 
        /**
-        * Simple utility for replacing variables of the form <js>"{key}"</js> 
with values in the specified map.
-        *
-        * <p>
-        * Nested variables are supported in both the input string and map 
values.
-        *
-        * <p>
-        * If the map does not contain the specified value, the variable is not 
replaced.
-        *
-        * <p>
-        * <jk>null</jk> values in the map are treated as blank strings.
+        * Efficiently determines whether a URL is of the pattern "xxx://xxx"
         *
-        * @param s The string containing variables to replace.
-        * @param m The map containing the variable values.
-        * @return The new string with variables replaced, or the original 
string if it didn't have variables in it.
+        * @param s The string to test.
+        * @return <jk>true</jk> if it's an absolute path.
         */
-       public static String replaceVars(String s, Map<String,Object> m) {
+       public static boolean isAbsoluteUri(String s) {  // NOSONAR - False 
positive.
 
-               if (s == null)
-                       return null;
+               if (Utils.isEmpty(s))
+                       return false;
 
-               if (m == null || m.isEmpty() || s.indexOf('{') == -1)
-                       return s;
+               // Use a state machine for maximum performance.
 
                final int
-                       S1 = 1,    // Not in variable, looking for '{'
-                       S2 = 2;    // Found '{', Looking for '}'
+                       S1 = 1,  // Looking for http
+                       S2 = 2,  // Found http, looking for :
+                       S3 = 3,  // Found :, looking for /
+                       S4 = 4,  // Found /, looking for /
+                       S5 = 5;  // Found /, looking for x
 
                var state = S1;
-               var hasInternalVar = false;
-               var x = 0;
-               var depth = 0;
-               var length = s.length();
-               var out = new StringBuilder();
 
-               for (var i = 0; i < length; i++) {
+               for (var i = 0; i < s.length(); i++) {
                        var c = s.charAt(i);
                        if (state == S1) {
-                               if (c == '{') {
+                               if (c >= 'a' && c <= 'z')
                                        state = S2;
-                                       x = i;
-                               } else {
-                                       out.append(c);
-                               }
-                       } else /* state == S2 */ {
-                               if (c == '{') {
-                                       depth++;
-                                       hasInternalVar = true;
-                               } else if (c == '}') {
-                                       if (depth > 0) {
-                                               depth--;
-                                       } else {
-                                               var key = s.substring(x+1, i);
-                                               key = (hasInternalVar ? 
replaceVars(key, m) : key);
-                                               hasInternalVar = false;
-                                               if (! m.containsKey(key))
-                                                       
out.append('{').append(key).append('}');
-                                               else {
-                                                       var val = m.get(key);
-                                                       if (val == null)
-                                                               val = "";
-                                                       var v = val.toString();
-                                                       // If the replacement 
also contains variables, replace them now.
-                                                       if (v.indexOf('{') != 
-1)
-                                                               v = 
replaceVars(v, m);
-                                                       out.append(v);
-                                               }
-                                               state = 1;
-                                       }
-                               }
+                               else
+                                       return false;
+                       } else if (state == S2) {
+                               if (c == ':')
+                                       state = S3;
+                               else if (c < 'a' || c > 'z')
+                                       return false;
+                       } else if (state == S3) {  // NOSONAR - False positive.
+                               if (c == '/')
+                                       state = S4;
+                               else
+                                       return false;
+                       } else if (state == S4) {
+                               if (c == '/')
+                                       state = S5;
+                               else
+                                       return false;
+                       } else if (state == S5) {
+                               return true;
                        }
                }
-               return out.toString();
+               return false;
        }
 
        /**
-        * Replaces <js>"\\uXXXX"</js> character sequences with their unicode 
characters.
+        * Returns <jk>true</jk> if the specified string is numeric.
         *
-        * @param s The string to replace unicode sequences in.
-        * @return A string with unicode sequences replaced.
+        * @param s The string to check.
+        * @return <jk>true</jk> if the specified string is numeric.
         */
-       public static String replaceUnicodeSequences(String s) {
-
-               if (s.indexOf('\\') == -1)
-                       return s;
-
-               var p = Pattern.compile("\\\\u(\\p{XDigit}{4})");
-               var m = p.matcher(s);
-               var sb = new StringBuffer(s.length());
-
-               while (m.find()) {
-                       var ch = String.valueOf((char) 
Integer.parseInt(m.group(1), 16));
-                       m.appendReplacement(sb, Matcher.quoteReplacement(ch));
+       public static boolean isDecimal(String s) {
+               if (s == null || s.isEmpty() || ! 
firstNumberChars.contains(s.charAt(0)))
+                       return false;
+               var i = 0;
+               var length = s.length();
+               var c = s.charAt(0);
+               var isPrefixed = false;
+               if (c == '+' || c == '-') {
+                       isPrefixed = true;
+                       i++;
                }
-
-               m.appendTail(sb);
-               return sb.toString();
+               if (i == length)
+                       return false;
+               c = s.charAt(i++);
+               if (c == '0' && length > (isPrefixed ? 2 : 1)) {
+                       c = s.charAt(i++);
+                       if (c == 'x' || c == 'X') {
+                               for (int j = i; j < length; j++) {
+                                       if (! hexChars.contains(s.charAt(j)))
+                                               return false;
+                               }
+                       } else if (octChars.contains(c)) {
+                               for (int j = i; j < length; j++)
+                                       if (! octChars.contains(s.charAt(j)))
+                                               return false;
+                       } else {
+                               return false;
+                       }
+               } else if (c == '#') {
+                       for (int j = i; j < length; j++) {
+                               if (! hexChars.contains(s.charAt(j)))
+                                       return false;
+                       }
+               } else if (decChars.contains(c)) {
+                       for (int j = i; j < length; j++)
+                               if (! decChars.contains(s.charAt(j)))
+                                       return false;
+               } else {
+                       return false;
+               }
+               return true;
        }
 
        /**
-        * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for 
the specified character.
+        * Returns <jk>true</jk> if the specified character is a valid first 
character for a number.
         *
-        * @param c The character to create a sequence for.
-        * @return An escaped-unicode sequence.
+        * @param c The character to test.
+        * @return <jk>true</jk> if the specified character is a valid first 
character for a number.
         */
-       public static String unicodeSequence(char c) {
-               var sb = new StringBuilder(6);
-               sb.append('\\').append('u');
-               for (var cc : toHex4(c))
-                       sb.append(cc);
-               return sb.toString();
+       public static boolean isFirstNumberChar(char c) {
+               return firstNumberChars.contains(c);
        }
 
        /**
-        * Converts the specified array to a string.
+        * Returns <jk>true</jk> if the specified string is a floating point 
number.
         *
-        * @param o The array to convert to a string.
-        * @return The array converted to a string, or <jk>null</jk> if the 
object was null.
+        * @param s The string to check.
+        * @return <jk>true</jk> if the specified string is a floating point 
number.
         */
-       public static String stringifyDeep(Object o) {
-               if (o == null)
-                       return null;
-               if (! o.getClass().isArray())
-                       return o.toString();
-               if (o.getClass().getComponentType().isPrimitive())
-                       return 
PRIMITIVE_ARRAY_STRINGIFIERS.get(o.getClass()).apply(o);
-               return Arrays.deepToString((Object[])o);
-       }
-
-       private static final Map<Class<?>,Function<Object,String>> 
PRIMITIVE_ARRAY_STRINGIFIERS = new HashMap<>();
-       static {
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(boolean[].class, x -> 
Arrays.toString((boolean[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(byte[].class, x -> 
Arrays.toString((byte[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(char[].class, x -> 
Arrays.toString((char[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(double[].class, x -> 
Arrays.toString((double[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(float[].class, x -> 
Arrays.toString((float[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(int[].class, x -> 
Arrays.toString((int[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(long[].class, x -> 
Arrays.toString((long[])x));
-               PRIMITIVE_ARRAY_STRINGIFIERS.put(short[].class, x -> 
Arrays.toString((short[])x));
+       public static boolean isFloat(String s) {
+               if (s == null || s.isEmpty())
+                       return false;
+               if (! firstNumberChars.contains(s.charAt(0)))
+                       return (s.equals("NaN") || s.equals("Infinity"));
+               var i = 0;
+               var length = s.length();
+               var c = s.charAt(0);
+               if (c == '+' || c == '-')
+                       i++;
+               if (i == length)
+                       return false;
+               c = s.charAt(i);
+               if (c == '.' || decChars.contains(c)) {
+                       return fpRegex.matcher(s).matches();
+               }
+               return false;
        }
 
        /**
-        * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 
encoded string.
+        * Returns <jk>true</jk> if the specified string is valid JSON.
         *
-        * @param hex The hexadecimal string.
-        * @return The UTF-8 string.
-        */
-       public static String fromHexToUTF8(String hex) {
-               var buff = ByteBuffer.allocate(hex.length()/2);
-               for (var i = 0; i < hex.length(); i+=2)
-                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
-               buff.rewind();  // Fixes Java 11 issue.
-               return UTF_8.decode(buff).toString();
-       }
-
-       /**
-        * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 
BC") into a UTF-8 encoded string.
+        * <p>
+        * Leading and trailing spaces are ignored.
+        * <br>Leading and trailing comments are not allowed.
         *
-        * @param hex The hexadecimal string.
-        * @return The UTF-8 string.
+        * @param s The string to test.
+        * @return <jk>true</jk> if the specified string is valid JSON.
         */
-       public static String fromSpacedHexToUTF8(String hex) {
-               var buff = ByteBuffer.allocate((hex.length()+1)/3);
-               for (var i = 0; i < hex.length(); i+=3)
-                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
-               buff.rewind();  // Fixes Java 11 issue.
-               return UTF_8.decode(buff).toString();
+       public static boolean isJson(String s) {
+               if (s == null)
+                       return false;
+               var c1 = firstNonWhitespaceChar(s);
+               var c2 = lastNonWhitespaceChar(s);
+               if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == 
'\'' && c2 == '\'')
+                       return true;
+               return (isOneOf(s, "true","false","null") || isNumeric(s));
        }
 
-       private static final char[] HEX = "0123456789ABCDEF".toCharArray();
-
        /**
-        * Converts a byte array into a simple hexadecimal character string.
+        * Returns <jk>true</jk> if the specified string appears to be an JSON 
array.
         *
-        * @param bytes The bytes to convert to hexadecimal.
-        * @return A new string consisting of hexadecimal characters.
+        * @param o The object to test.
+        * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and 
trailing whitespace and comments will be ignored.
+        * @return <jk>true</jk> if the specified string appears to be a JSON 
array.
         */
-       public static String toHex(byte[] bytes) {
-               var sb = new StringBuilder(bytes.length * 2);
-               for (var element : bytes) {
-                       var v = element & 0xFF;
-                       sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
+       public static boolean isJsonArray(Object o, boolean 
ignoreWhitespaceAndComments) {
+               if (o instanceof CharSequence) {
+                       var s = o.toString();
+                       if (! ignoreWhitespaceAndComments)
+                               return (s.startsWith("[") && s.endsWith("]"));
+                       if (firstRealCharacter(s) != '[')
+                               return false;
+                       var i = s.lastIndexOf(']');
+                       if (i == -1)
+                               return false;
+                       s = s.substring(i+1);
+                       return firstRealCharacter(s) == -1;
                }
-               return sb.toString();
+               return false;
        }
 
        /**
-        * Same as {@link #toHex(byte[])} but puts spaces between the byte 
strings.
+        * Returns <jk>true</jk> if the specified string appears to be a JSON 
object.
         *
-        * @param bytes The bytes to convert to hexadecimal.
-        * @return A new string consisting of hexadecimal characters.
+        * @param o The object to test.
+        * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and 
trailing whitespace and comments will be ignored.
+        * @return <jk>true</jk> if the specified string appears to be a JSON 
object.
         */
-       public static String toSpacedHex(byte[] bytes) {
-               var sb = new StringBuilder(bytes.length * 3);
-               for (var j = 0; j < bytes.length; j++) {
-                       if (j > 0)
-                               sb.append(' ');
-                       var v = bytes[j] & 0xFF;
-                       sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
+       public static boolean isJsonObject(Object o, boolean 
ignoreWhitespaceAndComments) {
+               if (o instanceof CharSequence) {
+                       var s = o.toString();
+                       if (! ignoreWhitespaceAndComments)
+                               return (s.startsWith("{") && s.endsWith("}"));
+                       if (firstRealCharacter(s) != '{')
+                               return false;
+                       var i = s.lastIndexOf('}');
+                       if (i == -1)
+                               return false;
+                       s = s.substring(i+1);
+                       return firstRealCharacter(s) == -1;
                }
-               return sb.toString();
-       }
-
-       /**
-        * Converts a hexadecimal character string to a byte array.
-        *
-        * @param hex The string to convert to a byte array.
-        * @return A new byte array.
-        */
-       public static byte[] fromHex(String hex) {
-               var buff = ByteBuffer.allocate(hex.length()/2);
-               for (var i = 0; i < hex.length(); i+=2)
-                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
-               buff.rewind();
-               return buff.array();
-       }
-
-       /**
-        * Same as {@link #fromHex(String)} except expects spaces between the 
byte strings.
-        *
-        * @param hex The string to convert to a byte array.
-        * @return A new byte array.
-        */
-       public static byte[] fromSpacedHex(String hex) {
-               var buff = ByteBuffer.allocate((hex.length()+1)/3);
-               for (var i = 0; i < hex.length(); i+=3)
-                       buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 
16));
-               buff.rewind();
-               return buff.array();
-       }
-
-       /**
-        * Creates a repeated pattern.
-        *
-        * @param count The number of times to repeat the pattern.
-        * @param pattern The pattern to repeat.
-        * @return A new string consisting of the repeated pattern.
-        */
-       public static String repeat(int count, String pattern) {
-               var sb = new StringBuilder(pattern.length() * count);
-               for (var i = 0; i < count; i++)
-                       sb.append(pattern);
-               return sb.toString();
+               return false;
        }
 
        /**
-        * Trims whitespace characters from the beginning of the specified 
string.
+        * Returns <jk>true</jk> if the specified character is a valid number 
character.
         *
-        * @param s The string to trim.
-        * @return The trimmed string, or <jk>null</jk> if the string was 
<jk>null</jk>.
+        * @param c The character to check.
+        * @return <jk>true</jk> if the specified character is a valid number 
character.
         */
-       public static String trimStart(String s) {
-               if (s != null)
-                       while (Utils.isNotEmpty(s) && isWhitespace(s.charAt(0)))
-                               s = s.substring(1);
-               return s;
+       public static boolean isNumberChar(char c) {
+               return numberChars.contains(c);
        }
 
        /**
-        * Trims whitespace characters from the end of the specified string.
+        * Returns <jk>true</jk> if this string can be parsed by {@link 
#parseNumber(String, Class)}.
         *
-        * @param s The string to trim.
-        * @return The trimmed string, or <jk>null</jk> if the string was 
<jk>null</jk>.
+        * @param s The string to check.
+        * @return <jk>true</jk> if this string can be parsed without causing 
an exception.
         */
-       public static String trimEnd(String s) {
-               if (s != null)
-                       while (Utils.isNotEmpty(s) && 
isWhitespace(s.charAt(s.length()-1)))
-                               s = s.substring(0, s.length()-1);
-               return s;
+       public static boolean isNumeric(String s) {
+               if (s == null || s.isEmpty() || ! 
isFirstNumberChar(s.charAt(0)))
+                       return false;
+               return isDecimal(s) || isFloat(s);
        }
 
        /**
@@ -1106,239 +1079,74 @@ public final class StringUtils {
        }
 
        /**
-        * Trims <js>'/'</js> characters from both the start and end of the 
specified string.
+        * Efficiently determines whether a URL is of the pattern "xxx:/xxx".
         *
-        * @param s The string to trim.
-        * @return A new trimmed string, or the same string if no trimming was 
necessary.
-        */
-       public static String trimSlashes(String s) {
-               if (s == null)
-                       return null;
-               if (s.isEmpty())
-                       return s;
-               while (endsWith(s, '/'))
-                       s = s.substring(0, s.length()-1);
-               while (Utils.isNotEmpty(s) && s.charAt(0) == '/')  // NOSONAR - 
NPE not possible here.
-                       s = s.substring(1);
-               return s;
-       }
-
-       /**
-        * Trims <js>'/'</js> and space characters from both the start and end 
of the specified string.
+        * <p>
+        * The pattern matched is: <c>[a-z]{2,}\:\/.*</c>
         *
-        * @param s The string to trim.
-        * @return A new trimmed string, or the same string if no trimming was 
necessary.
-        */
-       public static String trimSlashesAndSpaces(String s) {
-               if (s == null)
-                       return null;
-               while (Utils.isNotEmpty(s) && (s.charAt(s.length()-1) == '/' || 
isWhitespace(s.charAt(s.length()-1))))
-                       s = s.substring(0, s.length()-1);
-               while (Utils.isNotEmpty(s) && (s.charAt(0) == '/' || 
isWhitespace(s.charAt(0))))
-                       s = s.substring(1);
-               return s;
-       }
-
-       /**
-        * Trims <js>'/'</js> characters from the end of the specified string.
+        * <p>
+        * Note that this excludes filesystem paths such as <js>"C:/temp"</js>.
         *
-        * @param s The string to trim.
-        * @return A new trimmed string, or the same string if no trimming was 
necessary.
+        * @param s The string to test.
+        * @return <jk>true</jk> if it's an absolute path.
         */
-       public static String trimTrailingSlashes(String s) {
-               if (s == null)
-                       return null;
-               while (endsWith(s, '/'))
-                       s = s.substring(0, s.length()-1);
-               return s;
-       }
+       public static boolean isUri(String s) {  // NOSONAR - False positive.
 
-       /**
-        * Trims <js>'/'</js> characters from the beginning of the specified 
string.
-        *
-        * @param s The string to trim.
-        * @return A new trimmed string, or the same string if no trimming was 
necessary.
-        */
-       public static String trimLeadingSlashes(String s) {
-               if (s == null)
-                       return null;
-               while (Utils.isNotEmpty(s) && s.charAt(0) == '/')
-                       s = s.substring(1);
-               return s;
-       }
+               if (Utils.isEmpty(s))
+                       return false;
 
-       private static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS =
-               
AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.*/()").build();
-
-       /**
-        * Similar to {@link URLEncoder#encode(String, String)} but doesn't 
encode <js>"/"</js> characters.
-        *
-        * @param o The object to encode.
-        * @return The URL encoded string, or <jk>null</jk> if the object was 
null.
-        */
-       public static String urlEncodePath(Object o) {
-
-               if (o == null)
-                       return null;
-
-               var s = Utils.s(o);
+               // Use a state machine for maximum performance.
 
-               var needsEncode = false;
-               for (var i = 0; i < s.length() && ! needsEncode; i++)
-                       needsEncode = 
URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i));
-               if (! needsEncode)
-                       return s;
+               final int
+                       S1 = 1,  // Looking for protocol char 1
+                       S2 = 2,  // Found protocol char 1, looking for protocol 
char 2
+                       S3 = 3,  // Found protocol char 2, looking for :
+                       S4 = 4;  // Found :, looking for /
 
-               var sb = new StringBuilder();
-               var caw = new CharArrayWriter();
-               var caseDiff = ('a' - 'A');
+               var state = S1;
 
-               for (var i = 0; i < s.length();) {
+               for (var i = 0; i < s.length(); i++) {
                        var c = s.charAt(i);
-                       if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) {
-                               sb.append(c);
-                               i++;  // NOSONAR - Intentional.
-                       } else {
-                               if (c == ' ') {
-                                       sb.append('+');
-                                       i++;  // NOSONAR - Intentional.
-                               } else {
-                                       do {
-                                               caw.write(c);
-                                               if (c >= 0xD800 && c <= 0xDBFF) 
{
-                                                       if ((i+1) < s.length()) 
{  // NOSONAR - Intentional.
-                                                               int d = 
s.charAt(i+1);
-                                                               if (d >= 0xDC00 
&& d <= 0xDFFF) {
-                                                                       
caw.write(d);
-                                                                       i++;  
// NOSONAR - Intentional.
-                                                               }
-                                                       }
-                                               }
-                                               i++;  // NOSONAR - Intentional.
-                                       } while (i < s.length() && 
!URL_ENCODE_PATHINFO_VALIDCHARS.contains((c = s.charAt(i))));   // NOSONAR - 
Intentional.
-
-                                       caw.flush();
-                                       var s2 = new String(caw.toCharArray());
-                                       var ba = s2.getBytes(IOUtils.UTF8);
-                                       for (var element : ba) {
-                                               sb.append('%');
-                                               var ch = forDigit((element >> 
4) & 0xF, 16);
-                                               if (isLetter(ch)) {
-                                                       ch -= caseDiff;
-                                               }
-                                               sb.append(ch);
-                                               ch = forDigit(element & 0xF, 
16);
-                                               if (isLetter(ch)) {
-                                                       ch -= caseDiff;
-                                               }
-                                               sb.append(ch);
-                                       }
-                                       caw.reset();
-                               }
+                       if (state == S1) {
+                               if (c >= 'a' && c <= 'z')
+                                       state = S2;
+                               else
+                                       return false;
+                       } else if (state == S2) {
+                               if (c >= 'a' && c <= 'z')
+                                       state = S3;
+                               else
+                                       return false;
+                       } else if (state == S3) {  // NOSONAR - False positive.
+                               if (c == ':')
+                                       state = S4;
+                               else if (c < 'a' || c > 'z')
+                                       return false;
+                       } else if (state == S4) {
+                               return c == '/';
                        }
                }
-               return sb.toString();
-       }
-
-       /**
-        * Decodes a <c>application/x-www-form-urlencoded</c> string using 
<c>UTF-8</c> encoding scheme.
-        *
-        * @param s The string to decode.
-        * @return The decoded string, or <jk>null</jk> if input is 
<jk>null</jk>.
-        */
-       public static String urlDecode(String s) {
-
-               if (s == null)
-                       return s;
-
-               var needsDecode = false;
-               for (var i = 0; i < s.length() && ! needsDecode; i++) {
-                       var c = s.charAt(i);
-                       if (c == '+' || c == '%')
-                               needsDecode = true;
-               }
-
-               if (needsDecode) {
-                       try {
-                               return URLDecoder.decode(s, "UTF-8");
-                       } catch (UnsupportedEncodingException e) {/* Won't 
happen */}
-               }
-               return s;
-       }
-
-       /**
-        * Encodes a <c>application/x-www-form-urlencoded</c> string using 
<c>UTF-8</c> encoding scheme.
-        *
-        * @param s The string to encode.
-        * @return The encoded string, or <jk>null</jk> if input is 
<jk>null</jk>.
-        */
-       public static String urlEncode(String s) {
-
-               if (s == null)
-                       return null;
-
-               var needsEncode = false;
-
-               for (var i = 0; i < s.length() && ! needsEncode; i++)
-                       needsEncode |= (! unencodedChars.contains(s.charAt(i)));
-
-               if (needsEncode) {
-                       try {
-                               return URLEncoder.encode(s, "UTF-8");
-                       } catch (UnsupportedEncodingException e) {/* Won't 
happen */}
-               }
-
-               return s;
+               return false;
        }
 
        /**
-        * Same as {@link #urlEncode(String)} except only escapes characters 
that absolutely need to be escaped.
+        * Same as {@link Utils#join(Collection, char)} but escapes the 
delimiter if found in the tokens.
         *
-        * @param s The string to escape.
-        * @return The encoded string, or <jk>null</jk> if input is 
<jk>null</jk>.
+        * @param tokens The tokens to join.
+        * @param d The delimiter.
+        * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, 
returns <jk>null</jk>.
         */
-       public static String urlEncodeLax(String s) {
-               if (s == null)
+       public static String joine(List<?> tokens, char d) {
+               if (tokens == null)
                        return null;
-               var needsEncode = false;
-               for (var i = 0; i < s.length() && ! needsEncode; i++)
-                       needsEncode |= (! 
unencodedCharsLax.contains(s.charAt(i)));
-               if (needsEncode) {
-                       var sb = new StringBuilder(s.length()*2);
-                       for (var i = 0; i < s.length(); i++) {
-                               var c = s.charAt(i);
-                               if (unencodedCharsLax.contains(c))
-                                       sb.append(c);
-                               else if (c == ' ')
-                                       sb.append("+");
-                               else if (c <= 127)
-                                       sb.append('%').append(toHex2(c));
-                               else
-                                       try {
-                                               
sb.append(URLEncoder.encode(""+c, "UTF-8"));  // Yuck.
-                                       } catch (UnsupportedEncodingException 
e) {
-                                               // Not possible.
-                                       }
-                       }
-                       s = sb.toString();
+               var as = getEscapeSet(d);
+               var sb = new StringBuilder();
+               for (int i = 0, j = tokens.size(); i < j; i++) {
+                       if (i > 0)
+                               sb.append(d);
+                       sb.append(escapeChars(Utils.s(tokens.get(i)), as));
                }
-               return s;
-       }
-
-       /**
-        * Returns the first non-whitespace character in the string.
-        *
-        * @param s The string to check.
-        * @return
-        *      The first non-whitespace character, or <c>0</c> if the string 
is <jk>null</jk>, empty, or composed
-        *      of only whitespace.
-        */
-       public static char firstNonWhitespaceChar(String s) {
-               if (s != null)
-                       for (var i = 0; i < s.length(); i++)
-                               if (! isWhitespace(s.charAt(i)))
-                                       return s.charAt(i);
-               return 0;
+               return sb.toString();
        }
 
        /**
@@ -1357,782 +1165,974 @@ public final class StringUtils {
                return 0;
        }
 
+       private static int multiplier(String s) {
+               char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1);  // 
NOSONAR - NPE not possible.
+               if (c == 'G') return 1024*1024*1024;
+               if (c == 'M') return 1024*1024;
+               if (c == 'K') return 1024;
+               if (c == 'g') return 1000*1000*1000;
+               if (c == 'm') return 1000*1000;
+               if (c == 'k') return 1000;
+               return 1;
+       }
+
+       private static long multiplier2(String s) {
+               char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1);  // 
NOSONAR - NPE not possible.
+               if (c == 'P') return 1024*1024*1024*1024*1024l;
+               if (c == 'T') return 1024*1024*1024*1024l;
+               if (c == 'G') return 1024*1024*1024l;
+               if (c == 'M') return 1024*1024l;
+               if (c == 'K') return 1024l;
+               if (c == 'p') return 1000*1000*1000*1000*1000l;
+               if (c == 't') return 1000*1000*1000*1000l;
+               if (c == 'g') return 1000*1000*1000l;
+               if (c == 'm') return 1000*1000l;
+               if (c == 'k') return 1000l;
+               return 1;
+       }
+
        /**
-        * Returns the character at the specified index in the string without 
throwing exceptions.
+        * Converts a <c>String</c> to a <c>Character</c>
         *
-        * @param s The string.
-        * @param i The index position.
-        * @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>.
+        * @param o The string to convert.
+        * @return The first character of the string if the string is of length 
0, or <jk>null</jk> if the string is <jk>null</jk> or empty.
         */
-       public static char charAt(String s, int i) {
-               if (s == null || i < 0 || i >= s.length())
-                       return 0;
-               return s.charAt(i);
+       public static Character parseCharacter(Object o) {
+               if (o == null)
+                       return null;
+               var s = o.toString();
+               if (s.isEmpty())
+                       return null;
+               if (s.length() == 1)
+                       return s.charAt(0);
+               throw new IllegalArgumentException("Invalid character: '" + s + 
"'");
        }
 
        /**
-        * Efficiently determines whether a URL is of the pattern "xxx://xxx"
+        * Converts a string containing a possible multiplier suffix to an 
integer.
         *
-        * @param s The string to test.
-        * @return <jk>true</jk> if it's an absolute path.
-        */
-       public static boolean isAbsoluteUri(String s) {  // NOSONAR - False 
positive.
-
-               if (Utils.isEmpty(s))
-                       return false;
-
-               // Use a state machine for maximum performance.
+        * <p>
+        * The string can contain any of the following multiplier suffixes:
+        * <ul>
+        *      <li><js>"K"</js> - x 1024
+        *      <li><js>"M"</js> - x 1024*1024
+        *      <li><js>"G"</js> - x 1024*1024*1024
+        *      <li><js>"k"</js> - x 1000
+        *      <li><js>"m"</js> - x 1000*1000
+        *      <li><js>"g"</js> - x 1000*1000*1000
+        * </ul>
+        *
+        * @param s The string to parse.
+        * @return The parsed value.
+        */
+       public static int parseIntWithSuffix(String s) {
+               assertArgNotNull("s", s);
+               var m = multiplier(s);
+               if (m == 1)
+                       return Integer.decode(s);
+               return Integer.decode(s.substring(0, s.length()-1).trim()) * m; 
 // NOSONAR - NPE not possible here.
+       }
 
-               final int
-                       S1 = 1,  // Looking for http
-                       S2 = 2,  // Found http, looking for :
-                       S3 = 3,  // Found :, looking for /
-                       S4 = 4,  // Found /, looking for /
-                       S5 = 5;  // Found /, looking for x
+       /**
+        * Parses an ISO8601 string into a calendar.
+        *
+        * <p>
+        * Supports any of the following formats:
+        * <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>
+        *
+        * @param date The date string.
+        * @return The parsed calendar.
+        * @throws IllegalArgumentException Value was not a valid date.
+        */
+       public static Calendar parseIsoCalendar(String date) throws 
IllegalArgumentException {
+               if (Utils.isEmpty(date))
+                       return null;
+               date = date.trim().replace(' ', 'T');  // Convert to 'standard' 
ISO8601
+               if (date.indexOf(',') != -1)  // Trim milliseconds
+                       date = date.substring(0, date.indexOf(','));
+               if (date.matches("\\d{4}"))
+                       date += "-01-01T00:00:00";
+               else if (date.matches("\\d{4}\\-\\d{2}"))
+                       date += "-01T00:00:00";
+               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}"))
+                       date += "T00:00:00";
+               else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}"))
+                       date += ":00:00";
+               else if 
(date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}"))
+                       date += ":00";
+               return DatatypeConverter.parseDateTime(date);
+       }
 
-               var state = S1;
+       /**
+        * Parses an ISO8601 string into a date.
+        *
+        * <p>
+        * Supports any of the following formats:
+        * <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>
+        *
+        * @param date The date string.
+        * @return The parsed date.
+        * @throws IllegalArgumentException Value was not a valid date.
+        */
+       public static Date parseIsoDate(String date) throws 
IllegalArgumentException {
+               if (Utils.isEmpty(date))
+                       return null;
+               return parseIsoCalendar(date).getTime();  // NOSONAR - NPE not 
possible.
+       }
 
-               for (var i = 0; i < s.length(); i++) {
-                       var c = s.charAt(i);
-                       if (state == S1) {
-                               if (c >= 'a' && c <= 'z')
-                                       state = S2;
-                               else
-                                       return false;
-                       } else if (state == S2) {
-                               if (c == ':')
-                                       state = S3;
-                               else if (c < 'a' || c > 'z')
-                                       return false;
-                       } else if (state == S3) {  // NOSONAR - False positive.
-                               if (c == '/')
-                                       state = S4;
-                               else
-                                       return false;
-                       } else if (state == S4) {
-                               if (c == '/')
-                                       state = S5;
+       /**
+        * Converts a string containing a possible multiplier suffix to a long.
+        *
+        * <p>
+        * The string can contain any of the following multiplier suffixes:
+        * <ul>
+        *      <li><js>"K"</js> - x 1024
+        *      <li><js>"M"</js> - x 1024*1024
+        *      <li><js>"G"</js> - x 1024*1024*1024
+        *      <li><js>"T"</js> - x 1024*1024*1024*1024
+        *      <li><js>"P"</js> - x 1024*1024*1024*1024*1024
+        *      <li><js>"k"</js> - x 1000
+        *      <li><js>"m"</js> - x 1000*1000
+        *      <li><js>"g"</js> - x 1000*1000*1000
+        *      <li><js>"t"</js> - x 1000*1000*1000*1000
+        *      <li><js>"p"</js> - x 1000*1000*1000*1000*1000
+        * </ul>
+        *
+        * @param s The string to parse.
+        * @return The parsed value.
+        */
+       public static long parseLongWithSuffix(String s) {
+               assertArgNotNull("s", s);
+               var m = multiplier2(s);
+               if (m == 1)
+                       return Long.decode(s);
+               return Long.decode(s.substring(0, s.length()-1).trim()) * m;  
// NOSONAR - NPE not possible here.
+       }
+
+       /**
+        * Parses a number from the specified string.
+        *
+        * @param s The string to parse the number from.
+        * @param type
+        *      The number type to created.
+        *      Can be any of the following:
+        *      <ul>
+        *              <li> Integer
+        *              <li> Double
+        *              <li> Float
+        *              <li> Long
+        *              <li> Short
+        *              <li> Byte
+        *              <li> BigInteger
+        *              <li> BigDecimal
+        *      </ul>
+        *      If <jk>null</jk> or <c>Number</c>, uses the best guess.
+        * @return The parsed number, or <jk>null</jk> if the string was null.
+        */
+       public static Number parseNumber(String s, Class<? extends Number> 
type) {
+               if (s == null)
+                       return null;
+               if (s.isEmpty())
+                       s = "0";
+               if (type == null)
+                       type = Number.class;
+
+               // Determine the data type if it wasn't specified.
+               var isAutoDetect = (type == Number.class);
+               var isDecimal = false;
+               if (isAutoDetect) {
+                       // If we're auto-detecting, then we use either an 
Integer, Long, or Double depending on how
+                       // long the string is.
+                       // An integer range is -2,147,483,648 to 2,147,483,647
+                       // An long range is -9,223,372,036,854,775,808 to 
+9,223,372,036,854,775,807
+                       isDecimal = isDecimal(s);
+                       if (isDecimal) {
+                               if (s.length() > 20)
+                                       type = Double.class;
+                               else if (s.length() >= 10)
+                                       type = Long.class;
                                else
-                                       return false;
-                       } else if (state == S5) {
-                               return true;
+                                       type = Integer.class;
                        }
+                       else if (isFloat(s))
+                               type = Double.class;
+                       else
+                               throw new NumberFormatException(s);
                }
-               return false;
+
+               if (type == Double.class || type == Double.TYPE) {
+                       var d = Double.valueOf(s);
+                       var f = Float.valueOf(s);
+                       if (isAutoDetect && (!isDecimal) && 
d.toString().equals(f.toString()))
+                               return f;
+                       return d;
+               }
+               if (type == Float.class || type == Float.TYPE)
+                       return Float.valueOf(s);
+               if (type == BigDecimal.class)
+                       return new BigDecimal(s);
+               if (type == Long.class || type == Long.TYPE || type == 
AtomicLong.class) {
+                       try {
+                               var l = Long.decode(s);
+                               if (type == AtomicLong.class)
+                                       return new AtomicLong(l);
+                               if (isAutoDetect && l >= Integer.MIN_VALUE && l 
<= Integer.MAX_VALUE) {
+                                       // This occurs if the string is 10 
characters long but is still a valid integer value.
+                                       return l.intValue();
+                               }
+                               return l;
+                       } catch (NumberFormatException e) {
+                               if (isAutoDetect) {
+                                       // This occurs if the string is 20 
characters long but still falls outside the range of a valid long.
+                                       return Double.valueOf(s);
+                               }
+                               throw e;
+                       }
+               }
+               if (type == Integer.class || type == Integer.TYPE)
+                       return Integer.decode(s);
+               if (type == Short.class || type == Short.TYPE)
+                       return Short.decode(s);
+               if (type == Byte.class || type == Byte.TYPE)
+                       return Byte.decode(s);
+               if (type == BigInteger.class)
+                       return new BigInteger(s);
+               if (type == AtomicInteger.class)
+                       return new AtomicInteger(Integer.decode(s));
+               throw new NumberFormatException("Unsupported Number type: 
"+type.getName());
+       }
+
+       /**
+        * Generated a random UUID with the specified number of characters.
+        *
+        * <p>
+        * Characters are composed of lower-case ASCII letters and numbers only.
+        *
+        * <p>
+        * This method conforms to the restrictions for hostnames as specified 
in <a class="doclink" href="https://tools.ietf.org/html/rfc952";>RFC 952</a>
+        * Since each character has 36 possible values, the square 
approximation formula for the number of generated IDs
+        * that would produce a 50% chance of collision is:
+        * <c>sqrt(36^N)</c>.
+        * Dividing this number by 10 gives you an approximation of the number 
of generated IDs needed to produce a
+        * &lt;1% chance of collision.
+        *
+        * <p>
+        * For example, given 5 characters, the number of generated IDs need to 
produce a &lt;1% chance of collision would
+        * be:
+        * <c>sqrt(36^5)/10=777</c>
+        *
+        * @param numchars The number of characters in the generated UUID.
+        * @return A new random UUID.
+        */
+       public static String random(int numchars) {
+               var sb = new StringBuilder(numchars);
+               for (var i = 0; i < numchars; i++) {
+                       var c = RANDOM.nextInt(36) + 97;
+                       if (c > 'z')
+                               c -= ('z'-'0'+1);
+                       sb.append((char)c);
+               }
+               return sb.toString();
+       }
+
+       /**
+        * Creates a repeated pattern.
+        *
+        * @param count The number of times to repeat the pattern.
+        * @param pattern The pattern to repeat.
+        * @return A new string consisting of the repeated pattern.
+        */
+       public static String repeat(int count, String pattern) {
+               var sb = new StringBuilder(pattern.length() * count);
+               for (var i = 0; i < count; i++)
+                       sb.append(pattern);
+               return sb.toString();
+       }
+
+       /**
+        * Replaces <js>"\\uXXXX"</js> character sequences with their unicode 
characters.
+        *
+        * @param s The string to replace unicode sequences in.
+        * @return A string with unicode sequences replaced.
+        */
+       public static String replaceUnicodeSequences(String s) {
+
+               if (s.indexOf('\\') == -1)
+                       return s;
+
+               var p = Pattern.compile("\\\\u(\\p{XDigit}{4})");
+               var m = p.matcher(s);
+               var sb = new StringBuffer(s.length());
+
+               while (m.find()) {
+                       var ch = String.valueOf((char) 
Integer.parseInt(m.group(1), 16));
+                       m.appendReplacement(sb, Matcher.quoteReplacement(ch));
+               }
+
+               m.appendTail(sb);
+               return sb.toString();
        }
 
        /**
-        * Efficiently determines whether a URL is of the pattern "xxx:/xxx".
+        * Simple utility for replacing variables of the form <js>"{key}"</js> 
with values in the specified map.
         *
         * <p>
-        * The pattern matched is: <c>[a-z]{2,}\:\/.*</c>
+        * Nested variables are supported in both the input string and map 
values.
         *
         * <p>
-        * Note that this excludes filesystem paths such as <js>"C:/temp"</js>.
+        * If the map does not contain the specified value, the variable is not 
replaced.
         *
-        * @param s The string to test.
-        * @return <jk>true</jk> if it's an absolute path.
+        * <p>
+        * <jk>null</jk> values in the map are treated as blank strings.
+        *
+        * @param s The string containing variables to replace.
+        * @param m The map containing the variable values.
+        * @return The new string with variables replaced, or the original 
string if it didn't have variables in it.
         */
-       public static boolean isUri(String s) {  // NOSONAR - False positive.
+       public static String replaceVars(String s, Map<String,Object> m) {
 
-               if (Utils.isEmpty(s))
-                       return false;
+               if (s == null)
+                       return null;
 
-               // Use a state machine for maximum performance.
+               if (m == null || m.isEmpty() || s.indexOf('{') == -1)
+                       return s;
 
                final int
-                       S1 = 1,  // Looking for protocol char 1
-                       S2 = 2,  // Found protocol char 1, looking for protocol 
char 2
-                       S3 = 3,  // Found protocol char 2, looking for :
-                       S4 = 4;  // Found :, looking for /
+                       S1 = 1,    // Not in variable, looking for '{'
+                       S2 = 2;    // Found '{', Looking for '}'
 
                var state = S1;
+               var hasInternalVar = false;
+               var x = 0;
+               var depth = 0;
+               var length = s.length();
+               var out = new StringBuilder();
 
-               for (var i = 0; i < s.length(); i++) {
+               for (var i = 0; i < length; i++) {
                        var c = s.charAt(i);
                        if (state == S1) {
-                               if (c >= 'a' && c <= 'z')
+                               if (c == '{') {
                                        state = S2;
-                               else
-                                       return false;
-                       } else if (state == S2) {
-                               if (c >= 'a' && c <= 'z')
-                                       state = S3;
-                               else
-                                       return false;
-                       } else if (state == S3) {  // NOSONAR - False positive.
-                               if (c == ':')
-                                       state = S4;
-                               else if (c < 'a' || c > 'z')
-                                       return false;
-                       } else if (state == S4) {
-                               return c == '/';
+                                       x = i;
+                               } else {
+                                       out.append(c);
+                               }
+                       } else /* state == S2 */ {
+                               if (c == '{') {
+                                       depth++;
+                                       hasInternalVar = true;
+                               } else if (c == '}') {
+                                       if (depth > 0) {
+                                               depth--;
+                                       } else {
+                                               var key = s.substring(x+1, i);
+                                               key = (hasInternalVar ? 
replaceVars(key, m) : key);
+                                               hasInternalVar = false;
+                                               if (! m.containsKey(key))
+                                                       
out.append('{').append(key).append('}');
+                                               else {
+                                                       var val = m.get(key);
+                                                       if (val == null)
+                                                               val = "";
+                                                       var v = val.toString();
+                                                       // If the replacement 
also contains variables, replace them now.
+                                                       if (v.indexOf('{') != 
-1)
+                                                               v = 
replaceVars(v, m);
+                                                       out.append(v);
+                                               }
+                                               state = 1;
+                                       }
+                               }
                        }
                }
-               return false;
+               return out.toString();
        }
 
-       /**
-        * Given an absolute URI, returns just the authority portion (e.g. 
<js>"http://hostname:port";</js>)
-        *
-        * @param s The URI string.
-        * @return Just the authority portion of the URI.
-        */
-       public static String getAuthorityUri(String s) {  // NOSONAR - False 
positive.
-
-               // Use a state machine for maximum performance.
-
-               final int
-                       S1 = 1,  // Looking for http
-                       S2 = 2,  // Found http, looking for :
-                       S3 = 3,  // Found :, looking for /
-                       S4 = 4,  // Found /, looking for /
-                       S5 = 5,  // Found /, looking for x
-                       S6 = 6;  // Found x, looking for /
-
-               var state = S1;
-
-               for (var i = 0; i < s.length(); i++) {
-                       var c = s.charAt(i);
-                       if (state == S1) {
-                               if (c >= 'a' && c <= 'z')
-                                       state = S2;
-                               else
-                                       return s;
-                       } else if (state == S2) {
-                               if (c == ':')
-                                       state = S3;
-                               else if (c < 'a' || c > 'z')
-                                       return s;
-                       } else if (state == S3) {  // NOSONAR - False positive.
-                               if (c == '/')
-                                       state = S4;
-                               else
-                                       return s;
-                       } else if (state == S4) {
-                               if (c == '/')
-                                       state = S5;
-                               else
-                                       return s;
-                       } else if (state == S5) {
-                               if (c != '/')
-                                       state = S6;
-                               else
-                                       return s;
-                       } else if (state == S6) {
-                               if (c == '/')  // NOSONAR - Intentional.
-                                       return s.substring(0, i);
+       private static void skipComments(StringReader r) throws IOException {
+               var c = r.read();
+               //  "/* */" style comments
+               if (c == '*') {
+                       while (c != -1)
+                               if ((c = r.read()) == '*')
+                                       if ((c = r.read()) == '/')  // NOSONAR 
- Intentional.
+                                               return;
+               //  "//" style comments
+               } else if (c == '/') {
+                       while (c != -1) {
+                               c = r.read();
+                               if (c == -1 || c == '\n')
+                                       return;
                        }
                }
-
-               return s;
        }
 
        /**
-        * Converts the specified object to a URI.
+        * An efficient method for checking if a string starts with a character.
         *
-        * @param o The object to convert to a URI.
-        * @return A new URI, or the same object if the object was already a 
URI, or
+        * @param s The string to check.  Can be <jk>null</jk>.
+        * @param c The character to check for.
+        * @return <jk>true</jk> if the specified string is not <jk>null</jk> 
and starts with the specified character.
         */
-       public static URI toURI(Object o) {
-               if (o == null || o instanceof URI)
-                       return (URI)o;
-               try {
-                       return new URI(o.toString());
-               } catch (URISyntaxException e) {
-                       throw asRuntimeException(e);
+       public static boolean startsWith(String s, char c) {
+               if (s != null) {
+                       var i = s.length();
+                       if (i > 0)
+                               return s.charAt(0) == c;
                }
+               return false;
        }
 
        /**
-        * Returns the first non-null, non-empty string in the list.
+        * Converts the specified array to a string.
         *
-        * @param s The strings to test.
-        * @return The first non-empty string in the list, or <jk>null</jk> if 
they were all <jk>null</jk> or empty.
+        * @param o The array to convert to a string.
+        * @return The array converted to a string, or <jk>null</jk> if the 
object was null.
         */
-       public static String firstNonEmpty(String...s) {
-               for (var ss : s)
-                       if (Utils.isNotEmpty(ss))
-                               return ss;
-               return null;
+       public static String stringifyDeep(Object o) {
+               if (o == null)
+                       return null;
+               if (! o.getClass().isArray())
+                       return o.toString();
+               if (o.getClass().getComponentType().isPrimitive())
+                       return 
PRIMITIVE_ARRAY_STRINGIFIERS.get(o.getClass()).apply(o);
+               return Arrays.deepToString((Object[])o);
        }
 
        /**
-        * Same as {@link String#indexOf(int)} except allows you to check for 
multiple characters.
+        * Strips the first and last character from a string.
         *
-        * @param s The string to check.
-        * @param c The characters to check for.
-        * @return The index into the string that is one of the specified 
characters.
+        * @param s The string to strip.
+        * @return The striped string, or the same string if the input was 
<jk>null</jk> or less than length 2.
         */
-       public static int indexOf(String s, char...c) {
-               if (s == null)
-                       return -1;
-               for (var i = 0; i < s.length(); i++) {
-                       var c2 = s.charAt(i);
-                       for (var cc : c)
-                               if (c2 == cc)
-                                       return i;
-               }
-               return -1;
+       public static String strip(String s) {
+               if (s == null || s.length() <= 1)
+                       return s;
+               return s.substring(1, s.length()-1);
        }
 
        /**
-        * Similar to {@link MessageFormat#format(String, Object...)} except 
allows you to specify POJO arguments.
+        * Strips invalid characters such as CTRL characters from a string 
meant to be encoded
+        * as an HTTP header value.
         *
-        * @param pattern The string pattern.
-        * @param args The arguments.
-        * @return The formatted string.
+        * @param s The string to strip chars from.
+        * @return The string with invalid characters removed.
         */
-       public static String format(String pattern, Object...args) {
-               if (args == null || args.length == 0)
-                       return pattern;
-               var args2 = new Object[args.length];
-               for (var i = 0; i < args.length; i++)
-                       args2[i] = convertToReadable(args[i]);
+       public static String stripInvalidHttpHeaderChars(String s) {
 
-               var c = countChars(pattern, '\'');
-               if (c % 2 != 0)
-                       throw new AssertionError("Dangling single quote found 
in pattern: " + pattern);
+               if (s == null)
+                       return null;
 
-               return MessageFormat.format(pattern, args2);
+               var needsReplace = false;
+               for (var i = 0; i < s.length() && ! needsReplace; i++)
+                       needsReplace |= httpHeaderChars.contains(s.charAt(i));
+
+               if (! needsReplace)
+                       return s;
+
+               var sb = new StringBuilder(s.length());
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (httpHeaderChars.contains(c))
+                               sb.append(c);
+               }
+
+               return sb.toString();
        }
 
-       private static String convertToReadable(Object o) {
+       /**
+        * Converts the specified object to a comma-delimited list.
+        *
+        * @param o The object to convert.
+        * @return The specified object as a comma-delimited list.
+        */
+       public static String toCdl(Object o) {
                if (o == null)
                        return null;
-               if (o instanceof Class)
-                       return ((Class<?>)o).getName();
-               if (o instanceof Method)
-                       return Method.class.cast(o).getName();
-               if (o.getClass().isArray())
-                       return 
arrayAsList(o).stream().map(StringUtils::convertToReadable).collect(Collectors.joining(",
 ", "[", "]"));
+               if (o.getClass().isArray()) {
+                       var sb = new StringBuilder();
+                       for (int i = 0, j = Array.getLength(o); i < j; i++) {
+                               if (i > 0)
+                                       sb.append(", ");
+                               sb.append(Array.get(o, i));
+                       }
+                       return sb.toString();
+               }
+               if (o instanceof Collection)
+                       return Utils.join((Collection<?>)o, ", ");
                return o.toString();
        }
 
-       private static List<Object> arrayAsList(Object array) {
-               if (array.getClass().getComponentType().isPrimitive()) {
-                       var l = new ArrayList<>(Array.getLength(array));
-                       for (var i = 0; i < Array.getLength(array); i++)
-                               l.add(Array.get(array, i));
-                       return l;
-               }
-               return Arrays.asList((Object[])array);
+       /**
+        * Converts the specified byte into a 2 hexadecimal characters.
+        *
+        * @param b The number to convert to hex.
+        * @return A <code><jk>char</jk>[2]</code> containing the specified 
characters.
+        */
+       public static String toHex(byte b) {
+               var c = new char[2];
+               var v = b & 0xFF;
+               c[0] = hexArray[v >>> 4];
+               c[1] = hexArray[v & 0x0F];
+               return new String(c);
        }
 
        /**
-        * Converts a string containing a possible multiplier suffix to an 
integer.
-        *
-        * <p>
-        * The string can contain any of the following multiplier suffixes:
-        * <ul>
-        *      <li><js>"K"</js> - x 1024
-        *      <li><js>"M"</js> - x 1024*1024
-        *      <li><js>"G"</js> - x 1024*1024*1024
-        *      <li><js>"k"</js> - x 1000
-        *      <li><js>"m"</js> - x 1000*1000
-        *      <li><js>"g"</js> - x 1000*1000*1000
-        * </ul>
+        * Converts a byte array into a simple hexadecimal character string.
         *
-        * @param s The string to parse.
-        * @return The parsed value.
+        * @param bytes The bytes to convert to hexadecimal.
+        * @return A new string consisting of hexadecimal characters.
         */
-       public static int parseIntWithSuffix(String s) {
-               assertArgNotNull("s", s);
-               var m = multiplier(s);
-               if (m == 1)
-                       return Integer.decode(s);
-               return Integer.decode(s.substring(0, s.length()-1).trim()) * m; 
 // NOSONAR - NPE not possible here.
+       public static String toHex(byte[] bytes) {
+               var sb = new StringBuilder(bytes.length * 2);
+               for (var element : bytes) {
+                       var v = element & 0xFF;
+                       sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
+               }
+               return sb.toString();
        }
 
-       private static int multiplier(String s) {
-               char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1);  // 
NOSONAR - NPE not possible.
-               if (c == 'G') return 1024*1024*1024;
-               if (c == 'M') return 1024*1024;
-               if (c == 'K') return 1024;
-               if (c == 'g') return 1000*1000*1000;
-               if (c == 'm') return 1000*1000;
-               if (c == 'k') return 1000;
-               return 1;
+       public static String toHex(InputStream is) {
+               return safe(()->is == null ? null : toHex(readBytes(is)));
        }
 
        /**
-        * Converts a string containing a possible multiplier suffix to a long.
-        *
-        * <p>
-        * The string can contain any of the following multiplier suffixes:
-        * <ul>
-        *      <li><js>"K"</js> - x 1024
-        *      <li><js>"M"</js> - x 1024*1024
-        *      <li><js>"G"</js> - x 1024*1024*1024
-        *      <li><js>"T"</js> - x 1024*1024*1024*1024
-        *      <li><js>"P"</js> - x 1024*1024*1024*1024*1024
-        *      <li><js>"k"</js> - x 1000
-        *      <li><js>"m"</js> - x 1000*1000
-        *      <li><js>"g"</js> - x 1000*1000*1000
-        *      <li><js>"t"</js> - x 1000*1000*1000*1000
-        *      <li><js>"p"</js> - x 1000*1000*1000*1000*1000
-        * </ul>
+        * Converts the specified number into a 2 hexadecimal characters.
         *
-        * @param s The string to parse.
-        * @return The parsed value.
+        * @param num The number to convert to hex.
+        * @return A <code><jk>char</jk>[2]</code> containing the specified 
characters.
         */
-       public static long parseLongWithSuffix(String s) {
-               assertArgNotNull("s", s);
-               var m = multiplier2(s);
-               if (m == 1)
-                       return Long.decode(s);
-               return Long.decode(s.substring(0, s.length()-1).trim()) * m;  
// NOSONAR - NPE not possible here.
-       }
-
-       private static long multiplier2(String s) {
-               char c = Utils.isEmpty(s) ? null : s.charAt(s.length()-1);  // 
NOSONAR - NPE not possible.
-               if (c == 'P') return 1024*1024*1024*1024*1024l;
-               if (c == 'T') return 1024*1024*1024*1024l;
-               if (c == 'G') return 1024*1024*1024l;
-               if (c == 'M') return 1024*1024l;
-               if (c == 'K') return 1024l;
-               if (c == 'p') return 1000*1000*1000*1000*1000l;
-               if (c == 't') return 1000*1000*1000*1000l;
-               if (c == 'g') return 1000*1000*1000l;
-               if (c == 'm') return 1000*1000l;
-               if (c == 'k') return 1000l;
-               return 1;
+       public static char[] toHex2(int num) {
+               if (num < 0 || num > 255)
+                       throw new NumberFormatException("toHex2 can only be 
used on numbers between 0 and 255");
+               var n = new char[2];
+               var a = num%16;
+               n[1] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+               a = (num/16)%16;
+               n[0] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+               return n;
        }
 
        /**
-        * Same as {@link String#contains(CharSequence)} except returns 
<jk>null</jk> if the value is null.
+        * Converts the specified number into a 4 hexadecimal characters.
         *
-        * @param value The string to check.
-        * @param substring The value to check for.
-        * @return <jk>true</jk> if the value contains the specified substring.
+        * @param num The number to convert to hex.
+        * @return A <code><jk>char</jk>[4]</code> containing the specified 
characters.
         */
-       public static boolean contains(String value, CharSequence substring) {
-               return value != null && value.contains(substring);
+       public static char[] toHex4(int num) {
+               var n = new char[4];
+               var a = num%16;
+               n[3] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+               var base = 16;
+               for (var i = 1; i < 4; i++) {
+                       a = (num/base)%16;
+                       base <<= 4;
+                       n[3-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+               }
+               return n;
        }
 
        /**
-        * Returns <jk>true</jk> if the specified string appears to be an JSON 
array.
+        * Converts the specified number into a 8 hexadecimal characters.
         *
-        * @param o The object to test.
-        * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and 
trailing whitespace and comments will be ignored.
-        * @return <jk>true</jk> if the specified string appears to be a JSON 
array.
+        * @param num The number to convert to hex.
+        * @return A <code><jk>char</jk>[8]</code> containing the specified 
characters.
         */
-       public static boolean isJsonArray(Object o, boolean 
ignoreWhitespaceAndComments) {
-               if (o instanceof CharSequence) {
-                       var s = o.toString();
-                       if (! ignoreWhitespaceAndComments)
-                               return (s.startsWith("[") && s.endsWith("]"));
-                       if (firstRealCharacter(s) != '[')
-                               return false;
-                       var i = s.lastIndexOf(']');
-                       if (i == -1)
-                               return false;
-                       s = s.substring(i+1);
-                       return firstRealCharacter(s) == -1;
+       public static char[] toHex8(long num) {
+               var n = new char[8];
+               var a = num%16;
+               n[7] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
+               var base = 16;
+               for (var i = 1; i < 8; i++) {
+                       a = (num/base)%16;
+                       base <<= 4;
+                       n[7-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
                }
-               return false;
+               return n;
        }
 
        /**
-        * Returns <jk>true</jk> if the specified string is valid JSON.
+        * Converts the specified object to an ISO8601 date string.
         *
-        * <p>
-        * Leading and trailing spaces are ignored.
-        * <br>Leading and trailing comments are not allowed.
+        * @param c The object to convert.
+        * @return The converted object.
+        */
+       public static String toIsoDate(Calendar c) {
+               return DatatypeConverter.printDate(c);
+       }
+       /**
+        * Converts the specified object to an ISO8601 date-time string.
         *
-        * @param s The string to test.
-        * @return <jk>true</jk> if the specified string is valid JSON.
+        * @param c The object to convert.
+        * @return The converted object.
         */
-       public static boolean isJson(String s) {
-               if (s == null)
-                       return false;
-               var c1 = firstNonWhitespaceChar(s);
-               var c2 = lastNonWhitespaceChar(s);
-               if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == 
'\'' && c2 == '\'')
-                       return true;
-               return (isOneOf(s, "true","false","null") || isNumeric(s));
+       public static String toIsoDateTime(Calendar c) {
+               return DatatypeConverter.printDateTime(c);
        }
 
        /**
-        * Returns <jk>true</jk> if the specified string appears to be a JSON 
object.
+        * Converts the specified bytes into a readable string.
         *
-        * @param o The object to test.
-        * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and 
trailing whitespace and comments will be ignored.
-        * @return <jk>true</jk> if the specified string appears to be a JSON 
object.
+        * @param b The number to convert to hex.
+        * @return A <code><jk>char</jk>[2]</code> containing the specified 
characters.
         */
-       public static boolean isJsonObject(Object o, boolean 
ignoreWhitespaceAndComments) {
-               if (o instanceof CharSequence) {
-                       var s = o.toString();
-                       if (! ignoreWhitespaceAndComments)
-                               return (s.startsWith("{") && s.endsWith("}"));
-                       if (firstRealCharacter(s) != '{')
-                               return false;
-                       var i = s.lastIndexOf('}');
-                       if (i == -1)
-                               return false;
-                       s = s.substring(i+1);
-                       return firstRealCharacter(s) == -1;
+       public static String toReadableBytes(byte[] b) {
+               var sb = new StringBuilder();
+               for (var b2 : b)
+                       sb.append((b2 < ' ' || b2 > 'z') ? 
String.format("[%02X]", b2) : (char)b2 + "   ");
+               sb.append("\n");
+               for (var b2 : b)
+                       sb.append(String.format("[%02X]", b2));
+               return sb.toString();
+       }
+
+       /**
+        * Same as {@link #toHex(byte[])} but puts spaces between the byte 
strings.
+        *
+        * @param bytes The bytes to convert to hexadecimal.
+        * @return A new string consisting of hexadecimal characters.
+        */
+       public static String toSpacedHex(byte[] bytes) {
+               var sb = new StringBuilder(bytes.length * 3);
+               for (var j = 0; j < bytes.length; j++) {
+                       if (j > 0)
+                               sb.append(' ');
+                       var v = bytes[j] & 0xFF;
+                       sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
+               }
+               return sb.toString();
+       }
+
+       /**
+        * Converts the specified object to a URI.
+        *
+        * @param o The object to convert to a URI.
+        * @return A new URI, or the same object if the object was already a 
URI, or
+        */
+       public static URI toURI(Object o) {
+               if (o == null || o instanceof URI)
+                       return (URI)o;
+               try {
+                       return new URI(o.toString());
+               } catch (URISyntaxException e) {
+                       throw asRuntimeException(e);
                }
-               return false;
        }
 
-       private static int firstRealCharacter(String s) {
-               try (var r = new StringReader(s)) {
-                       var c = 0;
-                       while ((c = r.read()) != -1) {
-                               if (! isWhitespace(c)) {
-                                       if (c == '/') {
-                                               skipComments(r);
-                                       } else {
-                                               return c;
-                                       }
-                               }
-                       }
-                       return -1;
-               } catch (Exception e) {
-                       throw asRuntimeException(e);
-               }
+       public static String toUtf8(byte[] b) {
+               return b == null ? null : new String(b, IOUtils.UTF8);
        }
-       private static void skipComments(StringReader r) throws IOException {
-               var c = r.read();
-               //  "/* */" style comments
-               if (c == '*') {
-                       while (c != -1)
-                               if ((c = r.read()) == '*')
-                                       if ((c = r.read()) == '/')  // NOSONAR 
- Intentional.
-                                               return;
-               //  "//" style comments
-               } else if (c == '/') {
-                       while (c != -1) {
-                               c = r.read();
-                               if (c == -1 || c == '\n')
-                                       return;
-                       }
-               }
+
+       public static String toUtf8(InputStream is) {
+               return safe(()->is == null ? null : new String(readBytes(is), 
IOUtils.UTF8));
        }
 
        /**
-        * Takes in a string, splits it by lines, and then prepends each line 
with line numbers.
+        * Same as {@link String#trim()} but prevents 
<c>NullPointerExceptions</c>.
         *
-        * @param s The string.
-        * @return The string with line numbers added.
+        * @param s The string to trim.
+        * @return The trimmed string, or <jk>null</jk> if the string was 
<jk>null</jk>.
         */
-       public static String getNumberedLines(String s) {
-               return getNumberedLines(s, 1, Integer.MAX_VALUE);
+       public static String trim(String s) {
+               if (s == null)
+                       return null;
+               return s.trim();
        }
 
        /**
-        * Same as {@link #getNumberedLines(String)} except only returns the 
specified lines.
+        * Trims whitespace characters from the end of the specified string.
         *
-        * <p>
-        * Out-of-bounds values are allowed and fixed.
+        * @param s The string to trim.
+        * @return The trimmed string, or <jk>null</jk> if the string was 
<jk>null</jk>.
+        */
+       public static String trimEnd(String s) {
+               if (s != null)
+                       while (Utils.isNotEmpty(s) && 
isWhitespace(s.charAt(s.length()-1)))
+                               s = s.substring(0, s.length()-1);
+               return s;
+       }
+
+       /**
+        * Trims <js>'/'</js> characters from the beginning of the specified 
string.
         *
-        * @param s The string.
-        * @param start The starting line (1-indexed).
-        * @param end The ending line (1-indexed).
-        * @return The string with line numbers added.
+        * @param s The string to trim.
+        * @return A new trimmed string, or the same string if no trimming was 
necessary.
         */
-       public static String getNumberedLines(String s, int start, int end) {
+       public static String trimLeadingSlashes(String s) {
                if (s == null)
                        return null;
-               var lines = s.split("[\r\n]+");
-               var digits = String.valueOf(lines.length).length();
-               if (start < 1)
-                       start = 1;
-               if (end < 0)
-                       end = Integer.MAX_VALUE;
-               if (end > lines.length)
-                       end = lines.length;
-               var sb = new StringBuilder();
-               for (var l :  Arrays.asList(lines).subList(start-1, end))
-                       sb.append(String.format("%0"+digits+"d", 
start++)).append(": ").append(l).append("\n");  // NOSONAR - Intentional.
-               return sb.toString();
+               while (Utils.isNotEmpty(s) && s.charAt(0) == '/')
+                       s = s.substring(1);
+               return s;
        }
 
        /**
-        * Compares two strings, but gracefully handles <jk>nulls</jk>.
+        * Trims <js>'/'</js> characters from both the start and end of the 
specified string.
         *
-        * @param s1 The first string.
-        * @param s2 The second string.
-        * @return The same as {@link String#compareTo(String)}.
+        * @param s The string to trim.
+        * @return A new trimmed string, or the same string if no trimming was 
necessary.
         */
-       public static int compare(String s1, String s2) {
-               if (s1 == null && s2 == null)
-                       return 0;
-               if (s1 == null)
-                       return Integer.MIN_VALUE;
-               if (s2 == null)
-                       return Integer.MAX_VALUE;
-               return s1.compareTo(s2);
+       public static String trimSlashes(String s) {
+               if (s == null)
+                       return null;
+               if (s.isEmpty())
+                       return s;
+               while (endsWith(s, '/'))
+                       s = s.substring(0, s.length()-1);
+               while (Utils.isNotEmpty(s) && s.charAt(0) == '/')  // NOSONAR - 
NPE not possible here.
+                       s = s.substring(1);
+               return s;
        }
 
        /**
-        * Returns the first character in the specified string.
+        * Trims <js>'/'</js> and space characters from both the start and end 
of the specified string.
         *
-        * @param s The string to check.
-        * @return The first character in the string, or <c>0</c> if the string 
is <jk>null</jk> or empty.
+        * @param s The string to trim.
+        * @return A new trimmed string, or the same string if no trimming was 
necessary.
         */
-       public static char firstChar(String s) {
-               if (s == null || s.isEmpty())
-                       return 0;
-               return s.charAt(0);
+       public static String trimSlashesAndSpaces(String s) {
+               if (s == null)
+                       return null;
+               while (Utils.isNotEmpty(s) && (s.charAt(s.length()-1) == '/' || 
isWhitespace(s.charAt(s.length()-1))))
+                       s = s.substring(0, s.length()-1);
+               while (Utils.isNotEmpty(s) && (s.charAt(0) == '/' || 
isWhitespace(s.charAt(0))))
+                       s = s.substring(1);
+               return s;
        }
 
        /**
-        * Parses a duration string.
-        *
-        * <p>
-        * Examples:
-        * <ul>
-        *      <li><js>"1000"</js> - 1000 milliseconds.
-        *      <li><js>"10s"</js> - 10 seconds.
-        *      <li><js>"10 sec"</js> - 10 seconds.
-        *      <li><js>"10 seconds"</js> - 10 seconds.
-        * </ul>
-        *
-        * <p>
-        * Use any of the following suffixes:
-        * <ul>
-        *      <li>None (time in milliseconds).
-        *      
<li><js>"s"</js>/<js>"sec"</js>/<js>"second"</js>/<js>"seconds"</js>
-        *      
<li><js>"m"</js>/<js>"min"</js>/<js>"minutes"</js>/<js>"seconds"</js>
-        *      <li><js>"h"</js>/<js>"hour"</js>/<js>"hours"</js>
-        *      <li><js>"d"</js>/<js>"day"</js>/<js>"days"</js>
-        *      <li><js>"w"</js>/<js>"week"</js>/<js>"weeks"</js>
-        * </ul>
-        *
-        * <p>
-        * Suffixes are case-insensitive.
-        * <br>Whitespace is ignored.
+        * Trims whitespace characters from the beginning of the specified 
string.
         *
-        * @param s The string to parse.
-        * @return
-        *      The time in milliseconds, or <c>-1</c> if the string is empty 
or <jk>null</jk>.
+        * @param s The string to trim.
+        * @return The trimmed string, or <jk>null</jk> if the string was 
<jk>null</jk>.
         */
-       public static long getDuration(String s) {
-               s = trim(s);
-               if (Utils.isEmpty(s))
-                       return -1;
-               int i;
-               for (i = 0; i < s.length(); i++) {
-                       var c = s.charAt(i);
-                       if (c < '0' || c > '9')
-                               break;
-               }
-               long l;
-               if (i == s.length())
-                       l = Long.parseLong(s);
-               else {
-                       l = Long.parseLong(s.substring(0, i).trim());
-                       var r = s.substring(i).trim().toLowerCase();
-                       if (r.startsWith("s"))
-                               l *= 1000;
-                       else if (r.startsWith("m"))
-                               l *= 1000 * 60;
-                       else if (r.startsWith("h"))
-                               l *= 1000 * 60 * 60;
-                       else if (r.startsWith("d"))
-                               l *= 1000 * 60 * 60 * 24;
-                       else if (r.startsWith("w"))
-                               l *= 1000 * 60 * 60 * 24 * 7;
-               }
-               return l;
+       public static String trimStart(String s) {
+               if (s != null)
+                       while (Utils.isNotEmpty(s) && isWhitespace(s.charAt(0)))
+                               s = s.substring(1);
+               return s;
        }
 
        /**
-        * Strips invalid characters such as CTRL characters from a string 
meant to be encoded
-        * as an HTTP header value.
+        * Trims <js>'/'</js> characters from the end of the specified string.
         *
-        * @param s The string to strip chars from.
-        * @return The string with invalid characters removed.
+        * @param s The string to trim.
+        * @return A new trimmed string, or the same string if no trimming was 
necessary.
         */
-       public static String stripInvalidHttpHeaderChars(String s) {
-
+       public static String trimTrailingSlashes(String s) {
                if (s == null)
                        return null;
+               while (endsWith(s, '/'))
+                       s = s.substring(0, s.length()-1);
+               return s;
+       }
 
-               var needsReplace = false;
-               for (var i = 0; i < s.length() && ! needsReplace; i++)
-                       needsReplace |= httpHeaderChars.contains(s.charAt(i));
-
-               if (! needsReplace)
+       /**
+        * Removes escape characters from the specified characters.
+        *
+        * @param s The string to remove escape characters from.
+        * @param escaped The characters escaped.
+        * @return A new string if characters were removed, or the same string 
if not or if the input was <jk>null</jk>.
+        */
+       public static String unEscapeChars(String s, AsciiSet escaped) {
+               if (s == null || s.isEmpty())
                        return s;
-
-               var sb = new StringBuilder(s.length());
+               var count = 0;
+               for (var i = 0; i < s.length(); i++)
+                       if (escaped.contains(s.charAt(i)))
+                               count++;
+               if (count == 0)
+                       return s;
+               var sb = new StringBuffer(s.length()-count);
                for (var i = 0; i < s.length(); i++) {
                        var c = s.charAt(i);
-                       if (httpHeaderChars.contains(c))
-                               sb.append(c);
+
+                       if (c == '\\') {
+                               if (i+1 != s.length()) {  // NOSONAR - 
Intentional.
+                                       var c2 = s.charAt(i+1);
+                                       if (escaped.contains(c2)) {
+                                               i++;  // NOSONAR - Intentional.
+                                       } else if (c2 == '\\') {
+                                               sb.append('\\');
+                                               i++;  // NOSONAR - Intentional.
+                                       }
+                               }
+                       }
+                       sb.append(s.charAt(i));
                }
-
                return sb.toString();
        }
 
        /**
-        * Abbreviates a String using ellipses.
+        * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for 
the specified character.
         *
-        * @param in The input string.
-        * @param length The max length of the resulting string.
-        * @return The abbreviated string.
+        * @param c The character to create a sequence for.
+        * @return An escaped-unicode sequence.
         */
-       public static String abbreviate(String in, int length) {
-               if (in == null || in.length() <= length || in.length() <= 3)
-                       return in;
-               return in.substring(0, length-3) + "...";
+       public static String unicodeSequence(char c) {
+               var sb = new StringBuilder(6);
+               sb.append('\\').append('u');
+               for (var cc : toHex4(c))
+                       sb.append(cc);
+               return sb.toString();
        }
 
-       private static final AsciiSet URI_CHARS = 
AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0','9').range('A','Z').range('a','z').build();
-
        /**
-        * Attempts to escape any invalid characters found in a URI.
+        * Decodes a <c>application/x-www-form-urlencoded</c> string using 
<c>UTF-8</c> encoding scheme.
         *
-        * @param in The URI to fix.
-        * @return The fixed URI.
+        * @param s The string to decode.
+        * @return The decoded string, or <jk>null</jk> if input is 
<jk>null</jk>.
         */
-       public static String fixUrl(String in) {
-
-               if (in == null)
-                       return null;
-
-               StringBuilder sb = null;
+       public static String urlDecode(String s) {
 
-               var m = 0;
+               if (s == null)
+                       return s;
 
-               for (var i = 0; i < in.length(); i++) {
-                       var c = in.charAt(i);
-                       if (c <= 127 && ! URI_CHARS.contains(c)) {
-                               sb = append(sb, in.substring(m, i));
-                               if (c == ' ')
-                                       sb.append("+");
-                               else
-                                       sb.append('%').append(toHex2(c));
-                               m = i+1;
-                       }
-               }
-               if (sb != null) {
-                       sb.append(in.substring(m));
-                       return sb.toString();
+               var needsDecode = false;
+               for (var i = 0; i < s.length() && ! needsDecode; i++) {
+                       var c = s.charAt(i);
+                       if (c == '+' || c == '%')
+                               needsDecode = true;
                }
-               return in;
-
-       }
 
-       private static StringBuilder append(StringBuilder sb, String in) {
-               if (sb == null)
-                       return new StringBuilder(in);
-               sb.append(in);
-               return sb;
+               if (needsDecode) {
+                       try {
+                               return URLDecoder.decode(s, "UTF-8");
+                       } catch (UnsupportedEncodingException e) {/* Won't 
happen */}
+               }
+               return s;
        }
 
        /**
-        * Counts the number of the specified character in the specified string.
+        * Encodes a <c>application/x-www-form-urlencoded</c> string using 
<c>UTF-8</c> encoding scheme.
         *
-        * @param s The string to check.
-        * @param c The character to check for.
-        * @return The number of those characters or zero if the string was 
<jk>null</jk>.
+        * @param s The string to encode.
+        * @return The encoded string, or <jk>null</jk> if input is 
<jk>null</jk>.
         */
-       public static int countChars(String s, char c) {
-               var count = 0;
+       public static String urlEncode(String s) {
+
                if (s == null)
-                       return count;
-               for (var i = 0; i < s.length(); i++)
-                       if (s.charAt(i) == c)
-                               count++;
-               return count;
-       }
+                       return null;
 
-       /**
-        * Converts string into a GZipped input stream.
-        *
-        * @param contents The contents to compress.
-        * @return The input stream converted to GZip.
-        * @throws Exception Exception occurred.
-        */
-       public static byte[] compress(String contents) throws Exception {
-               var baos = new ByteArrayOutputStream(contents.length()>>1);
-               try (var gos = new GZIPOutputStream(baos)) {
-                       gos.write(contents.getBytes());
-                       gos.finish();
-                       gos.flush();
+               var needsEncode = false;
+
+               for (var i = 0; i < s.length() && ! needsEncode; i++)
+                       needsEncode |= (! unencodedChars.contains(s.charAt(i)));
+
+               if (needsEncode) {
+                       try {
+                               return URLEncoder.encode(s, "UTF-8");
+                       } catch (UnsupportedEncodingException e) {/* Won't 
happen */}
                }
-               return baos.toByteArray();
-       }
 
-       /**
-        * Converts a GZipped input stream into a string.
-        *
-        * @param is The contents to decompress.
-        * @return The string.
-        * @throws Exception Exception occurred.
-        */
-       public static String decompress(byte[] is) throws Exception {
-               return read(new GZIPInputStream(new ByteArrayInputStream(is)));
+               return s;
        }
 
        /**
-        * Converts the specified object to a comma-delimited list.
+        * Same as {@link #urlEncode(String)} except only escapes characters 
that absolutely need to be escaped.
         *
-        * @param o The object to convert.
-        * @return The specified object as a comma-delimited list.
+        * @param s The string to escape.
+        * @return The encoded string, or <jk>null</jk> if input is 
<jk>null</jk>.
         */
-       public static String toCdl(Object o) {
-               if (o == null)
+       public static String urlEncodeLax(String s) {
+               if (s == null)
                        return null;
-               if (o.getClass().isArray()) {
-                       var sb = new StringBuilder();
-                       for (int i = 0, j = Array.getLength(o); i < j; i++) {
-                               if (i > 0)
-                                       sb.append(", ");
-                               sb.append(Array.get(o, i));
+               var needsEncode = false;
+               for (var i = 0; i < s.length() && ! needsEncode; i++)
+                       needsEncode |= (! 
unencodedCharsLax.contains(s.charAt(i)));
+               if (needsEncode) {
+                       var sb = new StringBuilder(s.length()*2);
+                       for (var i = 0; i < s.length(); i++) {
+                               var c = s.charAt(i);
+                               if (unencodedCharsLax.contains(c))
+                                       sb.append(c);
+                               else if (c == ' ')
+                                       sb.append("+");
+                               else if (c <= 127)
+                                       sb.append('%').append(toHex2(c));
+                               else
+                                       try {
+                                               
sb.append(URLEncoder.encode(""+c, "UTF-8"));  // Yuck.
+                                       } catch (UnsupportedEncodingException 
e) {
+                                               // Not possible.
+                                       }
                        }
-                       return sb.toString();
+                       s = sb.toString();
                }
-               if (o instanceof Collection)
-                       return Utils.join((Collection<?>)o, ", ");
-               return o.toString();
+               return s;
        }
 
        /**
-        * Returns <jk>true</jk> if the specified character is a valid number 
character.
+        * Similar to {@link URLEncoder#encode(String, String)} but doesn't 
encode <js>"/"</js> characters.
         *
-        * @param c The character to check.
-        * @return <jk>true</jk> if the specified character is a valid number 
character.
+        * @param o The object to encode.
+        * @return The URL encoded string, or <jk>null</jk> if the object was 
null.
         */
-       public static boolean isNumberChar(char c) {
-               return numberChars.contains(c);
-       }
+       public static String urlEncodePath(Object o) {
 
-       public static String toUtf8(byte[] b) {
-               return b == null ? null : new String(b, IOUtils.UTF8);
-       }
+               if (o == null)
+                       return null;
 
-       public static String toUtf8(InputStream is) {
-               return safe(()->is == null ? null : new String(readBytes(is), 
IOUtils.UTF8));
-       }
+               var s = Utils.s(o);
 
-       public static String toHex(InputStream is) {
-               return safe(()->is == null ? null : toHex(readBytes(is)));
-       }
+               var needsEncode = false;
+               for (var i = 0; i < s.length() && ! needsEncode; i++)
+                       needsEncode = 
URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i));
+               if (! needsEncode)
+                       return s;
 
-       /**
-        * Same as {@link Utils#join(Collection, char)} but escapes the 
delimiter if found in the tokens.
-        *
-        * @param tokens The tokens to join.
-        * @param d The delimiter.
-        * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, 
returns <jk>null</jk>.
-        */
-       public static String joine(List<?> tokens, char d) {
-               if (tokens == null)
-                       return null;
-               var as = getEscapeSet(d);
                var sb = new StringBuilder();
-               for (int i = 0, j = tokens.size(); i < j; i++) {
-                       if (i > 0)
-                               sb.append(d);
-                       sb.append(escapeChars(Utils.s(tokens.get(i)), as));
+               var caw = new CharArrayWriter();
+               var caseDiff = ('a' - 'A');
+
+               for (var i = 0; i < s.length();) {
+                       var c = s.charAt(i);
+                       if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) {
+                               sb.append(c);
+                               i++;  // NOSONAR - Intentional.
+                       } else {
+                               if (c == ' ') {
+                                       sb.append('+');
+                                       i++;  // NOSONAR - Intentional.
+                               } else {
+                                       do {
+                                               caw.write(c);
+                                               if (c >= 0xD800 && c <= 0xDBFF) 
{
+                                                       if ((i+1) < s.length()) 
{  // NOSONAR - Intentional.
+                                                               int d = 
s.charAt(i+1);
+                                                               if (d >= 0xDC00 
&& d <= 0xDFFF) {
+                                                                       
caw.write(d);
+                                                                       i++;  
// NOSONAR - Intentional.
+                                                               }
+                                                       }
+                                               }
+                                               i++;  // NOSONAR - Intentional.
+                                       } while (i < s.length() && 
!URL_ENCODE_PATHINFO_VALIDCHARS.contains((c = s.charAt(i))));   // NOSONAR - 
Intentional.
+
+                                       caw.flush();
+                                       var s2 = new String(caw.toCharArray());
+                                       var ba = s2.getBytes(IOUtils.UTF8);
+                                       for (var element : ba) {
+                                               sb.append('%');
+                                               var ch = forDigit((element >> 
4) & 0xF, 16);
+                                               if (isLetter(ch)) {
+                                                       ch -= caseDiff;
+                                               }
+                                               sb.append(ch);
+                                               ch = forDigit(element & 0xF, 
16);
+                                               if (isLetter(ch)) {
+                                                       ch -= caseDiff;
+                                               }
+                                               sb.append(ch);
+                                       }
+                                       caw.reset();
+                               }
+                       }
                }
                return sb.toString();
        }
+
+       /**
+        * Constructor.
+        */
+       protected StringUtils() {}
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/SystemUtils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/SystemUtils.java
index f041c0086..141c398c6 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/SystemUtils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/SystemUtils.java
@@ -21,12 +21,8 @@ import java.util.function.*;
  */
 public class SystemUtils {
 
-       /**
-        * Constructor.
-        */
-       protected SystemUtils() {}
-
        static final List<Supplier<String>> SHUTDOWN_MESSAGES = new 
CopyOnWriteArrayList<>();
+
        static {
                Runtime.getRuntime().addShutdownHook(new Thread() {
                        @Override
@@ -36,7 +32,6 @@ public class SystemUtils {
                        }
                });
        }
-
        /**
         * Adds a console message to display when the JVM shuts down.
         *
@@ -45,4 +40,9 @@ public class SystemUtils {
        public static void shutdownMessage(Supplier<String> message) {
                SHUTDOWN_MESSAGES.add(message);
        }
+
+       /**
+        * Constructor.
+        */
+       protected SystemUtils() {}
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ThrowableUtils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ThrowableUtils.java
index d3fa8d9a0..fa3ae14dc 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ThrowableUtils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/ThrowableUtils.java
@@ -21,6 +21,21 @@ import org.apache.juneau.common.utils.*;
  */
 public class ThrowableUtils {
 
+       /**
+        * Interface used with {@link #safeSupplier(SupplierWithThrowable)}.
+        */
+       @FunctionalInterface
+       public interface SupplierWithThrowable<T> {
+
+               /**
+                * Gets a result.
+                *
+                * @return a result
+                * @throws Throwable if supplier threw an exception.
+                */
+               T get() throws Throwable;
+       }
+
        /**
         * Creates a new {@link RuntimeException}.
         *
@@ -64,54 +79,6 @@ public class ThrowableUtils {
                return null;
        }
 
-       /**
-        * Allows you to wrap a supplier that throws an exception so that it 
can be used in a fluent interface.
-        *
-        * @param <T> The supplier type.
-        * @param supplier The supplier throwing an exception.
-        * @return The supplied result.
-        * @throws RuntimeException if supplier threw an exception.
-        */
-       public static <T> T safeSupplier(SupplierWithThrowable<T> supplier) {
-               try {
-                       return supplier.get();
-               } catch (RuntimeException t) {
-                       throw t;
-               } catch (Throwable t) {
-                       throw asRuntimeException(t);
-               }
-       }
-
-       /**
-        * Interface used with {@link #safeSupplier(SupplierWithThrowable)}.
-        */
-       @FunctionalInterface
-       public interface SupplierWithThrowable<T> {
-
-               /**
-                * Gets a result.
-                *
-                * @return a result
-                * @throws Throwable if supplier threw an exception.
-                */
-               T get() throws Throwable;
-       }
-
-       /**
-        * Runs a snippet of code and encapsulates any throwable inside a 
{@link RuntimeException}.
-        *
-        * @param snippet The snippet of code to run.
-        */
-       public static void safeRun(Snippet snippet) {
-               try {
-                       snippet.run();
-               } catch (RuntimeException t) {
-                       throw t;
-               } catch (Throwable t) {
-                       throw asRuntimeException(t);
-               }
-       }
-
        /**
         * Convenience method for getting a stack trace as a string.
         *
@@ -146,4 +113,37 @@ public class ThrowableUtils {
                }
                return i;
        }
+
+       /**
+        * Runs a snippet of code and encapsulates any throwable inside a 
{@link RuntimeException}.
+        *
+        * @param snippet The snippet of code to run.
+        */
+       public static void safeRun(Snippet snippet) {
+               try {
+                       snippet.run();
+               } catch (RuntimeException t) {
+                       throw t;
+               } catch (Throwable t) {
+                       throw asRuntimeException(t);
+               }
+       }
+
+       /**
+        * Allows you to wrap a supplier that throws an exception so that it 
can be used in a fluent interface.
+        *
+        * @param <T> The supplier type.
+        * @param supplier The supplier throwing an exception.
+        * @return The supplied result.
+        * @throws RuntimeException if supplier threw an exception.
+        */
+       public static <T> T safeSupplier(SupplierWithThrowable<T> supplier) {
+               try {
+                       return supplier.get();
+               } catch (RuntimeException t) {
+                       throw t;
+               } catch (Throwable t) {
+                       throw asRuntimeException(t);
+               }
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/Utils.java
 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/Utils.java
index f21612235..1fc65897f 100644
--- 
a/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/Utils.java
+++ 
b/juneau-core/juneau-common/src/main/java/org/apache/juneau/common/internal/Utils.java
@@ -18,7 +18,6 @@ import static org.apache.juneau.common.internal.StringUtils.*;
 import java.io.*;
 import java.lang.reflect.*;
 import java.nio.charset.*;
-
 import java.text.*;
 import java.time.format.*;
 import java.util.*;

Reply via email to