001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.common.internal;
014
015import static org.apache.juneau.common.internal.ArgUtils.*;
016import static org.apache.juneau.common.internal.IOUtils.*;
017import static org.apache.juneau.common.internal.ThrowableUtils.*;
018
019import java.io.*;
020import java.lang.reflect.*;
021import java.math.*;
022import java.net.*;
023import java.nio.*;
024import java.nio.charset.*;
025import java.text.*;
026import java.util.*;
027import java.util.concurrent.*;
028import java.util.concurrent.atomic.*;
029import java.util.function.*;
030import java.util.regex.*;
031import java.util.stream.*;
032import java.util.zip.*;
033
034import javax.xml.bind.*;
035
036/**
037 * Reusable string utility methods.
038 */
039public final class StringUtils {
040
041   /**
042    * Predicate check to filter out null and empty strings.
043    */
044   public static final Predicate<String> NOT_EMPTY = StringUtils::isNotEmpty;
045
046   private static final AsciiSet numberChars = AsciiSet.create("-xX.+-#pP0123456789abcdefABCDEF");
047   private static final AsciiSet firstNumberChars =AsciiSet.create("+-.#0123456789");
048   private static final AsciiSet octChars = AsciiSet.create("01234567");
049   private static final AsciiSet decChars = AsciiSet.create("0123456789");
050   private static final AsciiSet hexChars = AsciiSet.create("0123456789abcdefABCDEF");
051
052   // Maps 6-bit nibbles to BASE64 characters.
053   private static final char[] base64m1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
054
055   // Characters that do not need to be URL-encoded
056   private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.!~*'()\\").build();
057
058   // Characters that really do not need to be URL-encoded
059   private static final AsciiSet unencodedCharsLax = unencodedChars.copy()
060      .chars(":@$,")  // reserved, but can't be confused in a query parameter.
061      .chars("{}|\\^[]`")  // unwise characters.
062      .build();
063
064   // Valid HTTP header characters (including quoted strings and comments).
065   private static final AsciiSet httpHeaderChars = AsciiSet
066      .create()
067      .chars("\t -")
068      .ranges("!-[","]-}")
069      .build();
070
071   // Maps BASE64 characters to 6-bit nibbles.
072   private static final byte[] base64m2 = new byte[128];
073   static {
074      for (int i = 0; i < 64; i++)
075         base64m2[base64m1[i]] = (byte)i;
076   }
077
078   /**
079    * Parses a number from the specified string.
080    *
081    * @param s The string to parse the number from.
082    * @param type
083    *    The number type to created.
084    *    Can be any of the following:
085    *    <ul>
086    *       <li> Integer
087    *       <li> Double
088    *       <li> Float
089    *       <li> Long
090    *       <li> Short
091    *       <li> Byte
092    *       <li> BigInteger
093    *       <li> BigDecimal
094    *    </ul>
095    *    If <jk>null</jk> or <c>Number</c>, uses the best guess.
096    * @return The parsed number, or <jk>null</jk> if the string was null.
097    */
098   public static Number parseNumber(String s, Class<? extends Number> type) {
099      if (s == null)
100         return null;
101      if (s.isEmpty())
102         s = "0";
103      if (type == null)
104         type = Number.class;
105
106      // Determine the data type if it wasn't specified.
107      boolean isAutoDetect = (type == Number.class);
108      boolean isDecimal = false;
109      if (isAutoDetect) {
110         // If we're auto-detecting, then we use either an Integer, Long, or Double depending on how
111         // long the string is.
112         // An integer range is -2,147,483,648 to 2,147,483,647
113         // An long range is -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
114         isDecimal = isDecimal(s);
115         if (isDecimal) {
116            if (s.length() > 20)
117               type = Double.class;
118            else if (s.length() >= 10)
119               type = Long.class;
120            else
121               type = Integer.class;
122         }
123         else if (isFloat(s))
124            type = Double.class;
125         else
126            throw new NumberFormatException(s);
127      }
128
129      if (type == Double.class || type == Double.TYPE) {
130         Double d = Double.valueOf(s);
131         Float f = Float.valueOf(s);
132         if (isAutoDetect && (!isDecimal) && d.toString().equals(f.toString()))
133            return f;
134         return d;
135      }
136      if (type == Float.class || type == Float.TYPE)
137         return Float.valueOf(s);
138      if (type == BigDecimal.class)
139         return new BigDecimal(s);
140      if (type == Long.class || type == Long.TYPE || type == AtomicLong.class) {
141         try {
142            Long l = Long.decode(s);
143            if (type == AtomicLong.class)
144               return new AtomicLong(l);
145            if (isAutoDetect && l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) {
146               // This occurs if the string is 10 characters long but is still a valid integer value.
147               return l.intValue();
148            }
149            return l;
150         } catch (NumberFormatException e) {
151            if (isAutoDetect) {
152               // This occurs if the string is 20 characters long but still falls outside the range of a valid long.
153               return Double.valueOf(s);
154            }
155            throw e;
156         }
157      }
158      if (type == Integer.class || type == Integer.TYPE)
159         return Integer.decode(s);
160      if (type == Short.class || type == Short.TYPE)
161         return Short.decode(s);
162      if (type == Byte.class || type == Byte.TYPE)
163         return Byte.decode(s);
164      if (type == BigInteger.class)
165         return new BigInteger(s);
166      if (type == AtomicInteger.class)
167         return new AtomicInteger(Integer.decode(s));
168      throw new NumberFormatException("Unsupported Number type: "+type.getName());
169   }
170
171   private static final Pattern fpRegex = Pattern.compile(
172      "[+-]?(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]*"
173   );
174
175   /**
176    * Converts a <c>String</c> to a <c>Character</c>
177    *
178    * @param o The string to convert.
179    * @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.
180    */
181   public static Character parseCharacter(Object o) {
182      if (o == null)
183         return null;
184      String s = o.toString();
185      if (s.isEmpty())
186         return null;
187      if (s.length() == 1)
188         return s.charAt(0);
189      throw new IllegalArgumentException("Invalid character: '" + s + "'");
190   }
191
192   /**
193    * Returns <jk>true</jk> if this string can be parsed by {@link #parseNumber(String, Class)}.
194    *
195    * @param s The string to check.
196    * @return <jk>true</jk> if this string can be parsed without causing an exception.
197    */
198   public static boolean isNumeric(String s) {
199      if (s == null || s.isEmpty() || ! isFirstNumberChar(s.charAt(0)))
200         return false;
201      return isDecimal(s) || isFloat(s);
202   }
203
204   /**
205    * Returns <jk>true</jk> if the specified character is a valid first character for a number.
206    *
207    * @param c The character to test.
208    * @return <jk>true</jk> if the specified character is a valid first character for a number.
209    */
210   public static boolean isFirstNumberChar(char c) {
211      return firstNumberChars.contains(c);
212   }
213
214   /**
215    * Returns <jk>true</jk> if the specified string is a floating point number.
216    *
217    * @param s The string to check.
218    * @return <jk>true</jk> if the specified string is a floating point number.
219    */
220   public static boolean isFloat(String s) {
221      if (s == null || s.isEmpty())
222         return false;
223      if (! firstNumberChars.contains(s.charAt(0)))
224         return (s.equals("NaN") || s.equals("Infinity"));
225      int i = 0;
226      int length = s.length();
227      char c = s.charAt(0);
228      if (c == '+' || c == '-')
229         i++;
230      if (i == length)
231         return false;
232      c = s.charAt(i++);
233      if (c == '.' || decChars.contains(c)) {
234         return fpRegex.matcher(s).matches();
235      }
236      return false;
237   }
238
239   /**
240    * Returns <jk>true</jk> if the specified string is numeric.
241    *
242    * @param s The string to check.
243    * @return <jk>true</jk> if the specified string is numeric.
244    */
245   public static boolean isDecimal(String s) {
246      if (s == null || s.isEmpty() || ! firstNumberChars.contains(s.charAt(0)))
247         return false;
248      int i = 0;
249      int length = s.length();
250      char c = s.charAt(0);
251      boolean isPrefixed = false;
252      if (c == '+' || c == '-') {
253         isPrefixed = true;
254         i++;
255      }
256      if (i == length)
257         return false;
258      c = s.charAt(i++);
259      if (c == '0' && length > (isPrefixed ? 2 : 1)) {
260         c = s.charAt(i++);
261         if (c == 'x' || c == 'X') {
262            for (int j = i; j < length; j++) {
263               if (! hexChars.contains(s.charAt(j)))
264                  return false;
265            }
266         } else if (octChars.contains(c)) {
267            for (int j = i; j < length; j++)
268               if (! octChars.contains(s.charAt(j)))
269                  return false;
270         } else {
271            return false;
272         }
273      } else if (c == '#') {
274         for (int j = i; j < length; j++) {
275            if (! hexChars.contains(s.charAt(j)))
276               return false;
277         }
278      } else if (decChars.contains(c)) {
279         for (int j = i; j < length; j++)
280            if (! decChars.contains(s.charAt(j)))
281               return false;
282      } else {
283         return false;
284      }
285      return true;
286   }
287
288   /**
289    * Join the specified tokens into a delimited string.
290    *
291    * @param tokens The tokens to join.
292    * @param separator The delimiter.
293    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
294    */
295   public static String join(Object[] tokens, String separator) {
296      if (tokens == null)
297         return null;
298      StringBuilder sb = new StringBuilder();
299      for (int i = 0; i < tokens.length; i++) {
300         if (i > 0)
301            sb.append(separator);
302         sb.append(tokens[i]);
303      }
304      return sb.toString();
305   }
306
307   /**
308    * Join the specified tokens into a delimited string.
309    *
310    * @param tokens The tokens to join.
311    * @param d The delimiter.
312    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
313    */
314   public static String join(Collection<?> tokens, String d) {
315      if (tokens == null)
316         return null;
317      return join(tokens, d, new StringBuilder()).toString();
318   }
319
320   /**
321    * Join the specified tokens into a delimited string.
322    *
323    * @param tokens The tokens to join.
324    * @param d The delimiter.
325    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
326    */
327   public static String join(List<?> tokens, String d) {
328      if (tokens == null)
329         return null;
330      return join(tokens, d, new StringBuilder()).toString();
331   }
332
333   /**
334    * Joins the specified tokens into a delimited string and writes the output to the specified string builder.
335    *
336    * @param tokens The tokens to join.
337    * @param d The delimiter.
338    * @param sb The string builder to append the response to.
339    * @return The same string builder passed in as <c>sb</c>.
340    */
341   public static StringBuilder join(Collection<?> tokens, String d, StringBuilder sb) {
342      if (tokens == null)
343         return sb;
344      for (Iterator<?> iter = tokens.iterator(); iter.hasNext();) {
345         sb.append(iter.next());
346         if (iter.hasNext())
347            sb.append(d);
348      }
349      return sb;
350   }
351
352   /**
353    * Joins the specified tokens into a delimited string and writes the output to the specified string builder.
354    *
355    * @param tokens The tokens to join.
356    * @param d The delimiter.
357    * @param sb The string builder to append the response to.
358    * @return The same string builder passed in as <c>sb</c>.
359    */
360   public static StringBuilder join(List<?> tokens, String d, StringBuilder sb) {
361      if (tokens == null)
362         return sb;
363      for (int i = 0, j = tokens.size(); i < j; i++) {
364         if (i > 0)
365            sb.append(d);
366         sb.append(tokens.get(i));
367      }
368      return sb;
369   }
370
371   /**
372    * Joins the specified tokens into a delimited string.
373    *
374    * @param tokens The tokens to join.
375    * @param d The delimiter.
376    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
377    */
378   public static String join(Object[] tokens, char d) {
379      if (tokens == null)
380         return null;
381      if (tokens.length == 1)
382         return emptyIfNull(stringify(tokens[0]));
383      return join(tokens, d, new StringBuilder()).toString();
384   }
385
386   private static AsciiSet getEscapeSet(char c) {
387      AsciiSet s = ESCAPE_SETS.get(c);
388      if (s == null) {
389         s = AsciiSet.create().chars(c, '\\').build();
390         ESCAPE_SETS.put(c, s);
391      }
392      return s;
393   }
394   static Map<Character,AsciiSet> ESCAPE_SETS = new ConcurrentHashMap<>();
395
396   /**
397    * Join the specified tokens into a delimited string and writes the output to the specified string builder.
398    *
399    * @param tokens The tokens to join.
400    * @param d The delimiter.
401    * @param sb The string builder to append the response to.
402    * @return The same string builder passed in as <c>sb</c>.
403    */
404   public static StringBuilder join(Object[] tokens, char d, StringBuilder sb) {
405      if (tokens == null)
406         return sb;
407      for (int i = 0; i < tokens.length; i++) {
408         if (i > 0)
409            sb.append(d);
410         sb.append(tokens[i]);
411      }
412      return sb;
413   }
414
415   /**
416    * Join the specified tokens into a delimited string.
417    *
418    * @param tokens The tokens to join.
419    * @param d The delimiter.
420    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
421    */
422   public static String join(int[] tokens, char d) {
423      if (tokens == null)
424         return null;
425      StringBuilder sb = new StringBuilder();
426      for (int i = 0; i < tokens.length; i++) {
427         if (i > 0)
428            sb.append(d);
429         sb.append(tokens[i]);
430      }
431      return sb.toString();
432   }
433
434   /**
435    * Join the specified tokens into a delimited string.
436    *
437    * @param tokens The tokens to join.
438    * @param d The delimiter.
439    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
440    */
441   public static String join(Collection<?> tokens, char d) {
442      if (tokens == null)
443         return null;
444      StringBuilder sb = new StringBuilder();
445      for (Iterator<?> iter = tokens.iterator(); iter.hasNext();) {
446         sb.append(iter.next());
447         if (iter.hasNext())
448            sb.append(d);
449      }
450      return sb.toString();
451   }
452
453   /**
454    * Join the specified tokens into a delimited string.
455    *
456    * @param tokens The tokens to join.
457    * @param d The delimiter.
458    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
459    */
460   public static String join(List<?> tokens, char d) {
461      if (tokens == null)
462         return null;
463      StringBuilder sb = new StringBuilder();
464      for (int i = 0, j = tokens.size(); i < j; i++) {
465         if (i > 0)
466            sb.append(d);
467         sb.append(tokens.get(i));
468      }
469      return sb.toString();
470   }
471
472   /**
473    * Same as {@link #join(Collection, char)} but escapes the delimiter if found in the tokens.
474    *
475    * @param tokens The tokens to join.
476    * @param d The delimiter.
477    * @return The delimited string.  If <c>tokens</c> is <jk>null</jk>, returns <jk>null</jk>.
478    */
479   public static String joine(List<?> tokens, char d) {
480      if (tokens == null)
481         return null;
482      AsciiSet as = getEscapeSet(d);
483      StringBuilder sb = new StringBuilder();
484      for (int i = 0, j = tokens.size(); i < j; i++) {
485         if (i > 0)
486            sb.append(d);
487         sb.append(escapeChars(stringify(tokens.get(i)), as));
488      }
489      return sb.toString();
490   }
491
492   /**
493    * Joins tokens with newlines.
494    *
495    * @param tokens The tokens to concatenate.
496    * @return A string with the specified tokens contatenated with newlines.
497    */
498   public static String joinnl(Object[] tokens) {
499      return join(tokens, '\n');
500   }
501
502   /**
503    * Shortcut for calling <code>split(s, <js>','</js>)</code>
504    *
505    * @param s The string to split.  Can be <jk>null</jk>.
506    * @return The tokens, or <jk>null</jk> if the string was null.
507    */
508   public static String[] split(String s) {
509      return split(s, ',');
510   }
511
512   /**
513    * Same as {@link #split(String)} but consumes the tokens instead of creating an array.
514    *
515    * @param s The string to split.
516    * @param consumer The consumer of the tokens.
517    */
518   public static void split(String s, Consumer<String> consumer) {
519      split(s, ',', consumer);
520   }
521
522   /**
523    * Splits a character-delimited string into a string array.
524    *
525    * <p>
526    * Does not split on escaped-delimiters (e.g. "\,");
527    * Resulting tokens are trimmed of whitespace.
528    *
529    * <p>
530    * <b>NOTE:</b>  This behavior is different than the Jakarta equivalent.
531    * split("a,b,c",',') -&gt; {"a","b","c"}
532    * split("a, b ,c ",',') -&gt; {"a","b","c"}
533    * split("a,,c",',') -&gt; {"a","","c"}
534    * split(",,",',') -&gt; {"","",""}
535    * split("",',') -&gt; {}
536    * split(null,',') -&gt; null
537    * split("a,b\,c,d", ',', false) -&gt; {"a","b\,c","d"}
538    * split("a,b\\,c,d", ',', false) -&gt; {"a","b\","c","d"}
539    * split("a,b\,c,d", ',', true) -&gt; {"a","b,c","d"}
540    *
541    * @param s The string to split.  Can be <jk>null</jk>.
542    * @param c The character to split on.
543    * @return The tokens, or <jk>null</jk> if the string was null.
544    */
545   public static String[] split(String s, char c) {
546      return split(s, c, Integer.MAX_VALUE);
547   }
548
549   /**
550    * Same as {@link #split(String,char)} but consumes the tokens instead of creating an array.
551    *
552    * @param s The string to split.
553    * @param c The character to split on.
554    * @param consumer The consumer of the tokens.
555    */
556   public static void split(String s, char c, Consumer<String> consumer) {
557      AsciiSet escapeChars = getEscapeSet(c);
558
559      if (isEmpty(s))
560         return;
561      if (s.indexOf(c) == -1) {
562         consumer.accept(s);
563         return;
564      }
565
566      int x1 = 0, escapeCount = 0;
567      for (int i = 0; i < s.length(); i++) {
568         if (s.charAt(i) == '\\')
569            escapeCount++;
570         else if (s.charAt(i)==c && escapeCount % 2 == 0) {
571            String s2 = s.substring(x1, i);
572            String s3 = unEscapeChars(s2, escapeChars);
573            consumer.accept(s3.trim());
574            x1 = i+1;
575         }
576         if (s.charAt(i) != '\\')
577            escapeCount = 0;
578      }
579      String s2 = s.substring(x1);
580      String s3 = unEscapeChars(s2, escapeChars);
581      consumer.accept(s3.trim());
582   }
583
584   /**
585    * Same as {@link #split(String, char)} but limits the number of tokens returned.
586    *
587    * @param s The string to split.  Can be <jk>null</jk>.
588    * @param c The character to split on.
589    * @param limit The maximum number of tokens to return.
590    * @return The tokens, or <jk>null</jk> if the string was null.
591    */
592   public static String[] split(String s, char c, int limit) {
593
594      AsciiSet escapeChars = getEscapeSet(c);
595
596      if (s == null)
597         return null;
598      if (isEmpty(s))
599         return new String[0];
600      if (s.indexOf(c) == -1)
601         return new String[]{s};
602
603      List<String> l = new LinkedList<>();
604      char[] sArray = s.toCharArray();
605      int x1 = 0, escapeCount = 0;
606      limit--;
607      for (int i = 0; i < sArray.length && limit > 0; i++) {
608         if (sArray[i] == '\\')
609            escapeCount++;
610         else if (sArray[i]==c && escapeCount % 2 == 0) {
611            String s2 = new String(sArray, x1, i-x1);
612            String s3 = unEscapeChars(s2, escapeChars);
613            l.add(s3.trim());
614            limit--;
615            x1 = i+1;
616         }
617         if (sArray[i] != '\\')
618            escapeCount = 0;
619      }
620      String s2 = new String(sArray, x1, sArray.length-x1);
621      String s3 = unEscapeChars(s2, escapeChars);
622      l.add(s3.trim());
623
624      return l.toArray(new String[l.size()]);
625   }
626
627   /**
628    * Same as {@link #split(String, char)} except splits all strings in the input and returns a single result.
629    *
630    * @param s The string to split.  Can be <jk>null</jk>.
631    * @param c The character to split on.
632    * @return The tokens.
633    */
634   public static String[] split(String[] s, char c) {
635      if (s == null)
636         return null;
637      List<String> l = new LinkedList<>();
638      for (String ss : s) {
639         if (ss == null || ss.indexOf(c) == -1)
640            l.add(ss);
641         else
642            Collections.addAll(l, split(ss, c));
643      }
644      return l.toArray(new String[l.size()]);
645   }
646
647   /**
648    * Splits a list of key-value pairs into an ordered map.
649    *
650    * <p>
651    * Example:
652    * <p class='bjava'>
653    *    String <jv>in</jv> = <js>"foo=1;bar=2"</js>;
654    *    Map <jv>map</jv> = StringUtils.<jsm>splitMap</jsm>(in, <js>';'</js>, <js>'='</js>, <jk>true</jk>);
655    * </p>
656    *
657    * @param s The string to split.
658    * @param trim Trim strings after parsing.
659    * @return The parsed map.  Never <jk>null</jk>.
660    */
661   public static Map<String,String> splitMap(String s, boolean trim) {
662
663      if (s == null)
664         return null;
665      if (isEmpty(s))
666         return Collections.emptyMap();
667
668      Map<String,String> m = new LinkedHashMap<>();
669
670      int
671         S1 = 1,  // Found start of key, looking for equals.
672         S2 = 2;  // Found equals, looking for delimiter (or end).
673
674      int state = S1;
675
676      char[] sArray = s.toCharArray();
677      int x1 = 0, escapeCount = 0;
678      String key = null;
679      for (int i = 0; i < sArray.length + 1; i++) {
680         char c = i == sArray.length ? ',' : sArray[i];
681         if (c == '\\')
682            escapeCount++;
683         if (escapeCount % 2 == 0) {
684            if (state == S1) {
685               if (c == '=') {
686                  key = s.substring(x1, i);
687                  if (trim)
688                     key = trim(key);
689                  key = unEscapeChars(key, MAP_ESCAPE_SET);
690                  state = S2;
691                  x1 = i+1;
692               } else if (c == ',') {
693                  key = s.substring(x1, i);
694                  if (trim)
695                     key = trim(key);
696                  key = unEscapeChars(key, MAP_ESCAPE_SET);
697                  m.put(key, "");
698                  state = S1;
699                  x1 = i+1;
700               }
701            } else if (state == S2) {
702               if (c == ',') {
703                  String val = s.substring(x1, i);
704                  if (trim)
705                     val = trim(val);
706                  val = unEscapeChars(val, MAP_ESCAPE_SET);
707                  m.put(key, val);
708                  key = null;
709                  x1 = i+1;
710                  state = S1;
711               }
712            }
713         }
714         if (c != '\\')
715            escapeCount = 0;
716      }
717
718      return m;
719   }
720
721   private static final AsciiSet MAP_ESCAPE_SET = AsciiSet.create(",=\\");
722
723   /**
724    * Returns <jk>true</jk> if the specified string contains any of the specified characters.
725    *
726    * @param s The string to test.
727    * @param chars The characters to look for.
728    * @return
729    *    <jk>true</jk> if the specified string contains any of the specified characters.
730    *    <br><jk>false</jk> if the string is <jk>null</jk>.
731    */
732   public static boolean containsAny(String s, char...chars) {
733      if (s == null)
734         return false;
735      for (int i = 0, j = s.length(); i < j; i++) {
736         char c = s.charAt(i);
737         for (char c2 : chars)
738            if (c == c2)
739               return true;
740      }
741      return false;
742   }
743
744   /**
745    * Splits a space-delimited string with optionally quoted arguments.
746    *
747    * <p>
748    * Examples:
749    * <ul>
750    *    <li><js>"foo"</js> =&gt; <c>["foo"]</c>
751    *    <li><js>" foo "</js> =&gt; <c>["foo"]</c>
752    *    <li><js>"foo bar baz"</js> =&gt; <c>["foo","bar","baz"]</c>
753    *    <li><js>"foo 'bar baz'"</js> =&gt; <c>["foo","bar baz"]</c>
754    *    <li><js>"foo \"bar baz\""</js> =&gt; <c>["foo","bar baz"]</c>
755    *    <li><js>"foo 'bar\'baz'"</js> =&gt; <c>["foo","bar'baz"]</c>
756    * </ul>
757    *
758    * @param s The input string.
759    * @return
760    *    The results, or <jk>null</jk> if the input was <jk>null</jk>.
761    *    <br>An empty string results in an empty array.
762    */
763   public static String[] splitQuoted(String s) {
764      return splitQuoted(s, false);
765   }
766
767   /**
768    * Same as {@link #splitQuoted(String)} but allows you to optionally keep the quote characters.
769    *
770    * @param s The input string.
771    * @param keepQuotes If <jk>true</jk>, quote characters are kept on the tokens.
772    * @return
773    *    The results, or <jk>null</jk> if the input was <jk>null</jk>.
774    *    <br>An empty string results in an empty array.
775    */
776   public static String[] splitQuoted(String s, boolean keepQuotes) {
777
778      if (s == null)
779         return null;
780
781      s = s.trim();
782
783      if (isEmpty(s))
784         return new String[0];
785
786      if (! containsAny(s, ' ', '\t', '\'', '"'))
787         return new String[]{s};
788
789      int
790         S1 = 1,  // Looking for start of token.
791         S2 = 2,  // Found ', looking for end '
792         S3 = 3,  // Found ", looking for end "
793         S4 = 4;  // Found non-whitespace, looking for end whitespace.
794
795      int state = S1;
796
797      boolean isInEscape = false, needsUnescape = false;
798      int mark = 0;
799
800      List<String> l = new ArrayList<>();
801      for (int i = 0; i < s.length(); i++) {
802         char c = s.charAt(i);
803
804         if (state == S1) {
805            if (c == '\'') {
806               state = S2;
807               mark = keepQuotes ? i : i+1;
808            } else if (c == '"') {
809               state = S3;
810               mark = keepQuotes ? i : i+1;
811            } else if (c != ' ' && c != '\t') {
812               state = S4;
813               mark = i;
814            }
815         } else if (state == S2 || state == S3) {
816            if (c == '\\') {
817               isInEscape = ! isInEscape;
818               needsUnescape = ! keepQuotes;
819            } else if (! isInEscape) {
820               if (c == (state == S2 ? '\'' : '"')) {
821                  String s2 = s.substring(mark, keepQuotes ? i+1 : i);
822                  if (needsUnescape)
823                     s2 = unEscapeChars(s2, QUOTE_ESCAPE_SET);
824                  l.add(s2);
825                  state = S1;
826                  isInEscape = needsUnescape = false;
827               }
828            } else {
829               isInEscape = false;
830            }
831         } else if (state == S4) {
832            if (c == ' ' || c == '\t') {
833               l.add(s.substring(mark, i));
834               state = S1;
835            }
836         }
837      }
838      if (state == S4)
839         l.add(s.substring(mark));
840      else if (state == S2 || state == S3)
841         throw new IllegalArgumentException("Unmatched string quotes: " + s);
842      return l.toArray(new String[l.size()]);
843   }
844
845   private static final AsciiSet QUOTE_ESCAPE_SET = AsciiSet.create("\"'\\");
846
847   /**
848    * Returns <jk>true</jk> if specified string is <jk>null</jk> or empty.
849    *
850    * @param s The string to check.
851    * @return <jk>true</jk> if specified string is <jk>null</jk> or empty.
852    */
853   public static boolean isEmpty(String s) {
854      return s == null || s.isEmpty();
855   }
856
857   /**
858    * Returns <jk>true</jk> if specified charsequence is <jk>null</jk> or empty.
859    *
860    * @param s The string to check.
861    * @return <jk>true</jk> if specified charsequence is <jk>null</jk> or empty.
862    */
863   public static boolean isEmpty(CharSequence s) {
864      return s == null || s.length() == 0;
865   }
866
867   /**
868    * Returns <jk>true</jk> if specified string is <jk>null</jk> or empty or consists of only blanks.
869    *
870    * @param s The string to check.
871    * @return <jk>true</jk> if specified string is <jk>null</jk> or emptyor consists of only blanks.
872    */
873   public static boolean isEmptyOrBlank(String s) {
874      return s == null || s.trim().isEmpty();
875   }
876
877   /**
878    * Returns <jk>true</jk> if specified string is not <jk>null</jk> or empty.
879    *
880    * @param s The string to check.
881    * @return <jk>true</jk> if specified string is not <jk>null</jk> or empty.
882    */
883   public static boolean isNotEmpty(String s) {
884      return ! isEmpty(s);
885   }
886
887   /**
888    * Returns <jk>true</jk> if either of the specified strings are not <jk>null</jk> or empty.
889    *
890    * @param s1 The string to check.
891    * @param s2 The string to check.
892    * @return <jk>true</jk> if either of the specified strings are not <jk>null</jk> or empty.
893    */
894   public static boolean isNotEmpty(String s1, String s2) {
895      return isNotEmpty(s1) || isNotEmpty(s2);
896   }
897
898   /**
899    * Returns <jk>null</jk> if the specified string is <jk>null</jk> or empty.
900    *
901    * @param s The string to check.
902    * @return <jk>null</jk> if the specified string is <jk>null</jk> or empty, or the same string if not.
903    */
904   public static String nullIfEmpty(String s) {
905      if (s == null || s.isEmpty())
906         return null;
907      return s;
908   }
909
910   /**
911    * Returns an empty string if the specified string is <jk>null</jk>.
912    *
913    * @param s The string to check.
914    * @return An empty string if the specified string is <jk>null</jk>, or the same string otherwise.
915    */
916   public static String emptyIfNull(String s) {
917      if (s == null)
918         return "";
919      return s;
920   }
921
922   /**
923    * Returns an empty string if the specified object is <jk>null</jk>.
924    *
925    * @param o The object to check.
926    * @return An empty string if the specified object is <jk>null</jk>, or the object converted to a string using {@link String#toString()}.
927    */
928   public static String emptyIfNull(Object o) {
929      if (o == null)
930         return "";
931      return o.toString();
932   }
933
934
935   /**
936    * Removes escape characters from the specified characters.
937    *
938    * @param s The string to remove escape characters from.
939    * @param escaped The characters escaped.
940    * @return A new string if characters were removed, or the same string if not or if the input was <jk>null</jk>.
941    */
942   public static String unEscapeChars(String s, AsciiSet escaped) {
943      if (s == null || s.isEmpty())
944         return s;
945      int count = 0;
946      for (int i = 0; i < s.length(); i++)
947         if (escaped.contains(s.charAt(i)))
948            count++;
949      if (count == 0)
950         return s;
951      StringBuffer sb = new StringBuffer(s.length()-count);
952      for (int i = 0; i < s.length(); i++) {
953         char c = s.charAt(i);
954
955         if (c == '\\') {
956            if (i+1 != s.length()) {
957               char c2 = s.charAt(i+1);
958               if (escaped.contains(c2)) {
959                  i++;
960               } else if (c2 == '\\') {
961                  sb.append('\\');
962                  i++;
963               }
964            }
965         }
966         sb.append(s.charAt(i));
967      }
968      return sb.toString();
969   }
970
971   /**
972    * Escapes the specified characters in the string.
973    *
974    * @param s The string with characters to escape.
975    * @param escaped The characters to escape.
976    * @return The string with characters escaped, or the same string if no escapable characters were found.
977    */
978   public static String escapeChars(String s, AsciiSet escaped) {
979      if (s == null || s.length() == 0)
980         return s;
981
982      int count = 0;
983      for (int i = 0; i < s.length(); i++)
984         if (escaped.contains(s.charAt(i)))
985            count++;
986      if (count == 0)
987         return s;
988
989      StringBuffer sb = new StringBuffer(s.length() + count);
990      for (int i = 0; i < s.length(); i++) {
991         char c = s.charAt(i);
992         if (escaped.contains(c))
993            sb.append('\\');
994         sb.append(c);
995      }
996      return sb.toString();
997   }
998
999   /**
1000    * Debug method for rendering non-ASCII character sequences.
1001    *
1002    * @param s The string to decode.
1003    * @return A string with non-ASCII characters converted to <js>"[hex]"</js> sequences.
1004    */
1005   public static String decodeHex(String s) {
1006      if (s == null)
1007         return null;
1008      StringBuilder sb = new StringBuilder();
1009      for (char c : s.toCharArray()) {
1010         if (c < ' ' || c > '~')
1011            sb.append("["+Integer.toHexString(c)+"]");
1012         else
1013            sb.append(c);
1014      }
1015      return sb.toString();
1016   }
1017
1018   /**
1019    * An efficient method for checking if a string starts with a character.
1020    *
1021    * @param s The string to check.  Can be <jk>null</jk>.
1022    * @param c The character to check for.
1023    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and starts with the specified character.
1024    */
1025   public static boolean startsWith(String s, char c) {
1026      if (s != null) {
1027         int i = s.length();
1028         if (i > 0)
1029            return s.charAt(0) == c;
1030      }
1031      return false;
1032   }
1033
1034   /**
1035    * An efficient method for checking if a string ends with a character.
1036    *
1037    * @param s The string to check.  Can be <jk>null</jk>.
1038    * @param c The character to check for.
1039    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character.
1040    */
1041   public static boolean endsWith(String s, char c) {
1042      if (s != null) {
1043         int i = s.length();
1044         if (i > 0)
1045            return s.charAt(i-1) == c;
1046      }
1047      return false;
1048   }
1049
1050   /**
1051    * Same as {@link #endsWith(String, char)} except check for multiple characters.
1052    *
1053    * @param s The string to check.  Can be <jk>null</jk>.
1054    * @param c The characters to check for.
1055    * @return <jk>true</jk> if the specified string is not <jk>null</jk> and ends with the specified character.
1056    */
1057   public static boolean endsWith(String s, char...c) {
1058      if (s != null) {
1059         int i = s.length();
1060         if (i > 0) {
1061            char c2 = s.charAt(i-1);
1062            for (char cc : c)
1063               if (c2 == cc)
1064                  return true;
1065         }
1066      }
1067      return false;
1068   }
1069
1070   /**
1071    * Converts the specified number into a 2 hexadecimal characters.
1072    *
1073    * @param num The number to convert to hex.
1074    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
1075    */
1076   public static char[] toHex2(int num) {
1077      if (num < 0 || num > 255)
1078         throw new NumberFormatException("toHex2 can only be used on numbers between 0 and 255");
1079      char[] n = new char[2];
1080      int a = num%16;
1081      n[1] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1082      a = (num/16)%16;
1083      n[0] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1084      return n;
1085   }
1086
1087   private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
1088
1089   /**
1090    * Converts the specified byte into a 2 hexadecimal characters.
1091    *
1092    * @param b The number to convert to hex.
1093    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
1094    */
1095   public static String toHex(byte b) {
1096      char[] c = new char[2];
1097      int v = b & 0xFF;
1098      c[0] = hexArray[v >>> 4];
1099      c[1] = hexArray[v & 0x0F];
1100      return new String(c);
1101   }
1102
1103   /**
1104    * Converts the specified bytes into a readable string.
1105    *
1106    * @param b The number to convert to hex.
1107    * @return A <code><jk>char</jk>[2]</code> containing the specified characters.
1108    */
1109   public static String toReadableBytes(byte[] b) {
1110      StringBuilder sb = new StringBuilder();
1111      for (byte b2 : b)
1112         sb.append((b2 < ' ' || b2 > 'z') ? String.format("[%02X]", b2) : (char)b2 + "   ");
1113      sb.append("\n");
1114      for (byte b2 : b)
1115         sb.append(String.format("[%02X]", b2));
1116      return sb.toString();
1117   }
1118
1119   /**
1120    * Converts the specified number into a 4 hexadecimal characters.
1121    *
1122    * @param num The number to convert to hex.
1123    * @return A <code><jk>char</jk>[4]</code> containing the specified characters.
1124    */
1125   public static char[] toHex4(int num) {
1126      char[] n = new char[4];
1127      int a = num%16;
1128      n[3] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1129      int base = 16;
1130      for (int i = 1; i < 4; i++) {
1131         a = (num/base)%16;
1132         base <<= 4;
1133         n[3-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1134      }
1135      return n;
1136   }
1137
1138   /**
1139    * Converts the specified number into a 8 hexadecimal characters.
1140    *
1141    * @param num The number to convert to hex.
1142    * @return A <code><jk>char</jk>[8]</code> containing the specified characters.
1143    */
1144   public static char[] toHex8(long num) {
1145      char[] n = new char[8];
1146      long a = num%16;
1147      n[7] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1148      int base = 16;
1149      for (int i = 1; i < 8; i++) {
1150         a = (num/base)%16;
1151         base <<= 4;
1152         n[7-i] = (char)(a > 9 ? 'A'+a-10 : '0'+a);
1153      }
1154      return n;
1155   }
1156
1157   /**
1158    * Tests two strings for equality, but gracefully handles nulls.
1159    *
1160    * @param s1 String 1.
1161    * @param s2 String 2.
1162    * @return <jk>true</jk> if the strings are equal.
1163    */
1164   public static boolean eq(String s1, String s2) {
1165      if (s1 == null)
1166         return s2 == null;
1167      if (s2 == null)
1168         return false;
1169      return s1.equals(s2);
1170   }
1171
1172   /**
1173    * Tests two strings for equality, but gracefully handles nulls.
1174    *
1175    * @param caseInsensitive Use case-insensitive matching.
1176    * @param s1 String 1.
1177    * @param s2 String 2.
1178    * @return <jk>true</jk> if the strings are equal.
1179    */
1180   public static boolean eq(boolean caseInsensitive, String s1, String s2) {
1181      return caseInsensitive ? eqic(s1, s2) : eq(s1, s2);
1182   }
1183
1184   /**
1185    * Finds the position where the two strings differ.
1186    *
1187    * @param s1 The first string.
1188    * @param s2 The second string.
1189    * @return The position where the two strings differ, or <c>-1</c> if they're equal.
1190    */
1191   public static int diffPosition(String s1, String s2) {
1192      s1 = emptyIfNull(s1);
1193      s2 = emptyIfNull(s2);
1194      int i = 0;
1195      int len = Math.min(s1.length(), s2.length());
1196      while (i < len) {
1197         int j = s1.charAt(i) - s2.charAt(i);
1198         if (j != 0)
1199            return i;
1200         i++;
1201      }
1202      if (i == len && s1.length() == s2.length())
1203         return -1;
1204      return i;
1205   }
1206
1207   /**
1208    * Finds the position where the two strings differ ignoring case.
1209    *
1210    * @param s1 The first string.
1211    * @param s2 The second string.
1212    * @return The position where the two strings differ, or <c>-1</c> if they're equal.
1213    */
1214   public static int diffPositionIc(String s1, String s2) {
1215      s1 = emptyIfNull(s1);
1216      s2 = emptyIfNull(s2);
1217      int i = 0;
1218      int len = Math.min(s1.length(), s2.length());
1219      while (i < len) {
1220         int j = Character.toLowerCase(s1.charAt(i)) - Character.toLowerCase(s2.charAt(i));
1221         if (j != 0)
1222            return i;
1223         i++;
1224      }
1225      if (i == len && s1.length() == s2.length())
1226         return -1;
1227      return i;
1228   }
1229
1230   /**
1231    * Tests two strings for case-insensitive equality, but gracefully handles nulls.
1232    *
1233    * @param s1 String 1.
1234    * @param s2 String 2.
1235    * @return <jk>true</jk> if the strings are equal.
1236    */
1237   public static boolean eqic(String s1, String s2) {
1238      if (s1 == null)
1239         return s2 == null;
1240      if (s2 == null)
1241         return false;
1242      return s1.equalsIgnoreCase(s2);
1243   }
1244
1245   /**
1246    * Tests two strings for non-equality, but gracefully handles nulls.
1247    *
1248    * @param s1 String 1.
1249    * @param s2 String 2.
1250    * @return <jk>true</jk> if the strings are not equal.
1251    */
1252   public static boolean ne(String s1, String s2) {
1253      return ! eq(s1, s2);
1254   }
1255
1256   /**
1257    * Tests two strings for non-equality ignoring case, but gracefully handles nulls.
1258    *
1259    * @param s1 String 1.
1260    * @param s2 String 2.
1261    * @return <jk>true</jk> if the strings are not equal ignoring case.
1262    */
1263   public static boolean neic(String s1, String s2) {
1264      return ! eqic(s1, s2);
1265   }
1266
1267   /**
1268    * Shortcut for calling <code>base64Encode(in.getBytes(<js>"UTF-8"</js>))</code>
1269    *
1270    * @param in The input string to convert.
1271    * @return The string converted to BASE-64 encoding.
1272    */
1273   public static String base64EncodeToString(String in) {
1274      if (in == null)
1275         return null;
1276      return base64Encode(in.getBytes(IOUtils.UTF8));
1277   }
1278
1279   /**
1280    * BASE64-encodes the specified byte array.
1281    *
1282    * @param in The input byte array to convert.
1283    * @return The byte array converted to a BASE-64 encoded string.
1284    */
1285   public static String base64Encode(byte[] in) {
1286      if (in == null)
1287         return null;
1288      int outLength = (in.length * 4 + 2) / 3;   // Output length without padding
1289      char[] out = new char[((in.length + 2) / 3) * 4];  // Length includes padding.
1290      int iIn = 0;
1291      int iOut = 0;
1292      while (iIn < in.length) {
1293         int i0 = in[iIn++] & 0xff;
1294         int i1 = iIn < in.length ? in[iIn++] & 0xff : 0;
1295         int i2 = iIn < in.length ? in[iIn++] & 0xff : 0;
1296         int o0 = i0 >>> 2;
1297         int o1 = ((i0 & 3) << 4) | (i1 >>> 4);
1298         int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
1299         int o3 = i2 & 0x3F;
1300         out[iOut++] = base64m1[o0];
1301         out[iOut++] = base64m1[o1];
1302         out[iOut] = iOut < outLength ? base64m1[o2] : '=';
1303         iOut++;
1304         out[iOut] = iOut < outLength ? base64m1[o3] : '=';
1305         iOut++;
1306      }
1307      return new String(out);
1308   }
1309
1310   /**
1311    * Shortcut for calling <c>base64Decode(String)</c> and converting the result to a UTF-8 encoded string.
1312    *
1313    * @param in The BASE-64 encoded string to decode.
1314    * @return The decoded string.
1315    */
1316   public static String base64DecodeToString(String in) {
1317      byte[] b = base64Decode(in);
1318      if (b == null)
1319         return null;
1320      return new String(b, IOUtils.UTF8);
1321   }
1322
1323   /**
1324    * BASE64-decodes the specified string.
1325    *
1326    * @param in The BASE-64 encoded string.
1327    * @return The decoded byte array.
1328    */
1329   public static byte[] base64Decode(String in) {
1330      if (in == null)
1331         return null;
1332
1333      byte bIn[] = in.getBytes(IOUtils.UTF8);
1334
1335      assertArg(bIn.length % 4 == 0, "Invalid BASE64 string length.  Must be multiple of 4.");
1336
1337      // Strip out any trailing '=' filler characters.
1338      int inLength = bIn.length;
1339      while (inLength > 0 && bIn[inLength - 1] == '=')
1340         inLength--;
1341
1342      int outLength = (inLength * 3) / 4;
1343      byte[] out = new byte[outLength];
1344      int iIn = 0;
1345      int iOut = 0;
1346      while (iIn < inLength) {
1347         int i0 = bIn[iIn++];
1348         int i1 = bIn[iIn++];
1349         int i2 = iIn < inLength ? bIn[iIn++] : 'A';
1350         int i3 = iIn < inLength ? bIn[iIn++] : 'A';
1351         int b0 = base64m2[i0];
1352         int b1 = base64m2[i1];
1353         int b2 = base64m2[i2];
1354         int b3 = base64m2[i3];
1355         int o0 = (b0 << 2) | (b1 >>> 4);
1356         int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2);
1357         int o2 = ((b2 & 3) << 6) | b3;
1358         out[iOut++] = (byte)o0;
1359         if (iOut < outLength)
1360            out[iOut++] = (byte)o1;
1361         if (iOut < outLength)
1362            out[iOut++] = (byte)o2;
1363      }
1364      return out;
1365   }
1366
1367   /**
1368    * Generated a random UUID with the specified number of characters.
1369    *
1370    * <p>
1371    * Characters are composed of lower-case ASCII letters and numbers only.
1372    *
1373    * <p>
1374    * This method conforms to the restrictions for hostnames as specified in <a class="doclink" href="https://tools.ietf.org/html/rfc952">RFC 952</a>
1375    * Since each character has 36 possible values, the square approximation formula for the number of generated IDs
1376    * that would produce a 50% chance of collision is:
1377    * <c>sqrt(36^N)</c>.
1378    * Dividing this number by 10 gives you an approximation of the number of generated IDs needed to produce a
1379    * &lt;1% chance of collision.
1380    *
1381    * <p>
1382    * For example, given 5 characters, the number of generated IDs need to produce a &lt;1% chance of collision would
1383    * be:
1384    * <c>sqrt(36^5)/10=777</c>
1385    *
1386    * @param numchars The number of characters in the generated UUID.
1387    * @return A new random UUID.
1388    */
1389   public static String random(int numchars) {
1390      Random r = new Random();
1391      StringBuilder sb = new StringBuilder(numchars);
1392      for (int i = 0; i < numchars; i++) {
1393         int c = r.nextInt(36) + 97;
1394         if (c > 'z')
1395            c -= ('z'-'0'+1);
1396         sb.append((char)c);
1397      }
1398      return sb.toString();
1399   }
1400
1401   /**
1402    * Same as {@link String#trim()} but prevents <c>NullPointerExceptions</c>.
1403    *
1404    * @param s The string to trim.
1405    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
1406    */
1407   public static String trim(String s) {
1408      if (s == null)
1409         return null;
1410      return s.trim();
1411   }
1412
1413   /**
1414    * Strips the first and last character from a string.
1415    *
1416    * @param s The string to strip.
1417    * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2.
1418    */
1419   public static String strip(String s) {
1420      if (s == null || s.length() <= 1)
1421         return s;
1422      return s.substring(1, s.length()-1);
1423   }
1424
1425   /**
1426    * Parses an ISO8601 string into a date.
1427    *
1428    * <p>
1429    * Supports any of the following formats:
1430    * <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>
1431    *
1432    * @param date The date string.
1433    * @return The parsed date.
1434    * @throws IllegalArgumentException Value was not a valid date.
1435    */
1436   public static Date parseIsoDate(String date) throws IllegalArgumentException {
1437      if (isEmpty(date))
1438         return null;
1439      return parseIsoCalendar(date).getTime();
1440   }
1441
1442   /**
1443    * Parses an ISO8601 string into a calendar.
1444    *
1445    * <p>
1446    * Supports any of the following formats:
1447    * <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>
1448    *
1449    * @param date The date string.
1450    * @return The parsed calendar.
1451    * @throws IllegalArgumentException Value was not a valid date.
1452    */
1453   public static Calendar parseIsoCalendar(String date) throws IllegalArgumentException {
1454      if (isEmpty(date))
1455         return null;
1456      date = date.trim().replace(' ', 'T');  // Convert to 'standard' ISO8601
1457      if (date.indexOf(',') != -1)  // Trim milliseconds
1458         date = date.substring(0, date.indexOf(','));
1459      if (date.matches("\\d{4}"))
1460         date += "-01-01T00:00:00";
1461      else if (date.matches("\\d{4}\\-\\d{2}"))
1462         date += "-01T00:00:00";
1463      else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}"))
1464         date += "T00:00:00";
1465      else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}"))
1466         date += ":00:00";
1467      else if (date.matches("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}"))
1468         date += ":00";
1469      return DatatypeConverter.parseDateTime(date);
1470   }
1471
1472   /**
1473    * Converts the specified object to an ISO8601 date string.
1474    *
1475    * @param c The object to convert.
1476    * @return The converted object.
1477    */
1478   public static String toIsoDate(Calendar c) {
1479      return DatatypeConverter.printDate(c);
1480   }
1481
1482   /**
1483    * Converts the specified object to an ISO8601 date-time string.
1484    *
1485    * @param c The object to convert.
1486    * @return The converted object.
1487    */
1488   public static String toIsoDateTime(Calendar c) {
1489      return DatatypeConverter.printDateTime(c);
1490   }
1491
1492   /**
1493    * Simple utility for replacing variables of the form <js>"{key}"</js> with values in the specified map.
1494    *
1495    * <p>
1496    * Nested variables are supported in both the input string and map values.
1497    *
1498    * <p>
1499    * If the map does not contain the specified value, the variable is not replaced.
1500    *
1501    * <p>
1502    * <jk>null</jk> values in the map are treated as blank strings.
1503    *
1504    * @param s The string containing variables to replace.
1505    * @param m The map containing the variable values.
1506    * @return The new string with variables replaced, or the original string if it didn't have variables in it.
1507    */
1508   public static String replaceVars(String s, Map<String,Object> m) {
1509
1510      if (s == null)
1511         return null;
1512
1513      if (m == null || m.isEmpty() || s.indexOf('{') == -1)
1514         return s;
1515
1516      int S1 = 1;    // Not in variable, looking for {
1517      int S2 = 2;    // Found {, Looking for }
1518
1519      int state = S1;
1520      boolean hasInternalVar = false;
1521      int x = 0;
1522      int depth = 0;
1523      int length = s.length();
1524      StringBuilder out = new StringBuilder();
1525      for (int i = 0; i < length; i++) {
1526         char c = s.charAt(i);
1527         if (state == S1) {
1528            if (c == '{') {
1529               state = S2;
1530               x = i;
1531            } else {
1532               out.append(c);
1533            }
1534         } else /* state == S2 */ {
1535            if (c == '{') {
1536               depth++;
1537               hasInternalVar = true;
1538            } else if (c == '}') {
1539               if (depth > 0) {
1540                  depth--;
1541               } else {
1542                  String key = s.substring(x+1, i);
1543                  key = (hasInternalVar ? replaceVars(key, m) : key);
1544                  hasInternalVar = false;
1545                  if (! m.containsKey(key))
1546                     out.append('{').append(key).append('}');
1547                  else {
1548                     Object val = m.get(key);
1549                     if (val == null)
1550                        val = "";
1551                     String v = val.toString();
1552                     // If the replacement also contains variables, replace them now.
1553                     if (v.indexOf('{') != -1)
1554                        v = replaceVars(v, m);
1555                     out.append(v);
1556                  }
1557                  state = 1;
1558               }
1559            }
1560         }
1561      }
1562      return out.toString();
1563   }
1564
1565   /**
1566    * Replaces <js>"\\uXXXX"</js> character sequences with their unicode characters.
1567    *
1568    * @param s The string to replace unicode sequences in.
1569    * @return A string with unicode sequences replaced.
1570    */
1571   public static String replaceUnicodeSequences(String s) {
1572      if (s.indexOf('\\') == -1)
1573         return s;
1574      Pattern p = Pattern.compile("\\\\u(\\p{XDigit}{4})");
1575      Matcher m = p.matcher(s);
1576      StringBuffer sb = new StringBuffer(s.length());
1577      while (m.find()) {
1578         String ch = String.valueOf((char) Integer.parseInt(m.group(1), 16));
1579         m.appendReplacement(sb, Matcher.quoteReplacement(ch));
1580      }
1581      m.appendTail(sb);
1582      return sb.toString();
1583   }
1584
1585   /**
1586    * Creates an escaped-unicode sequence (e.g. <js>"\\u1234"</js>) for the specified character.
1587    *
1588    * @param c The character to create a sequence for.
1589    * @return An escaped-unicode sequence.
1590    */
1591   public static String unicodeSequence(char c) {
1592      StringBuilder sb = new StringBuilder(6);
1593      sb.append('\\').append('u');
1594      for (char cc : toHex4(c))
1595         sb.append(cc);
1596      return sb.toString();
1597   }
1598
1599   /**
1600    * Calls {@link #toString()} on the specified object if it's not null.
1601    *
1602    * @param o The object to convert to a string.
1603    * @return The object converted to a string, or <jk>null</jk> if the object was null.
1604    */
1605   public static String stringify(Object o) {
1606      return o == null ? null : o.toString();
1607   }
1608
1609   /**
1610    * Converts the specified array to a string.
1611    *
1612    * @param o The array to convert to a string.
1613    * @return The array converted to a string, or <jk>null</jk> if the object was null.
1614    */
1615   public static String stringifyDeep(Object o) {
1616      if (o == null)
1617         return null;
1618      if (! o.getClass().isArray())
1619         return o.toString();
1620      if (o.getClass().getComponentType().isPrimitive())
1621         return PRIMITIVE_ARRAY_STRINGIFIERS.get(o.getClass()).apply(o);
1622      return Arrays.deepToString((Object[])o);
1623   }
1624
1625   private static final Map<Class<?>,Function<Object,String>> PRIMITIVE_ARRAY_STRINGIFIERS = new HashMap<>();
1626   static {
1627      PRIMITIVE_ARRAY_STRINGIFIERS.put(boolean[].class, x -> Arrays.toString((boolean[])x));
1628      PRIMITIVE_ARRAY_STRINGIFIERS.put(byte[].class, x -> Arrays.toString((byte[])x));
1629      PRIMITIVE_ARRAY_STRINGIFIERS.put(char[].class, x -> Arrays.toString((char[])x));
1630      PRIMITIVE_ARRAY_STRINGIFIERS.put(double[].class, x -> Arrays.toString((double[])x));
1631      PRIMITIVE_ARRAY_STRINGIFIERS.put(float[].class, x -> Arrays.toString((float[])x));
1632      PRIMITIVE_ARRAY_STRINGIFIERS.put(int[].class, x -> Arrays.toString((int[])x));
1633      PRIMITIVE_ARRAY_STRINGIFIERS.put(long[].class, x -> Arrays.toString((long[])x));
1634      PRIMITIVE_ARRAY_STRINGIFIERS.put(short[].class, x -> Arrays.toString((short[])x));
1635   }
1636
1637   /**
1638    * Converts a hexadecimal byte stream (e.g. "34A5BC") into a UTF-8 encoded string.
1639    *
1640    * @param hex The hexadecimal string.
1641    * @return The UTF-8 string.
1642    */
1643   public static String fromHexToUTF8(String hex) {
1644      ByteBuffer buff = ByteBuffer.allocate(hex.length()/2);
1645      for (int i = 0; i < hex.length(); i+=2)
1646         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
1647      ((Buffer)buff).rewind();  // Fixes Java 11 issue.
1648      Charset cs = Charset.forName("UTF-8");
1649      return cs.decode(buff).toString();
1650   }
1651
1652   /**
1653    * Converts a space-deliminted hexadecimal byte stream (e.g. "34 A5 BC") into a UTF-8 encoded string.
1654    *
1655    * @param hex The hexadecimal string.
1656    * @return The UTF-8 string.
1657    */
1658   public static String fromSpacedHexToUTF8(String hex) {
1659      ByteBuffer buff = ByteBuffer.allocate((hex.length()+1)/3);
1660      for (int i = 0; i < hex.length(); i+=3)
1661         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
1662      ((Buffer)buff).rewind();  // Fixes Java 11 issue.
1663      Charset cs = Charset.forName("UTF-8");
1664      return cs.decode(buff).toString();
1665   }
1666
1667   private static final char[] HEX = "0123456789ABCDEF".toCharArray();
1668
1669   /**
1670    * Converts a byte array into a simple hexadecimal character string.
1671    *
1672    * @param bytes The bytes to convert to hexadecimal.
1673    * @return A new string consisting of hexadecimal characters.
1674    */
1675   public static String toHex(byte[] bytes) {
1676      StringBuilder sb = new StringBuilder(bytes.length * 2);
1677      for (byte element : bytes) {
1678         int v = element & 0xFF;
1679         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
1680      }
1681      return sb.toString();
1682   }
1683
1684   /**
1685    * Same as {@link #toHex(byte[])} but puts spaces between the byte strings.
1686    *
1687    * @param bytes The bytes to convert to hexadecimal.
1688    * @return A new string consisting of hexadecimal characters.
1689    */
1690   public static String toSpacedHex(byte[] bytes) {
1691      StringBuilder sb = new StringBuilder(bytes.length * 3);
1692      for (int j = 0; j < bytes.length; j++) {
1693         if (j > 0)
1694            sb.append(' ');
1695         int v = bytes[j] & 0xFF;
1696         sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]);
1697      }
1698      return sb.toString();
1699   }
1700
1701   /**
1702    * Converts a hexadecimal character string to a byte array.
1703    *
1704    * @param hex The string to convert to a byte array.
1705    * @return A new byte array.
1706    */
1707   public static byte[] fromHex(String hex) {
1708      ByteBuffer buff = ByteBuffer.allocate(hex.length()/2);
1709      for (int i = 0; i < hex.length(); i+=2)
1710         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
1711      ((Buffer)buff).rewind();  // Fixes Java 11 issue.
1712      return buff.array();
1713   }
1714
1715   /**
1716    * Same as {@link #fromHex(String)} except expects spaces between the byte strings.
1717    *
1718    * @param hex The string to convert to a byte array.
1719    * @return A new byte array.
1720    */
1721   public static byte[] fromSpacedHex(String hex) {
1722      ByteBuffer buff = ByteBuffer.allocate((hex.length()+1)/3);
1723      for (int i = 0; i < hex.length(); i+=3)
1724         buff.put((byte)Integer.parseInt(hex.substring(i, i+2), 16));
1725      ((Buffer)buff).rewind();  // Fixes Java 11 issue.
1726      return buff.array();
1727   }
1728
1729   /**
1730    * Creates a repeated pattern.
1731    *
1732    * @param count The number of times to repeat the pattern.
1733    * @param pattern The pattern to repeat.
1734    * @return A new string consisting of the repeated pattern.
1735    */
1736   public static String repeat(int count, String pattern) {
1737      StringBuilder sb = new StringBuilder(pattern.length() * count);
1738      for (int i = 0; i < count; i++)
1739         sb.append(pattern);
1740      return sb.toString();
1741   }
1742
1743   /**
1744    * Trims whitespace characters from the beginning of the specified string.
1745    *
1746    * @param s The string to trim.
1747    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
1748    */
1749   public static String trimStart(String s) {
1750      if (s != null)
1751         while (s.length() > 0 && Character.isWhitespace(s.charAt(0)))
1752            s = s.substring(1);
1753      return s;
1754   }
1755
1756   /**
1757    * Trims whitespace characters from the end of the specified string.
1758    *
1759    * @param s The string to trim.
1760    * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>.
1761    */
1762   public static String trimEnd(String s) {
1763      if (s != null)
1764         while (s.length() > 0 && Character.isWhitespace(s.charAt(s.length()-1)))
1765            s = s.substring(0, s.length()-1);
1766      return s;
1767   }
1768
1769   /**
1770    * Returns <jk>true</jk> if the specified string is one of the specified values.
1771    *
1772    * @param s
1773    *    The string to test.
1774    *    Can be <jk>null</jk>.
1775    * @param values
1776    *    The values to test.
1777    *    Can contain <jk>null</jk>.
1778    * @return <jk>true</jk> if the specified string is one of the specified values.
1779    */
1780   public static boolean isOneOf(String s, String...values) {
1781      for (String value : values)
1782            if (StringUtils.eq(s, value))
1783            return true;
1784      return false;
1785   }
1786
1787   /**
1788    * Trims <js>'/'</js> characters from both the start and end of the specified string.
1789    *
1790    * @param s The string to trim.
1791    * @return A new trimmed string, or the same string if no trimming was necessary.
1792    */
1793   public static String trimSlashes(String s) {
1794      if (s == null)
1795         return null;
1796      if (s.isEmpty())
1797         return s;
1798      while (endsWith(s, '/'))
1799         s = s.substring(0, s.length()-1);
1800      while (s.length() > 0 && s.charAt(0) == '/')
1801         s = s.substring(1);
1802      return s;
1803   }
1804
1805   /**
1806    * Trims <js>'/'</js> and space characters from both the start and end of the specified string.
1807    *
1808    * @param s The string to trim.
1809    * @return A new trimmed string, or the same string if no trimming was necessary.
1810    */
1811   public static String trimSlashesAndSpaces(String s) {
1812      if (s == null)
1813         return null;
1814      while (s.length() > 0 && (s.charAt(s.length()-1) == '/' || Character.isWhitespace(s.charAt(s.length()-1))))
1815         s = s.substring(0, s.length()-1);
1816      while (s.length() > 0 && (s.charAt(0) == '/' || Character.isWhitespace(s.charAt(0))))
1817         s = s.substring(1);
1818      return s;
1819   }
1820
1821   /**
1822    * Trims <js>'/'</js> characters from the end of the specified string.
1823    *
1824    * @param s The string to trim.
1825    * @return A new trimmed string, or the same string if no trimming was necessary.
1826    */
1827   public static String trimTrailingSlashes(String s) {
1828      if (s == null)
1829         return null;
1830      while (endsWith(s, '/'))
1831         s = s.substring(0, s.length()-1);
1832      return s;
1833   }
1834
1835   /**
1836    * Trims <js>'/'</js> characters from the beginning of the specified string.
1837    *
1838    * @param s The string to trim.
1839    * @return A new trimmed string, or the same string if no trimming was necessary.
1840    */
1841   public static String trimLeadingSlashes(String s) {
1842      if (s == null)
1843         return null;
1844      while (s.length() > 0 && s.charAt(0) == '/')
1845         s = s.substring(1);
1846      return s;
1847   }
1848
1849   private static final AsciiSet URL_ENCODE_PATHINFO_VALIDCHARS =
1850      AsciiSet.create().ranges("a-z","A-Z","0-9").chars("-_.*/()").build();
1851
1852   /**
1853    * Similar to {@link URLEncoder#encode(String, String)} but doesn't encode <js>"/"</js> characters.
1854    *
1855    * @param o The object to encode.
1856    * @return The URL encoded string, or <jk>null</jk> if the object was null.
1857    */
1858   public static String urlEncodePath(Object o) {
1859      if (o == null)
1860         return null;
1861      String s = stringify(o);
1862
1863      boolean needsEncode = false;
1864      for (int i = 0; i < s.length() && ! needsEncode; i++)
1865         needsEncode = URL_ENCODE_PATHINFO_VALIDCHARS.contains(s.charAt(i));
1866      if (! needsEncode)
1867         return s;
1868
1869      StringBuilder sb = new StringBuilder();
1870      CharArrayWriter caw = new CharArrayWriter();
1871      int caseDiff = ('a' - 'A');
1872
1873      for (int i = 0; i < s.length();) {
1874         char c = s.charAt(i);
1875         if (URL_ENCODE_PATHINFO_VALIDCHARS.contains(c)) {
1876            sb.append(c);
1877            i++;
1878         } else {
1879            if (c == ' ') {
1880               sb.append('+');
1881               i++;
1882            } else {
1883               do {
1884                  caw.write(c);
1885                  if (c >= 0xD800 && c <= 0xDBFF) {
1886                     if ( (i+1) < s.length()) {
1887                        int d = s.charAt(i+1);
1888                        if (d >= 0xDC00 && d <= 0xDFFF) {
1889                           caw.write(d);
1890                           i++;
1891                        }
1892                     }
1893                  }
1894                  i++;
1895               } while (i < s.length() && !URL_ENCODE_PATHINFO_VALIDCHARS.contains((c = s.charAt(i))));
1896
1897               caw.flush();
1898               String s2 = new String(caw.toCharArray());
1899               byte[] ba = s2.getBytes(IOUtils.UTF8);
1900               for (byte element : ba) {
1901                  sb.append('%');
1902                  char ch = Character.forDigit((element >> 4) & 0xF, 16);
1903                  if (Character.isLetter(ch)) {
1904                     ch -= caseDiff;
1905                  }
1906                  sb.append(ch);
1907                  ch = Character.forDigit(element & 0xF, 16);
1908                  if (Character.isLetter(ch)) {
1909                     ch -= caseDiff;
1910                  }
1911                  sb.append(ch);
1912               }
1913               caw.reset();
1914            }
1915         }
1916      }
1917      return sb.toString();
1918   }
1919
1920   /**
1921    * Decodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
1922    *
1923    * @param s The string to decode.
1924    * @return The decoded string, or <jk>null</jk> if input is <jk>null</jk>.
1925    */
1926   public static String urlDecode(String s) {
1927      if (s == null)
1928         return s;
1929      boolean needsDecode = false;
1930      for (int i = 0; i < s.length() && ! needsDecode; i++) {
1931         char c = s.charAt(i);
1932         if (c == '+' || c == '%')
1933            needsDecode = true;
1934      }
1935      if (needsDecode) {
1936         try {
1937            return URLDecoder.decode(s, "UTF-8");
1938         } catch (UnsupportedEncodingException e) {/* Won't happen */}
1939      }
1940      return s;
1941   }
1942
1943   /**
1944    * Encodes a <c>application/x-www-form-urlencoded</c> string using <c>UTF-8</c> encoding scheme.
1945    *
1946    * @param s The string to encode.
1947    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
1948    */
1949   public static String urlEncode(String s) {
1950      if (s == null)
1951         return null;
1952      boolean needsEncode = false;
1953      for (int i = 0; i < s.length() && ! needsEncode; i++)
1954         needsEncode |= (! unencodedChars.contains(s.charAt(i)));
1955      if (needsEncode) {
1956         try {
1957            return URLEncoder.encode(s, "UTF-8");
1958         } catch (UnsupportedEncodingException e) {/* Won't happen */}
1959      }
1960      return s;
1961   }
1962
1963   /**
1964    * Same as {@link #urlEncode(String)} except only escapes characters that absolutely need to be escaped.
1965    *
1966    * @param s The string to escape.
1967    * @return The encoded string, or <jk>null</jk> if input is <jk>null</jk>.
1968    */
1969   public static String urlEncodeLax(String s) {
1970      if (s == null)
1971         return null;
1972      boolean needsEncode = false;
1973      for (int i = 0; i < s.length() && ! needsEncode; i++)
1974         needsEncode |= (! unencodedCharsLax.contains(s.charAt(i)));
1975      if (needsEncode) {
1976         StringBuilder sb = new StringBuilder(s.length()*2);
1977         for (int i = 0; i < s.length(); i++) {
1978            char c = s.charAt(i);
1979            if (unencodedCharsLax.contains(c))
1980               sb.append(c);
1981            else if (c == ' ')
1982               sb.append("+");
1983            else if (c <= 127)
1984               sb.append('%').append(toHex2(c));
1985            else
1986               try {
1987                  sb.append(URLEncoder.encode(""+c, "UTF-8"));  // Yuck.
1988               } catch (UnsupportedEncodingException e) {
1989                  // Not possible.
1990               }
1991         }
1992         s = sb.toString();
1993      }
1994      return s;
1995   }
1996
1997   /**
1998    * Returns the first non-whitespace character in the string.
1999    *
2000    * @param s The string to check.
2001    * @return
2002    *    The first non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed
2003    *    of only whitespace.
2004    */
2005   public static char firstNonWhitespaceChar(String s) {
2006      if (s != null)
2007         for (int i = 0; i < s.length(); i++)
2008            if (! Character.isWhitespace(s.charAt(i)))
2009               return s.charAt(i);
2010      return 0;
2011   }
2012
2013   /**
2014    * Returns the last non-whitespace character in the string.
2015    *
2016    * @param s The string to check.
2017    * @return
2018    *    The last non-whitespace character, or <c>0</c> if the string is <jk>null</jk>, empty, or composed
2019    *    of only whitespace.
2020    */
2021   public static char lastNonWhitespaceChar(String s) {
2022      if (s != null)
2023         for (int i = s.length()-1; i >= 0; i--)
2024            if (! Character.isWhitespace(s.charAt(i)))
2025               return s.charAt(i);
2026      return 0;
2027   }
2028
2029   /**
2030    * Returns the character at the specified index in the string without throwing exceptions.
2031    *
2032    * @param s The string.
2033    * @param i The index position.
2034    * @return
2035    *    The character at the specified index, or <c>0</c> if the index is out-of-range or the string
2036    *    is <jk>null</jk>.
2037    */
2038   public static char charAt(String s, int i) {
2039      if (s == null || i < 0 || i >= s.length())
2040         return 0;
2041      return s.charAt(i);
2042   }
2043
2044   /**
2045    * Efficiently determines whether a URL is of the pattern "xxx://xxx"
2046    *
2047    * @param s The string to test.
2048    * @return <jk>true</jk> if it's an absolute path.
2049    */
2050   public static boolean isAbsoluteUri(String s) {
2051
2052      if (isEmpty(s))
2053         return false;
2054
2055      // Use a state machine for maximum performance.
2056
2057      int S1 = 1;  // Looking for http
2058      int S2 = 2;  // Found http, looking for :
2059      int S3 = 3;  // Found :, looking for /
2060      int S4 = 4;  // Found /, looking for /
2061      int S5 = 5;  // Found /, looking for x
2062
2063      int state = S1;
2064      for (int i = 0; i < s.length(); i++) {
2065         char c = s.charAt(i);
2066         if (state == S1) {
2067            if (c >= 'a' && c <= 'z')
2068               state = S2;
2069            else
2070               return false;
2071         } else if (state == S2) {
2072            if (c == ':')
2073               state = S3;
2074            else if (c < 'a' || c > 'z')
2075               return false;
2076         } else if (state == S3) {
2077            if (c == '/')
2078               state = S4;
2079            else
2080               return false;
2081         } else if (state == S4) {
2082            if (c == '/')
2083               state = S5;
2084            else
2085               return false;
2086         } else if (state == S5) {
2087            return true;
2088         }
2089      }
2090      return false;
2091   }
2092
2093   /**
2094    * Efficiently determines whether a URL is of the pattern "xxx:/xxx".
2095    *
2096    * <p>
2097    * The pattern matched is: <c>[a-z]{2,}\:\/.*</c>
2098    *
2099    * <p>
2100    * Note that this excludes filesystem paths such as <js>"C:/temp"</js>.
2101    *
2102    * @param s The string to test.
2103    * @return <jk>true</jk> if it's an absolute path.
2104    */
2105   public static boolean isUri(String s) {
2106
2107      if (isEmpty(s))
2108         return false;
2109
2110      // Use a state machine for maximum performance.
2111
2112      int S1 = 1;  // Looking for protocol char 1
2113      int S2 = 2;  // Found protocol char 1, looking for protocol char 2
2114      int S3 = 3;  // Found protocol char 2, looking for :
2115      int S4 = 4;  // Found :, looking for /
2116
2117
2118      int state = S1;
2119      for (int i = 0; i < s.length(); i++) {
2120         char c = s.charAt(i);
2121         if (state == S1) {
2122            if (c >= 'a' && c <= 'z')
2123               state = S2;
2124            else
2125               return false;
2126         } else if (state == S2) {
2127            if (c >= 'a' && c <= 'z')
2128               state = S3;
2129            else
2130               return false;
2131         } else if (state == S3) {
2132            if (c == ':')
2133               state = S4;
2134            else if (c < 'a' || c > 'z')
2135               return false;
2136         } else if (state == S4) {
2137            if (c == '/')
2138               return true;
2139            return false;
2140         }
2141      }
2142      return false;
2143   }
2144
2145   /**
2146    * Given an absolute URI, returns just the authority portion (e.g. <js>"http://hostname:port"</js>)
2147    *
2148    * @param s The URI string.
2149    * @return Just the authority portion of the URI.
2150    */
2151   public static String getAuthorityUri(String s) {
2152
2153      // Use a state machine for maximum performance.
2154
2155      int S1 = 1;  // Looking for http
2156      int S2 = 2;  // Found http, looking for :
2157      int S3 = 3;  // Found :, looking for /
2158      int S4 = 4;  // Found /, looking for /
2159      int S5 = 5;  // Found /, looking for x
2160      int S6 = 6;  // Found x, looking for /
2161
2162      int state = S1;
2163      for (int i = 0; i < s.length(); i++) {
2164         char c = s.charAt(i);
2165         if (state == S1) {
2166            if (c >= 'a' && c <= 'z')
2167               state = S2;
2168            else
2169               return s;
2170         } else if (state == S2) {
2171            if (c == ':')
2172               state = S3;
2173            else if (c < 'a' || c > 'z')
2174               return s;
2175         } else if (state == S3) {
2176            if (c == '/')
2177               state = S4;
2178            else
2179               return s;
2180         } else if (state == S4) {
2181            if (c == '/')
2182               state = S5;
2183            else
2184               return s;
2185         } else if (state == S5) {
2186            if (c != '/')
2187               state = S6;
2188            else
2189               return s;
2190         } else if (state == S6) {
2191            if (c == '/')
2192               return s.substring(0, i);
2193         }
2194      }
2195      return s;
2196   }
2197
2198   /**
2199    * Converts the specified object to a URI.
2200    *
2201    * @param o The object to convert to a URI.
2202    * @return A new URI, or the same object if the object was already a URI, or
2203    */
2204   public static URI toURI(Object o) {
2205      if (o == null || o instanceof URI)
2206         return (URI)o;
2207      try {
2208         return new URI(o.toString());
2209      } catch (URISyntaxException e) {
2210         throw asRuntimeException(e);
2211      }
2212   }
2213
2214   /**
2215    * Returns the first non-null, non-empty string in the list.
2216    *
2217    * @param s The strings to test.
2218    * @return The first non-empty string in the list, or <jk>null</jk> if they were all <jk>null</jk> or empty.
2219    */
2220   public static String firstNonEmpty(String...s) {
2221      for (String ss : s)
2222         if (isNotEmpty(ss))
2223            return ss;
2224      return null;
2225   }
2226
2227   /**
2228    * Same as {@link String#indexOf(int)} except allows you to check for multiple characters.
2229    *
2230    * @param s The string to check.
2231    * @param c The characters to check for.
2232    * @return The index into the string that is one of the specified characters.
2233    */
2234   public static int indexOf(String s, char...c) {
2235      if (s == null)
2236         return -1;
2237      for (int i = 0; i < s.length(); i++) {
2238         char c2 = s.charAt(i);
2239         for (char cc : c)
2240            if (c2 == cc)
2241               return i;
2242      }
2243      return -1;
2244   }
2245
2246   /**
2247    * Similar to {@link MessageFormat#format(String, Object...)} except allows you to specify POJO arguments.
2248    *
2249    * @param pattern The string pattern.
2250    * @param args The arguments.
2251    * @return The formatted string.
2252    */
2253   public static String format(String pattern, Object...args) {
2254      if (args == null || args.length == 0)
2255         return pattern;
2256      Object[] args2 = new Object[args.length];
2257      for (int i = 0; i < args.length; i++)
2258         args2[i] = convertToReadable(args[i]);
2259
2260      int c = countChars(pattern, '\'');
2261      if (c % 2 != 0)
2262         throw new AssertionError("Dangling single quote found in pattern: " + pattern);
2263
2264      String msg = MessageFormat.format(pattern, args2);
2265      return msg;
2266   }
2267
2268   private static String convertToReadable(Object o) {
2269      if (o == null)
2270         return null;
2271      if (o instanceof Class)
2272         return ((Class<?>)o).getName();
2273      if (o instanceof Method)
2274         return Method.class.cast(o).getName();
2275      if (o.getClass().isArray())
2276         return arrayAsList(o).stream().map(StringUtils::convertToReadable).collect(Collectors.joining(", ", "[", "]"));
2277      return o.toString();
2278   }
2279
2280   private static List<Object> arrayAsList(Object array) {
2281      if (array.getClass().getComponentType().isPrimitive()) {
2282         List<Object> l = new ArrayList<>(Array.getLength(array));
2283         for (int i = 0; i < Array.getLength(array); i++)
2284            l.add(Array.get(array, i));
2285         return l;
2286      }
2287      return Arrays.asList((Object[])array);
2288   }
2289
2290   /**
2291    * Converts a string containing a possible multiplier suffix to an integer.
2292    *
2293    * <p>
2294    * The string can contain any of the following multiplier suffixes:
2295    * <ul>
2296    *    <li><js>"K"</js> - x 1024
2297    *    <li><js>"M"</js> - x 1024*1024
2298    *    <li><js>"G"</js> - x 1024*1024*1024
2299    *    <li><js>"k"</js> - x 1000
2300    *    <li><js>"m"</js> - x 1000*1000
2301    *    <li><js>"g"</js> - x 1000*1000*1000
2302    * </ul>
2303    *
2304    * @param s The string to parse.
2305    * @return The parsed value.
2306    */
2307   public static int parseIntWithSuffix(String s) {
2308      assertArgNotNull("s", s);
2309      int m = multiplier(s);
2310      if (m == 1)
2311         return Integer.decode(s);
2312      return Integer.decode(s.substring(0, s.length()-1).trim()) * m;
2313   }
2314
2315   private static int multiplier(String s) {
2316      char c = s.isEmpty() ? null : s.charAt(s.length()-1);
2317      if (c == 'G') return 1024*1024*1024;
2318      if (c == 'M') return 1024*1024;
2319      if (c == 'K') return 1024;
2320      if (c == 'g') return 1000*1000*1000;
2321      if (c == 'm') return 1000*1000;
2322      if (c == 'k') return 1000;
2323      return 1;
2324   }
2325
2326   /**
2327    * Converts a string containing a possible multiplier suffix to a long.
2328    *
2329    * <p>
2330    * The string can contain any of the following multiplier suffixes:
2331    * <ul>
2332    *    <li><js>"K"</js> - x 1024
2333    *    <li><js>"M"</js> - x 1024*1024
2334    *    <li><js>"G"</js> - x 1024*1024*1024
2335    *    <li><js>"T"</js> - x 1024*1024*1024*1024
2336    *    <li><js>"P"</js> - x 1024*1024*1024*1024*1024
2337    *    <li><js>"k"</js> - x 1000
2338    *    <li><js>"m"</js> - x 1000*1000
2339    *    <li><js>"g"</js> - x 1000*1000*1000
2340    *    <li><js>"t"</js> - x 1000*1000*1000*1000
2341    *    <li><js>"p"</js> - x 1000*1000*1000*1000*1000
2342    * </ul>
2343    *
2344    * @param s The string to parse.
2345    * @return The parsed value.
2346    */
2347   public static long parseLongWithSuffix(String s) {
2348      assertArgNotNull("s", s);
2349      long m = multiplier2(s);
2350      if (m == 1)
2351         return Long.decode(s);
2352      return Long.decode(s.substring(0, s.length()-1).trim()) * m;
2353   }
2354
2355   private static long multiplier2(String s) {
2356      char c = s.isEmpty() ? null : s.charAt(s.length()-1);
2357      if (c == 'P') return 1024*1024*1024*1024*1024;
2358      if (c == 'T') return 1024*1024*1024*1024;
2359      if (c == 'G') return 1024*1024*1024;
2360      if (c == 'M') return 1024*1024;
2361      if (c == 'K') return 1024;
2362      if (c == 'p') return 1000*1000*1000*1000*1000;
2363      if (c == 't') return 1000*1000*1000*1000;
2364      if (c == 'g') return 1000*1000*1000;
2365      if (c == 'm') return 1000*1000;
2366      if (c == 'k') return 1000;
2367      return 1;
2368   }
2369
2370   /**
2371    * Same as {@link String#contains(CharSequence)} except returns <jk>null</jk> if the value is null.
2372    *
2373    * @param value The string to check.
2374    * @param substring The value to check for.
2375    * @return <jk>true</jk> if the value contains the specified substring.
2376    */
2377   public static boolean contains(String value, CharSequence substring) {
2378      return value == null ? false : value.contains(substring);
2379   }
2380
2381   /**
2382    * Returns <jk>true</jk> if the specified string appears to be an JSON array.
2383    *
2384    * @param o The object to test.
2385    * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored.
2386    * @return <jk>true</jk> if the specified string appears to be a JSON array.
2387    */
2388   public static boolean isJsonArray(Object o, boolean ignoreWhitespaceAndComments) {
2389      if (o instanceof CharSequence) {
2390         String s = o.toString();
2391         if (! ignoreWhitespaceAndComments)
2392            return (s.startsWith("[") && s.endsWith("]"));
2393         if (firstRealCharacter(s) != '[')
2394            return false;
2395         int i = s.lastIndexOf(']');
2396         if (i == -1)
2397            return false;
2398         s = s.substring(i+1);
2399         if (firstRealCharacter(s) != -1)
2400            return false;
2401         return true;
2402      }
2403      return false;
2404   }
2405
2406   /**
2407    * Returns <jk>true</jk> if the specified string is valid JSON.
2408    *
2409    * <p>
2410    * Leading and trailing spaces are ignored.
2411    * <br>Leading and trailing comments are not allowed.
2412    *
2413    * @param s The string to test.
2414    * @return <jk>true</jk> if the specified string is valid JSON.
2415    */
2416   public static boolean isJson(String s) {
2417      if (s == null)
2418         return false;
2419      char c1 = firstNonWhitespaceChar(s), c2 = lastNonWhitespaceChar(s);
2420      if (c1 == '{' && c2 == '}' || c1 == '[' && c2 == ']' || c1 == '\'' && c2 == '\'')
2421         return true;
2422      if (isOneOf(s, "true","false","null") || isNumeric(s))
2423         return true;
2424      return false;
2425   }
2426
2427   /**
2428    * Returns <jk>true</jk> if the specified string appears to be a JSON object.
2429    *
2430    * @param o The object to test.
2431    * @param ignoreWhitespaceAndComments If <jk>true</jk>, leading and trailing whitespace and comments will be ignored.
2432    * @return <jk>true</jk> if the specified string appears to be a JSON object.
2433    */
2434   public static boolean isJsonObject(Object o, boolean ignoreWhitespaceAndComments) {
2435      if (o instanceof CharSequence) {
2436         String s = o.toString();
2437         if (! ignoreWhitespaceAndComments)
2438            return (s.startsWith("{") && s.endsWith("}"));
2439         if (firstRealCharacter(s) != '{')
2440            return false;
2441         int i = s.lastIndexOf('}');
2442         if (i == -1)
2443            return false;
2444         s = s.substring(i+1);
2445         if (firstRealCharacter(s) != -1)
2446            return false;
2447         return true;
2448      }
2449      return false;
2450   }
2451
2452   private static int firstRealCharacter(String s) {
2453      try (StringReader r = new StringReader(s)) {
2454         int c = 0;
2455         while ((c = r.read()) != -1) {
2456            if (! Character.isWhitespace(c)) {
2457               if (c == '/') {
2458                  skipComments(r);
2459               } else {
2460                  return c;
2461               }
2462            }
2463         }
2464         return -1;
2465      } catch (Exception e) {
2466         throw asRuntimeException(e);
2467      }
2468   }
2469   private static void skipComments(StringReader r) throws IOException {
2470      int c = r.read();
2471      //  "/* */" style comments
2472      if (c == '*') {
2473         while (c != -1)
2474            if ((c = r.read()) == '*')
2475               if ((c = r.read()) == '/')
2476                  return;
2477      //  "//" style comments
2478      } else if (c == '/') {
2479         while (c != -1) {
2480            c = r.read();
2481            if (c == -1 || c == '\n')
2482               return;
2483         }
2484      }
2485   }
2486
2487   /**
2488    * Takes in a string, splits it by lines, and then prepends each line with line numbers.
2489    *
2490    * @param s The string.
2491    * @return The string with line numbers added.
2492    */
2493   public static String getNumberedLines(String s) {
2494      return getNumberedLines(s, 1, Integer.MAX_VALUE);
2495   }
2496
2497   /**
2498    * Same as {@link #getNumberedLines(String)} except only returns the specified lines.
2499    *
2500    * <p>
2501    * Out-of-bounds values are allowed and fixed.
2502    *
2503    * @param s The string.
2504    * @param start The starting line (1-indexed).
2505    * @param end The ending line (1-indexed).
2506    * @return The string with line numbers added.
2507    */
2508   public static String getNumberedLines(String s, int start, int end) {
2509      if (s == null)
2510         return null;
2511      String[] lines = s.split("[\r\n]+");
2512      final int digits = String.valueOf(lines.length).length();
2513      if (start < 1)
2514         start = 1;
2515      if (end < 0)
2516         end = Integer.MAX_VALUE;
2517      if (end > lines.length)
2518         end = lines.length;
2519      StringBuilder sb = new StringBuilder();
2520      for (String l :  Arrays.asList(lines).subList(start-1, end))
2521         sb.append(String.format("%0"+digits+"d", start++)).append(": ").append(l).append("\n");
2522      return sb.toString();
2523   }
2524
2525   /**
2526    * Compares two strings, but gracefully handles <jk>nulls</jk>.
2527    *
2528    * @param s1 The first string.
2529    * @param s2 The second string.
2530    * @return The same as {@link String#compareTo(String)}.
2531    */
2532   public static int compare(String s1, String s2) {
2533      if (s1 == null && s2 == null)
2534         return 0;
2535      if (s1 == null)
2536         return Integer.MIN_VALUE;
2537      if (s2 == null)
2538         return Integer.MAX_VALUE;
2539      return s1.compareTo(s2);
2540   }
2541
2542   /**
2543    * Returns the first character in the specified string.
2544    *
2545    * @param s The string to check.
2546    * @return The first character in the string, or <c>0</c> if the string is <jk>null</jk> or empty.
2547    */
2548   public static char firstChar(String s) {
2549      if (s == null || s.isEmpty())
2550         return 0;
2551      return s.charAt(0);
2552   }
2553
2554   /**
2555    * Converts a string containing <js>"*"</js> meta characters with a regular expression pattern.
2556    *
2557    * @param s The string to create a pattern from.
2558    * @return A regular expression pattern.
2559    */
2560   public static Pattern getMatchPattern(String s) {
2561      return getMatchPattern(s, 0);
2562   }
2563
2564   /**
2565    * Converts a string containing <js>"*"</js> meta characters with a regular expression pattern.
2566    *
2567    * @param s The string to create a pattern from.
2568    * @param flags Regular expression flags.
2569    * @return A regular expression pattern.
2570    */
2571   public static Pattern getMatchPattern(String s, int flags) {
2572      if (s == null)
2573         return null;
2574      StringBuilder sb = new StringBuilder();
2575      sb.append("\\Q");
2576      for (int i = 0; i < s.length(); i++) {
2577         char c = s.charAt(i);
2578         if (c == '*')
2579            sb.append("\\E").append(".*").append("\\Q");
2580         else if (c == '?')
2581            sb.append("\\E").append(".").append("\\Q");
2582         else
2583            sb.append(c);
2584      }
2585      sb.append("\\E");
2586      return Pattern.compile(sb.toString(), flags);
2587   }
2588
2589   /**
2590    * Parses a duration string.
2591    *
2592    * <p>
2593    * Examples:
2594    * <ul>
2595    *    <li><js>"1000"</js> - 1000 milliseconds.
2596    *    <li><js>"10s"</js> - 10 seconds.
2597    *    <li><js>"10 sec"</js> - 10 seconds.
2598    *    <li><js>"10 seconds"</js> - 10 seconds.
2599    * </ul>
2600    *
2601    * <p>
2602    * Use any of the following suffixes:
2603    * <ul>
2604    *    <li>None (time in milliseconds).
2605    *    <li><js>"s"</js>/<js>"sec"</js>/<js>"second"</js>/<js>"seconds"</js>
2606    *    <li><js>"m"</js>/<js>"min"</js>/<js>"minutes"</js>/<js>"seconds"</js>
2607    *    <li><js>"h"</js>/<js>"hour"</js>/<js>"hours"</js>
2608    *    <li><js>"d"</js>/<js>"day"</js>/<js>"days"</js>
2609    *    <li><js>"w"</js>/<js>"week"</js>/<js>"weeks"</js>
2610    * </ul>
2611    *
2612    * <p>
2613    * Suffixes are case-insensitive.
2614    * <br>Whitespace is ignored.
2615    *
2616    * @param s The string to parse.
2617    * @return
2618    *    The time in milliseconds, or <c>-1</c> if the string is empty or <jk>null</jk>.
2619    */
2620   public static long getDuration(String s) {
2621      s = trim(s);
2622      if (isEmpty(s))
2623         return -1;
2624      int i;
2625      for (i = 0; i < s.length(); i++) {
2626         char c = s.charAt(i);
2627         if (c < '0' || c > '9')
2628            break;
2629      }
2630      long l;
2631      if (i == s.length())
2632         l = Long.parseLong(s);
2633      else {
2634         l = Long.parseLong(s.substring(0, i).trim());
2635         String r = s.substring(i).trim().toLowerCase();
2636         if (r.startsWith("s"))
2637            l *= 1000;
2638         else if (r.startsWith("m"))
2639            l *= 1000 * 60;
2640         else if (r.startsWith("h"))
2641            l *= 1000 * 60 * 60;
2642         else if (r.startsWith("d"))
2643            l *= 1000 * 60 * 60 * 24;
2644         else if (r.startsWith("w"))
2645            l *= 1000 * 60 * 60 * 24 * 7;
2646      }
2647      return l;
2648   }
2649
2650   /**
2651    * Strips invalid characters such as CTRL characters from a string meant to be encoded
2652    * as an HTTP header value.
2653    *
2654    * @param s The string to strip chars from.
2655    * @return The string with invalid characters removed.
2656    */
2657   public static String stripInvalidHttpHeaderChars(String s) {
2658      if (s == null)
2659         return null;
2660
2661      boolean needsReplace = false;
2662      for (int i = 0; i < s.length() && ! needsReplace; i++)
2663         needsReplace |= httpHeaderChars.contains(s.charAt(i));
2664
2665      if (! needsReplace)
2666         return s;
2667
2668      StringBuilder sb = new StringBuilder(s.length());
2669      for (int i = 0; i < s.length(); i++) {
2670         char c = s.charAt(i);
2671         if (httpHeaderChars.contains(c))
2672            sb.append(c);
2673      }
2674
2675      return sb.toString();
2676   }
2677
2678   /**
2679    * Abbreviates a String using ellipses.
2680    *
2681    * @param in The input string.
2682    * @param length The max length of the resulting string.
2683    * @return The abbreviated string.
2684    */
2685   public static String abbreviate(String in, int length) {
2686      if (in == null || in.length() <= length || in.length() <= 3)
2687         return in;
2688      return in.substring(0, length-3) + "...";
2689   }
2690
2691   /**
2692    * Splits the method arguments in the signature of a method.
2693    *
2694    * @param s The arguments to split.
2695    * @return The split arguments.
2696    */
2697   public static String[] splitMethodArgs(String s) {
2698      if (s == null)
2699         return null;
2700      if (isEmpty(s))
2701         return new String[0];
2702      if (s.indexOf(',') == -1)
2703         return new String[]{s};
2704
2705      List<String> l = new LinkedList<>();
2706      char[] sArray = s.toCharArray();
2707      int x1 = 0, paramDepth = 0;
2708      for (int i = 0; i < sArray.length; i++) {
2709         char c = s.charAt(i);
2710         if (c == '>')
2711            paramDepth++;
2712         else if (c == '<')
2713            paramDepth--;
2714         else if (c == ',' && paramDepth == 0) {
2715            String s2 = new String(sArray, x1, i-x1);
2716            l.add(s2.trim());
2717            x1 = i+1;
2718         }
2719      }
2720      String s2 = new String(sArray, x1, sArray.length-x1);
2721      l.add(s2.trim());
2722
2723      return l.toArray(new String[l.size()]);
2724   }
2725
2726   private static final AsciiSet URI_CHARS = AsciiSet.create().chars("?#+%;/:@&=+$,-_.!~*'()").range('0','9').range('A','Z').range('a','z').build();
2727
2728   /**
2729    * Attempts to escape any invalid characters found in a URI.
2730    *
2731    * @param in The URI to fix.
2732    * @return The fixed URI.
2733    */
2734   public static String fixUrl(String in) {
2735
2736      if (in == null)
2737         return null;
2738
2739      StringBuilder sb = null;
2740
2741      int m = 0;
2742      for (int i = 0; i < in.length(); i++) {
2743         char c = in.charAt(i);
2744         if (c <= 127 && ! URI_CHARS.contains(c)) {
2745            sb = append(sb, in.substring(m, i));
2746            if (c == ' ')
2747               sb.append("+");
2748            else
2749               sb.append('%').append(toHex2(c));
2750            m = i+1;
2751         }
2752      }
2753      if (sb != null) {
2754         sb.append(in.substring(m));
2755         return sb.toString();
2756      }
2757      return in;
2758
2759   }
2760
2761   private static StringBuilder append(StringBuilder sb, String in) {
2762      if (sb == null)
2763         return new StringBuilder(in);
2764      sb.append(in);
2765      return sb;
2766   }
2767
2768   /**
2769    * Counts the number of the specified character in the specified string.
2770    *
2771    * @param s The string to check.
2772    * @param c The character to check for.
2773    * @return The number of those characters or zero if the string was <jk>null</jk>.
2774    */
2775   public static int countChars(String s, char c) {
2776      int count = 0;
2777      if (s == null)
2778         return count;
2779      for (int i = 0; i < s.length(); i++)
2780         if (s.charAt(i) == c)
2781            count++;
2782      return count;
2783   }
2784
2785   /**
2786    * Converts string into a GZipped input stream.
2787    *
2788    * @param contents The contents to compress.
2789    * @return The input stream converted to GZip.
2790    * @throws Exception Exception occurred.
2791    */
2792   public static byte[] compress(String contents) throws Exception {
2793      ByteArrayOutputStream baos = new ByteArrayOutputStream(contents.length()>>1);
2794      try (GZIPOutputStream gos = new GZIPOutputStream(baos)) {
2795         gos.write(contents.getBytes());
2796         gos.finish();
2797         gos.flush();
2798      }
2799      return baos.toByteArray();
2800   }
2801
2802   /**
2803    * Converts a GZipped input stream into a string.
2804    *
2805    * @param is The contents to decompress.
2806    * @return The string.
2807    * @throws Exception Exception occurred.
2808    */
2809   public static String decompress(byte[] is) throws Exception {
2810      return read(new GZIPInputStream(new ByteArrayInputStream(is)));
2811   }
2812
2813   /**
2814    * Converts the specified object to a comma-delimited list.
2815    *
2816    * @param o The object to convert.
2817    * @return The specified object as a comma-delimited list.
2818    */
2819   public static String cdl(Object o) {
2820      if (o == null)
2821         return null;
2822      if (o.getClass().isArray()) {
2823         StringBuilder sb = new StringBuilder();
2824         for (int i = 0, j = Array.getLength(o); i < j; i++) {
2825            if (i > 0)
2826               sb.append(", ");
2827            sb.append(Array.get(o, i));
2828         }
2829         return sb.toString();
2830      }
2831      if (o instanceof Collection)
2832         return join((Collection<?>)o, ", ");
2833      return o.toString();
2834   }
2835
2836   /**
2837    * Returns <jk>true</jk> if the specified character is a valid number character.
2838    *
2839    * @param c The character to check.
2840    * @return <jk>true</jk> if the specified character is a valid number character.
2841    */
2842   public static boolean isNumberChar(char c) {
2843      return numberChars.contains(c);
2844   }
2845}