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}