001package jmri.jmrit.operations.setup.backup; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.io.*; 006import java.time.LocalDateTime; 007import java.time.format.DateTimeFormatter; 008import java.util.Arrays; 009 010import jmri.InstanceManagerAutoDefault; 011import jmri.beans.PropertyChangeSupport; 012import jmri.jmrit.XmlFile; 013import jmri.jmrit.operations.OperationsXml; 014 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018/** 019 * Base class for backing up and restoring Operations working files. Derived 020 * classes implement specifics for working with different backup set stores, 021 * such as Automatic and Default backups. 022 * 023 * @author Gregory Madsen Copyright (C) 2012 024 */ 025public abstract class BackupBase extends PropertyChangeSupport implements InstanceManagerAutoDefault { 026 027 private static final Logger log = LoggerFactory.getLogger(BackupBase.class); 028 029 // Property Changes 030 public static final String COPY_FILES_CHANGED_PROPERTY = "Backup Copy Files"; 031 032 // Just for testing...... 033 // If this is not null, it will be thrown to simulate various IO exceptions 034 // that are hard to reproduce when running tests.. 035 public RuntimeException testException = null; 036 037 // The root directory for all Operations files, usually 038 // "user / name / JMRI / operations" 039 protected File _operationsRoot = null; 040 041 public File getOperationsRoot() { 042 return _operationsRoot; 043 } 044 045 // This will be set to the appropriate backup root directory from the 046 // derived 047 // classes, as their constructor will fill in the correct directory. 048 protected File _backupRoot; 049 050 public File getBackupRoot() { 051 return _backupRoot; 052 } 053 054 // These constitute the set of files for a complete backup set. 055 private final String[] _backupSetFileNames = new String[]{"Operations.xml", // NOI18N 056 "OperationsCarRoster.xml", "OperationsEngineRoster.xml", // NOI18N 057 "OperationsLocationRoster.xml", "OperationsRouteRoster.xml", // NOI18N 058 "OperationsTrainRoster.xml"}; // NOI18N 059 060 private final String _demoPanelFileName = "Operations Demo Panel.xml"; // NOI18N 061 062 public String[] getBackupSetFileNames() { 063 return _backupSetFileNames.clone(); 064 } 065 066 /** 067 * Creates a BackupBase instance and initializes the Operations root 068 * directory to its normal value. 069 * @param rootName Directory name to use. 070 */ 071 protected BackupBase(String rootName) { 072 // A root directory name for the backups must be supplied, which will be 073 // from the derived class constructors. 074 if (rootName == null) { 075 throw new IllegalArgumentException("Backup root name can't be null"); // NOI18N 076 } 077 _operationsRoot = new File(OperationsXml.getFileLocation(), OperationsXml.getOperationsDirectoryName()); 078 079 _backupRoot = new File(getOperationsRoot(), rootName); 080 081 // Make sure it exists 082 if (!getBackupRoot().exists()) { 083 Boolean ok = getBackupRoot().mkdirs(); 084 if (!ok) { 085 throw new RuntimeException("Unable to make directory: " // NOI18N 086 + getBackupRoot().getAbsolutePath()); 087 } 088 } 089 090 // We maybe want to check if it failed and throw an exception. 091 } 092 093 /** 094 * Backs up Operations files to the named backup set under the backup root 095 * directory. 096 * 097 * @param setName The name of the new backup set 098 * @throws java.io.IOException Due to trouble writing files 099 * @throws IllegalArgumentException if string null or empty 100 */ 101 public void backupFilesToSetName(String setName) throws IOException, IllegalArgumentException { 102 validateNotNullOrEmpty(setName); 103 104 copyBackupSet(getOperationsRoot(), new File(getBackupRoot(), setName)); 105 } 106 107 private void validateNotNullOrEmpty(String s) throws IllegalArgumentException { 108 if (s == null || s.trim().length() == 0) { 109 throw new IllegalArgumentException( 110 "string cannot be null or empty."); // NOI18N 111 } 112 113 } 114 115 /** 116 * Creates backup files for the directory specified. Assumes that 117 * backupDirectory is a fully qualified path where the individual files will 118 * be created. This will backup files to any directory which does not have 119 * to be part of the JMRI hierarchy. 120 * 121 * @param backupDirectory The directory to use for the backup. 122 * @throws java.io.IOException Due to trouble writing files 123 */ 124 public void backupFilesToDirectory(File backupDirectory) throws IOException { 125 copyBackupSet(getOperationsRoot(), backupDirectory); 126 } 127 128 /** 129 * Returns a sorted list of the Backup Sets under the backup root. 130 * @return A sorted backup list. 131 * 132 */ 133 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 134 justification = "not possible") // NOI18N 135 public String[] getBackupSetList() { 136 String[] setList = getBackupRoot().list(); 137 // no guarantee of order, so we need to sort 138 Arrays.sort(setList); 139 return setList; 140 } 141 142 public File[] getBackupSetDirs() { 143 // Returns a list of File objects for the backup sets in the 144 // backup store. 145 // Not used at the moment, and can probably be removed in favor of 146 // getBackupSets() 147 File[] dirs = getBackupRoot().listFiles(); 148 149 return dirs; 150 } 151 152 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 153 justification = "not possible") // NOI18N 154 public BackupSet[] getBackupSets() { 155 // This is a bit of a kludge for now, until I learn more about dynamic 156 // sets 157 File[] dirs = getBackupRoot().listFiles(); 158 Arrays.sort(dirs); 159 BackupSet[] sets = new BackupSet[dirs.length]; 160 161 for (int i = 0; i < dirs.length; i++) { 162 sets[i] = new BackupSet(dirs[i]); 163 } 164 165 return sets; 166 } 167 168 /** 169 * Check to see if the given backup set already exists in the backup store. 170 * @param setName The directory name to check. 171 * 172 * @return true if it exists 173 */ 174 public boolean checkIfBackupSetExists(String setName) { 175 // This probably needs to be simplified, but leave for now. 176 177 try { 178 validateNotNullOrEmpty(setName); 179 File file = new File(getBackupRoot(), setName); 180 181 if (file.exists()) { 182 return true; 183 } 184 } catch (Exception e) { 185 log.error("Exception during backup set directory exists check"); 186 } 187 return false; 188 } 189 190 /** 191 * Restores a Backup Set with the given name from the backup store. 192 * @param setName The directory name. 193 * 194 * @throws java.io.IOException Due to trouble loading files 195 */ 196 public void restoreFilesFromSetName(String setName) throws IOException { 197 copyBackupSet(new File(getBackupRoot(), setName), getOperationsRoot()); 198 } 199 200 /** 201 * Restores a Backup Set from the given directory. 202 * @param directory The File directory. 203 * 204 * @throws java.io.IOException Due to trouble loading files 205 */ 206 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST", 207 justification = "I18N of Info Message") 208 public void restoreFilesFromDirectory(File directory) throws IOException { 209 log.info(Bundle.getMessage("InfoRestoringDirectory", directory.getAbsolutePath())); 210 211 copyBackupSet(directory, getOperationsRoot()); 212 } 213 214 /** 215 * Copies a complete set of Operations files from one directory to another 216 * directory. Usually used to copy to or from a backup location. Creates the 217 * destination directory if it does not exist. 218 * 219 * Only copies files that are included in the list of Operations files. 220 * @param sourceDir From Directory 221 * @param destDir To Directory 222 * 223 * @throws java.io.IOException Due to trouble reading or writing 224 */ 225 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 226 justification="I18N of Info Message") 227 public void copyBackupSet(File sourceDir, File destDir) throws IOException { 228 log.debug("copying backup set from: {} to: {}", sourceDir, destDir); 229 log.info(Bundle.getMessage("InfoSavingCopy", destDir)); 230 231 if (!sourceDir.exists()) // This throws an exception, as the dir should 232 // exist. 233 { 234 throw new IOException("Backup Set source directory: " // NOI18N 235 + sourceDir.getAbsolutePath() + " does not exist"); // NOI18N 236 } 237 // See how many Operations files we have. If they are all there, carry 238 // on, if there are none, just return, any other number MAY be an error, 239 // so just log it. 240 // We can't throw an exception, as this CAN be a valid state. 241 // There is no way to tell if a missing file is an error or not the way 242 // the files are created. 243 244 int sourceCount = getSourceFileCount(sourceDir); 245 246 if (sourceCount == 0) { 247 log.debug("No source files found in {} so skipping copy.", sourceDir.getAbsolutePath()); // NOI18N 248 return; 249 } 250 251 if (sourceCount != _backupSetFileNames.length) { 252 log.warn("Only {} file(s) found in directory {}", sourceCount, sourceDir.getAbsolutePath()); 253 // throw new IOException("Only " + sourceCount 254 // + " file(s) found in directory " 255 // + sourceDir.getAbsolutePath()); 256 } 257 258 // Ensure destination directory exists 259 if (!destDir.exists()) { 260 // Note that mkdirs does NOT throw an exception on error. 261 // It will return false if the directory already exists. 262 boolean result = destDir.mkdirs(); 263 264 if (!result) { 265 // This needs to use a better Exception class..... 266 throw new IOException( 267 destDir.getAbsolutePath() + " (Could not create all or part of the Backup Set path)"); // NOI18N 268 } 269 } 270 271 // Just copy the specific Operations files, now that we know they are 272 // all there. 273 for (String name : _backupSetFileNames) { 274 log.debug("copying file: {}", name); 275 276 File src = new File(sourceDir, name); 277 278 if (src.exists()) { 279 File dst = new File(destDir, name); 280 281 FileHelper.copy(src.getAbsolutePath(), dst.getAbsolutePath(), true); 282 } else { 283 log.debug("Source file: {} does not exist, and is not copied.", src.getAbsolutePath()); 284 } 285 286 } 287 288 // Throw a test exception, if we have one. 289 if (testException != null) { 290 testException.fillInStackTrace(); 291 throw testException; 292 } 293 firePropertyChange(COPY_FILES_CHANGED_PROPERTY, sourceDir, destDir); 294 } 295 296 /** 297 * Checks to see how many of the Operations files are present in the source 298 * directory. 299 * @param sourceDir The Directory to check. 300 * 301 * @return number of files 302 */ 303 public int getSourceFileCount(File sourceDir) { 304 int count = 0; 305 Boolean exists; 306 307 for (String name : _backupSetFileNames) { 308 exists = new File(sourceDir, name).exists(); 309 if (exists) { 310 count++; 311 } 312 } 313 314 return count; 315 } 316 317 /** 318 * Reloads the demo Operations files that are distributed with JMRI. 319 * 320 * @throws java.io.IOException Due to trouble loading files 321 */ 322 public void loadDemoFiles() throws IOException { 323 File fromDir = new File(XmlFile.xmlDir(), "demoOperations"); // NOI18N 324 copyBackupSet(fromDir, getOperationsRoot()); 325 326 // and the demo panel file 327 log.debug("copying file: {}", _demoPanelFileName); 328 329 File src = new File(fromDir, _demoPanelFileName); 330 File dst = new File(getOperationsRoot(), _demoPanelFileName); 331 332 FileHelper.copy(src.getAbsolutePath(), dst.getAbsolutePath(), true); 333 } 334 335 /** 336 * Searches for an unused directory name, based on the default base name, 337 * under the given directory. A name suffix as appended to the base name and 338 * can range from 00 to 99. 339 * 340 * @return A backup set name that is not already in use. 341 */ 342 public String suggestBackupSetName() { 343 // Start with a base name that is derived from today's date 344 // This checks to see if the default name already exists under the given 345 // backup root directory. 346 // If it exists, the name is incremented by 1 up to 99 and checked 347 // again. 348 String baseName = getDate(); 349 String fullName = null; 350 String[] dirNames = getBackupRoot().list(); 351 352 // Check for up to 100 backup file names to see if they already exist 353 for (int i = 0; i < 99; i++) { 354 // Create the trial name, then see if it already exists. 355 fullName = String.format("%s_%02d", baseName, i); // NOI18N 356 357 boolean foundFileNameMatch = false; 358 for (String name : dirNames) { 359 if (name.equals(fullName)) { 360 foundFileNameMatch = true; 361 break; 362 } 363 } 364 if (!foundFileNameMatch) { 365 return fullName; 366 } 367 368 // This should also work, commented out by D. Boudreau 369 // The Linux problem turned out to be related to the order 370 // files names are returned by list(). 371 // File testPath = new File(_backupRoot, fullName); 372 // 373 // if (!testPath.exists()) { 374 // return fullName; // Found an unused name 375 // Otherwise complain and keep trying... 376 log.debug("Operations backup directory: {} already exists", fullName); // NOI18N 377 } 378 379 // If we get here, we have tried all 100 variants without success. This 380 // should probably throw an exception, but for now it just returns the 381 // last file name tried. 382 return fullName; 383 } 384 385 /** 386 * Reset Operations by deleting XML files, leaves directories and backup 387 * files in place. 388 */ 389 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 390 justification = "not possible") // NOI18N 391 public void deleteOperationsFiles() { 392 // TODO Maybe this should also only delete specific files used by Operations, 393 // and not just all XML files. 394 File files = getOperationsRoot(); 395 396 if (!files.exists()) { 397 return; 398 } 399 400 String[] operationFileNames = files.list(); 401 for (String fileName : operationFileNames) { 402 // skip non-xml files 403 if (!fileName.toUpperCase().endsWith(".XML")) // NOI18N 404 { 405 continue; 406 } 407 // 408 log.debug("deleting file: {}", fileName); 409 File file = new File(getOperationsRoot() + File.separator + fileName); 410 if (!file.delete()) { 411 log.debug("file not deleted"); 412 } 413 // TODO This should probably throw an exception if a delete fails. 414 } 415 } 416 417 /** 418 * Returns the current date formatted for use as part of a Backup Set name. 419 */ 420 private String getDate() { 421 LocalDateTime date = LocalDateTime.now(); 422 DateTimeFormatter simpleDateFormat = DateTimeFormatter.ofPattern("yyyy_MM_dd"); // NOI18N 423 return date.format(simpleDateFormat); 424 } 425 426 /** 427 * Helper class for working with Files and Paths. Should probably be moved 428 * into its own public class. 429 * 430 * Probably won't be needed now that I discovered the File class and it can 431 * glue together paths. Need to explore it a bit more. 432 * 433 * @author Gregory Madsen Copyright (C) 2012 434 * 435 */ 436 private static class FileHelper { 437 438 /** 439 * Copies an existing file to a new file. Overwriting a file of the same 440 * name is allowed. The destination directory must exist. 441 * @param sourceFileName From directory name 442 * @param destFileName To directory name 443 * @param overwrite When true overwrite any existing files 444 * @throws IOException Thrown when overwrite false and destination directory exists. 445 * 446 */ 447 @SuppressFBWarnings(value = "OBL_UNSATISFIED_OBLIGATION") 448 public static void copy(String sourceFileName, String destFileName, 449 Boolean overwrite) throws IOException { 450 451 // If we can't overwrite the destination, check if the destination 452 // already exists 453 if (!overwrite) { 454 if (new File(destFileName).exists()) { 455 throw new IOException( 456 "Destination file exists and overwrite is false."); // NOI18N 457 } 458 } 459 460 try (InputStream source = new FileInputStream(sourceFileName); 461 OutputStream dest = new FileOutputStream(destFileName)) { 462 463 byte[] buffer = new byte[1024]; 464 465 int len; 466 467 while ((len = source.read(buffer)) > 0) { 468 dest.write(buffer, 0, len); 469 } 470 } catch (IOException ex) { 471 String msg = String.format("Error copying file: %s to: %s", // NOI18N 472 sourceFileName, destFileName); 473 throw new IOException(msg, ex); 474 } 475 476 // Now update the last modified time to equal the source file. 477 File src = new File(sourceFileName); 478 File dst = new File(destFileName); 479 480 Boolean ok = dst.setLastModified(src.lastModified()); 481 if (!ok) { 482 throw new RuntimeException( 483 "Failed to set modified time on file: " // NOI18N 484 + dst.getAbsolutePath()); 485 } 486 } 487 } 488 489}