View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.util.internal;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Iterator;
22  import java.util.List;
23  
24  import static io.netty.util.internal.ObjectUtil.*;
25  
26  /**
27   * String utility class.
28   */
29  public final class StringUtil {
30  
31      public static final String EMPTY_STRING = "";
32      public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n");
33  
34      public static final char DOUBLE_QUOTE = '\"';
35      public static final char COMMA = ',';
36      public static final char LINE_FEED = '\n';
37      public static final char CARRIAGE_RETURN = '\r';
38      public static final char TAB = '\t';
39      public static final char SPACE = 0x20;
40  
41      private static final String[] BYTE2HEX_PAD = new String[256];
42      private static final String[] BYTE2HEX_NOPAD = new String[256];
43      private static final byte[] HEX2B;
44  
45      /**
46       * 2 - Quote character at beginning and end.
47       * 5 - Extra allowance for anticipated escape characters that may be added.
48       */
49      private static final int CSV_NUMBER_ESCAPE_CHARACTERS = 2 + 5;
50      private static final char PACKAGE_SEPARATOR_CHAR = '.';
51  
52      static {
53          // Generate the lookup table that converts a byte into a 2-digit hexadecimal integer.
54          for (int i = 0; i < BYTE2HEX_PAD.length; i++) {
55              String str = Integer.toHexString(i);
56              BYTE2HEX_PAD[i] = i > 0xf ? str : ('0' + str);
57              BYTE2HEX_NOPAD[i] = str;
58          }
59          // Generate the lookup table that converts an hex char into its decimal value:
60          // the size of the table is such that the JVM is capable of save any bounds-check
61          // if a char type is used as an index.
62          HEX2B = new byte[Character.MAX_VALUE + 1];
63          Arrays.fill(HEX2B, (byte) -1);
64          HEX2B['0'] = 0;
65          HEX2B['1'] = 1;
66          HEX2B['2'] = 2;
67          HEX2B['3'] = 3;
68          HEX2B['4'] = 4;
69          HEX2B['5'] = 5;
70          HEX2B['6'] = 6;
71          HEX2B['7'] = 7;
72          HEX2B['8'] = 8;
73          HEX2B['9'] = 9;
74          HEX2B['A'] = 10;
75          HEX2B['B'] = 11;
76          HEX2B['C'] = 12;
77          HEX2B['D'] = 13;
78          HEX2B['E'] = 14;
79          HEX2B['F'] = 15;
80          HEX2B['a'] = 10;
81          HEX2B['b'] = 11;
82          HEX2B['c'] = 12;
83          HEX2B['d'] = 13;
84          HEX2B['e'] = 14;
85          HEX2B['f'] = 15;
86      }
87  
88      private StringUtil() {
89          // Unused.
90      }
91  
92      /**
93       * Get the item after one char delim if the delim is found (else null).
94       * This operation is a simplified and optimized
95       * version of {@link String#split(String, int)}.
96       */
97      public static String substringAfter(String value, char delim) {
98          int pos = value.indexOf(delim);
99          if (pos >= 0) {
100             return value.substring(pos + 1);
101         }
102         return null;
103     }
104 
105     /**
106      * Get the item before one char delim if the delim is found (else null).
107      * This operation is a simplified and optimized
108      * version of {@link String#split(String, int)}.
109      */
110     public static String substringBefore(String value, char delim) {
111         int pos = value.indexOf(delim);
112         if (pos >= 0) {
113             return value.substring(0, pos);
114         }
115         return null;
116     }
117 
118     /**
119      * Checks if two strings have the same suffix of specified length
120      *
121      * @param s   string
122      * @param p   string
123      * @param len length of the common suffix
124      * @return true if both s and p are not null and both have the same suffix. Otherwise - false
125      */
126     public static boolean commonSuffixOfLength(String s, String p, int len) {
127         return s != null && p != null && len >= 0 && s.regionMatches(s.length() - len, p, p.length() - len, len);
128     }
129 
130     /**
131      * Converts the specified byte value into a 2-digit hexadecimal integer.
132      */
133     public static String byteToHexStringPadded(int value) {
134         return BYTE2HEX_PAD[value & 0xff];
135     }
136 
137     /**
138      * Converts the specified byte value into a 2-digit hexadecimal integer and appends it to the specified buffer.
139      */
140     public static <T extends Appendable> T byteToHexStringPadded(T buf, int value) {
141         try {
142             buf.append(byteToHexStringPadded(value));
143         } catch (IOException e) {
144             PlatformDependent.throwException(e);
145         }
146         return buf;
147     }
148 
149     /**
150      * Converts the specified byte array into a hexadecimal value.
151      */
152     public static String toHexStringPadded(byte[] src) {
153         return toHexStringPadded(src, 0, src.length);
154     }
155 
156     /**
157      * Converts the specified byte array into a hexadecimal value.
158      */
159     public static String toHexStringPadded(byte[] src, int offset, int length) {
160         return toHexStringPadded(new StringBuilder(length << 1), src, offset, length).toString();
161     }
162 
163     /**
164      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
165      */
166     public static <T extends Appendable> T toHexStringPadded(T dst, byte[] src) {
167         return toHexStringPadded(dst, src, 0, src.length);
168     }
169 
170     /**
171      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
172      */
173     public static <T extends Appendable> T toHexStringPadded(T dst, byte[] src, int offset, int length) {
174         final int end = offset + length;
175         for (int i = offset; i < end; i++) {
176             byteToHexStringPadded(dst, src[i]);
177         }
178         return dst;
179     }
180 
181     /**
182      * Converts the specified byte value into a hexadecimal integer.
183      */
184     public static String byteToHexString(int value) {
185         return BYTE2HEX_NOPAD[value & 0xff];
186     }
187 
188     /**
189      * Converts the specified byte value into a hexadecimal integer and appends it to the specified buffer.
190      */
191     public static <T extends Appendable> T byteToHexString(T buf, int value) {
192         try {
193             buf.append(byteToHexString(value));
194         } catch (IOException e) {
195             PlatformDependent.throwException(e);
196         }
197         return buf;
198     }
199 
200     /**
201      * Converts the specified byte array into a hexadecimal value.
202      */
203     public static String toHexString(byte[] src) {
204         return toHexString(src, 0, src.length);
205     }
206 
207     /**
208      * Converts the specified byte array into a hexadecimal value.
209      */
210     public static String toHexString(byte[] src, int offset, int length) {
211         return toHexString(new StringBuilder(length << 1), src, offset, length).toString();
212     }
213 
214     /**
215      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
216      */
217     public static <T extends Appendable> T toHexString(T dst, byte[] src) {
218         return toHexString(dst, src, 0, src.length);
219     }
220 
221     /**
222      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
223      */
224     public static <T extends Appendable> T toHexString(T dst, byte[] src, int offset, int length) {
225         assert length >= 0;
226         if (length == 0) {
227             return dst;
228         }
229 
230         final int end = offset + length;
231         final int endMinusOne = end - 1;
232         int i;
233 
234         // Skip preceding zeroes.
235         for (i = offset; i < endMinusOne; i++) {
236             if (src[i] != 0) {
237                 break;
238             }
239         }
240 
241         byteToHexString(dst, src[i++]);
242         int remaining = end - i;
243         toHexStringPadded(dst, src, i, remaining);
244 
245         return dst;
246     }
247 
248     /**
249      * Helper to decode half of a hexadecimal number from a string.
250      * @param c The ASCII character of the hexadecimal number to decode.
251      * Must be in the range {@code [0-9a-fA-F]}.
252      * @return The hexadecimal value represented in the ASCII character
253      * given, or {@code -1} if the character is invalid.
254      */
255     public static int decodeHexNibble(final char c) {
256         // Character.digit() is not used here, as it addresses a larger
257         // set of characters (both ASCII and full-width latin letters).
258         return HEX2B[c];
259     }
260 
261     /**
262      * Helper to decode half of a hexadecimal number from a string.
263      * @param b The ASCII character of the hexadecimal number to decode.
264      * Must be in the range {@code [0-9a-fA-F]}.
265      * @return The hexadecimal value represented in the ASCII character
266      * given, or {@code -1} if the character is invalid.
267      */
268     public static int decodeHexNibble(final byte b) {
269         // Character.digit() is not used here, as it addresses a larger
270         // set of characters (both ASCII and full-width latin letters).
271         return HEX2B[b];
272     }
273 
274     /**
275      * Decode a 2-digit hex byte from within a string.
276      */
277     public static byte decodeHexByte(CharSequence s, int pos) {
278         int hi = decodeHexNibble(s.charAt(pos));
279         int lo = decodeHexNibble(s.charAt(pos + 1));
280         if (hi == -1 || lo == -1) {
281             throw new IllegalArgumentException(String.format(
282                     "invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s));
283         }
284         return (byte) ((hi << 4) + lo);
285     }
286 
287     /**
288      * Decodes part of a string with <a href="https://en.wikipedia.org/wiki/Hex_dump">hex dump</a>
289      *
290      * @param hexDump a {@link CharSequence} which contains the hex dump
291      * @param fromIndex start of hex dump in {@code hexDump}
292      * @param length hex string length
293      */
294     public static byte[] decodeHexDump(CharSequence hexDump, int fromIndex, int length) {
295         if (length < 0 || (length & 1) != 0) {
296             throw new IllegalArgumentException("length: " + length);
297         }
298         if (length == 0) {
299             return EmptyArrays.EMPTY_BYTES;
300         }
301         byte[] bytes = new byte[length >>> 1];
302         for (int i = 0; i < length; i += 2) {
303             bytes[i >>> 1] = decodeHexByte(hexDump, fromIndex + i);
304         }
305         return bytes;
306     }
307 
308     /**
309      * Decodes a <a href="https://en.wikipedia.org/wiki/Hex_dump">hex dump</a>
310      */
311     public static byte[] decodeHexDump(CharSequence hexDump) {
312         return decodeHexDump(hexDump, 0, hexDump.length());
313     }
314 
315     /**
316      * The shortcut to {@link #simpleClassName(Class) simpleClassName(o.getClass())}.
317      */
318     public static String simpleClassName(Object o) {
319         if (o == null) {
320             return "null_object";
321         } else {
322             return simpleClassName(o.getClass());
323         }
324     }
325 
326     /**
327      * Generates a simplified name from a {@link Class}.  Similar to {@link Class#getSimpleName()}, but it works fine
328      * with anonymous classes.
329      */
330     public static String simpleClassName(Class<?> clazz) {
331         String className = checkNotNull(clazz, "clazz").getName();
332         final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
333         if (lastDotIdx > -1) {
334             return className.substring(lastDotIdx + 1);
335         }
336         return className;
337     }
338 
339     /**
340      * Escapes the specified value, if necessary according to
341      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
342      *
343      * @param value The value which will be escaped according to
344      *              <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
345      * @return {@link CharSequence} the escaped value if necessary, or the value unchanged
346      */
347     public static CharSequence escapeCsv(CharSequence value) {
348         return escapeCsv(value, false);
349     }
350 
351     /**
352      * Escapes the specified value, if necessary according to
353      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
354      *
355      * @param value          The value which will be escaped according to
356      *                       <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
357      * @param trimWhiteSpace The value will first be trimmed of its optional white-space characters,
358      *                       according to <a href="https://tools.ietf.org/html/rfc7230#section-7">RFC-7230</a>
359      * @return {@link CharSequence} the escaped value if necessary, or the value unchanged
360      */
361     public static CharSequence escapeCsv(CharSequence value, boolean trimWhiteSpace) {
362         int length = checkNotNull(value, "value").length();
363         int start;
364         int last;
365         if (trimWhiteSpace) {
366             start = indexOfFirstNonOwsChar(value, length);
367             last = indexOfLastNonOwsChar(value, start, length);
368         } else {
369             start = 0;
370             last = length - 1;
371         }
372         if (start > last) {
373             return EMPTY_STRING;
374         }
375 
376         int firstUnescapedSpecial = -1;
377         boolean quoted = false;
378         if (isDoubleQuote(value.charAt(start))) {
379             quoted = isDoubleQuote(value.charAt(last)) && last > start;
380             if (quoted) {
381                 start++;
382                 last--;
383             } else {
384                 firstUnescapedSpecial = start;
385             }
386         }
387 
388         if (firstUnescapedSpecial < 0) {
389             if (quoted) {
390                 for (int i = start; i <= last; i++) {
391                     if (isDoubleQuote(value.charAt(i))) {
392                         if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
393                             firstUnescapedSpecial = i;
394                             break;
395                         }
396                         i++;
397                     }
398                 }
399             } else {
400                 for (int i = start; i <= last; i++) {
401                     char c = value.charAt(i);
402                     if (c == LINE_FEED || c == CARRIAGE_RETURN || c == COMMA) {
403                         firstUnescapedSpecial = i;
404                         break;
405                     }
406                     if (isDoubleQuote(c)) {
407                         if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
408                             firstUnescapedSpecial = i;
409                             break;
410                         }
411                         i++;
412                     }
413                 }
414             }
415 
416             if (firstUnescapedSpecial < 0) {
417                 // Special characters is not found or all of them already escaped.
418                 // In the most cases returns a same string. New string will be instantiated (via StringBuilder)
419                 // only if it really needed. It's important to prevent GC extra load.
420                 return quoted? value.subSequence(start - 1, last + 2) : value.subSequence(start, last + 1);
421             }
422         }
423 
424         StringBuilder result = new StringBuilder(last - start + 1 + CSV_NUMBER_ESCAPE_CHARACTERS);
425         result.append(DOUBLE_QUOTE).append(value, start, firstUnescapedSpecial);
426         for (int i = firstUnescapedSpecial; i <= last; i++) {
427             char c = value.charAt(i);
428             if (isDoubleQuote(c)) {
429                 result.append(DOUBLE_QUOTE);
430                 if (i < last && isDoubleQuote(value.charAt(i + 1))) {
431                     i++;
432                 }
433             }
434             result.append(c);
435         }
436         return result.append(DOUBLE_QUOTE);
437     }
438 
439     /**
440      * Unescapes the specified escaped CSV field, if necessary according to
441      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
442      *
443      * @param value The escaped CSV field which will be unescaped according to
444      *              <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
445      * @return {@link CharSequence} the unescaped value if necessary, or the value unchanged
446      */
447     public static CharSequence unescapeCsv(CharSequence value) {
448         int length = checkNotNull(value, "value").length();
449         if (length == 0) {
450             return value;
451         }
452         int last = length - 1;
453         boolean quoted = isDoubleQuote(value.charAt(0)) && isDoubleQuote(value.charAt(last)) && length != 1;
454         if (!quoted) {
455             validateCsvFormat(value);
456             return value;
457         }
458         StringBuilder unescaped = InternalThreadLocalMap.get().stringBuilder();
459         for (int i = 1; i < last; i++) {
460             char current = value.charAt(i);
461             if (current == DOUBLE_QUOTE) {
462                 if (isDoubleQuote(value.charAt(i + 1)) && (i + 1) != last) {
463                     // Followed by a double-quote but not the last character
464                     // Just skip the next double-quote
465                     i++;
466                 } else {
467                     // Not followed by a double-quote or the following double-quote is the last character
468                     throw newInvalidEscapedCsvFieldException(value, i);
469                 }
470             }
471             unescaped.append(current);
472         }
473         return unescaped.toString();
474     }
475 
476     /**
477      * Unescapes the specified escaped CSV fields according to
478      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
479      *
480      * @param value A string with multiple CSV escaped fields which will be unescaped according to
481      *              <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
482      * @return {@link List} the list of unescaped fields
483      */
484     public static List<CharSequence> unescapeCsvFields(CharSequence value) {
485         List<CharSequence> unescaped = new ArrayList<CharSequence>(2);
486         StringBuilder current = InternalThreadLocalMap.get().stringBuilder();
487         boolean quoted = false;
488         int last = value.length() - 1;
489         for (int i = 0; i <= last; i++) {
490             char c = value.charAt(i);
491             if (quoted) {
492                 switch (c) {
493                     case DOUBLE_QUOTE:
494                         if (i == last) {
495                             // Add the last field and return
496                             unescaped.add(current.toString());
497                             return unescaped;
498                         }
499                         char next = value.charAt(++i);
500                         if (next == DOUBLE_QUOTE) {
501                             // 2 double-quotes should be unescaped to one
502                             current.append(DOUBLE_QUOTE);
503                             break;
504                         }
505                         if (next == COMMA) {
506                             // This is the end of a field. Let's start to parse the next field.
507                             quoted = false;
508                             unescaped.add(current.toString());
509                             current.setLength(0);
510                             break;
511                         }
512                         // double-quote followed by other character is invalid
513                         throw newInvalidEscapedCsvFieldException(value, i - 1);
514                     default:
515                         current.append(c);
516                 }
517             } else {
518                 switch (c) {
519                     case COMMA:
520                         // Start to parse the next field
521                         unescaped.add(current.toString());
522                         current.setLength(0);
523                         break;
524                     case DOUBLE_QUOTE:
525                         if (current.length() == 0) {
526                             quoted = true;
527                             break;
528                         }
529                         // double-quote appears without being enclosed with double-quotes
530                         // fall through
531                     case LINE_FEED:
532                         // fall through
533                     case CARRIAGE_RETURN:
534                         // special characters appears without being enclosed with double-quotes
535                         throw newInvalidEscapedCsvFieldException(value, i);
536                     default:
537                         current.append(c);
538                 }
539             }
540         }
541         if (quoted) {
542             throw newInvalidEscapedCsvFieldException(value, last);
543         }
544         unescaped.add(current.toString());
545         return unescaped;
546     }
547 
548     /**
549      * Validate if {@code value} is a valid csv field without double-quotes.
550      *
551      * @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes.
552      */
553     private static void validateCsvFormat(CharSequence value) {
554         int length = value.length();
555         for (int i = 0; i < length; i++) {
556             switch (value.charAt(i)) {
557                 case DOUBLE_QUOTE:
558                 case LINE_FEED:
559                 case CARRIAGE_RETURN:
560                 case COMMA:
561                     // If value contains any special character, it should be enclosed with double-quotes
562                     throw newInvalidEscapedCsvFieldException(value, i);
563                 default:
564             }
565         }
566     }
567 
568     private static IllegalArgumentException newInvalidEscapedCsvFieldException(CharSequence value, int index) {
569         return new IllegalArgumentException("invalid escaped CSV field: " + value + " index: " + index);
570     }
571 
572     /**
573      * Get the length of a string, {@code null} input is considered {@code 0} length.
574      */
575     public static int length(String s) {
576         return s == null ? 0 : s.length();
577     }
578 
579     /**
580      * Determine if a string is {@code null} or {@link String#isEmpty()} returns {@code true}.
581      */
582     public static boolean isNullOrEmpty(String s) {
583         return s == null || s.isEmpty();
584     }
585 
586     /**
587      * Find the index of the first non-white space character in {@code s} starting at {@code offset}.
588      *
589      * @param seq    The string to search.
590      * @param offset The offset to start searching at.
591      * @return the index of the first non-white space character or &lt;{@code -1} if none was found.
592      */
593     public static int indexOfNonWhiteSpace(CharSequence seq, int offset) {
594         for (; offset < seq.length(); ++offset) {
595             if (!Character.isWhitespace(seq.charAt(offset))) {
596                 return offset;
597             }
598         }
599         return -1;
600     }
601 
602     /**
603      * Find the index of the first white space character in {@code s} starting at {@code offset}.
604      *
605      * @param seq    The string to search.
606      * @param offset The offset to start searching at.
607      * @return the index of the first white space character or &lt;{@code -1} if none was found.
608      */
609     public static int indexOfWhiteSpace(CharSequence seq, int offset) {
610         for (; offset < seq.length(); ++offset) {
611             if (Character.isWhitespace(seq.charAt(offset))) {
612                 return offset;
613             }
614         }
615         return -1;
616     }
617 
618     /**
619      * Determine if {@code c} lies within the range of values defined for
620      * <a href="https://unicode.org/glossary/#surrogate_code_point">Surrogate Code Point</a>.
621      *
622      * @param c the character to check.
623      * @return {@code true} if {@code c} lies within the range of values defined for
624      * <a href="https://unicode.org/glossary/#surrogate_code_point">Surrogate Code Point</a>. {@code false} otherwise.
625      */
626     public static boolean isSurrogate(char c) {
627         return c >= '\uD800' && c <= '\uDFFF';
628     }
629 
630     private static boolean isDoubleQuote(char c) {
631         return c == DOUBLE_QUOTE;
632     }
633 
634     /**
635      * Determine if the string {@code s} ends with the char {@code c}.
636      *
637      * @param s the string to test
638      * @param c the tested char
639      * @return true if {@code s} ends with the char {@code c}
640      */
641     public static boolean endsWith(CharSequence s, char c) {
642         int len = s.length();
643         return len > 0 && s.charAt(len - 1) == c;
644     }
645 
646     /**
647      * Trim optional white-space characters from the specified value,
648      * according to <a href="https://tools.ietf.org/html/rfc7230#section-7">RFC-7230</a>.
649      *
650      * @param value the value to trim
651      * @return {@link CharSequence} the trimmed value if necessary, or the value unchanged
652      */
653     public static CharSequence trimOws(CharSequence value) {
654         final int length = value.length();
655         if (length == 0) {
656             return value;
657         }
658         int start = indexOfFirstNonOwsChar(value, length);
659         int end = indexOfLastNonOwsChar(value, start, length);
660         return start == 0 && end == length - 1 ? value : value.subSequence(start, end + 1);
661     }
662 
663     /**
664      * Returns a char sequence that contains all {@code elements} joined by a given separator.
665      *
666      * @param separator for each element
667      * @param elements to join together
668      *
669      * @return a char sequence joined by a given separator.
670      */
671     public static CharSequence join(CharSequence separator, Iterable<? extends CharSequence> elements) {
672         ObjectUtil.checkNotNull(separator, "separator");
673         ObjectUtil.checkNotNull(elements, "elements");
674 
675         Iterator<? extends CharSequence> iterator = elements.iterator();
676         if (!iterator.hasNext()) {
677             return EMPTY_STRING;
678         }
679 
680         CharSequence firstElement = iterator.next();
681         if (!iterator.hasNext()) {
682             return firstElement;
683         }
684 
685         StringBuilder builder = new StringBuilder(firstElement);
686         do {
687             builder.append(separator).append(iterator.next());
688         } while (iterator.hasNext());
689 
690         return builder;
691     }
692 
693     /**
694      * @return {@code length} if no OWS is found.
695      */
696     private static int indexOfFirstNonOwsChar(CharSequence value, int length) {
697         int i = 0;
698         while (i < length && isOws(value.charAt(i))) {
699             i++;
700         }
701         return i;
702     }
703 
704     /**
705      * @return {@code start} if no OWS is found.
706      */
707     private static int indexOfLastNonOwsChar(CharSequence value, int start, int length) {
708         int i = length - 1;
709         while (i > start && isOws(value.charAt(i))) {
710             i--;
711         }
712         return i;
713     }
714 
715     private static boolean isOws(char c) {
716         return c == SPACE || c == TAB;
717     }
718 
719 }