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 & >, <, and " 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, "&", "&" ); 218 src = replaceString( src, "<", "<" ); 219 src = replaceString( src, ">", ">" ); 220 src = replaceString( src, "\"", """ ); 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("<"); 703 } else if( ch == '>' ) { 704 tmpBuf.append(">"); 705 } else if( ch == '\"' ) { 706 tmpBuf.append("""); 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 & 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( "&" ); 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}