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