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