001package jmri.jmrix.openlcb;
002
003import java.util.regex.Matcher;
004import java.util.regex.Pattern;
005
006import javax.annotation.CheckReturnValue;
007
008import jmri.NamedBean.BadSystemNameException;
009import jmri.jmrix.can.CanMessage;
010import jmri.jmrix.can.CanReply;
011
012import org.openlcb.EventID;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import javax.annotation.Nonnull;
017
018/**
019 * Utilities for handling OpenLCB event messages as addresses.
020 * <p>
021 * OpenLCB event messages have header information, plus an EventID in the data
022 * part. 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>Full 8 byte ID as pairs separated by "."
029 * </dl>
030 * Note: the {@link #check()} routine does a full, expensive
031 * validity check of the name.  All other operations
032 * assume correctness, diagnose some invalid-format strings, but
033 * may appear to successfully handle other invalid forms.
034 *
035 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2018
036 */
037public class OlcbAddress {
038
039    // groups
040    static final int GROUP_FULL_HEX = 1; // xhhhhhh
041    static final int GROUP_DOT_HEX = 3; // dotted hex form
042
043    static final String singleAddressPattern = "([xX](\\p{XDigit}\\p{XDigit}){1,8})|((\\p{XDigit}?\\p{XDigit}.){7}\\p{XDigit}?\\p{XDigit})";
044
045    private Matcher hCode = null;
046
047    private Matcher getMatcher() {
048        if (hCode == null)  hCode = Pattern.compile("^" + singleAddressPattern + "$").matcher("");
049        return hCode;
050    }
051
052    String aString;
053    int[] aFrame = null;
054    boolean match = false;
055
056    static final int NODEFACTOR = 100000;
057
058    /**
059     * Construct from OlcbEvent.
060     *
061     * @param e the event ID.
062     */
063    public OlcbAddress(EventID e) {
064        byte[] contents = e.getContents();
065        aFrame = new int[contents.length];
066        int i = 0;
067        for (byte b : contents) {
068            aFrame[i++] = b;
069        }
070        aString = toCanonicalString();
071    }
072
073    /**
074     * Construct from string without leading system or type letters
075     * @param s hex coded string of address
076     */
077    public OlcbAddress(String s) {
078        // This is done manually, rather than via regular expressions, for performance reasons.
079
080        // check for leading T, if so convert to numeric form
081        if (s.startsWith("T")) {
082            int from;
083            try {
084                from = Integer.parseInt(s.substring(1));
085            } catch (NumberFormatException e) {
086                from = 0;
087            }
088
089            int DD = (from-1) & 0x3;
090            int aaaaaa = (( (from-1) >> 2)+1 ) & 0x3F;
091            int AAA = ( (from) >> 8) & 0x7;
092            long event = 0x0101020000FF0000L | (AAA << 9) | (aaaaaa << 3) | (DD << 1);
093
094            s = String.format("%016X;%016X", event, event+1);
095            log.debug(" converted to {}", s);
096        }
097
098        aString = s;
099
100        // numeric address string format
101        if (aString.contains(";")) {
102            // multi-part address; leave match false and aFrame null
103        } else if (aString.contains(".")) {
104            // dotted form, 7 dots
105            String[] terms = s.split("\\.");
106            if (terms.length != 8) {
107                log.debug("unexpected number of terms: {}, address is {}", terms.length, s);
108            }
109            int[] tFrame = new int[terms.length];
110            try {
111                for (int i = 0; i < terms.length; i++) {
112                    tFrame[i] = Integer.parseInt(terms[i], 16);
113                }
114            } catch (NumberFormatException ex) { return; } // leaving the string unparsed
115            aFrame = tFrame;
116            match = true;
117        } else {
118            // assume single hex string - drop leading x if present
119            if (aString.startsWith("x")) aString = aString.substring(1);
120            if (aString.startsWith("X")) aString = aString.substring(1);
121            int len = aString.length() / 2;
122            int[] tFrame  = new int[len];
123            // get the frame data
124            try {
125                for (int i = 0; i < len; i++) {
126                    String two = aString.substring(2 * i, 2 * i + 2);
127                    tFrame[i] = Integer.parseInt(two, 16);
128                }
129            } catch (NumberFormatException ex) { return; }  // leaving the string unparsed
130            aFrame = tFrame;
131            match = true;
132        }
133    }
134
135    /**
136     * Two addresses are equal if they result in the same numeric contents
137     */
138    @Override
139    public boolean equals(Object r) {
140        if (r == null) {
141            return false;
142        }
143        if (!(r.getClass().equals(this.getClass()))) {
144            return false;
145        }
146        OlcbAddress opp = (OlcbAddress) r;
147        if (opp.aFrame.length != this.aFrame.length) {
148            return false;
149        }
150        for (int i = 0; i < this.aFrame.length; i++) {
151            if (this.aFrame[i] != opp.aFrame[i]) {
152                return false;
153            }
154        }
155        return true;
156    }
157
158    @Override
159    public int hashCode() {
160        int ret = 0;
161        for (int value : this.aFrame) {
162            ret += value;
163        }
164        return ret;
165    }
166
167    public int compare(@Nonnull OlcbAddress opp) {
168        // if neither matched, just do a lexical sort
169        if (!match && !opp.match) return aString.compareTo(opp.aString);
170
171        // match sorts before non-matched
172        if (match && !opp.match) return -1;
173        if (!match && opp.match) return +1;
174
175        // usual case: comparing on content
176        for (int i = 0; i < Math.min(aFrame.length, opp.aFrame.length); i++) {
177            if (aFrame[i] != opp.aFrame[i]) return Integer.signum(aFrame[i] - opp.aFrame[i]);
178        }
179        // check for different length (shorter sorts first)
180        return Integer.signum(aFrame.length - opp.aFrame.length);
181    }
182
183    public CanMessage makeMessage() {
184        CanMessage c = new CanMessage(aFrame, 0x195B4000);
185        c.setExtended(true);
186        return c;
187    }
188
189    /**
190     * Confirm that the address string (provided earlier) is fully
191     * valid.
192     * <p>
193     * This is an expensive call. It's complete-compliance done
194     * using a regular expression. It can reject some
195     * forms that the code will normally handle OK.
196     * @return true if valid, else false.
197     */
198    public boolean check() {
199        return getMatcher().reset(aString).matches();
200    }
201
202    boolean match(CanReply r) {
203        // check address first
204        if (r.getNumDataElements() != aFrame.length) {
205            return false;
206        }
207        for (int i = 0; i < aFrame.length; i++) {
208            if (aFrame[i] != r.getElement(i)) {
209                return false;
210            }
211        }
212        // check for event message type
213        if (!r.isExtended()) {
214            return false;
215        }
216        return (r.getHeader() & 0x1FFFF000) == 0x195B4000;
217    }
218
219    boolean match(CanMessage r) {
220        // check address first
221        if (r.getNumDataElements() != aFrame.length) {
222            return false;
223        }
224        for (int i = 0; i < aFrame.length; i++) {
225            if (aFrame[i] != r.getElement(i)) {
226                return false;
227            }
228        }
229        // check for event message type
230        if (!r.isExtended()) {
231            return false;
232        }
233        return (r.getHeader() & 0x1FFFF000) == 0x195B4000;
234    }
235
236    /**
237     * Split a string containing one or more addresses into individual ones.
238     *
239     * @return null if entire string can't be parsed.
240     */
241     @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
242        justification = "Documented API, no resources to improve")
243    public OlcbAddress[] split() {
244        // reject strings ending in ";"
245        if (aString.endsWith(";")) {
246            return null;
247        }
248
249        // split string at ";" points
250        String[] pStrings = aString.split(";");
251
252        OlcbAddress[] retval = new OlcbAddress[pStrings.length];
253
254        for (int i = 0; i < pStrings.length; i++) {
255            // check validity of each
256            if (pStrings[i].equals("")) {
257                return null;
258            }
259
260            // too expensive to do full regex check here, as this is used a lot in e.g. sorts
261            // if (!getMatcher().reset(pStrings[i]).matches()) return null;
262
263            retval[i] = new OlcbAddress(pStrings[i]);
264            if (!retval[i].match) {
265                return null;
266            }
267        }
268        return retval;
269    }
270
271    public boolean checkSplit() {
272        return (split() != null);
273    }
274
275    int[] elements() {
276        return aFrame;
277    }
278
279    @Override
280    public String toString() {
281        return aString;
282    }
283
284    public String toCanonicalString() {
285        String retval = "x";
286        for (int value : aFrame) {
287            retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval);
288        }
289        return retval;
290    }
291
292    /**
293     * Provide as dotted pairs.
294     * @return dotted pair form off string.
295     */
296    public String toDottedString() {
297        String retval = "";
298        for (int value : aFrame) {
299            if (!retval.isEmpty())
300                retval += ".";
301            retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval);
302        }
303        return retval;
304    }
305
306    public EventID toEventID() {
307        byte[] b = new byte[8];
308        for (int i = 0; i < Math.min(8, aFrame.length); ++i) b[i] = (byte)aFrame[i];
309        return new EventID(b);
310    }
311
312    /**
313     * Validates Strings for OpenLCB format.
314     * @param name   the system name to validate.
315     * @param locale the locale for a localized exception.
316     * @param prefix system prefix, eg. MT for OpenLcb turnout.
317     * @return the unchanged value of the name parameter.
318     * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format.
319     */
320    @Nonnull
321    public static String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale,
322        @Nonnull String prefix) throws BadSystemNameException {
323        String oAddr = name.substring(prefix.length());
324        OlcbAddress a = new OlcbAddress(oAddr);
325        OlcbAddress[] v = a.split();
326        if (v == null) {
327            throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " to a valid Olcb address");
328        }
329        switch (v.length) {
330            case 1:
331            case 2:
332                break;
333            default:
334                throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Wrong number of events in address: " + name);
335        }
336        return name;
337    }
338
339    /**
340     * Validates 2 part Hardware Address Strings for OpenLCB format.
341     * @param name   the system name to validate.
342     * @param locale the locale for a localized exception.
343     * @param prefix system prefix, eg. MT for OpenLcb turnout.
344     * @return the unchanged value of the name parameter.
345     * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format.
346     */
347    @Nonnull
348    public static String validateSystemNameFormat2Part(@Nonnull String name, @Nonnull java.util.Locale locale,
349        @Nonnull String prefix) throws BadSystemNameException {
350        String oAddr = name.substring(prefix.length());
351        OlcbAddress a = new OlcbAddress(oAddr);
352        OlcbAddress[] v = a.split();
353        if (v == null) {
354            throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " to a valid Olcb address");
355        }
356        if ( v.length == 2 ) {
357            return name;
358        }
359        throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Address requires 2 Events: " + name);
360    }
361
362    /**
363     * See {@link jmri.NamedBean#compareSystemNameSuffix} for background.
364     * This is a common implementation for OpenLCB Sensors and Turnouts
365     * of the comparison method.
366     *
367     * @param suffix1 1st suffix to compare.
368     * @param suffix2 2nd suffix to compare.
369     * @return true if suffixes match, else false.
370     */
371    @CheckReturnValue
372    public static int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2) {
373
374        // extract addresses
375        OlcbAddress[] array1 = new OlcbAddress(suffix1).split();
376        OlcbAddress[] array2 = new OlcbAddress(suffix2).split();
377
378        // compare on content
379        for (int i = 0; i < Math.min(array1.length, array2.length); i++) {
380            int c = array1[i].compare(array2[i]);
381            if (c != 0) return c;
382        }
383        // check for different length (shorter sorts first)
384        return Integer.signum(array1.length - array2.length);
385    }
386
387    private final static Logger log = LoggerFactory.getLogger(OlcbAddress.class);
388
389}
390
391
392