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 &amp; &gt;, &lt;, and &quot; 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, "&", "&amp;" );
213        src = replaceString( src, "<", "&lt;" );
214        src = replaceString( src, ">", "&gt;" );
215        src = replaceString( src, "\"", "&quot;" );
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}