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