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     */
019    package org.apache.shiro.util;
020    
021    import java.text.ParseException;
022    import java.util.ArrayList;
023    import java.util.Collection;
024    import java.util.List;
025    import java.util.StringTokenizer;
026    
027    /**
028     * <p>Simple utility class for String operations useful across the framework.
029     *
030     * <p>Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel,
031     * and in these cases, we have retained all license, copyright and author information.
032     *
033     * @author Les Hazlewood
034     * @author Rod Johnson
035     * @author Juergen Hoeller
036     * @author Keith Donald
037     * @author Rob Harrop
038     * @since 0.9
039     */
040    public class StringUtils {
041    
042        //TODO - complete JavaDoc
043    
044        /** Constant representing the empty string, equal to &quot;&quot; */
045        public static final String EMPTY_STRING = "";
046    
047        /** Constant representing the default delimiter character (comma), equal to <code>','</code> */
048        public static final char DEFAULT_DELIMITER_CHAR = ',';
049    
050        /** Constant representing the default quote character (double quote), equal to '&quot;'</code> */
051        public static final char DEFAULT_QUOTE_CHAR = '"';
052    
053        /**
054         * Check whether the given String has actual text.
055         * More specifically, returns <code>true</code> if the string not <code>null</code>,
056         * its length is greater than 0, and it contains at least one non-whitespace character.
057         * <p/>
058         * <code>StringUtils.hasText(null) == false<br/>
059         * StringUtils.hasText("") == false<br/>
060         * StringUtils.hasText(" ") == false<br/>
061         * StringUtils.hasText("12345") == true<br/>
062         * StringUtils.hasText(" 12345 ") == true</code>
063         *
064         * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
065         *
066         * @param str the String to check (may be <code>null</code>)
067         * @return <code>true</code> if the String is not <code>null</code>, its length is
068         *         greater than 0, and it does not contain whitespace only
069         * @see java.lang.Character#isWhitespace
070         */
071        public static boolean hasText(String str) {
072            if (!hasLength(str)) {
073                return false;
074            }
075            int strLen = str.length();
076            for (int i = 0; i < strLen; i++) {
077                if (!Character.isWhitespace(str.charAt(i))) {
078                    return true;
079                }
080            }
081            return false;
082        }
083    
084        /**
085         * Check that the given String is neither <code>null</code> nor of length 0.
086         * Note: Will return <code>true</code> for a String that purely consists of whitespace.
087         * <p/>
088         * <code>StringUtils.hasLength(null) == false<br/>
089         * StringUtils.hasLength("") == false<br/>
090         * StringUtils.hasLength(" ") == true<br/>
091         * StringUtils.hasLength("Hello") == true</code>
092         * <p/>
093         * Copied from the Spring Framework while retaining all license, copyright and author information.
094         *
095         * @param str the String to check (may be <code>null</code>)
096         * @return <code>true</code> if the String is not null and has length
097         * @see #hasText(String)
098         */
099        public static boolean hasLength(String str) {
100            return (str != null && str.length() > 0);
101        }
102    
103    
104        /**
105         * Test if the given String starts with the specified prefix,
106         * ignoring upper/lower case.
107         *
108         * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
109         *
110         * @param str    the String to check
111         * @param prefix the prefix to look for
112         * @return <code>true</code> starts with the specified prefix (ignoring case), <code>false</code> if it does not.
113         * @see java.lang.String#startsWith
114         */
115        public static boolean startsWithIgnoreCase(String str, String prefix) {
116            if (str == null || prefix == null) {
117                return false;
118            }
119            if (str.startsWith(prefix)) {
120                return true;
121            }
122            if (str.length() < prefix.length()) {
123                return false;
124            }
125            String lcStr = str.substring(0, prefix.length()).toLowerCase();
126            String lcPrefix = prefix.toLowerCase();
127            return lcStr.equals(lcPrefix);
128        }
129    
130        /**
131         * Returns a 'cleaned' representation of the specified argument.  'Cleaned' is defined as the following:
132         *
133         * <ol>
134         * <li>If the specified <code>String</code> is <code>null</code>, return <code>null</code></li>
135         * <li>If not <code>null</code>, {@link String#trim() trim()} it.</li>
136         * <li>If the trimmed string is equal to the empty String (i.e. &quot;&quot;), return <code>null</code></li>
137         * <li>If the trimmed string is not the empty string, return the trimmed version</li>.
138         * </ol>
139         *
140         * Therefore this method always ensures that any given string has trimmed text, and if it doesn't, <code>null</code>
141         * is returned.
142         *
143         * @param in the input String to clean.
144         * @return a populated-but-trimmed String or <code>null</code> otherwise
145         */
146        public static String clean(String in) {
147            String out = in;
148    
149            if (in != null) {
150                out = in.trim();
151                if (out.equals(EMPTY_STRING)) {
152                    out = null;
153                }
154            }
155    
156            return out;
157        }
158    
159        /**
160         * Returns the specified array as a comma-delimited (',') string.
161         * @param array the array whose contents will be converted to a string.
162         * @return the array's contents as a comma-delimited (',') string.
163         * @since 1.0
164         */
165        public static String toString(Object[] array) {
166            return toDelimitedString(array,",");
167        }
168    
169        /**
170         * Returns the array's contents as a string, with each element delimited by the specified
171         * {@code delimiter} argument.  Useful for {@code toString()} implementations and log messages.
172         *
173         * @param array the array whose contents will be converted to a string
174         * @param delimiter the delimiter to use between each element
175         * @return a single string, delimited by the specified {@code delimiter}.
176         * @since 1.0
177         */
178        public static String toDelimitedString(Object[] array, String delimiter ) {
179            if ( array == null || array.length == 0 ) {
180                return EMPTY_STRING;
181            }
182            StringBuffer sb = new StringBuffer();
183            for( int i=0; i < array.length; i++ ) {
184                if ( i > 0 ) {
185                    sb.append(delimiter);
186                }
187                sb.append(array[i]);
188            }
189            return sb.toString();
190        }
191    
192        /**
193         * Tokenize the given String into a String array via a StringTokenizer.
194         * Trims tokens and omits empty tokens.
195         * <p>The given delimiters string is supposed to consist of any number of
196         * delimiter characters. Each of those characters can be used to separate
197         * tokens. A delimiter is always a single character; for multi-character
198         * delimiters, consider using <code>delimitedListToStringArray</code>
199         *
200         * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
201         *
202         * @param str        the String to tokenize
203         * @param delimiters the delimiter characters, assembled as String
204         *                   (each of those characters is individually considered as delimiter).
205         * @return an array of the tokens
206         * @see java.util.StringTokenizer
207         * @see java.lang.String#trim()
208         */
209        public static String[] tokenizeToStringArray(String str, String delimiters) {
210            return tokenizeToStringArray(str, delimiters, true, true);
211        }
212    
213        /**
214         * Tokenize the given String into a String array via a StringTokenizer.
215         * <p>The given delimiters string is supposed to consist of any number of
216         * delimiter characters. Each of those characters can be used to separate
217         * tokens. A delimiter is always a single character; for multi-character
218         * delimiters, consider using <code>delimitedListToStringArray</code>
219         *
220         * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
221         *
222         * @param str               the String to tokenize
223         * @param delimiters        the delimiter characters, assembled as String
224         *                          (each of those characters is individually considered as delimiter)
225         * @param trimTokens        trim the tokens via String's <code>trim</code>
226         * @param ignoreEmptyTokens omit empty tokens from the result array
227         *                          (only applies to tokens that are empty after trimming; StringTokenizer
228         *                          will not consider subsequent delimiters as token in the first place).
229         * @return an array of the tokens (<code>null</code> if the input String
230         *         was <code>null</code>)
231         * @see java.util.StringTokenizer
232         * @see java.lang.String#trim()
233         */
234        @SuppressWarnings({"unchecked"})
235        public static String[] tokenizeToStringArray(
236                String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
237    
238            if (str == null) {
239                return null;
240            }
241            StringTokenizer st = new StringTokenizer(str, delimiters);
242            List tokens = new ArrayList();
243            while (st.hasMoreTokens()) {
244                String token = st.nextToken();
245                if (trimTokens) {
246                    token = token.trim();
247                }
248                if (!ignoreEmptyTokens || token.length() > 0) {
249                    tokens.add(token);
250                }
251            }
252            return toStringArray(tokens);
253        }
254    
255        /**
256         * Copy the given Collection into a String array.
257         * The Collection must contain String elements only.
258         *
259         * <p>Copied from the Spring Framework while retaining all license, copyright and author information.
260         *
261         * @param collection the Collection to copy
262         * @return the String array (<code>null</code> if the passed-in
263         *         Collection was <code>null</code>)
264         */
265        @SuppressWarnings({"unchecked"})
266        public static String[] toStringArray(Collection collection) {
267            if (collection == null) {
268                return null;
269            }
270            return (String[]) collection.toArray(new String[collection.size()]);
271        }
272    
273        public static String[] splitKeyValue(String aLine) throws ParseException {
274            String line = clean(aLine);
275            if (line == null) {
276                return null;
277            }
278            String[] split = line.split(" ", 2);
279            if (split.length != 2) {
280                //fallback to checking for an equals sign
281                split = line.split("=", 2);
282                if (split.length != 2) {
283                    String msg = "Unable to determine Key/Value pair from line [" + line + "].  There is no space from " +
284                            "which the split location could be determined.";
285                    throw new ParseException(msg, 0);
286                }
287    
288            }
289    
290            split[0] = clean(split[0]);
291            split[1] = clean(split[1]);
292            if (split[1].startsWith("=")) {
293                //they used spaces followed by an equals followed by zero or more spaces to split the key/value pair, so
294                //remove the equals sign to result in only the key and values in the
295                split[1] = clean(split[1].substring(1));
296            }
297    
298            if (split[0] == null) {
299                String msg = "No valid key could be found in line [" + line + "] to form a key/value pair.";
300                throw new ParseException(msg, 0);
301            }
302            if (split[1] == null) {
303                String msg = "No corresponding value could be found in line [" + line + "] for key [" + split[0] + "]";
304                throw new ParseException(msg, 0);
305            }
306    
307            return split;
308        }
309    
310        public static String[] split(String line) {
311            return split(line, DEFAULT_DELIMITER_CHAR);
312        }
313    
314        public static String[] split(String line, char delimiter) {
315            return split(line, delimiter, DEFAULT_QUOTE_CHAR);
316        }
317    
318        public static String[] split(String line, char delimiter, char quoteChar) {
319            return split(line, delimiter, quoteChar, quoteChar);
320        }
321    
322        public static String[] split(String line, char delimiter, char beginQuoteChar, char endQuoteChar) {
323            return split(line, delimiter, beginQuoteChar, endQuoteChar, false, true);
324        }
325    
326        /**
327         * Splits the specified delimited String into tokens, supporting quoted tokens so that quoted strings themselves
328         * won't be tokenized.
329         *
330         * <p>This method's implementation is very loosely based (with significant modifications) on
331         * <a href="http://blogs.bytecode.com.au/glen">Glen Smith</a>'s open-source
332         * <a href="http://opencsv.svn.sourceforge.net/viewvc/opencsv/trunk/src/au/com/bytecode/opencsv/CSVReader.java?&view=markup">CSVReader.java</a>
333         * file.
334         *
335         * <p>That file is Apache 2.0 licensed as well, making Glen's code a great starting point for us to modify to
336         * our needs.
337         *
338         * @param aLine          the String to parse
339         * @param delimiter      the delimiter by which the <tt>line</tt> argument is to be split
340         * @param beginQuoteChar the character signifying the start of quoted text (so the quoted text will not be split)
341         * @param endQuoteChar   the character signifying the end of quoted text
342         * @param retainQuotes   if the quotes themselves should be retained when constructing the corresponding token
343         * @param trimTokens     if leading and trailing whitespace should be trimmed from discovered tokens.
344         * @return the tokens discovered from parsing the given delimited <tt>line</tt>.
345         */
346        public static String[] split(String aLine, char delimiter, char beginQuoteChar, char endQuoteChar,
347                                     boolean retainQuotes, boolean trimTokens) {
348            String line = clean(aLine);
349            if (line == null) {
350                return null;
351            }
352    
353            List<String> tokens = new ArrayList<String>();
354            StringBuffer sb = new StringBuffer();
355            boolean inQuotes = false;
356    
357            for (int i = 0; i < line.length(); i++) {
358    
359                char c = line.charAt(i);
360                if (c == beginQuoteChar) {
361                    // this gets complex... the quote may end a quoted block, or escape another quote.
362                    // do a 1-char lookahead:
363                    if (inQuotes  // we are in quotes, therefore there can be escaped quotes in here.
364                            && line.length() > (i + 1)  // there is indeed another character to check.
365                            && line.charAt(i + 1) == beginQuoteChar) { // ..and that char. is a quote also.
366                        // we have two quote chars in a row == one quote char, so consume them both and
367                        // put one on the token. we do *not* exit the quoted text.
368                        sb.append(line.charAt(i + 1));
369                        i++;
370                    } else {
371                        inQuotes = !inQuotes;
372                        if (retainQuotes) {
373                            sb.append(c);
374                        }
375                    }
376                } else if (c == endQuoteChar) {
377                    inQuotes = !inQuotes;
378                    if (retainQuotes) {
379                        sb.append(c);
380                    }
381                } else if (c == delimiter && !inQuotes) {
382                    String s = sb.toString();
383                    if (trimTokens) {
384                        s = s.trim();
385                    }
386                    tokens.add(s);
387                    sb = new StringBuffer(); // start work on next token
388                } else {
389                    sb.append(c);
390                }
391            }
392            String s = sb.toString();
393            if (trimTokens) {
394                s = s.trim();
395            }
396            tokens.add(s);
397            return tokens.toArray(new String[tokens.size()]);
398        }
399    
400    }