/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */
package com.sun.jini.mahalo.log;

import com.sun.jini.logging.Levels;
import com.sun.jini.mahalo.log.MultiLogManager.LogRemovalManager;
import com.sun.jini.mahalo.TxnManager;

import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import java.io.*;

/**
 * An implementation of a re-usable <code>Log</code>.
 *
 * @author Sun Microsystems, Inc.
 *
 * @see com.sun.jini.mahalo.log.Log
 */
public class SimpleLogFile implements Log {
    /** Unique ID associated with this log */
    private /*final*/ long cookie;
    /** Output stream for writing log objects */
    private /*final*/ ObjectOutputStream out;
    /** 
     * File output stream associated with <code>out</code>. 
     * Used to get a handle to underlying file descriptor object.
     */
    private /*final*/ FileOutputStream outfile;
    /** (Relative) File name of the log file */
    private /*final*/ String name;
    /** 
     * Reference to <code>LogRemovalManager</code>, which is called
     * to remove this log from the managed set of logs.
     */
    private /*final*/ LogRemovalManager logMgr;
    /** 
     * Flag that indicates validity of this log. Set to false
     * by call to <code>invalidate()</code>.
     */
    private boolean valid = true;
    /**
     * Flag to indicate that the log file has been created via
     * the read-only constructor. This flag is set to false via
     * the non-read-only constructor or a call to <code>recover()</code>
     */
    private boolean readonly = false;

    /** Logger for persistence related messages */
    private static final Logger persistenceLogger = 
        Logger.getLogger(TxnManager.MAHALO + ".persistence");

    /** Logger for operations related messages */
    private static final Logger operationsLogger = 
        Logger.getLogger(TxnManager.MAHALO + ".operations");

    /** Logger for initialization related messages */
    private static final Logger initLogger = 
        Logger.getLogger(TxnManager.MAHALO + ".init");

    /**
     * This class extends <tt>ObjectInputStream</tt> and overrides the 
     * <code>readStreamHeader</code> method to a no-op operation. This class
     * is intended to work in conjunction with 
     * <code>HeaderlessObjectOutputStream</code>.
     */ 
    private static class HeaderlessObjectInputStream extends ObjectInputStream {
		
	/**
	 * Simple constructor that passes its argument to the superclass
         *
         * @exception IOException if an I/O error occurs
	 */
	public HeaderlessObjectInputStream(InputStream in) throws IOException {
	    super(in);
	}
	
	/**
	 * Overrides <tt>ObjectInputStream</tt>'s method with no-op
	 * functionality. 
         * @see HeaderlessObjectOutputStream#writeStreamHeader
         * @exception IOException if an I/O error occurs
	 */
	protected void readStreamHeader() throws IOException {
	    // Do nothing
	}
    }
    
    /**
     * This class extends <tt>ObjectOutputStream</tt> and overrides the
     * <code>writeStreamHeader</code> method to a no-op operation. This 
     * class is intended to be used in conjunction with 
     * <code>HeaderlessObjectInputStream</code>.
     */
    private static class HeaderlessObjectOutputStream extends ObjectOutputStream {
	
	/**
	 * Simple constructor that passes its argument to the superclass
         *
         * @exception IOException if an I/O error occurs
	 */
	public HeaderlessObjectOutputStream(OutputStream out) throws IOException {
	    super(out);
	}
	
	/**
	 * Overrides <tt>ObjectOutputStream</tt>'s method with no-op
	 * functionality.  This prevents header information from being
	 * sent to the stream, which makes appending to existing log files
	 * easier. Otherwise, appending header info to an existing log file
	 * would cause a corresponding <code>ObjectInputStream</code> to
	 * throw a <code>StreamCorruptedException</code> when it encountered
	 * the header information instead of the class/object type code 
	 * information it was expecting.
	 *
         * @exception IOException if an I/O error occurs
	 */
	protected void writeStreamHeader() throws IOException {
	    // Do nothing
	}
    }

    /**
     * Creates a read-only <code>SimpleLogFile</code>
     *
     * To be used for read-only access to a named <code>Log</code>.  This is
     * desired when recovering information from a <code>Log</code>.
     *
     * @param name names the file in which information is stored.
     *
     * @param logMgr    <code>LogRemovalManager</code> managing this log.
     *                  This object is called back to remove this log 
     *                  from the manager's managed set of log files. 
     *
     * @see com.sun.jini.mahalo.log.Log
     * @see com.sun.jini.mahalo.log.LogManager
     * @see com.sun.jini.mahalo.log.MultiLogManager
     * @see com.sun.jini.mahalo.log.MultiLogManager.LogRemovalManager
     */
    public SimpleLogFile(String name, LogRemovalManager logMgr) {
	init(name, 0, logMgr);
	readonly = true;
    }

    /**
     * Creates a <code>SimpleLogFile</code>.
     *
     * @param name names the file in which information is stored.
     *
     * @param cookie identifier representing information being stored.
     *
     * @param logMgr    <code>LogRemovalManager</code> managing this log.
     *                  This object is called back to remove this log 
     *                  from the manager's responsibility. 
     *
     * @see com.sun.jini.mahalo.log.Log
     * @see com.sun.jini.mahalo.log.LogManager
     * @see com.sun.jini.mahalo.log.MultiLogManager
     * @see com.sun.jini.mahalo.log.MultiLogManager.LogRemovalManager
     */
    public SimpleLogFile(String name, long cookie, LogRemovalManager logMgr) {
	init(name, cookie, logMgr);
    }

    /*
     * Utility method that contains code common to all constructors 
     */
    private void init(String name, long cookie, LogRemovalManager logMgr) {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(SimpleLogFile.class.getName(), 
	        "init", new Object[] {name, new Long(cookie), logMgr});
	}
        if (name == null)
            throw new IllegalArgumentException("SimpleLogFile: null name");
 
	if (logMgr == null)
            throw new IllegalArgumentException(
		"SimpleLogFile: null log manager");

        this.name = name;
        this.cookie = cookie;
	this.logMgr = logMgr;
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(SimpleLogFile.class.getName(), 
	        "init");
	}
    }


    /**
     * Returns the identifier associated with information in
     * this <code>Log</code>.
     *
     * @see com.sun.jini.mahalo.log.Log
     */
    public long cookie() {
	return cookie;
    }

    /**
     * Add a <code>LogRecord</code> to the <code>Log</code>.
     *
     * @param rec the record to be logged.
     *
     * @see com.sun.jini.mahalo.log.LogRecord
     */
    public synchronized void write(LogRecord rec) throws LogException {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(SimpleLogFile.class.getName(), 
	        "write", rec);
	}
	try {
	    if (!valid)
		throw new InvalidatedLogException("Cannot write to to " +
						"invalidated log");
	    if (readonly)
		throw new LogException("Unable to write to read only log");

	    if (out == null) {
	        boolean append = true;
		File log = new File(name);
                outfile = new FileOutputStream(name, append);
                out = 
		    new HeaderlessObjectOutputStream(
		        new BufferedOutputStream(outfile));
		if (log.length() == 0) {
		    out.writeLong(cookie);
                }
		out.reset();
	    }

	    out.writeObject(rec);
	    out.flush();
	    outfile.getFD().sync();

	    if (persistenceLogger.isLoggable(Level.FINEST)) {
                persistenceLogger.log(Level.FINEST,
		    "Wrote: {0}", rec);
	    }	
	} catch (InvalidClassException ice) {
	    if (persistenceLogger.isLoggable(Level.WARNING)) {
                persistenceLogger.log(Level.WARNING,
		"Problem persisting LogRecord", ice);
	    }
// TODO - assertion error? ... should not happen
	} catch (NotSerializableException nse) {
	    if (persistenceLogger.isLoggable(Level.WARNING)) {
                persistenceLogger.log(Level.WARNING,
		"Problem persisting LogRecord", nse);
	    }
// TODO - assertion error? ... should not happen
	} catch (IOException ioe) {
	    if (persistenceLogger.isLoggable(Level.WARNING)) {
                persistenceLogger.log(Level.WARNING,
		"Problem persisting LogRecord", ioe);
	    }
// TODO - throw LogException?
        } catch (SecurityException se) {
	    if (persistenceLogger.isLoggable(Level.WARNING)) {
                persistenceLogger.log(Level.WARNING,
		"Problem persisting LogRecord", se);
	    }
// TODO - assertion error? ... should not happen
        }
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(SimpleLogFile.class.getName(), 
	        "write", rec);
	}
    }

    /**
     * Invalidate the log.
     */
    public synchronized void invalidate() throws LogException {
        // No short circuit check because we allow repeat calls
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "invalidate");
	}
	
	if (persistenceLogger.isLoggable(Level.FINEST)) {
            persistenceLogger.log(Level.FINEST,
                "Invalidating log for cookie: {0}", new Long(cookie));
	}

	if (valid) {
	    // Set validity flag to false
	    valid = false;

	    // Ask log manager to remove us from its managed set
	    logMgr.release(cookie);
	}
		
	try { 
	    if (out != null)
	        out.close(); // calls outfile.close()
	} catch (IOException ioe) { // just log it
	    if (persistenceLogger.isLoggable(Levels.HANDLED)) {
                persistenceLogger.log(Levels.HANDLED,
		"Problem closing log file", ioe);
	    }
	}
	
	try {
	    File fl = new File(name);
	    if(!fl.delete()) {
	        if (persistenceLogger.isLoggable(Levels.HANDLED)) {
                    persistenceLogger.log(Levels.HANDLED,
		        "Could not delete log file");
		}
	    }
	} catch (SecurityException se) { // notify caller
	    if (persistenceLogger.isLoggable(Level.FINEST)) {
                persistenceLogger.log(Level.FINEST,
                    "SecurityException on log deletion", se);
	    }
	    throw new LogException("SimpleLogFile: invalidate: "
	        + "cannot delete log file.");
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "invalidate");
	}
    }

    /**
     * Recover information from the log.
     *
     * @param client who to inform with information from the log.
     *
     * @see com.sun.jini.mahalo.log.LogRecovery
     */
    public synchronized void recover(LogRecovery client) throws LogException {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(MultiLogManager.class.getName(), 
	        "recover", client);
	}
	if (!valid)
	    throw new InvalidatedLogException("Cannot recover from " +
						"invalidated log");
	if (client == null)
	    throw new IllegalArgumentException("Cannot have a <null> " 
	        + "client argument.");
	
	ObjectInputStream in = null;
      	ArrayList recList = new ArrayList();
	try {
	    if (persistenceLogger.isLoggable(Level.FINEST)) {
                persistenceLogger.log(Level.FINEST,
                    "Recovering from: {0}", name);
	    }
	    in = new HeaderlessObjectInputStream(
	             new BufferedInputStream(
		         new FileInputStream(name)));

	    this.cookie = in.readLong();
	    LogRecord rec = null;
	    boolean done = false;
	    boolean update = true;
	    try {
	        while (!done) {
		    // try to catch cast exceptions here
	            rec = (LogRecord)in.readObject();
		    if (rec != null) {
		        recList.add(rec);
		    } else { //TBD - ignore?
		        update = false;
			done = true; // bad log ... skip it
		        if (persistenceLogger.isLoggable(Levels.HANDLED)) {
                            persistenceLogger.log(Levels.HANDLED,
		                "Log for cookie {0} contained a null "
				+ "record object", new Long(cookie));
		        }
		    }
	        }
	    } catch (ClassNotFoundException cnfe) {
                update = false;
	        if (persistenceLogger.isLoggable(Level.WARNING)) {
                    persistenceLogger.log(Level.WARNING,
		    "Problem recovering log file", cnfe);
	        }
// TODO - assertion error? ... should not happen
            } catch (ClassCastException cce) {
                update = false;
	        if (persistenceLogger.isLoggable(Level.WARNING)) {
                    persistenceLogger.log(Level.WARNING,
		    "Problem recovering log file", cce);
	        }
// TODO - assertion error? ... should not happen
	    } catch (EOFException eofe) {
		// OK. Assume we've hit the end of the log file
            } catch (IOException ioe) {
                update = false;
	        if (persistenceLogger.isLoggable(Level.WARNING)) {
                    persistenceLogger.log(Level.WARNING,
		    "Problem recovering log file", ioe);
	        }
	    }
	    
	    if (update) {
	        for (int i=0; i<recList.size(); i++) {
	            client.recover(cookie, (LogRecord)recList.get(i));
	        }
	    } else {
	        if (persistenceLogger.isLoggable(Level.WARNING)) {
                    persistenceLogger.log(Level.WARNING,
                        "Skipping log recovery for", name);
	        }
            }
	} catch (IOException ioe) {
	    // bogus log file -- skip it
	    if (persistenceLogger.isLoggable(Level.WARNING)) {
                persistenceLogger.log(Level.WARNING,
		"Problem recovering log file", ioe);
	    }
        } finally {
	    try {
	        if (in != null) in.close(); // calls fin.close()
	    } catch (IOException ioe) {
	        if (persistenceLogger.isLoggable(Levels.HANDLED)) {
                    persistenceLogger.log(Levels.HANDLED,
		    "Problem closing recovered log file", ioe);
	        }
	    }
	    readonly = false;
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(MultiLogManager.class.getName(), 
	        "recover");
	}
    }
    // TBD - add a toString() method for debugging purposes
}
