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