1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  package io.netty5.util.internal;
17  
18  import io.netty5.util.concurrent.FastThreadLocal;
19  import io.netty5.util.internal.logging.InternalLogger;
20  import io.netty5.util.internal.logging.InternalLoggerFactory;
21  
22  import java.io.IOException;
23  import java.io.UncheckedIOException;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Iterator;
27  import java.util.List;
28  
29  import static java.util.Objects.requireNonNull;
30  
31  
32  
33  
34  public final class StringUtil {
35  
36      public static final String EMPTY_STRING = "";
37      public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n");
38  
39      public static final char DOUBLE_QUOTE = '\"';
40      public static final char COMMA = ',';
41      public static final char LINE_FEED = '\n';
42      public static final char CARRIAGE_RETURN = '\r';
43      public static final char TAB = '\t';
44      public static final char SPACE = 0x20;
45  
46      private static final String[] BYTE2HEX_PAD = new String[256];
47      private static final String[] BYTE2HEX_NOPAD = new String[256];
48      private static final byte[] HEX2B;
49  
50      
51  
52  
53  
54      private static final int CSV_NUMBER_ESCAPE_CHARACTERS = 2 + 5;
55      private static final char PACKAGE_SEPARATOR_CHAR = '.';
56  
57      private static final int STRING_BUILDER_INITIAL_SIZE;
58      private static final int STRING_BUILDER_MAX_SIZE;
59      private static final InternalLogger logger = InternalLoggerFactory.getInstance(StringUtil.class);
60  
61      static {
62          
63          for (int i = 0; i < BYTE2HEX_PAD.length; i++) {
64              String str = Integer.toHexString(i);
65              BYTE2HEX_PAD[i] = i > 0xf ? str : '0' + str;
66              BYTE2HEX_NOPAD[i] = str;
67          }
68          
69          
70          
71          HEX2B = new byte[Character.MAX_VALUE + 1];
72          Arrays.fill(HEX2B, (byte) -1);
73          HEX2B['0'] = 0;
74          HEX2B['1'] = 1;
75          HEX2B['2'] = 2;
76          HEX2B['3'] = 3;
77          HEX2B['4'] = 4;
78          HEX2B['5'] = 5;
79          HEX2B['6'] = 6;
80          HEX2B['7'] = 7;
81          HEX2B['8'] = 8;
82          HEX2B['9'] = 9;
83          HEX2B['A'] = 10;
84          HEX2B['B'] = 11;
85          HEX2B['C'] = 12;
86          HEX2B['D'] = 13;
87          HEX2B['E'] = 14;
88          HEX2B['F'] = 15;
89          HEX2B['a'] = 10;
90          HEX2B['b'] = 11;
91          HEX2B['c'] = 12;
92          HEX2B['d'] = 13;
93          HEX2B['e'] = 14;
94          HEX2B['f'] = 15;
95  
96          STRING_BUILDER_INITIAL_SIZE =
97                  SystemPropertyUtil.getInt("io.netty5.stringUtil.stringBuilder.initialSize", 1024);
98          logger.debug("-Dio.netty5.stringUtil.stringBuilder.initialSize: {}", STRING_BUILDER_INITIAL_SIZE);
99  
100         STRING_BUILDER_MAX_SIZE = SystemPropertyUtil.getInt("io.netty5.stringUtil.stringBuilder.maxSize", 1024 * 4);
101         logger.debug("-Dio.netty5.stringUtil.stringBuilder.maxSize: {}", STRING_BUILDER_MAX_SIZE);
102     }
103 
104     private static final FastThreadLocal<StringBuilder> STRING_BUILDERS = new FastThreadLocal<>() {
105         @Override
106         protected StringBuilder initialValue() {
107             return new StringBuilder(STRING_BUILDER_INITIAL_SIZE);
108         }
109     };
110 
111     private StringUtil() {
112         
113     }
114 
115     public static StringBuilder threadLocalStringBuilder() {
116         StringBuilder sb = STRING_BUILDERS.get();
117         if (sb.capacity() > STRING_BUILDER_MAX_SIZE) {
118             sb.setLength(STRING_BUILDER_INITIAL_SIZE);
119             sb.trimToSize();
120         }
121         sb.setLength(0);
122         return sb;
123     }
124 
125     
126 
127 
128 
129 
130     public static String substringAfter(String value, char delim) {
131         int pos = value.indexOf(delim);
132         if (pos >= 0) {
133             return value.substring(pos + 1);
134         }
135         return null;
136     }
137 
138     
139 
140 
141 
142 
143 
144 
145 
146     public static boolean commonSuffixOfLength(String s, String p, int len) {
147         return s != null && p != null && len >= 0 && s.regionMatches(s.length() - len, p, p.length() - len, len);
148     }
149 
150     
151 
152 
153     public static String byteToHexStringPadded(int value) {
154         return BYTE2HEX_PAD[value & 0xff];
155     }
156 
157     
158 
159 
160     public static <T extends Appendable> T byteToHexStringPadded(T buf, int value) {
161         try {
162             buf.append(byteToHexStringPadded(value));
163         } catch (IOException e) {
164             throw new UncheckedIOException(e);
165         }
166         return buf;
167     }
168 
169     
170 
171 
172     public static String toHexStringPadded(byte[] src) {
173         return toHexStringPadded(src, 0, src.length);
174     }
175 
176     
177 
178 
179     public static String toHexStringPadded(byte[] src, int offset, int length) {
180         return toHexStringPadded(new StringBuilder(length << 1), src, offset, length).toString();
181     }
182 
183     
184 
185 
186     public static <T extends Appendable> T toHexStringPadded(T dst, byte[] src) {
187         return toHexStringPadded(dst, src, 0, src.length);
188     }
189 
190     
191 
192 
193     public static <T extends Appendable> T toHexStringPadded(T dst, byte[] src, int offset, int length) {
194         final int end = offset + length;
195         for (int i = offset; i < end; i++) {
196             byteToHexStringPadded(dst, src[i]);
197         }
198         return dst;
199     }
200 
201     
202 
203 
204     public static String byteToHexString(int value) {
205         return BYTE2HEX_NOPAD[value & 0xff];
206     }
207 
208     
209 
210 
211     public static <T extends Appendable> T byteToHexString(T buf, int value) {
212         try {
213             buf.append(byteToHexString(value));
214         } catch (IOException e) {
215             throw new UncheckedIOException(e);
216         }
217         return buf;
218     }
219 
220     
221 
222 
223     public static String toHexString(byte[] src) {
224         return toHexString(src, 0, src.length);
225     }
226 
227     
228 
229 
230     public static String toHexString(byte[] src, int offset, int length) {
231         return toHexString(new StringBuilder(length << 1), src, offset, length).toString();
232     }
233 
234     
235 
236 
237     public static <T extends Appendable> T toHexString(T dst, byte[] src) {
238         return toHexString(dst, src, 0, src.length);
239     }
240 
241     
242 
243 
244     public static <T extends Appendable> T toHexString(T dst, byte[] src, int offset, int length) {
245         assert length >= 0;
246         if (length == 0) {
247             return dst;
248         }
249 
250         final int end = offset + length;
251         final int endMinusOne = end - 1;
252         int i;
253 
254         
255         for (i = offset; i < endMinusOne; i++) {
256             if (src[i] != 0) {
257                 break;
258             }
259         }
260 
261         byteToHexString(dst, src[i++]);
262         int remaining = end - i;
263         toHexStringPadded(dst, src, i, remaining);
264 
265         return dst;
266     }
267 
268     
269 
270 
271 
272 
273 
274 
275     public static int decodeHexNibble(final char c) {
276         assert HEX2B.length == Character.MAX_VALUE + 1;
277         
278         
279         return HEX2B[c];
280     }
281 
282     
283 
284 
285     public static byte decodeHexByte(CharSequence s, int pos) {
286         int hi = decodeHexNibble(s.charAt(pos));
287         int lo = decodeHexNibble(s.charAt(pos + 1));
288         if (hi == -1 || lo == -1) {
289             throw new IllegalArgumentException(String.format(
290                     "invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s));
291         }
292         return (byte) ((hi << 4) + lo);
293     }
294 
295     
296 
297 
298 
299 
300 
301 
302     public static byte[] decodeHexDump(CharSequence hexDump, int fromIndex, int length) {
303         if (length < 0 || (length & 1) != 0) {
304             throw new IllegalArgumentException("length: " + length);
305         }
306         if (length == 0) {
307             return EmptyArrays.EMPTY_BYTES;
308         }
309         byte[] bytes = new byte[length >>> 1];
310         for (int i = 0; i < length; i += 2) {
311             bytes[i >>> 1] = decodeHexByte(hexDump, fromIndex + i);
312         }
313         return bytes;
314     }
315 
316     
317 
318 
319     public static byte[] decodeHexDump(CharSequence hexDump) {
320         return decodeHexDump(hexDump, 0, hexDump.length());
321     }
322 
323     
324 
325 
326     public static String simpleClassName(Object o) {
327         if (o == null) {
328             return "null_object";
329         } else {
330             return simpleClassName(o.getClass());
331         }
332     }
333 
334     
335 
336 
337 
338     public static String simpleClassName(Class<?> clazz) {
339         String className = requireNonNull(clazz, "clazz").getName();
340         final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
341         if (lastDotIdx > -1) {
342             return className.substring(lastDotIdx + 1);
343         }
344         return className;
345     }
346 
347     
348 
349 
350 
351 
352 
353 
354 
355     public static CharSequence escapeCsv(CharSequence value) {
356         return escapeCsv(value, false);
357     }
358 
359     
360 
361 
362 
363 
364 
365 
366 
367 
368 
369     public static CharSequence escapeCsv(CharSequence value, boolean trimWhiteSpace) {
370         int length = requireNonNull(value, "value").length();
371         int start;
372         int last;
373         if (trimWhiteSpace) {
374             start = indexOfFirstNonOwsChar(value, length);
375             last = indexOfLastNonOwsChar(value, start, length);
376         } else {
377             start = 0;
378             last = length - 1;
379         }
380         if (start > last) {
381             return EMPTY_STRING;
382         }
383 
384         int firstUnescapedSpecial = -1;
385         boolean quoted = false;
386         if (isDoubleQuote(value.charAt(start))) {
387             quoted = isDoubleQuote(value.charAt(last)) && last > start;
388             if (quoted) {
389                 start++;
390                 last--;
391             } else {
392                 firstUnescapedSpecial = start;
393             }
394         }
395 
396         if (firstUnescapedSpecial < 0) {
397             if (quoted) {
398                 for (int i = start; i <= last; i++) {
399                     if (isDoubleQuote(value.charAt(i))) {
400                         if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
401                             firstUnescapedSpecial = i;
402                             break;
403                         }
404                         i++;
405                     }
406                 }
407             } else {
408                 for (int i = start; i <= last; i++) {
409                     char c = value.charAt(i);
410                     if (c == LINE_FEED || c == CARRIAGE_RETURN || c == COMMA) {
411                         firstUnescapedSpecial = i;
412                         break;
413                     }
414                     if (isDoubleQuote(c)) {
415                         if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
416                             firstUnescapedSpecial = i;
417                             break;
418                         }
419                         i++;
420                     }
421                 }
422             }
423 
424             if (firstUnescapedSpecial < 0) {
425                 
426                 
427                 
428                 return quoted? value.subSequence(start - 1, last + 2) : value.subSequence(start, last + 1);
429             }
430         }
431 
432         StringBuilder result = new StringBuilder(last - start + 1 + CSV_NUMBER_ESCAPE_CHARACTERS);
433         result.append(DOUBLE_QUOTE).append(value, start, firstUnescapedSpecial);
434         for (int i = firstUnescapedSpecial; i <= last; i++) {
435             char c = value.charAt(i);
436             if (isDoubleQuote(c)) {
437                 result.append(DOUBLE_QUOTE);
438                 if (i < last && isDoubleQuote(value.charAt(i + 1))) {
439                     i++;
440                 }
441             }
442             result.append(c);
443         }
444         return result.append(DOUBLE_QUOTE);
445     }
446 
447     
448 
449 
450 
451 
452 
453 
454 
455     public static CharSequence unescapeCsv(CharSequence value) {
456         int length = requireNonNull(value, "value").length();
457         if (length == 0) {
458             return value;
459         }
460         int last = length - 1;
461         boolean quoted = isDoubleQuote(value.charAt(0)) && isDoubleQuote(value.charAt(last)) && length != 1;
462         if (!quoted) {
463             validateCsvFormat(value);
464             return value;
465         }
466         StringBuilder unescaped = threadLocalStringBuilder();
467         for (int i = 1; i < last; i++) {
468             char current = value.charAt(i);
469             if (current == DOUBLE_QUOTE) {
470                 if (isDoubleQuote(value.charAt(i + 1)) && (i + 1) != last) {
471                     
472                     
473                     i++;
474                 } else {
475                     
476                     throw newInvalidEscapedCsvFieldException(value, i);
477                 }
478             }
479             unescaped.append(current);
480         }
481         return unescaped.toString();
482     }
483 
484     
485 
486 
487 
488 
489 
490 
491 
492     public static List<CharSequence> unescapeCsvFields(CharSequence value) {
493         List<CharSequence> unescaped = new ArrayList<>(2);
494         StringBuilder current = threadLocalStringBuilder();
495         boolean quoted = false;
496         int last = value.length() - 1;
497         for (int i = 0; i <= last; i++) {
498             char c = value.charAt(i);
499             if (quoted) {
500                 if (c == DOUBLE_QUOTE) {
501                     if (i == last) {
502                         
503                         unescaped.add(current.toString());
504                         return unescaped;
505                     }
506                     char next = value.charAt(++i);
507                     if (next == DOUBLE_QUOTE) {
508                         
509                         current.append(DOUBLE_QUOTE);
510                         continue;
511                     }
512                     if (next == COMMA) {
513                         
514                         quoted = false;
515                         unescaped.add(current.toString());
516                         current.setLength(0);
517                         continue;
518                     }
519                     
520                     throw newInvalidEscapedCsvFieldException(value, i - 1);
521                 } else {
522                     current.append(c);
523                 }
524             } else {
525                 switch (c) {
526                     case COMMA:
527                         
528                         unescaped.add(current.toString());
529                         current.setLength(0);
530                         break;
531                     case DOUBLE_QUOTE:
532                         if (current.length() == 0) {
533                             quoted = true;
534                             break;
535                         }
536                         
537                         
538                     case LINE_FEED:
539                         
540                     case CARRIAGE_RETURN:
541                         
542                         throw newInvalidEscapedCsvFieldException(value, i);
543                     default:
544                         current.append(c);
545                 }
546             }
547         }
548         if (quoted) {
549             throw newInvalidEscapedCsvFieldException(value, last);
550         }
551         unescaped.add(current.toString());
552         return unescaped;
553     }
554 
555     
556 
557 
558 
559 
560     private static void validateCsvFormat(CharSequence value) {
561         int length = value.length();
562         for (int i = 0; i < length; i++) {
563             switch (value.charAt(i)) {
564                 case DOUBLE_QUOTE:
565                 case LINE_FEED:
566                 case CARRIAGE_RETURN:
567                 case COMMA:
568                     
569                     throw newInvalidEscapedCsvFieldException(value, i);
570                 default:
571             }
572         }
573     }
574 
575     private static IllegalArgumentException newInvalidEscapedCsvFieldException(CharSequence value, int index) {
576         return new IllegalArgumentException("invalid escaped CSV field: " + value + " index: " + index);
577     }
578 
579     
580 
581 
582     public static int length(String s) {
583         return s == null ? 0 : s.length();
584     }
585 
586     
587 
588 
589     public static boolean isNullOrEmpty(String s) {
590         return s == null || s.isEmpty();
591     }
592 
593     
594 
595 
596 
597 
598 
599 
600     public static int indexOfNonWhiteSpace(CharSequence seq, int offset) {
601         for (; offset < seq.length(); ++offset) {
602             if (!Character.isWhitespace(seq.charAt(offset))) {
603                 return offset;
604             }
605         }
606         return -1;
607     }
608 
609     
610 
611 
612 
613 
614 
615 
616     public static int indexOfWhiteSpace(CharSequence seq, int offset) {
617         for (; offset < seq.length(); ++offset) {
618             if (Character.isWhitespace(seq.charAt(offset))) {
619                 return offset;
620             }
621         }
622         return -1;
623     }
624 
625     
626 
627 
628 
629 
630 
631 
632 
633     public static boolean isSurrogate(char c) {
634         return c >= '\uD800' && c <= '\uDFFF';
635     }
636 
637     private static boolean isDoubleQuote(char c) {
638         return c == DOUBLE_QUOTE;
639     }
640 
641     
642 
643 
644 
645 
646 
647 
648     public static boolean endsWith(CharSequence s, char c) {
649         int len = s.length();
650         return len > 0 && s.charAt(len - 1) == c;
651     }
652 
653     
654 
655 
656 
657 
658 
659 
660     public static CharSequence trimOws(CharSequence value) {
661         final int length = value.length();
662         if (length == 0) {
663             return value;
664         }
665         int start = indexOfFirstNonOwsChar(value, length);
666         int end = indexOfLastNonOwsChar(value, start, length);
667         return start == 0 && end == length - 1 ? value : value.subSequence(start, end + 1);
668     }
669 
670     
671 
672 
673 
674 
675 
676 
677 
678     public static CharSequence join(CharSequence separator, Iterable<? extends CharSequence> elements) {
679         requireNonNull(separator, "separator");
680         requireNonNull(elements, "elements");
681 
682         Iterator<? extends CharSequence> iterator = elements.iterator();
683         if (!iterator.hasNext()) {
684             return EMPTY_STRING;
685         }
686 
687         CharSequence firstElement = iterator.next();
688         if (!iterator.hasNext()) {
689             return firstElement;
690         }
691 
692         StringBuilder builder = new StringBuilder(firstElement);
693         do {
694             builder.append(separator).append(iterator.next());
695         } while (iterator.hasNext());
696 
697         return builder;
698     }
699 
700     
701 
702 
703     private static int indexOfFirstNonOwsChar(CharSequence value, int length) {
704         int i = 0;
705         while (i < length && isOws(value.charAt(i))) {
706             i++;
707         }
708         return i;
709     }
710 
711     
712 
713 
714     private static int indexOfLastNonOwsChar(CharSequence value, int start, int length) {
715         int i = length - 1;
716         while (i > start && isOws(value.charAt(i))) {
717             i--;
718         }
719         return i;
720     }
721 
722     private static boolean isOws(char c) {
723         return c == SPACE || c == TAB;
724     }
725 }