001package jmri.jmrix.can.cbus.node;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.text.SimpleDateFormat;
007import java.util.ArrayList;
008import java.util.Date;
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011
012import jmri.jmrix.can.CanSystemConnectionMemo;
013import jmri.jmrix.can.cbus.CbusPreferences;
014import jmri.jmrix.can.cbus.node.CbusNodeConstants.BackupType;
015import jmri.util.FileUtil;
016import jmri.util.StringUtil;
017import jmri.util.ThreadingUtil;
018import org.jdom2.Document;
019import org.jdom2.Element;
020import org.jdom2.JDOMException;
021
022
023/**
024 * Class to work with CbusNode xml files
025 * Loosely based on
026 * Load and store the timetable data file: TimeTableData.xml
027 * @author Dave Sand Copyright (C) 2018
028 * @author Steve Young Copyright (C) 2019
029 */
030public class CbusNodeBackupManager {
031
032    public final SimpleDateFormat xmlDateStyle = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss"); // NOI18N
033    private int _nodeNum = 0;
034    private final CbusBasicNodeWithManagers _node;
035    private ArrayList<CbusNodeFromBackup> _backupInfos;
036    private boolean backupInit; // node details loaded from file
037    private boolean backupStarted; // auto startup backup started
038
039    /**
040     * Create a new CbusNodeBackupManager
041     * @param node the CbusNode which the xml is associated with
042     */
043    public CbusNodeBackupManager(CbusBasicNodeWithManagers node) {
044        _nodeNum = node.getNodeNumber();
045        _node = node;
046        _backupInfos = new ArrayList<>(5);
047        
048        backupInit = false;
049        backupStarted = false;
050        // doLoad();
051        
052    }
053
054    /**
055     * Get a list of all of the backups currently in the xml file
056     * @return may be zero length if no backups
057     */
058    public ArrayList<CbusNodeFromBackup> getBackups() {
059        return _backupInfos;
060    }
061
062    public int getNumCompleteBackups() {
063        int i=0;
064        for (int j = 0; j <_backupInfos.size() ; j++) {
065            if (_backupInfos.get(j).getBackupResult() == BackupType.COMPLETE ){
066                i++;
067            }
068        }
069        return i;
070    }
071
072    /**
073     * Get the time of first full backup for the Node.
074     *
075     * @return value else null if unknown
076     */
077    @CheckForNull
078    public java.util.Date getFirstBackupTime() {
079        for (int j = _backupInfos.size()-1; j >-1 ; j--) {
080            if ( _backupInfos.get(j).getBackupResult() == BackupType.COMPLETE ){
081                return _backupInfos.get(j).getBackupTimeStamp();
082            }
083        }
084        return null;
085    }
086
087    /**
088     * Get the time of last full backup for the Node.
089     *
090     * @return value else null if unknown
091     */
092    @CheckForNull
093    public java.util.Date getLastBackupTime() {
094        for (int j = 0; j <_backupInfos.size(); j++) {
095            if ( _backupInfos.get(j).getBackupResult() == BackupType.COMPLETE ){
096                return _backupInfos.get(j).getBackupTimeStamp();
097            }
098        }
099        return null;
100    }
101
102    /**
103     * Get number of backups in arraylist that are complete, do no have a comment
104     * and could potentially be deleted.
105     */
106    private int numAutoBackups(){
107        int i=0;
108        for (int j = _backupInfos.size()-1; j >-1 ; j--) {
109            if (_backupInfos.get(j).getBackupComment().isEmpty()
110                && _backupInfos.get(j).getBackupResult() == BackupType.COMPLETE ){
111                i++;
112            }
113        }
114        return i;
115    }
116
117    /**
118     * Delete older backups depending on user pref. number
119     */
120    private void trimBackups(){
121        CbusPreferences preferences;
122        CanSystemConnectionMemo memo = _node.getMemo();
123        if ( memo != null ) {
124            preferences = memo.get(CbusPreferences.class);
125        } else {
126            preferences = jmri.InstanceManager.getDefault(CanSystemConnectionMemo.class).get(CbusPreferences.class);
127        }
128        if (preferences==null) {
129            return;
130        }
131
132        // note size-2 means we never delete the oldest one
133        for (int i = _backupInfos.size()-2; i >-1 ; i--) {
134            if ( numAutoBackups()<=preferences.getMinimumNumBackupsToKeep()){
135                return;
136            }
137            if ( _backupInfos.get(i).getBackupComment().isEmpty()){
138                _backupInfos.remove(i);
139            }
140        }
141    }
142
143    /**
144     * Full XML load.
145     * Searches for XML file for the node and reads info
146     * Sets internal flag so can only be triggered once
147     */
148    public final void doLoad(){
149        CbusNodeBackupFile x = new CbusNodeBackupFile(_node.getMemo());
150
151        if (!( _node instanceof CbusNode)){
152            return;
153        }
154
155        if (backupInit) {
156            return;
157        }
158
159        backupInit = true;
160
161        ThreadingUtil.runOnLayout( () -> {
162
163            File file = x.getFile(_node.getNodeNumber(),true);
164
165            if (file == null) {
166                log.debug("No backup file to load");
167                return;
168            }
169            boolean _sortOnLoad = true;
170            // Find root
171            Element root;
172
173            try {
174                root = x.rootFromFile(file);
175                if (root == null) {
176                    log.info("File could not be read");
177                    return;
178                }
179
180                Element details;
181
182                // UserName
183                details = root.getChild("UserName");  // NOI18N
184                if (details != null && (!details.getValue().isEmpty())) {
185                    ((CbusNode) _node).setUserName(details.getValue());
186                }
187
188                // Module Type Name
189                details = root.getChild("ModuleTypeName");  // NOI18N
190                if (details != null && (!details.getValue().isEmpty())) {
191                   ((CbusNode) _node).setNodeNameFromName(details.getValue());
192                }
193
194                // user Comments Freetext
195                details = root.getChild("FreeText");  // NOI18N
196                if (details != null && (!details.getValue().isEmpty())) {
197                    ((CbusNode) _node).setUserComment(
198                        details.getValue().replaceAll("\\\\n",System.getProperty("line.separator")));
199                }
200
201                Element BackupStatus = root.getChild("Backups");  // NOI18N
202                if (BackupStatus == null) {
203                    log.warn("Unable to find a Previous Layout Backup Entry");
204                }
205                else {
206                    for (Element info : BackupStatus.getChildren("BackupInfo")) {  // NOI18N
207                        boolean _backupInfoError = false;
208                        CbusNodeFromBackup nodeBackup = new CbusNodeFromBackup(null,_nodeNum);
209
210                        if ( info.getAttributeValue("dateTimeStamp") !=null ) { // NOI18N
211                            try {
212                                Date newDate = xmlDateStyle.parse(info.getAttributeValue("dateTimeStamp")); // NOI18N
213                                nodeBackup.setBackupTimeStamp( newDate ); // temp
214                            } catch (java.text.ParseException e) {
215                                log.error("Unable to parse date {}",info.getAttributeValue("dateTimeStamp")); // NOI18N
216                                _sortOnLoad = false;
217                                _backupInfoError = true;
218                            }
219                        } else {
220                            log.error("NO datetimestamp in a backup log entry");
221                            _sortOnLoad = false;
222                            _backupInfoError = true;
223                        }
224                        if ( info.getAttributeValue("result") !=null &&  // NOI18N
225                            CbusNodeConstants.lookupByName(info.getAttributeValue("result"))!=null ) { // NOI18N
226                            nodeBackup.setBackupResult(CbusNodeConstants.lookupByName(info.getAttributeValue("result")) );
227                        } else {
228                            log.error("NO result in a backup log entry");
229                            _backupInfoError = true;
230                            nodeBackup.setBackupResult(BackupType.INCOMPLETE);
231                        }
232                        Element params = info.getChild("Parameters");  // NOI18N
233                        if ( params !=null && !params.getValue().isEmpty()) {
234                            nodeBackup.getNodeParamManager().setParameters(StringUtil.intBytesWithTotalFromNonSpacedHexString(params.getValue(),true));
235                        } else {
236                            _backupInfoError = true;
237                        }
238                        Element nvs = info.getChild("NodeVariables");  // NOI18N
239                        if ( nvs !=null ) {
240                            nodeBackup.getNodeNvManager().setNVs(StringUtil.intBytesWithTotalFromNonSpacedHexString(nvs.getValue(),true));
241                        }
242                        Element comment = info.getChild("Comment");  // NOI18N
243                        if ( comment !=null ) {
244                            nodeBackup.setBackupComment(comment.getValue());
245                        }
246                        Element events = info.getChild("NodeEvents");  // NOI18N
247                        if ( events !=null ) {
248                            for (Element xmlEvent : events.getChildren("NodeEvent")) {  // NOI18N
249                                if ( xmlEvent.getAttributeValue("NodeNum") !=null &&  // NOI18N
250                                    xmlEvent.getAttributeValue("EventNum") !=null &&  // NOI18N
251                                    xmlEvent.getAttributeValue("EvVars") !=null ) {  // NOI18N
252
253                                    // check event variable length matches expected length in parameters
254                                    if (nodeBackup.getNodeParamManager().getParameter(5)!=(xmlEvent.getAttributeValue("EvVars").length()/2)) {
255                                        jmri.util.LoggingUtil.warnOnce(log, "Incorrect Event Variable Length in Backup for Node {}", nodeBackup.getNodeNumber());
256                                        _backupInfoError = true;
257                                    }
258                                    try {
259                                        nodeBackup.addBupEvent(
260                                            Integer.parseInt(xmlEvent.getAttributeValue("NodeNum")),
261                                            Integer.parseInt(xmlEvent.getAttributeValue("EventNum")),
262                                            xmlEvent.getAttributeValue("EvVars"));
263                                    } catch (java.lang.NumberFormatException ex) {
264                                        jmri.util.LoggingUtil.warnOnce(log,"Incorrect Node / Event Number in Backup for Node {}", nodeBackup.getNodeNumber());
265                                        _backupInfoError = true;
266                                    }
267                                }
268                                else {
269                                    log.error("Node / Event Number Missing in Backup");
270                                    _backupInfoError = true;
271                                }
272                            }
273                        }
274
275
276                        if (_backupInfoError && nodeBackup.getBackupResult()==BackupType.COMPLETE ){
277                            nodeBackup.setBackupResult(BackupType.INCOMPLETE);
278                        }
279
280                        _backupInfos.add(nodeBackup);
281                    }
282                }
283
284
285
286            } catch (JDOMException ex) {
287                log.error("File invalid: {}",file.getName(), ex);  // NOI18N
288                return;
289            } catch (IOException ex) {
290                // file might not yet exist as 1st time Node on Network
291                log.debug("Possible Error reading file: ", ex);  // NOI18N
292                return;
293            }
294            // make sure ArrayList is most recent at start array index 0, oldest at end
295
296            if (_sortOnLoad) {
297                java.util.Collections.sort(_backupInfos, java.util.Collections.reverseOrder());
298            }
299            _node.notifyPropertyChangeListener("BACKUPS", null, null);
300
301
302        });
303
304
305    }
306
307    /**
308     * Save the xml to user profile
309     * trims backup list as per user pref.
310     * @param createNew if true, creates a new backup then saves, false just saves
311     * @param seenErrors if true sets backup completed with errors, else logs as backup complete
312     * @return true if all OK, else false if error occurred
313     */
314    public boolean doStore( boolean createNew, boolean seenErrors) {
315
316        if (!( _node instanceof CbusNode)){
317            return false;
318        }
319
320        setBackupStarted(true);
321
322        Date thisBackupDate = new Date();
323        CbusNodeBackupFile x = new CbusNodeBackupFile(_node.getMemo());
324        File file = x.getFile(_node.getNodeNumber(),true);
325
326        if (file == null) {
327            log.error("Unable to get backup file prior to save");  // NOI18N
328            return false;
329        }
330
331        doRotate();
332
333        if ( createNew ) {
334            _backupInfos.add(0,new CbusNodeFromBackup((CbusNode) _node,thisBackupDate));
335            if (seenErrors){
336                _backupInfos.get(0).setBackupResult(BackupType.COMPLETEDWITHERROR);
337            }
338        }
339
340        // now we trim the number of backups in the list
341        trimBackups();
342
343        // Create root element
344        Element root = new Element("CbusNode");  // NOI18N
345        root.setAttribute("noNamespaceSchemaLocation", // NOI18N
346            "https://raw.githubusercontent.com/MERG-DEV/JMRI/master/schema/MergCBUSNodeBackup.xsd",  // NOI18N
347            org.jdom2.Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")); // NOI18N
348        root.setAttribute("NodeNum", ""+_node.getNodeNumber() );  // NOI18N
349
350        if (!((CbusNode) _node).getUserName().isEmpty()) {
351            root.addContent(new Element("UserName").addContent(((CbusNode) _node).getUserName() )); // NOI18N
352        }
353        if (!((CbusNode) _node).getNodeNameFromName().isEmpty()) {
354            root.addContent(new Element("ModuleTypeName").addContent(((CbusNode) _node).getNodeNameFromName() )); // NOI18N
355        }
356        if (!((CbusNode) _node).getUserComment().isEmpty()) {
357            root.addContent(new Element("FreeText").addContent( // NOI18N
358                ((CbusNode) _node).getUserComment().replaceAll("\r\n|\n|\r", "\\\\n")));
359        }
360
361        Document doc = new Document(root);
362        Element values = new Element("Backups");
363        root.addContent(values);  // NOI18N
364        _backupInfos.stream().map((node) -> {
365            Element e = new Element("BackupInfo");  // NOI18N
366            e.setAttribute("dateTimeStamp",xmlDateStyle.format( node.getBackupTimeStamp() ));  // NOI18N
367            e.setAttribute("result","" + node.getBackupResult());  // NOI18N
368            if (!node.getBackupComment().isEmpty()) {
369                e.addContent(new Element("Comment").addContent("" + node.getBackupComment() ));  // NOI18N
370            }
371            if (!node.getNodeParamManager().getParameterHexString().isEmpty()) {
372                e.addContent(new Element("Parameters").addContent("" + node.getNodeParamManager().getParameterHexString() ));  // NOI18N
373            }
374            if (!node.getNodeNvManager().getNvHexString().isEmpty()) {
375                e.addContent(new Element("NodeVariables").addContent("" + node.getNodeNvManager().getNvHexString() ));  // NOI18N
376            }
377            if (node.getNodeEventManager().getTotalNodeEvents()>0) {
378                // log.info("events on backup node");
379                Element bupev = new Element("NodeEvents"); // NOI18N
380
381                ArrayList<CbusNodeEvent> _tmpArr = node.getNodeEventManager().getEventArray();
382                if ( _tmpArr!=null ) {
383                    _tmpArr.forEach((bupndev) -> {
384                        Element ndev = new Element("NodeEvent"); // NOI18N
385                        ndev.setAttribute("NodeNum","" + bupndev.getNn());  // NOI18N
386                        ndev.setAttribute("EventNum","" + bupndev.getEn());  // NOI18N
387                        ndev.setAttribute("EvVars","" + bupndev.getHexEvVarString());  // NOI18N
388                        bupev.addContent(ndev);
389                    });
390                    e.addContent(bupev);
391                }
392            }
393            return e;
394        }).forEachOrdered((e) -> {
395            values.addContent(e);
396        });
397        try {
398            x.writeXML(file, doc);
399        } catch (FileNotFoundException ex) {
400            log.error("File not found when writing: ", ex);  // NOI18N
401            return false;
402        } catch (IOException ex) {
403            log.error("IO Exception when writing: ", ex);  // NOI18N
404            return false;
405        }
406
407        log.debug("...done");  // NOI18N
408        _node.notifyPropertyChangeListener("BACKUPS", null, null);
409        return true;
410    }
411
412    /**
413     * Add an xml entry advising Node Not on Network
414     */
415    protected void nodeNotOnNetwork(){
416        if (_node instanceof CbusNode) {
417            CbusNodeFromBackup newBup = new CbusNodeFromBackup((CbusNode)_node,new Date());
418            newBup.setBackupResult(BackupType.NOTONNETWORK);
419            _backupInfos.add(0,newBup);
420            doStore(false, false);
421        }
422    }
423
424    /**
425     * Add an xml entry advising Node in SLiM Mode
426     */
427    protected void nodeInSLiM(){
428        if (_node instanceof CbusNode) {
429            CbusNodeFromBackup newBup = new CbusNodeFromBackup((CbusNode)_node,new Date());
430            newBup.setBackupResult(BackupType.SLIM);
431            _backupInfos.add(0,newBup);
432            doStore(false, false);
433        }
434    }
435
436    /**
437     * Remove Node XML File
438     * @param rotate if true, creates and rotates .bup files before delete, false just deletes the core node file
439     * @return true on success, else false
440     */
441    protected boolean removeNode( boolean rotate){
442        CbusNodeBackupFile x = new CbusNodeBackupFile(_node.getMemo());
443        if (rotate) {
444            doRotate();
445        }
446        if ( !x.deleteFile(_node.getNodeNumber())){
447            log.error("Unable to delete node xml file");
448            return false;
449        }
450        return true;
451    }
452
453    private void doRotate(){
454        CbusNodeBackupFile x = new CbusNodeBackupFile(_node.getMemo());
455        File file = x.getFile(_node.getNodeNumber(),false);
456        if (file == null){
457            return;
458        }
459        try {
460            Element roots = x.rootFromFile(file);
461            if ( roots == null ) return;
462            FileUtil.rotate(file, 5, "bup");  // NOI18N
463        } catch (IOException ex) {
464            // the file might not exist
465            log.debug("Backup Rotate failed {}",file);  // NOI18N
466        } catch (JDOMException ex) {
467            // file might not exist
468            log.debug("File invalid: {}", file);  // NOI18N
469        }
470    }
471
472    /**
473     * Get the XML File Location
474     * @return Location of the file, creating new if required
475     */
476    protected File getFileLocation() {
477        return new CbusNodeBackupFile(_node.getMemo()).getFile(_node.getNodeNumber(),true);
478    }
479
480    /**
481     * Reset the backup array for testing
482     */
483    protected void resetBupArray() {
484        _backupInfos = new ArrayList<>(5);
485        backupInit = false;
486    }
487
488    /**
489     * Get the current backup status for the Node.
490     *
491     * @return ENUM from CbusNodeConstants, e.g. BackupType.OUTSTANDING or BackupType.COMPLETE
492     */
493    @Nonnull
494    public BackupType getSessionBackupStatus() {
495        if (backupStarted && !getBackups().isEmpty() ) {
496            return getBackups().get(0).getBackupResult();
497        } else {
498            return BackupType.OUTSTANDING;
499        }
500    }
501
502    /**
503     * Set internal flag for backup started.
504     * Triggered within the backup script which is called from various places
505     * @param started true if started
506     */
507    protected void setBackupStarted( boolean started) {
508        backupStarted = started;
509    }
510
511    protected boolean getBackupStarted(){
512        return backupStarted;
513    }
514
515    protected void setNodeInSlim() {
516        log.info("Node {} in SLiM mode",_node);
517        if (getBackupStarted()) { // 1st time in this session
518            doLoad();
519            nodeInSLiM();
520        }
521    }
522
523    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusNodeBackupManager.class);
524}