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