001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.lang.util;
020
021import java.text.ParseException;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.Iterator;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.StringTokenizer;
030
031
032/**
033 * <p>Simple utility class for String operations useful across the framework.
034 * <p/>
035 * <p>Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel,
036 * and in these cases, we have retained all license, copyright and author information.
037 *
038 * @since 0.9
039 */
040@SuppressWarnings("checkstyle:CyclomaticComplexity")
041public final class StringUtils {
042
043    /**
044     * Constant representing the empty string, equal to &quot;&quot;
045     */
046    public static final String EMPTY_STRING = "";
047
048    /**
049     * Constant representing the default delimiter character (comma), equal to <code>','</code>
050     */
051    public static final char DEFAULT_DELIMITER_CHAR = ',';
052
053    /**
054     * Constant representing the default quote character (double quote), equal to '&quot;'</code>
055     */
056    public static final char DEFAULT_QUOTE_CHAR = '"';
057
058    private StringUtils() {
059    }
060
061    /**
062     * Check whether the given String has actual text.
063     * More specifically, returns <code>true</code> if the string not <code>null</code>,
064     * its length is greater than 0, and it contains at least one non-whitespace character.
065     * <p/>
066     * <code>StringUtils.hasText(null) == false<br/>
067     * StringUtils.hasText("") == false<br/>
068     * StringUtils.hasText(" ") == false<br/>
069     * StringUtils.hasText("12345") == true<br/>
070     * StringUtils.hasText(" 12345 ") == true</code>
071     * <p/>
072     * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
073     *
074     * @param str the String to check (may be <code>null</code>)
075     * @return <code>true</code> if the String is not <code>null</code>, its length is
076     * greater than 0, and it does not contain whitespace only
077     * @see java.lang.Character#isWhitespace
078     */
079    public static boolean hasText(String str) {
080        if (!hasLength(str)) {
081            return false;
082        }
083        int strLen = str.length();
084        for (int i = 0; i < strLen; i++) {
085            if (!Character.isWhitespace(str.charAt(i))) {
086                return true;
087            }
088        }
089        return false;
090    }
091
092    /**
093     * Check that the given String is neither <code>null</code> nor of length 0.
094     * Note: Will return <code>true</code> for a String that purely consists of whitespace.
095     * <p/>
096     * <code>StringUtils.hasLength(null) == false<br/>
097     * StringUtils.hasLength("") == false<br/>
098     * StringUtils.hasLength(" ") == true<br/>
099     * 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}