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 public void restoreFilesFromDirectory(File directory) throws IOException { 200 log.debug("restoring files from directory {}", directory.getAbsolutePath()); 201 202 copyBackupSet(directory, getOperationsRoot()); 203 } 204 205 /** 206 * Copies a complete set of Operations files from one directory to another 207 * directory. Usually used to copy to or from a backup location. Creates the 208 * destination directory if it does not exist. 209 * 210 * Only copies files that are included in the list of Operations files. 211 * @param sourceDir From Directory 212 * @param destDir To Directory 213 * 214 * @throws java.io.IOException Due to trouble reading or writing 215 */ 216 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 217 justification="I18N of Info Message") 218 public void copyBackupSet(File sourceDir, File destDir) throws IOException { 219 log.debug("copying backup set from: {} to: {}", sourceDir, destDir); 220 log.info(Bundle.getMessage("InfoSavingCopy", destDir)); 221 222 if (!sourceDir.exists()) // This throws an exception, as the dir should 223 // exist. 224 { 225 throw new IOException("Backup Set source directory: " // NOI18N 226 + sourceDir.getAbsolutePath() + " does not exist"); // NOI18N 227 } 228 // See how many Operations files we have. If they are all there, carry 229 // on, if there are none, just return, any other number MAY be an error, 230 // so just log it. 231 // We can't throw an exception, as this CAN be a valid state. 232 // There is no way to tell if a missing file is an error or not the way 233 // the files are created. 234 235 int sourceCount = getSourceFileCount(sourceDir); 236 237 if (sourceCount == 0) { 238 log.debug("No source files found in {} so skipping copy.", sourceDir.getAbsolutePath()); // NOI18N 239 return; 240 } 241 242 if (sourceCount != _backupSetFileNames.length) { 243 log.warn("Only {} file(s) found in directory {}", sourceCount, sourceDir.getAbsolutePath()); 244 // throw new IOException("Only " + sourceCount 245 // + " file(s) found in directory " 246 // + sourceDir.getAbsolutePath()); 247 } 248 249 // Ensure destination directory exists 250 if (!destDir.exists()) { 251 // Note that mkdirs does NOT throw an exception on error. 252 // It will return false if the directory already exists. 253 boolean result = destDir.mkdirs(); 254 255 if (!result) { 256 // This needs to use a better Exception class..... 257 throw new IOException( 258 destDir.getAbsolutePath() + " (Could not create all or part of the Backup Set path)"); // NOI18N 259 } 260 } 261 262 // Just copy the specific Operations files, now that we know they are 263 // all there. 264 for (String name : _backupSetFileNames) { 265 log.debug("copying file: {}", name); 266 267 File src = new File(sourceDir, name); 268 269 if (src.exists()) { 270 File dst = new File(destDir, name); 271 272 FileHelper.copy(src.getAbsolutePath(), dst.getAbsolutePath(), true); 273 } else { 274 log.debug("Source file: {} does not exist, and is not copied.", src.getAbsolutePath()); 275 } 276 277 } 278 279 // Throw a test exception, if we have one. 280 if (testException != null) { 281 testException.fillInStackTrace(); 282 throw testException; 283 } 284 } 285 286 /** 287 * Checks to see how many of the Operations files are present in the source 288 * directory. 289 * @param sourceDir The Directory to check. 290 * 291 * @return number of files 292 */ 293 public int getSourceFileCount(File sourceDir) { 294 int count = 0; 295 Boolean exists; 296 297 for (String name : _backupSetFileNames) { 298 exists = new File(sourceDir, name).exists(); 299 if (exists) { 300 count++; 301 } 302 } 303 304 return count; 305 } 306 307 /** 308 * Reloads the demo Operations files that are distributed with JMRI. 309 * 310 * @throws java.io.IOException Due to trouble loading files 311 */ 312 public void loadDemoFiles() throws IOException { 313 File fromDir = new File(XmlFile.xmlDir(), "demoOperations"); // NOI18N 314 copyBackupSet(fromDir, getOperationsRoot()); 315 316 // and the demo panel file 317 log.debug("copying file: {}", _demoPanelFileName); 318 319 File src = new File(fromDir, _demoPanelFileName); 320 File dst = new File(getOperationsRoot(), _demoPanelFileName); 321 322 FileHelper.copy(src.getAbsolutePath(), dst.getAbsolutePath(), true); 323 324 } 325 326 /** 327 * Searches for an unused directory name, based on the default base name, 328 * under the given directory. A name suffix as appended to the base name and 329 * can range from 00 to 99. 330 * 331 * @return A backup set name that is not already in use. 332 */ 333 public String suggestBackupSetName() { 334 // Start with a base name that is derived from today's date 335 // This checks to see if the default name already exists under the given 336 // backup root directory. 337 // If it exists, the name is incremented by 1 up to 99 and checked 338 // again. 339 String baseName = getDate(); 340 String fullName = null; 341 String[] dirNames = getBackupRoot().list(); 342 343 // Check for up to 100 backup file names to see if they already exist 344 for (int i = 0; i < 99; i++) { 345 // Create the trial name, then see if it already exists. 346 fullName = String.format("%s_%02d", baseName, i); // NOI18N 347 348 boolean foundFileNameMatch = false; 349 for (String name : dirNames) { 350 if (name.equals(fullName)) { 351 foundFileNameMatch = true; 352 break; 353 } 354 } 355 if (!foundFileNameMatch) { 356 return fullName; 357 } 358 359 // This should also work, commented out by D. Boudreau 360 // The Linux problem turned out to be related to the order 361 // files names are returned by list(). 362 // File testPath = new File(_backupRoot, fullName); 363 // 364 // if (!testPath.exists()) { 365 // return fullName; // Found an unused name 366 // Otherwise complain and keep trying... 367 log.debug("Operations backup directory: {} already exists", fullName); // NOI18N 368 } 369 370 // If we get here, we have tried all 100 variants without success. This 371 // should probably throw an exception, but for now it just returns the 372 // last file name tried. 373 return fullName; 374 } 375 376 /** 377 * Reset Operations by deleting XML files, leaves directories and backup 378 * files in place. 379 */ 380 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", 381 justification = "not possible") // NOI18N 382 public void deleteOperationsFiles() { 383 // TODO Maybe this should also only delete specific files used by Operations, 384 // and not just all XML files. 385 File files = getOperationsRoot(); 386 387 if (!files.exists()) { 388 return; 389 } 390 391 String[] operationFileNames = files.list(); 392 for (String fileName : operationFileNames) { 393 // skip non-xml files 394 if (!fileName.toUpperCase().endsWith(".XML")) // NOI18N 395 { 396 continue; 397 } 398 // 399 log.debug("deleting file: {}", fileName); 400 File file = new File(getOperationsRoot() + File.separator + fileName); 401 if (!file.delete()) { 402 log.debug("file not deleted"); 403 } 404 // TODO This should probably throw an exception if a delete fails. 405 } 406 } 407 408 /** 409 * Returns the current date formatted for use as part of a Backup Set name. 410 */ 411 private String getDate() { 412 Date date = Calendar.getInstance().getTime(); 413 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd"); // NOI18N 414 return simpleDateFormat.format(date); 415 } 416 417 /** 418 * Helper class for working with Files and Paths. Should probably be moved 419 * into its own public class. 420 * 421 * Probably won't be needed now that I discovered the File class and it can 422 * glue together paths. Need to explore it a bit more. 423 * 424 * @author Gregory Madsen Copyright (C) 2012 425 * 426 */ 427 private static class FileHelper { 428 429 /** 430 * Copies an existing file to a new file. Overwriting a file of the same 431 * name is allowed. The destination directory must exist. 432 * @param sourceFileName From directory name 433 * @param destFileName To directory name 434 * @param overwrite When true overwrite any existing files 435 * @throws IOException Thrown when overwrite false and destination directory exists. 436 * 437 */ 438 @SuppressFBWarnings(value = "OBL_UNSATISFIED_OBLIGATION") 439 public static void copy(String sourceFileName, String destFileName, 440 Boolean overwrite) throws IOException { 441 442 // If we can't overwrite the destination, check if the destination 443 // already exists 444 if (!overwrite) { 445 if (new File(destFileName).exists()) { 446 throw new IOException( 447 "Destination file exists and overwrite is false."); // NOI18N 448 } 449 } 450 451 try (InputStream source = new FileInputStream(sourceFileName); 452 OutputStream dest = new FileOutputStream(destFileName)) { 453 454 byte[] buffer = new byte[1024]; 455 456 int len; 457 458 while ((len = source.read(buffer)) > 0) { 459 dest.write(buffer, 0, len); 460 } 461 } catch (IOException ex) { 462 String msg = String.format("Error copying file: %s to: %s", // NOI18N 463 sourceFileName, destFileName); 464 throw new IOException(msg, ex); 465 } 466 467 // Now update the last modified time to equal the source file. 468 File src = new File(sourceFileName); 469 File dst = new File(destFileName); 470 471 Boolean ok = dst.setLastModified(src.lastModified()); 472 if (!ok) { 473 throw new RuntimeException( 474 "Failed to set modified time on file: " // NOI18N 475 + dst.getAbsolutePath()); 476 } 477 } 478 } 479 480}