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