001package jmri.util; 002 003import java.util.Arrays; 004import java.util.regex.Pattern; 005import java.util.regex.Matcher; 006 007import javax.annotation.CheckForNull; 008import javax.annotation.CheckReturnValue; 009import javax.annotation.Nonnull; 010 011/** 012 * Common utility methods for working with Strings. 013 * <p> 014 * We needed a place to refactor common string-processing idioms in JMRI code, 015 * so this class was created. It's more of a library of procedures than a real 016 * class, as (so far) all of the operations have needed no state information. 017 * <p> 018 * In some cases, these routines use a Java 1.3 or later method, falling back to 019 * an explicit implementation when running on Java 1.1 020 * 021 * @author Bob Jacobsen Copyright 2003 022 */ 023public class StringUtil { 024 025 public static final String HTML_CLOSE_TAG = "</html>"; 026 public static final String HTML_OPEN_TAG = "<html>"; 027 public static final String LINEBREAK = "\n"; 028 029 /** 030 * Starting with two arrays, one of names and one of corresponding numeric 031 * state values, find the state value that matches a given name string 032 * 033 * @param name the name to search for 034 * @param states the state values 035 * @param names the name values 036 * @return the state or -1 if none found 037 */ 038 @CheckReturnValue 039 public static int getStateFromName(String name, int[] states, String[] names) { 040 for (int i = 0; i < states.length; i++) { 041 if (name.equals(names[i])) { 042 return states[i]; 043 } 044 } 045 return -1; 046 } 047 048 /** 049 * Starting with three arrays, one of names, one of corresponding numeric 050 * state values, and one of masks for the state values, find the name 051 * string(s) that match a given state value 052 * 053 * @param state the given state 054 * @param states the state values 055 * @param masks the state masks 056 * @param names the state names 057 * @return names matching the given state or an empty array 058 */ 059 @CheckReturnValue 060 public static String[] getNamesFromStateMasked(int state, int[] states, int[] masks, String[] names) { 061 // first pass to count, get refs 062 int count = 0; 063 String[] temp = new String[states.length]; 064 065 for (int i = 0; i < states.length; i++) { 066 if (((state ^ states[i]) & masks[i]) == 0) { 067 temp[count++] = names[i]; 068 } 069 } 070 // second pass to create output array 071 String[] output = new String[count]; 072 System.arraycopy(temp, 0, output, 0, count); 073 return output; 074 } 075 076 /** 077 * Starting with two arrays, one of names and one of corresponding numeric 078 * state values, find the name string that matches a given state value. Only 079 * one may be returned. 080 * 081 * @param state the given state 082 * @param states the state values 083 * @param names the state names 084 * @return the first matching name or null if none found 085 */ 086 @CheckReturnValue 087 @CheckForNull 088 public static String getNameFromState(int state, @Nonnull int[] states, @Nonnull String[] names) { 089 for (int i = 0; i < states.length; i++) { 090 if (state == states[i]) { 091 return names[i]; 092 } 093 } 094 return null; 095 } 096 097 private static final char[] HEX_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 098 099 /** 100 * Convert an integer to an exactly two hexadecimal characters string 101 * 102 * @param val the integer value 103 * @return String exactly two characters long 104 */ 105 @CheckReturnValue 106 @Nonnull 107 public static String twoHexFromInt(int val) { 108 StringBuilder sb = new StringBuilder(); 109 sb.append(HEX_CHARS[(val & 0xF0) >> 4]); 110 sb.append(HEX_CHARS[val & 0x0F]); 111 return sb.toString(); 112 } 113 114 /** 115 * Quickly append an integer to a String as exactly two hexadecimal 116 * characters 117 * 118 * @param val Value to append in hex 119 * @param inString String to be extended 120 * @return String exactly two characters long 121 */ 122 @CheckReturnValue 123 @Nonnull 124 public static String appendTwoHexFromInt(int val, @Nonnull String inString) { 125 StringBuilder sb = new StringBuilder(inString); 126 sb.append(StringUtil.twoHexFromInt(val)); 127 return sb.toString(); 128 } 129 130 /** 131 * Convert a small number to eight 1/0 characters. 132 * 133 * @param val the number to convert 134 * @param msbLeft true if the MSB is on the left of the display 135 * @return a string of binary characters 136 */ 137 @CheckReturnValue 138 @Nonnull 139 public static String to8Bits(int val, boolean msbLeft) { 140 StringBuilder result = new StringBuilder(8); 141 for (int i = 0; i < 8; i++) { 142 if (msbLeft) { 143 result.insert(0,(val & 0x01) != 0 ? "1" : "0"); 144 } else { 145 result.append(((val & 0x01) != 0 ? "1" : "0")); 146 } 147 val = val >> 1; 148 } 149 return result.toString(); 150 } 151 152 /** 153 * Create a String containing hexadecimal values from a byte[]. 154 * 155 * eg. byte[]{1,2,3,10} will return String "01 02 03 0A " 156 * eg. byte[]{-1} will return "FF " 157 * eg. byte[]{(byte)256} will return "00 " 158 * eg. byte[]{(byte)257} will return "01 " 159 * 160 * @param bytes byte array. Can be zero length, but must not be null. 161 * @return String of hex values, ala "01 02 0A B1 21 ". 162 */ 163 @CheckReturnValue 164 @Nonnull 165 public static String hexStringFromBytes(@Nonnull byte[] bytes) { 166 StringBuilder sb = new StringBuilder(); 167 for (byte aByte : bytes) { 168 sb.append(HEX_CHARS[(aByte & 0xF0) >> 4]); 169 sb.append(HEX_CHARS[aByte & 0x0F]); 170 sb.append(' '); 171 } 172 return sb.toString(); 173 } 174 175 /** 176 * Convert an array of integers into a single spaced hex. string. 177 * Each int value will receive 2 hex characters. 178 * <p> 179 * eg. int[]{1,2,3,10} will return "01 02 03 0A " 180 * eg. int[]{-1} will return "FF " 181 * eg. int[]{256} will return "00 " 182 * eg. int[]{257} will return "01 " 183 * 184 * @param v the array of integers. Can be zero length, but must not be null. 185 * @return the formatted String or an empty String 186 */ 187 @CheckReturnValue 188 @Nonnull 189 public static String hexStringFromInts(@Nonnull int[] v) { 190 StringBuilder retval = new StringBuilder(); 191 for (int e : v) { 192 retval.append(twoHexFromInt(e)); 193 retval.append(" "); 194 } 195 return retval.toString(); 196 } 197 198 /** 199 * Create a byte[] from a String containing hexadecimal values. 200 * 201 * @param s String of hex values, ala "01 02 0A B1 21". 202 * @return byte array, with one byte for each pair. Can be zero length, but 203 * will not be null. 204 */ 205 @CheckReturnValue 206 @Nonnull 207 public static byte[] bytesFromHexString(@Nonnull String s) { 208 String ts = s + " "; // ensure blanks on end to make scan easier 209 int len = 0; 210 // scan for length 211 for (int i = 0; i < s.length(); i++) { 212 if (ts.charAt(i) != ' ') { 213 // need to process char for number. Is this a single digit? 214 if (ts.charAt(i + 1) != ' ') { 215 // 2 char value 216 i++; 217 len++; 218 } else { 219 // 1 char value 220 len++; 221 } 222 } 223 } 224 byte[] b = new byte[len]; 225 // scan for content 226 int saveAt = 0; 227 for (int i = 0; i < s.length(); i++) { 228 if (ts.charAt(i) != ' ') { 229 // need to process char for number. Is this a single digit? 230 if (ts.charAt(i + 1) != ' ') { 231 // 2 char value 232 String v = "" + ts.charAt(i) + ts.charAt(i + 1); 233 b[saveAt] = (byte) Integer.valueOf(v, 16).intValue(); 234 i++; 235 saveAt++; 236 } else { 237 // 1 char value 238 String v = "" + ts.charAt(i); 239 b[saveAt] = (byte) Integer.valueOf(v, 16).intValue(); 240 saveAt++; 241 } 242 } 243 } 244 return b; 245 } 246 247 /** 248 * Create an int[] from a String containing paired hexadecimal values. 249 * <p> 250 * Option to include array length as leading array value 251 * <p> 252 * eg. #("01020AB121",true) returns int[5, 1, 2, 10, 177, 33] 253 * <p> 254 * eg. ("01020AB121",false) returns int[1, 2, 10, 177, 33] 255 * 256 * @param s String of hex value pairs, eg "01020AB121". 257 * @param headerTotal if true, adds index [0] with total of pairs found 258 * @return int array, with one field for each pair. 259 * 260 */ 261 @Nonnull 262 public static int[] intBytesWithTotalFromNonSpacedHexString(@Nonnull String s, boolean headerTotal) { 263 if (s.length() % 2 == 0) { 264 int numBytes = ( s.length() / 2 ); 265 if ( headerTotal ) { 266 int[] arr = new int[(numBytes+1)]; 267 arr[0]=numBytes; 268 for (int i = 0; i < numBytes; i++) { 269 arr[(i+1)] = getByte(i,s); 270 } 271 return arr; 272 } 273 else { 274 int[] arr = new int[(numBytes)]; 275 for (int i = 0; i < numBytes; i++) { 276 arr[(i)] = getByte(i,s); 277 } 278 return arr; 279 } 280 } else { 281 return new int[]{0}; 282 } 283 } 284 285 /** 286 * Get a single hex digit from a String. 287 * <p> 288 * eg. getHexDigit(0,"ABCDEF") returns 10 289 * eg. getHexDigit(3,"ABCDEF") returns 14 290 * 291 * @param index digit offset, 0 is very first digit on left. 292 * @param byteString String of hex values, eg "01020AB121". 293 * @return hex value of single digit 294 */ 295 public static int getHexDigit(int index, @Nonnull String byteString) { 296 int b = byteString.charAt(index); 297 if ((b >= '0') && (b <= '9')) { 298 b = b - '0'; 299 } else if ((b >= 'A') && (b <= 'F')) { 300 b = b - 'A' + 10; 301 } else if ((b >= 'a') && (b <= 'f')) { 302 b = b - 'a' + 10; 303 } else { 304 b = 0; 305 } 306 return (byte) b; 307 } 308 309 /** 310 * Get a single hex data byte from a string 311 * <p> 312 * eg. getByte(2,"0102030405") returns 3 313 * 314 * @param b The byte offset, 0 is byte 1 315 * @param byteString the whole string, eg "01AB2CD9" 316 * @return The value, else 0 317 */ 318 public static int getByte(int b, @Nonnull String byteString) { 319 if ((b >= 0)) { 320 int index = b * 2; 321 int hi = getHexDigit(index++, byteString); 322 int lo = getHexDigit(index, byteString); 323 if ((hi < 16) && (lo < 16)) { 324 return (hi * 16 + lo); 325 } 326 } 327 return 0; 328 } 329 330 /** 331 * Create a hex byte[] of Unicode character values from a String containing full text (non hex) values. 332 * <p> 333 * eg fullTextToHexArray("My FroG",8) would return byte[0x4d,0x79,0x20,0x46,0x72,0x6f,0x47,0x20] 334 * 335 * @param s String, eg "Test", value is trimmed to max byte length 336 * @param numBytes Number of bytes expected in return ( eg. to match max. message size ) 337 * @return hex byte array, with one byte for each character. Right padded with empty spaces (0x20) 338 * 339 */ 340 @CheckReturnValue 341 @Nonnull 342 public static byte[] fullTextToHexArray(@Nonnull String s, int numBytes) { 343 byte[] b = new byte[numBytes]; 344 java.util.Arrays.fill(b, (byte) 0x20); 345 s = s.substring(0, Math.min(s.length(), numBytes)); 346 String convrtedNoSpaces = String.format( "%x", 347 new java.math.BigInteger(1, s.getBytes(/*YOUR_CHARSET?*/) ) ); 348 int byteNum=0; 349 for (int i = 0; i < convrtedNoSpaces.length(); i+=2) { 350 b[byteNum] = (byte) Integer.parseInt(convrtedNoSpaces.substring(i, i + 2), 16); 351 byteNum++; 352 } 353 return b; 354 } 355 356 /** 357 * This is a case-independent lexagraphic sort. Identical entries are 358 * retained, so the output length is the same as the input length. 359 * 360 * @param values the Objects to sort 361 */ 362 public static void sortUpperCase(@Nonnull Object[] values) { 363 Arrays.sort(values, (Object o1, Object o2) -> o1.toString().compareToIgnoreCase(o2.toString())); 364 } 365 366 /** 367 * Sort String[] representing numbers, in ascending order. 368 * 369 * @param values the Strings to sort 370 * @throws NumberFormatException if string[] doesn't only contain numbers 371 */ 372 public static void numberSort(@Nonnull String[] values) throws NumberFormatException { 373 for (int i = 0; i <= values.length - 2; i++) { // stop sort early to save time! 374 for (int j = values.length - 2; j >= i; j--) { 375 // check that the jth value is larger than j+1th, 376 // else swap 377 if (Integer.parseInt(values[j]) > Integer.parseInt(values[j + 1])) { 378 // swap 379 String temp = values[j]; 380 values[j] = values[j + 1]; 381 values[j + 1] = temp; 382 } 383 } 384 } 385 } 386 387 /** 388 * Quotes unmatched closed parentheses; matched ( ) pairs are left 389 * unchanged. 390 * 391 * If there's an unmatched ), quote it with \, and quote \ with \ too. 392 * 393 * @param in String potentially containing unmatched closing parenthesis 394 * @return null if given null 395 */ 396 @CheckReturnValue 397 @CheckForNull 398 public static String parenQuote(@CheckForNull String in) { 399 if (in == null || in.equals("")) { 400 return in; 401 } 402 StringBuilder result = new StringBuilder(); 403 int level = 0; 404 for (int i = 0; i < in.length(); i++) { 405 char c = in.charAt(i); 406 switch (c) { 407 case '(': 408 level++; 409 break; 410 case '\\': 411 result.append('\\'); 412 break; 413 case ')': 414 level--; 415 if (level < 0) { 416 level = 0; 417 result.append('\\'); 418 } 419 break; 420 default: 421 break; 422 } 423 result.append(c); 424 } 425 return new String(result); 426 } 427 428 /** 429 * Undo parenQuote 430 * 431 * @param in the input String 432 * @return null if given null 433 */ 434 @CheckReturnValue 435 @CheckForNull 436 static String parenUnQuote(@CheckForNull String in) { 437 if (in == null || in.equals("")) { 438 return in; 439 } 440 StringBuilder result = new StringBuilder(); 441 for (int i = 0; i < in.length(); i++) { 442 char c = in.charAt(i); 443 if (c == '\\') { 444 i++; 445 c = in.charAt(i); 446 if (c != '\\' && c != ')') { 447 // if none of those, just leave both in place 448 c += '\\'; 449 } 450 } 451 result.append(c); 452 } 453 return new String(result); 454 } 455 456 @CheckReturnValue 457 @Nonnull 458 public static java.util.List<String> splitParens(@CheckForNull String in) { 459 java.util.ArrayList<String> result = new java.util.ArrayList<>(); 460 if (in == null || in.equals("")) { 461 return result; 462 } 463 int level = 0; 464 String temp = ""; 465 for (int i = 0; i < in.length(); i++) { 466 char c = in.charAt(i); 467 switch (c) { 468 case '(': 469 level++; 470 break; 471 case '\\': 472 temp += c; 473 i++; 474 c = in.charAt(i); 475 break; 476 case ')': 477 level--; 478 break; 479 default: 480 break; 481 } 482 temp += c; 483 if (level == 0) { 484 result.add(temp); 485 temp = ""; 486 } 487 } 488 return result; 489 } 490 491 /** 492 * Convert an array of objects into a single string. Each object's toString 493 * value is displayed within square brackets and separated by commas. 494 * 495 * @param <E> the array class 496 * @param v the array to process 497 * @return a string; empty if the array was empty 498 */ 499 @CheckReturnValue 500 @Nonnull 501 public static <E> String arrayToString(@Nonnull E[] v) { 502 StringBuilder retval = new StringBuilder(); 503 boolean first = true; 504 for (E e : v) { 505 if (!first) { 506 retval.append(','); 507 } 508 first = false; 509 retval.append('['); 510 retval.append(e.toString()); 511 retval.append(']'); 512 } 513 return new String(retval); 514 } 515 516 /** 517 * Convert an array of bytes into a single string. Each element is displayed 518 * within square brackets and separated by commas. 519 * 520 * @param v the array of bytes 521 * @return the formatted String, or an empty String 522 */ 523 @CheckReturnValue 524 @Nonnull 525 public static String arrayToString(@Nonnull byte[] v) { 526 StringBuilder retval = new StringBuilder(); 527 boolean first = true; 528 for (byte e : v) { 529 if (!first) { 530 retval.append(','); 531 } 532 first = false; 533 retval.append('['); 534 retval.append(e); 535 retval.append(']'); 536 } 537 return new String(retval); 538 } 539 540 /** 541 * Convert an array of integers into a single string. Each element is 542 * displayed within square brackets and separated by commas. 543 * 544 * @param v the array of integers 545 * @return the formatted String or an empty String 546 */ 547 @CheckReturnValue 548 @Nonnull 549 public static String arrayToString(@Nonnull int[] v) { 550 StringBuilder retval = new StringBuilder(); 551 boolean first = true; 552 for (int e : v) { 553 if (!first) { 554 retval.append(','); 555 } 556 first = false; 557 retval.append('['); 558 retval.append(e); 559 retval.append(']'); 560 } 561 return new String(retval); 562 } 563 564 /** 565 * Trim a text string to length provided and (if shorter) pad with trailing spaces. 566 * Removes 1 extra character to the right for clear column view. 567 * 568 * @param value contents to process 569 * @param length trimming length 570 * @return trimmed string, left aligned by padding to the right 571 */ 572 @CheckReturnValue 573 public static String padString (String value, int length) { 574 if (length > 1) { 575 return String.format("%-" + length + "s", value.substring(0, Math.min(value.length(), length - 1))); 576 } else { 577 return value; 578 } 579 } 580 581 /** 582 * Return the first int value within a string 583 * eg :X X123XX456X: will return 123 584 * eg :X123 456: will return 123 585 * 586 * @param str contents to process 587 * @return first value in int form , -1 if not found 588 */ 589 @CheckReturnValue 590 public static int getFirstIntFromString(@Nonnull String str){ 591 StringBuilder sb = new StringBuilder(); 592 for (int i =0; i<str.length(); i ++) { 593 char c = str.charAt(i); 594 if (c != ' ' ){ 595 if (Character.isDigit(c)) { 596 sb.append(c); 597 } else { 598 if ( sb.length() > 0 ) { 599 break; 600 } 601 } 602 } else { 603 if ( sb.length() > 0 ) { 604 break; 605 } 606 } 607 } 608 if ( sb.length() > 0 ) { 609 return (Integer.parseInt(sb.toString())); 610 } 611 return -1; 612 } 613 614 /** 615 * Return the last int value within a string 616 * eg :XX123XX456X: will return 456 617 * eg :X123 456: will return 456 618 * 619 * @param str contents to process 620 * @return last value in int form , -1 if not found 621 */ 622 @CheckReturnValue 623 public static int getLastIntFromString(@Nonnull String str){ 624 StringBuilder sb = new StringBuilder(); 625 for (int i = str.length() - 1; i >= 0; i --) { 626 char c = str.charAt(i); 627 if(c != ' '){ 628 if (Character.isDigit(c)) { 629 sb.insert(0, c); 630 } else { 631 if ( sb.length() > 0 ) { 632 break; 633 } 634 } 635 } else { 636 if ( sb.length() > 0 ) { 637 break; 638 } 639 } 640 } 641 if ( sb.length() > 0 ) { 642 return (Integer.parseInt(sb.toString())); 643 } 644 return -1; 645 } 646 647 /** 648 * Increment the last number found in a string. 649 * @param str Initial string to increment. 650 * @param increment number to increment by. 651 * @return null if not possible, else incremented String. 652 */ 653 @CheckForNull 654 public static String incrementLastNumberInString(@Nonnull String str, int increment){ 655 int num = getLastIntFromString(str); 656 return ( (num == -1) ? null : replaceLast(str,String.valueOf(num),String.valueOf(num+increment))); 657 } 658 659 /** 660 * Replace the last occurance of string value within a String 661 * eg from ABC to DEF will convert XXABCXXXABCX to XXABCXXXDEFX 662 * 663 * @param string contents to process 664 * @param from value within string to be replaced 665 * @param to new value 666 * @return string with the replacement, original value if no match. 667 */ 668 @CheckReturnValue 669 @Nonnull 670 public static String replaceLast(@Nonnull String string, @Nonnull String from, @Nonnull String to) { 671 int lastIndex = string.lastIndexOf(from); 672 if (lastIndex < 0) { 673 return string; 674 } 675 String tail = string.substring(lastIndex).replaceFirst(from, to); 676 return string.substring(0, lastIndex) + tail; 677 } 678 679 /** 680 * Concatenates text Strings where either could possibly be in HTML format 681 * (as used in many Swing components). 682 * <p> 683 * Ensures any appended text is added within the {@code <html>...</html>} 684 * element, if there is any. 685 * 686 * @param baseText original text 687 * @param extraText text to be appended to original text 688 * @return Combined text, with a single enclosing {@code <html>...</html>} 689 * element (only if needed). 690 */ 691 public static String concatTextHtmlAware(String baseText, String extraText) { 692 if (baseText == null && extraText == null) { 693 return null; 694 } 695 if (baseText == null) { 696 return extraText; 697 } 698 if (extraText == null) { 699 return baseText; 700 } 701 boolean hasHtml = false; 702 String result = baseText + extraText; 703 result = result.replaceAll("(?i)" + HTML_OPEN_TAG, ""); 704 result = result.replaceAll("(?i)" + HTML_CLOSE_TAG, ""); 705 if (!result.equals(baseText + extraText)) { 706 hasHtml = true; 707 log.debug("\n\nbaseText:\n\"{}\"\nextraText:\n\"{}\"\n", baseText, extraText); 708 } 709 if (hasHtml) { 710 result = HTML_OPEN_TAG + result + HTML_CLOSE_TAG; 711 log.debug("\nCombined String:\n\"{}\"\n", result); 712 } 713 return result; 714 } 715 716 /** 717 * Removes HTML tags from a String. 718 * Replaces HTML line breaks with newline characters from a given input string. 719 * 720 * @param originalText The input string that may contain HTML tags. 721 * @return A cleaned string with HTML tags removed. 722 */ 723 public static String stripHtmlTags( final String originalText) { 724 String replaceA = originalText.replace("<br>", System.lineSeparator()); 725 String replaceB = replaceA.replace("<br/>", System.lineSeparator()); 726 String replaceC = replaceB.replace("<br />", System.lineSeparator()); 727 String regex = "<[^>]*>"; 728 Matcher matcher = Pattern.compile(regex).matcher(replaceC); 729 return matcher.replaceAll(""); 730 } 731 732 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StringUtil.class); 733 734}