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.wiki.util; 020 021import java.io.File; 022import java.io.IOException; 023import java.nio.charset.Charset; 024import java.nio.charset.StandardCharsets; 025import java.security.SecureRandom; 026import java.util.Properties; 027import java.util.Random; 028 029import org.apache.commons.lang.StringUtils; 030 031 032/** 033 * Contains a number of static utility methods. 034 */ 035public final class TextUtil { 036 037 static final String HEX_DIGITS = "0123456789ABCDEF"; 038 039 /** 040 * Private constructor prevents instantiation. 041 */ 042 private TextUtil() {} 043 044 /** 045 * java.net.URLEncoder.encode() method in JDK < 1.4 is buggy. This duplicates 046 * its functionality. 047 * @param rs the string to encode 048 * @return the URL-encoded string 049 */ 050 protected static String urlEncode( byte[] rs ) { 051 StringBuilder result = new StringBuilder(rs.length*2); 052 053 // Does the URLEncoding. We could use the java.net one, but 054 // it does not eat byte[]s. 055 056 for( int i = 0; i < rs.length; i++ ) { 057 char c = ( char )rs[i]; 058 059 switch( c ) { 060 case '_': 061 case '.': 062 case '*': 063 case '-': 064 case '/': 065 result.append( c ); 066 break; 067 068 case ' ': 069 result.append( '+' ); 070 break; 071 072 default: 073 if( ( c >= 'a' && c <= 'z' ) || 074 ( c >= 'A' && c <= 'Z' ) || 075 ( c >= '0' && c <= '9' ) ) { 076 result.append( c ); 077 } else { 078 result.append( '%' ); 079 result.append( HEX_DIGITS.charAt( ( c & 0xF0 ) >> 4 ) ); 080 result.append( HEX_DIGITS.charAt( c & 0x0F ) ); 081 } 082 } 083 084 } // for 085 086 return result.toString(); 087 } 088 089 /** 090 * URL encoder does not handle all characters correctly. 091 * See <A HREF="http://developer.java.sun.com/developer/bugParade/bugs/4257115.html"> 092 * Bug parade, bug #4257115</A> for more information. 093 * <P> 094 * Thanks to CJB for this fix. 095 * 096 * @param bytes The byte array containing the bytes of the string 097 * @param encoding The encoding in which the string should be interpreted 098 * @return A decoded String 099 * 100 * @throws IllegalArgumentException If the byte array is not a valid string. 101 */ 102 protected static String urlDecode( byte[] bytes, String encoding ) throws IllegalArgumentException { 103 if( bytes == null ) { 104 return null; 105 } 106 107 byte[] decodeBytes = new byte[bytes.length]; 108 int decodedByteCount = 0; 109 110 try { 111 for( int count = 0; count < bytes.length; count++ ) { 112 switch( bytes[count] ) { 113 case '+': 114 decodeBytes[decodedByteCount++] = ( byte ) ' '; 115 break ; 116 117 case '%': 118 decodeBytes[decodedByteCount++] = ( byte )( ( HEX_DIGITS.indexOf( bytes[++count] ) << 4 ) + 119 ( HEX_DIGITS.indexOf( bytes[++count] ) ) ); 120 break ; 121 122 default: 123 decodeBytes[decodedByteCount++] = bytes[count] ; 124 } 125 } 126 127 } catch( IndexOutOfBoundsException ae ) { 128 throw new IllegalArgumentException( "Malformed UTF-8 string?" ); 129 } 130 131 return new String(decodeBytes, 0, decodedByteCount, Charset.forName( encoding ) ); 132 } 133 134 /** 135 * As java.net.URLEncoder class, but this does it in UTF8 character set. 136 * 137 * @param text The text to decode 138 * @return An URLEncoded string. 139 */ 140 public static String urlEncodeUTF8( String text ) { 141 // If text is null, just return an empty string 142 if ( text == null ) { 143 return ""; 144 } 145 146 byte[] rs = text.getBytes( StandardCharsets.UTF_8 ); 147 return urlEncode( rs ); 148 } 149 150 /** 151 * As java.net.URLDecoder class, but for UTF-8 strings. null is a safe value and returns null. 152 * 153 * @param utf8 The UTF-8 encoded string 154 * @return A plain, normal string. 155 */ 156 public static String urlDecodeUTF8( String utf8 ) { 157 if( utf8 == null ) { 158 return null; 159 } 160 161 return urlDecode( utf8.getBytes( StandardCharsets.ISO_8859_1 ), StandardCharsets.UTF_8.toString() ); 162 } 163 164 /** 165 * Provides encoded version of string depending on encoding. Encoding may be UTF-8 or ISO-8859-1 (default). 166 * 167 * <p>This implementation is the same as in FileSystemProvider.mangleName(). 168 * 169 * @param data A string to encode 170 * @param encoding The encoding in which to encode 171 * @return An URL encoded string. 172 */ 173 public static String urlEncode( String data, String encoding ) { 174 // Presumably, the same caveats apply as in FileSystemProvider. 175 // Don't see why it would be horribly kludgy, though. 176 if( StandardCharsets.UTF_8.toString().equals( encoding ) ) { 177 return urlEncodeUTF8( data ); 178 } 179 180 return urlEncode( data.getBytes( Charset.forName( encoding ) ) ); 181 } 182 183 /** 184 * Provides decoded version of string depending on encoding. Encoding may be UTF-8 or ISO-8859-1 (default). 185 * 186 * <p>This implementation is the same as in FileSystemProvider.unmangleName(). 187 * 188 * @param data The URL-encoded string to decode 189 * @param encoding The encoding to use 190 * @return A decoded string. 191 * @throws IllegalArgumentException If the data cannot be decoded. 192 */ 193 public static String urlDecode( String data, String encoding ) throws IllegalArgumentException { 194 // Presumably, the same caveats apply as in FileSystemProvider. 195 // Don't see why it would be horribly kludgy, though. 196 if( "UTF-8".equals( encoding ) ) { 197 return urlDecodeUTF8( data ); 198 } 199 200 return urlDecode( data.getBytes( Charset.forName( encoding ) ), encoding ); 201 } 202 203 /** 204 * Replaces the relevant entities inside the String. All & >, <, and " are replaced by their 205 * respective names. 206 * 207 * @since 1.6.1 208 * @param src The source string. 209 * @return The encoded string. 210 */ 211 public static String replaceEntities( String src ) { 212 src = replaceString( src, "&", "&" ); 213 src = replaceString( src, "<", "<" ); 214 src = replaceString( src, ">", ">" ); 215 src = replaceString( src, "\"", """ ); 216 217 return src; 218 } 219 220 /** 221 * Replaces a string with an other string. 222 * 223 * @param orig Original string. Null is safe. 224 * @param src The string to find. 225 * @param dest The string to replace <I>src</I> with. 226 * @return A string with the replacement done. 227 */ 228 public static String replaceString( String orig, String src, String dest ) { 229 if ( orig == null ) return null; 230 if ( src == null || dest == null ) throw new NullPointerException(); 231 if ( src.length() == 0 ) return orig; 232 233 StringBuilder res = new StringBuilder( orig.length() + 20 ); // Pure guesswork 234 int start = 0; 235 int end = 0; 236 int last = 0; 237 238 while ( ( start = orig.indexOf( src,end ) ) != -1 ) { 239 res.append( orig.substring( last, start ) ); 240 res.append( dest ); 241 end = start + src.length(); 242 last = start + src.length(); 243 } 244 245 res.append( orig.substring( end ) ); 246 247 return res.toString(); 248 } 249 250 /** 251 * Replaces a part of a string with a new String. 252 * 253 * @param start Where in the original string the replacing should start. 254 * @param end Where the replacing should end. 255 * @param orig Original string. Null is safe. 256 * @param text The new text to insert into the string. 257 * @return The string with the orig replaced with text. 258 */ 259 public static String replaceString( String orig, int start, int end, String text ) { 260 if( orig == null ) return null; 261 262 StringBuilder buf = new StringBuilder(orig); 263 buf.replace( start, end, text ); 264 return buf.toString(); 265 } 266 267 /** 268 * Replaces a string with an other string. Case insensitive matching is used 269 * 270 * @param orig Original string. Null is safe. 271 * @param src The string to find. 272 * @param dest The string to replace <I>src</I> with. 273 * @return A string with all instances of src replaced with dest. 274 */ 275 public static String replaceStringCaseUnsensitive( String orig, String src, String dest ) { 276 if( orig == null ) return null; 277 278 StringBuilder res = new StringBuilder(); 279 int start = 0; 280 int end = 0; 281 int last = 0; 282 283 String origCaseUnsn = orig.toLowerCase(); 284 String srcCaseUnsn = src.toLowerCase(); 285 286 while( ( start = origCaseUnsn.indexOf( srcCaseUnsn, end ) ) != -1 ) { 287 res.append( orig.substring( last, start ) ); 288 res.append( dest ); 289 end = start + src.length(); 290 last = start + src.length(); 291 } 292 293 res.append( orig.substring( end ) ); 294 295 return res.toString(); 296 } 297 298 /** 299 * Parses an integer parameter, returning a default value if the value is null or a non-number. 300 * 301 * @param value The value to parse 302 * @param defvalue A default value in case the value is not a number 303 * @return The parsed value (or defvalue). 304 */ 305 public static int parseIntParameter( String value, int defvalue ) { 306 int val = defvalue; 307 308 try { 309 val = Integer.parseInt( value.trim() ); 310 } catch( Exception e ) {} 311 312 return val; 313 } 314 315 /** 316 * Gets an integer-valued property from a standard Properties list. 317 * 318 * Before inspecting the props, we first check if there is a Java System Property with the same name, if it exists 319 * we use that value, if not we check an environment variable with that (almost) same name, almost meaning we replace 320 * dots with underscores. 321 * 322 * If the value does not exist, or is a 323 * non-integer, returns defVal. 324 * 325 * @since 2.1.48. 326 * @param props The property set to look through 327 * @param key The key to look for 328 * @param defVal If the property is not found or is a non-integer, returns this value. 329 * @return The property value as an integer (or defVal). 330 */ 331 public static int getIntegerProperty( Properties props, String key, int defVal ) { 332 String val = System.getProperties().getProperty(key, System.getenv(StringUtils.replace(key,".","_"))); 333 if (val == null) { 334 val = props.getProperty(key); 335 } 336 return parseIntParameter( val, defVal ); 337 } 338 339 /** 340 * Gets a boolean property from a standard Properties list. Returns the default value, in case the key has not 341 * been set. 342 * Before inspecting the props, we first check if there is a Java System Property with the same name, if it exists 343 * we use that value, if not we check an environment variable with that (almost) same name, almost meaning we replace 344 * dots with underscores. 345 * <P> 346 * The possible values for the property are "true"/"false", "yes"/"no", or "on"/"off". Any value not 347 * recognized is always defined as "false". 348 * 349 * @param props A list of properties to search. 350 * @param key The property key. 351 * @param defval The default value to return. 352 * 353 * @return True, if the property "key" was set to "true", "on", or "yes". 354 * 355 * @since 2.0.11 356 */ 357 public static boolean getBooleanProperty( Properties props, String key, boolean defval ) { 358 String val = System.getProperties().getProperty(key, System.getenv(StringUtils.replace(key,".","_"))); 359 if (val == null) { 360 val = props.getProperty(key); 361 } 362 if( val == null ) { 363 return defval; 364 } 365 366 return isPositive( val ); 367 } 368 369 /** 370 * Fetches a String property from the set of Properties. This differs from Properties.getProperty() in a 371 * couple of key respects: First, property value is trim()med (so no extra whitespace back and front). 372 * 373 * Before inspecting the props, we first check if there is a Java System Property with the same name, if it exists 374 * we use that value, if not we check an environment variable with that (almost) same name, almost meaning we replace 375 * dots with underscores. 376 * 377 * @param props The Properties to search through 378 * @param key The property key 379 * @param defval A default value to return, if the property does not exist. 380 * @return The property value. 381 * @since 2.1.151 382 */ 383 public static String getStringProperty(Properties props, String key, String defval) { 384 String val = System.getProperties().getProperty(key, System.getenv(StringUtils.replace(key,".","_"))); 385 if (val == null) { 386 val = props.getProperty(key); 387 } 388 if (val == null) { 389 return defval; 390 } 391 return val.trim(); 392 } 393 394 /** 395 * Fetches a file path property from the set of Properties. 396 * 397 * Before inspecting the props, we first check if there is a Java System Property with the same name, if it exists 398 * we use that value, if not we check an environment variable with that (almost) same name, almost meaning we replace 399 * dots with underscores. 400 * 401 * If the implementation fails to create the canonical path it just returns 402 * the original value of the property which is a bit doggy. 403 * 404 * @param props The Properties to search through 405 * @param key The property key 406 * @param defval A default value to return, if the property does not exist. 407 * @return the canonical path of the file or directory being referenced 408 * @since 2.10.1 409 */ 410 public static String getCanonicalFilePathProperty(Properties props, String key, String defval) { 411 412 String result; 413 String val = System.getProperties().getProperty(key, System.getenv(StringUtils.replace(key,".","_"))); 414 if (val == null) { 415 val = props.getProperty(key); 416 } 417 418 if( val == null ) { 419 val = defval; 420 } 421 422 try { 423 result = new File(new File(val.trim()).getCanonicalPath()).getAbsolutePath(); 424 } 425 catch(IOException e) { 426 result = val.trim(); 427 } 428 return result; 429 } 430 431 /** 432 * Returns true, if the string "val" denotes a positive string. Allowed values are "yes", "on", and "true". 433 * Comparison is case-insignificant. Null values are safe. 434 * 435 * @param val Value to check. 436 * @return True, if val is "true", "on", or "yes"; otherwise false. 437 * 438 * @since 2.0.26 439 */ 440 public static boolean isPositive( String val ) { 441 if( val == null ) { 442 return false; 443 } 444 val = val.trim(); 445 return val.equalsIgnoreCase( "true" ) || val.equalsIgnoreCase( "on" ) || val.equalsIgnoreCase( "yes" ); 446 } 447 448 /** 449 * Makes sure that the POSTed data is conforms to certain rules. These rules are: 450 * <UL> 451 * <LI>The data always ends with a newline (some browsers, such as NS4.x series, does not send a newline at 452 * the end, which makes the diffs a bit strange sometimes. 453 * <LI>The CR/LF/CRLF mess is normalized to plain CRLF. 454 * </UL> 455 * 456 * The reason why we're using CRLF is that most browser already return CRLF since that is the closest thing to 457 * a HTTP standard. 458 * 459 * @param postData The data to normalize 460 * @return Normalized data 461 */ 462 public static String normalizePostData( String postData ) { 463 StringBuilder sb = new StringBuilder(); 464 465 for( int i = 0; i < postData.length(); i++ ) { 466 switch( postData.charAt(i) ) { 467 case 0x0a: // LF, UNIX 468 sb.append( "\r\n" ); 469 break; 470 471 case 0x0d: // CR, either Mac or MSDOS 472 sb.append( "\r\n" ); 473 // If it's MSDOS, skip the LF so that we don't add it again. 474 if( i < postData.length() - 1 && postData.charAt( i + 1 ) == 0x0a ) { 475 i++; 476 } 477 break; 478 479 default: 480 sb.append( postData.charAt(i) ); 481 break; 482 } 483 } 484 485 if( sb.length() < 2 || !sb.substring( sb.length()-2 ).equals( "\r\n" ) ) { 486 sb.append( "\r\n" ); 487 } 488 489 return sb.toString(); 490 } 491 492 private static final int EOI = 0; 493 private static final int LOWER = 1; 494 private static final int UPPER = 2; 495 private static final int DIGIT = 3; 496 private static final int OTHER = 4; 497 private static final Random RANDOM = new SecureRandom(); 498 499 private static int getCharKind( int c ) { 500 if( c == -1 ) { 501 return EOI; 502 } 503 504 char ch = ( char )c; 505 506 if( Character.isLowerCase( ch ) ) { 507 return LOWER; 508 } else if( Character.isUpperCase( ch ) ) { 509 return UPPER; 510 } else if( Character.isDigit( ch ) ) { 511 return DIGIT; 512 } else { 513 return OTHER; 514 } 515 } 516 517 /** 518 * Adds spaces in suitable locations of the input string. This is used to transform a WikiName into a more 519 * readable format. 520 * 521 * @param s String to be beautified. 522 * @return A beautified string. 523 */ 524 public static String beautifyString( String s ) { 525 return beautifyString( s, " " ); 526 } 527 528 /** 529 * Adds spaces in suitable locations of the input string. This is used to transform a WikiName into a more 530 * readable format. 531 * 532 * @param s String to be beautified. 533 * @param space Use this string for the space character. 534 * @return A beautified string. 535 * @since 2.1.127 536 */ 537 public static String beautifyString( String s, String space ) { 538 if( s == null || s.length() == 0 ) { 539 return ""; 540 } 541 542 StringBuilder result = new StringBuilder(); 543 544 int cur = s.charAt( 0 ); 545 int curKind = getCharKind( cur ); 546 547 int prevKind = LOWER; 548 int nextKind = -1; 549 550 int next = -1; 551 int nextPos = 1; 552 553 while( curKind != EOI ) { 554 next = ( nextPos < s.length() ) ? s.charAt( nextPos++ ) : -1; 555 nextKind = getCharKind( next ); 556 557 if( ( prevKind == UPPER ) && ( curKind == UPPER ) && ( nextKind == LOWER ) ) { 558 result.append( space ); 559 result.append( ( char ) cur ); 560 } else { 561 result.append((char) cur ); 562 if( ( ( curKind == UPPER ) && (nextKind == DIGIT) ) 563 || ( ( curKind == LOWER ) && ( ( nextKind == DIGIT ) || ( nextKind == UPPER ) ) ) 564 || ( ( curKind == DIGIT ) && ( ( nextKind == UPPER ) || ( nextKind == LOWER ) ) ) ) { 565 result.append( space ); 566 } 567 } 568 prevKind = curKind; 569 cur = next; 570 curKind = nextKind; 571 } 572 573 return result.toString(); 574 } 575 576 /** 577 * Creates a Properties object based on an array which contains alternatively a key and a value. It is useful 578 * for generating default mappings. For example: 579 * <pre> 580 * String[] properties = { "jspwiki.property1", "value1", 581 * "jspwiki.property2", "value2 }; 582 * 583 * Properties props = TextUtil.createPropertes( values ); 584 * 585 * System.out.println( props.getProperty("jspwiki.property1") ); 586 * </pre> 587 * would output "value1". 588 * 589 * @param values Alternating key and value pairs. 590 * @return Property object 591 * @see java.util.Properties 592 * @throws IllegalArgumentException if the property array is missing a value for a key. 593 * @since 2.2. 594 */ 595 public static Properties createProperties( String[] values ) throws IllegalArgumentException { 596 if( values.length % 2 != 0 ) { 597 throw new IllegalArgumentException( "One value is missing."); 598 } 599 600 Properties props = new Properties(); 601 for( int i = 0; i < values.length; i += 2 ) { 602 props.setProperty( values[i], values[i + 1] ); 603 } 604 605 return props; 606 } 607 608 /** 609 * Counts the number of sections (separated with "----") from the page. 610 * 611 * @param pagedata The WikiText to parse. 612 * @return int Number of counted sections. 613 * @since 2.1.86. 614 */ 615 public static int countSections( String pagedata ) { 616 int tags = 0; 617 int start = 0; 618 619 while( ( start = pagedata.indexOf( "----", start ) ) != -1 ) { 620 tags++; 621 start += 4; // Skip this "----" 622 } 623 624 // 625 // The first section does not get the "----" 626 // 627 return pagedata.length() > 0 ? tags + 1 : 0; 628 } 629 630 /** 631 * Gets the given section (separated with "----") from the page text. 632 * Note that the first section is always #1. If a page has no section markers, 633 * then there is only a single section, #1. 634 * 635 * @param pagedata WikiText to parse. 636 * @param section Which section to get. 637 * @return String The section. 638 * @throws IllegalArgumentException If the page does not contain this many sections. 639 * @since 2.1.86. 640 */ 641 public static String getSection( String pagedata, int section ) throws IllegalArgumentException { 642 int tags = 0; 643 int start = 0; 644 int previous = 0; 645 646 while( ( start = pagedata.indexOf( "----", start ) ) != -1 ) { 647 if( ++tags == section ) { 648 return pagedata.substring( previous, start ); 649 } 650 651 start += 4; // Skip this "----" 652 // allow additional dashes, treat it as if it was a correct 4-dash 653 while (start < pagedata.length() && pagedata.charAt( start ) == '-') { 654 start++; 655 } 656 657 previous = start; 658 } 659 660 if( ++tags == section ) { 661 return pagedata.substring( previous ); 662 } 663 664 throw new IllegalArgumentException( "There is no section no. " + section + " on the page." ); 665 } 666 667 /** 668 * A simple routine which just repeates the arguments. This is useful for creating something like a line or 669 * something. 670 * 671 * @param what String to repeat 672 * @param times How many times to repeat the string. 673 * @return Guess what? 674 * @since 2.1.98. 675 */ 676 public static String repeatString( String what, int times ) { 677 StringBuilder sb = new StringBuilder(); 678 for( int i = 0; i < times; i++ ) { 679 sb.append( what ); 680 } 681 682 return sb.toString(); 683 } 684 685 /** 686 * Converts a string from the Unicode representation into something that can be embedded in a java 687 * properties file. All references outside the ASCII range are replaced with \\uXXXX. 688 * 689 * @param s The string to convert 690 * @return the ASCII string 691 */ 692 public static String native2Ascii( String s ) { 693 StringBuilder sb = new StringBuilder(); 694 for( int i = 0; i < s.length(); i++ ) { 695 char aChar = s.charAt(i); 696 if( ( aChar < 0x0020 ) || ( aChar > 0x007e ) ) { 697 sb.append( '\\'); 698 sb.append( 'u'); 699 sb.append( toHex( ( aChar >> 12 ) & 0xF ) ); 700 sb.append( toHex( ( aChar >> 8 ) & 0xF ) ); 701 sb.append( toHex( ( aChar >> 4 ) & 0xF ) ); 702 sb.append( toHex( aChar & 0xF ) ); 703 } else { 704 sb.append( aChar ); 705 } 706 } 707 return sb.toString(); 708 } 709 710 private static char toHex( int nibble ) { 711 final char[] hexDigit = { 712 '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' 713 }; 714 return hexDigit[nibble & 0xF]; 715 } 716 717 /** 718 * Generates a hexadecimal string from an array of bytes. For example, if the array contains 719 * { 0x01, 0x02, 0x3E }, the resulting string will be "01023E". 720 * 721 * @param bytes A Byte array 722 * @return A String representation 723 * @since 2.3.87 724 */ 725 public static String toHexString( byte[] bytes ) { 726 StringBuilder sb = new StringBuilder( bytes.length * 2 ); 727 for( int i = 0; i < bytes.length; i++ ) { 728 sb.append( toHex( bytes[i] >> 4 ) ); 729 sb.append( toHex( bytes[i] ) ); 730 } 731 732 return sb.toString(); 733 } 734 735 /** 736 * Returns true, if the argument contains a number, otherwise false. In a quick test this is roughly the same 737 * speed as Integer.parseInt() if the argument is a number, and roughly ten times the speed, if the argument 738 * is NOT a number. 739 * 740 * @since 2.4 741 * @param s String to check 742 * @return True, if s represents a number. False otherwise. 743 */ 744 public static boolean isNumber( String s ) { 745 if( s == null ) { 746 return false; 747 } 748 749 if( s.length() > 1 && s.charAt(0) == '-' ) { 750 s = s.substring(1); 751 } 752 753 for( int i = 0; i < s.length(); i++ ) { 754 if( !Character.isDigit( s.charAt( i ) ) ) { 755 return false; 756 } 757 } 758 759 return true; 760 } 761 762 /** Length of password. @see #generateRandomPassword() */ 763 public static final int PASSWORD_LENGTH = 8; 764 765 /** 766 * Generate a random String suitable for use as a temporary password. 767 * 768 * @return String suitable for use as a temporary password 769 * @since 2.4 770 */ 771 public static String generateRandomPassword() { 772 // Pick from some letters that won't be easily mistaken for each 773 // other. So, for example, omit o O and 0, 1 l and L. 774 String letters = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789+@"; 775 776 String pw = ""; 777 for( int i = 0; i < PASSWORD_LENGTH; i++ ) { 778 int index = ( int )( RANDOM.nextDouble() * letters.length() ); 779 pw += letters.substring( index, index + 1 ); 780 } 781 return pw; 782 } 783 784}