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}