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 "" */
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 '"'</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. ""), 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 }