/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */
package org.apache.qpid.server.store.berkeleydb;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.util.DbBackup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.qpid.server.store.StoreException;
import org.apache.qpid.server.util.CommandLineParser;
import org.apache.qpid.server.util.FileUtils;

/**
 * BDBBackup is a utility for taking hot backups of the current state of a BDB transaction log database.
 * <p>
 * This utility makes the following assumptions/performs the following actions:
 * <p>
 * <ul> <li>The from and to directory locations will already exist. This scripts does not create them. <li>If this
 * script fails to complete in one minute it will terminate. <li>This script always exits with code 1 on error, code 0
 * on success (standard unix convention). <li>This script will log out at info level, when it starts and ends and a list
 * of all files backed up. <li>This script logs all errors at error level. <li>This script does not perform regular
 * backups, wrap its calling script in a cron job or similar to do this. </ul>
 * <p>
 * This utility is build around the BDB provided backup helper utility class, DbBackup. This utility class provides
 * an ability to force BDB to stop writing to the current log file set, whilst the backup is taken, to ensure that a
 * consistent snapshot is acquired. Preventing BDB from writing to the current log file set, does not stop BDB from
 * continuing to run concurrently while the backup is running, it simply moves onto a new set of log files; this
 * provides a 'hot' backup facility.
 * <p>
 * DbBackup can also help with incremental backups, by providing the number of the last log file backed up.
 * Subsequent backups can be taken, from later log files only. In a messaging application, messages are not expected to
 * be long-lived in most cases, so the log files will usually have been completely turned over between backups. This
 * utility does not support incremental backups for this reason.
 * <p>
 * If the database is locked by BDB, as is required when using transactions, and therefore will always be the case
 * in Qpid, this utility cannot make use of the DbBackup utility in a seperate process. DbBackup, needs to ensure that
 * the BDB envinronment used to take the backup has exclusive write access to the log files. This utility can take a
 * backup as a standalone utility against log files, when a broker is not running, using the {@link #takeBackup(String,
 *String,com.sleepycat.je.Environment)} method.
 * <p>
 * A separate backup machanism is provided by the {@link #takeBackupNoLock(String,String)} method which can take a
 * hot backup against a running broker. This works by finding out the set of files to copy, and then opening them all to
 * read, and repeating this process until a consistent set of open files is obtained. This is done to avoid the
 * situation where the BDB cleanup thread deletes a file, between the directory listing and opening of the file to copy.
 * All consistently opened files are copied. This is the default mechanism the the {@link #main} method of this utility
 * uses.
 */
public class BDBBackup
{
    /** Used for debugging. */
    private static final Logger log = LoggerFactory.getLogger(BDBBackup.class);

    /** Used for communicating with the user. */
    private static final Logger console = LoggerFactory.getLogger("Console");

    /** Defines the suffix used to identify BDB log files. */
    private static final String LOG_FILE_SUFFIX = ".jdb";

    /** Defines the command line format for this utility. */
    public static final String[][] COMMAND_LINE_SPEC =
        new String[][]
        {
            { "fromdir", "The path to the directory to back the bdb log file from.", "dir", "true" },
            { "todir", "The path to the directory to save the backed up bdb log files to.", "dir", "true" }
        };

    /** Defines the timeout to terminate the backup operation on if it fails to complete. One minute. */
    public static final long TIMEOUT = 60000;

    /**
     * Runs a backup of the BDB log files in a specified directory, copying the backed up files to another specified
     * directory.
     * <p>
     * The following arguments must be specified:
     * <table>
     * <caption>Command Line</caption> <tr><th> Option <th> Comment <tr><td> -fromdir <td> The path to the
     * directory to back the bdb log file from. <tr><td> -todir   <td> The path to the directory to save the backed up
     * bdb log files to. </table>
     *
     * @param args The command line arguments.
     */
    public static void main(String[] args)
    {
        // Process the command line using standard handling (errors and usage followed by System.exit when it is wrong).
        Properties options =
            CommandLineParser.processCommandLine(args, new CommandLineParser(COMMAND_LINE_SPEC), System.getProperties());

        // Extract the from and to directory locations and perform a backup between them.
        try
        {
            String fromDir = options.getProperty("fromdir");
            String toDir = options.getProperty("todir");

            log.info("BDBBackup Utility: Starting Hot Backup.");

            BDBBackup bdbBackup = new BDBBackup();
            String[] backedUpFiles = bdbBackup.takeBackupNoLock(fromDir, toDir);

            if (log.isInfoEnabled())
            {
                log.info("BDBBackup Utility: Hot Backup Completed.");
                log.info(backedUpFiles.length + " file(s) backed-up:");
                for(String backedUpFile : backedUpFiles)
                {
                    log.info(backedUpFile);
                }
            }
        }
        catch (Exception e)
        {
            console.info("Backup script encountered an error and has failed: " + e.getMessage());
            log.error("Backup script got exception: " + e.getMessage(), e);
            System.exit(1);
        }
    }

    /**
     * Creates a backup of the BDB log files in the source directory, copying them to the destination directory.
     *
     * @param fromdir     The source directory path.
     * @param todir       The destination directory path.
     * @param environment An open BDB environment to perform the back up.
     *
     * @throws DatabaseException Any underlying execeptions from BDB are allowed to fall through.
     */
    public void takeBackup(String fromdir, String todir, Environment environment) throws DatabaseException
    {
        DbBackup backupHelper = null;

        try
        {
            backupHelper = new DbBackup(environment);

            // Prevent BDB from writing to its log files while the backup it taken.
            backupHelper.startBackup();

            // Back up the BDB log files to the destination directory.
            String[] filesForBackup = backupHelper.getLogFilesInBackupSet();

            for (int i = 0; i < filesForBackup.length; i++)
            {
                File sourceFile = new File(fromdir + File.separator + filesForBackup[i]);
                File destFile = new File(todir + File.separator + filesForBackup[i]);
                FileUtils.copy(sourceFile, destFile);
            }
        }
        finally
        {
            // Remember to exit backup mode, or all log files won't be cleaned and disk usage will bloat.
            if (backupHelper != null)
            {
                backupHelper.endBackup();
            }
        }
    }

    /**
     * Takes a hot backup when another process has locked the BDB database.
     *
     * @param fromdir The source directory path.
     * @param todir   The destination directory path.
     *
     * @return A list of all of the names of the files succesfully backed up.
     */
    public String[] takeBackupNoLock(String fromdir, String todir)
    {
        if (log.isDebugEnabled())
        {
            log.debug("public void takeBackupNoLock(String fromdir = " + fromdir + ", String todir = " + todir
                + "): called");
        }

        File fromDirFile = new File(fromdir);

        if (!fromDirFile.isDirectory())
        {
            throw new IllegalArgumentException("The specified fromdir(" + fromdir
                + ") must be the directory containing your bdbstore.");
        }

        File toDirFile = new File(todir);

        if (!toDirFile.exists())
        {
            // Create directory if it doesn't exist
            toDirFile.mkdirs();

            if (log.isDebugEnabled())
            {
                log.debug("Created backup directory:" + toDirFile);
            }
        }

        if (!toDirFile.isDirectory())
        {
            throw new IllegalArgumentException("The specified todir(" + todir + ") must be a directory.");
        }

        // Repeat until manage to open consistent set of files for reading.
        boolean consistentSet = false;
        FileInputStream[] fileInputStreams = new FileInputStream[0];
        File[] fileSet = new File[0];
        long start = System.currentTimeMillis();

        while (!consistentSet)
        {
            // List all .jdb files in the directory.
            fileSet = fromDirFile.listFiles(new FilenameFilter()
                    {
                        @Override
                        public boolean accept(File dir, String name)
                        {
                            return name.endsWith(LOG_FILE_SUFFIX);
                        }
                    });

            if (fileSet == null || fileSet.length == 0)
            {
                throw new StoreException("There are no BDB log files to backup in the '" + fromdir + "' directory.");
            }

            // The files must be copied in alphabetical order (numerical in effect)
            Arrays.sort(fileSet);

            // Open them all for reading.
            fileInputStreams = new FileInputStream[fileSet.length];


            for (int i = 0; i < fileSet.length; i++)
            {
                try
                {
                    fileInputStreams[i] = new FileInputStream(fileSet[i]);
                }
                catch (FileNotFoundException e)
                {
                    // Close any files opened for reading so far.
                    for (int j = 0; j < i; j++)
                    {
                        if (fileInputStreams[j] != null)
                        {
                            try
                            {
                                fileInputStreams[j].close();
                            }
                            catch (IOException ioEx)
                            {
                                // Rethrow this as a runtime exception, as something strange has happened.
                                throw new StoreException(ioEx);
                            }
                        }
                    }

                    // Could not open a consistent file set so try again.
                    break;
                }

                // A consistent set has been opened if all files were successfully opened for reading.
                if (i == (fileSet.length - 1))
                {
                    consistentSet = true;
                }
            }

            // Check that the script has not timed out, and raise an error if it has.
            long now = System.currentTimeMillis();
            if ((now - start) > TIMEOUT)
            {
                throw new StoreException("Hot backup script failed to complete in " + (TIMEOUT / 1000) + " seconds.");
            }
        }

        // Copy the consistent set of open files.
        List<String> backedUpFileNames = new LinkedList<String>();

        for (int j = 0; j < fileSet.length; j++)
        {
            File destFile = new File(todir + File.separator + fileSet[j].getName());
            try
            {
                Files.copy(fileInputStreams[j], destFile.toPath());
            }
            catch (IOException ioe)
            {
                throw new StoreException(ioe.getMessage() + " fromDir:" + fromdir + " toDir:" + toDirFile, ioe);
            }

            backedUpFileNames.add(destFile.getName());

            // Close all of the files.
            try
            {
                fileInputStreams[j].close();
            }
            catch (IOException e)
            {
                // Rethrow this as a runtime exception, as something strange has happened.
                throw new StoreException(e);
            }
        }

        return backedUpFileNames.toArray(new String[backedUpFileNames.size()]);
    }

    /*
     * Creates an environment for the bdb log files in the specified directory. This envrinonment can only be used
     * to backup these files, if they are not locked by another database instance.
     *
     * @param fromdir The path to the directory to create the environment for.
     *
     * @throws DatabaseException Any underlying exceptions from BDB are allowed to fall through.
     */
    private Environment createSourceDirEnvironment(String fromdir) throws DatabaseException
    {
        // Initialize the BDB backup utility on the source directory.
        return new Environment(new File(fromdir), new EnvironmentConfig());
    }
}