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