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