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}