001package jmri.jmrix.can.cbus;
002
003import java.util.Arrays;
004import java.util.Locale;
005import java.util.regex.Matcher;
006import java.util.regex.Pattern;
007
008import javax.annotation.Nonnull;
009
010import jmri.JmriException;
011import jmri.jmrix.can.CanMessage;
012import jmri.jmrix.can.CanReply;
013import jmri.util.StringUtil;
014
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Utilities for handling CBUS addresses.
020 * <p>
021 * CBUS frames have a one byte command and length, optionally followed by data
022 * bytes. JMRI maps these into address strings.
023 * <p>
024 * Forms:
025 * <dl>
026 * <dt>Full hex string preceeded by "X"<dd>Needs to be pairs of digits: 0123,
027 * not 123
028 * <dt>+/-ddd<dd>ddd is node*100,000 (a.k.a NODEFACTOR) + event
029 * <dt>+/-nNNNeEEE<dd>where NNN is a node number and EEE is an event number
030 * </dl>
031 * If ddd &lt; 65536 then the CBUS address is taken to represent a short event.
032 *
033 * @author Bob Jacobsen Copyright (C) 2008
034 * @author Andrew Crosland Copyright (C) 2011
035 */
036public class CbusAddress {
037
038    // groups
039    // 1: +ddd/-ddd  where ddd is node*NODEFACTOR + event
040    // 2: the +/- from that
041    // 3: xhhhhhh
042    // 5: NE form
043    // 6: the +/- from that
044    // 7: optional "N"
045    // 8: node number
046    // 9: event number
047    static final String SINGLE_ADDRESS_PATTERN = "((\\+|-)?\\d++)|([Xx](\\p{XDigit}\\p{XDigit}){1,8})|((\\+|-)?([Nn])?(\\d++)[Ee](\\d++))";
048
049    private final Matcher hCode = Pattern.compile("^" + SINGLE_ADDRESS_PATTERN + "$").matcher("");
050
051    private String aString = null;
052    protected int[] aFrame = null;
053    private boolean match = false;
054
055    static final int NODEFACTOR = 100000;
056
057    /**
058     * Construct from string without leading system or type letters.
059     * @param s CBUS Hardware Address format
060     */
061    public CbusAddress(String s) {
062        aString = s;
063        // now parse
064        match = hCode.reset(aString).matches();
065        if (match) {
066            if (hCode.group(1) != null) {
067                // hit on +/-ddd, sign is optional and an unsigned value is treated as +
068                String sign = hCode.group(2);
069                String digits = (sign == null) ? hCode.group(1) : hCode.group(1).substring(1);
070
071                // parse as long then range check to avoid int overflow for node >= 21475
072                long n = parseNonNegative(digits);
073                long node = n / NODEFACTOR;
074                long event = n % NODEFACTOR;
075
076                if (n < 0 || !setEventFrame(node, event)) {
077                    log.debug("CBUS address {} out of range, node and event must be 0-65535", s);
078                    match = false;
079                } else if ("-".equals(sign)) {
080                    aFrame[0] = (node > 0) ? CbusConstants.CBUS_ACOF : CbusConstants.CBUS_ASOF;
081                } else { // + or unsigned
082                    aFrame[0] = (node > 0) ? CbusConstants.CBUS_ACON : CbusConstants.CBUS_ASON;
083                }
084            } else if (hCode.group(3) != null) {
085                // hit on hex form
086                String l = hCode.group(3);
087                int len = (l.length() - 1) / 2;
088                aFrame = new int[len];
089                // get the frame data
090                for (int i = 0; i < len; i++) {
091                    String two = l.substring(1 + 2 * i, 1 + 2 * i + 2);
092                    aFrame[i] = Integer.parseInt(two, 16);
093                }
094            } else if (hCode.group(5) != null) {
095                // hit on EN form
096                long node = parseNonNegative(hCode.group(8));
097                long event = parseNonNegative(hCode.group(9));
098
099                if (!setEventFrame(node, event)) {
100                    log.debug("CBUS address {} out of range, node and event must be 0-65535", s);
101                    match = false;
102                } else if ("-".equals(hCode.group(6))) {
103                    aFrame[0] = CbusConstants.CBUS_ACOF;
104                } else { // + or unsigned
105                    aFrame[0] = CbusConstants.CBUS_ACON;
106                }
107            }
108        } else {
109            // no match, leave match false and aFrame null
110        }
111    }
112
113    /**
114     * Parse a String of decimal digits.
115     * @param digits the digits to parse
116     * @return the value, or -1 if the value can not be parsed ( eg. too large ).
117     */
118    private static long parseNonNegative(String digits) {
119        try {
120            return Long.parseLong(digits);
121        } catch (NumberFormatException ex) {
122            return -1;
123        }
124    }
125
126    /**
127     * Create the 5 element aFrame for a long or short event, element 0 is left as 0.
128     * @param node the node number
129     * @param event the event number
130     * @return true if node and event are within 0-65535 and aFrame was set, else false.
131     */
132    private boolean setEventFrame(long node, long event) {
133        if (node < 0 || node > 0xffff || event < 0 || event > 0xffff) {
134            return false;
135        }
136        aFrame = new int[5];
137        aFrame[4] = (int) (event & 0xff);
138        aFrame[3] = (int) ((event >> 8) & 0xff);
139        aFrame[2] = (int) (node & 0xff);
140        aFrame[1] = (int) ((node >> 8) & 0xff);
141        return true;
142    }
143
144    /**
145     * Two addresses are equal if they result in the same numeric contents.
146     * Two invalid addresses ( which have no numeric contents ) are also equal.
147     * @param r The other CbusAddress to compare
148     */
149    @Override
150    public boolean equals(Object r) {
151        if (r == null) {
152            return false;
153        }
154        if (!(r.getClass().equals(this.getClass()))) {
155            return false;
156        }
157        CbusAddress opp = (CbusAddress) r;
158        return Arrays.equals(this.aFrame, opp.aFrame);
159    }
160
161    @Override
162    public int hashCode() {
163        return Arrays.hashCode(aFrame); // 0 for an invalid address
164    }
165
166    public CanMessage makeMessage(int header) {
167        return new CanMessage(aFrame, header);
168    }
169
170    /**
171     * Check if the address String was successfully parsed into a Frame.
172     * @return true if valid, else false.
173     */
174    public boolean check() {
175        return match;
176    }
177
178    /**
179     * Does the CbusAddress match.
180     *
181     * @param r CanReply or CanMessage being tested
182     * @return true if matches
183     */
184    public boolean match(jmri.jmrix.AbstractMessage r) {
185        if (r.getNumDataElements() != aFrame.length) {
186            return false;
187        }
188        if (CbusMessage.isShort(r)) {
189            // Skip node number for short events
190            if (aFrame[0] != r.getElement(0)) {
191                return false;
192            }
193            for (int i = 3; i < aFrame.length; i++) {
194                if (aFrame[i] != r.getElement(i)) {
195                    return false;
196                }
197            }
198        } else {
199            for (int i = 0; i < aFrame.length; i++) {
200                if (aFrame[i] != r.getElement(i)) {
201                    return false;
202                }
203            }
204        }
205        return true;
206    }
207
208    /**
209     * Does the CbusAddress match a CanReply event request.
210     *
211     * @param r CanReply being tested
212     * @return true if matches
213     */
214    public boolean matchRequest(CanReply r) {
215        if (r.getNumDataElements() != aFrame.length) {
216            return false;
217        }
218        if (CbusMessage.isShort(r)) {
219            // Skip node number for short events
220            if (CbusConstants.CBUS_ASRQ != r.getElement(0)) {
221                return false;
222            }
223            for (int i = 3; i < aFrame.length; i++) {
224                if (aFrame[i] != r.getElement(i)) {
225                    return false;
226                }
227            }
228        } else {
229            if (CbusConstants.CBUS_AREQ != r.getElement(0)) {
230                return false;
231            }
232            for (int i = 1; i < aFrame.length; i++) {
233                if (aFrame[i] != r.getElement(i)) {
234                    return false;
235                }
236            }
237        }
238        return true;
239    }
240
241    /**
242     * Split a string containing one or more addresses into individual ones.
243     *
244     * @return 0 length if entire string can't be parsed.
245     */
246    @Nonnull
247    public CbusAddress[] split() {
248        // reject strings ending in ";"
249        if (aString.endsWith(";")) {
250            return new CbusAddress[0];
251        }
252
253        // split string at ";" points
254        String[] pStrings = aString.split(";");
255
256        CbusAddress[] retval = new CbusAddress[pStrings.length];
257
258        for (int i = 0; i < pStrings.length; i++) {
259            // check validity of each
260            if (pStrings[i].isEmpty()) {
261                return new CbusAddress[0];
262            }
263            if (!hCode.reset(pStrings[i]).matches()) {
264                return new CbusAddress[0];
265            }
266            retval[i] = new CbusAddress(pStrings[i]);
267            if (!retval[i].check()) { // matched the pattern but out of range
268                return new CbusAddress[0];
269            }
270        }
271        return retval;
272    }
273
274    /**
275     * Increments a CBUS address by 1 eg +123 to +124 eg -N123E456 to -N123E457
276     *
277     * @param testAddr initial CbusAddress String, eg -N123E456
278     * @return incremented address. 
279     * @throws jmri.JmriException if unable to make the address
280     */
281    @Nonnull
282    public static String getIncrement(@Nonnull String testAddr) throws JmriException{
283        log.debug("testing address {}", testAddr);
284        validateSysName(testAddr);
285        CbusAddress a = new CbusAddress(testAddr);
286        CbusAddress[] v = a.split();
287        String newString;
288        switch (v.length) {
289            case 2:
290                int lasta = StringUtil.getLastIntFromString(v[0].toString());
291                int lastb = StringUtil.getLastIntFromString(v[1].toString());
292                StringBuilder sb = new StringBuilder();
293                sb.append(StringUtil.replaceLast(v[0].toString(), String.valueOf(lasta), String.valueOf(lasta + 1)));
294                sb.append(";");
295                sb.append(StringUtil.replaceLast(v[1].toString(), String.valueOf(lastb), String.valueOf(lastb + 1)));
296                newString = sb.toString();
297                break;
298            case 1:
299                // get last part and increment
300                int last = StringUtil.getLastIntFromString(v[0].toString());
301                newString = StringUtil.replaceLast(v[0].toString(), String.valueOf(last), String.valueOf(last + 1));
302                break;
303            default:
304                throw new JmriException("Unable to increment " + testAddr);
305        }
306        try {
307            return validateSysName(newString);
308        } catch (IllegalArgumentException e) {
309            throw new JmriException("Unable to increment " + testAddr + " " + e.getMessage());
310        }
311    }
312
313    // not A-F, N or X
314    private static final String[] invalidChars = {
315        "G","H","I","J","K","L","M","S","T","U","V","W","Y","Z",
316        "?",":","++","--",",","*","NN","XX"};
317    
318    /**
319     * Validate a CBUS hardware address validation.
320     *
321     * @param address the hardware address to check, excluding both system prefix and type letter.
322     * @return same address if all OK.
323     * @throws IllegalArgumentException when address is not validated.
324     * or contains too many parts
325     */
326    public static String validateSysName(String address) throws IllegalArgumentException {
327
328        if (address == null || address.isEmpty()) {
329            throw new IllegalArgumentException("No Address passed ");
330        }
331        // address=address.toUpperCase().trim();
332        for (String s : invalidChars) {
333            if (address.contains(s)) {
334                throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemNameCharacter",address,s);
335            }
336        }
337        
338        if (address.endsWith(";")) {
339            throw new IllegalArgumentException("Should not end with ; " + address);
340        }
341
342        // 1st set of switch cases enable strings to pass as a CbusAddress if unsigned
343        String[] addressArray = address.split(";");
344        switch (addressArray.length) {
345            case 1:
346                address = checkPartOfName(addressArray[0], "+");
347                // adds sign when addressArray[0] is unsigned int (eg. "4" address is updated to "+4")
348                break;
349            case 2:
350                address = checkPartOfName(addressArray[0], "+") + ";" + checkPartOfName(addressArray[1], "-");
351                break;
352            default:
353                log.debug("validateSysName switch 1 found > 2 events");
354                throw new IllegalArgumentException("Unable to convert Address: " + address);
355        }
356
357        CbusAddress a = new CbusAddress(address);
358        CbusAddress[] v = a.split();
359        switch (v.length) {
360            case 1:
361                if (address.startsWith("+") || address.startsWith("-")) {
362                    break;
363                }
364                long unsigned;
365                try {
366                    unsigned = Long.parseLong(address); // accept unsigned integer
367                    if (unsigned > 100000) {
368                        break;
369                    }
370                } catch (NumberFormatException ex) {
371                    log.debug("Unable to convert {} into Cbus format +nn", address);
372                }
373                throw new IllegalArgumentException("can't make 2nd event from address " + address);
374            case 2:
375                break;
376            default:
377                log.debug("validateSysName switch 2 found > 2 events");
378                throw new IllegalArgumentException("Wrong number of events in address: " + address);
379        }
380        return address;
381    }
382
383    /**
384     * Check part of a CbusAddress. Will add "+" or "-" if not present in part.
385     *
386     * @param testpart    string part of Cbus address to check, will accept
387     *                    unsigned single integer
388     * @param plusOrMinus character to add in front if not yet present
389     * @return part of CBUS address including + or - (on off) sign
390     */
391    private static String checkPartOfName(String testpart, String plusOrMinus) {
392        long unsigned = 0;
393        String part = testpart;
394        try {
395            unsigned = Long.parseLong(part); // long so node numbers >= 21475 do not overflow
396            log.debug("part {} is integer {}", part, unsigned);
397            if ((part.charAt(0) != '+') && (part.charAt(0) != '-')) {
398                if (unsigned > 0 && unsigned < 65536) {
399                    part = plusOrMinus + part;
400                } else if (unsigned == 0) {
401                    // an unsigned 0 is not accepted, must be +0 or -0
402                    throw new IllegalArgumentException("Unsigned 0 event requires + or - : " + part);
403                }
404            }
405            if (unsigned > 65535 && unsigned < 100000) {
406                throw new IllegalArgumentException("On Too big for an event, too low for node + event : " + part);
407            }
408            if (unsigned < -65535 && unsigned > -100000) {
409                throw new IllegalArgumentException("Off Too big for an event, too low for node + event : " + part);
410            }
411        } catch (NumberFormatException ex) {
412            log.debug("Unable to convert {} into Cbus format +nn", part);
413        }
414        if (unsigned == 0) {
415            // so it's a string.
416            // ignoring anything starting with x or X as it may be a HEX value
417            // which is checked by core CbusAddress
418            try {
419                if (part.toUpperCase().charAt(0) != 'X') {
420                    log.debug("not an int or hex {}", part);
421
422                    // it's got a string in somewhere, start by checking event number
423                    int lasta = StringUtil.getLastIntFromString(part);
424                    log.debug("last string {}", lasta);
425                    if (lasta > 65535) {
426                        throw new IllegalArgumentException("Event Too Large in address: " + part);
427                    }
428                    int firsta = StringUtil.getFirstIntFromString(part);
429                    log.debug("first string {}", firsta);
430                    if (firsta > 65535) {
431                        throw new IllegalArgumentException("Node Too Large in address: " + part);
432                    }
433                }
434            } catch (StringIndexOutOfBoundsException ex) {
435                throw new IllegalArgumentException("Address Too Short? : " + part);
436            }
437        }
438        return part;
439    }
440
441    /**
442     * Used in Testing.
443     * @return true if split length is 1 or 2, else false.
444     */
445    public boolean checkSplit() {
446        switch (split().length) {
447            case 1:
448            case 2:
449                return true;
450            default:
451                return false;
452        }
453    }
454
455    int[] elements() {
456        return java.util.Arrays.copyOf(aFrame, aFrame.length);
457    }
458
459    /**
460     * eg. X9801D203A4 or +N123E456
461     */
462    @Override
463    public String toString() {
464        return aString;
465    }
466
467    /**
468     * eg.x9801D203A4 or x90007B01C8
469     * @return x followed by Can Frame Data
470     */
471    public String toCanonicalString() {
472        String retval = "x";
473        for (int i = 0; i < aFrame.length; i++) {
474            retval = jmri.util.StringUtil.appendTwoHexFromInt(aFrame[i], retval);
475        }
476        return retval;
477    }
478
479    private static final Logger log = LoggerFactory.getLogger(CbusAddress.class);
480
481}