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}