001package jmri.jmrit.symbolicprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import static java.nio.charset.Charset.defaultCharset;
006import static java.nio.charset.Charset.isSupported;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.beans.PropertyChangeEvent;
011import java.beans.PropertyChangeListener;
012import java.util.Arrays;
013import java.util.List;
014import java.util.Objects;
015import java.util.Vector;
016import javax.swing.JButton;
017import javax.swing.JFrame;
018import javax.swing.JLabel;
019import javax.swing.JTextField;
020import javax.swing.table.AbstractTableModel;
021
022import jmri.AddressedProgrammer;
023import jmri.jmrit.decoderdefn.DecoderFile;
024import jmri.util.CvUtil;
025import jmri.util.jdom.LocaleSelector;
026import jmri.util.swing.JmriJOptionPane;
027
028import org.jdom2.Attribute;
029import org.jdom2.Content;
030import org.jdom2.Element;
031import org.jdom2.util.IteratorIterable;
032
033/**
034 * Table data model for display of variables in symbolic programmer. Also
035 * responsible for loading from the XML file.
036 *
037 * @author Bob Jacobsen Copyright (C) 2001, 2006, 2010
038 * @author Howard G. Penny Copyright (C) 2005
039 * @author Daniel Boudreau Copyright (C) 2007
040 * @author Dave Heap Copyright (C) 2012 Added support for Marklin mfx style
041 * speed table
042 */
043public class VariableTableModel extends AbstractTableModel implements ActionListener, PropertyChangeListener {
044
045    private String[] headers;
046
047    private Vector<VariableValue> rowVector = new Vector<>();  // vector of Variable items
048    private CvTableModel _cvModel;          // reference to external table model
049    private Vector<JButton> _writeButtons = new Vector<>();
050    private Vector<JButton> _readButtons = new Vector<>();
051    private JLabel _status;
052    protected transient volatile DecoderFile _df = null;
053
054    /**
055     * Define the columns.
056     * <p>
057     * Values understood are: "Name", "Value", "Range", "Read", "Write",
058     * "Comment", "CV", "Mask", "State".
059     * <p>
060     * For each, a property key in SymbolicProgBundle by the same name allows
061     * i18n.
062     *
063     * @param status  variable status.
064     * @param h       values headers array.
065     * @param cvModel cv table model to use.
066     */
067    public VariableTableModel(JLabel status, String[] h, CvTableModel cvModel) {
068        super();
069        _status = status;
070        _cvModel = cvModel;
071        headers = Arrays.copyOf(h, h.length);
072    }
073
074    // basic methods for AbstractTableModel implementation
075    @Override
076    public int getRowCount() {
077        return rowVector.size();
078    }
079
080    @Override
081    public int getColumnCount() {
082        return headers.length;
083    }
084
085    @Override
086    public String getColumnName(int col) {
087        log.debug("getColumnName {}", col);
088        return Bundle.getMessage(headers[col]); // I18N
089    }
090
091    @Override
092    public Class<?> getColumnClass(int col) {
093        // log.debug("getColumnClass {}", col;
094        switch (headers[col]) {
095            case "Value":
096                return JTextField.class;
097            case "Read":
098            case "Write":
099                return JButton.class;
100            default:
101                return String.class;
102        }
103    }
104
105    @Override
106    public boolean isCellEditable(int row, int col) {
107        log.debug("isCellEditable {}", col);
108        if (headers[col].equals("Value")) {
109            return true;
110        } else if (headers[col].equals("Read")) {
111            return true;
112        } else
113            return headers[col].equals("Write") && !((rowVector.elementAt(row))).getReadOnly();
114    }
115
116    public VariableValue getVariable(int row) {
117        return (rowVector.elementAt(row));
118    }
119
120    public String getLabel(int row) {
121        return (rowVector.elementAt(row)).label();
122    }
123
124    public String getItem(int row) {
125        return (rowVector.elementAt(row)).item();
126    }
127
128    public String getCvName(int row) {
129        return (rowVector.elementAt(row)).cvName();
130    }
131
132    public String getValString(int row) {
133        return (rowVector.elementAt(row)).getValueString();
134    }
135
136    public void setIntValue(int row, int val) {
137        (rowVector.elementAt(row)).setIntValue(val);
138    }
139
140    public void setState(int row, AbstractValue.ValueState val) {
141        log.debug("setState row: {} val: {}", row, val);
142        (rowVector.elementAt(row)).setState(val);
143    }
144
145    public AbstractValue.ValueState getState(int row) {
146        return (rowVector.elementAt(row)).getState();
147    }
148
149    /*
150     * Request a "unique representation", e.g. something we can show
151     * for the row-th variable.
152     */
153    public Object getRep(int row, String format) {
154        VariableValue v = rowVector.elementAt(row);
155        return v.getNewRep(format);
156    }
157
158    @Override
159    public Object getValueAt(int row, int col) {
160        // log.debug("getValueAt {} {}", row, col;
161        if (row >= rowVector.size()) {
162            log.debug("row index greater than row vector size");
163            return "Error";
164        }
165        VariableValue v = rowVector.elementAt(row);
166        if (v == null) {
167            log.debug("v is null!");
168            return "Error value";
169        }
170        switch (headers[col]) {
171            case "Value":
172                return v.getCommonRep();
173            case "Read":
174                // NOI18N
175                return _readButtons.elementAt(row);
176            case "Write":
177                // NOI18N
178                return _writeButtons.elementAt(row);
179            case "CV":
180                // NOI18N
181                return "" + v.getCvNum();
182            case "Name":
183                // NOI18N
184                return "" + v.label();
185            case "Comment":
186                // NOI18N
187                return v.getComment();
188            case "Mask":
189                // NOI18N
190                return v.getMask();
191            case "State":
192                // NOI18N
193                AbstractValue.ValueState state = v.getState();
194                switch (state) {
195                    case UNKNOWN:
196                        return "Unknown";
197                    case READ:
198                        return "Read";
199                    case EDITED:
200                        return "Edited";
201                    case STORED:
202                        return "Stored";
203                    case FROMFILE:
204                        return "From file, known previously valid";
205                    case FROMFILEUNKNOWN:
206                        return "From file, possibly still at default";
207                    case FROMDEFAULT:
208                        return "From decoder definition defaults";
209                    default:
210                        return "inconsistent";
211                }
212            case "Range":
213                return v.rangeVal();
214            default:
215                return "Later, dude";
216        }
217    }
218
219    @Override
220    public void setValueAt(Object value, int row, int col) {
221        log.debug("setvalueAt {} {} {}", row, col, value);
222        setFileDirty(true);
223    }
224
225    /**
226     * Load one row in the VariableTableModel, by reading in the Element
227     * containing its definition.
228     * <p>
229     * Note that this method does not pass a reference to a {@link DecoderFile}
230     * instance, hence include/exclude processing at the sub-variable level is
231     * not possible and will be ignored.
232     * <p>
233     * Use of {@link #setRow(int row, Element e, DecoderFile df)} is preferred.
234     *
235     * @param row number of row to fill
236     * @param e   Element of type "variable"
237     */
238    public void setRow(int row, Element e) {
239        this.setRow(row, e, null);
240    }
241
242    /**
243     * Load one row in the VariableTableModel, by reading in the Element
244     * containing its definition.
245     * <p>
246     * Invoked from {@link DecoderFile}
247     *
248     * @param row number of row to fill
249     * @param e   Element of type "variable"
250     * @param df  the source {@link DecoderFile} instance (needed for
251     *            include/exclude processing at the sub-variable level)
252     */
253    public void setRow(int row, Element e, DecoderFile df) {    
254        // get the values for the VariableValue ctor
255        _df = df;
256        String name = LocaleSelector.getAttribute(e, "label");  // Note the name variable is actually the label attribute
257        log.debug("Starting to setRow \"{}\"", name);
258        String item = (e.getAttribute("item") != null
259                ? e.getAttribute("item").getValue()
260                : null);
261        // as a special case, if no item, use label
262        if (item == null) {
263            item = name;
264            log.debug("no item attribute, used label \"{}\"", name);
265        }
266        // as a special case, if no label, use item
267        if (name == null) {
268            name = item;
269            log.debug("no label attribute, used item attribute \"{}\"", item);
270        }
271
272        String comment = LocaleSelector.getAttribute(e, "comment");
273
274        String CV = "";
275        if (e.getAttribute("CV") != null) {
276            CV = e.getAttribute("CV").getValue();
277        }
278        String mask;
279        if (e.getAttribute("mask") != null) {
280            mask = e.getAttribute("mask").getValue();
281        } else {
282            mask = "VVVVVVVV"; // default mask is 8 bits
283            // for some VariableValue types this is replaced in #processDecVal() by larger mask if maxVal>256
284        }
285
286        boolean readOnly = e.getAttribute("readOnly") != null && e.getAttribute("readOnly").getValue().equals("yes");
287        boolean infoOnly = e.getAttribute("infoOnly") != null && e.getAttribute("infoOnly").getValue().equals("yes");
288        boolean writeOnly = e.getAttribute("writeOnly") != null && e.getAttribute("writeOnly").getValue().equals("yes");
289        boolean opsOnly = e.getAttribute("opsOnly") != null && e.getAttribute("opsOnly").getValue().equals("yes");
290
291        // Handle special case of opsOnly mode & specific programmer type
292        if (_cvModel.getProgrammer() != null) {
293            if (opsOnly && !AddressedProgrammer.class.isAssignableFrom(_cvModel.getProgrammer().getClass())) {
294                // opsOnly but not Ops mode, so adjust
295                readOnly = false;
296                writeOnly = false;
297                infoOnly = true;
298            }
299        }
300
301        JButton bw = new JButton(Bundle.getMessage("ButtonWrite"));
302        _writeButtons.addElement(bw);
303        JButton br = new JButton(Bundle.getMessage("ButtonRead"));
304        _readButtons.addElement(br);
305        setButtonsReadWrite(readOnly, infoOnly, writeOnly, bw, br, row);
306
307        if (_cvModel == null) {
308            log.error("CvModel reference is null; cannot add variables");
309            return;
310        }
311        if (!CV.equals("")) { // some variables have no CV per se
312            List<String> cvList = CvUtil.expandCvList(CV);
313            if (cvList.isEmpty()) {
314                _cvModel.addCV(CV, readOnly, infoOnly, writeOnly);
315            } else { // or require expansion
316                for (String s : cvList) {
317                    _cvModel.addCV(s, readOnly, infoOnly, writeOnly);
318                }
319            }
320        }
321
322        // decode and handle specific types
323        Element child;
324        VariableValue v;
325        if ((child = e.getChild("decVal")) != null) {
326            v = processDecVal(child, name, comment, readOnly, infoOnly, writeOnly, opsOnly, CV, mask, item);
327
328        } else if ((child = e.getChild("hexVal")) != null) {
329            v = processHexVal(child, name, comment, readOnly, infoOnly, writeOnly, opsOnly, CV, mask, item);
330
331        } else if ((child = e.getChild("enumVal")) != null) {
332            v = processEnumVal(child, name, comment, readOnly, infoOnly, writeOnly, opsOnly, CV, mask, item);
333
334        } else if ((child = e.getChild("compositeVal")) != null) {
335            // loop over the choices
336            v = processCompositeVal(child, name, comment, readOnly, infoOnly, writeOnly, opsOnly, CV, mask, item);
337
338        } else if ((child = e.getChild("speedTableVal")) != null) {
339            v = processSpeedTableVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
340
341        } else if ((child = e.getChild("longAddressVal")) != null) {
342            v = processLongAddressVal(CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
343
344        } else if ((child = e.getChild("shortAddressVal")) != null) {
345            v = processShortAddressVal(name, comment, readOnly, infoOnly, writeOnly, opsOnly, CV, mask, item, child);
346
347        } else if ((child = e.getChild("splitVal")) != null) {
348            v = processSplitVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
349
350        } else if ((child = e.getChild("splitHexVal")) != null) {
351            v = processSplitHexVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
352
353        } else if ((child = e.getChild("splitHundredsVal")) != null) {
354            v = processSplitHundredsVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
355
356        } else if ((child = e.getChild("splitTextVal")) != null) {
357            v = processSplitTextVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
358
359        } else if ((child = e.getChild("splitDateTimeVal")) != null) {
360            v = processSplitDateTimeVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
361
362        } else if ((child = e.getChild("splitEnumVal")) != null) {
363            v = processSplitEnumVal(child, CV, readOnly, infoOnly, writeOnly, name, comment, opsOnly, mask, item);
364
365        } else {
366            reportBogus(e);
367            return;
368        }
369
370        processModifierElements(e, v);
371
372        setToolTip(e, v);
373
374        // record new variable, update state, hook up listeners
375        rowVector.addElement(v);
376        v.setState(AbstractValue.ValueState.FROMDEFAULT);
377        v.addPropertyChangeListener(this);
378
379        // set to default value if specified (CV load may later override this)
380        if (setDefaultValue(e, v)) {
381            // need to correct state of associated CV(s) & handle a possible CV List
382            List<String> cvList = CvUtil.expandCvList(CV);  // see if CV is in list format
383            if (cvList.isEmpty()) {
384                cvList.add(CV);  // it's an ordinary CV so add it as such
385            }
386            for (String theCV : cvList) {
387                log.debug("Setting CV={} of '{}'to {}", theCV, CV, AbstractValue.ValueState.FROMDEFAULT.getName());
388                _cvModel.getCvByNumber(theCV).setState(AbstractValue.ValueState.FROMDEFAULT); // correct for transition to "edited"
389            }
390        }
391    }
392
393    /**
394     * If there are any modifier elements, process them by e.g. setting
395     * attributes on the VariableValue.
396     *
397     * @param e        Element that's source of info
398     * @param variable Variable to load
399     */
400    protected void processModifierElements(final Element e, final VariableValue variable) {
401        QualifierAdder qa = new QualifierAdder() {
402            @Override
403            protected Qualifier createQualifier(VariableValue variable2, String relation, String value) {
404                return new ValueQualifier(variable, variable2, Integer.parseInt(value), relation);
405            }
406
407            @Override
408            protected void addListener(java.beans.PropertyChangeListener qc) {
409                variable.addPropertyChangeListener(qc);
410            }
411        };
412
413        qa.processModifierElements(e, this);
414    }
415
416    /**
417     * If there's a "default" attribute, or matching defaultItem element, set
418     * that value to start.
419     *
420     * @param e        Element that's source of info
421     * @param variable Variable to load
422     * @return true if the value was set
423     */
424    boolean setDefaultValue(Element e, VariableValue variable) {
425        Attribute a;
426        boolean set = false;
427        if ((a = e.getAttribute("default")) != null) {
428            String val = a.getValue();
429            variable.setValue(val);
430            set = true;
431        }
432        // check for matching child
433        List<Element> elements = e.getChildren("defaultItem");
434        for (Element defaultItem : elements) {
435            if (_df != null && DecoderFile.isIncluded(defaultItem, _df.getProductID(), _df.getModel(), _df.getFamily(), "", "")) {
436                log.debug("element included by productID={} model={} family={}", _df.getProductID(), _df.getModel(), _df.getFamily());
437                variable.setValue(defaultItem.getAttribute("default").getValue());
438                return true;
439            }
440        }
441        return set;
442    }
443
444    protected VariableValue processCompositeVal(Element child, String name, String comment, boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, String CV, String mask, String item) {
445        int count = 0;
446        IteratorIterable<Content> iterator = child.getDescendants();
447        while (iterator.hasNext()) {
448            Object ex = iterator.next();
449            if (ex instanceof Element) {
450                if (((Element) ex).getName().equals("compositeChoice")) {
451                    count++;
452                }
453            }
454        }
455
456        VariableValue v;
457        CompositeVariableValue v1 = new CompositeVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, 0, count, _cvModel.allCvMap(), _status, item);
458        v = v1; // v1 is of CompositeVariableType, so doesn't need casts
459
460        v1.nItems(count);
461        handleCompositeValChildren(child, v1);
462        v1.lastItem();
463        return v;
464    }
465
466    /**
467     * Recursively walk the child compositeChoice elements, working through the
468     * compositeChoiceGroup elements as needed.
469     * <p>
470     * Adapted from handleEnumValChildren for use in LocoIO Legacy tool.
471     *
472     * @param e   Element that's source of info
473     * @param var Variable to load
474     */
475    protected void handleCompositeValChildren(Element e, CompositeVariableValue var) {
476        List<Element> local = e.getChildren();
477        for (Element el : local) {
478            log.debug("processing element='{}' name='{}' choice='{}' value='{}'", el.getName(), LocaleSelector.getAttribute(el, "name"), LocaleSelector.getAttribute(el, "choice"), el.getAttribute("value"));
479            if (_df != null && !DecoderFile.isIncluded(el, _df.getProductID(), _df.getModel(), _df.getFamily(), "", "")) {
480                log.debug("element excluded by productID={} model={} family={}", _df.getProductID(), _df.getModel(), _df.getFamily());
481                continue;
482            }
483            if (el.getName().equals("compositeChoice")) {
484                // Create the choice
485                String choice = LocaleSelector.getAttribute(el, "choice");
486                var.addChoice(choice);
487                // for each choice, capture the settings
488                List<Element> lSetting = el.getChildren("compositeSetting");
489                for (Element settingElement : lSetting) {
490                    String varName = LocaleSelector.getAttribute(settingElement, "label");
491                    String value = settingElement.getAttribute("value").getValue();
492                    var.addSetting(choice, varName, findVar(varName), value);
493                }
494            } else if (el.getName().equals("compositeChoiceGroup")) {
495                // no tree to manage as in enumGroup
496                handleCompositeValChildren(el, var);
497            }
498            log.debug("element processed");
499        }
500    }
501
502    protected VariableValue processDecVal(Element child, String name, String comment, boolean readOnly, boolean infoOnly,
503                                          boolean writeOnly, boolean opsOnly, String CV, String mask, String item)
504            throws NumberFormatException {
505        VariableValue v;
506        Attribute a;
507        int minVal = 0;
508        int maxVal = 255;
509        if ((a = child.getAttribute("min")) != null) {
510            minVal = Integer.parseInt(a.getValue());
511        }
512        if ((a = child.getAttribute("max")) != null) {
513            maxVal = Integer.parseInt(a.getValue());
514        }
515        int factor = 1;
516        if ((a = child.getAttribute("factor")) != null) {
517            factor = Integer.parseInt(a.getValue());
518        }
519        int offset = 0;
520        if ((a = child.getAttribute("offset")) != null) {
521            offset = Integer.parseInt(a.getValue());
522        }
523        if (maxVal > 255 && Objects.equals(mask, "VVVVVVVV")) {
524            mask = VariableValue.getMaxMask(maxVal); // replaces the default 8 bit mask when no mask is provided in xml
525            log.debug("Created mask {} for DecVar CV {}", mask, name);
526        }
527        v = new DecVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly,
528                CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, offset, factor);
529        _cvModel.registerCvToVariableMapping(CV, name);
530        return v;
531    }
532
533    protected VariableValue processEnumVal(Element child, String name, String comment, boolean readOnly, boolean infoOnly,
534                                           boolean writeOnly, boolean opsOnly, String CV, String mask, String item)
535            throws NumberFormatException {
536        VariableValue v;
537        int count = 0;
538        IteratorIterable<Content> iterator = child.getDescendants();
539        while (iterator.hasNext()) {
540            Object ex = iterator.next();
541            if (ex instanceof Element) {
542                if (((Element) ex).getName().equals("enumChoice")) {
543                    count++;
544                }
545            }
546        }
547        Attribute a;
548        int maxVal = 255;
549        if ((a = child.getAttribute("max")) != null) {
550            // requires explicit max attribute for Radix mask if not all options are filled by enum
551            maxVal = Integer.parseInt(a.getValue());
552        } else {
553            maxVal = count;
554        }
555        EnumVariableValue v1 = new EnumVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly,
556                CV, mask, 0, maxVal, _cvModel.allCvMap(), _status, item);
557        v = v1; // v1 is of EnumVariableValue type, so doesn't need casts
558        _cvModel.registerCvToVariableMapping(CV, name);
559
560        v1.nItems(count);
561        handleEnumValChildren(child, v1);
562        v1.lastItem();
563        return v;
564    }
565
566    protected VariableValue processSplitEnumVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
567        VariableValue v;
568        Attribute a;
569        int minVal = 0;
570        int maxVal = 255;
571        String highCV = null;
572
573        int count = 0;
574        IteratorIterable<Content> iterator = child.getDescendants();
575        while (iterator.hasNext()) {
576            Object ex = iterator.next();
577            if (ex instanceof Element) {
578                if (((Element) ex).getName().equals("enumChoice")) {
579                    count++;
580                }
581            }
582        }
583
584        if ((a = child.getAttribute("highCV")) != null) {
585            highCV = a.getValue();
586            _cvModel.addCV("" + (highCV), readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
587            _cvModel.registerCvToVariableMapping(highCV, name);
588        }
589        int factor = 1;
590        if ((a = child.getAttribute("factor")) != null) {
591            factor = Integer.parseInt(a.getValue());
592        }
593        int offset = 0;
594        if ((a = child.getAttribute("offset")) != null) {
595            offset = Integer.parseInt(a.getValue());
596        }
597        String uppermask = "VVVVVVVV";
598        if ((a = child.getAttribute("upperMask")) != null) {
599            uppermask = a.getValue();
600        }
601        String extra3 = "0";
602        if ((a = child.getAttribute("min")) != null) {
603            extra3 = a.getValue();
604        }
605        String extra4 = Long.toUnsignedString(~0);
606        if ((a = child.getAttribute("max")) != null) {
607            extra4 = a.getValue();
608        }
609
610        SplitEnumVariableValue v1 = new SplitEnumVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, highCV, factor, offset, uppermask, null, null, extra3, extra4);
611        v = v1; // v1 is of EnunVariableValue type, so doesn't need casts
612        _cvModel.registerCvToVariableMapping(CV, name);
613
614        v1.nItems(count);
615
616        handleSplitEnumValChildren(child, v1);
617        v1.lastItem();
618        return v;
619    }
620
621    /**
622     * Recursively walk the child enumChoice elements, working through the
623     * enumChoiceGroup elements as needed.
624     *
625     * @param e   Element that's source of info
626     * @param var Variable to load
627     */
628    protected void handleSplitEnumValChildren(Element e, SplitEnumVariableValue var) {
629        List<Element> local = e.getChildren();
630
631        for (Element el : local) {
632            log.debug("processing element='{}' name='{}' choice='{}' value='{}'", el.getName(), LocaleSelector.getAttribute(el, "name"), LocaleSelector.getAttribute(el, "choice"), el.getAttribute("value"));
633            if (_df != null && !DecoderFile.isIncluded(el, _df.getProductID(), _df.getModel(), _df.getFamily(), "", "")) {
634                log.debug("element excluded by productID={} model={} family={}", _df.getProductID(), _df.getModel(), _df.getFamily());
635                continue;
636            }
637            if (el.getName().equals("enumChoice")) {
638                Attribute valAttr = el.getAttribute("value");
639                if (valAttr == null) {
640                    var.addItem(LocaleSelector.getAttribute(el, "choice"));
641                } else {
642                    var.addItem(LocaleSelector.getAttribute(el, "choice"),
643                            Integer.parseInt(valAttr.getValue()));
644                }
645            } else if (el.getName().equals("enumChoiceGroup")) {
646                var.startGroup(LocaleSelector.getAttribute(el, "name"));
647                handleSplitEnumValChildren(el, var);
648                var.endGroup();
649            }
650            log.debug("element processed");
651        }
652    }
653
654    /**
655     * Recursively walk the child enumChoice elements, working through the
656     * enumChoiceGroup elements as needed.
657     *
658     * @param e   Element that's source of info
659     * @param var Variable to load
660     */
661    protected void handleEnumValChildren(Element e, EnumVariableValue var) {
662        List<Element> local = e.getChildren();
663        for (Element el : local) {
664            log.debug("processing element='{}' name='{}' choice='{}' value='{}'", el.getName(), LocaleSelector.getAttribute(el, "name"), LocaleSelector.getAttribute(el, "choice"), el.getAttribute("value"));
665            if (_df != null && !DecoderFile.isIncluded(el, _df.getProductID(), _df.getModel(), _df.getFamily(), "", "")) {
666                log.debug("element excluded by productID={} model={} family={}", _df.getProductID(), _df.getModel(), _df.getFamily());
667                continue;
668            }
669            if (el.getName().equals("enumChoice")) {
670                Attribute valAttr = el.getAttribute("value");
671                if (valAttr == null) {
672                    var.addItem(LocaleSelector.getAttribute(el, "choice"));
673                } else {
674                    var.addItem(LocaleSelector.getAttribute(el, "choice"),
675                            Integer.parseInt(valAttr.getValue()));
676                }
677            } else if (el.getName().equals("enumChoiceGroup")) {
678                var.startGroup(LocaleSelector.getAttribute(el, "name"));
679                handleEnumValChildren(el, var);
680                var.endGroup();
681            }
682            log.debug("element processed");
683        }
684    }
685
686    protected VariableValue processHexVal(Element child, String name, String comment, boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, String CV, String mask, String item) throws NumberFormatException {
687        VariableValue v;
688        Attribute a;
689        int minVal = 0;
690        int maxVal = 255;
691        if ((a = child.getAttribute("min")) != null) {
692            minVal = Integer.valueOf(a.getValue(), 16);
693        }
694        if ((a = child.getAttribute("max")) != null) {
695            maxVal = Integer.valueOf(a.getValue(), 16);
696        }
697        if (maxVal > 255 && Objects.equals(mask, "VVVVVVVV")) {
698            mask = VariableValue.getMaxMask(maxVal); // replaces the default 8 bit mask when no mask is provided in xml
699            log.debug("Created mask {} for Hex CV {}", mask, name);
700        }
701        v = new HexVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item);
702        _cvModel.registerCvToVariableMapping(CV, name);
703        return v;
704    }
705
706    protected VariableValue processLongAddressVal(String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) {
707        VariableValue v;
708        int minVal = 0;
709        int maxVal = 255;
710        _cvModel.addCV("18", readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
711        v = new LongAddrVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, _cvModel.allCvMap().get("18"));
712        _cvModel.registerCvToVariableMapping(CV, name);
713        _cvModel.registerCvToVariableMapping("18", name); // see fixed value two lines up
714        return v;
715    }
716
717    protected VariableValue processShortAddressVal(String name, String comment, boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly, String CV, String mask, String item, Element child) {
718        VariableValue v;
719        ShortAddrVariableValue v1 = new ShortAddrVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, _cvModel.allCvMap(), _status, item);
720        v = v1;
721        // get specifics if any
722        List<Element> l = child.getChildren("shortAddressChanges");
723        for (Element element : l) {
724            v1.setModifiedCV(element.getAttribute("cv").getValue());
725        }
726        _cvModel.registerCvToVariableMapping(CV, name);
727        return v;
728    }
729
730    protected VariableValue processSpeedTableVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
731        VariableValue v;
732        Attribute a;
733        int minVal = 0;
734        int maxVal = 255;
735        if ((a = child.getAttribute("min")) != null) {
736            minVal = Integer.parseInt(a.getValue());
737        }
738        if ((a = child.getAttribute("max")) != null) {
739            maxVal = Integer.parseInt(a.getValue());
740        }
741        Attribute entriesAttr = child.getAttribute("entries");
742        int entries = 28;
743        try {
744            if (entriesAttr != null) {
745                entries = entriesAttr.getIntValue();
746            }
747        } catch (org.jdom2.DataConversionException ignored) {
748        }
749        Attribute ESUAttr = child.getAttribute("mfx");
750        boolean mfxFlag = false;
751        try {
752            if (ESUAttr != null) {
753                mfxFlag = ESUAttr.getBooleanValue();
754            }
755        } catch (org.jdom2.DataConversionException ignored) {
756        }
757        // ensure all CVs exist
758        for (int i = 0; i < entries; i++) {
759            _cvModel.addCV(Integer.toString(Integer.parseInt(CV) + i), readOnly, infoOnly, writeOnly);
760            _cvModel.registerCvToVariableMapping(Integer.toString(Integer.parseInt(CV) + i), name);
761
762        }
763        if (mfxFlag) {
764            _cvModel.addCV("2", readOnly, infoOnly, writeOnly);
765            _cvModel.registerCvToVariableMapping("2", name);
766
767            _cvModel.addCV("5", readOnly, infoOnly, writeOnly);
768            _cvModel.registerCvToVariableMapping("5", name);
769
770        }
771        v = new SpeedTableVarValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, entries, mfxFlag);
772        return v;
773    }
774
775    protected VariableValue processSplitVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
776        VariableValue v;
777        Attribute a;
778        int minVal = 0;
779        int maxVal = 255;
780        String highCV = null;
781
782        if ((a = child.getAttribute("highCV")) != null) {
783            highCV = a.getValue();
784            _cvModel.addCV("" + (highCV), readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
785            _cvModel.registerCvToVariableMapping("" + (highCV), name);
786
787        }
788        int factor = 1;
789        if ((a = child.getAttribute("factor")) != null) {
790            factor = Integer.parseInt(a.getValue());
791        }
792        int offset = 0;
793        if ((a = child.getAttribute("offset")) != null) {
794            offset = Integer.parseInt(a.getValue());
795        }
796        String uppermask = "VVVVVVVV";
797        if ((a = child.getAttribute("upperMask")) != null) {
798            uppermask = a.getValue();
799        }
800        String extra3 = "0";
801        if ((a = child.getAttribute("min")) != null) {
802            extra3 = a.getValue();
803        }
804        String extra4 = Long.toUnsignedString(~0);
805        if ((a = child.getAttribute("max")) != null) {
806            extra4 = a.getValue();
807        }
808        v = new SplitVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, highCV, factor, offset, uppermask, null, null, extra3, extra4);
809        _cvModel.registerCvToVariableMapping(CV, name);
810        return v;
811    }
812
813    protected VariableValue processSplitHexVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
814        VariableValue v;
815        Attribute a;
816        int minVal = 0;
817        int maxVal = 255;
818        String highCV = null;
819
820        if ((a = child.getAttribute("highCV")) != null) {
821            highCV = a.getValue();
822            _cvModel.addCV("" + (highCV), readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
823            _cvModel.registerCvToVariableMapping("" + (highCV), name);
824        }
825        int factor = 1;
826        if ((a = child.getAttribute("factor")) != null) {
827            factor = Integer.parseInt(a.getValue());
828        }
829        int offset = 0;
830        if ((a = child.getAttribute("offset")) != null) {
831            offset = Integer.parseInt(a.getValue());
832        }
833        String uppermask = "VVVVVVVV";
834        if ((a = child.getAttribute("upperMask")) != null) {
835            uppermask = a.getValue();
836        }
837        String extra1 = "default";
838        if ((a = child.getAttribute("case")) != null) {
839            extra1 = a.getValue();
840        }
841        String extra3 = "0";
842        if ((a = child.getAttribute("min")) != null) {
843            extra3 = a.getValue();
844        }
845        String extra4 = Long.toUnsignedString(~0, 16);
846        if ((a = child.getAttribute("max")) != null) {
847            extra4 = a.getValue();
848        }
849        v = new SplitHexVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, highCV, factor, offset, uppermask, extra1, null, extra3, extra4);
850        _cvModel.registerCvToVariableMapping(CV, name);
851        return v;
852    }
853
854    protected VariableValue processSplitHundredsVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
855        VariableValue v;
856        Attribute a;
857        int minVal = 0;
858        int maxVal = 255;
859        String highCV = null;
860
861        if ((a = child.getAttribute("highCV")) != null) {
862            highCV = a.getValue();
863            _cvModel.addCV("" + (highCV), readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
864            _cvModel.registerCvToVariableMapping("" + (highCV), name);
865        }
866        int factor = 1;
867        if ((a = child.getAttribute("factor")) != null) {
868            factor = Integer.parseInt(a.getValue());
869        }
870        int offset = 0;
871        if ((a = child.getAttribute("offset")) != null) {
872            offset = Integer.parseInt(a.getValue());
873        }
874        String uppermask = "VVVVVVVV";
875        if ((a = child.getAttribute("upperMask")) != null) {
876            uppermask = a.getValue();
877        }
878        String extra3 = "0";
879        if ((a = child.getAttribute("min")) != null) {
880            extra3 = a.getValue();
881        }
882        String extra4 = Long.toUnsignedString(~0);
883        if ((a = child.getAttribute("max")) != null) {
884            extra4 = a.getValue();
885        }
886        v = new SplitHundredsVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, highCV, factor, offset, uppermask, null, null, extra3, extra4);
887        _cvModel.registerCvToVariableMapping(CV, name);
888        return v;
889    }
890
891    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
892        justification="I18N of Error Message")
893    protected VariableValue processSplitTextVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
894        VariableValue v;
895        Attribute a;
896        int minVal = 0;
897        int maxVal = 255;
898        String highCV = null;
899
900        if ((a = child.getAttribute("min")) != null) {
901            minVal = Integer.parseInt(a.getValue());
902        }
903        if ((a = child.getAttribute("max")) != null) {
904            maxVal = Integer.parseInt(a.getValue());
905        }
906        if ((a = child.getAttribute("highCV")) != null) {
907            highCV = a.getValue();
908            _cvModel.addCV("" + (highCV), readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
909            _cvModel.registerCvToVariableMapping("" + (highCV), name);
910        }
911        int factor = 1;
912        if ((a = child.getAttribute("factor")) != null) {
913            factor = Integer.parseInt(a.getValue());
914        }
915        int offset = 0;
916        if ((a = child.getAttribute("offset")) != null) {
917            offset = Integer.parseInt(a.getValue());
918        }
919        String uppermask = "VVVVVVVV";
920        if ((a = child.getAttribute("upperMask")) != null) {
921            uppermask = a.getValue();
922        }
923        String match = null;
924        if ((a = child.getAttribute("match")) != null) {
925            match = a.getValue();
926        }
927        String termByte = "0";
928        if ((a = child.getAttribute("termByte")) != null) {
929            termByte = a.getValue();
930        }
931        String padByte = "0";
932        if ((a = child.getAttribute("padByte")) != null) {
933            padByte = a.getValue();
934        }
935        String charSet = defaultCharset().name();
936        if ((a = child.getAttribute("charSet")) != null) {
937            charSet = a.getValue();
938        }
939        boolean ok;
940        try {
941            ok = isSupported(charSet);
942        } catch (IllegalArgumentException ex) {
943            ok = false;
944        }
945        if (!ok) {
946            synchronized (this) {
947                JmriJOptionPane.showMessageDialog(new JFrame(), Bundle.getMessage("UnsupportedCharset", charSet, name),
948                        Bundle.getMessage("DecoderDefError"), JmriJOptionPane.ERROR_MESSAGE); // NOI18N
949            }
950            log.error(Bundle.getMessage("UnsupportedCharset", charSet, name));
951        }
952        v = new SplitTextVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, highCV, factor, offset, uppermask, match, termByte, padByte, charSet);
953        _cvModel.registerCvToVariableMapping(CV, name);
954        return v;
955    }
956
957    protected VariableValue processSplitDateTimeVal(Element child, String CV, boolean readOnly, boolean infoOnly, boolean writeOnly, String name, String comment, boolean opsOnly, String mask, String item) throws NumberFormatException {
958        VariableValue v;
959        Attribute a;
960        int minVal = 0;
961        int maxVal = 255;
962        boolean varRreadOnly = true; // unable to parse text dates accurately enough so force variable (but not CVs) to be read only
963        String highCV = null;
964
965        if ((a = child.getAttribute("min")) != null) {
966            minVal = Integer.parseInt(a.getValue());
967        }
968        if ((a = child.getAttribute("max")) != null) {
969            maxVal = Integer.parseInt(a.getValue());
970        }
971        if ((a = child.getAttribute("highCV")) != null) {
972            highCV = a.getValue();
973            _cvModel.addCV("" + (highCV), readOnly, infoOnly, writeOnly); // ensure 2nd CV exists
974            _cvModel.registerCvToVariableMapping("" + (highCV), name);
975        }
976        int factor = 1;
977        int offset = 0;
978
979        String uppermask = "VVVVVVVV";
980        if ((a = child.getAttribute("upperMask")) != null) {
981            uppermask = a.getValue();
982        }
983        String extra1 = "2000-01-01T00:00:00";  // The S9.3.2 RailCom epoch
984        // Java epoch is "1970-01-01T00:00:00"
985        if ((a = child.getAttribute("base")) != null) {
986            extra1 = a.getValue();
987        }
988        String extra2 = "1";
989        if ((a = child.getAttribute("factor")) != null) {
990            extra2 = a.getValue();
991        }
992        String extra3 = "Seconds";
993        if ((a = child.getAttribute("unit")) != null) {
994            extra3 = a.getValue();
995        }
996        String extra4 = "default";
997        if ((a = child.getAttribute("display")) != null) {
998            extra4 = a.getValue();
999        }
1000        v = new SplitDateTimeVariableValue(name, comment, "", varRreadOnly, infoOnly, writeOnly, opsOnly, CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, item, highCV, factor, offset, uppermask, extra1, extra2, extra3, extra4);
1001        _cvModel.registerCvToVariableMapping(CV, name);
1002        return v;
1003    }
1004
1005    protected void setButtonsReadWrite(boolean readOnly, boolean infoOnly, boolean writeOnly, JButton bw, JButton br, int row) {
1006        if (readOnly || infoOnly) {
1007            // readOnly or infoOnly, config write, read buttons
1008            if (writeOnly) {
1009                bw.setEnabled(true);
1010                bw.setActionCommand("W" + row);
1011                bw.addActionListener(this);
1012            } else {
1013                bw.setEnabled(false);
1014            }
1015            if (infoOnly) {
1016                br.setEnabled(false);
1017            } else {
1018                br.setActionCommand("R" + row);
1019                br.addActionListener(this);
1020            }
1021        } else {
1022            // not readOnly or infoOnly, config write, read buttons
1023            bw.setActionCommand("W" + row);
1024            bw.addActionListener(this);
1025            if (writeOnly) {
1026                br.setEnabled(false);
1027            } else {
1028                br.setActionCommand("R" + row);
1029                br.addActionListener(this);
1030            }
1031        }
1032    }
1033
1034    public void setButtonModeFromProgrammer() {
1035        if (_cvModel.getProgrammer() == null || !_cvModel.getProgrammer().getCanRead()) {
1036            for (JButton b : _readButtons) {
1037                b.setEnabled(false);
1038            }
1039        }
1040    }
1041
1042    protected void setToolTip(Element e, VariableValue v) {
1043        // back to general processing
1044        // add tooltip text if present
1045        {
1046            String t;
1047            if ((t = LocaleSelector.getAttribute(e, "tooltip")) != null) {
1048                v.setToolTipText(t);
1049            }
1050        }
1051    }
1052
1053    void reportBogus(Element elem) {
1054        log.error("Did not find a valid type in {}", elem.getChildren());
1055        for (Attribute a : elem.getAttributes()) log.error("   attribute: {}",a);
1056    }
1057
1058    /**
1059     * Configure from a constant. This is like setRow (which processes a
1060     * variable Element).
1061     *
1062     * @param e element to set.
1063     */
1064    @SuppressFBWarnings(value = "NP_LOAD_OF_KNOWN_NULL_VALUE",
1065            justification = "null mask parameter to ConstantValue constructor expected.")
1066    public void setConstant(Element e) {
1067        // get the values for the VariableValue ctor
1068        String stdname = e.getAttribute("item").getValue();
1069        log.debug("Starting to setConstant \"{}\"", stdname);
1070
1071        String name = LocaleSelector.getAttribute(e, "label");
1072        if (name == null || name.equals("")) {
1073            name = stdname;
1074        }
1075
1076        String comment = LocaleSelector.getAttribute(e, "comment");
1077
1078        String mask = null;
1079
1080        // intrinsically readOnly, so use just that branch
1081        JButton bw = new JButton();
1082        _writeButtons.addElement(bw);
1083
1084        // config read button as a dummy - there's really nothing to read
1085        JButton br = new JButton("Read"); // NOI18N
1086        _readButtons.addElement(br);
1087
1088        // no CV references are added here
1089        // have to handle various value types, see "snippet"
1090        Attribute a;
1091
1092        // set to default value if specified (CV load will later override this)
1093        int defaultVal = 0;
1094        if ((a = e.getAttribute("default")) != null) {
1095            String val = a.getValue();
1096            log.debug("Found default value: {} for {}", val, stdname);
1097            defaultVal = Integer.parseInt(val);
1098        }
1099
1100        // create the specific object
1101        ConstantValue v = new ConstantValue(name, comment, "", true, true, false, false,
1102                "", mask, defaultVal, defaultVal,
1103                _cvModel.allCvMap(), _status, stdname);
1104
1105        // record new variable, update state, hook up listeners
1106        rowVector.addElement(v);
1107        v.setState(AbstractValue.ValueState.FROMFILE);
1108        v.addPropertyChangeListener(this);
1109
1110        // set to default value if specified (CV load will later override this)
1111        if ((a = e.getAttribute("default")) != null) {
1112            String val = a.getValue();
1113            log.debug("Found default value: {} for {}", val, name);
1114            v.setIntValue(defaultVal);
1115        }
1116    }
1117
1118    /**
1119     * Programmatically create a new DecVariableValue from parameters.
1120     *
1121     * @param name      variable name.
1122     * @param CV        CV string.
1123     * @param comment   variable comment.
1124     * @param mask      CV mask.
1125     * @param readOnly  true if read only, else false.
1126     * @param infoOnly  true if information only, else false.
1127     * @param writeOnly true if write only, else false.
1128     * @param opsOnly   true if ops only, else false.
1129     */
1130    public void newDecVariableValue(String name, String CV, String comment, String mask,
1131            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly) {
1132        setFileDirty(true);
1133
1134        int minVal = 0;
1135        int maxVal = 255;
1136        _cvModel.addCV("" + CV, readOnly, infoOnly, writeOnly);
1137
1138        int row = getRowCount();
1139
1140        // config write button
1141        JButton bw = new JButton(Bundle.getMessage("ButtonWrite"));
1142        bw.setActionCommand("W" + row);
1143        bw.addActionListener(this);
1144        _writeButtons.addElement(bw);
1145
1146        // config read button
1147        JButton br = new JButton(Bundle.getMessage("ButtonRead"));
1148        br.setActionCommand("R" + row);
1149        br.addActionListener(this);
1150        _readButtons.addElement(br);
1151
1152        VariableValue v = new DecVariableValue(name, comment, "", readOnly, infoOnly, writeOnly, opsOnly,
1153                CV, mask, minVal, maxVal, _cvModel.allCvMap(), _status, null);
1154        rowVector.addElement(v);
1155        v.addPropertyChangeListener(this);
1156    }
1157
1158    @Override
1159    public void actionPerformed(ActionEvent e) {
1160        if (log.isDebugEnabled()) {
1161            log.debug("action performed,  command: {}", e.getActionCommand());
1162        }
1163        setFileDirty(true);
1164        char b = e.getActionCommand().charAt(0);
1165        int row = Integer.parseInt(e.getActionCommand().substring(1));
1166        log.debug("event on {} row {}", b, row);
1167        if (b == 'R') {
1168            // read command
1169            read(row);
1170        } else {
1171            // write command
1172            write(row);
1173        }
1174    }
1175
1176    /**
1177     * Command reading of a particular variable.
1178     *
1179     * @param i row number
1180     */
1181    public void read(int i) {
1182        VariableValue v = rowVector.elementAt(i);
1183        v.readAll();
1184    }
1185
1186    /**
1187     * Command writing of a particular variable.
1188     *
1189     * @param i row number
1190     */
1191    public void write(int i) {
1192        VariableValue v = rowVector.elementAt(i);
1193        v.writeAll();
1194    }
1195
1196    @Override
1197    public void propertyChange(PropertyChangeEvent e) {
1198        if (log.isDebugEnabled()) {
1199            log.debug("prop changed {} new value: {}{} Source {}", e.getPropertyName(), e.getNewValue(), e.getPropertyName().equals("State") ? (" (" + ((AbstractValue.ValueState) e.getNewValue()).getName() + ") ") : " ", e.getSource());
1200        }
1201        if (e.getNewValue() == null) {
1202            log.error("new value of {} should not be null!", e.getPropertyName(), new Exception());
1203        }
1204        // set dirty only if edited or read
1205        if (e.getPropertyName().equals("State")
1206                && ((AbstractValue.ValueState) e.getNewValue()) == AbstractValue.ValueState.READ
1207                || e.getPropertyName().equals("State")
1208                && ((AbstractValue.ValueState) e.getNewValue()) == AbstractValue.ValueState.EDITED) {
1209            setFileDirty(true);
1210
1211        }
1212        fireTableDataChanged();
1213    }
1214
1215    public void configDone() {
1216        fireTableDataChanged();
1217    }
1218
1219    /**
1220     * Represents any change to values, etc, hence rewriting the file is
1221     * desirable.
1222     *
1223     * @return true if dirty, else false.
1224     */
1225    public boolean fileDirty() {
1226        return _fileDirty;
1227    }
1228
1229    public void setFileDirty(boolean b) {
1230        _fileDirty = b;
1231    }
1232    private boolean _fileDirty;
1233
1234    /**
1235     * Check for change to values, etc, hence rewriting the decoder is
1236     * desirable.
1237     *
1238     * @return true if dirty, else false.
1239     */
1240    public boolean decoderDirty() {
1241        int len = rowVector.size();
1242        for (int i = 0; i < len; i++) {
1243            if (((rowVector.elementAt(i))).getState() == AbstractValue.ValueState.EDITED) {
1244                return true;
1245            }
1246        }
1247        return false;
1248    }
1249
1250    /**
1251     * Returns the (first) variable that matches a given name string.
1252     * <p>
1253     * Searches first for "item", the true name, but if none found will attempt
1254     * to find a matching "label". In that case, only the default language is
1255     * checked.
1256     *
1257     * @param name search string.
1258     * @return first matching variable found.
1259     */
1260    public VariableValue findVar(String name) {
1261        for (int i = 0; i < getRowCount(); i++) {
1262            if (name.equals(getItem(i))) {
1263                log.trace("findVar matched '{}' by Item", name);
1264                return getVariable(i);
1265            }
1266        }
1267        for (int i = 0; i < getRowCount(); i++) {
1268            if (name.equals(getLabel(i))) {
1269                log.trace("findVar matched '{}' by Label rather than Item", name);
1270                return getVariable(i);
1271            }
1272        }
1273        log.debug("findVar did not match {}, returns null", name);
1274        return null;
1275    }
1276
1277    /**
1278     * Returns the index of the first variable that matches a given name string.
1279     * <p>
1280     * Checks the search string against every variable's "item", the true name,
1281     * then against their "label" (default language only) and finally the
1282     * CV name before moving on to the next variable if none of those match.
1283     *
1284     * @param name search string.
1285     * @return index of the first matching variable found.
1286     */
1287    public int findVarIndex(String name) {
1288        return findVarIndex(name, false);
1289    }
1290
1291    /**
1292     * Returns the index of a variable that matches a given name string.
1293     * <p>
1294     * Checks the search string against every variable's "item", the true name,
1295     * then against their "label" (default language only) and finally the
1296     * CV name before moving on to the next variable if none of those match.
1297     *
1298     * Depending on the second parameter, it will return the index of the first
1299     * or last variable in our internal rowVector that matches the given string.
1300     *
1301     * @param name search string.
1302     * @param searchFromEnd If true, will start searching from the end.
1303     * @return index of the first matching variable found.
1304     */
1305    public int findVarIndex(String name, boolean searchFromEnd) {
1306        if(searchFromEnd) {
1307            for (int i = getRowCount() - 1; i >= 0; i--) {
1308                if (name.equals(getItem(i))) {
1309                    return i;
1310                }
1311                if (name.equals(getLabel(i))) {
1312                    return i;
1313                }
1314                if (name.equals("CV" + getCvName(i))) {
1315                    return i;
1316                }
1317            }
1318        } else {
1319            for (int i = 0; i < getRowCount(); i++) {
1320                if (name.equals(getItem(i))) {
1321                    return i;
1322                }
1323                if (name.equals(getLabel(i))) {
1324                    return i;
1325                }
1326                if (name.equals("CV" + getCvName(i))) {
1327                    return i;
1328                }
1329            }
1330        }
1331        return -1;
1332    }
1333
1334    public void dispose() {
1335        log.debug("dispose");
1336
1337        // remove buttons
1338        for (int i = 0; i < _writeButtons.size(); i++) {
1339            _writeButtons.elementAt(i).removeActionListener(this);
1340        }
1341        for (int i = 0; i < _readButtons.size(); i++) {
1342            _readButtons.elementAt(i).removeActionListener(this);
1343        }
1344
1345        // remove variables listeners
1346        for (int i = 0; i < rowVector.size(); i++) {
1347            VariableValue v = rowVector.elementAt(i);
1348            v.removePropertyChangeListener(this);
1349            v.dispose();
1350        }
1351
1352        headers = null;
1353
1354        rowVector.removeAllElements();
1355        rowVector = null;
1356
1357        _cvModel = null;
1358
1359        _writeButtons.removeAllElements();
1360        _writeButtons = null;
1361
1362        _readButtons.removeAllElements();
1363        _readButtons = null;
1364
1365        _status = null;
1366    }
1367
1368    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VariableTableModel.class);
1369
1370}