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