001package jmri.jmrix.can.cbus;
002
003import java.io.IOException;
004import java.util.Collections;
005import java.util.EnumSet;
006import java.util.HashMap;
007import java.util.Map;
008import javax.annotation.CheckForNull;
009import javax.annotation.Nonnull;
010import javax.xml.parsers.DocumentBuilder;
011import javax.xml.parsers.DocumentBuilderFactory;
012import javax.xml.parsers.ParserConfigurationException;
013import jmri.jmrix.AbstractMessage;
014import jmri.jmrix.can.CanFrame;
015import jmri.util.FileUtil;
016import org.w3c.dom.Document;
017import org.w3c.dom.Element;
018import org.w3c.dom.Node;
019import org.w3c.dom.NodeList;
020import org.xml.sax.SAXException;
021
022/**
023 * Methods to decode CBUS opcodes
024 *
025 * https://github.com/MERG-DEV/CBUSlib
026 * @author Andrew Crosland Copyright (C) 2009, 2021
027 * @author Steve Young (C) 2018
028 */
029public class CbusOpCodes {
030
031    private CbusOpCodes() {
032        throw new IllegalStateException("Utility class");
033    }
034
035    /**
036     * Return a string representation of a decoded CBUS Message.
037     * See CbusOpcData.xml for replacement codes.
038     * Used in CBUS Console Log.
039     * @param msg CbusMessage to be decoded Return String decoded message
040     * @return decoded CBUS message
041     */
042    @Nonnull
043    public static final String fullDecode(AbstractMessage msg) {
044        StringBuilder buf = new StringBuilder();
045        // split the format string at each comma
046        String[] fields = getOpc(msg.getElement(0)).getDecode().split(",");
047
048        int idx = 1;
049        for (int i = 0; i < fields.length; i++) {
050            if (fields[i].startsWith("%")) { // replace with bytes from the message
051                int value = 0;
052                int bytes = Integer.parseInt(fields[i].substring(1, 2));
053                for (; bytes > 0; bytes--) {
054                    value = value * 256 + msg.getElement(idx++);
055                }
056                fields[i] = String.valueOf(value);
057            }
058            else if (fields[i].startsWith("^2")) { // replace with loco id from 2 bytes
059                fields[i] = locoFromBytes(msg.getElement(idx++), msg.getElement(idx++) );
060            }
061            else if (fields[i].startsWith("^S")) { // replace with speed string from 1 byte
062                fields[i] = speedDirFromByte(msg.getElement(idx++) );
063            }
064            else if (fields[i].startsWith("$4")) { // replace the 4 bytes with event / node name ( if possible )
065                int nn = (256*msg.getElement(idx++))+(msg.getElement(idx++));
066                int en = (256*msg.getElement(idx++))+(msg.getElement(idx++));
067                fields[i] = new CbusNameService().getEventNodeString(nn,en);
068            }
069            else if (fields[i].startsWith("$2")) { // replace the 2 bytes with node name ( if possible )
070                int nodenum = (256*msg.getElement(idx++))+(msg.getElement(idx++));
071                fields[i] = "NN:" + nodenum + " " + new CbusNameService().getNodeName(nodenum);
072            }
073
074            // concatenat to the result
075            buf.append(fields[i]);
076        }
077
078        // special cases
079        switch (msg.getElement(0)) {
080            case CbusConstants.CBUS_ERR: // extra info for ERR opc
081                buf.append(getCbusErr(msg));
082                break;
083            case CbusConstants.CBUS_CMDERR: // extra info for CMDERR opc
084                if ((msg.getElement(3) > 0 ) && (msg.getElement(3) < 13 )) {
085                    buf.append(Bundle.getMessage("CMDERR"+msg.getElement(3)));
086                }
087                break;
088            case CbusConstants.CBUS_GLOC: // extra info GLOC OPC
089                appendGloc(msg,buf);
090                break;
091            case CbusConstants.CBUS_FCLK:
092                return CbusClockControl.dateFromCanFrame(msg);
093            default:
094                break;
095        }
096        return buf.toString();
097    }
098
099    private static void appendGloc(AbstractMessage msg, StringBuilder buf) {
100        buf.append(" ");
101        if (( ( ( msg.getElement(3) ) & 1 ) == 1 ) // bit 0 is 1
102            && ( ( ( msg.getElement(3) >> 1 ) & 1 ) == 1 )) { // bit 1 is 1
103            buf.append(Bundle.getMessage("invalidFlags"));
104        }
105        else if ( ( ( msg.getElement(3) ) & 1 ) == 1 ){ // bit 0 is 1
106            buf.append(Bundle.getMessage("stealRequest"));
107        }
108        else if ( ( ( msg.getElement(3) >> 1 ) & 1 ) == 1 ){ // bit 1 is 1
109            buf.append(Bundle.getMessage("shareRequest"));
110        }
111        else { // bit 0 and bit 1 are 0
112            buf.append(Bundle.getMessage("standardRequest"));
113        }
114    }
115
116    /**
117     * Return CBUS ERR OPC String.
118     * @param msg CanMessage or CanReply containing the CBUSERR OPC
119     * @return Error String
120     */
121    @Nonnull
122    public static final String getCbusErr(AbstractMessage msg){
123        StringBuilder buf = new StringBuilder();
124        // elements 1 & 2 depend on element 3
125        switch (msg.getElement(3)) {
126            case 1:
127                buf.append(Bundle.getMessage("ERR_LOCO_STACK_FULL"))
128                    .append(locoFromBytes(msg.getElement(1),msg.getElement(2)));
129                break;
130            case 2:
131                buf.append(Bundle.getMessage("ERR_LOCO_ADDRESS_TAKEN",
132                locoFromBytes(msg.getElement(1),msg.getElement(2))));
133                break;
134            case 3:
135                buf.append(Bundle.getMessage("ERR_SESSION_NOT_PRESENT",msg.getElement(1)));
136                break;
137            case 4:
138                buf.append(Bundle.getMessage("ERR_CONSIST_EMPTY"))
139                .append(msg.getElement(1));
140                break;
141            case 5:
142                buf.append(Bundle.getMessage("ERR_LOCO_NOT_FOUND"))
143                .append(msg.getElement(1));
144                break;
145            case 6:
146                buf.append(Bundle.getMessage("ERR_CAN_BUS_ERROR"));
147                break;
148            case 7:
149                buf.append(Bundle.getMessage("ERR_INVALID_REQUEST"))
150                .append(locoFromBytes(msg.getElement(1),msg.getElement(2)));
151                break;
152            case 8:
153                buf.append(Bundle.getMessage("ERR_SESSION_CANCELLED",msg.getElement(1)));
154                break;
155            default:
156                break;
157            }
158        return buf.toString();
159    }
160
161    /**
162     * Return Loco Address String
163     *
164     * @param byteA 1st loco byte
165     * @param byteB 2nd loco byte
166     * @return Loco Address String
167     */
168    @Nonnull
169    public static final String locoFromBytes(int byteA, int byteB ) {
170        return new jmri.DccLocoAddress(((byteA & 0x3f) * 256 + byteB ),
171            ((byteA & 0xc0) != 0)).toString();
172    }
173
174    /**
175     * Get text string of speed / direction.
176     * @param byteA the Speed / Direction byte value.
177     * @return translated String.
178     */
179    @Nonnull
180    public static final String speedDirFromByte(int byteA) {
181        StringBuilder sb = new StringBuilder();
182        sb.append(" ");
183        sb.append(Bundle.getMessage("SpeedCol"));
184        sb.append(" ");
185        sb.append(getSpeedFromByte(byteA));
186        sb.append(" ");
187        sb.append(getDirectionFromByte(byteA));
188        sb.append(" ");
189        return sb.toString();
190    }
191
192    /**
193     * Get loco speed from byte value.
194     * @param speed byte value 0-255 of speed containing direction flag.
195     * @return interpreted String, maybe with EStop localised text.
196     */
197    public static String getSpeedFromByte( int speed ) {
198        int noDirectionSpeed = speed & ~(1 << 7);
199        switch (noDirectionSpeed){
200            case 0:
201                return "0";
202            case 1:
203                return "0 " + Bundle.getMessage("EStop");
204            default:
205                return String.valueOf(noDirectionSpeed-1);
206        }
207    }
208
209    /**
210     * Get localised direction from speed byte.
211     * @param speed 0-255, 0-127 Reverse, else Forwards.
212     * @return localised Forward or Reverse String.
213     */
214    public static String getDirectionFromByte( int speed ) {
215        return Bundle.getMessage( ( speed >> 7 ) == 1 ? "FWD" : "REV");
216    }
217
218    /**
219     * Return a string representation of a decoded CBUS Message
220     *
221     * @param msg CbusMessage to be decoded
222     * @return decoded message after extended frame check
223     */
224    @Nonnull
225    public static final String decode(AbstractMessage msg) {
226        if (msg instanceof CanFrame) {
227            if (!((CanFrame) msg).isExtended()) {
228                return fullDecode(msg);
229            }
230            else {
231                return decodeExtended((CanFrame)msg);
232            }
233        }
234        return "";
235    }
236
237    /**
238     * Return a string representation of a decoded Extended CBUS Message
239     *
240     * @param msg Extended CBUS CAN Frame to be decoded
241     * @return decoded message after extended frame check
242     */
243    @Nonnull
244    public static final String decodeExtended(CanFrame msg) {
245        StringBuilder sb = new StringBuilder(Bundle.getMessage("decodeBootloader"));
246        switch (msg.getHeader()) {
247            case 4: // outgoing Bootload Command are always 8 data
248                int newAddress;
249                int newChecksum;
250                if (msg.getNumDataElements() == 8) {
251                    switch (msg.getElement(5)) { // data payload of bootloader control frames
252                        case CbusConstants.CBUS_BOOT_NOP: // 0
253                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_NOP"));
254                            break;
255                        case CbusConstants.CBUS_BOOT_RESET: // 1
256                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_RESET"));
257                            break;
258                        case CbusConstants.CBUS_BOOT_INIT: // 2
259                            newAddress = ( msg.getElement(2)*65536+msg.getElement(1)*256+msg.getElement(0)  );
260                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_INIT",newAddress));
261                            break;
262                        case CbusConstants.CBUS_BOOT_CHECK: // 3
263                            newChecksum = ( msg.getElement(7)*256+msg.getElement(6)  );
264                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_CHECK",newChecksum));
265                            break;
266                        case CbusConstants.CBUS_BOOT_TEST: // 4
267                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_TEST"));
268                            break;
269                        case CbusConstants.CBUS_BOOT_DEVID: // 5
270                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_DEVID"));
271                            break;
272                        case CbusConstants.CBUS_BOOT_BOOTID: // 6
273                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_BOOTID"));
274                            break;
275                        case CbusConstants.CBUS_BOOT_ENABLES: // 7
276                            sb.append(Bundle.getMessage("decodeCBUS_BOOT_ENABLES"));
277                            break;
278                        default:
279                            break;
280                    }
281                }
282                break;
283            case 5: // outgoing pure data frames are always 8 data
284                if (msg.getNumDataElements() == 8) {
285                    sb.append( Bundle.getMessage("OPC_DA")).append(" :");
286                    msg.appendHexElements(sb);
287                }
288                break;
289            case 0x10000004: // incoming Bootload Reply with variable data
290                switch (msg.getNumDataElements()) {
291                    case 1:     // 1 data
292                        switch (msg.getElement(0)) { // data payload of bootloader control frames
293                            case CbusConstants.CBUS_EXT_BOOT_ERROR: // 0
294                                sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_ERROR"));
295                                break;
296                            case CbusConstants.CBUS_EXT_BOOT_OK: // 1
297                                sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_OK"));
298                                break;
299                            case CbusConstants.CBUS_EXT_BOOTC: // 2
300                                sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOTC"));
301                                break;
302                            case CbusConstants.CBUS_EXT_BOOT_OUT_OF_RANGE: // 3
303                                sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_OUT_OF_RANGE"));
304                                break;
305                            default:
306                                break;
307                        }
308                        break;
309                    case 5:     // 5 data
310                        switch (msg.getElement(0)) { // data payload of bootloader control frames
311                            case CbusConstants.CBUS_EXT_BOOTID: // 6
312                                sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOTID"));
313                                break;
314                            default:
315                                break;
316                        }
317                        break;
318                    case 7:     // 7 data
319                        switch (msg.getElement(0)) { // data payload of bootloader control frames
320                            case CbusConstants.CBUS_EXT_DEVID: // 5
321                                sb.append(Bundle.getMessage("decodeCBUS_EXT_DEVID"));
322                                break;
323                            default:
324                                break;
325                        }
326                        break;
327                    default:    // All other data - not used
328                        break;
329                }
330                break;
331            case 0x10000005: // incoming Bootload Data reply are always 1 data
332                if (msg.getNumDataElements() == 1) {
333                    switch (msg.getElement(0)) { // data payload of bootloader control frames
334                        case CbusConstants.CBUS_EXT_BOOT_ERROR: // 0
335                            sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_DATA_ERROR"));
336                            break;
337                        case CbusConstants.CBUS_EXT_BOOT_OK: // 1
338                            sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_DATA_OK"));
339                            break;
340                        case CbusConstants.CBUS_EXT_BOOT_OUT_OF_RANGE: // 3
341                            sb.append(Bundle.getMessage("decodeCBUS_EXT_BOOT_OUT_OF_RANGE"));
342                            break;
343                        default:
344                            break;
345                    }
346                }
347                break;
348            default:
349                break;
350        }
351        if (sb.toString().equals(Bundle.getMessage("decodeBootloader"))){
352            return(Bundle.getMessage("decodeUnknownExtended"));
353        }
354        return sb.toString();
355    }
356
357    /**
358     * Return a string representation of a decoded CBUS OPC
359     *
360     * @param msg CbusMessage to be decoded Return String decoded OPC
361     * @return decoded CBUS OPC, eg. "RTON" or "ACON2", else Reserved string.
362     */
363    @Nonnull
364    public static final String decodeopcNonExtended(AbstractMessage msg) {
365        var mapString = getOpcOrNull(msg.getElement(0));
366        return ( mapString != null ? mapString.getName() :
367            Bundle.getMessage("OPC_RESERVED") + " " +
368                msg.toMonitorString().toUpperCase()); // uppercase to emphasise hex value
369    }
370
371    /**
372     * Return a string OPC of a CBUS Message
373     *
374     * @param msg CbusMessage
375     * @return decoded CBUS OPC, eg. "RTON" or "ACON2", else Reserved string.
376     * Empty String for Extended Frames as no OPC concept.
377     */
378    @Nonnull
379    public static final String decodeopc(AbstractMessage msg) {
380        if ((msg instanceof CanFrame) &&  !((CanFrame) msg).extendedOrRtr()) {
381            return decodeopcNonExtended(msg);
382        }
383        else {
384            return "";
385        }
386    }
387
388    /**
389     * Test if CBUS OpCode is known to JMRI.
390     * Performs Extended / RTR Frame check.
391     *
392     * @param msg CanReply or CanMessage
393     * @return True if opcode is known
394     */
395    public static final boolean isKnownOpc(AbstractMessage msg){
396        return ( getOpcOrNull(msg.getElement(0))!=null
397                && ( msg instanceof CanFrame)
398                && (!((CanFrame) msg).extendedOrRtr()));
399    }
400
401    /**
402     * Test if CBUS OpCode represents a CBUS event.
403     * <p>
404     * Defined in the CBUS Developer Manual as accessory commands.
405     * Excludes fast clock.
406     * <p>
407     * ACON, ACOF, AREQ, ARON, AROF, ASON, ASOF, ASRQ, ARSON, ARSOF,
408     * ACON1, ACOF1, ARON1, AROF1, ASON1, ASOF1, ARSON1, ARSOF1,
409     * ACON2, ACOF2, ARON2, AROF2, ASON2, ASOF2, ARSON2, ARSOF2
410     *
411     * @param opc CBUS op code
412     * @return True if opcode represents an event
413     */
414    public static final boolean isEvent(int opc) {
415        return getOpc(opc).hasFilter(CbusFilterType.CFEVENT);
416    }
417
418    /**
419     * Test if CBUS opcode represents a JMRI event table event.
420     * Event codes excluding request codes + fastclock.
421     * <p>
422     * ACON, ACOF, ARON, AROF, ASON, ASOF, ARSON, ARSOF,
423     * ACON1, ACOF1, ARON1, AROF1, ASON1, ASOF1, ARSON1, ARSOF1,
424     * ACON2, ACOF2, ARON2, AROF2, ASON2, ASOF2, ARSON2, ARSOF2,
425     * ACON3, ACOF3, ARON3, AROF3, ASON3, ASOF3, ARSON3, ARSOF3,
426     *
427     * @param opc CBUS op code
428     * @return True if opcode represents an event
429     */
430    public static final boolean isEventNotRequest(int opc) {
431        CbusOpc cbusOpc = getOpc(opc);
432        return ( cbusOpc.hasFilter(CbusFilterType.CFEVENT)
433            && !cbusOpc.hasFilter(CbusFilterType.CFREQUEST));
434    }
435
436    /**
437     * Test if CBUS opcode represents a DCC Command Station Message
438     * <p>
439     * TOF, TON, ESTOP, RTOF, RTON, RESTP, KLOC, QLOC, DKEEP,
440     * RLOC, QCON, ALOC, STMOD, PCON, KCON, DSPD, DFLG, DFNON, DFNOF, SSTAT,
441     * DFUN, GLOC, ERR, RDCC3, WCVO, WCVB, QCVS, PCVS, RDCC4, WCVS, VCVS,
442     * RDCC5, WCVOA, RDCC6, PLOC, STAT, RSTAT
443     *
444     * @param opc CBUS op code
445     * @return True if opcode represents a dcc command
446     */
447    public static final boolean isDcc(int opc) {
448        return getOpc(opc).hasFilter(CbusFilterType.CFCS);
449    }
450
451    /**
452     * Test if CBUS opcode represents an on event.
453     * <p>
454     * ACON, ARON, ASON, ARSON
455     * ACON1, ARON1, ASON1, ARSON1
456     * ACON2, ARON2, ASON2, ARSON2
457     * ACON3, ARON3, ASON3, ARSON3
458     *
459     * @param opc CBUS op code
460     * @return True if opcode represents an on event
461     */
462    public static final boolean isOnEvent(int opc) {
463        return getOpc(opc).hasFilter(CbusFilterType.CFON);
464    }
465
466    /**
467     * Test if CBUS opcode represents an event request.
468     * Excludes node data requests RQDAT + RQDDS.
469     * AREQ, ASRQ
470     *
471     * @param opc CBUS op code
472     * @return True if opcode represents a short event
473     */
474    public static final boolean isEventRequest(int opc) {
475        return getOpc(opc).hasFilter(CbusFilterType.CFREQUEST);
476    }
477
478    /**
479     * Test if CBUS opcode represents a short event.
480     * <p>
481     * ASON, ASOF, ASRQ, ARSON, ARSOF
482     * ASON1, ASOF1, ARSON1, ARSOF1
483     * ASON2, ASOF2, ARSON2, ARSOF2
484     * ASON3, ASOF3, ARSON3, ARSOF3
485     *
486     * @param opc CBUS op code
487     * @return True if opcode represents a short event
488     */
489    public static final boolean isShortEvent(int opc) {
490        return getOpc(opc).hasFilter(CbusFilterType.CFSHORT);
491    }
492
493    /**
494     * Get the filters for a CBUS OpCode.
495     *
496     * @param opc CBUS op code
497     * @return Filter EnumSet, a new copy which the caller may modify.
498     */
499    @Nonnull
500    public static final EnumSet<CbusFilterType> getOpcFilters(int opc){
501        return getOpc(opc).getFilters();
502    }
503
504    /**
505     * Get the Name of a CBUS OpCode.
506     *
507     * @param opc CBUS op code
508     * @return Name if known, else empty String.
509     */
510    @Nonnull
511    public static final String getOpcName(int opc){
512        CbusOpc cbusOpc = getOpcOrNull(opc);
513        if ( cbusOpc!=null){
514            return cbusOpc.getName();
515        }
516        return "";
517    }
518
519    /**
520     * Get the Minimum Priority for a CBUS OpCode.
521     *
522     * @param opc CBUS op code
523     * @return Minimum Priority
524     */
525    public static final int getOpcMinPriority(int opc){
526        return getOpc(opc).getMinPri();
527    }
528
529    private static final Map<Integer, CbusOpc> MAP = createMainMap();
530
531    /**
532     * Array form of MAP for fast lookup by int OPC without boxing,
533     * used on the incoming frame path by every Sensor, Turnout and Light.
534     */
535    private static final CbusOpc[] OPC_ARRAY = createOpcArray();
536
537    /**
538     * Default OPC returned for unknown OPCs.
539     * Created once, after MAP so that the Bundle is available.
540     */
541    private static final CbusOpc DEFAULT_OPC = createDefaultOpc();
542
543    private static CbusOpc[] createOpcArray() {
544        CbusOpc[] result = new CbusOpc[256];
545        MAP.forEach((key, value) -> {
546            if (key >= 0 && key < result.length) {
547                result[key] = value;
548            }
549        });
550        return result;
551    }
552
553    /**
554     * Get the CbusOpc for an OPC.
555     *
556     * @param opc CBUS op code
557     * @return the CbusOpc, or null if the OPC is unknown or out of range.
558     */
559    @CheckForNull
560    private static CbusOpc getOpcOrNull(int opc) {
561        return (opc >= 0 && opc < OPC_ARRAY.length) ? OPC_ARRAY[opc] : null;
562    }
563
564    /**
565     * Get the CbusOpc for an OPC.
566     *
567     * @param opc CBUS op code
568     * @return the CbusOpc, or the default unknown CbusOpc if OPC is unknown.
569     */
570    @Nonnull
571    private static CbusOpc getOpc(int opc) {
572        CbusOpc cbusOpc = getOpcOrNull(opc);
573        return ( cbusOpc != null ? cbusOpc : DEFAULT_OPC );
574    }
575
576    private static Map<Integer, CbusOpc> createMainMap()  {
577        Map<Integer, CbusOpc> result = new HashMap<>(150); // 134 as of April 2022
578        try {
579            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
580            // disable DOCTYPE declaration & setXIncludeAware to reduce Sonar security warnings
581            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
582            factory.setXIncludeAware(false);
583            DocumentBuilder builder = factory.newDocumentBuilder();
584            Document document = builder.parse(FileUtil.getFile("program:xml/cbus/CbusOpcData.xml"));
585            document.getDocumentElement().normalize();
586
587            //Get all opcs
588            NodeList nList = document.getElementsByTagName("CbusOpc");
589            for (int temp = 0; temp < nList.getLength(); temp++) {
590                Node node = nList.item(temp);
591                if (node.getNodeType() == Node.ELEMENT_NODE) {
592                    Element eElement = (Element) node;
593
594                    // split the format string at each comma
595                    String[] fields = eElement.getAttribute("decode").split("~");
596                    StringBuilder fieldbuf = new StringBuilder();
597
598                    for (String field : fields) {
599                        if (field.startsWith("OPC_")) {
600                            field = Bundle.getMessage(field);
601                        }
602                        fieldbuf.append(field);
603                    }
604
605                    EnumSet<CbusFilterType> filterSet = EnumSet.noneOf(CbusFilterType.class);
606                    String[] filters = eElement.getAttribute("filter").split(",");
607                    for (String filter : filters) {
608                        CbusFilterType tmp = CbusFilterType.valueOf(filter);
609                        filterSet.add(tmp);
610                    }
611
612                    result.put(jmri.util.StringUtil.getByte(0,eElement.getAttribute("hex")),
613                        new CbusOpc(
614                            Integer.parseInt(eElement.getAttribute("minPri")),
615                            eElement.getAttribute("name"),
616                            fieldbuf.toString(),
617                            filterSet
618                        ));
619                }
620            }
621        } catch (ParserConfigurationException | SAXException | IOException ex) {
622            log.error("Error importing xml file", ex);
623        }
624        return Collections.unmodifiableMap(result);
625    }
626
627    /**
628     * Create a CBUS OpCode with default unknown values.
629     *
630     * @return Default OPC
631     */
632    @Nonnull
633    private static CbusOpc createDefaultOpc(){
634        return new CbusOpc(
635            3,Bundle.getMessage("OPC_RESERVED"),"",
636            EnumSet.of(CbusFilterType.CFMISC,CbusFilterType.CFUNKNOWN));
637    }
638
639    private static class CbusOpc {
640        private final int _minPri;
641        private final String _name;
642        private final String _decodeText;
643        private final EnumSet<CbusFilterType> _filterMap;
644
645        private CbusOpc(int minPri, String name, String decode, EnumSet<CbusFilterType> filterMap){
646            _minPri = minPri;
647            _name = name;
648            _decodeText = decode;
649            _filterMap = filterMap;
650        }
651
652        private int getMinPri(){
653            return _minPri;
654        }
655
656        private String getName(){
657            return _name;
658        }
659
660        private String getDecode(){
661            return _decodeText;
662        }
663
664        /**
665         * Get a copy of the Filters for the OPC.
666         * @return new EnumSet copy.
667         */
668        private EnumSet<CbusFilterType> getFilters(){
669            return EnumSet.copyOf(_filterMap);
670        }
671
672        /**
673         * Test if the OPC has a Filter, without copying the Filter set.
674         * @param filter the Filter to test for.
675         * @return true if the OPC has the Filter, else false.
676         */
677        private boolean hasFilter(CbusFilterType filter){
678            return _filterMap.contains(filter);
679        }
680    }
681
682    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusOpCodes.class);
683
684}