/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.util;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Vector;

/**
 * An abstract base class to provide a way of opening files subject
 * to a policy of what to do if a file of the same name already exists.
 */
public abstract class BackupPolicy 
{
    /**
     * Backup a file by renaming it to have have a new name of 
     * <i>old-name</i>~<i>n</i> where n is chosen to be higher than
     * any other for which a candidate new filename exists.
     * Thus, successive backups of a file named "x" will create
     * files named "x~1~", "x~2~", "x~3~", etc.
     * The number of backup files to keep is determined by 
     * getNumBackupsToKeep. In addition, backups can be suppressed
     * by isBackupRequired if desired.
     * @param file the file to be backed up
     * @throws IOException if there is a problem renaming the file
     * @throws SecurityException if the file could not be backed up because
     * permission was not given by the security manager
     * @see #getNumBackupsToKeep
     * @see #isBackupRequired
     */
    public void backup(File file) throws IOException, SecurityException {
	if (!isBackupRequired(file))
	    return;

	if (!file.exists())
	    return;

	if (file.isDirectory())
	    throw new IOException("Cannot backup a directory");

	if (!file.canWrite())
	    throw new IOException("File is write-protected");

	File canon = new File(file.getCanonicalPath());
	String p = canon.getParent();
	if (p == null)
	    throw new IOException("Cannot determine parent directory of " + file);
       
	File dir = new File(p);
	String[] dirFiles = dir.list();

	String prefix = file.getName() + "~";
	String suffix = "~";
	int maxBackupIndex = 0;
	Vector backups = new Vector(); 
    nextFile:
	if (dirFiles != null) {
	    for (int i = 0; i < dirFiles.length; i++) {
		String s = dirFiles[i];
		if (s.length() > (prefix.length() + suffix.length()) &&
		    	s.startsWith(prefix) && s.endsWith(suffix)) {
		    String mid = s.substring(prefix.length(), s.length() - suffix.length());
		    // verify filename is numeric between prefix and suffix; if not; skip it
		    for (int m = 0; m < mid.length(); m++)
			if (!Character.isDigit(mid.charAt(m)))
			    break nextFile;
		    // if numeric, get the backup index; ignore NumberFormatException
		    // since is really should not happen, given preceding check
		    int index = Integer.parseInt(mid);
		    if (index > maxBackupIndex)
			maxBackupIndex = index;
		    backups.addElement(new Integer(index));
		}
	    }
	}

	// try renaming file to file~(++maxBackupIndex)~
	File backup = new File(file.getPath() + "~" + (++maxBackupIndex) + "~");

	boolean ok = file.renameTo(backup);
	if (!ok)
	    throw new IOException("failed to backup file: " + file);

	// delete old backups
	int numBackupsToKeep = getNumBackupsToKeep(file);
	for (int i = 0; i < backups.size(); i++) {
	    int index = ((Integer)(backups.elementAt(i))).intValue();
	    if (index <= (maxBackupIndex-numBackupsToKeep)) {
	        File backupToGo = new File(file.getPath() + "~" + index + "~");
		// let SecurityExceptions out, but otherwise ignore failures
		// to delete old backups
		boolean ignore = backupToGo.delete();
	    }
	}
    }

    /**
     * Rename (or move) the source file to the target name.  Backup the target if
     * necessary.  This method will return without action if the source file does not
     * exist.
     *
     * @param source The file to be backed up. 
     * 			It must be a file (not a directory) which is deleteable.
     * @param target The new name for the file. It must be a file (not a directory) 
     * 			which is will be at a writable location.
     * @throws IOException if there is a problem renaming the file.
     * 		This may happen if the source is a
     *         directory, the source file is not writable, or the rename operation
     *         fails.  In all cases, the rename operation was not successful.
     * @throws SecurityException if the backup operation fails because of a security
     *         constraint.
     * @see #backup(File)
     * @since 3.0.1
     */
    public void backupAndRename(File source, File target) throws IOException, SecurityException {
	if (!source.exists())
	    return;

	if (source.isDirectory())
	    throw new IOException("Cannot backup a directory.");

	if (!source.canWrite())
	    throw new IOException("Cannot rename, source file is write-protected " + source.getPath());

	if (isBackupRequired(target)) {
	    backup(target);
	}
	else if (target.exists()) {
	    // remove the file we are about to overwrite
	    // not really needed on Solaris, seems to be needed on Win32
	    target.delete();
	}

	boolean result = source.renameTo(target);
	if (!result)
	    throw new IOException("Rename of " + target.getPath() + " failed."); 
    }

    /**
     * Backup a file and open a new Writer to the file.
     * @param file the file to be backed up, and for which a new Writer will be opened
     * @return a buffered file writer to the specified file
     * @throws IOException if there is a problem backing up the file or creating
     * 	the new writer object
     * @throws SecurityException if the operation could not be completed because
     * of a security constraint
     */
    public Writer backupAndOpenWriter(File file) throws IOException, SecurityException {
	backup(file);
	return new BufferedWriter(new FileWriter(file));
    }

    /**
     * Backup a file and open a new Writer to the file with specified charset.
     * @param file the file to be backed up, and for which a new Writer will be opened
     * @param charsetName Create an OutputStreamWriter that uses the named charset
     * @return a buffered file writer to the specified file
     * @throws IOException if there is a problem backing up the file or creating
     * 	the new writer object
     * @throws SecurityException if the operation could not be completed because
     * of a security constraint
     */
    public Writer backupAndOpenWriter(File file, String charsetName) throws IOException, SecurityException {
	backup(file);
	return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charsetName));
    }
    
    
    /**
     * Backup a file and open a new output stream to the file.
     * @param file the file to be backed up, and for which a new output stream will be opened
     * @return a buffered output stream to the specified file
     * @throws IOException if there is a problem backing up the file or creating
     * 	the new output stream
     * @throws SecurityException if the operation could not be completed because
     * of a security constraint
     */
    public OutputStream backupAndOpenStream(File file) throws IOException, SecurityException {
	backup(file);
	return new BufferedOutputStream(new FileOutputStream(file));
    }

    /**
     * Get the number of backup files to keep for a particular file.
     * When backup is called, the oldest backups are automatically deleted
     * to limit the number of backup files.
     * @param file the file for which to check how many backups are required
     * @return the maximum number of backups to keep for this file
     * @see #backup
     */
    public abstract int getNumBackupsToKeep(File file);

    /**
     * Determine if backups are enabled for this file. If backups are not
     * enabled, backup will return without affecting the file.
     * @param file the file for which to check if backups are enabled
     * @return true if backups are enabled for this type of file, and false otherwise
     */
    public abstract boolean isBackupRequired(File file);

    /** 
     * Get a BackupPolicy object which does no backups for any files.
     * @return a BackupPolicy object which does no backups for any files
     */
    public static BackupPolicy noBackups() {
	return new BackupPolicy() {
	    public int getNumBackupsToKeep(File file) {
		return 0;
	    }
	    public boolean isBackupRequired(File file) {
		return false;
	    }
	};	
    }

    /** 
     * Get a BackupPolicy object which does a set number of backups for all files.
     * @param n The number of backups to kept for each file
     * @return a BackupPolicy object which does a set number of backups for all files
     */
    public static BackupPolicy simpleBackups(final int n) {
	return new BackupPolicy() {
	    public int getNumBackupsToKeep(File file) {
		return n;
	    }
	    public boolean isBackupRequired(File file) {
		return true;
	    }
	};	
    }
}
