JAL-3141 Tests now available and working, bringing to light a problem with the classI...
[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   // 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 static String getNumPlaceHolder()
189   {
190     return NUM_PLACEHOLDER;
191   }
192
193   public boolean setWriteSuccess(boolean flag)
194   {
195     boolean old = this.tempFileWriteSuccess;
196     this.tempFileWriteSuccess = flag;
197     return old;
198   }
199
200   public boolean getWriteSuccess()
201   {
202     return this.tempFileWriteSuccess;
203   }
204
205   public boolean renameTempFile()
206   {
207     return tempFile.renameTo(file);
208   }
209
210
211   // roll the backupfiles
212   public boolean rollBackupFiles()
213   {
214
215     // file doesn't yet exist or backups are not enabled
216     if ((!file.exists()) || (!enabled) || (max < 0))
217     {
218       // nothing to do
219       return true;
220     }
221
222     // split filename up to insert suffix template in the right place. template
223     // and backupMax can be set in .jalview_properties
224     String dir = "";
225     File dirFile;
226     try
227     {
228       dirFile = file.getParentFile();
229       dir = dirFile.getCanonicalPath();
230     } catch (Exception e)
231     {
232       System.out.println(
233               "Could not get canonical path for file '" + file + "'");
234       return false;
235     }
236     String filename = file.getName();
237     String basename = filename;
238     String extension = "";
239     int dotcharpos = filename.lastIndexOf('.');
240     // don't split of filenames with the last '.' at the very beginning or
241     // very end of the filename
242     if ((dotcharpos > 0) && (dotcharpos < filename.length() - 1))
243     {
244       basename = filename.substring(0, dotcharpos);
245       extension = filename.substring(dotcharpos); // NOTE this includes the '.'
246     }
247
248     boolean ret = true;
249     // Create/move backups up one
250
251     File[] oldFilesToDelete = null;
252     
253     // find existing backup files
254     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
255             digits,
256             extension);
257     File[] backupFiles = dirFile.listFiles(bff);
258     int nextIndexNum = 0;
259     String confirmDeleteExtraInfo = null;
260     
261     if (backupFiles.length == 0)
262     {
263       // No other backup files. Just need to move existing file to backupfile_1
264       nextIndexNum = 1;
265     }
266     else
267     {
268
269       // sort the backup files (based on integer found in the suffix) using a
270       // precomputed Hashmap for speed
271       HashMap<Integer, File> bfHashMap = new HashMap<>();
272       for (int i = 0; i < backupFiles.length; i++)
273       {
274         File f = backupFiles[i];
275         BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, digits, extension);
276         bfHashMap.put(bfp.indexNum(), f);
277       }
278       TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
279       bfTreeMap.putAll(bfHashMap);
280
281       if (reverseOrder)
282       {
283         // backup style numbering
284
285         File lastfile = null;
286         int tempMax = noMax ? -1 : max;
287         // noMax == true means no limits
288         // look for first "gap" in backupFiles
289         // if tempMax is -1 at this stage just keep going until there's a gap,
290         // then hopefully tempMax gets set to the right index (a positive
291         // integer so the loop breaks)...
292         // why do I feel a little uneasy about this loop?..
293         for (int i = 1; tempMax < 0 || i <= max; i++)
294         {
295           if (!bfTreeMap.containsKey(i)) // first index without existent
296                                          // backupfile
297           {
298             tempMax = i;
299           }
300         }
301
302         // for (int m = 0; m < tempMax; m++)
303         for (int n = tempMax; n > 0; n--)
304         {
305           // int n = tempMax - m;
306           String backupfilename = dir + File.separatorChar
307                   + BackupFilenameFilter.getBackupFilename(n, basename,
308                           suffix, digits, extension);
309           File backupfile_n = new File(backupfilename);
310
311           if (!backupfile_n.exists())
312           {
313             lastfile = backupfile_n;
314             continue;
315           }
316
317           // if (m == 0 && backupfile_n.exists())
318           if ((!noMax) && n == tempMax && backupfile_n.exists())
319           {
320             // move the largest (max) rolled file to a temp file and add to the delete list
321             try
322             {
323               File temp = File.createTempFile(backupfilename, ".tmp",
324                     dirFile);
325               backupfile_n.renameTo(temp);
326
327               oldFilesToDelete = new File[] { temp };
328               confirmDeleteExtraInfo = "(was " + backupfile_n.getName()
329                       + ")";
330             } catch (IOException e)
331             {
332               System.out.println(
333                       "IOException when creating temporary file for backupfilename");
334             }
335           }
336           else
337           {
338             // Just In Case
339             if (lastfile != null)
340             {
341               ret = ret && backupfile_n.renameTo(lastfile);
342             }
343           }
344
345           lastfile = backupfile_n;
346         }
347
348         // index to use for the latest backup
349         nextIndexNum = 1;
350       }
351       else
352       {
353         // version style numbering (with earliest file deletion if max files
354         // reached)
355
356
357         bfTreeMap.values().toArray(backupFiles);
358
359         // noMax == true means keep all backup files
360         if ((!noMax) && bfTreeMap.size() >= max)
361         {
362           // need to delete some files to keep number of backups to designated
363           // max
364           int numToDelete = bfTreeMap.size() - max + 1;
365           oldFilesToDelete = Arrays.copyOfRange(backupFiles, 0,
366                   numToDelete);
367
368         }
369
370         nextIndexNum = bfTreeMap.lastKey() + 1;
371
372       }
373     }
374
375     if (oldFilesToDelete != null && oldFilesToDelete.length > 0)
376     {
377       // delete old backup/version files
378
379       boolean delete = false;
380       if (confirmDelete)
381       {
382         // Object[] confirmMessageArray = {};
383         StringBuilder confirmMessage = new StringBuilder();
384         confirmMessage.append(MessageManager
385                 .getString("label.backupfiles_confirm_delete_old_files"));
386         for (File f : oldFilesToDelete)
387         {
388           confirmMessage.append("\n");
389           confirmMessage.append(f.getName());
390         }
391         if (confirmDeleteExtraInfo != null
392                 && confirmDeleteExtraInfo.length() > 0)
393         {
394           confirmMessage.append("\n");
395           confirmMessage.append(confirmDeleteExtraInfo);
396         }
397         int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
398                 confirmMessage.toString(),
399                 MessageManager
400                         .getString("label.backupfiles_confirm_delete"),
401                 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
402
403         delete = (confirm == JvOptionPane.YES_OPTION);
404       }
405       else
406       {
407         delete = true;
408       }
409
410       if (delete)
411       {
412         for (int i = 0; i < oldFilesToDelete.length; i++)
413         {
414           File fileToDelete = oldFilesToDelete[i];
415           fileToDelete.delete();
416           // System.out.println("DELETING '" + fileToDelete.getName() +
417           // "'");
418         }
419       }
420
421     }
422
423     // Let's make the new backup file!! yay, got there at last!
424     String latestBackupFilename = dir + File.separatorChar
425             + BackupFilenameFilter.getBackupFilename(nextIndexNum, basename,
426                     suffix, digits, extension);
427     File latestBackupFile = new File(latestBackupFilename);
428     ret = ret && file.renameTo(latestBackupFile);
429
430     return ret;
431   }
432
433   public boolean rollBackupsAndRenameTempFile()
434   {
435     boolean write = this.getWriteSuccess();
436     
437     boolean roll = false;
438     if (write) {
439       roll = this.rollBackupFiles();
440     } else {
441       return false;
442     }
443     
444     /*
445      * Not sure that this confirmation is desirable.  By this stage the new file is
446      * already written successfully, but something (e.g. disk full) has happened while 
447      * trying to roll the backup files, and most likely the filename needed will already
448      * be vacant so renaming the temp file is nearly always correct!
449      */
450     if (!roll)
451     {
452       int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
453               MessageManager.getString(
454                       "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"),
455               MessageManager.getString("label.backupfiles_confirm_save_file"),
456               JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
457
458       if (confirm == JvOptionPane.YES_OPTION)
459       {
460         roll = true;
461       }
462     }
463
464     boolean rename = false;
465     if (roll)
466     {
467       rename = this.renameTempFile();
468     }
469
470     return rename;
471   }
472
473   public static TreeMap<Integer, File> lsBackupFilesAsTreeMap(
474           String fileName,
475           String suffix, int digits)
476   {
477     File[] backupFiles = null;
478
479     File file = new File(fileName);
480
481     String dir = "";
482     File dirFile;
483     try
484     {
485       dirFile = file.getParentFile();
486       dir = dirFile.getCanonicalPath();
487     } catch (Exception e)
488     {
489       System.out.println(
490               "Could not get canonical path for file '" + file + "'");
491       return new TreeMap<>();
492     }
493
494     String filename = file.getName();
495     String basename = filename;
496     String extension = "";
497     int dotcharpos = filename.lastIndexOf('.');
498     // don't split of filenames with the last '.' at the very beginning or
499     // very end of the filename
500     if ((dotcharpos > 0) && (dotcharpos < filename.length() - 1))
501     {
502       basename = filename.substring(0, dotcharpos);
503       extension = filename.substring(dotcharpos); // NOTE this includes the '.'
504     }
505     
506     // find existing backup files
507     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix, digits, extension);
508     backupFiles = dirFile.listFiles(bff); // is clone needed?
509     
510     // sort the backup files (based on integer found in the suffix) using a
511     // precomputed Hashmap for speed
512     HashMap<Integer, File> bfHashMap = new HashMap<>();
513     for (int i = 0; i < backupFiles.length; i++)
514     {
515       File f = backupFiles[i];
516       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
517               digits, extension);
518       bfHashMap.put(bfp.indexNum(), f);
519     }
520     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
521     bfTreeMap.putAll(bfHashMap);
522
523     return bfTreeMap;
524   }
525
526   public static File[] lsBackupFiles(String fileName, String suffix,
527           int digits)
528   {
529     TreeMap<Integer, File> bfTreeMap = lsBackupFilesAsTreeMap(fileName,
530             suffix, digits);
531     File[] backupFiles = new File[bfTreeMap.size()];
532     bfTreeMap.values().toArray(backupFiles);
533     return backupFiles;
534   }
535
536 }
537