JAL-3141 Added warning/confirmation when suffixReverse is changed without changing...
[jalview.git] / src / jalview / io / BackupFiles.java
1 package jalview.io;
2
3 import jalview.bin.Cache;
4 import jalview.gui.Desktop;
5 import jalview.gui.JvOptionPane;
6 import jalview.util.MessageManager;
7
8 import java.io.File;
9 import java.io.IOException;
10 import java.util.Arrays;
11 import java.util.HashMap;
12 import java.util.Map;
13 import java.util.TreeMap;
14
15 /*
16  * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
17  * User configurable options are:
18  * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
19  * BACKUPFILES_SUFFIX - a template to insert after the file extension.  Use '%n' to be replaced by a 0-led SUFFIX_DIGITS long integer.
20  * BACKUPFILES_NO_MAX - flag to turn off setting a maximum number of backup files to keep.
21  * BACKUPFILES_ROLL_MAX - the maximum number of backupfiles to keep for any one alignment or project file.
22  * BACKUPFILES_SUFFIX_DIGITS - the number of digits to insert replace %n with (e.g. BACKUPFILES_SUFFIX_DIGITS = 3 would make "001", "002", etc)
23  * BACKUPFILES_REVERSE_ORDER - if true then "logfile" style numbering and file rolling will occur. If false then ever-increasing version numbering will occur, but old files will still be deleted if there are more than ROLL_MAX backup files. 
24  * BACKUPFILES_CONFIRM_DELETE_OLD - if true then prompt/confirm with the user when deleting older backup/version files.
25  */
26
27 public class BackupFiles
28 {
29
30   // labels for saved params in Cache and .jalview_properties
31   public static final String NS = "BACKUPFILES";
32
33   public static final String ENABLED = NS + "_ENABLED";
34
35   public static final String SUFFIX = NS + "_SUFFIX";
36
37   public static final String NO_MAX = NS + "_NO_MAX";
38
39   public static final String ROLL_MAX = NS + "_ROLL_MAX";
40
41   public static final String SUFFIX_DIGITS = NS + "_SUFFIX_DIGITS";
42
43   public static final String NUM_PLACEHOLDER = "%n";
44
45   public static final String REVERSE_ORDER = NS + "_REVERSE_ORDER";
46
47   public static final String CONFIRM_DELETE_OLD = NS
48           + "_CONFIRM_DELETE_OLD";
49
50   private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
51
52   private static final String TEMP_FILE_EXT = ".tmp";
53
54   // file - File object to be backed up and then updated (written over)
55   private File file;
56
57   // enabled - default flag as to whether to do the backup file roll (if not
58   // defined in preferences)
59   private static boolean enabled;
60
61   // confirmDelete - default flag as to whether to confirm with the user before
62   // deleting old backup/version files
63   private static boolean confirmDelete;
64
65   // defaultSuffix - default template to use to append to basename of file
66   private String suffix;
67
68   // noMax - flag to turn off a maximum number of files
69   private boolean noMax;
70
71   // defaultMax - default max number of backup files
72   private int max;
73
74   // defaultDigits - number of zero-led digits to use in the filename
75   private int digits;
76
77   // reverseOrder - set to true to make newest (latest) files lowest number
78   // (like rolled log files)
79   private boolean reverseOrder;
80
81   // temp saved file to become new saved file
82   private File tempFile;
83
84   // flag set to see if file save to temp file was successful
85   private boolean tempFileWriteSuccess;
86
87   public BackupFiles(String filename)
88   {
89     this(new File(filename));
90   }
91
92   // first time defaults for SUFFIX, NO_MAX, ROLL_MAX, SUFFIX_DIGITS and
93   // REVERSE_ORDER
94   public BackupFiles(File file)
95   {
96     this(file, ".bak" + NUM_PLACEHOLDER, false, 3, 3, false);
97   }
98
99   public BackupFiles(File file, String defaultSuffix, boolean defaultNoMax,
100           int defaultMax, int defaultDigits, boolean defaultReverseOrder)
101   {
102     classInit();
103     this.file = file;
104     this.suffix = Cache.getDefault(SUFFIX, defaultSuffix);
105     this.noMax = Cache.getDefault(NO_MAX, defaultNoMax);
106     this.max = Cache.getDefault(ROLL_MAX, defaultMax);
107     this.digits = Cache.getDefault(SUFFIX_DIGITS, defaultDigits);
108     this.reverseOrder = Cache.getDefault(REVERSE_ORDER,
109             defaultReverseOrder);
110
111     // create a temp file to save new data in
112     File temp = null;
113     try
114     {
115       if (file != null)
116       {
117         String tempfilename = file.getName();
118         File tempdir = file.getParentFile();
119         temp = File.createTempFile(tempfilename, TEMP_FILE_EXT, tempdir);
120       }
121       else
122       {
123         temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
124       }
125     } catch (IOException e)
126     {
127       System.out.println(
128               "Could not create temp file to save into (IOException)");
129     } catch (Exception e)
130     {
131       System.out.println("Exception ctreating temp file for saving");
132     }
133     this.setTempFile(temp);
134   }
135
136   public static void classInit()
137   {
138     setEnabled(Cache.getDefault(ENABLED, true));
139     setConfirmDelete(Cache.getDefault(CONFIRM_DELETE_OLD, true));
140   }
141
142   public static void setEnabled(boolean flag)
143   {
144     enabled = flag;
145   }
146
147   public static boolean getEnabled()
148   {
149     classInit();
150     return enabled;
151   }
152
153   public static void setConfirmDelete(boolean flag)
154   {
155     confirmDelete = flag;
156   }
157
158   public static boolean getConfirmDelete()
159   {
160     classInit();
161     return confirmDelete;
162   }
163
164   // set, get and rename temp file into place
165   public void setTempFile(File temp)
166   {
167     this.tempFile = temp;
168   }
169
170   public File getTempFile()
171   {
172     return tempFile;
173   }
174
175   public String getTempFilePath()
176   {
177     String path = null;
178     try
179     {
180       path = this.getTempFile().getCanonicalPath();
181     } catch (IOException e)
182     {
183       System.out.println(
184               "IOException when getting Canonical Path of temp file '"
185                       + this.getTempFile().getName() + "'");
186     }
187     return path;
188   }
189
190   public boolean setWriteSuccess(boolean flag)
191   {
192     boolean old = this.tempFileWriteSuccess;
193     this.tempFileWriteSuccess = flag;
194     return old;
195   }
196
197   public boolean getWriteSuccess()
198   {
199     return this.tempFileWriteSuccess;
200   }
201
202   public boolean renameTempFile()
203   {
204     return tempFile.renameTo(file);
205   }
206
207   // roll the backupfiles
208   public boolean rollBackupFiles()
209   {
210
211     // file doesn't yet exist or backups are not enabled or template is null or
212     // empty
213     if ((!file.exists()) || (!enabled) || max < 0 || suffix == null
214             || suffix.length() == 0)
215     {
216       // nothing to do
217       return true;
218     }
219
220     String dir = "";
221     File dirFile;
222     try
223     {
224       dirFile = file.getParentFile();
225       dir = dirFile.getCanonicalPath();
226     } catch (Exception e)
227     {
228       System.out.println(
229               "Could not get canonical path for file '" + file + "'");
230       return false;
231     }
232     String filename = file.getName();
233     String basename = filename;
234
235     boolean ret = true;
236     // Create/move backups up one
237
238     File[] oldFilesToDelete = null;
239
240     // find existing backup files
241     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
242             digits);
243     File[] backupFiles = dirFile.listFiles(bff);
244     int nextIndexNum = 0;
245     String confirmDeleteExtraInfo = null;
246
247     if (backupFiles.length == 0)
248     {
249       // No other backup files. Just need to move existing file to backupfile_1
250       nextIndexNum = 1;
251     }
252     else
253     {
254       TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(
255               backupFiles, basename);
256
257       if (reverseOrder)
258       {
259         // backup style numbering
260
261         File lastfile = null;
262         int tempMax = noMax ? -1 : max;
263         // noMax == true means no limits
264         // look for first "gap" in backupFiles
265         // if tempMax is -1 at this stage just keep going until there's a gap,
266         // then hopefully tempMax gets set to the right index (a positive
267         // integer so the loop breaks)...
268         // why do I feel a little uneasy about this loop?..
269         for (int i = 1; tempMax < 0 || i <= max; i++)
270         {
271           if (!bfTreeMap.containsKey(i)) // first index without existent
272                                          // backupfile
273           {
274             tempMax = i;
275           }
276         }
277
278         // for (int m = 0; m < tempMax; m++)
279         for (int n = tempMax; n > 0; n--)
280         {
281           // int n = tempMax - m;
282           String backupfilename = dir + File.separatorChar
283                   + BackupFilenameParts.getBackupFilename(n, basename,
284                           suffix, digits);
285           File backupfile_n = new File(backupfilename);
286
287           if (!backupfile_n.exists())
288           {
289             lastfile = backupfile_n;
290             continue;
291           }
292
293           // if (m == 0 && backupfile_n.exists())
294           if ((!noMax) && n == tempMax && backupfile_n.exists())
295           {
296             // move the largest (max) rolled file to a temp file and add to the
297             // delete list
298             try
299             {
300               File temp = File.createTempFile(backupfilename, TEMP_FILE_EXT,
301                       dirFile);
302               backupfile_n.renameTo(temp);
303
304               oldFilesToDelete = new File[] { temp };
305               confirmDeleteExtraInfo = "(was " + backupfile_n.getName()
306                       + ")";
307             } catch (IOException e)
308             {
309               System.out.println(
310                       "IOException when creating temporary file for backupfilename");
311             }
312           }
313           else
314           {
315             // Just In Case
316             if (lastfile != null)
317             {
318               ret = ret && backupfile_n.renameTo(lastfile);
319             }
320           }
321
322           lastfile = backupfile_n;
323         }
324
325         // index to use for the latest backup
326         nextIndexNum = 1;
327       }
328       else
329       {
330         // version style numbering (with earliest file deletion if max files
331         // reached)
332
333         bfTreeMap.values().toArray(backupFiles);
334
335         // noMax == true means keep all backup files
336         if ((!noMax) && bfTreeMap.size() >= max)
337         {
338           // need to delete some files to keep number of backups to designated
339           // max
340           int numToDelete = bfTreeMap.size() - max + 1;
341           oldFilesToDelete = Arrays.copyOfRange(backupFiles, 0,
342                   numToDelete);
343
344         }
345
346         nextIndexNum = bfTreeMap.lastKey() + 1;
347
348       }
349     }
350
351     deleteOldFiles(oldFilesToDelete, confirmDeleteExtraInfo);
352
353     // Let's make the new backup file!! yay, got there at last!
354     String latestBackupFilename = dir + File.separatorChar
355             + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
356                     suffix, digits);
357     File latestBackupFile = new File(latestBackupFilename);
358     ret = ret && file.renameTo(latestBackupFile);
359
360     return ret;
361   }
362
363   private void deleteOldFiles(File[] oldFilesToDelete,
364           String confirmDeleteExtraInfo)
365   {
366     if (oldFilesToDelete != null && oldFilesToDelete.length > 0)
367     {
368       // delete old backup/version files
369
370       boolean delete = false;
371       if (confirmDelete)
372       {
373         StringBuilder confirmMessage = new StringBuilder();
374         confirmMessage.append(MessageManager
375                 .getString("label.backupfiles_confirm_delete_old_files"));
376         for (File f : oldFilesToDelete)
377         {
378           confirmMessage.append("\n");
379           confirmMessage.append(f.getName());
380         }
381         if (confirmDeleteExtraInfo != null
382                 && confirmDeleteExtraInfo.length() > 0)
383         {
384           confirmMessage.append("\n");
385           confirmMessage.append(confirmDeleteExtraInfo);
386         }
387         int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
388                 confirmMessage.toString(),
389                 MessageManager
390                         .getString("label.backupfiles_confirm_delete"),
391                 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
392
393         delete = (confirm == JvOptionPane.YES_OPTION);
394       }
395       else
396       {
397         delete = true;
398       }
399
400       if (delete)
401       {
402         for (int i = 0; i < oldFilesToDelete.length; i++)
403         {
404           File fileToDelete = oldFilesToDelete[i];
405           fileToDelete.delete();
406           // System.out.println("DELETING '" + fileToDelete.getName() +
407           // "'");
408         }
409       }
410
411     }
412   }
413
414   private TreeMap<Integer, File> sortBackupFilesAsTreeMap(
415           File[] backupFiles,
416           String basename)
417   {
418     // sort the backup files (based on integer found in the suffix) using a
419     // precomputed Hashmap for speed
420     Map<Integer, File> bfHashMap = new HashMap<>();
421     for (int i = 0; i < backupFiles.length; i++)
422     {
423       File f = backupFiles[i];
424       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
425               digits);
426       bfHashMap.put(bfp.indexNum(), f);
427     }
428     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
429     bfTreeMap.putAll(bfHashMap);
430     return bfTreeMap;
431   }
432
433   public boolean rollBackupsAndRenameTempFile()
434   {
435     boolean write = this.getWriteSuccess();
436
437     boolean roll = false;
438     if (write)
439     {
440       roll = this.rollBackupFiles();
441     }
442     else
443     {
444       return false;
445     }
446
447     /*
448      * Not sure that this confirmation is desirable.  By this stage the new file is
449      * already written successfully, but something (e.g. disk full) has happened while 
450      * trying to roll the backup files, and most likely the filename needed will already
451      * be vacant so renaming the temp file is nearly always correct!
452      */
453     if (!roll)
454     {
455       int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
456               MessageManager.getString(
457                       "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"),
458               MessageManager
459                       .getString("label.backupfiles_confirm_save_file"),
460               JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
461
462       if (confirm == JvOptionPane.YES_OPTION)
463       {
464         roll = true;
465       }
466     }
467
468     boolean rename = false;
469     if (roll)
470     {
471       rename = this.renameTempFile();
472     }
473
474     return rename;
475   }
476
477   public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
478           String fileName, String suffix, int digits)
479   {
480     File[] backupFiles = null;
481
482     File file = new File(fileName);
483
484     File dirFile;
485     try
486     {
487       dirFile = file.getParentFile();
488     } catch (Exception e)
489     {
490       System.out.println(
491               "Could not get canonical path for file '" + file + "'");
492       return new TreeMap<>();
493     }
494
495     String filename = file.getName();
496     String basename = filename;
497
498     // find existing backup files
499     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
500             digits);
501     backupFiles = dirFile.listFiles(bff); // is clone needed?
502
503     // sort the backup files (based on integer found in the suffix) using a
504     // precomputed Hashmap for speed
505     Map<Integer, File> bfHashMap = new HashMap<>();
506     for (int i = 0; i < backupFiles.length; i++)
507     {
508       File f = backupFiles[i];
509       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
510               digits);
511       bfHashMap.put(bfp.indexNum(), f);
512     }
513     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
514     bfTreeMap.putAll(bfHashMap);
515
516     return bfTreeMap;
517   }
518
519 }