001package jmri.jmrix.can.cbus.swing.nodeconfig;
002
003import java.awt.event.ActionEvent;
004import java.awt.BorderLayout;
005import java.awt.Dimension;
006import java.awt.event.ActionListener;
007import java.awt.event.WindowEvent;
008import java.beans.PropertyChangeEvent;
009import java.beans.PropertyChangeListener;
010import java.io.File;
011import java.io.IOException;
012
013import javax.annotation.CheckForNull;
014import javax.annotation.Nonnull;
015import javax.swing.*;
016import javax.swing.event.*;
017import javax.xml.parsers.DocumentBuilder;
018import javax.xml.parsers.DocumentBuilderFactory;
019import javax.xml.parsers.ParserConfigurationException;
020
021import jmri.jmrix.can.CanSystemConnectionMemo;
022import jmri.jmrix.can.cbus.CbusEvent;
023import jmri.jmrix.can.cbus.eventtable.CbusEventTableDataModel;
024import jmri.jmrix.can.cbus.node.*;
025import jmri.util.JmriJFrame;
026import jmri.util.StringUtil;
027import jmri.util.swing.JmriJOptionPane;
028
029import org.w3c.dom.Document;
030import org.w3c.dom.DOMException;
031import org.w3c.dom.Element;
032import org.w3c.dom.Node;
033import org.w3c.dom.NodeList;
034
035import org.xml.sax.SAXException;
036
037/**
038 *
039 * @author Steve Young Copyright (C) 2019
040 */
041public class CbusNodeRestoreFcuFrame extends JmriJFrame {
042
043    private CbusNodeFromFcuTableDataModel cbusNodeFcuDataModel;
044
045    private JTabbedPane tabbedPane;
046    private CbusNodeTableDataModel nodeModel;
047    private CanSystemConnectionMemo _memo;
048    private final NodeConfigToolPane mainpane;
049    private CbusNodeNVEditTablePane nodevarPane;
050    private CbusNodeEventTablePane nodeEventPane;
051    private JSplitPane split;
052    private CbusNodeInfoPane nodeinfoPane;
053    private JTable nodeTable;
054    private JButton openFCUButton;
055    private JButton nodeToBeTaughtButton;
056    private JLabel fileLocationDisplayLabel;
057    private JLabel eventTableRunningLabel;
058
059    private final JList<String> nodeToTeachTolist;
060
061    private JCheckBox teachNvsCheckBox;
062    private JCheckBox teachEventsCheckBox;
063    private JCheckBox resetEventsBeforeTeach;
064
065    private final PropertyChangeListener memoListener = this::updateEventTableActive;
066    private final TableModelListener nodeModelListener = this::updateNodeToTeachList;
067
068    /**
069     * Create a new instance of CbusNodeRestoreFcuFrame.
070     * @param main the main node table pane
071     */
072    public CbusNodeRestoreFcuFrame( NodeConfigToolPane main ) {
073        super();
074        mainpane = main;
075        nodeToTeachTolist = new JList<>();
076    }
077
078    public void initComponents(@Nonnull CanSystemConnectionMemo memo) {
079        _memo = memo;
080        cbusNodeFcuDataModel = new CbusNodeFromFcuTableDataModel(_memo, 2, CbusNodeFromFcuTableDataModel.FCU_MAX_COLUMN);
081        nodeModel = memo.get(CbusNodeTableDataModel.class);
082        initMainPane();
083        nodeModel.addTableModelListener(nodeModelListener);
084        _memo.addPropertyChangeListener(memoListener);
085    }
086
087    private void initMainPane() {
088        mainpane.setRestoreFcuActive(true);
089        JPanel infoPane = new JPanel();
090        infoPane.setLayout(new BorderLayout() );
091
092        JPanel topPanel = new JPanel();
093        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
094        JPanel selectFilePanel = new JPanel();
095
096        openFCUButton = new JButton(Bundle.getMessage("SelectFcuFile"));
097        selectFilePanel.add(openFCUButton );
098        fileLocationDisplayLabel = new JLabel();
099        selectFilePanel.add(fileLocationDisplayLabel);
100        topPanel.add(selectFilePanel);
101
102        JPanel eventTableRunningPanel = new JPanel();
103        eventTableRunningLabel = new JLabel();
104        updateEventTableActive(null);
105        eventTableRunningPanel.add(eventTableRunningLabel);
106        topPanel.add(eventTableRunningPanel);
107
108        infoPane.add(topPanel, BorderLayout.PAGE_START);
109        infoPane.add(getMiddlePane(), BorderLayout.CENTER);
110        infoPane.add(getNodeToBeTaughtButtonPane(), BorderLayout.PAGE_END);
111
112        this.add(infoPane);
113
114        pack();
115        this.setResizable(true);
116
117        validate();
118        repaint();
119
120        setTitle(getTitle());
121        setVisible(true);
122
123        addWindowListener(new java.awt.event.WindowAdapter() {
124            @Override
125            public void windowClosed(WindowEvent e) {
126                mainpane.setRestoreFcuActive(false);
127            }
128
129            @Override
130            public void windowClosing(WindowEvent e) {
131                mainpane.setRestoreFcuActive(false);
132            }
133        });
134
135        ActionListener save = ae -> {
136            // pre-validation checks, ie same nv's and same ev vars should be by button enabled
137            CbusNode fromNode = nodeFromSelectedRow();
138            CbusNode toNode = nodeFromSelectedList();
139            if ( fromNode == null || toNode == null ) {
140                return;
141            }
142            mainpane.showConfirmThenSave(fromNode, toNode,
143                teachNvsCheckBox.isSelected(),resetEventsBeforeTeach.isSelected(),
144                teachEventsCheckBox.isSelected(), this );
145        };
146        nodeToBeTaughtButton.addActionListener(save);
147
148        openFCUButton.addActionListener(this::selectInputFile);
149
150        nodeTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> {
151            if ( !e.getValueIsAdjusting() ) {
152                updateTabs();
153                updateRestoreNodeButton();
154            }
155        });
156
157        nodeToTeachTolist.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> {
158            if ( !e.getValueIsAdjusting() ) {
159                updateRestoreNodeButton();
160            }
161        });
162        updateRestoreNodeButton();
163    }
164
165    private JSplitPane getMiddlePane(){
166        
167        CbusNodeFcuTablePane fcuTablePane = new CbusNodeFcuTablePane();
168        fcuTablePane.initComponents(_memo,cbusNodeFcuDataModel);
169
170        nodeTable = fcuTablePane.nodeTable;
171
172        JPanel fcuPane = new JPanel();
173        fcuPane.setLayout(new BoxLayout(fcuPane, BoxLayout.Y_AXIS));
174        fcuPane.setPreferredSize(new Dimension(200, 150));
175        fcuPane.add(fcuTablePane);
176        
177        tabbedPane = new JTabbedPane();
178
179        nodeinfoPane = new CbusNodeInfoPane(null);
180
181        CbusNodeNVTableDataModel nodeNVModel = new CbusNodeNVTableDataModel(_memo, 5,
182            CbusNodeNVTableDataModel.MAX_COLUMN); // controller, row, column
183        nodevarPane = new CbusNodeNVEditTablePane(nodeNVModel);
184        nodevarPane.setNonEditable();
185
186        CbusNodeEventTableDataModel nodeEvModel = new CbusNodeEventTableDataModel( null, _memo, 10,
187            CbusNodeEventTableDataModel.MAX_COLUMN);
188        nodeEventPane = new CbusNodeEventTablePane(nodeEvModel);
189
190        nodeEventPane.setHideEditButton();
191
192        nodeEventPane.initComponents(_memo);
193
194        tabbedPane.addTab(Bundle.getMessage("NodeInfo"), nodeinfoPane);
195        tabbedPane.addTab(Bundle.getMessage("NodeVariables"), nodevarPane);
196        tabbedPane.addTab(Bundle.getMessage("NodeEvents"), nodeEventPane);
197
198        tabbedPane.addChangeListener((ChangeEvent e) -> {
199            updateTabs();
200        });
201
202        split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, fcuPane, tabbedPane);
203        split.setContinuousLayout(true);
204        return split;
205    }
206
207    private JPanel getNodeToBeTaughtButtonPane() {
208
209        JPanel nodeToBeTaughtButtonPane = new JPanel();
210
211        nodeToBeTaughtButtonPane.setBorder(BorderFactory.createTitledBorder(
212                BorderFactory.createEtchedBorder(), Bundle.getMessage("ChooseNodeToTeach")));
213
214        JPanel nodeToBeTaughtPane = new JPanel();
215        nodeToBeTaughtPane.setLayout(new BoxLayout(nodeToBeTaughtPane, BoxLayout.Y_AXIS));
216
217        updateNodeToTeachList(null);
218
219        nodeToTeachTolist.setLayoutOrientation(JList.VERTICAL);
220        nodeToTeachTolist.setVisibleRowCount(-1);
221        nodeToTeachTolist.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
222        JScrollPane listScroller = new JScrollPane(nodeToTeachTolist);
223        listScroller.setPreferredSize(new Dimension(300, 80));
224
225        nodeToBeTaughtPane.add(listScroller);
226        nodeToBeTaughtButtonPane.add(nodeToBeTaughtPane);
227
228        JPanel nodeToBeTaughtCheckboxPane = new JPanel();
229        nodeToBeTaughtCheckboxPane.setLayout(new BoxLayout(nodeToBeTaughtCheckboxPane, BoxLayout.Y_AXIS));
230
231        nodeToBeTaughtButton = new JButton(Bundle.getMessage("UpdateNodeButton"));
232
233        teachNvsCheckBox = new JCheckBox(Bundle.getMessage("WriteNVs"));
234        teachEventsCheckBox = new JCheckBox(Bundle.getMessage("WriteEvents"));
235        resetEventsBeforeTeach = new JCheckBox(Bundle.getMessage("CBUS_NNCLR"));
236
237        teachNvsCheckBox.setSelected(true);
238        teachEventsCheckBox.setSelected(true);
239        resetEventsBeforeTeach.setSelected(true);
240
241        nodeToBeTaughtCheckboxPane.add(teachNvsCheckBox);
242        nodeToBeTaughtCheckboxPane.add(resetEventsBeforeTeach);
243        nodeToBeTaughtCheckboxPane.add(teachEventsCheckBox);
244
245        nodeToBeTaughtButtonPane.add(nodeToBeTaughtCheckboxPane);
246        nodeToBeTaughtButtonPane.add(nodeToBeTaughtButton);
247        return nodeToBeTaughtButtonPane;
248    }
249
250    private void updateNodeToTeachList(TableModelEvent e) {
251        String before = nodeToTeachTolist.getSelectedValue();
252        String[] data = nodeModel.getListOfNodeNumberNames().toArray(new String[0]);
253        if ( data.length ==0 ){
254            data = new String[]{Bundle.getMessage("NodeTableEmpty")};
255        }
256        nodeToTeachTolist.setListData(data);
257        nodeToTeachTolist.setSelectedValue(before, true);
258    }
259
260    @CheckForNull
261    private CbusNode nodeFromSelectedRow() {
262        int sel = nodeTable.getSelectedRow();
263        if ( sel > -1 ) {
264            int modelIndex = nodeTable.convertRowIndexToModel(sel);
265            int nodenum = (int) nodeTable.getModel().getValueAt(modelIndex,
266                CbusNodeFromFcuTableDataModel.FCU_NODE_NUMBER_COLUMN);
267            return cbusNodeFcuDataModel.getNodeByNodeNum(nodenum);
268        } else {
269            return null;
270        }
271    }
272
273    /**
274     * Get the selected node to teach to.
275     * @return the node, if one is selected, else null.
276     */
277    @CheckForNull
278    private CbusNode nodeFromSelectedList() {
279        String obj = nodeToTeachTolist.getSelectedValue();
280        if ( obj == null ) {
281            return null;
282        }
283        int targetnodenum =  StringUtil.getFirstIntFromString(obj);
284        return nodeModel.getNodeByNodeNum(targetnodenum);
285    }
286
287    private void updateTabs() {
288        if ( nodeTable.getSelectedRow() > -1 ) {
289            switch (tabbedPane.getSelectedIndex()) {
290                case 1: // nv pane
291                    nodevarPane.setNode( nodeFromSelectedRow() );
292                    break;
293                case 2: // ev pane
294                    nodeEventPane.setNode( nodeFromSelectedRow() );
295                    break;
296                default: // info pane
297                    nodeinfoPane.setNode( nodeFromSelectedRow() );
298                    break;
299            }
300        }
301        else {
302            nodeinfoPane.setNode( null );
303        }
304    }
305
306    // only allow nodes with same amount of nv's and ev's
307    private void updateRestoreNodeButton() {
308
309        CbusNode nodeFrom = nodeFromSelectedRow();
310        if ( nodeFrom == null ) {
311            nodeToBeTaughtButton.setEnabled(false);
312            nodeToBeTaughtButton.setToolTipText("Select a Node from file in top table");
313            return;
314        }
315
316        CbusNode nodeTo = nodeFromSelectedList();
317        if ( nodeTo == null ) {
318            nodeToBeTaughtButton.setEnabled(false);
319            nodeToBeTaughtButton.setToolTipText("Select a target Node from list on left");
320            return;
321        }
322
323        if ( ( nodeFrom.getNodeNvManager().getTotalNVs() == nodeTo.getNodeNvManager().getTotalNVs() )
324            && ( nodeFrom.getNodeParamManager().getParameter(5) == nodeTo.getNodeParamManager().getParameter(5) ) ) {
325
326            nodeToBeTaughtButton.setEnabled(true);
327            nodeToBeTaughtButton.setToolTipText(null);
328            return;
329        }
330        // default
331        nodeToBeTaughtButton.setEnabled(false);
332        nodeToBeTaughtButton.setToolTipText("Both nodes must have same amount of NV's");
333    }
334
335    private static JFileChooser chooser;
336
337    private static void initChooser(){
338        if (chooser == null) {
339            chooser = jmri.jmrit.XmlFile.userFileChooser("XML Files", "xml", "XML");
340        }
341    }
342
343    private void selectInputFile(ActionEvent e){
344
345        initChooser();
346        chooser.rescanCurrentDirectory();
347        int retVal = chooser.showOpenDialog(this);
348        if (retVal != JFileChooser.APPROVE_OPTION) {
349            return;  // give up if no file selected
350        }
351
352        File testForXml = chooser.getSelectedFile();
353
354        if (!testForXml.getPath().toUpperCase().endsWith("XML")) {
355            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ImportNotXml"),
356                Bundle.getMessage("WarningTitle"), JmriJOptionPane.ERROR_MESSAGE);
357            return;
358        }
359
360        // success, open the file
361        addFile(testForXml);
362
363    }
364
365    protected void addFile(File inputFile) {
366
367        fileLocationDisplayLabel.setText( inputFile.toString() );
368
369        try {
370            cbusNodeFcuDataModel.resetData();
371            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
372            // disable DOCTYPE declaration & setXIncludeAware to reduce Sonar security warnings
373            dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
374            dbFactory.setXIncludeAware(false);
375            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
376            Document doc = dBuilder.parse(inputFile);
377            doc.getDocumentElement().normalize();
378
379            setNodesAndNVs(doc);
380            setEventstoNodes(doc);
381
382        }
383        catch (NumberFormatException | DOMException | IOException | ParserConfigurationException | SAXException e) {
384            log.warn("Error importing xml file. Valid xml?", e);
385            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("ImportError") + " Valid XML?"),
386                Bundle.getMessage("WarningTitle"), JmriJOptionPane.ERROR_MESSAGE);
387        }
388    }
389
390    private void setNodesAndNVs(@Nonnull Document doc) {
391        NodeList nodeList = doc.getElementsByTagName("userNodes");
392        for ( int temp = 0; temp < nodeList.getLength(); temp++) {
393
394            Node nNode = nodeList.item(temp);
395            Element eElement = (Element) nNode;
396            String nodeNum = eElement.getElementsByTagName("nodeNum").item(0).getTextContent();
397            String nodeName = eElement.getElementsByTagName("nodeName").item(0).getTextContent();
398            String moduleIdNum = eElement.getElementsByTagName("moduleId").item(0).getTextContent();
399            String moduleNvString = eElement.getElementsByTagName("NodeVars").item(0).getTextContent();
400            String nodeVersion = eElement.getElementsByTagName("Version").item(0).getTextContent();
401
402            int nodenum = Integer.parseInt(nodeNum);
403            int nodetype = Integer.parseInt(moduleIdNum);
404            if ( nodenum>0 ) {
405                CbusNodeFromBackup actualnode = cbusNodeFcuDataModel.provideNodeByNodeNum( nodenum );
406                actualnode.setNameIfNoName( nodeName );
407                actualnode.getNodeEventManager().resetNodeEvents();
408
409                log.debug("node version {}",nodeVersion);
410
411                int[] nvArray = StringUtil.intBytesWithTotalFromNonSpacedHexString(moduleNvString,true);
412
413                // 1st value, ie 7 is total params
414                int [] myarray = new int[] {7,165,-1,nodetype,-1,-1,nvArray[0],-1};
415                actualnode.getNodeParamManager().setParameters(myarray);
416                if (nvArray.length>1) {
417                    // log.info("node {} has {} nvs",actualnode,numNvs);
418                    actualnode.getNodeNvManager().setNVs( nvArray );
419                }
420            }
421        }
422    }
423
424    // loop through the events and add them to their nodes
425    private void setEventstoNodes(@Nonnull Document doc) {
426
427        NodeList eventList = doc.getElementsByTagName("userEvents"); // NOI18N
428        CbusEventTableDataModel eventModel = _memo.get(CbusEventTableDataModel.class);
429        if ( eventModel == null ) {
430            log.info("CBUS Event Table not running, no Event Names imported.");
431        }
432        for ( int temp = 0; temp < eventList.getLength(); temp++) {
433
434            Node nNode = eventList.item(temp);
435            Element eElement = (Element) nNode;
436
437            String hostNodeNumString = eElement.getElementsByTagName("ownerNode").item(0).getTextContent();
438            String event = eElement.getElementsByTagName("eventValue").item(0).getTextContent();
439            String eventNode = eElement.getElementsByTagName("eventNode").item(0).getTextContent();
440            String eventName = eElement.getElementsByTagName("eventName").item(0).getTextContent();
441            String eventVars = eElement.getElementsByTagName("Values").item(0).getTextContent();
442
443            int hostNodeNum = Integer.parseInt(hostNodeNumString);
444            int eventNum = Integer.parseInt(event);
445            int eventNodeNum = Integer.parseInt(eventNode);
446            log.debug("event host {} event {} event node {} vars {}",hostNodeNum,eventNum,eventNodeNum,eventVars);
447
448            if ( ( hostNodeNum > 0 )  ) {
449                CbusNodeFromBackup hostNode = cbusNodeFcuDataModel.provideNodeByNodeNum( hostNodeNum );
450                int[] evVarArray = StringUtil.intBytesWithTotalFromNonSpacedHexString(eventVars,false);
451
452                if ( !eventVars.isEmpty() && hostNode.getNodeParamManager().getParameter(5) < 0 ) {
453                    hostNode.getNodeParamManager().setParameter(5,evVarArray.length);
454                }
455
456                CbusNodeEvent ev = new CbusNodeEvent(_memo,eventNodeNum,eventNum,hostNodeNum,-1,hostNode.getNodeParamManager().getParameter(5));
457                ev.setEvArr(evVarArray);
458                ev.setName(eventName);
459                ev.setTempFcuNodeName(cbusNodeFcuDataModel.getNodeName( eventNodeNum ) );
460                hostNode.getNodeEventManager().addNewEvent(ev);
461            }
462
463            if ( eventModel != null ) {
464                CbusEvent ev = eventModel.provideEvent(eventNodeNum,eventNum);
465                ev.setNameIfNoName(eventName);
466            }
467        }
468    }
469
470    private void updateEventTableActive(PropertyChangeEvent evt) {
471        CbusEventTableDataModel eventModel = _memo.get(CbusEventTableDataModel.class);
472        eventTableRunningLabel.setText(Bundle.getMessage( eventModel==null ? 
473            "EventTableNotRunning" : "EventsImportToTable"));
474    }
475
476    @Override
477    public String getTitle() {
478        return Bundle.getMessage("FcuImportTitle");
479    }
480
481    @Override
482    public void dispose() {
483        if ( _memo != null) {
484            _memo.removePropertyChangeListener(memoListener);
485        }
486        if ( nodeModel !=null ) {
487            nodeModel.removeTableModelListener(nodeModelListener);
488        }
489        super.dispose();
490    }
491
492    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusNodeRestoreFcuFrame.class);
493
494}