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