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