001package jmri.jmrix.grapevine;
002
003import java.util.Locale;
004import javax.annotation.Nonnull;
005import jmri.Manager.NameValidity;
006import java.util.regex.Matcher;
007import java.util.regex.Pattern;
008import jmri.Manager;
009import jmri.NamedBean;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * Utility Class supporting parsing and testing of Grapevine addresses.
015 * <p>
016 * Multiple address formats are supported:
017 * <ul>
018 * <li>Gtnnnxxx where: G is the (multichar) system connection prefix,
019 * t is the type code: 'T' for turnouts, 'S' for sensors, 'H' for signal
020 * heads and 'L' for lights;
021 * nnn is the node address (1-127); xxx is a bit number of the input or
022 * output bit (001-999)</li>
023 * <li>Gtnnnxxx = (node address x 1000) + bit number.<br>
024 * Examples: GT1002 (node address 1, bit 2), G1S1003 (node address 1, bit 3),
025 * GL11234 (node address 11, bit234)</li>
026 * <li>Gtnnnaxxxx where: t is the type code, 'T' for turnouts, 'S' for
027 * sensors, 'H' for signal heads and 'L' for lights; nnn is the node address of the
028 * input or output bit (1-127); xxxx is a bit number of the input or output bit
029 * (1-2048); a is a subtype-specific letter:
030 *  <ul>
031 *  <li>'B' for a bit number (e.g. GT12B3 is a shorter form of GT12003)
032 *  <li>'a' is for advanced serial occupancy sensors (only valid t = S)
033 *  <li>'m' is for advanced serial motion sensors (only valid t = S)
034 *  <li>'pattern' is for parallel sensors (only valid t = S)
035  <li>'s' is for serial occupancy sensors (only valid t = S)
036 *  </ul>
037 * Examples: GT1B2 (node address 1, bit 2), G1S1B3 (node address 1, bit 3),
038 * G22L11B234 (node address 11, bit 234)
039 * </li>
040 * </ul>
041 *
042 * @author Dave Duchamp, Copyright (C) 2004
043 * @author Bob Jacobsen, Copyright (C) 2006, 2007, 2008
044 */
045public class SerialAddress {
046
047    public SerialAddress() {
048    }
049
050    /**
051     * Regular expression used to parse Turnout names.
052     * <p>
053     * Groups:
054     * <ul>
055     * <li> - System letter/prefix (not captured in regex)
056     * <li>1 - Type letter
057     * <li>2 - suffix, if of nnnAnnn form
058     * <li>3 - node number in nnnAnnn form
059     * <li>4 - address type in nnnAnnn form
060     * <li>5 - bit number in nnnAnnn form
061     * <li>6 - combined number in nnnnnn form
062     * </ul>
063     */
064    static final String turnoutRegex = "^\\w\\d*(T)(?:((\\d++)(B)(\\d++))|(\\d++))$";
065    static volatile Pattern turnoutPattern = null;
066
067    static Pattern getTurnoutPattern() {
068        // defer compiling pattern until used, instead of at loading time
069        if (turnoutPattern == null) {
070            turnoutPattern = Pattern.compile(turnoutRegex);
071        }
072        return turnoutPattern;
073    }
074
075    /**
076     * Regular expression used to parse Light names.
077     * <p>
078     * Groups:
079     * <ul>
080     * <li> - System letter/prefix (not captured in regex)
081     * <li>1 - Type letter
082     * <li>2 - suffix, if of nnnAnnn form
083     * <li>3 - node number in nnnAnnn form
084     * <li>4 - address type in nnnAnnn form
085     * <li>5 - bit number in nnnAnnn form
086     * <li>6 - combined number in nnnnnn form
087     * </ul>
088     */
089    static final String lightRegex = "^\\w\\d*(L)(?:((\\d++)(B)(\\d++))|(\\d++))$";
090    static volatile Pattern lightPattern = null;
091
092    static Pattern getLightPattern() {
093        // defer compiling pattern until used, instead of at loading time
094        if (lightPattern == null) {
095            lightPattern = Pattern.compile(lightRegex);
096        }
097        return lightPattern;
098    }
099
100    /**
101     * Regular expression used to parse SignalHead names.
102     * <p>
103     * Groups:
104     * <ul>
105     * <li> - System letter/prefix (not captured in regex)
106     * <li>1 - Type letter
107     * <li>2 - suffix, if of nnnAnnn form
108     * <li>3 - node number in nnnAnnn form
109     * <li>4 - address type in nnnAnnn form
110     * <li>5 - bit number in nnnAnnn form
111     * <li>6 - combined number in nnnnnn form
112     * </ul>
113     */
114    static final String headRegex = "^\\w\\d*(H)(?:((\\d++)(B)(\\d++))|(\\d++))$";
115    static volatile Pattern headPattern = null;
116
117    static Pattern getHeadPattern() {
118        // defer compiling pattern until used, instead of at loading time
119        if (headPattern == null) {
120            headPattern = Pattern.compile(headRegex);
121        }
122        return headPattern;
123    }
124
125    /**
126     * Regular expression used to parse Sensor names.
127     * <p>
128     * Groups:
129     * <ul>
130     * <li> - System letter/prefix (not captured in regex)
131     * <li>1 - Type letter
132     * <li>2 - suffix, if of nnnAnnn form
133     * <li>3 - node number in nnnAnnn form
134     * <li>4 - address type in nnnAnnn form
135     * <li>5 - bit number in nnnAnnn form
136     * <li>6 - combined number in nnnnnn form
137     * </ul>
138     */
139    static final String sensorRegex = "^\\w\\d*(S)(?:((\\d++)([BbAaMmPpSs])(\\d++))|(\\d++))$";
140    static volatile Pattern sensorPattern = null;
141
142    static Pattern getSensorPattern() {
143        // defer compiling pattern until used, instead of at loading time
144        if (sensorPattern == null) {
145            sensorPattern = Pattern.compile(sensorRegex);
146        }
147        return sensorPattern;
148    }
149
150    /**
151     * Regular expression used to parse from any type of name.
152     * <p>
153     * Groups:
154     * <ul>
155     * <li> - System letter/prefix (not captured in regex)
156     * <li>1 - Type letter
157     * <li>2 - suffix, if of nnnAnnn form
158     * <li>3 - node number in nnnAnnn form
159     * <li>4 - address type in nnnAnnn form
160     * <li>5 - bit number in nnnAnnn form
161     * <li>6 - combined number in nnnnnn form
162     * </ul>
163     */
164    static final String allRegex = "^\\w\\d*([SHLT])(?:((\\d++)([BbAaMmPpSs])(\\d++))|(\\d++))$";
165    static volatile Pattern allPattern = null;
166
167    static Pattern getAllPattern() {
168        // defer compiling pattern until used, instead of at loading time
169        if (allPattern == null) {
170            allPattern = Pattern.compile(allRegex);
171        }
172        return allPattern;
173    }
174
175    /**
176     * Parse for secondary letters.
177     *
178     * @param type Secondary letter from message
179     * @return offset for type letter, or -1 if none
180     */
181    static int typeOffset(String type) {
182        switch (type.toUpperCase().charAt(0)) {
183            case 'B':
184                return 0;
185            case 'A':
186                return SerialNode.offsetA;
187            case 'M':
188                return SerialNode.offsetM;
189            case 'P':
190                return SerialNode.offsetP;
191            case 'S':
192                return SerialNode.offsetS;
193            default:
194                return -1;
195        }
196    }
197
198    /**
199     * Public static method to parse a system name and return the Serial Node.
200     *
201     * @param systemName system name.
202     * @param tc system connection traffic controller.
203     * @return 'NULL' if illegal systemName format or if the node is not found
204     */
205    public static SerialNode getNodeFromSystemName(String systemName, SerialTrafficController tc) {
206        // validate the System Name leader characters
207        Matcher matcher = getAllPattern().matcher(systemName);
208        if (!matcher.matches()) {
209            // here if an illegal format
210            log.error("illegal system name format in getNodeFromSystemName: {}", systemName);
211            return null;
212        }
213
214        // start decode
215        int ua;
216        if (matcher.group(6) != null) {
217            // This is a Gitnnxxx address
218            int num = Integer.parseInt(matcher.group(6));
219            if (num > 0) {
220                ua = num / 1000;
221            } else {
222                log.error("invalid value in system name: {}", systemName);
223                return null;
224            }
225        } else {
226            ua = Integer.parseInt(matcher.group(3));
227        }
228        return (SerialNode) tc.getNodeFromAddress(ua);
229    }
230
231    /**
232     * Public static method to parse a system name and return the bit number.
233     * Notes: Bits are numbered from 1.
234     *
235     * @param systemName system name.
236     * @param prefix unused.
237     * @return 0 if an error is found
238     */
239    public static int getBitFromSystemName(String systemName, String prefix) {
240        // validate the System Name leader characters
241        Matcher matcher = getAllPattern().matcher(systemName);
242        if (!matcher.matches()) {
243            // here if an illegal format
244            log.error("illegal system name format in getBitFromSystemName: {} prefix: {}", systemName, prefix, new Exception("traceback"));
245            return 0;
246        }
247
248        // start decode
249        int n;
250        if (matcher.group(6) != null) {
251            // name in be Gitnnxxx format
252            int num = Integer.parseInt(matcher.group(6));
253            if (num > 0) {
254                n = num % 1000;
255            } else {
256                log.error("invalid value in system name: {}", systemName);
257                return 0;
258            }
259        } else {
260            // This is a Gitnnaxxxx address
261            n = Integer.parseInt(matcher.group(5));
262        }
263        return n;
264    }
265
266    /**
267     * Public static method to parse a system name to fetch the node number.
268     * <p>
269     * Note: Nodes are numbered from 1.
270     *
271     * @param systemName system name.
272     * @param prefix unused.
273     * @return node number. If an error is found, returns -1
274     */
275    public static int getNodeAddressFromSystemName(String systemName, String prefix) {
276        // validate the System Name leader characters
277        Matcher matcher = getAllPattern().matcher(systemName);
278        if (!matcher.matches()) {
279            // here if an illegal format
280            log.error("illegal system name format in getNodeAddressFromSystemName: {}", systemName);
281            return -1;
282        }
283
284        // start decode
285        int ua;
286        if (matcher.group(6) != null) {
287            // This is a Gitnnxxx address
288            int num = Integer.parseInt(matcher.group(6));
289            if (num > 0) {
290                ua = num / 1000;
291            } else {
292                log.error("invalid value in system name: {}", systemName);
293                return -1;
294            }
295        } else {
296            ua = Integer.parseInt(matcher.group(3));
297            log.debug("node ua: {}", ua);
298        }
299        return ua;
300    }
301
302    /**
303     * Validate a system name.
304     *
305     * @param name    the name to validate
306     * @param manager the manager requesting validation
307     * @param locale  the locale for user messages
308     * @return the name, unchanged
309     * @throws IllegalArgumentException if name is not valid
310     * @see Manager#validateSystemNameFormat(java.lang.String, java.util.Locale)
311     */
312    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST",
313        justification = "Passing Locale.ENGLISH Bundle exception text before stack trace")
314    static String validateSystemNameFormat(String name, Manager<?> manager, Locale locale) {
315        name = manager.validateSystemNamePrefix(name, locale);
316        Pattern pattern;
317        switch (manager.typeLetter()) {
318            case 'L':
319                pattern = getLightPattern();
320                break;
321            case 'T':
322                pattern = getTurnoutPattern();
323                break;
324            case 'H':
325                pattern = getHeadPattern();
326                break;
327            case 'S':
328                pattern = getSensorPattern();
329                break;
330            default:
331                // validateSystemNamePrefix did not validate correctly, so log a stack trace
332                NamedBean.BadSystemNameException ex = new NamedBean.BadSystemNameException(
333                        Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidUnknownType", name),
334                        Bundle.getMessage(locale, "SystemNameInvalidUnknownType", name));
335                log.error(ex.getMessage(), ex); // second parameter logs stack trace
336                throw ex;
337        }
338        Matcher matcher = pattern.matcher(name);
339        if (!matcher.matches()) {
340            throw new NamedBean.BadSystemNameException(
341                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameFailedRegex", name, pattern.pattern()),
342                    Bundle.getMessage(locale, "InvalidSystemNameFailedRegex", name, pattern.pattern()));
343        }
344        int node;
345        int bit;
346        if (matcher.group(6) != null) {
347            // Gitnnxxx format
348            int num = Integer.parseInt(matcher.group(6));
349            node = num / 1000;
350            bit = num % 1000;
351        } else {
352            // Gitnnaxxxx address
353            node = Integer.parseInt(matcher.group(3));
354            bit = Integer.parseInt(matcher.group(5));
355        }
356        // check values
357        if ((node < 1) || (node > 127)) {
358            throw new NamedBean.BadSystemNameException(
359                    Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidNode", name, bit, 1, 127),
360                    Bundle.getMessage(locale, "SystemNameInvalidNode", name, bit, 1, 127));
361        }
362
363        // check bit numbers
364        if (manager.typeLetter() != 'S') {
365            if (!((bit >= 101 && bit <= 124)
366                    || (bit >= 201 && bit <= 224)
367                    || (bit >= 301 && bit <= 324)
368                    || (bit >= 401 && bit <= 424))) {
369                throw new NamedBean.BadSystemNameException(
370                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameFailedRegex", name, pattern.pattern()),
371                        Bundle.getMessage(locale, "InvalidSystemNameFailedRegex", name, pattern.pattern()));
372            }
373        } else {
374            // sort on subtype
375            String subtype = matcher.group(4);
376            if (null == subtype) { // no subtype, just look at total
377                if ((bit < 1) || (bit > 224)) {
378                    throw new NamedBean.BadSystemNameException(
379                            Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 224),
380                            Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 224));
381                }
382            } else {
383                switch (subtype.toUpperCase()) {
384                    case "A":
385                        // advanced serial occ
386                        if ((bit < 1) || (bit > 24)) {
387                            throw new NamedBean.BadSystemNameException(
388                                    Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 24),
389                                    Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 24));
390                        }
391                        break;
392                    case "M":
393                        // advanced serial motion
394                        if ((bit < 1) || (bit > 24)) {
395                            throw new NamedBean.BadSystemNameException(
396                                    Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 24),
397                                    Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 24));
398                        }
399                        break;
400                    case "S":
401                        // old serial
402                        if ((bit < 1) || (bit > 24)) {
403                            throw new NamedBean.BadSystemNameException(
404                                    Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 24),
405                                    Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 24));
406                        }
407                        break;
408                    case "P":
409                        // parallel
410                        if ((bit < 1) || (bit > 96)) {
411                            throw new NamedBean.BadSystemNameException(
412                                    Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 96),
413                                    Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 96));
414                        }
415                        break;
416                    default:
417                        break;
418                }
419            }
420        }
421        return name;
422    }
423
424    /**
425     * Public static method to validate system name format.
426     * Logging of handled cases no higher than WARN.
427     *
428     * @param systemName name to check
429     * @param type       expected device type letter
430     * @param prefix     system connection prefix from memo
431     * @return 'true' if system name has a valid format, else returns 'false'
432     */
433    public static NameValidity validSystemNameFormat(@Nonnull String systemName, final char type, String prefix) {
434        // validate the System Name leader characters
435        Matcher matcher = getAllPattern().matcher(systemName);
436        if (!matcher.matches()) {
437            // here if an illegal format, e.g. another system letter
438            // which happens all the time due to how proxy managers work
439            return NameValidity.INVALID;
440        }
441        if (matcher.group(1).charAt(0) != type) { // notice we skipped the multichar prefix
442            log.warn("type in {} does not match type {}", systemName, type);
443            return NameValidity.INVALID;
444        }
445        Pattern p;
446        switch (type) {
447            case 'L':
448                p = getLightPattern();
449                break;
450            case 'T':
451                p = getTurnoutPattern();
452                break;
453            case 'H':
454                p = getHeadPattern();
455                break;
456            case 'S':
457                p = getSensorPattern();
458                break;
459            default:
460                log.error("cannot match type in {}, which is unexpected", systemName);
461                return NameValidity.INVALID;
462        }
463
464        // check format
465        matcher = p.matcher(systemName);
466        if (!matcher.matches()) {
467            // here if cannot parse specifically (only accepts GTnnn or GTnnnB
468            log.debug("invalid system name format: {} for type {}", systemName, type);
469            return NameValidity.INVALID;
470        }
471
472        // check for the two different formats
473        int node;
474        int bit;
475        if (matcher.group(6) != null) {
476            // name in be Gitnnxxx format
477            int num = Integer.parseInt(matcher.group(6));
478            if (num > 0) {
479                node = num / 1000;
480                bit = num % 1000;
481            } else {
482                log.debug("invalid value in system name: {}", systemName);
483                return NameValidity.INVALID;
484            }
485        } else {
486            // This is a Gitnnaxxxx address, get values
487            node = Integer.parseInt(matcher.group(3));
488            bit = Integer.parseInt(matcher.group(5));
489        }
490
491        // check values
492        if ((node < 1) || (node > 127)) {
493            log.debug("invalid node number {} in {}", node, systemName);
494            return NameValidity.INVALID;
495        }
496
497        // check bit numbers
498        if ((type == 'T') || (type == 'H') || (type == 'L')) {
499            if (!((bit >= 101 && bit <= 124)
500                    || (bit >= 201 && bit <= 224)
501                    || (bit >= 301 && bit <= 324)
502                    || (bit >= 401 && bit <= 424))) {
503                log.debug("invalid bit number {} in {}", bit, systemName);
504                return NameValidity.INVALID;
505            }
506        } else { // type MUST be 'S', see earlier logic to get Pattern
507            // sort on subtype
508            String subtype = matcher.group(4);
509            if (subtype == null) { // no subtype, just look at total
510                if ((bit < 1) || (bit > 224)) {
511                    log.debug("invalid bit number {} in {}", bit, systemName);
512                    return NameValidity.INVALID;
513                } else {
514                    return NameValidity.VALID;
515                }
516            }
517            subtype = subtype.toUpperCase();
518            if (subtype.equals("A")) { // advanced serial occ
519                if ((bit < 1) || (bit > 24)) {
520                    log.debug("invalid bit number {} in {}", bit, systemName);
521                    return NameValidity.INVALID;
522                }
523            } else if (subtype.equals("M")) { // advanced serial motion
524                if ((bit < 1) || (bit > 24)) {
525                    log.debug("invalid bit number {} in  {}", bit, systemName);
526                    return NameValidity.INVALID;
527                }
528            } else if (subtype.equals("S")) { // old serial
529                if ((bit < 1) || (bit > 24)) {
530                    log.debug("invalid bit number {} in {}", bit, systemName);
531                    return NameValidity.INVALID;
532                }
533            } else if (subtype.equals("P")) { // parallel
534                if ((bit < 1) || (bit > 96)) {
535                    log.debug("invalid bit number {} in {}", bit, systemName);
536                    return NameValidity.INVALID;
537                }
538            }
539        }
540
541        // finally, return VALID
542        return NameValidity.VALID;
543    }
544
545    /**
546     * Public static method to validate system name for configuration.
547     *
548     * @param systemName system name to validate.
549     * @param type bean type, S, T or L.
550     * @param tc system connection traffic controller.
551     * @return 'true' if system name has a valid meaning in current configuration,
552     *                else returns 'false'.
553     *
554     */
555    public static boolean validSystemNameConfig(String systemName, char type, SerialTrafficController tc) {
556        String prefix = tc.getSystemConnectionMemo().getSystemPrefix();
557        if (validSystemNameFormat(systemName, type, prefix) != NameValidity.VALID) {
558            // No point in trying if a valid system name format is not present
559            log.debug("invalid system name {}", systemName);
560            return false;
561        }
562        SerialNode node = getNodeFromSystemName(systemName, tc);
563        if (node == null) {
564            log.warn("invalid system name {}; no such node", systemName);
565            // The node indicated by this system address is not present
566            return false;
567        }
568        int bit = getBitFromSystemName(systemName, prefix);
569        if ((type == 'T') || (type == 'L')) {
570            if ((bit <= 0) || (bit > SerialNode.outputBits[node.nodeType])) {
571                // The bit is not valid for this defined Serial node
572                log.warn("invalid system name {}; bad output bit number {} > {}",
573                        systemName, bit, SerialNode.outputBits[node.nodeType]);
574                return false;
575            }
576        } else if (type == 'S') {
577            if ((bit <= 0) || (bit > SerialNode.inputBits[node.nodeType])) {
578                // The bit is not valid for this defined Serial node
579                log.warn("invalid system name {}; bad input bit number {} > {}",
580                        systemName, bit, SerialNode.inputBits[node.nodeType]);
581                return false;
582            }
583        } else {
584            log.error("Invalid type specification in validSystemNameConfig call");
585            return false;
586        }
587        // System name has passed all tests
588        return true;
589    }
590
591    /**
592     * Public static method to convert any format system name for the alternate
593     * format (nnBnn).
594     * <p>
595     * If the supplied system name does not have a valid format,
596     * or if there is no representation in the alternate naming scheme,
597     * an empty string is returned.
598     * @param systemName system name to convert.
599     * @param prefix system prefix.
600     * @return alternate string.
601     */
602    public static String convertSystemNameToAlternate(String systemName, String prefix) {
603        // ensure that input system name has a valid format
604        if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) {
605            // No point in normalizing if a valid system name format is not present
606            return "";
607        }
608
609        Matcher matcher = getAllPattern().matcher(systemName);
610        matcher.matches(); // known to work, just need values
611        // check format
612        if (matcher.group(6) != null) {
613            int num = Integer.parseInt(matcher.group(6));
614            return prefix + matcher.group(1) + (num / 1000) + "B" + (num % 1000);
615        } else {
616            int node = Integer.parseInt(matcher.group(3));
617            int bit = Integer.parseInt(matcher.group(5));
618            return prefix + matcher.group(1) + node + "B" + bit;
619        }
620    }
621
622    /**
623     * Public static method to normalize a system name
624     * <p>
625     * This routine is used to ensure that each system name is uniquely linked
626     * to one bit, by removing extra zeros inserted by the user.
627     * <p>
628     * If the supplied system name does not have a valid format, an empty string
629     * is returned. Otherwise a normalized name is returned in the same format
630     * as the input name.
631     * @param systemName system name to normalize.
632     * @param prefix system prefix.
633     * @return normalized string.
634     */
635    public static String normalizeSystemName(String systemName, String prefix) {
636        // ensure that input system name has a valid format
637        try {
638           if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) {
639               // No point in normalizing if a valid system name format is not present
640               return "";
641           }
642
643           Matcher matcher = getAllPattern().matcher(systemName);
644           matcher.matches(); // known to work, just need values
645
646           // check format
647           if (matcher.group(6) != null) {
648              int num = Integer.parseInt(matcher.group(6));
649              return prefix + matcher.group(1) + num;
650           } else {
651              // there are alternate forms...
652              int offset = typeOffset(matcher.group(4));
653              int node = Integer.parseInt(matcher.group(3));
654              int bit = Integer.parseInt(matcher.group(5));
655              return prefix + matcher.group(1) + (node * 1000 + bit + offset);
656           }
657       } catch(java.lang.StringIndexOutOfBoundsException sobe){
658             throw new IllegalArgumentException("Invalid System Name Format: " + systemName);
659       }
660    }
661
662    private final static Logger log = LoggerFactory.getLogger(SerialAddress.class);
663
664}