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}