//----------------------------BEGIN LICENSE----------------------------
/* 
 * Willow : the Open Source WorkFlow Project
 * Distributable under GNU LGPL license by gun.org
 *
 * Copyright (C) 2004-2010  huihoo.org 
 * Copyright (C) 2004-2010  ZosaTapo <dertyang@hotmail.com> 
 * 
 * ====================================================================
 * Project Homepage : http://www.huihoo.org/willow
 * Source Forge     : http://sourceforge.net/projects/huihoo
 * Mailing list     : willow@lists.sourceforge.net 
 */
//----------------------------END  LICENSE-----------------------------
package org.huihoo.willow.session;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Iterator;

import org.huihoo.willow.Context;
import org.huihoo.willow.Lifecycle;
import org.huihoo.willow.LifecycleException;
import org.huihoo.willow.LifecycleListener;
import org.huihoo.willow.Loader;
import org.huihoo.willow.Service;
import org.huihoo.willow.util.CustomObjectInputStream;
import org.huihoo.willow.util.LifecycleSupport;

public class StandardManager extends ManagerBase implements Lifecycle, PropertyChangeListener
{
	// ---------------------------------------------------- Security Classes
	private class PrivilegedDoLoad implements PrivilegedExceptionAction
	{

		PrivilegedDoLoad()
		{
		}

		public Object run() throws Exception
		{
			doLoad();
			return null;
		}
	}

	private class PrivilegedDoUnload implements PrivilegedExceptionAction
	{

		PrivilegedDoUnload()
		{
		}

		public Object run() throws Exception
		{
			doUnload();
			return null;
		}

	}

	// ----------------------------------------------------- Instance Variables

	/**
	 * The descriptive information about this implementation.
	 */
	protected static final String info = "StandardManager/1.0";

	/**
	 * The lifecycle event support for this component.
	 */
	protected LifecycleSupport lifecycle = new LifecycleSupport(this);

	/**
	 * The maximum number of active Sessions allowed, or -1 for no limit.
	 */
	protected int maxActiveSessions = -1;

	/**
	 * The descriptive name of this Manager implementation (for logging).
	 */
	protected static String name = "StandardManager";

	/**
	 * Path name of the disk file in which active sessions are saved
	 * when we stop, and from which these sessions are loaded when we start.
	 * A <code>null</code> value indicates that no persistence is desired.
	 * If this pathname is relative, it will be resolved against the
	 * temporary working directory provided by our context, available via
	 * the <code>willow.ws_scratchdir</code> context attribute.
	 */
	protected String pathname = "SESSIONS.ser";

	/**
	 * Has this component been started yet?
	 */
	protected boolean started = false;

	int rejectedSessions = 0;
	int expiredSessions = 0;
	long processingTime = 0;

	// ------------------------------------------------------------- Properties

	/**
	 * Set the Service with which this Manager has been associated. 
	 *
	 * @param service The associated Service
	 */
	public void setService(Service service)
	{
		// Default processing provided by our superclass
		super.setService(service);

		setMaxInactiveInterval(this.service.getSessionTimeout() * 60);
		this.service.addPropertyChangeListener(this);

	}

	/**
	 * Return descriptive information about this Manager implementation and
	 * the corresponding version number, in the format
	 * <code>&lt;description&gt;/&lt;version&gt;</code>.
	 */
	public String getInfo()
	{

		return (this.info);

	}

	/**
	 * Return the maximum number of active Sessions allowed, or -1 for
	 * no limit.
	 */
	public int getMaxActiveSessions()
	{

		return (this.maxActiveSessions);

	}

	/** Number of session creations that failed due to maxActiveSessions
	 *
	 * @return
	 */
	public int getRejectedSessions()
	{
		return rejectedSessions;
	}

	public void setRejectedSessions(int rejectedSessions)
	{
		this.rejectedSessions = rejectedSessions;
	}

	/** Number of sessions that expired.
	 *
	 * @return
	 */
	public int getExpiredSessions()
	{
		return expiredSessions;
	}

	public void setExpiredSessions(int expiredSessions)
	{
		this.expiredSessions = expiredSessions;
	}

	public long getProcessingTime()
	{
		return processingTime;
	}

	public void setProcessingTime(long processingTime)
	{
		this.processingTime = processingTime;
	}

	/**
	 * Set the maximum number of actives Sessions allowed, or -1 for
	 * no limit.
	 *
	 * @param max The new maximum number of sessions
	 */
	public void setMaxActiveSessions(int max)
	{

		int oldMaxActiveSessions = this.maxActiveSessions;
		this.maxActiveSessions = max;
		support.firePropertyChange(
			"maxActiveSessions",
			new Integer(oldMaxActiveSessions),
			new Integer(this.maxActiveSessions));

	}

	/**
	 * Return the descriptive short name of this Manager implementation.
	 */
	public String getName()
	{

		return (name);

	}

	/**
	 * Return the session persistence pathname, if any.
	 */
	public String getPathname()
	{

		return (this.pathname);

	}

	/**
	 * Set the session persistence pathname to the specified value.  If no
	 * persistence support is desired, set the pathname to <code>null</code>.
	 *
	 * @param pathname New session persistence pathname
	 */
	public void setPathname(String pathname)
	{

		String oldPathname = this.pathname;
		this.pathname = pathname;
		support.firePropertyChange("pathname", oldPathname, this.pathname);

	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Construct and return a new session object, based on the default
	 * settings specified by this Manager's properties.  The session
	 * id will be assigned by this method, and available via the getId()
	 * method of the returned session.  If a new session cannot be created
	 * for any reason, return <code>null</code>.
	 *
	 * @exception IllegalStateException if a new session cannot be
	 *  instantiated for any reason
	 */
	public AbstractSession createSession()
	{

		if ((maxActiveSessions >= 0) && (sessions.size() >= maxActiveSessions))
		{
			rejectedSessions++;
			throw new IllegalStateException(sm.getString("standardManager.createSession.ise"));
		}

		return (super.createSession());

	}

	/**
	 * Load any currently active sessions that were previously unloaded
	 * to the appropriate persistence mechanism, if any.  If persistence is not
	 * supported, this method returns without doing anything.
	 *
	 * @exception ClassNotFoundException if a serialized class cannot be
	 *  found during the reload
	 * @exception IOException if an input/output error occurs
	 */
	public void load() throws ClassNotFoundException, IOException
	{
		if (System.getSecurityManager() != null)
		{
			try
			{
				AccessController.doPrivileged(new PrivilegedDoLoad());
			}
			catch (PrivilegedActionException ex)
			{
				Exception exception = ex.getException();
				if (exception instanceof ClassNotFoundException)
				{
					throw (ClassNotFoundException) exception;
				}
				else if (exception instanceof IOException)
				{
					throw (IOException) exception;
				}
				if (log.isDebugEnabled())
					log.debug("Unreported exception in load() " + exception);
			}
		}
		else
		{
			doLoad();
		}
	}

	/**
	 * Load any currently active sessions that were previously unloaded
	 * to the appropriate persistence mechanism, if any.  If persistence is not
	 * supported, this method returns without doing anything.
	 *
	 * @exception ClassNotFoundException if a serialized class cannot be
	 *  found during the reload
	 * @exception IOException if an input/output error occurs
	 */
	protected void doLoad() throws ClassNotFoundException, IOException
	{
		if (log.isDebugEnabled())
		{
			log.debug("Start: Loading persisted sessions");
		}

		// Initialize our internal data structures
		sessions.clear();

		// Open an input stream to the specified pathname, if any
		File file = file();
		if (file == null)
		{
			return;
		}
		if (log.isDebugEnabled())
		{
			log.debug(sm.getString("standardManager.loading", pathname));
		}

		FileInputStream fis = null;
		ObjectInputStream ois = null;
		Loader loader = null;
		ClassLoader classLoader = null;
		try
		{
			fis = new FileInputStream(file.getAbsolutePath());
			BufferedInputStream bis = new BufferedInputStream(fis);
			if (service != null)
				loader = service.getLoader();
			if (loader != null)
				classLoader = loader.getClassLoader();
			if (classLoader != null)
			{
				if (log.isDebugEnabled())
					log.debug("Creating custom object input stream for class loader ");
				ois = new CustomObjectInputStream(bis, classLoader);
			}
			else
			{
				if (log.isDebugEnabled())
					log.debug("Creating standard object input stream");
				ois = new ObjectInputStream(bis);
			}
		}
		catch (FileNotFoundException e)
		{
			if (log.isDebugEnabled())
				log.debug("No persisted data file found");
			return;
		}
		catch (IOException e)
		{
			log.error(sm.getString("standardManager.loading.ioe", e), e);
			if (ois != null)
			{
				try
				{
					ois.close();
				}
				catch (IOException f)
				{
					;
				}
				ois = null;
			}
			throw e;
		}

		// Load the previously unloaded active sessions
		synchronized (sessions)
		{
			try
			{
				Integer count = (Integer) ois.readObject();
				int n = count.intValue();
				if (log.isDebugEnabled())
					log.debug("Loading " + n + " persisted sessions");
				for (int i = 0; i < n; i++)
				{
					StandardSession session = getNewSession();
					session.readObjectData(ois);
					session.setManager(this);
					sessions.put(session.getId(), session);
				}
			}
			catch (ClassNotFoundException e)
			{
				log.error(sm.getString("standardManager.loading.cnfe", e), e);
				if (ois != null)
				{
					try
					{
						ois.close();
					}
					catch (IOException f)
					{
						;
					}
					ois = null;
				}
				throw e;
			}
			catch (IOException e)
			{
				log.error(sm.getString("standardManager.loading.ioe", e), e);
				if (ois != null)
				{
					try
					{
						ois.close();
					}
					catch (IOException f)
					{
						;
					}
					ois = null;
				}
				throw e;
			}
			finally
			{
				// Close the input stream
				try
				{
					if (ois != null)
						ois.close();
				}
				catch (IOException f)
				{
					// ignored
				}

				// Delete the persistent storage file
				if (file != null && file.exists())
					file.delete();
			}
		}

		if (log.isDebugEnabled())
			log.debug("Finish: Loading persisted sessions");
	}

	/**
	 * Save any currently active sessions in the appropriate persistence
	 * mechanism, if any.  If persistence is not supported, this method
	 * returns without doing anything.
	 *
	 * @exception IOException if an input/output error occurs
	 */
	public void unload() throws IOException
	{
		if (System.getSecurityManager() != null)
		{
			try
			{
				AccessController.doPrivileged(new PrivilegedDoUnload());
			}
			catch (PrivilegedActionException ex)
			{
				Exception exception = ex.getException();
				if (exception instanceof IOException)
				{
					throw (IOException) exception;
				}
				if (log.isDebugEnabled())
					log.debug("Unreported exception in unLoad() " + exception);
			}
		}
		else
		{
			doUnload();
		}
	}

	/**
	 * Save any currently active sessions in the appropriate persistence
	 * mechanism, if any.  If persistence is not supported, this method
	 * returns without doing anything.
	 *
	 * @exception IOException if an input/output error occurs
	 */
	protected void doUnload() throws IOException
	{

		if (log.isDebugEnabled())
		{
			log.debug("Unloading persisted sessions");
		}

		// Open an output stream to the specified pathname, if any
		File file = file();
		if (file == null)
		{
			return;
		}
		if (log.isDebugEnabled())
		{
			log.debug(sm.getString("standardManager.unloading", pathname));
		}
		FileOutputStream fos = null;
		ObjectOutputStream oos = null;
		try
		{
			fos = new FileOutputStream(file.getAbsolutePath());
			oos = new ObjectOutputStream(new BufferedOutputStream(fos));
		}
		catch (IOException e)
		{
			log.error(sm.getString("standardManager.unloading.ioe", e), e);
			if (oos != null)
			{
				try
				{
					oos.close();
				}
				catch (IOException f)
				{
					;
				}
				oos = null;
			}
			throw e;
		}

		// Write the number of active sessions, followed by the details
		ArrayList list = new ArrayList();
		synchronized (sessions)
		{
			if (log.isDebugEnabled())
				log.debug("Unloading " + sessions.size() + " sessions");
			try
			{
				oos.writeObject(new Integer(sessions.size()));
				Iterator elements = sessions.values().iterator();
				while (elements.hasNext())
				{
					StandardSession session = (StandardSession) elements.next();
					list.add(session);
					session.writeObjectData(oos);
				}
			}
			catch (IOException e)
			{
				log.error(sm.getString("standardManager.unloading.ioe", e), e);
				if (oos != null)
				{
					try
					{
						oos.close();
					}
					catch (IOException f)
					{
						;
					}
					oos = null;
				}
				throw e;
			}
		}

		// Flush and close the output stream
		try
		{
			oos.flush();
			oos.close();
			oos = null;
		}
		catch (IOException e)
		{
			if (oos != null)
			{
				try
				{
					oos.close();
				}
				catch (IOException f)
				{
					;
				}
				oos = null;
			}
			throw e;
		}

		// Expire all the sessions we just wrote
		if (log.isDebugEnabled())
			log.debug("Expiring " + list.size() + " persisted sessions");
		Iterator expires = list.iterator();
		while (expires.hasNext())
		{
			StandardSession session = (StandardSession) expires.next();
			try
			{
				session.expire();
			}
			catch (Throwable t)
			{
				;
			}
		}

		if (log.isDebugEnabled())
			log.debug("Unloading complete");

	}

	// ------------------------------------------------------ Lifecycle Methods

	/**
	 * Add a lifecycle event listener to this component.
	 *
	 * @param listener The listener to add
	 */
	public void addLifecycleListener(LifecycleListener listener)
	{

		lifecycle.addLifecycleListener(listener);

	}

	/**
	 * Get the lifecycle listeners associated with this lifecycle. If this 
	 * Lifecycle has no listeners registered, a zero-length array is returned.
	 */
	public LifecycleListener[] findLifecycleListeners()
	{

		return lifecycle.findLifecycleListeners();

	}

	/**
	 * Remove a lifecycle event listener from this component.
	 *
	 * @param listener The listener to remove
	 */
	public void removeLifecycleListener(LifecycleListener listener)
	{

		lifecycle.removeLifecycleListener(listener);

	}

	/**
	 * Prepare for the beginning of active use of the public methods of this
	 * component.  This method should be called after <code>configure()</code>,
	 * and before any of the public methods of the component are utilized.
	 *
	 * @exception LifecycleException if this component detects a fatal error
	 *  that prevents this component from being used
	 */
	public void start() throws LifecycleException
	{

		// Validate and update our current component state
		if (started)
		{
			return;
		}
		lifecycle.fireLifecycleEvent(START_EVENT, null);
		started = true;

		// Force initialization of the random number generator
		if (log.isDebugEnabled())
			log.debug("Force random number initialization starting");
		String dummy = generateSessionId();
		if (log.isDebugEnabled())
			log.debug("Force random number initialization completed");

		// Load unloaded sessions, if any
		try
		{
			load();
		}
		catch (Throwable t)
		{
			log.error(sm.getString("standardManager.managerLoad"), t);
		}

	}

	/**
	 * Gracefully terminate the active use of the public methods of this
	 * component.  This method should be the last one called on a given
	 * instance of this component.
	 *
	 * @exception LifecycleException if this component detects a fatal error
	 *  that needs to be reported
	 */
	public void stop() throws LifecycleException
	{

		if (log.isDebugEnabled())
			log.debug("Stopping");

		// Validate and update our current component state
		if (!started)
			throw new LifecycleException(sm.getString("standardManager.notStarted"));
		lifecycle.fireLifecycleEvent(STOP_EVENT, null);
		started = false;

		// Write out sessions
		try
		{
			unload();
		}
		catch (IOException e)
		{
			log.error(sm.getString("standardManager.managerUnload"), e);
		}

		// Expire all active sessions
		AbstractSession sessions[] = findSessions();
		for (int i = 0; i < sessions.length; i++)
		{
			StandardSession session = (StandardSession) sessions[i];
			if (!session.isValid())
				continue;
			try
			{
				session.expire();
			}
			catch (Throwable t)
			{
				;
			}
		}

		// Require a new random number generator if we are restarted
		this.random = null;
	}

	// ----------------------------------------- PropertyChangeListener Methods

	/**
	 * Process property change events from our associated Context.
	 *
	 * @param event The property change event that has occurred
	 */
	public void propertyChange(PropertyChangeEvent event)
	{

		// Validate the source of this event
		if (!(event.getSource() instanceof Context))
			return;
		Context context = (Context) event.getSource();

		// Process a relevant property change
		if (event.getPropertyName().equals("sessionTimeout"))
		{
			try
			{
				setMaxInactiveInterval(((Integer) event.getNewValue()).intValue() * 60);
			}
			catch (NumberFormatException e)
			{
				log.error(sm.getString("standardManager.sessionTimeout", event.getNewValue().toString()));
			}
		}

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Return a File object representing the pathname to our
	 * persistence file, if any.
	 */
	protected File file()
	{
		if ((pathname == null) || (pathname.length() == 0))
		{
			return (null);
		}
		File file = new File(pathname);
		return (file);
	}

	/**
	 * Invalidate all sessions that have expired.
	 */
	public void processExpires()
	{

		long timeNow = System.currentTimeMillis();
		AbstractSession sessions[] = findSessions();

		for (int i = 0; i < sessions.length; i++)
		{
			StandardSession session = (StandardSession) sessions[i];
			if (!session.isValid())
			{
				try
				{
					expiredSessions++;
					session.expire();
				}
				catch (Throwable t)
				{
					log.error(sm.getString("standardManager.expireException"), t);
				}
			}
		}
		long timeEnd = System.currentTimeMillis();
		processingTime += (timeEnd - timeNow);

	}
}