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, 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        if (type == 'L') {
447            p = getLightPattern();
448        } else if (type == 'T') {
449            p = getTurnoutPattern();
450        } else if (type == 'H') {
451            p = getHeadPattern();
452        } else if (type == 'S') {
453            p = getSensorPattern();
454        } else {
455            log.error("cannot match type in {}, which is unexpected", systemName);
456            return NameValidity.INVALID;
457        }
458
459        // check format
460        matcher = p.matcher(systemName);
461        if (!matcher.matches()) {
462            // here if cannot parse specifically (only accepts GTnnn or GTnnnB
463            log.debug("invalid system name format: {} for type {}", systemName, type);
464            return NameValidity.INVALID;
465        }
466
467        // check for the two different formats
468        int node;
469        int bit;
470        if (matcher.group(6) != null) {
471            // name in be Gitnnxxx format
472            int num = Integer.parseInt(matcher.group(6));
473            if (num > 0) {
474                node = num / 1000;
475                bit = num % 1000;
476            } else {
477                log.debug("invalid value in system name: {}", systemName);
478                return NameValidity.INVALID;
479            }
480        } else {
481            // This is a Gitnnaxxxx address, get values
482            node = Integer.parseInt(matcher.group(3));
483            bit = Integer.parseInt(matcher.group(5));
484        }
485
486        // check values
487        if ((node < 1) || (node > 127)) {
488            log.debug("invalid node number {} in {}", node, systemName);
489            return NameValidity.INVALID;
490        }
491
492        // check bit numbers
493        if ((type == 'T') || (type == 'H') || (type == 'L')) {
494            if (!((bit >= 101 && bit <= 124)
495                    || (bit >= 201 && bit <= 224)
496                    || (bit >= 301 && bit <= 324)
497                    || (bit >= 401 && bit <= 424))) {
498                log.debug("invalid bit number {} in {}", bit, systemName);
499                return NameValidity.INVALID;
500            }
501        } else {
502            assert type == 'S'; // see earlier decoding
503            // sort on subtype
504            String subtype = matcher.group(4);
505            if (subtype == null) { // no subtype, just look at total
506                if ((bit < 1) || (bit > 224)) {
507                    log.debug("invalid bit number {} in {}", bit, systemName);
508                    return NameValidity.INVALID;
509                } else {
510                    return NameValidity.VALID;
511                }
512            }
513            subtype = subtype.toUpperCase();
514            if (subtype.equals("A")) { // advanced serial occ
515                if ((bit < 1) || (bit > 24)) {
516                    log.debug("invalid bit number {} in {}", bit, systemName);
517                    return NameValidity.INVALID;
518                }
519            } else if (subtype.equals("M")) { // advanced serial motion
520                if ((bit < 1) || (bit > 24)) {
521                    log.debug("invalid bit number {} in  {}", bit, systemName);
522                    return NameValidity.INVALID;
523                }
524            } else if (subtype.equals("S")) { // old serial
525                if ((bit < 1) || (bit > 24)) {
526                    log.debug("invalid bit number {} in {}", bit, systemName);
527                    return NameValidity.INVALID;
528                }
529            } else if (subtype.equals("P")) { // parallel
530                if ((bit < 1) || (bit > 96)) {
531                    log.debug("invalid bit number {} in {}", bit, systemName);
532                    return NameValidity.INVALID;
533                }
534            }
535        }
536
537        // finally, return VALID
538        return NameValidity.VALID;
539    }
540
541    /**
542     * Public static method to validate system name for configuration.
543     *
544     * @param systemName system name to validate.
545     * @param type bean type, S, T or L.
546     * @param tc system connection traffic controller.
547     * @return 'true' if system name has a valid meaning in current configuration,
548     *                else returns 'false'.
549     *
550     */
551    public static boolean validSystemNameConfig(String systemName, char type, SerialTrafficController tc) {
552        String prefix = tc.getSystemConnectionMemo().getSystemPrefix();
553        if (validSystemNameFormat(systemName, type, prefix) != NameValidity.VALID) {
554            // No point in trying if a valid system name format is not present
555            log.debug("invalid system name {}", systemName);
556            return false;
557        }
558        SerialNode node = getNodeFromSystemName(systemName, tc);
559        if (node == null) {
560            log.warn("invalid system name {}; no such node", systemName);
561            // The node indicated by this system address is not present
562            return false;
563        }
564        int bit = getBitFromSystemName(systemName, prefix);
565        if ((type == 'T') || (type == 'L')) {
566            if ((bit <= 0) || (bit > SerialNode.outputBits[node.nodeType])) {
567                // The bit is not valid for this defined Serial node
568                log.warn("invalid system name {}; bad output bit number {} > {}",
569                        systemName, bit, SerialNode.outputBits[node.nodeType]);
570                return false;
571            }
572        } else if (type == 'S') {
573            if ((bit <= 0) || (bit > SerialNode.inputBits[node.nodeType])) {
574                // The bit is not valid for this defined Serial node
575                log.warn("invalid system name {}; bad input bit number {} > {}",
576                        systemName, bit, SerialNode.inputBits[node.nodeType]);
577                return false;
578            }
579        } else {
580            log.error("Invalid type specification in validSystemNameConfig call");
581            return false;
582        }
583        // System name has passed all tests
584        return true;
585    }
586
587    /**
588     * Public static method to convert any format system name for the alternate
589     * format (nnBnn).
590     * <p>
591     * If the supplied system name does not have a valid format,
592     * or if there is no representation in the alternate naming scheme,
593     * an empty string is returned.
594     * @param systemName system name to convert.
595     * @param prefix system prefix.
596     * @return alternate string.
597     */
598    public static String convertSystemNameToAlternate(String systemName, String prefix) {
599        // ensure that input system name has a valid format
600        if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) {
601            // No point in normalizing if a valid system name format is not present
602            return "";
603        }
604
605        Matcher matcher = getAllPattern().matcher(systemName);
606        matcher.matches(); // known to work, just need values
607        // check format
608        if (matcher.group(6) != null) {
609            int num = Integer.parseInt(matcher.group(6));
610            return prefix + matcher.group(1) + (num / 1000) + "B" + (num % 1000);
611        } else {
612            int node = Integer.parseInt(matcher.group(3));
613            int bit = Integer.parseInt(matcher.group(5));
614            return prefix + matcher.group(1) + node + "B" + bit;
615        }
616    }
617
618    /**
619     * Public static method to normalize a system name
620     * <p>
621     * This routine is used to ensure that each system name is uniquely linked
622     * to one bit, by removing extra zeros inserted by the user.
623     * <p>
624     * If the supplied system name does not have a valid format, an empty string
625     * is returned. Otherwise a normalized name is returned in the same format
626     * as the input name.
627     * @param systemName system name to normalize.
628     * @param prefix system prefix.
629     * @return normalized string.
630     */
631    public static String normalizeSystemName(String systemName, String prefix) {
632        // ensure that input system name has a valid format
633        try {
634           if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) {
635               // No point in normalizing if a valid system name format is not present
636               return "";
637           }
638
639           Matcher matcher = getAllPattern().matcher(systemName);
640           matcher.matches(); // known to work, just need values
641
642           // check format
643           if (matcher.group(6) != null) {
644              int num = Integer.parseInt(matcher.group(6));
645              return prefix + matcher.group(1) + num;
646           } else {
647              // there are alternate forms...
648              int offset = typeOffset(matcher.group(4));
649              int node = Integer.parseInt(matcher.group(3));
650              int bit = Integer.parseInt(matcher.group(5));
651              return prefix + matcher.group(1) + (node * 1000 + bit + offset);
652           }
653       } catch(java.lang.StringIndexOutOfBoundsException sobe){
654             throw new IllegalArgumentException("Invalid System Name Format: " + systemName);
655       }
656    }
657
658    private final static Logger log = LoggerFactory.getLogger(SerialAddress.class);
659
660}