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