001package jmri.jmrix.maple;
002
003import java.util.Locale;
004import jmri.Manager;
005import jmri.Manager.NameValidity;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import javax.annotation.Nonnull;
010
011/**
012 * Utility Class supporting parsing and testing of Maple addresses.
013 * <p>
014 * One address format is supported: Ktxxxx where:
015 * <ul>
016 *   <li>K is (user configurable) system prefix for Maple</li>
017 *   <li>t is the type code: 'T' for turnouts, 'S' for sensors,
018 *   and 'L' for lights</li>
019 *   <li>xxxx is a bit number of the input or output bit (001-9999)</li>
020 * </ul>
021 * Note: with Maple, all panels (nodes) have the
022 * same address space, so there is no node number in the address.
023 *
024 * @author Dave Duchamp, Copyright (C) 2004 - 2009
025 * @author Egbert Broerse, Copyright (C) 2017
026 */
027public class SerialAddress {
028
029    public SerialAddress() {
030    }
031
032    /**
033     * Public static method to parse a Maple system name and return the bit number.
034     * <p>
035     * Notes: Bits are numbered from 1.
036     *
037     * @param systemName system name.
038     * @param prefix system prefix.
039     * @return the bit number, return 0 if an error is found
040     */
041    public static int getBitFromSystemName(String systemName, String prefix) {
042        if (prefix.length() < 1) {
043            return 0;
044        }
045        log.debug("systemName = {}", systemName);
046        log.debug("prefix = {}", prefix);
047        // validate the system Name leader characters
048        if (!(systemName.startsWith(prefix)) || ((systemName.charAt(prefix.length()) != 'L')
049                && (systemName.charAt(prefix.length()) != 'S') && (systemName.charAt(prefix.length()) != 'T'))) {
050            // here if an illegal format
051            log.debug("invalid character in header field of system name: {}", systemName);
052            return (0);
053        }
054        // try to parse remaining system name part
055        int num = 0;
056        try {
057            num = Integer.parseInt(systemName.substring(prefix.length() + 1)); // multi char prefix
058        } catch (NumberFormatException ex) {
059            log.warn("invalid character in number field of system name: {}", systemName);
060            return (0);
061        }
062        if (num <= 0) {
063            log.debug("invalid Maple system name: {}", systemName);
064            return (0);
065        }
066        return (num);
067    }
068
069    /**
070     * Validate the system name.
071     *
072     * @param name the name to validate
073     * @param manager the manager requesting validation
074     * @param locale the locale for user messages
075     * @return the name; unchanged
076     * @throws IllegalArgumentException if name is not valid
077     * @see Manager#validateSystemNameFormat(java.lang.String, java.util.Locale)
078     */
079    public static String validateSystemNameFormat(String name, Manager<?> manager, Locale locale) throws IllegalArgumentException {
080        int max = manager.typeLetter() == 'S' ? 1000 : 8000;
081        return manager.validateIntegerSystemNameFormat(name, 0, max, locale);
082    }
083
084    /**
085     * Public static method to validate system name format.
086     *
087     * @param systemName system name to validate.
088     * @param type bean type, ie S for Sensor, T for Turnout.
089     * @param prefix system prefix.
090     * @return 'true' if system name has a valid format,
091     * else returns 'false'
092     */
093    public static NameValidity validSystemNameFormat(@Nonnull String systemName, char type, String prefix) {
094        // validate the system Name leader characters
095        if (!(systemName.startsWith(prefix)) || (systemName.charAt(prefix.length()) != type )) {
096            // here if an illegal format
097            log.error("invalid character in header field of system name: {}", systemName);
098            return NameValidity.INVALID;
099        }
100        if (systemName.length() <= prefix.length() + 1) {
101            log.warn("missing numerical node address in system name: {}", systemName);
102            return NameValidity.INVALID;
103        }
104        // This is a KLxxxx (or KTxxxx or KSxxxx) address, make sure xxxx is OK
105        int bit = getBitFromSystemName(systemName, prefix);
106        // now check range
107        if ((bit <= 0) || (type == 'S' && bit > 1000) || (bit > 8000)) {
108            log.warn("node address field out of range in system name - {}", systemName);
109            return NameValidity.INVALID;
110        }
111        return NameValidity.VALID;
112    }
113
114    /**
115     * Public static method to validate system name for configuration.
116     *
117     * @param systemName system name to validate.
118     * @param type bean type, ie S for Sensor, T for Turnout.
119     * @param memo system connection.
120     * @return 'true' if system name has a valid meaning in current configuration,
121     * else returns 'false'
122     */
123    public static boolean validSystemNameConfig(String systemName, char type, MapleSystemConnectionMemo memo) {
124        if (validSystemNameFormat(systemName, type, memo.getSystemPrefix()) != NameValidity.VALID) {
125            // No point in trying if a valid system name format is not present
126            return false;
127        }
128        int bit = getBitFromSystemName(systemName, memo.getSystemPrefix());
129        switch (type) {
130            case 'T':
131            case 'L':
132                if ((bit > 0) && (bit <= OutputBits.getNumOutputBits())) {
133                    // The bit is within valid range for this Maple configuration
134                    return true;
135                }
136                break;
137            case 'S':
138                if ((bit > 0) && (bit <= InputBits.getNumInputBits())) {
139                    // The bit is within valid range for this Maple configuration
140                    return true;
141                }
142                break;
143            default:
144                log.error("Invalid type specification in validSystemNameConfig call");
145                return false;
146        }
147        // System name has failed all tests
148        log.warn("Maple hardware address out of range in system name: {}", systemName);
149        return false;
150    }
151
152    /**
153     * Public static method to normalize a system name.
154     * <p>
155     * This routine is used to ensure that each system name is uniquely linked
156     * to a bit, by removing extra zeros inserted by the user.
157     * It's not applied to sensors (whick might be addressed using the KS3:5 format.
158     *
159     * @param systemName systemname to normalize.
160     * @param prefix system prefix.
161     * @return if the supplied system name does not have a valid format, an empty string
162     * is returned. If the address in the system name is not within the legal
163     * maximum range for the type of item (L, T, or S), an empty string is
164     * returned. Otherwise a normalized name is returned in the same format as
165     * the input name.
166     */
167    public static String normalizeSystemName(String systemName, String prefix) {
168        if (prefix.length() < 1) {
169            log.error("invalid system name prefix: {}", prefix);
170            return "";
171        }
172        // ensure that input system name has a valid format
173        // precheck startsWith(prefix) to pass jmri.managers.AbstractSensorMgrTestBase line 95/96 calling "foo" and "bar"
174        if ((systemName.length() < prefix.length() + 1) || (!systemName.startsWith(prefix)) ||
175                (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID)) {
176            // No point in normalizing if a valid system name format is not present
177            return "";
178        }
179        // check if bit number is within the valid range
180        int bitNum = getBitFromSystemName(systemName, prefix);
181        char type = systemName.charAt(prefix.length());
182        if ((bitNum <= 0) || ((type == 'S') && bitNum > 1000) || (bitNum > 8000)) {
183            log.warn("node address field out of range in system name - {}", systemName);
184            return "";
185        }
186        // everything OK, normalize the address
187        String nName = "";
188        nName = prefix + type + bitNum;
189        return nName;
190    }
191
192    /**
193     * Public static method to construct a system name from type character and
194     * bit number.
195     * <p>
196     * This routine returns a system name in the KLxxxx, KTxxxx, or KSxxxx
197     * format. The returned name is normalized.
198     *
199     * @param type bean type, ie S Sensor, T Turnout.
200     * @param bitNum bit number.
201     * @param prefix system prefix.
202     * @return "" (null string) if the supplied type character is not valid,
203     * or the bit number is out of the 1 - 9000 range, and an error message is
204     * logged.
205     */
206    public static String makeSystemName(String type, int bitNum, String prefix) {
207        if (prefix.length() < 1) {
208            log.error("invalid system name prefix: {}", prefix);
209            return "";
210        }
211        String nName = "";
212        // check the type character
213        if ((!type.equals("S")) && (!type.equals("L")) && (!type.equals("T"))) {
214            // here if an illegal type character
215            log.error("illegal type character proposed for system name - {}", type);
216            return (nName);
217        }
218        // check the bit number
219        if ((bitNum < 1) || ((type.equals("S")) && (bitNum > 1000)) || (bitNum > 8000)) {
220            // here if an illegal bit number
221            log.warn("illegal address range proposed for system name - {}", bitNum);
222            return (nName);
223        }
224        // construct the address
225        nName = prefix + type + Integer.toString(bitNum);
226        return (nName);
227    }
228
229    /**
230     * Public static method to test if an output bit is free for assignment.
231     *
232     * @param bitNum bit number.
233     * @param prefix system prefix.
234     * @return "" (null string) if the specified output bit is free for
235     * assignment, else returns the system name of the conflicting assignment.
236     * Test is not performed if the node address or bit number are valid.
237     */
238    public static String isOutputBitFree(int bitNum, String prefix) {
239        if (prefix.length() < 1) {
240            log.error("invalid system name prefix: {}", prefix);
241            return "";
242        }
243        // check the bit number
244        if ((bitNum < 1) || (bitNum > 8000)) {
245            // here if an illegal bit number
246            log.error("illegal bit number in free bit test - {}", bitNum);
247            return ("");
248        }
249        // check for a turnout using the bit
250        jmri.Turnout t = null;
251        String sysName = "";
252        sysName = makeSystemName("T", bitNum, prefix);
253        t = jmri.InstanceManager.turnoutManagerInstance().getBySystemName(sysName);
254        if (t != null) {
255            return (sysName);
256        }
257        // check for a two-bit turnout assigned to the previous bit
258        if (bitNum > 1) {
259            sysName = makeSystemName("T", bitNum - 1, prefix);
260            t = jmri.InstanceManager.turnoutManagerInstance().getBySystemName(sysName);
261            if (t != null) {
262                if (t.getNumberControlBits() == 2) {
263                    // bit is second bit for this Turnout
264                    return (sysName);
265                }
266            }
267        }
268        // check for a light using the bit
269        jmri.Light lgt = null;
270        sysName = makeSystemName("L", bitNum, prefix);
271        lgt = jmri.InstanceManager.lightManagerInstance().getBySystemName(sysName);
272        if (lgt != null) {
273            return (sysName);
274        }
275        // not assigned to a turnout or a light
276        return ("");
277    }
278
279    /**
280     * Public static method to test if an input bit is free for assignment.
281     *
282     * @param bitNum bit number.
283     * @param prefix system prefix.
284     * @return "" (empty string) if the specified input bit is free for
285     * assignment, else returns the system name of the conflicting assignment.
286     * Test is not performed if the node address is illegal or bit number is
287     * valid.
288     */
289    public static String isInputBitFree(int bitNum, String prefix) {
290        if (prefix.length() < 1) {
291            log.error("invalid system name prefix: {}", prefix);
292            return "";
293        }
294        // check the bit number
295        if ((bitNum < 1) || (bitNum > 1000)) {
296            // here if an illegal bit number
297            log.error("illegal bit number in free bit test");
298            return ("");
299        }
300        // check for a sensor using the bit
301        jmri.Sensor s = null;
302        String sysName = "";
303        sysName = makeSystemName("S", bitNum, prefix);
304        s = jmri.InstanceManager.sensorManagerInstance().getBySystemName(sysName);
305        if (s != null) {
306            return (sysName);
307        }
308        // not assigned to a sensor
309        return ("");
310    }
311
312    /**
313     * Public static method to get the user name for a valid system name.
314     *
315     * @param systemName system name.
316     * @param prefix system prefix.
317     * @return "" (empty string) if the system name is not valid or does not exist
318     */
319    public static String getUserNameFromSystemName(String systemName, String prefix) {
320        if (prefix.length() < 1) {
321            log.error("invalid system name prefix: {}", prefix);
322            return "";
323        }
324        // check for a valid system name
325        if ((systemName.length() < (prefix.length() + 2)) || (!systemName.startsWith(prefix))) { // use multi char prefix
326            // not a valid system name
327            return ("");
328        }
329        // check for a sensor
330        if (systemName.charAt(prefix.length()) == 'S') {
331            jmri.Sensor s = null;
332            s = jmri.InstanceManager.sensorManagerInstance().getBySystemName(systemName);
333            if (s != null) {
334                return s.getUserName();
335            } else {
336                return ("");
337            }
338        } // check for a turnout
339        else if (systemName.charAt(prefix.length()) == 'T') {
340            jmri.Turnout t = null;
341            t = jmri.InstanceManager.turnoutManagerInstance().getBySystemName(systemName);
342            if (t != null) {
343                return t.getUserName();
344            } else {
345                return ("");
346            }
347        } // check for a light
348        else if (systemName.charAt(prefix.length()) == 'L') {
349            jmri.Light lgt = null;
350            lgt = jmri.InstanceManager.lightManagerInstance().getBySystemName(systemName);
351            if (lgt != null) {
352                return lgt.getUserName();
353            } else {
354                return ("");
355            }
356        }
357        // not any known sensor, light, or turnout
358        return ("");
359    }
360
361    private final static Logger log = LoggerFactory.getLogger(SerialAddress.class);
362
363}