JAL-1988 Added a saves in progress check for quit
[jalview.git] / src / jalview / io / BackupFiles.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.nio.file.Paths;
28 import java.nio.file.StandardCopyOption;
29 import java.text.SimpleDateFormat;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.TreeMap;
35
36 import jalview.bin.Cache;
37 import jalview.bin.Console;
38 import jalview.gui.Desktop;
39 import jalview.gui.JvOptionPane;
40 import jalview.util.MessageManager;
41 import jalview.util.Platform;
42
43 /*
44  * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
45  * User configurable options are:
46  * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
47  * The rest of the options are now saved as BACKUPFILES_PRESET, BACKUPFILES_SAVED and BACKUPFILES_CUSTOM
48  * (see BackupFilesPresetEntry)
49  */
50
51 public class BackupFiles
52 {
53
54   // labels for saved params in Cache and .jalview_properties
55   public static final String NS = "BACKUPFILES";
56
57   public static final String ENABLED = NS + "_ENABLED";
58
59   public static final String NUM_PLACEHOLDER = "%n";
60
61   private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
62
63   private static final String TEMP_FILE_EXT = ".tmp";
64
65   // file - File object to be backed up and then updated (written over)
66   private File file;
67
68   // enabled - default flag as to whether to do the backup file roll (if not
69   // defined in preferences)
70   private static boolean enabled;
71
72   // confirmDelete - default flag as to whether to confirm with the user before
73   // deleting old backup/version files
74   private static boolean confirmDelete;
75
76   // defaultSuffix - default template to use to append to basename of file
77   private String suffix;
78
79   // noMax - flag to turn off a maximum number of files
80   private boolean noMax;
81
82   // defaultMax - default max number of backup files
83   private int max;
84
85   // defaultDigits - number of zero-led digits to use in the filename
86   private int digits;
87
88   // reverseOrder - set to true to make newest (latest) files lowest number
89   // (like rolled log files)
90   private boolean reverseOrder;
91
92   // temp saved file to become new saved file
93   private File tempFile;
94
95   // flag set to see if file save to temp file was successful
96   private boolean tempFileWriteSuccess;
97
98   // array of files to be deleted, with extra information
99   private ArrayList<File> deleteFiles = new ArrayList<>();
100
101   // date formatting for modification times
102   private static final SimpleDateFormat sdf = new SimpleDateFormat(
103           "yyyy-MM-dd HH:mm:ss");
104
105   private static final String newTempFileSuffix = "_newfile";
106
107   private static final String oldTempFileSuffix = "_oldfile_tobedeleted";
108
109   private static List<BackupFiles> savesInProgress = new ArrayList<>();
110
111   private boolean addSaveInProgress()
112   {
113     if (savesInProgress.contains(this))
114     {
115       return false;
116     }
117     else
118     {
119       savesInProgress.add(this);
120       return true;
121     }
122   }
123
124   private boolean removeSaveInProgress()
125   {
126     if (savesInProgress.contains(this))
127     {
128       // remove all occurrences
129       while (savesInProgress.remove(this))
130       {
131       }
132       return true;
133     }
134     else
135     {
136       return false;
137     }
138   }
139
140   public static boolean hasSavesInProgress()
141   {
142     return savesInProgress.size() > 0;
143   }
144
145   public static List<File> savesInProgressFiles()
146   {
147     List<File> files = new ArrayList<>();
148     for (BackupFiles bfile : savesInProgress)
149     {
150       files.add(bfile.getFile());
151     }
152     return files;
153   }
154
155   public BackupFiles(String filename)
156   {
157     this(new File(filename));
158   }
159
160   // first time defaults for SUFFIX, NO_MAX, ROLL_MAX, SUFFIX_DIGITS and
161   // REVERSE_ORDER
162   public BackupFiles(File file)
163   {
164     classInit();
165     this.file = file;
166
167     // add this file from the save in progress stack
168     addSaveInProgress();
169
170     BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
171             .getSavedBackupEntry();
172     this.suffix = bfpe.suffix;
173     this.noMax = bfpe.keepAll;
174     this.max = bfpe.rollMax;
175     this.digits = bfpe.digits;
176     this.reverseOrder = bfpe.reverse;
177
178     // create a temp file to save new data in
179     File temp = null;
180     try
181     {
182       if (file != null)
183       {
184         String tempfilename = file.getName();
185         File tempdir = file.getParentFile();
186         Console.trace(
187                 "BACKUPFILES [file!=null] attempting to create temp file for "
188                         + tempfilename + " in dir " + tempdir);
189         temp = File.createTempFile(tempfilename,
190                 TEMP_FILE_EXT + newTempFileSuffix, tempdir);
191         Console.debug(
192                 "BACKUPFILES using temp file " + temp.getAbsolutePath());
193       }
194       else
195       {
196         Console.trace(
197                 "BACKUPFILES [file==null] attempting to create default temp file "
198                         + DEFAULT_TEMP_FILE + " with extension "
199                         + TEMP_FILE_EXT);
200         temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
201       }
202     } catch (IOException e)
203     {
204       Console.error("Could not create temp file to save to (IOException)");
205       Console.error(e.getMessage());
206       Console.debug(Cache.getStackTraceString(e));
207     } catch (Exception e)
208     {
209       Console.error("Exception creating temp file for saving");
210       Console.debug(Cache.getStackTraceString(e));
211     }
212     this.setTempFile(temp);
213   }
214
215   public static void classInit()
216   {
217     Console.initLogger();
218     Console.trace("BACKUPFILES classInit");
219     boolean e = Cache.getDefault(ENABLED, !Platform.isJS());
220     setEnabled(e);
221     Console.trace("BACKUPFILES " + (e ? "enabled" : "disabled"));
222     BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
223             .getSavedBackupEntry();
224     Console.trace("BACKUPFILES preset scheme " + bfpe.toString());
225     setConfirmDelete(bfpe.confirmDelete);
226     Console.trace("BACKUPFILES confirm delete " + bfpe.confirmDelete);
227   }
228
229   public static void setEnabled(boolean flag)
230   {
231     enabled = flag;
232   }
233
234   public static boolean getEnabled()
235   {
236     classInit();
237     return enabled;
238   }
239
240   public static void setConfirmDelete(boolean flag)
241   {
242     confirmDelete = flag;
243   }
244
245   public static boolean getConfirmDelete()
246   {
247     classInit();
248     return confirmDelete;
249   }
250
251   // set, get and rename temp file into place
252   public void setTempFile(File temp)
253   {
254     this.tempFile = temp;
255   }
256
257   public File getTempFile()
258   {
259     return tempFile;
260   }
261
262   public String getTempFilePath()
263   {
264     String path = null;
265     try
266     {
267       path = this.getTempFile().getCanonicalPath();
268     } catch (IOException e)
269     {
270       Console.error("IOException when getting Canonical Path of temp file '"
271               + this.getTempFile().getName() + "'");
272       Console.debug(Cache.getStackTraceString(e));
273     }
274     return path;
275   }
276
277   public boolean setWriteSuccess(boolean flag)
278   {
279     boolean old = this.tempFileWriteSuccess;
280     this.tempFileWriteSuccess = flag;
281     return old;
282   }
283
284   public boolean getWriteSuccess()
285   {
286     return this.tempFileWriteSuccess;
287   }
288
289   public boolean renameTempFile()
290   {
291     return moveFileToFile(tempFile, file);
292   }
293
294   // roll the backupfiles
295   public boolean rollBackupFiles()
296   {
297     return this.rollBackupFiles(true);
298   }
299
300   public boolean rollBackupFiles(boolean tidyUp)
301   {
302     // file doesn't yet exist or backups are not enabled or template is null or
303     // empty
304     if ((!file.exists()) || (!enabled) || max < 0 || suffix == null
305             || suffix.length() == 0)
306     {
307       // nothing to do
308       Console.debug("BACKUPFILES rollBackupFiles nothing to do." + ", "
309               + "filename: " + (file != null ? file.getName() : "null")
310               + ", " + "file exists: " + file.exists() + ", " + "enabled: "
311               + enabled + ", " + "max: " + max + ", " + "suffix: '" + suffix
312               + "'");
313       return true;
314     }
315
316     Console.trace("BACKUPFILES rollBackupFiles starting");
317
318     String dir = "";
319     File dirFile;
320     try
321     {
322       dirFile = file.getParentFile();
323       dir = dirFile.getCanonicalPath();
324       Console.trace("BACKUPFILES dir: " + dir);
325     } catch (Exception e)
326     {
327       Console.error("Could not get canonical path for file '" + file + "'");
328       Console.error(e.getMessage());
329       Console.debug(Cache.getStackTraceString(e));
330       return false;
331     }
332     String filename = file.getName();
333     String basename = filename;
334
335     Console.trace("BACKUPFILES filename is " + filename);
336     boolean ret = true;
337     // Create/move backups up one
338
339     deleteFiles.clear();
340
341     // find existing backup files
342     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
343             digits);
344     File[] backupFiles = dirFile.listFiles(bff);
345     int nextIndexNum = 0;
346
347     Console.trace("BACKUPFILES backupFiles.length: " + backupFiles.length);
348     if (backupFiles.length == 0)
349     {
350       // No other backup files. Just need to move existing file to backupfile_1
351       Console.trace(
352               "BACKUPFILES no existing backup files, setting index to 1");
353       nextIndexNum = 1;
354     }
355     else
356     {
357       TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(
358               backupFiles, basename);
359       // bfTreeMap now a sorted list of <Integer index>,<File backupfile>
360       // mappings
361
362       if (reverseOrder)
363       {
364         // backup style numbering
365         Console.trace("BACKUPFILES rolling files in reverse order");
366
367         int tempMax = noMax ? -1 : max;
368         // noMax == true means no limits
369         // look for first "gap" in backupFiles
370         // if tempMax is -1 at this stage just keep going until there's a gap,
371         // then hopefully tempMax gets set to the right index (a positive
372         // integer so the loop breaks)...
373         // why do I feel a little uneasy about this loop?..
374         for (int i = 1; tempMax < 0 || i <= max; i++)
375         {
376           if (!bfTreeMap.containsKey(i)) // first index without existent
377                                          // backupfile
378           {
379             tempMax = i;
380           }
381         }
382
383         File previousFile = null;
384         File fileToBeDeleted = null;
385         for (int n = tempMax; n > 0; n--)
386         {
387           String backupfilename = dir + File.separatorChar
388                   + BackupFilenameParts.getBackupFilename(n, basename,
389                           suffix, digits);
390           File backupfile_n = new File(backupfilename);
391
392           if (!backupfile_n.exists())
393           {
394             // no "oldest" file to delete
395             previousFile = backupfile_n;
396             fileToBeDeleted = null;
397             Console.trace("BACKUPFILES No oldest file to delete");
398             continue;
399           }
400
401           // check the modification time of this (backupfile_n) and the previous
402           // file (fileToBeDeleted) if the previous file is going to be deleted
403           if (fileToBeDeleted != null)
404           {
405             File replacementFile = backupfile_n;
406             long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
407             long replacementFileLMT = replacementFile.lastModified();
408             Console.trace("BACKUPFILES fileToBeDeleted is "
409                     + fileToBeDeleted.getAbsolutePath());
410             Console.trace("BACKUPFILES replacementFile is "
411                     + backupfile_n.getAbsolutePath());
412
413             try
414             {
415               File oldestTempFile = nextTempFile(fileToBeDeleted.getName(),
416                       dirFile);
417
418               if (fileToBeDeletedLMT > replacementFileLMT)
419               {
420                 String fileToBeDeletedLMTString = sdf
421                         .format(fileToBeDeletedLMT);
422                 String replacementFileLMTString = sdf
423                         .format(replacementFileLMT);
424                 Console.warn("WARNING! I am set to delete backupfile "
425                         + fileToBeDeleted.getName()
426                         + " has modification time "
427                         + fileToBeDeletedLMTString
428                         + " which is newer than its replacement "
429                         + replacementFile.getName()
430                         + " with modification time "
431                         + replacementFileLMTString);
432
433                 boolean delete = confirmNewerDeleteFile(fileToBeDeleted,
434                         replacementFile, true);
435                 Console.trace("BACKUPFILES "
436                         + (delete ? "confirmed" : "not") + " deleting file "
437                         + fileToBeDeleted.getAbsolutePath()
438                         + " which is newer than "
439                         + replacementFile.getAbsolutePath());
440
441                 if (delete)
442                 {
443                   // User has confirmed delete -- no need to add it to the list
444                   fileToBeDeleted.delete();
445                 }
446                 else
447                 {
448                   Console.debug("BACKUPFILES moving "
449                           + fileToBeDeleted.getAbsolutePath() + " to "
450                           + oldestTempFile.getAbsolutePath());
451                   moveFileToFile(fileToBeDeleted, oldestTempFile);
452                 }
453               }
454               else
455               {
456                 Console.debug("BACKUPFILES going to move "
457                         + fileToBeDeleted.getAbsolutePath() + " to "
458                         + oldestTempFile.getAbsolutePath());
459                 moveFileToFile(fileToBeDeleted, oldestTempFile);
460                 addDeleteFile(oldestTempFile);
461               }
462
463             } catch (Exception e)
464             {
465               Console.error(
466                       "Error occurred, probably making new temp file for '"
467                               + fileToBeDeleted.getName() + "'");
468               Console.error(Cache.getStackTraceString(e));
469             }
470
471             // reset
472             fileToBeDeleted = null;
473           }
474
475           if (!noMax && n == tempMax && backupfile_n.exists())
476           {
477             fileToBeDeleted = backupfile_n;
478           }
479           else
480           {
481             if (previousFile != null)
482             {
483               // using boolean '&' instead of '&&' as don't want moveFileToFile
484               // attempt to be conditional (short-circuit)
485               ret = ret & moveFileToFile(backupfile_n, previousFile);
486             }
487           }
488
489           previousFile = backupfile_n;
490         }
491
492         // index to use for the latest backup
493         nextIndexNum = 1;
494       }
495       else // not reverse numbering
496       {
497         // version style numbering (with earliest file deletion if max files
498         // reached)
499
500         bfTreeMap.values().toArray(backupFiles);
501         StringBuilder bfsb = new StringBuilder();
502         for (int i = 0; i < backupFiles.length; i++)
503         {
504           if (bfsb.length() > 0)
505           {
506             bfsb.append(", ");
507           }
508           bfsb.append(backupFiles[i].getName());
509         }
510         Console.trace("BACKUPFILES backupFiles: " + bfsb.toString());
511
512         // noMax == true means keep all backup files
513         if ((!noMax) && bfTreeMap.size() >= max)
514         {
515           Console.trace("BACKUPFILES noMax: " + noMax + ", " + "max: " + max
516                   + ", " + "bfTreeMap.size(): " + bfTreeMap.size());
517           // need to delete some files to keep number of backups to designated
518           // max.
519           // Note that if the suffix is not numbered then do not delete any
520           // backup files later or we'll delete the new backup file (there can
521           // be only one).
522           int numToDelete = suffix.indexOf(NUM_PLACEHOLDER) > -1
523                   ? bfTreeMap.size() - max + 1
524                   : 0;
525           Console.trace("BACKUPFILES numToDelete: " + numToDelete);
526           // the "replacement" file is the latest backup file being kept (it's
527           // not replacing though)
528           File replacementFile = numToDelete < backupFiles.length
529                   ? backupFiles[numToDelete]
530                   : null;
531           for (int i = 0; i < numToDelete; i++)
532           {
533             // check the deletion files for modification time of the last
534             // backupfile being saved
535             File fileToBeDeleted = backupFiles[i];
536             boolean delete = true;
537
538             Console.trace(
539                     "BACKUPFILES fileToBeDeleted: " + fileToBeDeleted);
540
541             boolean newer = false;
542             if (replacementFile != null)
543             {
544               long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
545               long replacementFileLMT = replacementFile != null
546                       ? replacementFile.lastModified()
547                       : Long.MAX_VALUE;
548               if (fileToBeDeletedLMT > replacementFileLMT)
549               {
550                 String fileToBeDeletedLMTString = sdf
551                         .format(fileToBeDeletedLMT);
552                 String replacementFileLMTString = sdf
553                         .format(replacementFileLMT);
554
555                 Console.warn("WARNING! I am set to delete backupfile '"
556                         + fileToBeDeleted.getName()
557                         + "' has modification time "
558                         + fileToBeDeletedLMTString
559                         + " which is newer than the oldest backupfile being kept '"
560                         + replacementFile.getName()
561                         + "' with modification time "
562                         + replacementFileLMTString);
563
564                 delete = confirmNewerDeleteFile(fileToBeDeleted,
565                         replacementFile, false);
566                 if (delete)
567                 {
568                   // User has confirmed delete -- no need to add it to the list
569                   fileToBeDeleted.delete();
570                   Console.debug("BACKUPFILES deleting fileToBeDeleted: "
571                           + fileToBeDeleted);
572                   delete = false;
573                 }
574                 else
575                 {
576                   // keeping file, nothing to do!
577                   Console.debug("BACKUPFILES keeping fileToBeDeleted: "
578                           + fileToBeDeleted);
579                 }
580               }
581             }
582             if (delete)
583             {
584               addDeleteFile(fileToBeDeleted);
585               Console.debug("BACKUPFILES addDeleteFile(fileToBeDeleted): "
586                       + fileToBeDeleted);
587             }
588
589           }
590
591         }
592
593         nextIndexNum = bfTreeMap.lastKey() + 1;
594       }
595     }
596
597     // Let's make the new backup file!! yay, got there at last!
598     String latestBackupFilename = dir + File.separatorChar
599             + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
600                     suffix, digits);
601     Console.trace("BACKUPFILES Moving old file [" + file
602             + "] to latestBackupFilename [" + latestBackupFilename + "]");
603     // using boolean '&' instead of '&&' as don't want moveFileToFile attempt to
604     // be conditional (short-circuit)
605     ret = ret & moveFileToFile(file, new File(latestBackupFilename));
606     Console.debug(
607             "BACKUPFILES moving " + file + " to " + latestBackupFilename
608                     + " was " + (ret ? "" : "NOT ") + "successful");
609     if (tidyUp)
610     {
611       Console.debug("BACKUPFILES tidying up files");
612       tidyUpFiles();
613     }
614
615     return ret;
616   }
617
618   private static File nextTempFile(String filename, File dirFile)
619           throws IOException
620   {
621     File temp = null;
622     COUNT: for (int i = 1; i < 1000; i++)
623     {
624       File trythis = new File(dirFile,
625               filename + '~' + Integer.toString(i));
626       if (!trythis.exists())
627       {
628         temp = trythis;
629         break COUNT;
630       }
631
632     }
633     if (temp == null)
634     {
635       temp = File.createTempFile(filename, TEMP_FILE_EXT, dirFile);
636     }
637     return temp;
638   }
639
640   private void tidyUpFiles()
641   {
642     deleteOldFiles();
643   }
644
645   private static boolean confirmNewerDeleteFile(File fileToBeDeleted,
646           File replacementFile, boolean replace)
647   {
648     StringBuilder messageSB = new StringBuilder();
649
650     File ftbd = fileToBeDeleted;
651     String ftbdLMT = sdf.format(ftbd.lastModified());
652     String ftbdSize = Long.toString(ftbd.length());
653
654     File rf = replacementFile;
655     String rfLMT = sdf.format(rf.lastModified());
656     String rfSize = Long.toString(rf.length());
657
658     int confirmButton = JvOptionPane.NO_OPTION;
659     if (replace)
660     {
661       File saveFile = null;
662       try
663       {
664         saveFile = nextTempFile(ftbd.getName(), ftbd.getParentFile());
665       } catch (Exception e)
666       {
667         Console.error(
668                 "Error when confirming to keep backup file newer than other backup files.");
669         e.printStackTrace();
670       }
671       messageSB.append(MessageManager.formatMessage(
672               "label.newerdelete_replacement_line", new String[]
673               { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
674                   rfSize }));
675       // "Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted and
676       // replaced by apparently older file \n''{1}''\t(modified {3}, size
677       // {5}).""
678       messageSB.append("\n\n");
679       messageSB.append(MessageManager.formatMessage(
680               "label.confirm_deletion_or_rename", new String[]
681               { ftbd.getName(), saveFile.getName() }));
682       // "Confirm deletion of ''{0}'' or rename to ''{1}''?"
683       String[] options = new String[] {
684           MessageManager.getString("label.delete"),
685           MessageManager.getString("label.rename") };
686
687       confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
688               : JvOptionPane.showOptionDialog(Desktop.desktop,
689                       messageSB.toString(),
690                       MessageManager.getString(
691                               "label.backupfiles_confirm_delete"),
692                       // "Confirm delete"
693                       JvOptionPane.YES_NO_OPTION,
694                       JvOptionPane.WARNING_MESSAGE, null, options,
695                       options[0]);
696     }
697     else
698     {
699       messageSB.append(MessageManager
700               .formatMessage("label.newerdelete_line", new String[]
701               { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
702                   rfSize }));
703       // "Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted but
704       // is newer than the oldest remaining backup file \n''{1}''\t(modified
705       // {3}, size {5})."
706       messageSB.append("\n\n");
707       messageSB.append(MessageManager
708               .formatMessage("label.confirm_deletion", new String[]
709               { ftbd.getName() }));
710       // "Confirm deletion of ''{0}''?"
711       String[] options = new String[] {
712           MessageManager.getString("label.delete"),
713           MessageManager.getString("label.keep") };
714
715       confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
716               : JvOptionPane.showOptionDialog(Desktop.desktop,
717                       messageSB.toString(),
718                       MessageManager.getString(
719                               "label.backupfiles_confirm_delete"),
720                       // "Confirm delete"
721                       JvOptionPane.YES_NO_OPTION,
722                       JvOptionPane.WARNING_MESSAGE, null, options,
723                       options[0]);
724     }
725
726     // return should be TRUE if file is to be deleted
727     return (confirmButton == JvOptionPane.YES_OPTION);
728   }
729
730   private void deleteOldFiles()
731   {
732     if (deleteFiles != null && !deleteFiles.isEmpty())
733     {
734       boolean doDelete = false;
735       StringBuilder messageSB = null;
736       if (confirmDelete && deleteFiles.size() > 0)
737       {
738         messageSB = new StringBuilder();
739         messageSB.append(MessageManager
740                 .getString("label.backupfiles_confirm_delete_old_files"));
741         // "Delete the following older backup files? (see the Backups tab in
742         // Preferences for more options)"
743         for (int i = 0; i < deleteFiles.size(); i++)
744         {
745           File df = deleteFiles.get(i);
746           messageSB.append("\n");
747           messageSB.append(df.getName());
748           messageSB.append(" ");
749           messageSB.append(MessageManager.formatMessage("label.file_info",
750                   new String[]
751                   { sdf.format(df.lastModified()),
752                       Long.toString(df.length()) }));
753           // "(modified {0}, size {1})"
754         }
755
756         int confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
757                 : JvOptionPane.showConfirmDialog(Desktop.desktop,
758                         messageSB.toString(),
759                         MessageManager.getString(
760                                 "label.backupfiles_confirm_delete"),
761                         // "Confirm delete"
762                         JvOptionPane.YES_NO_OPTION,
763                         JvOptionPane.WARNING_MESSAGE);
764
765         doDelete = (confirmButton == JvOptionPane.YES_OPTION);
766       }
767       else
768       {
769         doDelete = true;
770       }
771
772       if (doDelete)
773       {
774         for (int i = 0; i < deleteFiles.size(); i++)
775         {
776           File fileToDelete = deleteFiles.get(i);
777           Console.trace("BACKUPFILES about to delete fileToDelete:"
778                   + fileToDelete);
779           fileToDelete.delete();
780           Console.warn("deleted '" + fileToDelete.getName() + "'");
781         }
782       }
783
784     }
785
786     deleteFiles.clear();
787   }
788
789   private TreeMap<Integer, File> sortBackupFilesAsTreeMap(
790           File[] backupFiles, String basename)
791   {
792     // sort the backup files (based on integer found in the suffix) using a
793     // precomputed Hashmap for speed
794     Map<Integer, File> bfHashMap = new HashMap<>();
795     for (int i = 0; i < backupFiles.length; i++)
796     {
797       File f = backupFiles[i];
798       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
799               digits);
800       bfHashMap.put(bfp.indexNum(), f);
801     }
802     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
803     bfTreeMap.putAll(bfHashMap);
804     return bfTreeMap;
805   }
806
807   public boolean rollBackupsAndRenameTempFile()
808   {
809     boolean write = this.getWriteSuccess();
810
811     boolean roll = false;
812     boolean rename = false;
813     if (write)
814     {
815       roll = this.rollBackupFiles(false); // tidyUpFiles at the end
816       rename = this.renameTempFile();
817     }
818
819     /*
820      * Not sure that this confirmation is desirable.  By this stage the new file is
821      * already written successfully, but something (e.g. disk full) has happened while 
822      * trying to roll the backup files, and most likely the filename needed will already
823      * be vacant so renaming the temp file is nearly always correct!
824      */
825     boolean okay = roll && rename;
826     if (!okay)
827     {
828       StringBuilder messageSB = new StringBuilder();
829       messageSB.append(MessageManager.getString(
830               "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"));
831       // "Something possibly went wrong with the backups of this file."
832       if (rename)
833       {
834         if (messageSB.length() > 0)
835         {
836           messageSB.append("\n");
837         }
838         messageSB.append(MessageManager.getString(
839                 "label.backupfiles_confirm_save_new_saved_file_ok"));
840         // "The new saved file seems okay."
841       }
842       else
843       {
844         if (messageSB.length() > 0)
845         {
846           messageSB.append("\n");
847         }
848         messageSB.append(MessageManager.getString(
849                 "label.backupfiles_confirm_save_new_saved_file_not_ok"));
850         // "The new saved file might not be okay."
851       }
852       if (messageSB.length() > 0)
853       {
854         messageSB.append("\n");
855       }
856       messageSB
857               .append(MessageManager.getString("label.continue_operation"));
858
859       int confirmButton = Platform.isHeadless() ? JvOptionPane.OK_OPTION
860               : JvOptionPane.showConfirmDialog(Desktop.desktop,
861                       messageSB.toString(),
862                       MessageManager.getString(
863                               "label.backupfiles_confirm_save_file"),
864                       // "Confirm save file"
865                       JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
866       okay = confirmButton == JvOptionPane.OK_OPTION;
867     }
868     if (okay)
869     {
870       tidyUpFiles();
871     }
872
873     // remove this file from the save in progress stack
874     removeSaveInProgress();
875
876     return rename;
877   }
878
879   public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
880           String fileName, String suffix, int digits)
881   {
882     File[] backupFiles = null;
883
884     File file = new File(fileName);
885
886     File dirFile;
887     try
888     {
889       dirFile = file.getParentFile();
890     } catch (Exception e)
891     {
892       Console.error("Could not get canonical path for file '" + file + "'");
893       return new TreeMap<>();
894     }
895
896     String filename = file.getName();
897     String basename = filename;
898
899     // find existing backup files
900     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
901             digits);
902     backupFiles = dirFile.listFiles(bff); // is clone needed?
903
904     // sort the backup files (based on integer found in the suffix) using a
905     // precomputed Hashmap for speed
906     Map<Integer, File> bfHashMap = new HashMap<>();
907     for (int i = 0; i < backupFiles.length; i++)
908     {
909       File f = backupFiles[i];
910       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
911               digits);
912       bfHashMap.put(bfp.indexNum(), f);
913     }
914     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
915     bfTreeMap.putAll(bfHashMap);
916
917     return bfTreeMap;
918   }
919
920   /*
921   private boolean addDeleteFile(File fileToBeDeleted, File originalFile,
922           boolean delete, boolean newer)
923   {
924     return addDeleteFile(fileToBeDeleted, originalFile, null, delete, newer);
925   }
926   */
927   private boolean addDeleteFile(File fileToBeDeleted)
928   {
929     boolean ret = false;
930     int pos = deleteFiles.indexOf(fileToBeDeleted);
931     if (pos > -1)
932     {
933       Console.debug("BACKUPFILES not adding file "
934               + fileToBeDeleted.getAbsolutePath()
935               + " to the delete list (already at index" + pos + ")");
936       return true;
937     }
938     else
939     {
940       Console.debug("BACKUPFILES adding file "
941               + fileToBeDeleted.getAbsolutePath() + " to the delete list");
942       deleteFiles.add(fileToBeDeleted);
943     }
944     return ret;
945   }
946
947   public File getFile()
948   {
949     return file;
950   }
951
952   public static boolean moveFileToFile(File oldFile, File newFile)
953   {
954     Console.initLogger();
955     boolean ret = false;
956     Path oldPath = Paths.get(oldFile.getAbsolutePath());
957     Path newPath = Paths.get(newFile.getAbsolutePath());
958     try
959     {
960       // delete destination file - not usually necessary but Just In Case...
961       Console.trace("BACKUPFILES deleting " + newFile.getAbsolutePath());
962       newFile.delete();
963       Console.trace("BACKUPFILES moving " + oldFile.getAbsolutePath()
964               + " to " + newFile.getAbsolutePath());
965       Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING);
966       ret = true;
967       Console.trace("BACKUPFILES move seems to have succeeded");
968     } catch (IOException e)
969     {
970       Console.warn("Could not move file '" + oldPath.toString() + "' to '"
971               + newPath.toString() + "'");
972       Console.error(e.getMessage());
973       Console.debug(Cache.getStackTraceString(e));
974       ret = false;
975     } catch (Exception e)
976     {
977       Console.error(e.getMessage());
978       Console.debug(Cache.getStackTraceString(e));
979       ret = false;
980     }
981     return ret;
982   }
983 }