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