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