View Javadoc
1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.util;
20  
21  import java.io.UnsupportedEncodingException;
22  import java.nio.charset.StandardCharsets;
23  import java.util.ArrayList;
24  import java.util.List;
25  
26  import org.eclipse.jetty.util.log.Log;
27  import org.eclipse.jetty.util.log.Logger;
28  
29  /** Fast String Utilities.
30   *
31   * These string utilities provide both convenience methods and
32   * performance improvements over most standard library versions. The
33   * main aim of the optimizations is to avoid object creation unless
34   * absolutely required.
35   *
36   * 
37   */
38  public class StringUtil
39  {
40      private static final Logger LOG = Log.getLogger(StringUtil.class);
41      
42      
43      private final static Trie<String> CHARSETS= new ArrayTrie<>(256);
44      
45      public static final String ALL_INTERFACES="0.0.0.0";
46      public static final String CRLF="\015\012";
47      
48      /** @deprecated use {@link System#lineSeparator()} instead */
49      @Deprecated
50      public static final String __LINE_SEPARATOR = System.lineSeparator();
51         
52      public static final String __ISO_8859_1="iso-8859-1";
53      public final static String __UTF8="utf-8";
54      public final static String __UTF16="utf-16";
55      
56      static
57      {
58          CHARSETS.put("utf-8",__UTF8);
59          CHARSETS.put("utf8",__UTF8);
60          CHARSETS.put("utf-16",__UTF16);
61          CHARSETS.put("utf16",__UTF16);
62          CHARSETS.put("iso-8859-1",__ISO_8859_1);
63          CHARSETS.put("iso_8859_1",__ISO_8859_1);
64      }
65      
66      /* ------------------------------------------------------------ */
67      /** Convert alternate charset names (eg utf8) to normalized
68       * name (eg UTF-8).
69       * @param s the charset to normalize
70       * @return the normalized charset (or null if normalized version not found)
71       */
72      public static String normalizeCharset(String s)
73      {
74          String n=CHARSETS.get(s);
75          return (n==null)?s:n;
76      }
77      
78      /* ------------------------------------------------------------ */
79      /** Convert alternate charset names (eg utf8) to normalized
80       * name (eg UTF-8).
81       * @param s the charset to normalize
82       * @param offset the offset in the charset
83       * @param length the length of the charset in the input param
84       * @return the normalized charset (or null if not found)
85       */
86      public static String normalizeCharset(String s,int offset,int length)
87      {
88          String n=CHARSETS.get(s,offset,length);       
89          return (n==null)?s.substring(offset,offset+length):n;
90      }
91      
92  
93      /* ------------------------------------------------------------ */
94      public static final char[] lowercases = {
95            '\000','\001','\002','\003','\004','\005','\006','\007',
96            '\010','\011','\012','\013','\014','\015','\016','\017',
97            '\020','\021','\022','\023','\024','\025','\026','\027',
98            '\030','\031','\032','\033','\034','\035','\036','\037',
99            '\040','\041','\042','\043','\044','\045','\046','\047',
100           '\050','\051','\052','\053','\054','\055','\056','\057',
101           '\060','\061','\062','\063','\064','\065','\066','\067',
102           '\070','\071','\072','\073','\074','\075','\076','\077',
103           '\100','\141','\142','\143','\144','\145','\146','\147',
104           '\150','\151','\152','\153','\154','\155','\156','\157',
105           '\160','\161','\162','\163','\164','\165','\166','\167',
106           '\170','\171','\172','\133','\134','\135','\136','\137',
107           '\140','\141','\142','\143','\144','\145','\146','\147',
108           '\150','\151','\152','\153','\154','\155','\156','\157',
109           '\160','\161','\162','\163','\164','\165','\166','\167',
110           '\170','\171','\172','\173','\174','\175','\176','\177' };
111 
112     /* ------------------------------------------------------------ */
113     /**
114      * fast lower case conversion. Only works on ascii (not unicode)
115      * @param s the string to convert
116      * @return a lower case version of s
117      */
118     public static String asciiToLowerCase(String s)
119     {
120         if (s == null)
121             return null;
122         
123         char[] c = null;
124         int i=s.length();
125 
126         // look for first conversion
127         while (i-->0)
128         {
129             char c1=s.charAt(i);
130             if (c1<=127)
131             {
132                 char c2=lowercases[c1];
133                 if (c1!=c2)
134                 {
135                     c=s.toCharArray();
136                     c[i]=c2;
137                     break;
138                 }
139             }
140         }
141 
142         while (i-->0)
143         {
144             if(c[i]<=127)
145                 c[i] = lowercases[c[i]];
146         }
147         
148         return c==null?s:new String(c);
149     }
150 
151 
152     /* ------------------------------------------------------------ */
153     public static boolean startsWithIgnoreCase(String s,String w)
154     {
155         if (w==null)
156             return true;
157         
158         if (s==null || s.length()<w.length())
159             return false;
160         
161         for (int i=0;i<w.length();i++)
162         {
163             char c1=s.charAt(i);
164             char c2=w.charAt(i);
165             if (c1!=c2)
166             {
167                 if (c1<=127)
168                     c1=lowercases[c1];
169                 if (c2<=127)
170                     c2=lowercases[c2];
171                 if (c1!=c2)
172                     return false;
173             }
174         }
175         return true;
176     }
177     
178     /* ------------------------------------------------------------ */
179     public static boolean endsWithIgnoreCase(String s,String w)
180     {
181         if (w==null)
182             return true;
183 
184         if (s==null)
185             return false;
186             
187         int sl=s.length();
188         int wl=w.length();
189         
190         if (sl<wl)
191             return false;
192         
193         for (int i=wl;i-->0;)
194         {
195             char c1=s.charAt(--sl);
196             char c2=w.charAt(i);
197             if (c1!=c2)
198             {
199                 if (c1<=127)
200                     c1=lowercases[c1];
201                 if (c2<=127)
202                     c2=lowercases[c2];
203                 if (c1!=c2)
204                     return false;
205             }
206         }
207         return true;
208     }
209     
210     /* ------------------------------------------------------------ */
211     /**
212      * returns the next index of a character from the chars string
213      * @param s the input string to search
214      * @param chars the chars to look for
215      * @return the index of the character in the input stream found.
216      */
217     public static int indexFrom(String s,String chars)
218     {
219         for (int i=0;i<s.length();i++)
220            if (chars.indexOf(s.charAt(i))>=0)
221               return i;
222         return -1;
223     }
224     
225     /* ------------------------------------------------------------ */
226     /**
227      * replace substrings within string.
228      * @param s the input string
229      * @param sub the string to look for
230      * @param with the string to replace with
231      * @return the now replaced string
232      */
233     public static String replace(String s, String sub, String with)
234     {
235         int c=0;
236         int i=s.indexOf(sub,c);
237         if (i == -1)
238             return s;
239     
240         StringBuilder buf = new StringBuilder(s.length()+with.length());
241 
242         do
243         {
244             buf.append(s.substring(c,i));
245             buf.append(with);
246             c=i+sub.length();
247         } while ((i=s.indexOf(sub,c))!=-1);
248 
249         if (c<s.length())
250             buf.append(s.substring(c,s.length()));
251 
252         return buf.toString();   
253     }
254 
255     /* ------------------------------------------------------------ */
256     /** Remove single or double quotes.
257      * @param s the input string
258      * @return the string with quotes removed
259      */
260     @Deprecated
261     public static String unquote(String s)
262     {
263         return QuotedStringTokenizer.unquote(s);
264     }
265 
266 
267     /* ------------------------------------------------------------ */
268     /** Append substring to StringBuilder 
269      * @param buf StringBuilder to append to
270      * @param s String to append from
271      * @param offset The offset of the substring
272      * @param length The length of the substring
273      */
274     public static void append(StringBuilder buf,
275                               String s,
276                               int offset,
277                               int length)
278     {
279         synchronized(buf)
280         {
281             int end=offset+length;
282             for (int i=offset; i<end;i++)
283             {
284                 if (i>=s.length())
285                     break;
286                 buf.append(s.charAt(i));
287             }
288         }
289     }
290 
291     
292     /* ------------------------------------------------------------ */
293     /**
294      * append hex digit
295      * @param buf the buffer to append to
296      * @param b the byte to append
297      * @param base the base of the hex output (almost always 16).
298      * 
299      */
300     public static void append(StringBuilder buf,byte b,int base)
301     {
302         int bi=0xff&b;
303         int c='0'+(bi/base)%base;
304         if (c>'9')
305             c= 'a'+(c-'0'-10);
306         buf.append((char)c);
307         c='0'+bi%base;
308         if (c>'9')
309             c= 'a'+(c-'0'-10);
310         buf.append((char)c);
311     }
312 
313     /* ------------------------------------------------------------ */
314     /**
315      * Append 2 digits (zero padded) to the StringBuffer
316      * 
317      * @param buf the buffer to append to
318      * @param i the value to append
319      */
320     public static void append2digits(StringBuffer buf,int i)
321     {
322         if (i<100)
323         {
324             buf.append((char)(i/10+'0'));
325             buf.append((char)(i%10+'0'));
326         }
327     }
328     
329     /* ------------------------------------------------------------ */
330     /**
331      * Append 2 digits (zero padded) to the StringBuilder
332      * 
333      * @param buf the buffer to append to
334      * @param i the value to append
335      */
336     public static void append2digits(StringBuilder buf,int i)
337     {
338         if (i<100)
339         {
340             buf.append((char)(i/10+'0'));
341             buf.append((char)(i%10+'0'));
342         }
343     }
344     
345     /* ------------------------------------------------------------ */
346     /** Return a non null string.
347      * @param s String
348      * @return The string passed in or empty string if it is null. 
349      */
350     public static String nonNull(String s)
351     {
352         if (s==null)
353             return "";
354         return s;
355     }
356     
357     /* ------------------------------------------------------------ */
358     public static boolean equals(String s,char[] buf, int offset, int length)
359     {
360         if (s.length()!=length)
361             return false;
362         for (int i=0;i<length;i++)
363             if (buf[offset+i]!=s.charAt(i))
364                 return false;
365         return true;
366     }
367 
368     /* ------------------------------------------------------------ */
369     public static String toUTF8String(byte[] b,int offset,int length)
370     {
371         return new String(b,offset,length,StandardCharsets.UTF_8);
372     }
373 
374     /* ------------------------------------------------------------ */
375     public static String toString(byte[] b,int offset,int length,String charset)
376     {
377         try
378         {
379             return new String(b,offset,length,charset);
380         }
381         catch (UnsupportedEncodingException e)
382         {
383             throw new IllegalArgumentException(e);
384         }
385     }
386 
387     /**
388      * Find the index of a control characters in String
389      * <p>
390      * This will return a result on the first occurrence of a control character, regardless if
391      * there are more than one.
392      * </p>
393      * <p>
394      * Note: uses codepoint version of {@link Character#isISOControl(int)} to support Unicode better.
395      * </p>
396      *
397      * <pre>
398      *   indexOfControlChars(null)      == -1
399      *   indexOfControlChars("")        == -1
400      *   indexOfControlChars("\r\n")    == 0
401      *   indexOfControlChars("\t")      == 0
402      *   indexOfControlChars("   ")     == -1
403      *   indexOfControlChars("a")       == -1
404      *   indexOfControlChars(".")       == -1
405      *   indexOfControlChars(";\n")     == 1
406      *   indexOfControlChars("abc\f")   == 3
407      *   indexOfControlChars("z\010")   == 1
408      *   indexOfControlChars(":\u001c") == 1
409      * </pre>
410      *
411      * @param str
412      *            the string to test.
413      * @return the index of first control character in string, -1 if no control characters encountered
414      */
415     public static int indexOfControlChars(String str)
416     {
417         if (str == null)
418         {
419             return -1;
420         }
421         int len = str.length();
422         for (int i = 0; i < len; i++)
423         {
424             if (Character.isISOControl(str.codePointAt(i)))
425             {
426                 // found a control character, we can stop searching  now
427                 return i;
428             }
429         }
430         // no control characters
431         return -1;
432     }
433 
434     /* ------------------------------------------------------------ */
435     /**
436      * Test if a string is null or only has whitespace characters in it.
437      * <p>
438      * Note: uses codepoint version of {@link Character#isWhitespace(int)} to support Unicode better.
439      * 
440      * <pre>
441      *   isBlank(null)   == true
442      *   isBlank("")     == true
443      *   isBlank("\r\n") == true
444      *   isBlank("\t")   == true
445      *   isBlank("   ")  == true
446      *   isBlank("a")    == false
447      *   isBlank(".")    == false
448      *   isBlank(";\n")  == false
449      * </pre>
450      * 
451      * @param str
452      *            the string to test.
453      * @return true if string is null or only whitespace characters, false if non-whitespace characters encountered.
454      */
455     public static boolean isBlank(String str)
456     {
457         if (str == null)
458         {
459             return true;
460         }
461         int len = str.length();
462         for (int i = 0; i < len; i++)
463         {
464             if (!Character.isWhitespace(str.codePointAt(i)))
465             {
466                 // found a non-whitespace, we can stop searching  now
467                 return false;
468             }
469         }
470         // only whitespace
471         return true;
472     }
473     
474     /* ------------------------------------------------------------ */
475     /**
476      * Test if a string is not null and contains at least 1 non-whitespace characters in it.
477      * <p>
478      * Note: uses codepoint version of {@link Character#isWhitespace(int)} to support Unicode better.
479      * 
480      * <pre>
481      *   isNotBlank(null)   == false
482      *   isNotBlank("")     == false
483      *   isNotBlank("\r\n") == false
484      *   isNotBlank("\t")   == false
485      *   isNotBlank("   ")  == false
486      *   isNotBlank("a")    == true
487      *   isNotBlank(".")    == true
488      *   isNotBlank(";\n")  == true
489      * </pre>
490      * 
491      * @param str
492      *            the string to test.
493      * @return true if string is not null and has at least 1 non-whitespace character, false if null or all-whitespace characters.
494      */
495     public static boolean isNotBlank(String str)
496     {
497         if (str == null)
498         {
499             return false;
500         }
501         int len = str.length();
502         for (int i = 0; i < len; i++)
503         {
504             if (!Character.isWhitespace(str.codePointAt(i)))
505             {
506                 // found a non-whitespace, we can stop searching  now
507                 return true;
508             }
509         }
510         // only whitespace
511         return false;
512     }
513 
514     /* ------------------------------------------------------------ */
515     public static boolean isUTF8(String charset)
516     {
517         return __UTF8.equalsIgnoreCase(charset)||__UTF8.equalsIgnoreCase(normalizeCharset(charset));
518     }
519 
520 
521     /* ------------------------------------------------------------ */
522     public static String printable(String name)
523     {
524         if (name==null)
525             return null;
526         StringBuilder buf = new StringBuilder(name.length());
527         for (int i=0;i<name.length();i++)
528         {
529             char c=name.charAt(i);
530             if (!Character.isISOControl(c))
531                 buf.append(c);
532         }
533         return buf.toString();
534     }
535     
536     /* ------------------------------------------------------------ */
537     public static String printable(byte[] b)
538     {
539         StringBuilder buf = new StringBuilder();
540         for (int i=0;i<b.length;i++)
541         {
542             char c=(char)b[i];
543             if (Character.isWhitespace(c)|| c>' ' && c<0x7f)
544                 buf.append(c);
545             else 
546             {
547                 buf.append("0x");
548                 TypeUtil.toHex(b[i],buf);
549             }
550         }
551         return buf.toString();
552     }
553     
554     public static byte[] getBytes(String s)
555     {
556         return s.getBytes(StandardCharsets.ISO_8859_1);
557     }
558     
559     public static byte[] getUtf8Bytes(String s)
560     {
561         return s.getBytes(StandardCharsets.UTF_8);
562     }
563     
564     public static byte[] getBytes(String s,String charset)
565     {
566         try
567         {
568             return s.getBytes(charset);
569         }
570         catch(Exception e)
571         {
572             LOG.warn(e);
573             return s.getBytes();
574         }
575     }
576     
577     
578     
579     /**
580      * Converts a binary SID to a string SID
581      * 
582      * http://en.wikipedia.org/wiki/Security_Identifier
583      * 
584      * S-1-IdentifierAuthority-SubAuthority1-SubAuthority2-...-SubAuthorityn
585      * @param sidBytes the SID bytes to build from
586      * @return the string SID
587      */
588     public static String sidBytesToString(byte[] sidBytes)
589     {
590         StringBuilder sidString = new StringBuilder();
591         
592         // Identify this as a SID
593         sidString.append("S-");
594         
595         // Add SID revision level (expect 1 but may change someday)
596         sidString.append(Byte.toString(sidBytes[0])).append('-');
597         
598         StringBuilder tmpBuilder = new StringBuilder();
599         
600         // crunch the six bytes of issuing authority value
601         for (int i = 2; i <= 7; ++i)
602         {
603             tmpBuilder.append(Integer.toHexString(sidBytes[i] & 0xFF));
604         }
605         
606         sidString.append(Long.parseLong(tmpBuilder.toString(), 16)); // '-' is in the subauth loop
607    
608         // the number of subAuthorities we need to attach
609         int subAuthorityCount = sidBytes[1];
610 
611         // attach each of the subAuthorities
612         for (int i = 0; i < subAuthorityCount; ++i)
613         {
614             int offset = i * 4;
615             tmpBuilder.setLength(0);
616             // these need to be zero padded hex and little endian
617             tmpBuilder.append(String.format("%02X%02X%02X%02X", 
618                     (sidBytes[11 + offset] & 0xFF),
619                     (sidBytes[10 + offset] & 0xFF),
620                     (sidBytes[9 + offset] & 0xFF),
621                     (sidBytes[8 + offset] & 0xFF)));  
622             sidString.append('-').append(Long.parseLong(tmpBuilder.toString(), 16));
623         }
624         
625         return sidString.toString();
626     }
627     
628     /**
629      * Converts a string SID to a binary SID
630      * 
631      * http://en.wikipedia.org/wiki/Security_Identifier
632      * 
633      * S-1-IdentifierAuthority-SubAuthority1-SubAuthority2-...-SubAuthorityn
634      * @param sidString the string SID
635      * @return the binary SID
636      */
637     public static byte[] sidStringToBytes( String sidString )
638     {
639         String[] sidTokens = sidString.split("-");
640         
641         int subAuthorityCount = sidTokens.length - 3; // S-Rev-IdAuth-
642         
643         int byteCount = 0;
644         byte[] sidBytes = new byte[1 + 1 + 6 + (4 * subAuthorityCount)];
645         
646         // the revision byte
647         sidBytes[byteCount++] = (byte)Integer.parseInt(sidTokens[1]);
648 
649         // the # of sub authorities byte
650         sidBytes[byteCount++] = (byte)subAuthorityCount;
651 
652         // the certAuthority
653         String hexStr = Long.toHexString(Long.parseLong(sidTokens[2]));
654         
655         while( hexStr.length() < 12) // pad to 12 characters
656         {
657             hexStr = "0" + hexStr;
658         }
659 
660         // place the certAuthority 6 bytes
661         for ( int i = 0 ; i < hexStr.length(); i = i + 2)
662         {
663             sidBytes[byteCount++] = (byte)Integer.parseInt(hexStr.substring(i, i + 2),16);
664         }
665                 
666         
667         for ( int i = 3; i < sidTokens.length ; ++i)
668         {
669             hexStr = Long.toHexString(Long.parseLong(sidTokens[i]));
670             
671             while( hexStr.length() < 8) // pad to 8 characters
672             {
673                 hexStr = "0" + hexStr;
674             }     
675             
676             // place the inverted sub authorities, 4 bytes each
677             for ( int j = hexStr.length(); j > 0; j = j - 2)
678             {          
679                 sidBytes[byteCount++] = (byte)Integer.parseInt(hexStr.substring(j-2, j),16);
680             }
681         }
682       
683         return sidBytes;
684     }
685 
686     /**
687      * Convert String to an integer. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
688      * 
689      * @param string A String containing an integer.
690      * @param from The index to start parsing from
691      * @return an int
692      */
693     public static int toInt(String string,int from)
694     {
695         int val = 0;
696         boolean started = false;
697         boolean minus = false;
698 
699         for (int i = from; i < string.length(); i++)
700         {
701             char b = string.charAt(i);
702             if (b <= ' ')
703             {
704                 if (started)
705                     break;
706             }
707             else if (b >= '0' && b <= '9')
708             {
709                 val = val * 10 + (b - '0');
710                 started = true;
711             }
712             else if (b == '-' && !started)
713             {
714                 minus = true;
715             }
716             else
717                 break;
718         }
719 
720         if (started)
721             return minus?(-val):val;
722         throw new NumberFormatException(string);
723     }
724     
725     /**
726      * Convert String to an long. Parses up to the first non-numeric character. If no number is found an IllegalArgumentException is thrown
727      * 
728      * @param string
729      *            A String containing an integer.
730      * @return an int
731      */
732     public static long toLong(String string)
733     {
734         long val = 0;
735         boolean started = false;
736         boolean minus = false;
737 
738         for (int i = 0; i < string.length(); i++)
739         {
740             char b = string.charAt(i);
741             if (b <= ' ')
742             {
743                 if (started)
744                     break;
745             }
746             else if (b >= '0' && b <= '9')
747             {
748                 val = val * 10L + (b - '0');
749                 started = true;
750             }
751             else if (b == '-' && !started)
752             {
753                 minus = true;
754             }
755             else
756                 break;
757         }
758 
759         if (started)
760             return minus?(-val):val;
761         throw new NumberFormatException(string);
762     }
763     
764     /**
765      * Truncate a string to a max size.
766      * 
767      * @param str the string to possibly truncate
768      * @param maxSize the maximum size of the string
769      * @return the truncated string.  if <code>str</code> param is null, then the returned string will also be null.
770      */
771     public static String truncate(String str, int maxSize)
772     {
773         if (str == null)
774         {
775             return null;
776         }
777 
778         if (str.length() <= maxSize)
779         {
780             return str;
781         }
782 
783         return str.substring(0,maxSize);
784     }
785 
786     /**
787     * Parse the string representation of a list using {@link #csvSplit(List,String,int,int)}
788     * @param s The string to parse, expected to be enclosed as '[...]'
789     * @return An array of parsed values.
790     */
791     public static String[] arrayFromString(String s) 
792     {
793         if (s==null)
794             return new String[]{};
795 
796         if (!s.startsWith("[") || !s.endsWith("]"))
797             throw new IllegalArgumentException();
798         if (s.length()==2)
799             return new String[]{};
800 
801         return csvSplit(s,1,s.length()-2);
802     }
803     
804     /**
805     * Parse a CSV string using {@link #csvSplit(List,String, int, int)}
806     * @param s The string to parse
807     * @return An array of parsed values.
808     */
809     public static String[] csvSplit(String s)
810     {
811         if (s==null)
812             return null;
813         return csvSplit(s,0,s.length());
814     }
815     
816     /**
817      * Parse a CSV string using {@link #csvSplit(List,String, int, int)}
818      * @param s The string to parse
819      * @param off The offset into the string to start parsing
820      * @param len The len in characters to parse
821      * @return An array of parsed values.
822      */
823     public static String[] csvSplit(String s, int off,int len)
824     {
825         if (s==null)
826             return null;
827         if (off<0 || len<0 || off>s.length())
828             throw new IllegalArgumentException();
829 
830         List<String> list = new ArrayList<>();
831         csvSplit(list,s,off,len);
832         return list.toArray(new String[list.size()]);
833     }
834 
835     enum CsvSplitState { PRE_DATA, QUOTE, SLOSH, DATA, WHITE, POST_DATA };
836 
837     /** Split a quoted comma separated string to a list
838      * <p>Handle <a href="https://www.ietf.org/rfc/rfc4180.txt">rfc4180</a>-like 
839      * CSV strings, with the exceptions:<ul>
840      * <li>quoted values may contain double quotes escaped with back-slash
841      * <li>Non-quoted values are trimmed of leading trailing white space
842      * <li>trailing commas are ignored
843      * <li>double commas result in a empty string value
844      * </ul>  
845      * @param list The Collection to split to (or null to get a new list)
846      * @param s The string to parse
847      * @param off The offset into the string to start parsing
848      * @param len The len in characters to parse
849      * @return list containing the parsed list values
850      */
851     public static List<String> csvSplit(List<String> list,String s, int off,int len)
852     {
853         if (list==null)
854             list=new ArrayList<>();
855         CsvSplitState state = CsvSplitState.PRE_DATA;
856         StringBuilder out = new StringBuilder();
857         int last=-1;
858         while (len>0)
859         {
860             char ch = s.charAt(off++);
861             len--;
862             
863             switch(state)
864             {
865                 case PRE_DATA:
866                     if (Character.isWhitespace(ch))
867                         continue;
868 
869                     if ('"'==ch)
870                     {
871                         state=CsvSplitState.QUOTE;
872                         continue;
873                     }
874                     
875                     if (','==ch)
876                     {
877                         list.add("");
878                         continue;
879                     }
880 
881                     state=CsvSplitState.DATA;
882                     out.append(ch);
883                     continue;
884 
885                 case DATA:
886                     if (Character.isWhitespace(ch))
887                     {
888                         last=out.length();
889                         out.append(ch);
890                         state=CsvSplitState.WHITE;
891                         continue;
892                     }
893                     
894                     if (','==ch)
895                     {
896                         list.add(out.toString());
897                         out.setLength(0);
898                         state=CsvSplitState.PRE_DATA;
899                         continue;
900                     }
901 
902                     out.append(ch);
903                     continue;
904                     
905                 case WHITE:
906                     if (Character.isWhitespace(ch))
907                     {
908                         out.append(ch);
909                         continue;
910                     }
911                     
912                     if (','==ch)
913                     {
914                         out.setLength(last);
915                         list.add(out.toString());
916                         out.setLength(0);
917                         state=CsvSplitState.PRE_DATA;
918                         continue;
919                     }
920                     
921                     state=CsvSplitState.DATA;
922                     out.append(ch);
923                     last=-1;
924                     continue;
925 
926                 case QUOTE:
927                     if ('\\'==ch)
928                     {
929                         state=CsvSplitState.SLOSH;
930                         continue;
931                     }
932                     if ('"'==ch)
933                     {
934                         list.add(out.toString());
935                         out.setLength(0);
936                         state=CsvSplitState.POST_DATA;
937                         continue;
938                     }
939                     out.append(ch);
940                     continue;
941                     
942                 case SLOSH:
943                     out.append(ch);
944                     state=CsvSplitState.QUOTE;
945                     continue;
946                     
947                 case POST_DATA:
948                     if (','==ch)
949                     {
950                         state=CsvSplitState.PRE_DATA;
951                         continue;
952                     }
953                     continue;
954             }
955         }
956 
957         switch(state)
958         {
959             case PRE_DATA:
960             case POST_DATA:
961                 break;
962 
963             case DATA:
964             case QUOTE:
965             case SLOSH:
966                 list.add(out.toString());
967                 break;
968                 
969             case WHITE:
970                 out.setLength(last);
971                 list.add(out.toString());
972                 break;
973         }
974         
975         return list;
976     }
977 
978     public static String sanitizeXmlString(String html)
979     {
980         if (html==null)
981             return null;
982         
983         int i=0;
984         
985         // Are there any characters that need sanitizing?
986         loop: for (;i<html.length();i++)
987         {
988             char c=html.charAt(i);
989 
990             switch(c)
991             {
992                 case '&' :
993                 case '<' :
994                 case '>' :
995                 case '\'':
996                 case '"':
997                     break loop;
998 
999                 default:
1000                     if (Character.isISOControl(c) && !Character.isWhitespace(c))
1001                         break loop;
1002             }
1003         }
1004 
1005         // No characters need sanitizing, so return original string
1006         if (i==html.length())
1007             return html;
1008         
1009         // Create builder with OK content so far 
1010         StringBuilder out = new StringBuilder(html.length()*4/3);
1011         out.append(html,0,i);
1012         
1013         // sanitize remaining content
1014         for (;i<html.length();i++)
1015         {
1016             char c=html.charAt(i);
1017 
1018             switch(c)
1019             {
1020                 case '&' :
1021                     out.append("&amp;");
1022                     break;
1023                 case '<' :
1024                     out.append("&lt;");
1025                     break;
1026                 case '>' :
1027                     out.append("&gt;");
1028                     break;
1029                 case '\'':
1030                     out.append("&apos;");
1031                     break;
1032                 case '"':
1033                     out.append("&quot;");
1034                     break;
1035 
1036                 default:
1037                     if (Character.isISOControl(c) && !Character.isWhitespace(c))
1038                         out.append('?');
1039                     else
1040                         out.append(c);
1041             }
1042         }
1043         return out.toString();
1044     }
1045     
1046     /* ------------------------------------------------------------ */
1047     /** The String value of an Object
1048      * <p>This method calls {@link String#valueOf(Object)} unless the object is null,
1049      * in which case null is returned</p>
1050      * @param object The object
1051      * @return String value or null
1052      */
1053     public static String valueOf(Object object)
1054     {
1055         return object==null?null:String.valueOf(object);
1056     }
1057 
1058 }