/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2000-2009 Sun Microsystems, Inc. All rights reserved. 
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License ("CDDL") (collectively, the "License").  You may
 * not use this file except in compliance with the License.  You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or mq/legal/LICENSE.txt.  See the License for the specific language
 * governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at mq/legal/LICENSE.txt.  Sun designates
 * this particular file as subject to the "Classpath" exception as provided by
 * Sun in the GPL Version 2 section of the License file that accompanied this
 * code.  If applicable, add the following below the License Header, with the
 * fields enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or  to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright holder. 
 */

/*
 * @(#)RollingFileOutputStream.java	1.10 06/27/07
 */ 

package com.sun.messaging.jmq.io;

import java.io.OutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.util.Date;

import com.sun.messaging.jmq.resources.SharedResources;

/**
 * An output stream for writing bytes to a file in a "rolling" 
 * fasion.  The filename and rollover point are configurable.
 * Rolling files work by checking if the rollover point
 * (size of file in bytes or time since creation) has been exceeded before
 * every write.  If the rollover point is exceeded, the file is closed,
 * renamed to the filename followed by a "_1" (with all previous files
 * "shifted down" by one), and a new file is created.  If the "shift down"
 * causes the maximum number of saved files to be exceeded, the oldest
 * file is deleted.
 *
 * Part of this code was taken from Java Web Server
 */
public class RollingFileOutputStream extends OutputStream {

    /**
     * Size of file. We set this when we initially open the file
     * and increment it on every write. This way we don't have to keep
     * going to the File object to get the file length.
     */ 
    long file_size = 0L;

    File outfile;

    /** 
     * Allowed file size before rollover. 0 to never rollover based on size.
     */
    long rollover_bytes = 0;

    /** 
     * Allowed age of file before rollover. 0 to never rollover based on time.
     */
    long rollover_ms = 0;

    /**
     * Time file was created
     */
    long file_creation_time = 0;

    /**
     * Number of backup files
     */
    private int num_files = 9;

    /**
     * The outputstream logged to.
     */
    private FileOutputStream out;

    private static SharedResources rb = SharedResources.getResources();
    private RandomAccessFile raf = null;


    /**
     * Creates a new output stream with rollover criteria
     *
     * @param outfile the file to write output to.
     *
     * @param rollover_bytes the size of the file before rollover occurs. 
     *                       0 means never rollover on size.
     * @param rollover_secs   the age of the file before rollover occurs in
     *			      secs. 
     *                       0 means never rollover on age.
     */
    public RollingFileOutputStream(
	File outfile, 
	long rollover_bytes,
	long rollover_secs
    ) throws IOException {
        this.outfile = outfile;

	setRolloverBytes(rollover_bytes);
	setRolloverSecs(rollover_secs);

	if (outfile.exists()) {
	    if (!outfile.canWrite()) {
		throw new FileNotFoundException(
		    rb.getString(rb.X_FILE_WRITE, outfile));
	    } else if (outfile.isDirectory()) {
		throw new FileNotFoundException(
		    rb.getString(rb.X_DIR_NOT_FILE, outfile));
	    }
	}

	file_size = outfile.length();

        // Create file, appending if exists
	raf = null;
	try {
	    raf = new RandomAccessFile(outfile, "rw");
	} catch (IOException exc) {
	    throw exc;
	}

	if (file_size == 0) {
            out = new FileOutputStream(raf.getFD());
	    file_creation_time = System.currentTimeMillis();
	    try {
	        writeCreationTime(out, file_creation_time);
            } catch (IOException e) {
		throw new IOException(
		    rb.getString(rb.X_FILE_WRITE_TIMESTAMP, outfile));
            }
        } else {
	    try {
	        file_creation_time = readCreationTime(raf);
            } catch (IOException e) {
		throw new IOException(
		    rb.getString(rb.X_FILE_READ_TIMESTAMP, outfile));
            }
            raf.seek(raf.length());
            out = new FileOutputStream(raf.getFD());
	}

	// check if file is already too big, if so, move it
	if (doRollover()) {
            rolloverFile();
        }
    }

    /**
     * Write the creation time to a file. We encode the creation
     * time at the start of the file (since there is no way to
     * retrieve the creation time from a file in java). The first
     * line of a log file looks like:
     *
     * # 968803396174 Do not modify this line.
     *
     * Where 968803396174 is the creation time as specified by
     * System.currentTimeMillis()
     */
    private void writeCreationTime(OutputStream os, long time)
	throws IOException {

	String s = "# " + time + " Do not modify this line" + rb.NL;
	os.write(s.getBytes("us-ascii"));
    }

    /**
     * Read the creation time from a file.
     */
    private long readCreationTime(RandomAccessFile raf)
	throws IOException, EOFException {


	raf.seek(0);
	if (raf.skipBytes(2) != 2) {
	    throw new IOException("Could not skip bytes");
	};

	byte[] timeBytes = new byte[128];
	long time = 0;
	byte digit;
	int n;

	// Read digits until we hit the first non digit
	for (n = 0; n < 128; n++) { 
	    digit = raf.readByte();
	    if (digit < 48 || digit > 57) {
		break;
            } else {
                timeBytes[n] = digit;
            }
        }

	int base = 0;
	for (n = n - 1; n >= 0; n--) {
	    time += ((timeBytes[n] - 48) * Math.pow(10, base));
	    base++;
	}

        raf.seek(raf.length());

	return time;
    }

    /**
     * Creates a new output stream with rollover criteria
     *
     * @param outfile the file to write output to.
     *
     * @param rollover_bytes the size of the file before rollover occurs. 
     *                       0 means never roll overon size.
     */
    public RollingFileOutputStream(
	File outfile, 
	long rollover_bytes
    ) throws IOException {
	this(outfile, 1024, 0);
    }


    /**
     * Creates a new output stream with defaults
     * @param outfile the output file
     */
    public RollingFileOutputStream(File outfile) throws IOException {
        this(outfile, 1024);
    }

    public void setNumFiles(int n) {
	num_files = n;
    }

    public int getNumFiles() {
	return num_files;
    }

    /**
     * Writes a byte of data to the output stream. If the rollover point
     * is exceeded, the existing file will be rolled over to a new file.
     * @param b the byte to be written
     * @exception IOException if an I/O error has occurred
     */
    public synchronized void write(int b) throws IOException {
        file_size++;
        out.write(b);
    }


    /**
     *  Writes an array of bytes to the output stream.  If the
     *  rollover point is exceeded, the existing file will be rolled
     *  over to a new file.
     *  @param b the bytes to be written.
     *  @exception IOException If an I/O error has occurred.
     */
    public void write(byte[] b) throws IOException {
        write(b,0,b.length);
    }

    /**
     * Writes an array of bytes to the output stream. This method will block
     * until all the bytes have been written.  If the rollover point is
     * exceeded, the existing file will be rolled over to a new file.
     * @param b the bytes to be written
     * @param off the start offset of the bytes
     * @param len the number of bytes to write
     * @exception IOException if an I/O error has occurred
     */
    public synchronized void write(byte[] b, int off, int len) throws IOException {
        if (doRollover()) {
            rolloverFile();
        }

        file_size += len;
        out.write(b, off, len);
    }

    /**
     * Closes the output stream.
     * @exception IOException if an I/O error has occurred
     */
    public void close() throws IOException {
        if (raf != null) raf.close();
        out.close();
    }

    /**
     * Flushes the output stream.
     * @throws IOException if an I/O error has occurred
     */
    public void flush() throws IOException {
        out.flush();
    }

    /*
     * Move files around.
     */
    void rolloverFile() throws IOException {
        
        File testfile;
        File newfile;
	String  s;

        if (out != null) {
            out.flush();
            out.close();
        }
	s = outfile.getAbsolutePath();

        testfile = new File(generateFilename(s, num_files));
        if (testfile.exists()) testfile.delete();

        for (int i = num_files - 1; i > 0; i--) {
	    testfile = new File(generateFilename(s, i));

            if (testfile.exists()) {
	        newfile = new File(generateFilename(s, i + 1));

                testfile.renameTo(newfile);
            }
        }
	newfile = new File(generateFilename(s, 1));
        outfile.renameTo(newfile);

        out = new FileOutputStream(outfile);

	file_creation_time = System.currentTimeMillis();

	try {
	    writeCreationTime(out, file_creation_time);
        } catch (Exception e) {
	    System.out.println("Got exception" + e);
        }

        file_size = outfile.length();
    }

    /*
     * Insert rollover number into file name. It is placed at the
     * end of the file name unless there is a ".suffix" in which 
     * case it is placed before the ".suffix". I.e.:
     *
     *  log     -> log_1
     *  log.txt -> log_1.txt
     */
    public static String generateFilename(String name, int i) {

	int n = name.lastIndexOf('.');

	if (n == -1) {
            return name + "_" + i;
	} else {
            return name.substring(0, n) + "_" + i + name.substring(n);
	}
    }

    /**
     * Check if rollover condition has been met
     */
    boolean doRollover() {
        return
	    (rollover_bytes != 0 && file_size >= rollover_bytes) ||
	    (rollover_ms    != 0 && (System.currentTimeMillis() -
				    file_creation_time > rollover_ms));
    }

    public void setRolloverSecs(long seconds) {
        this.rollover_ms = seconds * 1000;
    }

    public long getRolloverSecs() {
	return this.rollover_ms / 1000;
    }

    public void setRolloverBytes(long bytes) {
	this.rollover_bytes = bytes;
    }

    public long getRolloverBytes() {
	return this.rollover_bytes;
    }
}
