001package jmri.jmrix.secsi;
002
003import java.util.Locale;
004import javax.annotation.Nonnull;
005import jmri.Manager.NameValidity;
006import jmri.NamedBean;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * Utility Class supporting parsing and testing of addresses
012 * <p>
013 * Two address formats are supported:
014 * <ul>
015 *   <li>Vtnnnxxx where:
016 *      <ul>
017 *      <li>V is the system connection prefix with optional index
018 *      <li>t is the type code: 'T' for turnouts, 'S' for sensors,
019 *      and 'L' for lights
020 *      <li>nn is the node address (0-127)
021 *      <li>xxx is a bit number of the input or
022 *      <li>output bit (001-999) nnxxx = (node address x 1000) + bit number.
023 *      </ul>
024 *      Examples: VT2 (node address 0, bit 2), V2S1003 (node address 1,
025 *      bit 3), VL11234 (node address 11, bit234)
026 *   </li>
027 *   <li>VtnnnBxxxx where:
028 *      <ul>
029 *      <li>V is the system connection prefix with optional index
030 *      <li>t is the type code: 'T' for turnouts, 'S' for sensors,
031 *      and 'L' for lights
032 *      <li>nnn is the node address of the input or output bit (0-127)
033 *      <li>xxxx is a bit number of the input or output bit (1-999).
034 *      </ul>
035 *      Examples: VT0B2 (node address 0, bit 2), VS1B3 (node address 1, bit 3),
036 *      VL11B234 (node address 11, bit234)
037 *   </li>
038 * </ul>
039 *
040 * @author Dave Duchamp, Copyright (C) 2004
041 * @author Bob Jacobsen, Copyright (C) 2006, 2007, 2008
042 */
043public class SerialAddress {
044
045    public SerialAddress() {
046    }
047
048    /**
049     * Parse a system name and return the Serial Node.
050     *
051     * @param systemName system name.
052     * @param tc system connection traffic controller.
053     * @return 'NULL' if illegal systemName format or if the node is not
054     * found
055     */
056    public static SerialNode getNodeFromSystemName(String systemName, SerialTrafficController tc) {
057        String prefix = tc.getSystemConnectionMemo().getSystemPrefix();
058        // validate the system Name leader characters
059        if (!(systemName.startsWith(prefix)) || ((systemName.charAt(prefix.length()) != 'L')
060                && (systemName.charAt(prefix.length()) != 'S') && (systemName.charAt(prefix.length()) != 'T'))) {
061            // here if an illegal format 
062            log.error("illegal character in header field of system name: {}", systemName);
063            return (null);
064        }
065        String s = "";
066        boolean noB = true;
067        for (int i = prefix.length() + 1; (i < systemName.length()) && noB; i++) {
068            if (systemName.charAt(i) == 'B') {
069                s = systemName.substring(prefix.length() + 1, i);
070                noB = false;
071            }
072        }
073        int ua;
074        if (noB) {
075            // This is a ViLnnxxx address
076            int num = Integer.parseInt(systemName.substring(prefix.length() + 1));
077            if (num > 0) {
078                ua = num / 1000;
079            } else {
080                log.error("invalid system name: {}", systemName);
081                return (null);
082            }
083        } else {
084            if (s.length() == 0) {
085                log.error("no node address before 'B' in system name: {}", systemName);
086                return (null);
087            } else {
088                try {
089                    ua = Integer.parseInt(s);
090                } catch (Exception e) {
091                    log.error("illegal character in system name: {}", systemName);
092                    return (null);
093                }
094            }
095        }
096        return (SerialNode) tc.getNodeFromAddress(ua);
097    }
098
099    /**
100     * Parse a system name and return the bit number.
101     * <p>
102     * Note: Bits are numbered from 1.
103     *
104     * @param systemName system name.
105     * @param prefix system prefix.
106     * @return the bit number, 0 if an error occurred
107     */
108    public static int getBitFromSystemName(String systemName, String prefix) {
109        // validate the system Name leader characters
110        if (!(systemName.startsWith(prefix)) || ((systemName.charAt(prefix.length()) != 'L')
111                && (systemName.charAt(prefix.length()) != 'S') && (systemName.charAt(prefix.length()) != 'T'))) {
112            // here if an illegal format 
113            log.error("invalid character in header field of system name: {}", systemName);
114            return (0);
115        }
116        // Find the beginning of the bit number field
117        int k = 0;
118        for (int i = prefix.length() + 1; ((i < systemName.length()) && (k == 0)); i++) {
119            if (systemName.charAt(i) == 'B') {
120                k = i + 1;
121            }
122        }
123        int n = 0;
124        if (k == 0) {
125            // here if 'B' not found, name must be ViLnnxxx format
126            int num;
127            try {
128                num = Integer.parseInt(systemName.substring(prefix.length() + 1));
129            } catch (Exception e) {
130                log.error("invalid character in number field of system name: {}", systemName);
131                return (0);
132            }
133            if (num > 0) {
134                n = num - ((num / 1000) * 1000);
135            } else {
136                log.error("invalid system name: {}", systemName);
137                return (0);
138            }
139        } else {
140            // This is a ViLnnBxxxx address
141            try {
142                n = Integer.parseInt(systemName.substring(k, systemName.length()));
143            } catch (Exception e) {
144                log.error("illegal character in bit number field system name: {}", systemName);
145                return (0);
146            }
147        }
148        return (n);
149    }
150
151    /**
152     * Validate system name format. Does not check whether that node is defined
153     * on current system.
154     *
155     * @param systemName the system name
156     * @param prefix     the system connection prefix
157     * @param locale     the Locale for user messages
158     * @return systemName unmodified
159     * @throws IllegalArgumentException if unable to validate systemName
160     */
161    public static String validateSystemNameFormat(String systemName, String prefix, Locale locale) throws IllegalArgumentException {
162        if (!systemName.startsWith(prefix)) {
163            throw new NamedBean.BadSystemNameException(
164                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidPrefix", systemName),
165                    Bundle.getMessage(locale, "InvalidSystemNameInvalidPrefix", systemName));
166        }
167        String address = systemName.substring(prefix.length());
168        int node = 0;
169        int bit = 0;
170        if (!address.contains("B")) {
171            // This is a CLnnnxxx pattern address
172            int num;
173            try {
174                num = Integer.parseInt(address);
175                node = num / 1000;
176                bit = num - ((num / 1000) * 1000);
177            } catch (NumberFormatException ex) {
178                throw new NamedBean.BadSystemNameException(
179                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNotInteger", systemName, prefix),
180                        Bundle.getMessage(locale, "InvalidSystemNameNotInteger", systemName, prefix));
181            }
182        } else {
183            // This is a CLnBxxx pattern address
184            String[] parts = address.split("B");
185            if (parts.length != 2) {
186                if (address.indexOf("B") == 0) {
187                    // no node
188                    throw new NamedBean.BadSystemNameException(
189                            Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNodeInvalid", systemName, ""),
190                            Bundle.getMessage(locale, "InvalidSystemNameNodeInvalid", systemName, ""));
191                } else {
192                    // no bit
193                    throw new NamedBean.BadSystemNameException(
194                            Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBitInvalid", systemName, ""),
195                            Bundle.getMessage(locale, "InvalidSystemNameBitInvalid", systemName, ""));
196                }
197            }
198            try {
199                node = Integer.parseInt(parts[0]);
200            } catch (NumberFormatException ex) {
201                throw new NamedBean.BadSystemNameException(
202                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNodeInvalid", systemName, parts[0]),
203                        Bundle.getMessage(locale, "InvalidSystemNameNodeInvalid", systemName, parts[0]));
204            }
205            try {
206                bit = Integer.parseInt(parts[1]);
207            } catch (NumberFormatException ex) {
208                throw new NamedBean.BadSystemNameException(
209                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBitInvalid", systemName, parts[1]),
210                        Bundle.getMessage(locale, "InvalidSystemNameBitInvalid", systemName, parts[1]));
211            }
212        }
213        if (node < 0 || node >= 128) {
214            throw new NamedBean.BadSystemNameException(
215                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNodeInvalid", systemName, node),
216                    Bundle.getMessage(locale, "InvalidSystemNameNodeInvalid", systemName, node));
217        }
218        if (bit < 1 || bit > 32) {
219            throw new NamedBean.BadSystemNameException(
220                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBitInvalid", systemName, bit),
221                    Bundle.getMessage(locale, "InvalidSystemNameBitInvalid", systemName, bit));
222        }
223        return systemName;
224    }
225
226    /**
227     * Public static method to validate system name format.
228     * <p>
229     * Logging of handled cases no higher than WARN.
230     *
231     * @param systemName system name.
232     * @param type Letter indicating device type expected
233     * @param prefix system prefix.
234     * @return 'true' if system name has a valid format, else returns 'false'
235     */
236    public static NameValidity validSystemNameFormat(@Nonnull String systemName, char type, String prefix) {
237        // validate the system Name leader characters
238        if (!(systemName.startsWith(prefix)) || (systemName.charAt(prefix.length()) != type )) {
239            // here if an illegal format 
240            log.error("illegal character in header field system name: {}", systemName);
241            return NameValidity.INVALID;
242        }
243        // check for the presence of a 'B' to differentiate the two address formats
244        String s = "";
245        int k = 0;
246        boolean noB = true;
247        for (int i = prefix.length() + 1; (i < systemName.length()) && noB; i++) {
248            if (systemName.charAt(i) == 'B') {
249                s = systemName.substring(prefix.length() + 1, i);
250                k = i + 1;
251                noB = false;
252            }
253        }
254        if (noB) {
255            // This is a ViLnnnxxx address
256            int num;
257            try {
258                num = Integer.parseInt(systemName.substring(prefix.length() + 1));
259            } catch (Exception e) {
260                log.debug("invalid character in number field system name: {}", systemName);
261                return NameValidity.INVALID;
262            }
263            if ((num < 1) || (num >= 128000)) {
264                log.debug("number field out of range in system name: {}", systemName);
265                return NameValidity.INVALID;
266            }
267            if ((num - ((num / 1000) * 1000)) == 0) {
268                log.debug("bit number not in range 1 - 999 in system name: {}", systemName);
269                return NameValidity.INVALID;
270            }
271        } else {
272            // This is a ViLnnnBxxxx address - validate the node address field
273            if (s.length() == 0) {
274                log.debug("no node address before 'B' in system name: {}", systemName);
275                return NameValidity.INVALID;
276            }
277            int num;
278            try {
279                num = Integer.parseInt(s);
280            } catch (Exception e) {
281                log.debug("invalid character in node address field of system name: {}",
282                        systemName);
283                return NameValidity.INVALID;
284            }
285            if ((num < 0) || (num >= 128)) {
286                log.debug("node address field out of range in system name: {}", systemName);
287                return NameValidity.INVALID;
288            }
289            // validate the bit number field
290            try {
291                num = Integer.parseInt(systemName.substring(k, systemName.length()));
292            } catch (Exception e) {
293                log.debug("invalid character in bit number field of system name: {}",
294                        systemName);
295                return NameValidity.INVALID;
296            }
297            if ((num < 1) || (num > 32)) {
298                log.debug("bit number field out of range in system name: {}", systemName);
299                return NameValidity.INVALID;
300            }
301        }
302
303        return NameValidity.VALID;
304    }
305
306    /**
307     * Public static method to validate system name for configuration.
308     *
309     * @param systemName system name.
310     * @param type bean type, e.g. S for Sensor, T for Turnout.
311     * @param tc system traffic controller.
312     * @return 'true' if system name has a valid meaning in current configuration, else
313     * returns 'false'
314     */
315    public static boolean validSystemNameConfig(String systemName, char type, SerialTrafficController tc) {
316        String prefix = tc.getSystemConnectionMemo().getSystemPrefix();
317        if (validSystemNameFormat(systemName, type, prefix) != NameValidity.VALID) {
318            // No point in trying if a valid system name format is not present
319            log.warn("{} invalid; bad format", systemName);
320            return false;
321        }
322        SerialNode node = getNodeFromSystemName(systemName, tc);
323        if (node == null) {
324            log.warn("{} invalid; no such node", systemName);
325            // The node indicated by this system address is not present
326            return false;
327        }
328        int bit = getBitFromSystemName(systemName, prefix);
329        if ((type == 'T') || (type == 'L')) {
330            if ((bit <= 0) || (bit > SerialNode.outputBits[node.nodeType])) {
331                // The bit is not valid for this defined Serial node
332                log.warn("{} invalid; bad bit number", systemName);
333                return false;
334            }
335        } else if (type == 'S') {
336            if ((bit <= 0) || (bit > SerialNode.inputBits[node.nodeType])) {
337                // The bit is not valid for this defined Serial node
338                log.warn("{} invalid; bad bit number", systemName);
339                return false;
340            }
341        } else {
342            log.error("Invalid type specification in validSystemNameConfig call");
343            return false;
344        }
345        // System name has passed all tests
346        return true;
347    }
348
349    /**
350     * Public static method to convert one format system name for the alternate
351     * format.
352     *
353     * @param systemName system name.
354     * @param prefix system prefix.
355     * @return an empty string if the supplied system name does not have a valid
356     * format, or if there is no representation in the alternate naming scheme
357     */
358    public static String convertSystemNameToAlternate(String systemName, String prefix) {
359        // ensure that input system name has a valid format
360        if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) {
361            // No point in trying if a valid system name format is not present
362            return "";
363        }
364        String altName = "";
365        // check for the presence of a 'B' to differentiate the two address formats
366        String s = "";
367        int k = 0;
368        boolean noB = true;
369        for (int i = prefix.length() + 1; (i < systemName.length()) && noB; i++) {
370            if (systemName.charAt(i) == 'B') {
371                s = systemName.substring(prefix.length() + 1, i);
372                k = i + 1;
373                noB = false;
374            }
375        }
376        if (noB) {
377            // This is a ViLnnnxxx address, convert to num-style
378            int num = Integer.parseInt(systemName.substring(prefix.length() + 1));
379            int nAddress = num / 1000;
380            int bitNum = num - (nAddress * 1000);
381            altName = prefix + systemName.charAt(prefix.length()) + Integer.toString(nAddress) + "B"
382                    + Integer.toString(bitNum);
383        } else {
384            // This is a VLnnnBxxxx address 
385            int nAddress = Integer.parseInt(s);
386            int bitNum = Integer.parseInt(systemName.substring(k, systemName.length()));
387            if (bitNum > 999) {
388                // bit number is out-of-range for a CLnnnxxx address
389                return "";
390            }
391            altName = prefix + systemName.charAt(prefix.length()) + Integer.toString((nAddress * 1000) + bitNum);
392        }
393        return altName;
394    }
395
396    /**
397     * Normalize a system name.
398     * <p>
399     * This routine is used to ensure that each system name is uniquely linked
400     * to one bit, by removing extra zeros inserted by the user.
401     *
402     * @param systemName system name.
403     * @param prefix system prefix.
404     * @return an empty string if the supplied system name does not have a valid
405     * format. Otherwise a normalized name is returned in the same format
406     * as the input name.
407     */
408    public static String normalizeSystemName(String systemName, String prefix) {
409        if (prefix.length() < 1) {
410            log.error("invalid system name prefix: {}", prefix);
411            return "";
412        }
413        // ensure that input system name has a valid format
414        if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) {
415            // No point in normalizing if a valid system name format is not present
416            return "";
417        }
418        String nName = "";
419        // check for the presence of a 'B' to differentiate the two address formats
420        String s = "";
421        int k = 0;
422        boolean noB = true;
423        for (int i = prefix.length() + 1; (i < systemName.length()) && noB; i++) {
424            if (systemName.charAt(i) == 'B') {
425                s = systemName.substring(prefix.length() + 1, i);
426                k = i + 1;
427                noB = false;
428            }
429        }
430        char type = systemName.charAt(prefix.length());
431        if (noB) {
432            // This is a ViLnnnxxx address
433            int num = Integer.parseInt(systemName.substring(prefix.length() + 1));
434            int nAddress = num / 1000;
435            int bitNum = num - (nAddress * 1000);
436            nName = prefix + type + Integer.toString((nAddress * 1000) + bitNum);
437        } else {
438            // This is a ViLnnnBxxxx address
439            int nAddress = Integer.parseInt(s);
440            int bitNum = Integer.parseInt(systemName.substring(k, systemName.length()));
441            nName = prefix + type + Integer.toString(nAddress) + "B"
442                    + Integer.toString(bitNum);
443        }
444        return nName;
445    }
446
447    private final static Logger log = LoggerFactory.getLogger(SerialAddress.class);
448
449}