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