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