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