JAL-3628 More Cache.log.debug statements
[jalview.git] / src / jalview / io / BackupFiles.java
index c58a714..91cc5fa 100644 (file)
@@ -1,52 +1,64 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.io;
 
-import jalview.bin.Cache;
-import jalview.gui.Desktop;
-import jalview.gui.JvOptionPane;
-import jalview.util.MessageManager;
-
 import java.io.File;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Map;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.TreeMap;
 
+import jalview.bin.Cache;
+import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+
 /*
  * BackupFiles used for manipulating (naming rolling/deleting) backup/version files when an alignment or project file is saved.
  * User configurable options are:
  * BACKUPFILES_ENABLED - boolean flag as to whether to use this mechanism or act as before, including overwriting files as saved.
- * BACKUPFILES_SUFFIX - a template to insert just before the file extension.  Use '%n' to be replaced by a 0-led SUFFIX_DIGITS long integer.
- * BACKUPFILES_NO_MAX - flag to turn off setting a maximum number of backup files to keep.
- * BACKUPFILES_ROLL_MAX - the maximum number of backupfiles to keep for any one alignment or project file.
- * BACKUPFILES_SUFFIX_DIGITS - the number of digits to insert replace %n with (e.g. BACKUPFILES_SUFFIX_DIGITS = 3 would make "001", "002", etc)
- * BACKUPFILES_REVERSE_ORDER - if true then "logfile" style numbering and file rolling will occur. If false then ever-increasing version numbering will occur, but old files will still be deleted if there are more than ROLL_MAX backup files. 
- * BACKUPFILES_CONFIRM_DELETE_OLD - if true then prompt/confirm with the user when deleting older backup/version files.
+ * The rest of the options are now saved as BACKUPFILES_PRESET, BACKUPFILES_SAVED and BACKUPFILES_CUSTOM
+ * (see BackupFilesPresetEntry)
  */
 
 public class BackupFiles
 {
 
   // labels for saved params in Cache and .jalview_properties
-  private static String NS = "BACKUPFILES";
-
-  public static String ENABLED = NS + "_ENABLED";
+  public static final String NS = "BACKUPFILES";
 
-  public static String SUFFIX = NS + "_SUFFIX";
-
-  public static String NO_MAX = NS + "_NO_MAX";
-
-  public static String ROLL_MAX = NS + "_ROLL_MAX";
-
-  public static String SUFFIX_DIGITS = NS + "_SUFFIX_DIGITS";
+  public static final String ENABLED = NS + "_ENABLED";
 
   public static final String NUM_PLACEHOLDER = "%n";
 
-  public static String REVERSE_ORDER = NS + "_REVERSE_ORDER";
+  private static final String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
 
-  public static String CONFIRM_DELETE_OLD = NS + "_CONFIRM_DELETE_OLD";
-
-  private static String DEFAULT_TEMP_FILE = "jalview_temp_file_" + NS;
+  private static final String TEMP_FILE_EXT = ".tmp";
 
   // file - File object to be backed up and then updated (written over)
   private File file;
@@ -81,6 +93,17 @@ public class BackupFiles
   // flag set to see if file save to temp file was successful
   private boolean tempFileWriteSuccess;
 
+  // array of files to be deleted, with extra information
+  private ArrayList<File> deleteFiles = new ArrayList<>();
+
+  // date formatting for modification times
+  private static final SimpleDateFormat sdf = new SimpleDateFormat(
+          "yyyy-MM-dd HH:mm:ss");
+
+  private static final String newTempFileSuffix = "_newfile";
+
+  private static final String oldTempFileSuffix = "_oldfile_tobedeleted";
+
   public BackupFiles(String filename)
   {
     this(new File(filename));
@@ -90,22 +113,15 @@ public class BackupFiles
   // REVERSE_ORDER
   public BackupFiles(File file)
   {
-    this(file, "-v" + NUM_PLACEHOLDER, false, 4, 3, false);
-  }
-
-  public BackupFiles(File file,
-          String defaultSuffix, boolean defaultNoMax, int defaultMax,
-          int defaultDigits,
-          boolean defaultReverseOrder)
-  {
     classInit();
     this.file = file;
-    this.suffix = Cache.getDefault(SUFFIX, defaultSuffix);
-    this.noMax = Cache.getDefault(NO_MAX, defaultNoMax);
-    this.max = Cache.getDefault(ROLL_MAX, defaultMax);
-    this.digits = Cache.getDefault(SUFFIX_DIGITS, defaultDigits);
-    this.reverseOrder = Cache.getDefault(REVERSE_ORDER,
-            defaultReverseOrder);
+    BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
+            .getSavedBackupEntry();
+    this.suffix = bfpe.suffix;
+    this.noMax = bfpe.keepAll;
+    this.max = bfpe.rollMax;
+    this.digits = bfpe.digits;
+    this.reverseOrder = bfpe.reverse;
 
     // create a temp file to save new data in
     File temp = null;
@@ -115,27 +131,41 @@ public class BackupFiles
       {
         String tempfilename = file.getName();
         File tempdir = file.getParentFile();
-        temp = File.createTempFile(tempfilename, ".tmp", tempdir);
+        Cache.log.debug(
+                "BACKUPFILES [file!=null] attempting to create temp file "
+                        + tempfilename + " in dir " + tempdir);
+        temp = File.createTempFile(tempfilename,
+                TEMP_FILE_EXT + newTempFileSuffix, tempdir);
       }
       else
       {
-        temp = File.createTempFile(DEFAULT_TEMP_FILE, ".tmp");
+        Cache.log.debug(
+                "BACKUPFILES [file==null] attempting to create default temp file "
+                        + DEFAULT_TEMP_FILE + " with extension "
+                        + TEMP_FILE_EXT);
+        temp = File.createTempFile(DEFAULT_TEMP_FILE, TEMP_FILE_EXT);
       }
     } catch (IOException e)
     {
-      System.out.println(
-              "Could not create temp file to save into (IOException)");
+      Cache.log
+              .error("Could not create temp file to save to (IOException)");
+      Cache.log.error(e.getMessage());
+      Cache.log.debug(e.getStackTrace());
     } catch (Exception e)
     {
-      System.out.println("Exception ctreating temp file for saving");
+      Cache.log.error("Exception creating temp file for saving");
+      Cache.log.debug(e.getStackTrace());
     }
     this.setTempFile(temp);
   }
 
   public static void classInit()
   {
-    setEnabled(Cache.getDefault(ENABLED, true));
-    setConfirmDelete(Cache.getDefault(CONFIRM_DELETE_OLD, true));
+    Cache.log.debug("BACKUPFILES classInit");
+    setEnabled(Cache.getDefault(ENABLED, !Platform.isJS()));
+    BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
+            .getSavedBackupEntry();
+    setConfirmDelete(bfpe.confirmDelete);
   }
 
   public static void setEnabled(boolean flag)
@@ -179,9 +209,10 @@ public class BackupFiles
       path = this.getTempFile().getCanonicalPath();
     } catch (IOException e)
     {
-      System.out.println(
+      Cache.log.error(
               "IOException when getting Canonical Path of temp file '"
                       + this.getTempFile().getName() + "'");
+      Cache.log.debug(e.getStackTrace());
     }
     return path;
   }
@@ -200,85 +231,84 @@ public class BackupFiles
 
   public boolean renameTempFile()
   {
-    return tempFile.renameTo(file);
+    return moveFileToFile(tempFile, file);
   }
 
-
   // roll the backupfiles
   public boolean rollBackupFiles()
   {
+    return this.rollBackupFiles(true);
+  }
 
-    // file doesn't yet exist or backups are not enabled
-    if ((!file.exists()) || (!enabled) || (max < 0))
+  public boolean rollBackupFiles(boolean tidyUp)
+  {
+    // file doesn't yet exist or backups are not enabled or template is null or
+    // empty
+    if ((!file.exists()) || (!enabled) || max < 0 || suffix == null
+            || suffix.length() == 0)
     {
       // nothing to do
+      Cache.log.debug("BACKUPFILES rollBackupFiles nothing to do." + ", "
+              + "filename: " + (file != null ? file.getName() : "null")
+              + ", " + "file exists: " + file.exists() + ", " + "enabled: "
+              + enabled + ", " + "max: " + max + ", " + "suffix: '" + suffix
+              + "'");
       return true;
     }
 
-    // split filename up to insert suffix template in the right place. template
-    // and backupMax can be set in .jalview_properties
+    Cache.log.debug("BACKUPFILES rollBackupFiles starting");
+
     String dir = "";
     File dirFile;
     try
     {
       dirFile = file.getParentFile();
       dir = dirFile.getCanonicalPath();
+      Cache.log.debug("BACKUPFILES dir: " + dir);
     } catch (Exception e)
     {
-      System.out.println(
+      Cache.log.error(
               "Could not get canonical path for file '" + file + "'");
+      Cache.log.error(e.getMessage());
+      Cache.log.debug(e.getStackTrace());
       return false;
     }
     String filename = file.getName();
     String basename = filename;
-    String extension = "";
-    int dotcharpos = filename.lastIndexOf('.');
-    // don't split of filenames with the last '.' at the very beginning or
-    // very end of the filename
-    if ((dotcharpos > 0) && (dotcharpos < filename.length() - 1))
-    {
-      basename = filename.substring(0, dotcharpos);
-      extension = filename.substring(dotcharpos); // NOTE this includes the '.'
-    }
 
+    Cache.log.debug("BACKUPFILES filename is " + filename);
     boolean ret = true;
     // Create/move backups up one
 
-    File[] oldFilesToDelete = null;
-    
+    deleteFiles.clear();
+
     // find existing backup files
     BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
-            digits,
-            extension);
+            digits);
     File[] backupFiles = dirFile.listFiles(bff);
     int nextIndexNum = 0;
-    String confirmDeleteExtraInfo = null;
-    
+
+    Cache.log
+            .debug("BACKUPFILES backupFiles.length: " + backupFiles.length);
     if (backupFiles.length == 0)
     {
       // No other backup files. Just need to move existing file to backupfile_1
+      Cache.log.debug(
+              "BACKUPFILES no existing backup files, setting index to 1");
       nextIndexNum = 1;
     }
     else
     {
-
-      // sort the backup files (based on integer found in the suffix) using a
-      // precomputed Hashmap for speed
-      Map<Integer, File> bfHashMap = new HashMap<>();
-      for (int i = 0; i < backupFiles.length; i++)
-      {
-        File f = backupFiles[i];
-        BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix, digits, extension);
-        bfHashMap.put(bfp.indexNum(), f);
-      }
-      TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
-      bfTreeMap.putAll(bfHashMap);
+      TreeMap<Integer, File> bfTreeMap = sortBackupFilesAsTreeMap(
+              backupFiles, basename);
+      // bfTreeMap now a sorted list of <Integer index>,<File backupfile>
+      // mappings
 
       if (reverseOrder)
       {
         // backup style numbering
+        Cache.log.debug("BACKUPFILES rolling files in reverse order");
 
-        File lastfile = null;
         int tempMax = noMax ? -1 : max;
         // noMax == true means no limits
         // look for first "gap" in backupFiles
@@ -295,214 +325,515 @@ public class BackupFiles
           }
         }
 
-        // for (int m = 0; m < tempMax; m++)
+        File previousFile = null;
+        File fileToBeDeleted = null;
         for (int n = tempMax; n > 0; n--)
         {
-          // int n = tempMax - m;
           String backupfilename = dir + File.separatorChar
-                  + BackupFilenameFilter.getBackupFilename(n, basename,
-                          suffix, digits, extension);
+                  + BackupFilenameParts.getBackupFilename(n, basename,
+                          suffix, digits);
           File backupfile_n = new File(backupfilename);
 
           if (!backupfile_n.exists())
           {
-            lastfile = backupfile_n;
+            // no "oldest" file to delete
+            previousFile = backupfile_n;
+            fileToBeDeleted = null;
+            Cache.log.debug("BACKUPFILES No oldest file to delete");
             continue;
           }
 
-          // if (m == 0 && backupfile_n.exists())
-          if ((!noMax) && n == tempMax && backupfile_n.exists())
+          // check the modification time of this (backupfile_n) and the previous
+          // file (fileToBeDeleted) if the previous file is going to be deleted
+          if (fileToBeDeleted != null)
           {
-            // move the largest (max) rolled file to a temp file and add to the delete list
+            File replacementFile = backupfile_n;
+            long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
+            long replacementFileLMT = replacementFile.lastModified();
+            Cache.log.debug("BACKUPFILES fileToBeDeleted is "
+                    + fileToBeDeleted.getAbsolutePath());
+            Cache.log.debug("BACKUPFILES replacementFile is "
+                    + backupfile_n.getAbsolutePath());
+
             try
             {
-              File temp = File.createTempFile(backupfilename, ".tmp",
-                    dirFile);
-              backupfile_n.renameTo(temp);
-
-              oldFilesToDelete = new File[] { temp };
-              confirmDeleteExtraInfo = "(was " + backupfile_n.getName()
-                      + ")";
-            } catch (IOException e)
+              File oldestTempFile = nextTempFile(fileToBeDeleted.getName(),
+                      dirFile);
+
+              if (fileToBeDeletedLMT > replacementFileLMT)
+              {
+                String fileToBeDeletedLMTString = sdf
+                        .format(fileToBeDeletedLMT);
+                String replacementFileLMTString = sdf
+                        .format(replacementFileLMT);
+                Cache.log.warn("WARNING! I am set to delete backupfile "
+                        + fileToBeDeleted.getName()
+                        + " has modification time "
+                        + fileToBeDeletedLMTString
+                        + " which is newer than its replacement "
+                        + replacementFile.getName()
+                        + " with modification time "
+                        + replacementFileLMTString);
+
+                boolean delete = confirmNewerDeleteFile(fileToBeDeleted,
+                        replacementFile, true);
+                Cache.log.debug("BACKUPFILES "
+                        + (delete ? "confirmed" : "not") + " deleting file "
+                        + fileToBeDeleted.getAbsolutePath()
+                        + " which is newer than "
+                        + replacementFile.getAbsolutePath());
+
+                if (delete)
+                {
+                  // User has confirmed delete -- no need to add it to the list
+                  fileToBeDeleted.delete();
+                }
+                else
+                {
+                  Cache.log.debug("BACKUPFILES moving "
+                          + fileToBeDeleted.getAbsolutePath() + " to "
+                          + oldestTempFile.getAbsolutePath());
+                  moveFileToFile(fileToBeDeleted, oldestTempFile);
+                }
+              }
+              else
+              {
+                Cache.log.debug("BACKUPFILES going to move "
+                        + fileToBeDeleted.getAbsolutePath() + " to "
+                        + oldestTempFile.getAbsolutePath());
+                moveFileToFile(fileToBeDeleted, oldestTempFile);
+                addDeleteFile(oldestTempFile);
+              }
+
+            } catch (Exception e)
             {
-              System.out.println(
-                      "IOException when creating temporary file for backupfilename");
+              Cache.log.error(
+                      "Error occurred, probably making new temp file for '"
+                              + fileToBeDeleted.getName() + "'");
+              Cache.log.error(e.getStackTrace());
             }
+
+            // reset
+            fileToBeDeleted = null;
+          }
+
+          if (!noMax && n == tempMax && backupfile_n.exists())
+          {
+            fileToBeDeleted = backupfile_n;
           }
           else
           {
-            // Just In Case
-            if (lastfile != null)
+            if (previousFile != null)
             {
-              ret = ret && backupfile_n.renameTo(lastfile);
+              ret = ret && moveFileToFile(backupfile_n, previousFile);
             }
           }
 
-          lastfile = backupfile_n;
+          previousFile = backupfile_n;
         }
 
         // index to use for the latest backup
         nextIndexNum = 1;
       }
-      else
+      else // not reverse numbering
       {
         // version style numbering (with earliest file deletion if max files
         // reached)
 
-
         bfTreeMap.values().toArray(backupFiles);
+        StringBuilder bfsb = new StringBuilder();
+        for (int i = 0; i < backupFiles.length; i++)
+        {
+          if (bfsb.length() > 0)
+          {
+            bfsb.append(", ");
+          }
+          bfsb.append(backupFiles[i].getName());
+        }
+        Cache.log.debug("BACKUPFILES backupFiles: " + bfsb.toString());
 
         // noMax == true means keep all backup files
         if ((!noMax) && bfTreeMap.size() >= max)
         {
+          Cache.log.debug("BACKUPFILES noMax: " + noMax + ", " + "max: "
+                  + max + ", " + "bfTreeMap.size(): " + bfTreeMap.size());
           // need to delete some files to keep number of backups to designated
-          // max
-          int numToDelete = bfTreeMap.size() - max + 1;
-          oldFilesToDelete = Arrays.copyOfRange(backupFiles, 0,
-                  numToDelete);
+          // max.
+          // Note that if the suffix is not numbered then do not delete any
+          // backup files later or we'll delete the new backup file (there can
+          // be only one).
+          int numToDelete = suffix.indexOf(NUM_PLACEHOLDER) > -1
+                  ? bfTreeMap.size() - max + 1
+                  : 0;
+          Cache.log.debug("BACKUPFILES numToDelete: " + numToDelete);
+          // the "replacement" file is the latest backup file being kept (it's
+          // not replacing though)
+          File replacementFile = numToDelete < backupFiles.length
+                  ? backupFiles[numToDelete]
+                  : null;
+          for (int i = 0; i < numToDelete; i++)
+          {
+            // check the deletion files for modification time of the last
+            // backupfile being saved
+            File fileToBeDeleted = backupFiles[i];
+            boolean delete = true;
+
+            Cache.log.debug(
+                    "BACKUPFILES fileToBeDeleted: " + fileToBeDeleted);
+
+            boolean newer = false;
+            if (replacementFile != null)
+            {
+              long fileToBeDeletedLMT = fileToBeDeleted.lastModified();
+              long replacementFileLMT = replacementFile != null
+                      ? replacementFile.lastModified()
+                      : Long.MAX_VALUE;
+              if (fileToBeDeletedLMT > replacementFileLMT)
+              {
+                String fileToBeDeletedLMTString = sdf
+                        .format(fileToBeDeletedLMT);
+                String replacementFileLMTString = sdf
+                        .format(replacementFileLMT);
+
+                Cache.log.warn("WARNING! I am set to delete backupfile '"
+                        + fileToBeDeleted.getName()
+                        + "' has modification time "
+                        + fileToBeDeletedLMTString
+                        + " which is newer than the oldest backupfile being kept '"
+                        + replacementFile.getName()
+                        + "' with modification time "
+                        + replacementFileLMTString);
+
+                delete = confirmNewerDeleteFile(fileToBeDeleted,
+                        replacementFile, false);
+                if (delete)
+                {
+                  // User has confirmed delete -- no need to add it to the list
+                  fileToBeDeleted.delete();
+                  Cache.log.debug("BACKUPFILES deleting fileToBeDeleted: "
+                          + fileToBeDeleted);
+                  delete = false;
+                }
+                else
+                {
+                  // keeping file, nothing to do!
+                  Cache.log.debug("BACKUPFILES keeping fileToBeDeleted: "
+                          + fileToBeDeleted);
+                }
+              }
+            }
+            if (delete)
+            {
+              addDeleteFile(fileToBeDeleted);
+              Cache.log.debug("BACKUPFILES addDeleteFile(fileToBeDeleted): "
+                      + fileToBeDeleted);
+            }
+
+          }
 
         }
 
         nextIndexNum = bfTreeMap.lastKey() + 1;
+      }
+    }
 
+    // Let's make the new backup file!! yay, got there at last!
+    String latestBackupFilename = dir + File.separatorChar
+            + BackupFilenameParts.getBackupFilename(nextIndexNum, basename,
+                    suffix, digits);
+    Cache.log.debug("BACKUPFILES Moving old file [" + file
+            + "] to latestBackupFilename [" + latestBackupFilename + "]");
+    ret |= moveFileToFile(file, new File(latestBackupFilename));
+    Cache.log.debug("BACKUPFILES moving " + latestBackupFilename + " to "
+            + file + " was " + (ret ? "" : "NOT ") + "successful");
+
+    if (tidyUp)
+    {
+      Cache.log.debug("BACKUPFILES tidying up files");
+      tidyUpFiles();
+    }
+
+    return ret;
+  }
+
+  private static File nextTempFile(String filename, File dirFile)
+          throws IOException
+  {
+    File temp = null;
+    COUNT: for (int i = 1; i < 1000; i++)
+    {
+      File trythis = new File(dirFile,
+              filename + '~' + Integer.toString(i));
+      if (!trythis.exists())
+      {
+        temp = trythis;
+        break COUNT;
       }
+
     }
+    if (temp == null)
+    {
+      temp = File.createTempFile(filename, TEMP_FILE_EXT, dirFile);
+    }
+    return temp;
+  }
+
+  private void tidyUpFiles()
+  {
+    deleteOldFiles();
+  }
+
+  private static boolean confirmNewerDeleteFile(File fileToBeDeleted,
+          File replacementFile, boolean replace)
+  {
+    StringBuilder messageSB = new StringBuilder();
+
+    File ftbd = fileToBeDeleted;
+    String ftbdLMT = sdf.format(ftbd.lastModified());
+    String ftbdSize = Long.toString(ftbd.length());
+
+    File rf = replacementFile;
+    String rfLMT = sdf.format(rf.lastModified());
+    String rfSize = Long.toString(rf.length());
 
-    if (oldFilesToDelete != null && oldFilesToDelete.length > 0)
+    int confirmButton = JvOptionPane.NO_OPTION;
+    if (replace)
+    {
+      File saveFile = null;
+      try
+      {
+        saveFile = nextTempFile(ftbd.getName(), ftbd.getParentFile());
+      } catch (Exception e)
+      {
+        Cache.log.error(
+                "Error when confirming to keep backup file newer than other backup files.");
+        e.printStackTrace();
+      }
+      messageSB.append(MessageManager.formatMessage(
+              "label.newerdelete_replacement_line", new String[]
+              { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
+                  rfSize }));
+      // "Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted and
+      // replaced by apparently older file \n''{1}''\t(modified {3}, size
+      // {5}).""
+      messageSB.append("\n\n");
+      messageSB.append(MessageManager.formatMessage(
+              "label.confirm_deletion_or_rename", new String[]
+              { ftbd.getName(), saveFile.getName() }));
+      // "Confirm deletion of ''{0}'' or rename to ''{1}''?"
+      String[] options = new String[] {
+          MessageManager.getString("label.delete"),
+          MessageManager.getString("label.rename") };
+
+      confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
+              : JvOptionPane.showOptionDialog(Desktop.desktop,
+                      messageSB.toString(),
+                      MessageManager.getString(
+                              "label.backupfiles_confirm_delete"),
+                      // "Confirm delete"
+                      JvOptionPane.YES_NO_OPTION,
+                      JvOptionPane.WARNING_MESSAGE, null, options,
+                      options[0]);
+    }
+    else
     {
-      // delete old backup/version files
+      messageSB.append(MessageManager
+              .formatMessage("label.newerdelete_line", new String[]
+              { ftbd.getName(), rf.getName(), ftbdLMT, rfLMT, ftbdSize,
+                  rfSize }));
+      // "Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted but
+      // is newer than the oldest remaining backup file \n''{1}''\t(modified
+      // {3}, size {5})."
+      messageSB.append("\n\n");
+      messageSB.append(MessageManager
+              .formatMessage("label.confirm_deletion", new String[]
+              { ftbd.getName() }));
+      // "Confirm deletion of ''{0}''?"
+      String[] options = new String[] {
+          MessageManager.getString("label.delete"),
+          MessageManager.getString("label.keep") };
+
+      confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
+              : JvOptionPane.showOptionDialog(Desktop.desktop,
+                      messageSB.toString(),
+                      MessageManager.getString(
+                              "label.backupfiles_confirm_delete"),
+                      // "Confirm delete"
+                      JvOptionPane.YES_NO_OPTION,
+                      JvOptionPane.WARNING_MESSAGE, null, options,
+                      options[0]);
+    }
 
-      boolean delete = false;
-      if (confirmDelete)
+    // return should be TRUE if file is to be deleted
+    return (confirmButton == JvOptionPane.YES_OPTION);
+  }
+
+  private void deleteOldFiles()
+  {
+    if (deleteFiles != null && !deleteFiles.isEmpty())
+    {
+      boolean doDelete = false;
+      StringBuilder messageSB = null;
+      if (confirmDelete && deleteFiles.size() > 0)
       {
-        // Object[] confirmMessageArray = {};
-        StringBuilder confirmMessage = new StringBuilder();
-        confirmMessage.append(MessageManager
+        messageSB = new StringBuilder();
+        messageSB.append(MessageManager
                 .getString("label.backupfiles_confirm_delete_old_files"));
-        for (File f : oldFilesToDelete)
-        {
-          confirmMessage.append("\n");
-          confirmMessage.append(f.getName());
-        }
-        if (confirmDeleteExtraInfo != null
-                && confirmDeleteExtraInfo.length() > 0)
+        // "Delete the following older backup files? (see the Backups tab in
+        // Preferences for more options)"
+        for (int i = 0; i < deleteFiles.size(); i++)
         {
-          confirmMessage.append("\n");
-          confirmMessage.append(confirmDeleteExtraInfo);
+          File df = deleteFiles.get(i);
+          messageSB.append("\n");
+          messageSB.append(df.getName());
+          messageSB.append(" ");
+          messageSB.append(MessageManager.formatMessage("label.file_info",
+                  new String[]
+                  { sdf.format(df.lastModified()),
+                      Long.toString(df.length()) }));
+          // "(modified {0}, size {1})"
         }
-        int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
-                confirmMessage.toString(),
-                MessageManager
-                        .getString("label.backupfiles_confirm_delete"),
-                JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
 
-        delete = (confirm == JvOptionPane.YES_OPTION);
+        int confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
+                : JvOptionPane.showConfirmDialog(Desktop.desktop,
+                        messageSB.toString(),
+                        MessageManager.getString(
+                                "label.backupfiles_confirm_delete"),
+                        // "Confirm delete"
+                        JvOptionPane.YES_NO_OPTION,
+                        JvOptionPane.WARNING_MESSAGE);
+
+        doDelete = (confirmButton == JvOptionPane.YES_OPTION);
       }
       else
       {
-        delete = true;
+        doDelete = true;
       }
 
-      if (delete)
+      if (doDelete)
       {
-        for (int i = 0; i < oldFilesToDelete.length; i++)
+        for (int i = 0; i < deleteFiles.size(); i++)
         {
-          File fileToDelete = oldFilesToDelete[i];
+          File fileToDelete = deleteFiles.get(i);
+          Cache.log.debug(
+                  "BACKUPFILES deleting fileToDelete:" + fileToDelete);
           fileToDelete.delete();
-          // System.out.println("DELETING '" + fileToDelete.getName() +
-          // "'");
+          Cache.log.warn("deleting '" + fileToDelete.getName() + "'");
         }
       }
 
     }
 
-    // Let's make the new backup file!! yay, got there at last!
-    String latestBackupFilename = dir + File.separatorChar
-            + BackupFilenameFilter.getBackupFilename(nextIndexNum, basename,
-                    suffix, digits, extension);
-    File latestBackupFile = new File(latestBackupFilename);
-    ret = ret && file.renameTo(latestBackupFile);
+    deleteFiles.clear();
+  }
 
-    return ret;
+  private TreeMap<Integer, File> sortBackupFilesAsTreeMap(
+          File[] backupFiles, String basename)
+  {
+    // sort the backup files (based on integer found in the suffix) using a
+    // precomputed Hashmap for speed
+    Map<Integer, File> bfHashMap = new HashMap<>();
+    for (int i = 0; i < backupFiles.length; i++)
+    {
+      File f = backupFiles[i];
+      BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
+              digits);
+      bfHashMap.put(bfp.indexNum(), f);
+    }
+    TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
+    bfTreeMap.putAll(bfHashMap);
+    return bfTreeMap;
   }
 
   public boolean rollBackupsAndRenameTempFile()
   {
     boolean write = this.getWriteSuccess();
-    
+
     boolean roll = false;
-    if (write) {
-      roll = this.rollBackupFiles();
-    } else {
-      return false;
+    boolean rename = false;
+    if (write)
+    {
+      roll = this.rollBackupFiles(false); // tidyUpFiles at the end
+      rename = this.renameTempFile();
     }
-    
+
     /*
      * Not sure that this confirmation is desirable.  By this stage the new file is
      * already written successfully, but something (e.g. disk full) has happened while 
      * trying to roll the backup files, and most likely the filename needed will already
      * be vacant so renaming the temp file is nearly always correct!
      */
-    if (!roll)
+    boolean okay = roll && rename;
+    if (!okay)
     {
-      int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
-              MessageManager.getString(
-                      "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"),
-              MessageManager.getString("label.backupfiles_confirm_save_file"),
-              JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
-
-      if (confirm == JvOptionPane.YES_OPTION)
+      StringBuilder messageSB = new StringBuilder();
+      messageSB.append(MessageManager.getString(
+              "label.backupfiles_confirm_save_file_backupfiles_roll_wrong"));
+      // "Something possibly went wrong with the backups of this file."
+      if (rename)
       {
-        roll = true;
+        if (messageSB.length() > 0)
+        {
+          messageSB.append("\n");
+        }
+        messageSB.append(MessageManager.getString(
+                "label.backupfiles_confirm_save_new_saved_file_ok"));
+        // "The new saved file seems okay."
+      }
+      else
+      {
+        if (messageSB.length() > 0)
+        {
+          messageSB.append("\n");
+        }
+        messageSB.append(MessageManager.getString(
+                "label.backupfiles_confirm_save_new_saved_file_not_ok"));
+        // "The new saved file might not be okay."
       }
-    }
 
-    boolean rename = false;
-    if (roll)
+      int confirmButton = Platform.isHeadless() ? JvOptionPane.OK_OPTION
+              : JvOptionPane.showConfirmDialog(Desktop.desktop,
+                      messageSB.toString(),
+                      MessageManager.getString(
+                              "label.backupfiles_confirm_save_file"),
+                      // "Confirm save file"
+                      JvOptionPane.OK_OPTION, JvOptionPane.WARNING_MESSAGE);
+      okay = confirmButton == JvOptionPane.OK_OPTION;
+    }
+    if (okay)
     {
-      rename = this.renameTempFile();
+      tidyUpFiles();
     }
 
     return rename;
   }
 
   public static TreeMap<Integer, File> getBackupFilesAsTreeMap(
-          String fileName,
-          String suffix, int digits)
+          String fileName, String suffix, int digits)
   {
     File[] backupFiles = null;
 
     File file = new File(fileName);
 
-    String dir = "";
     File dirFile;
     try
     {
       dirFile = file.getParentFile();
-      dir = dirFile.getCanonicalPath();
     } catch (Exception e)
     {
-      System.out.println(
+      Cache.log.error(
               "Could not get canonical path for file '" + file + "'");
       return new TreeMap<>();
     }
 
     String filename = file.getName();
     String basename = filename;
-    String extension = "";
-    int dotcharpos = filename.lastIndexOf('.');
-    // don't split of filenames with the last '.' at the very beginning or
-    // very end of the filename
-    if ((dotcharpos > 0) && (dotcharpos < filename.length() - 1))
-    {
-      basename = filename.substring(0, dotcharpos);
-      extension = filename.substring(dotcharpos); // NOTE this includes the '.'
-    }
-    
+
     // find existing backup files
-    BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix, digits, extension);
+    BackupFilenameFilter bff = new BackupFilenameFilter(basename, suffix,
+            digits);
     backupFiles = dirFile.listFiles(bff); // is clone needed?
-    
+
     // sort the backup files (based on integer found in the suffix) using a
     // precomputed Hashmap for speed
     Map<Integer, File> bfHashMap = new HashMap<>();
@@ -510,7 +841,7 @@ public class BackupFiles
     {
       File f = backupFiles[i];
       BackupFilenameParts bfp = new BackupFilenameParts(f, basename, suffix,
-              digits, extension);
+              digits);
       bfHashMap.put(bfp.indexNum(), f);
     }
     TreeMap<Integer, File> bfTreeMap = new TreeMap<>();
@@ -519,15 +850,61 @@ public class BackupFiles
     return bfTreeMap;
   }
 
-  public static File[] getBackupFiles(String fileName, String suffix,
-          int digits)
+  /*
+  private boolean addDeleteFile(File fileToBeDeleted, File originalFile,
+          boolean delete, boolean newer)
   {
-    TreeMap<Integer, File> bfTreeMap = getBackupFilesAsTreeMap(fileName,
-            suffix, digits);
-    File[] backupFiles = new File[bfTreeMap.size()];
-    bfTreeMap.values().toArray(backupFiles);
-    return backupFiles;
+    return addDeleteFile(fileToBeDeleted, originalFile, null, delete, newer);
+  }
+  */
+  private boolean addDeleteFile(File fileToBeDeleted)
+  {
+    boolean ret = false;
+    int pos = deleteFiles.indexOf(fileToBeDeleted);
+    if (pos > -1)
+    {
+      Cache.log.debug("BACKUPFILES not adding file "
+              + fileToBeDeleted.getAbsolutePath()
+              + " to the delete list (already at index" + pos + ")");
+      return true;
+    }
+    else
+    {
+      Cache.log.debug("BACKUPFILES adding file "
+              + fileToBeDeleted.getAbsolutePath() + " to the delete list");
+      deleteFiles.add(fileToBeDeleted);
+    }
+    return ret;
   }
 
+  public static boolean moveFileToFile(File oldFile, File newFile)
+  {
+    boolean ret = false;
+    Path oldPath = Paths.get(oldFile.getAbsolutePath());
+    Path newPath = Paths.get(newFile.getAbsolutePath());
+    try
+    {
+      // delete destination file - not usually necessary but Just In Case...
+      Cache.log.debug("BACKUPFILES deleting " + newFile.getAbsolutePath());
+      newFile.delete();
+      Cache.log.debug("BACKUPFILES moving " + oldFile.getAbsolutePath()
+              + " to " + newFile.getAbsolutePath());
+      Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING);
+      ret = true;
+      Cache.log.debug("BACKUPFILES move seemse to have succeeded");
+    } catch (IOException e)
+    {
+      Cache.log.warn("Could not move file '" + oldPath.toString() + "' to '"
+              + newPath.toString() + "'");
+      Cache.log.error(e.getMessage());
+      Cache.log.debug(e.getStackTrace());
+      ret = false;
+    } catch (Exception e)
+    {
+      Cache.log.error(e.getMessage());
+      Cache.log.debug(e.getStackTrace());
+      ret = false;
+    }
+    return ret;
+  }
 }
-