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;
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
322        return defvalue;
323    }
324
325    /**
326     *  Gets an integer-valued property from a standard Properties list.
327     *
328     *  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,
329     *  if not we check an environment variable with that (almost) same name, almost meaning we replace dots with underscores.
330     *
331     *  If the value does not exist, or is a non-integer, returns defVal.
332     *
333     *  @since 2.1.48.
334     *  @param props The property set to look through
335     *  @param key   The key to look for
336     *  @param defVal If the property is not found or is a non-integer, returns this value.
337     *  @return The property value as an integer (or defVal).
338     */
339    public static int getIntegerProperty( final Properties props, final String key, final int defVal ) {
340        String val = System.getProperties().getProperty( key, System.getenv( StringUtils.replace( key,".","_" ) ) );
341        if( val == null ) {
342            val = props.getProperty( key );
343        }
344        return parseIntParameter( val, defVal );
345    }
346
347    /**
348     *  Gets a boolean property from a standard Properties list. Returns the default value, in case the key has not been set.
349     *  Before inspecting the props, we first check if there is a Java System Property with the same name, if it exists
350     *  we use that value, if not we check an environment variable with that (almost) same name, almost meaning we replace
351     *  dots with underscores.
352     *  <P>
353     *  The possible values for the property are "true"/"false", "yes"/"no", or "on"/"off".  Any value not recognized is always defined
354     *  as "false".
355     *
356     *  @param props   A list of properties to search.
357     *  @param key     The property key.
358     *  @param defval  The default value to return.
359     *
360     *  @return True, if the property "key" was set to "true", "on", or "yes".
361     *
362     *  @since 2.0.11
363     */
364    public static boolean getBooleanProperty( final Properties props, final String key, final boolean defval ) {
365        String val = System.getProperties().getProperty( key, System.getenv( StringUtils.replace( key,".","_" ) ) );
366        if( val == null ) {
367            val = props.getProperty( key );
368        }
369        if( val == null ) {
370            return defval;
371        }
372
373        return isPositive( val );
374    }
375
376    /**
377     * Fetches a String property from the set of Properties.  This differs from Properties.getProperty() in a
378     * couple of key respects: First, property value is trim()med (so no extra whitespace back and front).
379     *
380     * Before inspecting the props, we first check if there is a Java System Property with the same name, if it exists
381     * we use that value, if not we check an environment variable with that (almost) same name, almost meaning we replace
382     * dots with underscores.
383     *
384     * @param props The Properties to search through
385     * @param key   The property key
386     * @param defval A default value to return, if the property does not exist.
387     * @return The property value.
388     * @since 2.1.151
389     */
390    public static String getStringProperty( final Properties props, final String key, final String 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        return val.trim();
399    }
400
401    /**
402     * {@link #getStringProperty(Properties, String, String)} overload that handles deprecated keys, so that a key and its
403     * deprecated counterpart can coexist in a given version of JSPWiki.
404     *
405     * @param props The Properties to search through
406     * @param key The property key
407     * @param deprecatedKey the property key being superseeded by key
408     * @param defval A default value to return, if the property does not exist.
409     * @return The property value.
410     */
411    public static String getStringProperty( final Properties props, final String key, final String deprecatedKey, final String defval ) {
412        final String val = getStringProperty( props, deprecatedKey, null );
413        if( val != null ) {
414            LOG.warn( "{} is being deprecated and will be removed on a future version, please consider using {} instead " +
415                      "in your jspwiki[-custom].properties file", deprecatedKey, key );
416            return val;
417        }
418        return getStringProperty( props, key, defval );
419    }
420
421    /**
422     *  Throws an exception if a property is not found.
423     *
424     *  @param props A set of properties to search the key in.
425     *  @param key The key to look for.
426     *  @return The required property
427     *
428     *  @throws NoSuchElementException If the search key is not in the property set.
429     *  @since 2.0.26 (on TextUtils, moved To WikiEngine on 2.11.0-M1 and back to TextUtils on 2.11.0-M6)
430     */
431    public static String getRequiredProperty( final Properties props, final String key ) throws NoSuchElementException {
432        final String value = getStringProperty( props, key, null );
433        if( value == null ) {
434            throw new NoSuchElementException( "Required property not found: " + key );
435        }
436        return value;
437    }
438
439    /**
440     * {@link #getRequiredProperty(Properties, String)} overload that handles deprecated keys, so that a key and its
441     * deprecated counterpart can coexist in a given version of JSPWiki.
442     *
443     * @param props The Properties to search through
444     * @param key The property key
445     * @param deprecatedKey the property key being superseeded by key
446     * @return The property value.
447     */
448    public static String getRequiredProperty( final Properties props, final String key, final String deprecatedKey ) throws NoSuchElementException {
449        final String value = getStringProperty( props, deprecatedKey, null );
450        if( value == null ) {
451            return getRequiredProperty( props, key );
452        }
453        LOG.warn( "{} is being deprecated and will be removed on a future version, please consider using {} instead " +
454                  "in your jspwiki[-custom].properties file", deprecatedKey, key );
455        return value;
456    }
457
458    /**
459     *  Fetches a file path property from the set of Properties.
460     *
461     *  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,
462     *  if not we check an environment variable with that (almost) same name, almost meaning we replace dots with underscores.
463     *
464     *  If the implementation fails to create the canonical path it just returns the original value of the property which is a bit doggy.
465     *
466     *  @param props The Properties to search through
467     *  @param key   The property key
468     *  @param defval A default value to return, if the property does not exist.
469     *  @return the canonical path of the file or directory being referenced
470     *  @since 2.10.1
471     */
472    public static String getCanonicalFilePathProperty( final Properties props, final String key, final String defval ) {
473        String val = System.getProperties().getProperty( key, System.getenv( StringUtils.replace( key,".","_" ) ) );
474        if( val == null ) {
475            val = props.getProperty( key );
476        }
477
478        if( val == null ) {
479            val = defval;
480        }
481
482        String result;
483        try {
484            result = new File( new File( val.trim() ).getCanonicalPath() ).getAbsolutePath();
485        } catch( final IOException e ) {
486            result = val.trim();
487        }
488        return result;
489    }
490
491    /**
492     *  Returns true, if the string "val" denotes a positive string.  Allowed values are "yes", "on", and "true".
493     *  Comparison is case-insignificant. Null values are safe.
494     *
495     *  @param val Value to check.
496     *  @return True, if val is "true", "on", or "yes"; otherwise false.
497     *
498     *  @since 2.0.26
499     */
500    public static boolean isPositive( String val ) {
501        if( val == null ) {
502            return false;
503        }
504        val = val.trim();
505        return val.equalsIgnoreCase( "true" )
506               || val.equalsIgnoreCase( "on" )
507               || val.equalsIgnoreCase( "yes" );
508    }
509
510    /**
511     *  Makes sure that the POSTed data is conforms to certain rules.  These rules are:
512     *  <UL>
513     *  <LI>The data always ends with a newline (some browsers, such as NS4.x series, does not send a newline at
514     *      the end, which makes the diffs a bit strange sometimes.
515     *  <LI>The CR/LF/CRLF mess is normalized to plain CRLF.
516     *  </UL>
517     *
518     *  The reason why we're using CRLF is that most browser already return CRLF since that is the closest thing to an HTTP standard.
519     *
520     *  @param postData The data to normalize
521     *  @return Normalized data
522     */
523    public static String normalizePostData( final String postData ) {
524        final StringBuilder sb = new StringBuilder();
525        for( int i = 0; i < postData.length(); i++ ) {
526            switch( postData.charAt(i) ) {
527              case 0x0a: // LF, UNIX
528                sb.append( "\r\n" );
529                break;
530
531              case 0x0d: // CR, either Mac or MSDOS
532                sb.append( "\r\n" );
533                // If it's MSDOS, skip the LF so that we don't add it again.
534                if( i < postData.length() - 1 && postData.charAt( i + 1 ) == 0x0a ) {
535                    i++;
536                }
537                break;
538
539              default:
540                sb.append( postData.charAt( i ) );
541                break;
542            }
543        }
544
545        if( sb.length() < 2 || !sb.substring( sb.length()-2 ).equals( "\r\n" ) ) {
546            sb.append( "\r\n" );
547        }
548
549        return sb.toString();
550    }
551
552    private static final int EOI   = 0;
553    private static final int LOWER = 1;
554    private static final int UPPER = 2;
555    private static final int DIGIT = 3;
556    private static final int OTHER = 4;
557    private static final Random RANDOM = new SecureRandom();
558
559    private static int getCharKind( final int c ) {
560        if( c == -1 ) {
561            return EOI;
562        }
563
564        final char ch = ( char )c;
565
566        if( Character.isLowerCase( ch ) ) {
567            return LOWER;
568        } else if( Character.isUpperCase( ch ) ) {
569            return UPPER;
570        } else if( Character.isDigit( ch ) ) {
571            return DIGIT;
572        } else {
573            return OTHER;
574        }
575    }
576
577    /**
578     *  Adds spaces in suitable locations of the input string.  This is used to transform a WikiName into a more readable format.
579     *
580     *  @param s String to be beautified.
581     *  @return A beautified string.
582     */
583    public static String beautifyString( final String s ) {
584        return beautifyString( s, " " );
585    }
586
587    /**
588     *  Adds spaces in suitable locations of the input string.  This is used to transform a WikiName into a more readable format.
589     *
590     *  @param s String to be beautified.
591     *  @param space Use this string for the space character.
592     *  @return A beautified string.
593     *  @since 2.1.127
594     */
595    public static String beautifyString( final String s, final String space ) {
596        if( s == null || s.isEmpty() ) {
597            return "";
598        }
599
600        final StringBuilder result = new StringBuilder();
601
602        int cur = s.charAt( 0 );
603        int curKind = getCharKind( cur );
604
605        int prevKind = LOWER;
606        int nextKind;
607        int next;
608        int nextPos = 1;
609
610        while( curKind != EOI ) {
611            next = ( nextPos < s.length() ) ? s.charAt( nextPos++ ) : -1;
612            nextKind = getCharKind( next );
613
614            if( ( prevKind == UPPER ) && ( curKind == UPPER ) && ( nextKind == LOWER ) ) {
615                result.append( space );
616                result.append( ( char ) cur );
617            } else {
618                result.append((char) cur );
619                if( ( ( curKind == UPPER ) && (nextKind == DIGIT) )
620                    || ( ( curKind == LOWER ) && ( ( nextKind == DIGIT ) || ( nextKind == UPPER ) ) )
621                    || ( ( curKind == DIGIT ) && ( ( nextKind == UPPER ) || ( nextKind == LOWER ) ) ) ) {
622                    result.append( space );
623                }
624            }
625            prevKind = curKind;
626            cur      = next;
627            curKind  = nextKind;
628        }
629
630        return result.toString();
631    }
632
633    /**
634     *  Cleans a Wiki name based on a list of characters.  Also, any multiple whitespace is collapsed into a single space, and any
635     *  leading or trailing space is removed.
636     *
637     *  @param text text to be cleared. Null is safe, and causes this to return null.
638     *  @param allowedChars Characters which are allowed in the string.
639     *  @return A cleaned text.
640     *
641     *  @since 2.6
642     */
643    public static String cleanString( String text, final String allowedChars ) {
644        if( text == null ) {
645            return null;
646        }
647
648        text = text.trim();
649        final StringBuilder clean = new StringBuilder( text.length() );
650
651        //  Remove non-alphanumeric characters that should not be put inside WikiNames.  Note that all valid Unicode letters are
652        //  considered okay for WikiNames. It is the problem of the WikiPageProvider to take care of actually storing that information.
653        //
654        //  Also capitalize things, if necessary.
655
656        boolean isWord = true;  // If true, we've just crossed a word boundary
657        boolean wasSpace = false;
658        for( int i = 0; i < text.length(); i++ ) {
659            char ch = text.charAt( i );
660
661            //  Cleans away repetitive whitespace and only uses the first one.
662            if( Character.isWhitespace( ch ) ) {
663                if( wasSpace ) {
664                    continue;
665                }
666
667                wasSpace = true;
668            } else {
669                wasSpace = false;
670            }
671
672            //  Check if it is allowed to use this char, and capitalize, if necessary.
673            if( Character.isLetterOrDigit( ch ) || allowedChars.indexOf( ch ) != -1 ) {
674                // Is a letter
675                if( isWord ) {
676                    ch = Character.toUpperCase( ch );
677                }
678                clean.append( ch );
679                isWord = false;
680            } else {
681                isWord = true;
682            }
683        }
684
685        return clean.toString();
686    }
687
688    /**
689     *  Escapes XML entities in an HTML-compatible way (i.e. does not escape entities that are already escaped).
690     *
691     *  @param buf String to be escaped.
692     *  @return An escaped string.
693     */
694    public static String escapeHTMLEntities( final String buf ) {
695        final StringBuilder tmpBuf = new StringBuilder( buf.length() + 20 );
696        for( int i = 0; i < buf.length(); i++ ) {
697            final char ch = buf.charAt(i);
698            if( ch == '<' ) {
699                tmpBuf.append("&lt;");
700            } else if( ch == '>' ) {
701                tmpBuf.append("&gt;");
702            } else if( ch == '\"' ) {
703                tmpBuf.append("&quot;");
704            } else if( ch == '&' ) {
705                // If the following is an XML entity reference (&#.*;) we'll leave it as it is; otherwise we'll replace it with an &amp;
706                boolean isEntity = false;
707                final StringBuilder entityBuf = new StringBuilder();
708                if( i < buf.length() -1 ) {
709                    for( int j = i; j < buf.length(); j++ ) {
710                        final char ch2 = buf.charAt( j );
711                        if( Character.isLetterOrDigit( ch2 ) || (ch2 == '#' && j == i+1) || ch2 == ';' || ch2 == '&' ) {
712                            entityBuf.append(ch2);
713                            if( ch2 == ';' ) {
714                                isEntity = true;
715                                break;
716                            }
717                        } else {
718                            break;
719                        }
720                    }
721                }
722
723                if( isEntity ) {
724                    tmpBuf.append( entityBuf );
725                    i = i + entityBuf.length() - 1;
726                } else {
727                    tmpBuf.append( "&amp;" );
728                }
729
730            } else {
731                tmpBuf.append( ch );
732            }
733        }
734
735        return tmpBuf.toString();
736    }
737
738    /**
739     *  Creates a Properties object based on an array which contains alternatively a key and a value.  It is useful
740     *  for generating default mappings. For example:
741     *  <pre>
742     *     String[] properties = { "jspwiki.property1", "value1", "jspwiki.property2", "value2 };
743     *     Properties props = TextUtil.createPropertes( values );
744     *     System.out.println( props.getProperty("jspwiki.property1") );
745     *  </pre>
746     *  would output "value1".
747     *
748     *  @param values Alternating key and value pairs.
749     *  @return Property object
750     *  @see java.util.Properties
751     *  @throws IllegalArgumentException if the property array is missing a value for a key.
752     *  @since 2.2.
753     */
754    public static Properties createProperties( final String[] values ) throws IllegalArgumentException {
755        if( values.length % 2 != 0 ) {
756            throw new IllegalArgumentException( "One value is missing.");
757        }
758
759        final Properties props = new Properties();
760        for( int i = 0; i < values.length; i += 2 ) {
761            props.setProperty( values[i], values[i + 1] );
762        }
763
764        return props;
765    }
766
767    /**
768     *  Counts the number of sections (separated with "----") from the page.
769     *
770     *  @param pagedata The WikiText to parse.
771     *  @return int Number of counted sections.
772     *  @since 2.1.86.
773     */
774    public static int countSections( final String pagedata ) {
775        int tags  = 0;
776        int start = 0;
777
778        while( ( start = pagedata.indexOf( "----", start ) ) != -1 ) {
779            tags++;
780            start += 4; // Skip this "----"
781        }
782
783        // The first section does not get the "----"
784        return !pagedata.isEmpty() ? tags + 1 : 0;
785    }
786
787    /**
788     *  Gets the given section (separated with "----") from the page text. Note that the first section is always #1.  If a page has no
789     *  section markers, then there is only a single section, #1.
790     *
791     *  @param pagedata WikiText to parse.
792     *  @param section  Which section to get.
793     *  @return String  The section.
794     *  @throws IllegalArgumentException If the page does not contain this many sections.
795     *  @since 2.1.86.
796     */
797    public static String getSection( final String pagedata, final int section ) throws IllegalArgumentException {
798        int tags  = 0;
799        int start = 0;
800        int previous = 0;
801
802        while( ( start = pagedata.indexOf( "----", start ) ) != -1 ) {
803            if( ++tags == section ) {
804                return pagedata.substring( previous, start );
805            }
806
807            start += 4; // Skip this "----"
808            // allow additional dashes, treat it as if it was a correct 4-dash
809            while (start < pagedata.length() && pagedata.charAt( start ) == '-') {
810                start++;
811            }
812
813            previous = start;
814        }
815
816        if( ++tags == section ) {
817            return pagedata.substring( previous );
818        }
819
820        throw new IllegalArgumentException( "There is no section no. " + section + " on the page." );
821    }
822
823    /**
824     *  A simple routine which just repeates the arguments.  This is useful for creating something like a line or something.
825     *
826     *  @param what String to repeat
827     *  @param times How many times to repeat the string.
828     *  @return Guess what?
829     *  @since 2.1.98.
830     */
831    public static String repeatString( final String what, final int times ) {
832        return IntStream.range(0, times).mapToObj(i -> what).collect(Collectors.joining());
833    }
834
835    /**
836     *  Converts a string from the Unicode representation into something that can be embedded in a java
837     *  properties file.  All references outside the ASCII range are replaced with \\uXXXX.
838     *
839     *  @param s The string to convert
840     *  @return the ASCII string
841     */
842    public static String native2Ascii( final String s ) {
843        final StringBuilder sb = new StringBuilder();
844        for( int i = 0; i < s.length(); i++ ) {
845            final char aChar = s.charAt(i);
846            if( ( aChar < 0x0020 ) || ( aChar > 0x007e ) ) {
847                sb.append( '\\');
848                sb.append( 'u');
849                sb.append( toHex( ( aChar >> 12 ) & 0xF ) );
850                sb.append( toHex( ( aChar >>  8 ) & 0xF ) );
851                sb.append( toHex( ( aChar >>  4 ) & 0xF ) );
852                sb.append( toHex( aChar        & 0xF ) );
853            } else {
854                sb.append( aChar );
855            }
856        }
857        return sb.toString();
858    }
859
860    private static char toHex( final int nibble ) {
861        final char[] hexDigit = {
862            '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
863        };
864        return hexDigit[ nibble & 0xF ];
865    }
866
867    /**
868     *  Generates a hexadecimal string from an array of bytes.  For example, if the array contains
869     *  { 0x01, 0x02, 0x3E }, the resulting string will be "01023E".
870     *
871     * @param bytes A Byte array
872     * @return A String representation
873     * @since 2.3.87
874     */
875    public static String toHexString( final byte[] bytes ) {
876        final StringBuilder sb = new StringBuilder( bytes.length * 2 );
877        for( final byte aByte : bytes ) {
878            sb.append( toHex( aByte >> 4 ) );
879            sb.append( toHex( aByte ) );
880        }
881
882        return sb.toString();
883    }
884
885    /**
886     *  Returns true, if the argument contains a number, otherwise false. In a quick test this is roughly the same
887     *  speed as Integer.parseInt() if the argument is a number, and roughly ten times the speed, if the argument
888     *  is NOT a number.
889     *
890     *  @since 2.4
891     *  @param s String to check
892     *  @return True, if s represents a number.  False otherwise.
893     */
894    public static boolean isNumber( String s ) {
895        if( s == null ) {
896            return false;
897        }
898
899        if( s.length() > 1 && s.charAt(0) == '-' ) {
900            s = s.substring( 1 );
901        }
902
903        for( int i = 0; i < s.length(); i++ ) {
904            if( !Character.isDigit( s.charAt( i ) ) ) {
905                return false;
906            }
907        }
908
909        return true;
910    }
911
912    /**
913     * Generate a random String suitable for use as a temporary password.
914     *
915     * @return String suitable for use as a temporary password
916     * @since 2.4
917     */
918    public static String generateRandomPassword() {
919        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());
920    }
921
922}