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