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