/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (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/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.ejb.base.sfsb.store;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import java.util.logging.*;

import com.sun.ejb.spi.monitorable.sfsb.MonitorableSFSBStore;

import com.sun.ejb.spi.stats.MonitorableSFSBStoreManager;

import com.sun.ejb.spi.sfsb.store.SFSBBeanState;
import com.sun.ejb.spi.sfsb.store.SFSBStoreManager;
import com.sun.ejb.spi.sfsb.store.SFSBStoreManagerException;

import com.sun.logging.*;

/**
 * An implementation of SFSBStoreManager that uses file system to
 *  persist SFSB state.
 *
 * @author     Mahesh Kannan
 */
 
public class FileStoreManager
    implements SFSBStoreManager, MonitorableSFSBStoreManager
{
    protected static Logger _logger =
        LogDomains.getLogger(LogDomains.EJB_LOGGER);

    protected String	clusterId = "";
    protected long	containerId;

    protected File	baseDir;
    protected String	storeName;

    protected int	passivationTimeoutInSeconds;

    private int         loadCount; 
    private int         loadSuccessCount; 
    private int         loadErrorCount; 
    private int         storeCount; 
    private int         storeSuccessCount; 
    private int         storeErrorCount; 
    private int         expiredSessionCount; 

    private boolean     shutdown;

    private Level	TRACE_LEVEL = Level.FINE;

    private int		gracePeriodInSeconds;

    /**
     * No arg constructor
     */
    public FileStoreManager() {
    }

    /****************************************************/
    /**** Implementation of SFSBStoreManager methods ****/
    /****************************************************/

    public void checkpointSave(SFSBBeanState beanState)
	throws SFSBStoreManagerException
    {
        saveState(beanState, false);
	if (_logger.isLoggable(TRACE_LEVEL)) {
	    _logger.log(TRACE_LEVEL, storeName + "Checkpoint saved: "
		+ beanState.getId());
	}
    }

    public SFSBBeanState createSFSBBeanState(Object sessionId,
	    long lastAccess, boolean isNew, byte[] state)
    {
	return new SFSBBeanState(clusterId, containerId,
	    sessionId, lastAccess, isNew, state, this);
    }
    
    public SFSBBeanState getState(Object sessionKey) {

        String fileName = sessionKey.toString();
        SFSBBeanState beanState = null;

	if(_logger.isLoggable(TRACE_LEVEL)) {
	    _logger.log(TRACE_LEVEL, storeName + "Attempting to load session: "
			    + sessionKey);
	}

        File file = new File(baseDir, fileName);
        if (file.exists()) {
            int dataSize = (int) file.length();
            byte[] data = new byte[dataSize];
            BufferedInputStream bis = null;
            FileInputStream fis = null;
            try {
                loadCount++;
                fis = new FileInputStream(file);
                bis = new BufferedInputStream(fis);
                int offset = 0;
                for (int toRead = dataSize; toRead > 0; ) {
                    int count = bis.read(data, offset, toRead);
                    offset += count;
                    toRead -= count;
                }	
        
		beanState = new SFSBBeanState("", -1, sessionKey, -1,
		    false, data, this);

                loadSuccessCount++;
		if(_logger.isLoggable(TRACE_LEVEL)) {
		    _logger.log(TRACE_LEVEL, storeName
			    + " Successfully loaded session: " + sessionKey);
		}
            } catch (Exception ex) {
                loadErrorCount++;
                _logger.log(Level.WARNING,
                    "ejb.sfsb_storemgr_loadstate_failed",
                    new Object[] {fileName});
                _logger.log(Level.WARNING,
                    "ejb.sfsb_storemgr_loadstate_exception", ex);
            } finally {
                try {
		    bis.close();
		} catch (Exception ex) {
		    _logger.log(Level.FINEST, storeName + " Error while "
			    + "closing buffered input stream", ex);
		}
                try {
		    fis.close();
		} catch (Exception ex) {
		    _logger.log(Level.FINEST, storeName + " Error while "
			    + "closing file input stream", ex);
		}
            }
        } else {
	    if(_logger.isLoggable(TRACE_LEVEL)) {
		_logger.log(Level.WARNING, storeName + "Could not find passivated "
			+ "file for: " + sessionKey);
	    }
	}
        return beanState;
    }
     
    public void initSessionStore(Map storeEnv) {

        this.storeName = (String) storeEnv.get(
		FileStoreManagerConstants.STORE_MANAGER_NAME);
        try {
	    Long cId = (Long) storeEnv.get(
		    FileStoreManagerConstants.CONTAINER_ID);
	    this.containerId = cId.longValue();
	} catch (Exception ex) {
	    _logger.log(Level.WARNING, "Couldn't get containerID", ex);
	}

        String baseDirName = (String) storeEnv.get(
                FileStoreManagerConstants.PASSIVATION_DIRECTORY_NAME);

        this.baseDir = new File(baseDirName);

        try {
	    Integer sessionTimeout = (Integer) storeEnv.get(
		FileStoreManagerConstants.SESSION_TIMEOUT_IN_SECONDS);
	    if (sessionTimeout != null) {
		this.passivationTimeoutInSeconds = sessionTimeout.intValue();
	    }
	} catch (Exception ex) {
	    _logger.log(Level.WARNING, "Couldn't get session timeout", ex);
	}

        try {
	    Integer graceTimeout = (Integer) storeEnv.get(
		FileStoreManagerConstants.GRACE_SESSION_TIMEOUT_IN_SECONDS);
	    if (graceTimeout != null) {
		this.gracePeriodInSeconds = graceTimeout.intValue();
	    }
	} catch (Exception ex) {
	    _logger.log(Level.WARNING, "Couldn't get session timeout", ex);
	}

        try {
            if ((baseDir.mkdirs() == false) && (!baseDir.isDirectory())) {
		_logger.log(Level.WARNING, "ejb.sfsb_storemgr_mdirs_failed",
			new Object[] {baseDirName});
		//TODO: Log storeName also
	    } else {
		if (_logger.isLoggable(TRACE_LEVEL)) {
		    _logger.log(TRACE_LEVEL, "Successfully Initialized "
			    + "FileStoreManager for: " + storeName);
		}
	    }
        } catch (Exception ex) {
	    _logger.log(Level.WARNING, "ejb.sfsb_storemgr_init_failed",
                    new Object[] {baseDirName});
	    _logger.log(Level.WARNING, "ejb.sfsb_storemgr_init_exception", ex);
        }

    }

    public void passivateSave(SFSBBeanState beanState)
	throws SFSBStoreManagerException
    {
        saveState(beanState, true);
    }

    public void remove(Object sessionKey) {
        try {
            removeFile(new File(baseDir, sessionKey.toString()));
        } catch (Exception ex) {
            _logger.log(Level.WARNING,
                "ejb.sfsb_storemgr_removestate_failed",
                new Object[] {sessionKey.toString()});
            _logger.log(Level.WARNING,
                "ejb.sfsb_storemgr_removestate_exception", ex);
        }
    }
      
    public void removeAll() {
        try {
	    String[] fileNames = baseDir.list();
	    for (int i=0; i<fileNames.length; i++) {
                remove(fileNames[i]);
	    }

	    if (baseDir.delete() == false) {
		if (baseDir.exists()) {
		    Object[] params = {baseDir.getAbsolutePath()};
		    _logger.log(Level.WARNING,
			"ejb.sfsb_storemgr_removedir_failed", params);
		}
	    }
        } catch (Throwable th) {
            _logger.log(Level.WARNING, "ejb.sfsb_storemgr_removeall_exception", th);
        }
    }
      
    public void removeExpired() {
	expiredSessionCount += removeExpiredSessions();
    }

    public int removeExpiredSessions() {
	if (passivationTimeoutInSeconds <= 0) {
	    return 0;
	}
	long threshold = System.currentTimeMillis()
	    - (passivationTimeoutInSeconds * 1000)
	    - (gracePeriodInSeconds * 1000);
	int expiredSessions = 0;
        try {
	    String[] fileNames = baseDir.list();
	    int size = fileNames.length;
	    for (int i=0; (i<size) && (! shutdown); i++) {
		File file = new File(baseDir, fileNames[i]);
		if (file.exists()) {
		    long lastAccessed = file.lastModified();
		    if (lastAccessed < threshold) {
			if (! file.delete()) {
			    if (file.exists()) {
				_logger.log(Level.WARNING, storeName
				    + "Couldn't remove file: " + fileNames[i]);
			    }
			} else {
			    expiredSessions++;
			}
		    }
		}
	    }
	} catch (Exception ex) {
	    _logger.log(Level.WARNING, storeName + "Exception while getting "
		    + "expired files", ex);
	}

	return expiredSessions;
    }

    public void shutdown() {
        shutdown = true;
    }
      
    public void updateLastAccessTime(Object sessionKey, long time)
        throws SFSBStoreManagerException
    {
	String fileName = sessionKey.toString();
	try {
	    File file = new File(baseDir, fileName);

	    if (file.setLastModified(time) == false) {
		if (file.exists() == false) {
		    _logger.log(Level.WARNING, storeName 
			+ ": Cannot update timsestamp for: " + sessionKey
			    + "; File does not exist");
		} else {
		    throw new SFSBStoreManagerException(
			storeName + ": Cannot update timsestamp for: " + sessionKey);
		}
	    }
	} catch (SFSBStoreManagerException sfsbSMEx) {
	    throw sfsbSMEx;
	} catch (Exception ex) {
	    _logger.log(Level.WARNING, storeName 
		+ ": Exception while updating timestamp", ex);			
	    throw new SFSBStoreManagerException(
		"Cannot update timsestamp for: " + sessionKey
		    + "; Got exception: " + ex);			
	}
    }

    /***************************************************************/
    /**************  Methods on MonitorableSFSBCache  **************/
    /***************************************************************/

    public int getCurrentSize() {
        //return passivatedSessions.getEntryCount();
	try {
	    return baseDir.list().length;
	} catch (Exception ex) {
	}
	return 0;
    }

    public int getLoadCount() {
        return loadCount;
    }
    
    public int getLoadSuccessCount() {
        return loadSuccessCount;
    }
    
    public int getLoadErrorCount() {
        return loadErrorCount;
    }

    public int getPassivationCount() {
        return storeCount;
    }
    
    public int getPassivationSuccessCount() {
        return storeSuccessCount;
    }
    
    public int getPassivationErrorCount() {
        return storeErrorCount;
    }

    public int getCheckpointCount() {
        return storeCount;
    }
    
    public int getCheckpointSuccessCount() {
        return storeSuccessCount;
    }
    
    public int getCheckpointErrorCount() {
        return storeErrorCount;
    }

    public int getExpiredSessionCount() {
        return expiredSessionCount;
    }


    /***************************************************************/
    /*********************  Internal methods  **********************/
    /***************************************************************/

    private void saveState(SFSBBeanState beanState, boolean isPassivated)
	throws SFSBStoreManagerException
    {

        Object sessionKey =  beanState.getId();
        String fileName = sessionKey.toString();

	if(_logger.isLoggable(TRACE_LEVEL)) {
	    _logger.log(TRACE_LEVEL, storeName + " Attempting to save "
		    + "session: " + sessionKey);
	}
        File file = null;

        BufferedOutputStream bos = null;
        FileOutputStream fos = null;

        try {
            storeCount++;
            file = new File(baseDir, fileName);
	    if (file.exists()) {
		if (beanState.isNew()) {
		    _logger.log(Level.WARNING, storeName + " [InternalError] "
			+ "isNew() must be false for: " + sessionKey);
		}
	    } else {
		if (beanState.isNew() == false) {
		    _logger.log(Level.WARNING, storeName + " [InternalError] "
			+ "isNew() must be true for: " + sessionKey);
		}
	    }
            fos = new FileOutputStream(file);
            bos = new BufferedOutputStream(fos);
            byte[] data = beanState.getState();
            bos.write(data, 0, data.length);

            storeSuccessCount++;
	    if(_logger.isLoggable(TRACE_LEVEL)) {
		_logger.log(TRACE_LEVEL, storeName + " Successfully saved "
			+ "session: " + sessionKey);
            }
        } catch (Exception ex) {
            storeErrorCount++;
	    _logger.log(Level.WARNING, "ejb.sfsb_storemgr_savestate_failed",
                new Object[] {fileName});
	    _logger.log(Level.WARNING, "ejb.sfsb_storemgr_savestate_exception", ex);
            try { removeFile(file); } catch (Exception ex1) {}
            String errMsg = "Could not save session: " + beanState.getId();
            throw new SFSBStoreManagerException(errMsg, ex);
        } finally {
            try {
		if (bos != null) bos.close();
	    } catch (Exception ex) {
		_logger.log(Level.FINE, "Error while closing buffered output stream", ex);
	    }
            try {
		if (fos != null) fos.close();
	    } catch (Exception ex) {
		_logger.log(Level.FINE, "Error while closing file output stream", ex);
	    }
        }
    }
      
    private boolean removeFile(final File file) {
        Boolean status = (Boolean) java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction() {
                public java.lang.Object run() {
                    return (new Boolean(file.delete()));
                }
            }                
        );

        boolean success = status.booleanValue();
        if (!success) {
            _logger.log(Level.WARNING, "ejb.sfsb_storemgr_removestate_failed",
                new Object[] {file.getName()});
        } else {
	    if(_logger.isLoggable(TRACE_LEVEL)) {
		_logger.log(TRACE_LEVEL, storeName + " Removed session: "
			+ file.getName());
            }
	}

	return success;
    }

    public MonitorableSFSBStoreManager getMonitorableSFSBStoreManager() {
	return this;
    }

    public long getCurrentStoreSize() {
	try {
	    return baseDir.list().length;
	} catch (Exception ex) {
	}
	return 0;
    }

    public void appendStats(StringBuffer sbuf) {
    }

    public void monitoringLevelChanged(boolean monitoringOn) {
    }

}
