package com.iplanet.server.http.session;

import java.io.*;
import java.util.*;
import javax.servlet.http.*;
import javax.servlet.ServletContext;

/**
 * Contains methods to send log data to error log
 */
import com.iplanet.server.http.util.LogUtil;

/**
 * Contains methods to send internationalized messages to log
 */
import com.iplanet.server.http.util.ResUtil;

/**
 * iWS checks the sanity of the session manager by simply creating one
 * internal session just after initialising the session manager.
 *
 * This internal session  never times out and its name has the prefix
 * specified by <code>NSServletRunner.contextSessionName</code>.
 */
import com.iplanet.server.http.servlet.NSServletRunner;

/**
 * This class implements <code>javax.servlet.http.HttpSession</code> to
 * provide support for sessions in the iPlanet Web Server.
 *
 * Session provides a way to identify a user across more than one page
 * request or visit to a Web site. The servlet engine uses this interface
 * to create a session between an HTTP client and an HTTP server. The
 * session persists for a specified time period, across more than one
 * connection or page request from the user. A session usually
 * corresponds to one user, who may visit a site many times. The server
 * can maintain a session either by using cookies or by rewriting URLs. <P>
 *
 * <code>IWSHttpSession</code> provides methods to do the following:
 * <ul>
 * <li>View and manipulate information about a session, such
 *     as the session identifier, creation time, or context
 * <li>Bind objects to sessions, allowing you to use an online
 *     shopping cart to hold data that persists across multiple
 *     user connections. The objects bound to the sessions by the
 *     servlets are stored in a <code>java.util.Hashtable</code>.
 * </ul><P>
 * 
 * When an application stores an object in or removes it from a session,
 * the session checks whether the object implements
 * <code>HttpSessionBindingListener</code>. If it does, 
 * the session notifies the object that it has been bound to or unbound 
 * from the session.<P>
 *
 * An HTTP session represents the server's view of the session.
 * The server considers a session new under any of these conditions:
 * <ul>
 * <li>The client does not yet know about the session
 * <li>The session has not yet begun
 * <li>The client chooses not to join the session, for example,
 *     if the server supports only cookies and the client rejects
 *     the cookies the server sends
 * </ul>
 * When the session is new, the <CODE>isNew()</CODE> method 
 * returns <code>true</code>.<P>
 *
 * In addition to all the methods in <code>javax.servlet.http.HttpSession</code>
 * <code>IWSHttpSession</code> implements few addtional <em>protected</em>
 * methods to facilitate managing of these sessions by
 * <code>SessionManager</code>, they are listed below:
 * <ul>
 * <li><code>setNew</code>
 * <li><code>unsetNew</code>
 * <li><code>removeAllObjects</code>
 * </ul>
 *
 * iPlanet Web Server 6.0's http session implementation.
 *
 * @deprecated
 */
class IWSHttpSession implements javax.servlet.http.HttpSession, java.io.Serializable
{

    /**
     * The session id that uniquely identifies the session.
     *
     * <code>SessionManager</code> maintains all active sessions in 
     * a <code>Hashtable</code> key being this session id.
     *
     * This id is generated by the Servlet Engine automatically 
     * for every new session.
     */
    private String _id = null;

    /**
     * A mangled form of <code>_id</code> that can be used as a filename.
     *
     * Essentially the same as <code>_id</code> with colons replaced by
     * underscores.
     */
    private String _mangledId = null;

    /**
     * The time when this session was created, measured in
     * milliseconds since midnight January 1, 1970 GMT.
     */
    private long _creationTime = 0L;

    /**
     * The time when this session was last accessed by a servlet,
     * measured in milliseconds since midnight January 1, 1970 GMT.
     *
     * This is the time at which the container received the *previous* request.
     */
    private long _lastAccessedTime = 0L;

    /**
     * Contains the time at which the container *received* the current request.
     *
     * This value is transferred to _lastAccessedTime during the getSession()
     * phase of the servlet request lifecycle. By doing so, _lastAccessedTime
     * will contain the timestamp of the receipt of the previous request.
     */
    private long _currentAccessTime = 0L;

    /**
     * Has this session been accessed by a servlet since it was
     * created? It should be noted that the servlet which created
     * this session will see it as a new session, everybody else
     * subsequently will see it as not a new session.
     */
    private boolean _isNew  = false;

    /**
     * The maximum length of time, in milliseconds, that the servlet
     * engine keeps this session if no user requests have been made of
     * the session.
     */
    private long _maxInactiveInterval;

    /**
     * A session can be invalidated in two ways:
     *  - session timed out
     *  - servlet explicitly invoked the <code>invalidate</code> method
     *
     * <code>_isValid</code> tells the servlet engine if the session
     * has been invalidated.
     */
    private boolean _isValid = true;

    /**
     * A <code>Hashtable</code> which stores all the objects and the
     * names they are bound to. The name the object is bound to serves
     * as a key in this table.
     */
    private Hashtable _values = new Hashtable();

    /**
     * A counter which stores the number of objects in <code>_values</code>
     * implementing <code>HttpSessionBindingListener</code> interface. If
     * there aren't any objects implementing this interface removal of the
     * session gets faster as unbind method doesn't need to be called on any
     * of the objects stored in <code>_values</code>.
     */
    private int _numObjectsToBeUnbound = 0;
    
    /**
     * A boolean variable that is set to true when the session is being
     * invalidated because it has expired.
     *
     * The session is invalid at this point but the user may need to invoke
     * some of its methods in order to clean up.
     */
    private boolean _timingOut = false;

    /**
     * The session manager object that manages this session.
     */
    private IWSHttpSessionManager _mgr = null;
    
    /**
     * The context in which this session was created.
     */
    private ServletContext _context = null;

    /**
     * A utility class which takes care of internationalizing the
     * messages sent to the error log.
     */
    private static ResUtil _res = ResUtil.getDefaultResUtil();

    /**
     * Construct a <code>IWSHttpSession</code> object with a given session
     * id, timeout value.<P>
     *
     * @param id      a String specifying the unique identifier
     *                assigned to this session
     * @param timeOut the maximum length of time, in
     *                seconds, that the servlet engine keeps this
     *                session if no user requests have been made
     *                of the session
     * @param mgr     The <code>IWSHttpSessionManager</code> object that
     *                manages this session
     * @see           IWSHttpSessionManager
     */
    public IWSHttpSession(String id, int timeOut, IWSHttpSessionManager mgr,
                          ServletContext context)
    {
        _id = id;
        if (timeOut != 0)
            setMaxInactiveInterval(timeOut);      // handles timeOut == -1
        else
            _maxInactiveInterval = 0;
        _mgr = mgr;
        _context = context;
        _values = new Hashtable();
        _creationTime = System.currentTimeMillis();
        _lastAccessedTime = _creationTime;
        _currentAccessTime = _creationTime;
    }

    /**
     * Returns the time when this session was created, measured
     * in milliseconds since midnight January 1, 1970 GMT.
     *
     * @return    a <code>long</code> integer specifying when this session 
     *            was created relative to 1-1-1970 GMT
     *
     * @exception IllegalStateException if you attempt to get the session's
     *            creation time after the session has been invalidated
     */
    public long getCreationTime()
    {
        if (_isInvalid())
            throw new IllegalStateException();

        return _creationTime;
    }

    /**
     * Returns a string containing the unique identifier assigned 
     * to this session.
     *
     * The identifier is assigned by the servlet engine and is 
     * implementation dependent.
     * 
     * @return   a string specifying the identifier assigned to this session
     *
     * @exception IllegalStateException if the session is invalid
     */
    public String getId()
    {
        return _id;
    }


    /**
     * Returns a mangled form of this session's unique ID such that
     * the string returned contains valid filename characters only.
     */
    protected final String getMangledId()
    {
        if (_id == null)
            return null;

        if (_mangledId == null)
            _mangledId = IWSHttpSession.getMangledString(_id);

        return _mangledId;
    }

    /**
     * Mangles the specified string, converting non-filename characters
     * to underscores.
     */
    protected static final String getMangledString(String id)
    {
        if (id == null)
            return null;

        StringBuffer buffer = new StringBuffer(id);
        int length = id.length();

        for (int i = 0; i < length; i++)
        {
            char c = buffer.charAt(i);
            if (Character.isLetterOrDigit(c) == false)
                buffer.setCharAt(i, '_');
        }
        return buffer.toString();
    }

    /**
     * Returns the last time the client sent a request associated with
     * this session, as the number of milliseconds since midnight
     * January 1, 1970 GMT. 
     *
     * <p>Actions that your application takes, such as getting or setting
     * a value associated with the session, do not affect the access
     * time.
     *
     * <p>The last accessed time can help you manage sessions. For example,
     * the sessions can be sorted according to age to optimize some task.
     *
     * @return    a <code>long</code> integer representing the last time 
     *            the client sent a request associated with this session,
     *            expressed in milliseconds since 1-1-1970 GMT
     */
    public long getLastAccessedTime()
    {
        return _lastAccessedTime;
    }

    /**
     * Returns the maximum time interval, in seconds, that 
     * the servlet engine will keep this session open between 
     * client requests.
     *
     * You can set the maximum time interval with the 
     * <code>setMaxInactiveInterval</code> method.
     *
     * @return    an integer specifying the number of seconds this session 
     *            remains open between client requests
     *
     * @see       #setMaxInactiveInterval
     */
    public int getMaxInactiveInterval()
    {
        int res = getTimeout();
        if (res == Integer.MAX_VALUE) {
            res = -1;
        }

        return res;
    }

    protected final int getTimeout()
    {
        int res = (int) (_maxInactiveInterval == Long.MAX_VALUE ? Integer.MAX_VALUE : (_maxInactiveInterval/1000));
        return res;
    }

    /**
     * @deprecated As of Version 2.1, this method is deprecated and has no 
     *             replacement. It will be removed in a future version of the 
     *             Java Servlet API.
     */
    public HttpSessionContext getSessionContext()
    {
        if (_isInvalid())
            throw new IllegalStateException();
        
        return _mgr.getContext();
    }

    /**
     * Returns the object bound with the specified name in this session
     * or null if no object of that name exists.
     *
     * @param name          a string specifying the name of the object
     * @return              the object with the specified name
     * @exception IllegalStateException if the session is invalid
     */
    public Object getAttribute(String name)
    {
        if (_isInvalid())
            throw new IllegalStateException();

        // name cannot be null
        if (name == null)
            throw new NullPointerException(_res.getProp("session.IWSHttpSession.msg_namevalueNull"));

        return _values.get(name);
    }

    /**
     * @deprecated  As of Version 2.2, this method is deprecated.
     *              Its replacement method is <code>getAttribute</code>.
     *
     * Returns the object bound with the specified name in this session
     * or null if no object of that name exists.
     *
     * @param name a string specifying the name of the object
     * @return     the object with the specified name
     * @exception  IllegalStateException if the session is invalid
     */
    public Object getValue(String name)
    {
        return getAttribute(name);
    }
    
    /**
     * Returns an Enumeration of String objects containing the names
     * of all the objects bound to this session.
     *
     * This method is useful, for example, when you want to delete
     * all the objects bound to this session.
     *
     * @return    an enumeration of strings specifying the names of all the 
     *            objects bound to this session
     *
     * @exception IllegalStateException if the session is invalid
     */
    public Enumeration getAttributeNames()
    {
        if (_isInvalid())
            throw new IllegalStateException();
        
        return _values.keys ();
    }

    /**
     * @deprecated As of Version 2.2, this method is deprecated.
     *             Its replacement method is <code>getAttributeNames</code>.
     *
     * Returns an array containing the names of all the objects
     * bound to this session. This method is useful, for example,
     * when you want to delete all the objects bound to this
     * session.
     *
     * @return    an array of strings specifying the names of all the objects 
     *            bound to this session
     *
     * @exception IllegalStateException if the session is invalid
     */
    public String [] getValueNames()
    {
        if (_isInvalid())
            throw new IllegalStateException();
        
        String [] names = null;
        synchronized(_values)
        {
            names = new String [_values.size()];

            int i = 0;
            for (Enumeration e = _values.keys(); e.hasMoreElements() ; i++)
                names[i] = (String) e.nextElement(); 
        }
        
        return names;
    }

    /**
     * Invalidates this session and unbinds any objects bound to it.
     *
     * @exception IllegalStateException if the session is already invalid
     */
    public void invalidate()
    {
        if (_isInvalid())
            throw new IllegalStateException();
        
        _mgr.deleteSession(this);
        
        _isValid = false;
    }

    /**
     * Returns <code>true</code> if the Web server has created a session
     * but the client has not yet joined. 
     *
     * For example, if the server used only cookie-based sessions, and
     * the client had disabled the use of cookies, then a session would
     * be new.
     *
     * @return    <code>true</code> if the server has created a session, 
     *            but the client has not yet joined
     *
     * @exception IllegalStateException if the session is invalid
     */
    public boolean isNew()
    {
        if (_isInvalid())
            throw new IllegalStateException();

        return _isNew;
    }

    /**
     * Binds an object to this session, using the name specified.
     *
     * If an object of the same name is already bound to the session,
     * the object is replaced.
     *
     * @param name  the name to which the object is bound; cannot be null
     * @param value the object to be bound; cannot be null
     *
     * @exception   IllegalStateException if the session is invalid
     */
    public void setAttribute (String name, Object value)
    {
        if (_isInvalid())
            throw new IllegalStateException();

        // name or value cannot be null
        if (name == null || value == null)
            throw new NullPointerException(_res.getProp("session.IWSHttpSession.msg_namevalueNull"));

        // If a persistent store is configured, then ensure that all
        // attribute values are serializable
        if (_mgr.hasPersistence() && !(value instanceof Serializable))
        {
            throw new IllegalArgumentException(_res.getProp("session.IWSHttpSession.msg_badAttribute", name));
        }

        // First, remove the old value and call unbind method on it
        Object old_value = _values.remove(name);
        if (old_value != null)
            unbindObject(name, old_value);

        // insert the new value
        _values.put(name, value);

        if (value instanceof HttpSessionBindingListener)
        {
            _numObjectsToBeUnbound++;

            HttpSessionBindingListener listner =
                (HttpSessionBindingListener) value;
            
            listner.valueBound(new HttpSessionBindingEvent(this, name));
        }
    }

    /**
     * @deprecated As of Version 2.2, this method is deprecated.
     *             Its replacement method is <code>setAttribute</code>.
     *
     * Binds an object to this session, using the name specified.
     * If an object of the same name is already bound to the session,
     * the object is replaced.
     *
     * @param name  the name to which the object is bound; cannot be null
     * @param value the object to be bound; cannot be null
     *
     * @exception   IllegalStateException if the session is invalid
     */
    public void putValue(String name, Object value)
    {
        setAttribute(name, value);
    }

    /**
     * Removes the object bound with the specified name from
     * this session.
     *
     * If the session does not have an object
     * bound with the specified name, this method does nothing.
     *
     * <p>After this method executes, and if the object
     * implements <code>HttpSessionBindingListener</code>,
     * the object calls <code>HttpSessionBindingListener.valueUnbound</code>.
     *
     * @param name the name of the object to remove from this session
     *
     * @exception  IllegalStateException if the session is invalid
     */
    public void removeAttribute (String name)
    {
        if (_isInvalid() && !_timingOut)
            throw new IllegalStateException();
        
        // name cannot be null
        if (name == null)
            throw new NullPointerException(_res.getProp("session.IWSHttpSession.msg_namevalueNull"));

        Object value = _values.remove(name);
        if (value != null)
            unbindObject(name, value);
    }

    /**
     * @deprecated As of Version 2.2, this method is deprecated.
     *             Its replacement method is <code>removeAttribute</code>.
     *
     * Removes the object bound with the specified name from
     * this session. If the session does not have an object
     * bound with the specified name, this method does nothing.
     *
     * <p>After this method executes, and if the object
     * implements <code>HttpSessionBindingListener</code>,
     * the object calls <code>HttpSessionBindingListener.valueUnbound</code>.
     *
     * @param name the name of the object to remove from this session
     *
     * @exception  IllegalStateException if the session is invalid
     */
    public void removeValue(String name)
    {
        removeAttribute(name);
    }

    /**
     * Specifies the maximum length of time, in seconds, that the
     * servlet engine keeps this session if no user requests
     * have been made of the session.
     *
     * @param   interval An integer specifying the number of seconds
     * @exception        IllegalStateException if the session is invalid
     */
    public void setMaxInactiveInterval(int interval)
    {
        _maxInactiveInterval = interval * 1000;
        if (interval < 0)
        {
            _maxInactiveInterval = Long.MAX_VALUE;
        }
        else if (interval == 0)
        {
            _mgr.deleteSession(this);
            _isValid = false;
        }
    }

    /**
     * Indicates whether this session object is still a valid one
     * or if it has previously been invalidated or expired.
     *
     * This method differs from isInvalid() in that it checks if
     * the session was *previously* detected to be invalid. It does
     * not check for session expiry based on the current time.
     * This method is used by IWSSessionManager to avoid saving
     * invalidated sessions to persistent store.
     */
    protected boolean isValid()
    {
        return _isValid;
    }

    /**
     * Checks if the session is still valid. 
     *
     * Session is invalid if either it's already marked invalid or if it 
     * has timed out. If the session has timeout this method then marks 
     * this session as invalid.
     */
    protected boolean isInvalid(long currentTime)
    {
        if (_isValid == false)
        {
            return true;
        }

        long elapsed = currentTime - _lastAccessedTime;
        if (elapsed  >= _maxInactiveInterval)
        {
            _isValid = false;
            _timingOut = true;
            return true;
        }
        
        return false;
    }

    /**
     * Checks if the session is still valid. 
     *
     * Session is invalid if either it's already marked invalid or if it 
     * has timed out. If the session has timeout this method then marks 
     * this session as invalid.
     */
    protected boolean _isInvalid()
    {
        return isInvalid(System.currentTimeMillis());
    }

    /**
     * A convenience method to record the fact that it's a new session.
     *
     * It's called only by <code>SessionManager</code>.
     */
    protected void setNew()
    {
        _isNew = true;
    }

    /**
     * A convenience method to record the fact that it's not a new session.
     *
     * It's called only by <code>SessionManager</code>.
     */
    protected void unsetNew()
    {
        _isNew = false;
    }

    protected void updateAccessTime()
    {
        // update the time when the previous request was received
        _lastAccessedTime = _currentAccessTime;
        // initialize the time of receipt of the current request
        _currentAccessTime = System.currentTimeMillis();
    }

    /**
     * 
     * Removes all the objects bound to this session. If the session doesn't
     * have any objects bound this method does nothing.
     *
     * <p>After this method executes, and if the object implements 
     * <code>HttpSessionBindingListener</code>, the object calls 
     * <code>HttpSessionBindingListener.valueUnbound</code>.
     */
    protected synchronized void removeAllObjects()
    {
        // First call unbind methods on all the objects

        if (_numObjectsToBeUnbound > 0)
        {
            Enumeration enum = _values.keys();
            while (enum.hasMoreElements())
            {
                String key = (String) enum.nextElement();
            
                Object value = _values.get(key);
                if (value == null)
                    continue;
            
                try
                {
                    unbindObject(key, value);
                }
                catch (Exception e)
                {
                    LogUtil.logWarning(_res.getProp("session.IWSHttpSession.msg_UnbindError", key, LogUtil.getStackTrace(e)));
                }

                if (_numObjectsToBeUnbound == 0)
                    break;
            }
        }
        
        _values = null;
    }

    /**
     * A convenience method to call valueUnbound method on an object about
     * to be removed from this session.
     *
     * If the object implements <code>HttpSessionBindingListener</code>,
     * the object calls <code>HttpSessionBindingListener.valueUnbound</code>.
     *
     * @param name the name with which this value is bound to this session
     */
    private void unbindObject(String name, Object value)
    {
        if (value instanceof HttpSessionBindingListener)
        {
            _numObjectsToBeUnbound--;

            HttpSessionBindingListener listner =
                (HttpSessionBindingListener) value;

            listner.valueUnbound(new HttpSessionBindingEvent(this, name));
        }
    }

    private void writeObject(ObjectOutputStream out) throws IOException
    {
        if (out != null)
        {
            out.writeObject(_id);
            out.writeLong(_creationTime);
            out.writeLong(_lastAccessedTime);
            out.writeLong(_currentAccessTime);
            out.writeLong(_maxInactiveInterval);
            out.writeObject(_values);
        }
    }

    private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        if (in != null)
        {
            _id = (String)in.readObject();
            _creationTime = in.readLong();
            _lastAccessedTime = in.readLong();
            _currentAccessTime = in.readLong();
            _maxInactiveInterval = in.readLong();
            _values = (Hashtable)in.readObject();
        }
    }

    protected final void setIWSContext(IWSHttpSessionManager mgr,
                                       ServletContext context)
    {
        _mgr = mgr;
        _context = context;
        _isValid = true;
    }

    public final ServletContext getServletContext()
    {
        return _context;
    }

    /**
     * Cleanup/release object references.
     *
     * Don't set _id or _mangledId to null as IWSSessionManager::update()
     * needs these to identify and unlock any cross-process locks that may
     * have been held for this session.
     */
    protected void close()
    {
        _values = null;
        _mgr = null;
        _context = null;
    }
}
