Merge branch 'documentation/JAL-3407_2.11.1_release' into releases/Release_2_11_1_Branch
[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.text.SimpleDateFormat;
11 import java.util.ArrayList;
12 import java.util.HashMap;
13 import java.util.Map;
14 import java.util.TreeMap;
15
16 /*
17  * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
18  * User configurable options are:
19  * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
20  * The rest of the options are now saved as BACKUPFILES_PRESET, BACKUPFILES_SAVED and BACKUPFILES_CUSTOM
21  * (see BackupFilesPresetEntry)
22  */
23
24 public class BackupFiles
25 {
26
27   // labels for saved params in Cache and .jalview_properties
28   public static final String NS = "BACKUPFILES";
29
30   public static final String ENABLED = NS + "_ENABLED";
31
32   public static final String NUM_PLACEHOLDER = "%n";
33
34   private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
35
36   private static final String TEMP_FILE_EXT = ".tmp";
37
38   // file - File object to be backed up and then updated (written over)
39   private File file;
40
41   // enabled - default flag as to whether to do the backup file roll (if not
42   // defined in preferences)
43   private static boolean enabled;
44
45   // confirmDelete - default flag as to whether to confirm with the user before
46   // deleting old backup/version files
47   private static boolean confirmDelete;
48
49   // defaultSuffix - default template to use to append to basename of file
50   private String suffix;
51
52   // noMax - flag to turn off a maximum number of files
53   private boolean noMax;
54
55   // defaultMax - default max number of backup files
56   private int max;
57
58   // defaultDigits - number of zero-led digits to use in the filename
59   private int digits;
60
61   // reverseOrder - set to true to make newest (latest) files lowest number
62   // (like rolled log files)
63   private boolean reverseOrder;
64
65   // temp saved file to become new saved file
66   private File tempFile;
67
68   // flag set to see if file save to temp file was successful
69   private boolean tempFileWriteSuccess;
70
71   // array of files to be deleted, with extra information
72   private ArrayList<File> deleteFiles = new ArrayList<>();
73
74   // date formatting for modification times
75   private static final SimpleDateFormat sdf = new SimpleDateFormat(
76           "yyyy-MM-dd HH:mm:ss");
77
78   public BackupFiles(String filename)
79   {
80     this(new File(filename));
81   }
82
83   // first time defaults for SUFFIX, NO_MAX, ROLL_MAX, SUFFIX_DIGITS and
84   // REVERSE_ORDER
85   public BackupFiles(File file)
86   {
87     classInit();
88     this.file = file;
89     BackupFilesPresetEntry bfpe = BackupFilesPresetEntry.getSavedBackupEntry();
90     this.suffix = bfpe.suffix;
91     this.noMax = bfpe.keepAll;
92     this.max = bfpe.rollMax;
93     this.digits = bfpe.digits;
94     this.reverseOrder = bfpe.reverse;
95
96     // create a temp file to save new data in
97     File temp = null;
98     try
99     {
100       if (file != null)
101       {
102         String tempfilename = file.getName();
103         File tempdir = file.getParentFile();
104         temp = File.createTempFile(tempfilename, TEMP_FILE_EXT + "_newfile",
105                 tempdir);
106       }
107       else
108       {
109         temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
110       }
111     } catch (IOException e)
112     {
113       System.out.println(
114               "Could not create temp file to save into (IOException)");
115     } catch (Exception e)
116     {
117       System.out.println("Exception ctreating temp file for saving");
118     }
119     this.setTempFile(temp);
120   }
121
122   public static void classInit()
123   {
124     setEnabled(Cache.getDefault(ENABLED, true));
125     BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
126             .getSavedBackupEntry();
127     setConfirmDelete(bfpe.confirmDelete);
128   }
129
130   public static void setEnabled(boolean flag)
131   {
132     enabled = flag;
133   }
134
135   public static boolean getEnabled()
136   {
137     classInit();
138     return enabled;
139   }
140
141   public static void setConfirmDelete(boolean flag)
142   {
143     confirmDelete = flag;
144   }
145
146   public static boolean getConfirmDelete()
147   {
148     classInit();
149     return confirmDelete;
150   }
151
152   // set, get and rename temp file into place
153   public void setTempFile(File temp)
154   {
155     this.tempFile = temp;
156   }
157
158   public File getTempFile()
159   {
160     return tempFile;
161   }
162
163   public String getTempFilePath()
164   {
165     String path = null;
166     try
167     {
168       path = this.getTempFile().getCanonicalPath();
169     } catch (IOException e)
170     {
171       System.out.println(
172               "IOException when getting Canonical Path of temp file '"
173                       + this.getTempFile().getName() + "'");
174     }
175     return path;
176   }
177
178   public boolean setWriteSuccess(boolean flag)
179   {
180     boolean old = this.tempFileWriteSuccess;
181     this.tempFileWriteSuccess = flag;
182     return old;
183   }
184
185   public boolean getWriteSuccess()
186   {
187     return this.tempFileWriteSuccess;
188   }
189
190   public boolean renameTempFile()
191   {
192     return tempFile.renameTo(file);
193   }
194
195   // roll the backupfiles
196   public boolean rollBackupFiles()
197   {
198     return this.rollBackupFiles(true);
199   }
200
201   public boolean rollBackupFiles(boolean tidyUp)
202   {
203     // file doesn't yet exist or backups are not enabled or template is null or
204     // empty
205     if ((!file.exists()) || (!enabled) || max < 0 || suffix == null
206             || suffix.length() == 0)
207     {
208       // nothing to do
209       return true;
210     }
211
212     String dir = "";
213     File dirFile;
214     try
215     {
216       dirFile = file.getParentFile();
217       dir = dirFile.getCanonicalPath();
218     } catch (Exception e)
219     {
220       System.out.println(
221               "Could not get canonical path for file '" + file + "'");
222       return false;
223     }
224     String filename = file.getName();
225     String basename = filename;
226
227     boolean ret = true;
228     // Create/move backups up one
229
230     deleteFiles.clear();
231
232     // find existing backup files
233     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
234             digits);
235     File[] backupFiles = dirFile.listFiles(bff);
236     int nextIndexNum = 0;
237
238     if (backupFiles.length == 0)
239     {
240       // No other backup files. Just need to move existing file to backupfile_1
241       nextIndexNum = 1;
242     }
243     else
244     {
245       TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(
246               backupFiles, basename);
247       // bfTreeMap now a sorted list of <Integer index>,<File backupfile>
248       // mappings
249
250       if (reverseOrder)
251       {
252         // backup style numbering
253
254
255         int tempMax = noMax ? -1 : max;
256         // noMax == true means no limits
257         // look for first "gap" in backupFiles
258         // if tempMax is -1 at this stage just keep going until there's a gap,
259         // then hopefully tempMax gets set to the right index (a positive
260         // integer so the loop breaks)...
261         // why do I feel a little uneasy about this loop?..
262         for (int i = 1; tempMax < 0 || i <= max; i++)
263         {
264           if (!bfTreeMap.containsKey(i)) // first index without existent
265                                          // backupfile
266           {
267             tempMax = i;
268           }
269         }
270         
271         File previousFile = null;
272         File fileToBeDeleted = null;
273         for (int n = tempMax; n > 0; n--)
274         {
275           String backupfilename = dir + File.separatorChar
276                   + BackupFilenameParts.getBackupFilename(n, basename,
277                           suffix, digits);
278           File backupfile_n = new File(backupfilename);
279
280           if (!backupfile_n.exists())
281           {
282             // no "oldest" file to delete
283             previousFile = backupfile_n;
284             fileToBeDeleted = null;
285             continue;
286           }
287
288           // check the modification time of this (backupfile_n) and the previous
289           // file (fileToBeDeleted) if the previous file is going to be deleted
290           if (fileToBeDeleted != null)
291           {
292             File replacementFile = backupfile_n;
293             long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
294             long replacementFileLMT = replacementFile.lastModified();
295
296             try
297             {
298               File oldestTempFile = nextTempFile(fileToBeDeleted.getName(),
299                       dirFile);
300               
301               if (fileToBeDeletedLMT > replacementFileLMT)
302               {
303                 String fileToBeDeletedLMTString = sdf
304                         .format(fileToBeDeletedLMT);
305                 String replacementFileLMTString = sdf
306                         .format(replacementFileLMT);
307                 System.out.println("WARNING! I am set to delete backupfile "
308                         + fileToBeDeleted.getName()
309                         + " has modification time "
310                         + fileToBeDeletedLMTString
311                         + " which is newer than its replacement "
312                         + replacementFile.getName()
313                         + " with modification time "
314                         + replacementFileLMTString);
315
316                 boolean delete = confirmNewerDeleteFile(fileToBeDeleted,
317                         replacementFile, true);
318
319                 if (delete)
320                 {
321                   // User has confirmed delete -- no need to add it to the list
322                   fileToBeDeleted.delete();
323                 }
324                 else
325                 {
326                   fileToBeDeleted.renameTo(oldestTempFile);
327                 }
328               }
329               else
330               {
331                 fileToBeDeleted.renameTo(oldestTempFile);
332                 addDeleteFile(oldestTempFile);
333               }
334
335             } catch (Exception e)
336             {
337               System.out.println(
338                       "Error occurred, probably making new temp file for '"
339                               + fileToBeDeleted.getName() + "'");
340               e.printStackTrace();
341             }
342
343             // reset
344             fileToBeDeleted = null;
345           }
346
347           if (!noMax && n == tempMax && backupfile_n.exists())
348           {
349             fileToBeDeleted = backupfile_n;
350           }
351           else
352           {
353             if (previousFile != null)
354             {
355               ret = ret && backupfile_n.renameTo(previousFile);
356             }
357           }
358
359           previousFile = backupfile_n;
360         }
361
362         // index to use for the latest backup
363         nextIndexNum = 1;
364       }
365       else
366       {
367         // version style numbering (with earliest file deletion if max files
368         // reached)
369
370         bfTreeMap.values().toArray(backupFiles);
371
372         // noMax == true means keep all backup files
373         if ((!noMax) && bfTreeMap.size() >= max)
374         {
375           // need to delete some files to keep number of backups to designated
376           // max
377           int numToDelete = bfTreeMap.size() - max + 1;
378           // the "replacement" file is the latest backup file being kept (it's
379           // not replacing though)
380           File replacementFile = numToDelete < backupFiles.length
381                   ? backupFiles[numToDelete]
382                   : null;
383           for (int i = 0; i < numToDelete; i++)
384           {
385             // check the deletion files for modification time of the last
386             // backupfile being saved
387             File fileToBeDeleted = backupFiles[i];
388             boolean delete = true;
389
390             boolean newer = false;
391             if (replacementFile != null)
392             {
393               long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
394               long replacementFileLMT = replacementFile != null
395                       ? replacementFile.lastModified()
396                       : Long.MAX_VALUE;
397               if (fileToBeDeletedLMT > replacementFileLMT)
398               {
399                 String fileToBeDeletedLMTString = sdf
400                         .format(fileToBeDeletedLMT);
401                 String replacementFileLMTString = sdf
402                         .format(replacementFileLMT);
403
404                 System.out
405                         .println("WARNING! I am set to delete backupfile '"
406                                 + fileToBeDeleted.getName()
407                                 + "' has modification time "
408                         + fileToBeDeletedLMTString
409                                 + " which is newer than the oldest backupfile being kept '"
410                         + replacementFile.getName()
411                                 + "' with modification time "
412                         + replacementFileLMTString);
413
414                 delete = confirmNewerDeleteFile(fileToBeDeleted,
415                         replacementFile, false);
416                 if (delete)
417                 {
418                   // User has confirmed delete -- no need to add it to the list
419                   fileToBeDeleted.delete();
420                   delete = false;
421                 }
422                 else
423                 {
424                   // keeping file, nothing to do!
425                 }
426               }
427             }
428             if (delete)
429             {
430               addDeleteFile(fileToBeDeleted);
431             }
432
433           }
434
435         }
436
437         nextIndexNum = bfTreeMap.lastKey() + 1;
438       }
439     }
440
441     // Let's make the new backup file!! yay, got there at last!
442     String latestBackupFilename = dir + File.separatorChar
443             + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
444                     suffix, digits);
445     ret |= file.renameTo(new File(latestBackupFilename));
446
447     if (tidyUp)
448     {
449       tidyUpFiles();
450     }
451
452     return ret;
453   }
454
455   private static File nextTempFile(String filename, File dirFile)
456           throws IOException
457   {
458     File temp = null;
459     COUNT: for (int i = 1; i < 1000; i++)
460     {
461       File trythis = new File(dirFile,
462               filename + '~' + Integer.toString(i));
463       if (!trythis.exists())
464       {
465         temp = trythis;
466         break COUNT;
467       }
468
469     }
470     if (temp == null)
471     {
472       temp = File.createTempFile(filename, TEMP_FILE_EXT, dirFile);
473     }
474     return temp;
475   }
476
477   private void tidyUpFiles()
478   {
479     deleteOldFiles();
480   }
481
482   private static boolean confirmNewerDeleteFile(File fileToBeDeleted,
483           File replacementFile, boolean replace)
484   {
485     StringBuilder messageSB = new StringBuilder();
486
487     File ftbd = fileToBeDeleted;
488     String ftbdLMT = sdf.format(ftbd.lastModified());
489     String ftbdSize = Long.toString(ftbd.length());
490
491     File rf = replacementFile;
492     String rfLMT = sdf.format(rf.lastModified());
493     String rfSize = Long.toString(rf.length());
494
495     int confirmButton = JvOptionPane.NO_OPTION;
496     if (replace)
497     {
498       File saveFile = null;
499       try
500       {
501         saveFile = nextTempFile(ftbd.getName(), ftbd.getParentFile());
502       } catch (Exception e)
503       {
504         System.out.println(
505                 "Error when confirming to keep backup file newer than other backup files.");
506         e.printStackTrace();
507       }
508       messageSB.append(MessageManager.formatMessage(
509               "label.newerdelete_replacement_line", new String[]
510               { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
511                   rfSize }));
512       messageSB.append("\n\n");
513       messageSB.append(MessageManager.formatMessage(
514               "label.confirm_deletion_or_rename", new String[]
515               { ftbd.getName(), saveFile.getName() }));
516       String[] options = new String[] {
517           MessageManager.getString("label.delete"),
518           MessageManager.getString("label.rename") };
519
520       confirmButton = JvOptionPane.showOptionDialog(Desktop.desktop,
521               messageSB.toString(),
522               MessageManager.getString("label.backupfiles_confirm_delete"),
523               JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
524               null, options, options[0]);
525     }
526     else
527     {
528       messageSB.append(MessageManager
529               .formatMessage("label.newerdelete_line", new String[]
530               { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
531                   rfSize }));
532       messageSB.append("\n\n");
533       messageSB.append(MessageManager
534               .formatMessage("label.confirm_deletion", new String[]
535               { ftbd.getName() }));
536       String[] options = new String[] {
537           MessageManager.getString("label.delete"),
538           MessageManager.getString("label.keep") };
539
540       confirmButton = JvOptionPane.showOptionDialog(Desktop.desktop,
541               messageSB.toString(),
542               MessageManager.getString("label.backupfiles_confirm_delete"),
543               JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
544               null, options, options[0]);
545     }
546
547
548     // return should be TRUE if file is to be deleted
549     return (confirmButton == JvOptionPane.YES_OPTION);
550   }
551
552   private void deleteOldFiles()
553   {
554     if (deleteFiles != null && !deleteFiles.isEmpty())
555     {
556       boolean doDelete = false;
557       StringBuilder messageSB = null;
558       if (confirmDelete && deleteFiles.size() > 0)
559       {
560         messageSB = new StringBuilder();
561         messageSB.append(MessageManager
562                 .getString("label.backupfiles_confirm_delete_old_files"));
563         for (int i = 0; i < deleteFiles.size(); i++)
564         {
565           File df = deleteFiles.get(i);
566           messageSB.append("\n");
567           messageSB.append(df.getName());
568           messageSB.append(" ");
569           messageSB.append(MessageManager.formatMessage("label.file_info",
570                   new String[]
571                   { sdf.format(df.lastModified()),
572                       Long.toString(df.length()) }));
573         }
574
575         int confirmButton = JvOptionPane.showConfirmDialog(Desktop.desktop,
576                 messageSB.toString(),
577                 MessageManager
578                         .getString("label.backupfiles_confirm_delete"),
579                 JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
580
581         doDelete = (confirmButton == JvOptionPane.YES_OPTION);
582       }
583       else
584       {
585         doDelete = true;
586       }
587
588       if (doDelete)
589       {
590         for (int i = 0; i < deleteFiles.size(); i++)
591         {
592           File fileToDelete = deleteFiles.get(i);
593           fileToDelete.delete();
594           System.out.println("DELETING '" + fileToDelete.getName() + "'");
595         }
596       }
597
598     }
599
600     deleteFiles.clear();
601   }
602
603   private TreeMap<Integer, File> sortBackupFilesAsTreeMap(
604           File[] backupFiles,
605           String basename)
606   {
607     // sort the backup files (based on integer found in the suffix) using a
608     // precomputed Hashmap for speed
609     Map<Integer, File> bfHashMap = new HashMap<>();
610     for (int i = 0; i < backupFiles.length; i++)
611     {
612       File f = backupFiles[i];
613       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
614               digits);
615       bfHashMap.put(bfp.indexNum(), f);
616     }
617     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
618     bfTreeMap.putAll(bfHashMap);
619     return bfTreeMap;
620   }
621
622   public boolean rollBackupsAndRenameTempFile()
623   {
624     boolean write = this.getWriteSuccess();
625
626     boolean roll = false;
627     boolean rename = false;
628     if (write)
629     {
630       roll = this.rollBackupFiles(false);
631       rename = this.renameTempFile();
632     }
633
634     /*
635      * Not sure that this confirmation is desirable.  By this stage the new file is
636      * already written successfully, but something (e.g. disk full) has happened while 
637      * trying to roll the backup files, and most likely the filename needed will already
638      * be vacant so renaming the temp file is nearly always correct!
639      */
640     boolean okay = roll && rename;
641     if (!okay)
642     {
643       StringBuilder messageSB = new StringBuilder();
644       messageSB.append(MessageManager.getString( "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"));
645       if (rename)
646       {
647         if (messageSB.length() > 0)
648         {
649           messageSB.append("\n");
650         }
651         messageSB.append(MessageManager.getString(
652                 "label.backupfiles_confirm_save_new_saved_file_ok"));
653       }
654       else
655       {
656         if (messageSB.length() > 0)
657         {
658           messageSB.append("\n");
659         }
660         messageSB.append(MessageManager.getString(
661                 "label.backupfiles_confirm_save_new_saved_file_not_ok"));
662       }
663
664       int confirmButton = JvOptionPane.showConfirmDialog(Desktop.desktop,
665               messageSB.toString(),
666               MessageManager
667                       .getString("label.backupfiles_confirm_save_file"),
668               JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
669       okay = confirmButton == JvOptionPane.OK_OPTION;
670     }
671     if (okay)
672     {
673       tidyUpFiles();
674     }
675
676     return rename;
677   }
678
679   public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
680           String fileName, String suffix, int digits)
681   {
682     File[] backupFiles = null;
683
684     File file = new File(fileName);
685
686     File dirFile;
687     try
688     {
689       dirFile = file.getParentFile();
690     } catch (Exception e)
691     {
692       System.out.println(
693               "Could not get canonical path for file '" + file + "'");
694       return new TreeMap<>();
695     }
696
697     String filename = file.getName();
698     String basename = filename;
699
700     // find existing backup files
701     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
702             digits);
703     backupFiles = dirFile.listFiles(bff); // is clone needed?
704
705     // sort the backup files (based on integer found in the suffix) using a
706     // precomputed Hashmap for speed
707     Map<Integer, File> bfHashMap = new HashMap<>();
708     for (int i = 0; i < backupFiles.length; i++)
709     {
710       File f = backupFiles[i];
711       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
712               digits);
713       bfHashMap.put(bfp.indexNum(), f);
714     }
715     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
716     bfTreeMap.putAll(bfHashMap);
717
718     return bfTreeMap;
719   }
720
721   /*
722   private boolean addDeleteFile(File fileToBeDeleted, File originalFile,
723           boolean delete, boolean newer)
724   {
725     return addDeleteFile(fileToBeDeleted, originalFile, null, delete, newer);
726   }
727   */
728   private boolean addDeleteFile(File fileToBeDeleted)
729   {
730     boolean ret = false;
731     int pos = deleteFiles.indexOf(fileToBeDeleted);
732     if (pos > -1)
733     {
734       return true;
735     }
736     else
737     {
738       deleteFiles.add(fileToBeDeleted);
739     }
740     return ret;
741   }
742
743 }