View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.lang.util;
20  
21  import java.text.ParseException;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Iterator;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Set;
29  import java.util.StringTokenizer;
30  
31  
32  /**
33   * <p>Simple utility class for String operations useful across the framework.
34   * <p/>
35   * <p>Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel,
36   * and in these cases, we have retained all license, copyright and author information.
37   *
38   * @since 0.9
39   */
40  @SuppressWarnings("checkstyle:CyclomaticComplexity")
41  public final class StringUtils {
42  
43      /**
44       * Constant representing the empty string, equal to &quot;&quot;
45       */
46      public static final String EMPTY_STRING = "";
47  
48      /**
49       * Constant representing the default delimiter character (comma), equal to <code>','</code>
50       */
51      public static final char DEFAULT_DELIMITER_CHAR = ',';
52  
53      /**
54       * Constant representing the default quote character (double quote), equal to '&quot;'</code>
55       */
56      public static final char DEFAULT_QUOTE_CHAR = '"';
57  
58      private StringUtils() {
59      }
60  
61      /**
62       * Check whether the given String has actual text.
63       * More specifically, returns <code>true</code> if the string not <code>null</code>,
64       * its length is greater than 0, and it contains at least one non-whitespace character.
65       * <p/>
66       * <code>StringUtils.hasText(null) == false<br/>
67       * StringUtils.hasText("") == false<br/>
68       * StringUtils.hasText(" ") == false<br/>
69       * StringUtils.hasText("12345") == true<br/>
70       * StringUtils.hasText(" 12345 ") == true</code>
71       * <p/>
72       * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
73       *
74       * @param str the String to check (may be <code>null</code>)
75       * @return <code>true</code> if the String is not <code>null</code>, its length is
76       * greater than 0, and it does not contain whitespace only
77       * @see java.lang.Character#isWhitespace
78       */
79      public static boolean hasText(String str) {
80          if (!hasLength(str)) {
81              return false;
82          }
83          int strLen = str.length();
84          for (int i = 0; i < strLen; i++) {
85              if (!Character.isWhitespace(str.charAt(i))) {
86                  return true;
87              }
88          }
89          return false;
90      }
91  
92      /**
93       * Check that the given String is neither <code>null</code> nor of length 0.
94       * Note: Will return <code>true</code> for a String that purely consists of whitespace.
95       * <p/>
96       * <code>StringUtils.hasLength(null) == false<br/>
97       * StringUtils.hasLength("") == false<br/>
98       * StringUtils.hasLength(" ") == true<br/>
99       * StringUtils.hasLength("Hello") == true</code>
100      * <p/>
101      * Copied from the Spring Framework while retaining all license, copyright and author information.
102      *
103      * @param str the String to check (may be <code>null</code>)
104      * @return <code>true</code> if the String is not null and has length
105      * @see #hasText(String)
106      */
107     public static boolean hasLength(String str) {
108         return (str != null && str.length() > 0);
109     }
110 
111 
112     /**
113      * Test if the given String starts with the specified prefix,
114      * ignoring upper/lower case.
115      * <p/>
116      * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
117      *
118      * @param str    the String to check
119      * @param prefix the prefix to look for
120      * @return <code>true</code> starts with the specified prefix (ignoring case), <code>false</code> if it does not.
121      * @see java.lang.String#startsWith
122      */
123     public static boolean startsWithIgnoreCase(String str, String prefix) {
124         if (str == null || prefix == null) {
125             return false;
126         }
127         if (str.startsWith(prefix)) {
128             return true;
129         }
130         if (str.length() < prefix.length()) {
131             return false;
132         }
133         String lcStr = str.substring(0, prefix.length()).toLowerCase();
134         String lcPrefix = prefix.toLowerCase();
135         return lcStr.equals(lcPrefix);
136     }
137 
138     /**
139      * Returns a 'cleaned' representation of the specified argument.  'Cleaned' is defined as the following:
140      * <p/>
141      * <ol>
142      * <li>If the specified <code>String</code> is <code>null</code>, return <code>null</code></li>
143      * <li>If not <code>null</code>, {@link String#trim() trim()} it.</li>
144      * <li>If the trimmed string is equal to the empty String (i.e. &quot;&quot;), return <code>null</code></li>
145      * <li>If the trimmed string is not the empty string, return the trimmed version</li>.
146      * </ol>
147      * <p/>
148      * Therefore this method always ensures that any given string has trimmed text, and if it doesn't, <code>null</code>
149      * is returned.
150      *
151      * @param in the input String to clean.
152      * @return a populated-but-trimmed String or <code>null</code> otherwise
153      */
154     public static String clean(String in) {
155         String out = in;
156 
157         if (in != null) {
158             out = in.trim();
159             if (out.equals(EMPTY_STRING)) {
160                 out = null;
161             }
162         }
163 
164         return out;
165     }
166 
167     /**
168      * Returns the specified array as a comma-delimited (',') string.
169      *
170      * @param array the array whose contents will be converted to a string.
171      * @return the array's contents as a comma-delimited (',') string.
172      * @since 1.0
173      */
174     public static String toString(Object[] array) {
175         return toDelimitedString(array, ",");
176     }
177 
178     /**
179      * Returns the array's contents as a string, with each element delimited by the specified
180      * {@code delimiter} argument.  Useful for {@code toString()} implementations and log messages.
181      *
182      * @param array     the array whose contents will be converted to a string
183      * @param delimiter the delimiter to use between each element
184      * @return a single string, delimited by the specified {@code delimiter}.
185      * @since 1.0
186      */
187     public static String toDelimitedString(Object[] array, String delimiter) {
188         if (array == null || array.length == 0) {
189             return EMPTY_STRING;
190         }
191         StringBuilder sb = new StringBuilder();
192         for (int i = 0; i < array.length; i++) {
193             if (i > 0) {
194                 sb.append(delimiter);
195             }
196             sb.append(array[i]);
197         }
198         return sb.toString();
199     }
200 
201     /**
202      * Returns the collection's contents as a string, with each element delimited by the specified
203      * {@code delimiter} argument.  Useful for {@code toString()} implementations and log messages.
204      *
205      * @param c         the collection whose contents will be converted to a string
206      * @param delimiter the delimiter to use between each element
207      * @return a single string, delimited by the specified {@code delimiter}.
208      * @since 1.2
209      */
210     public static String toDelimitedString(Collection c, String delimiter) {
211         if (c == null || c.isEmpty()) {
212             return EMPTY_STRING;
213         }
214         return join(c.iterator(), delimiter);
215     }
216 
217     /**
218      * Tokenize the given String into a String array via a StringTokenizer.
219      * Trims tokens and omits empty tokens.
220      * <p>The given delimiters string is supposed to consist of any number of
221      * delimiter characters. Each of those characters can be used to separate
222      * tokens. A delimiter is always a single character; for multi-character
223      * delimiters, consider using <code>delimitedListToStringArray</code>
224      * <p/>
225      * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
226      *
227      * @param str        the String to tokenize
228      * @param delimiters the delimiter characters, assembled as String
229      *                   (each of those characters is individually considered as delimiter).
230      * @return an array of the tokens
231      * @see java.util.StringTokenizer
232      * @see java.lang.String#trim()
233      */
234     public static String[] tokenizeToStringArray(String str, String delimiters) {
235         return tokenizeToStringArray(str, delimiters, true, true);
236     }
237 
238     /**
239      * Tokenize the given String into a String array via a StringTokenizer.
240      * <p>The given delimiters string is supposed to consist of any number of
241      * delimiter characters. Each of those characters can be used to separate
242      * tokens. A delimiter is always a single character; for multi-character
243      * delimiters, consider using <code>delimitedListToStringArray</code>
244      * <p/>
245      * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
246      *
247      * @param str               the String to tokenize
248      * @param delimiters        the delimiter characters, assembled as String
249      *                          (each of those characters is individually considered as delimiter)
250      * @param trimTokens        trim the tokens via String's <code>trim</code>
251      * @param ignoreEmptyTokens omit empty tokens from the result array
252      *                          (only applies to tokens that are empty after trimming; StringTokenizer
253      *                          will not consider subsequent delimiters as token in the first place).
254      * @return an array of the tokens (<code>null</code> if the input String
255      * was <code>null</code>)
256      * @see java.util.StringTokenizer
257      * @see java.lang.String#trim()
258      */
259     @SuppressWarnings({"unchecked"})
260     public static String[] tokenizeToStringArray(
261             String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
262 
263         if (str == null) {
264             return null;
265         }
266         StringTokenizer st = new StringTokenizer(str, delimiters);
267         List tokens = new ArrayList();
268         while (st.hasMoreTokens()) {
269             String token = st.nextToken();
270             if (trimTokens) {
271                 token = token.trim();
272             }
273             if (!ignoreEmptyTokens || token.length() > 0) {
274                 tokens.add(token);
275             }
276         }
277         return toStringArray(tokens);
278     }
279 
280     /**
281      * Copy the given Collection into a String array.
282      * The Collection must contain String elements only.
283      * <p/>
284      * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
285      *
286      * @param collection the Collection to copy
287      * @return the String array (<code>null</code> if the passed-in
288      * Collection was <code>null</code>)
289      */
290     @SuppressWarnings({"unchecked"})
291     public static String[] toStringArray(Collection collection) {
292         if (collection == null) {
293             return null;
294         }
295         return (String[]) collection.toArray(new String[collection.size()]);
296     }
297 
298     public static String[] splitKeyValue(String aLine) throws ParseException {
299         String line = clean(aLine);
300         if (line == null) {
301             return null;
302         }
303         String[] split = line.split(" ", 2);
304         if (split.length != 2) {
305             //fallback to checking for an equals sign
306             split = line.split("=", 2);
307             if (split.length != 2) {
308                 String msg = "Unable to determine Key/Value pair from line [" + line + "].  There is no space from "
309                         + "which the split location could be determined.";
310                 throw new ParseException(msg, 0);
311             }
312 
313         }
314 
315         split[0] = clean(split[0]);
316         split[1] = clean(split[1]);
317         if (split[1].startsWith("=")) {
318             //they used spaces followed by an equals followed by zero or more spaces to split the key/value pair, so
319             //remove the equals sign to result in only the key and values in the
320             split[1] = clean(split[1].substring(1));
321         }
322 
323         if (split[0] == null) {
324             String msg = "No valid key could be found in line [" + line + "] to form a key/value pair.";
325             throw new ParseException(msg, 0);
326         }
327         if (split[1] == null) {
328             String msg = "No corresponding value could be found in line [" + line + "] for key [" + split[0] + "]";
329             throw new ParseException(msg, 0);
330         }
331 
332         return split;
333     }
334 
335     /**
336      * Splits a string using the {@link #DEFAULT_DELIMITER_CHAR} (which is {@value #DEFAULT_DELIMITER_CHAR}).
337      * This method also recognizes quoting using the {@link #DEFAULT_QUOTE_CHAR}
338      * (which is {@value #DEFAULT_QUOTE_CHAR}), but does not retain them.
339      *
340      * <p>This is equivalent of calling {@link #split(String, char, char, char, boolean, boolean)} with
341      * {@code line, DEFAULT_DELIMITER_CHAR, DEFAULT_QUOTE_CHAR, DEFAULT_QUOTE_CHAR, false, true}.</p>
342      *
343      * @param line the line to split using the {@link #DEFAULT_DELIMITER_CHAR}.
344      * @return the split line, split tokens do not contain quotes and are trimmed.
345      * @see #split(String, char, char, char, boolean, boolean)
346      */
347     public static String[] split(String line) {
348         return split(line, DEFAULT_DELIMITER_CHAR);
349     }
350 
351     public static String[] split(String line, char delimiter) {
352         return split(line, delimiter, DEFAULT_QUOTE_CHAR);
353     }
354 
355     public static String[] split(String line, char delimiter, char quoteChar) {
356         return split(line, delimiter, quoteChar, quoteChar);
357     }
358 
359     public static String[] split(String line, char delimiter, char beginQuoteChar, char endQuoteChar) {
360         return split(line, delimiter, beginQuoteChar, endQuoteChar, false, true);
361     }
362 
363     /**
364      * Splits the specified delimited String into tokens, supporting quoted tokens so that quoted strings themselves
365      * won't be tokenized.
366      * <p/>
367      * This method's implementation is very loosely based (with significant modifications) on
368      * <a href="http://blogs.bytecode.com.au/glen">Glen Smith</a>'s open-source
369      * <a href="http://opencsv.svn.sourceforge.net/viewvc/opencsv/trunk/src/au/com/bytecode/opencsv/CSVReader.java
370      * ?&view=markup">CSVReader.java</a>
371      * file.
372      * <p/>
373      * That file is Apache 2.0 licensed as well, making Glen's code a great starting point for us to modify to
374      * our needs.
375      *
376      * @param aLine          the String to parse
377      * @param delimiter      the delimiter by which the <tt>line</tt> argument is to be split
378      * @param beginQuoteChar the character signifying the start of quoted text (so the quoted text will not be split)
379      * @param endQuoteChar   the character signifying the end of quoted text
380      * @param retainQuotes   if the quotes themselves should be retained when constructing the corresponding token
381      * @param trimTokens     if leading and trailing whitespace should be trimmed from discovered tokens.
382      * @return the tokens discovered from parsing the given delimited <tt>line</tt>.
383      */
384     public static String[] split(String aLine, char delimiter, char beginQuoteChar, char endQuoteChar,
385                                  boolean retainQuotes, boolean trimTokens) {
386         String line = clean(aLine);
387         if (line == null) {
388             return null;
389         }
390 
391         List<String> tokens = new ArrayList<String>();
392         StringBuilder sb = new StringBuilder();
393         boolean inQuotes = false;
394 
395         for (int i = 0; i < line.length(); i++) {
396 
397             char c = line.charAt(i);
398             if (c == beginQuoteChar) {
399                 // this gets complex... the quote may end a quoted block, or escape another quote.
400                 // do a 1-char lookahead:
401                 // we are in quotes, therefore there can be escaped quotes in here.
402                 if (inQuotes
403                         // there is indeed another character to check.
404                         && line.length() > (i + 1)
405                         // ..and that char. is a quote also.
406                         && line.charAt(i + 1) == beginQuoteChar) {
407                     // we have two quote chars in a row == one quote char, so consume them both and
408                     // put one on the token. we do *not* exit the quoted text.
409                     sb.append(line.charAt(i + 1));
410                     i++;
411                 } else {
412                     inQuotes = !inQuotes;
413                     if (retainQuotes) {
414                         sb.append(c);
415                     }
416                 }
417             } else if (c == endQuoteChar) {
418                 inQuotes = !inQuotes;
419                 if (retainQuotes) {
420                     sb.append(c);
421                 }
422             } else if (c == delimiter && !inQuotes) {
423                 String s = sb.toString();
424                 if (trimTokens) {
425                     s = s.trim();
426                 }
427                 tokens.add(s);
428                 // start work on next token
429                 sb = new StringBuilder();
430             } else {
431                 sb.append(c);
432             }
433         }
434         String s = sb.toString();
435         if (trimTokens) {
436             s = s.trim();
437         }
438         tokens.add(s);
439         return tokens.toArray(new String[tokens.size()]);
440     }
441 
442     /**
443      * Joins the elements of the provided {@code Iterator} into
444      * a single String containing the provided elements.</p>
445      * <p/>
446      * No delimiter is added before or after the list.
447      * A {@code null} separator is the same as an empty String ("").</p>
448      * <p/>
449      * Copied from Commons Lang, version 3 (r1138702).</p>
450      *
451      * @param iterator  the {@code Iterator} of values to join together, may be null
452      * @param separator the separator character to use, null treated as ""
453      * @return the joined String, {@code null} if null iterator input
454      * @since 1.2
455      */
456     public static String join(Iterator<?> iterator, String separator) {
457         final String empty = "";
458 
459         // handle null, zero and one elements before building a buffer
460         if (iterator == null) {
461             return null;
462         }
463         if (!iterator.hasNext()) {
464             return empty;
465         }
466         Object first = iterator.next();
467         if (!iterator.hasNext()) {
468             return first == null ? empty : first.toString();
469         }
470 
471         // two or more elements
472         // Java default is 16, probably too small
473         @SuppressWarnings("checkstyle:MagicNumber")
474         StringBuilder buf = new StringBuilder(256);
475         if (first != null) {
476             buf.append(first);
477         }
478 
479         while (iterator.hasNext()) {
480             if (separator != null) {
481                 buf.append(separator);
482             }
483             Object obj = iterator.next();
484             if (obj != null) {
485                 buf.append(obj);
486             }
487         }
488         return buf.toString();
489     }
490 
491     /**
492      * Splits the {@code delimited} string (delimited by the specified {@code separator} character) and returns the
493      * delimited values as a {@code Set}.
494      * <p/>
495      * If either argument is {@code null}, this method returns {@code null}.
496      *
497      * @param delimited the string to split
498      * @param separator the character that delineates individual tokens to split
499      * @return the delimited values as a {@code Set}.
500      * @since 1.2
501      */
502     public static Set<String> splitToSet(String delimited, String separator) {
503         if (delimited == null || separator == null) {
504             return null;
505         }
506         String[] split = split(delimited, separator.charAt(0));
507         return asSet(split);
508     }
509 
510     /**
511      * Returns the input argument, but ensures the first character is capitalized (if possible).
512      *
513      * @param in the string to uppercase the first character.
514      * @return the input argument, but with the first character capitalized (if possible).
515      * @since 1.2
516      */
517     public static String uppercaseFirstChar(String in) {
518         if (in == null || in.length() == 0) {
519             return in;
520         }
521         int length = in.length();
522         StringBuilder sb = new StringBuilder(length);
523 
524         sb.append(Character.toUpperCase(in.charAt(0)));
525         if (length > 1) {
526             String remaining = in.substring(1);
527             sb.append(remaining);
528         }
529         return sb.toString();
530     }
531 
532     //////////////////////////
533     // From CollectionUtils //
534     //////////////////////////
535     // CollectionUtils cannot be removed from shiro-core until 2.0 as it has a dependency on PrincipalCollection
536 
537 
538     @SafeVarargs
539     private static <E> Set<E> asSet(E... elements) {
540         if (elements == null || elements.length == 0) {
541             return Collections.emptySet();
542         }
543 
544         if (elements.length == 1) {
545             return Collections.singleton(elements[0]);
546         }
547 
548         LinkedHashSet<E> set = new LinkedHashSet<E>(elements.length * 4 / 3 + 1);
549         Collections.addAll(set, elements);
550         return set;
551     }
552 
553 }