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