// ========================================================================
// $Id: AbstractReplicatedStore.java,v 1.5 2004/06/22 16:23:44 jules_gosnell Exp $
// Copyright 2002-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================

package org.mortbay.j2ee.session;

//----------------------------------------

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.jboss.logging.Logger;

//----------------------------------------

// implement scavenging
// implement setMaxInactiveInterval
// look for NYI/TODO

// this infrastructure could probably be used across JMS aswell -
// think about it...

/**
 * Maintain synchronisation with other States representing the same
 * session by publishing changes made to ourself and updating ourself
 * according to notifications published by the other State objects.
 *
 * @author <a href="mailto:jules@mortbay.com">Jules Gosnell</a>
 * @version 1.0
 */

abstract public class
  AbstractReplicatedStore
  extends AbstractStore
{
  protected final static Logger _log=Logger.getLogger(AbstractReplicatedStore.class);

  protected ClassLoader _loader;

  public
    AbstractReplicatedStore()
    {
      super();
      _loader=Thread.currentThread().getContextClassLoader();
    }

  public ClassLoader getLoader() {return _loader;}
  public void setLoader(ClassLoader loader) {_loader=loader;}

  //----------------------------------------
  // tmp hack to prevent infinite loop
  private final static ThreadLocal _replicating=new ThreadLocal();
  public static boolean     getReplicating()                    {return _replicating.get()==Boolean.TRUE;}
  public static void        setReplicating(boolean replicating) {_replicating.set(replicating?Boolean.TRUE:Boolean.FALSE);}
  //----------------------------------------

  public Object
    clone()
    {
      AbstractReplicatedStore ars=(AbstractReplicatedStore)super.clone();
      ars.setLoader(getLoader());
      return ars;
    }

  protected Map     _sessions=new HashMap();

  public int
    getSessions()
    {
      return _sessions.size();
    }

  //----------------------------------------
  // Store API - Store LifeCycle

  public void
    destroy()			// corresponds to ctor
    {
      _log.trace("destroying...");
      _sessions.clear();
      _sessions=null;
      setManager(null);
      super.destroy();
      _log.trace("...destroyed");
    }

  //----------------------------------------
  // Store API - State LifeCycle

  public State
    newState(String id, int maxInactiveInterval)
    throws Exception
    {
      long creationTime=System.currentTimeMillis();

      if (!AbstractReplicatedStore.getReplicating())
      {
	Object[] argInstances = {id, new Long(creationTime), new Integer(maxInactiveInterval), new Integer(_actualMaxInactiveInterval)};
	publish(null, CREATE_SESSION, argInstances);
      }

      createSession(id, creationTime, maxInactiveInterval, _actualMaxInactiveInterval);

      // if we get one - all we have to do is loadState - because we
      // will have just created it...
      return loadState(id);
    }

  public State
    loadState(String id)
    {
      // pull it out of our cache - if it is not there, it doesn't
      // exist/hasn't been distributed...

      Object tmp;
      synchronized (_sessions) {tmp=_sessions.get(id);}
      return (State)tmp;
    }

  public void
    storeState(State state)
    {
      try
      {
	String id=state.getId();
	synchronized (_sessions){_sessions.put(id, state);}
      }
      catch (Exception e)
      {
	_log.error("error storing session", e);
      }
    }

  public void
    removeState(State state)
    throws Exception
    {
      String id=state.getId();

      if (!AbstractReplicatedStore.getReplicating())
      {
	Object[] argInstances = {id};
	publish(null, DESTROY_SESSION, argInstances);
      }

      destroySession(id);
    }

  //----------------------------------------
  // Store API - garbage collection

  public void
    scavenge()
    throws Exception
    {
      _log.trace("starting distributed scavenging...");
      Collection copy;
      synchronized (_sessions) { copy=new ArrayList(_sessions.values()); }
      if (_log.isTraceEnabled())
      {
        int n;
      	synchronized (_subscribers) { n = _subscribers.size(); }
      	_log.trace(copy.size()+" distributed sessions, "+n+" subscribers");
      }
      int n = 0;
      for (Iterator i = copy.iterator(); i.hasNext(); )
      {
        LocalState state = (LocalState) i.next();
        if (! state.isValid(_scavengerExtraTime))
	  {
            String id = state.getId();
            if (_log.isDebugEnabled()) _log.debug("scavenging distributed session "+id);
            destroySession(id);
	    i.remove();
            ++n;
	  }
      }
     if (_log.isTraceEnabled()) _log.trace("scavenged "+n+" distributed sessions");
     _log.trace("...finished distributed scavenging");
    }

  //----------------------------------------
  // Store API - hacks... - NYI/TODO

  public void passivateSession(StateAdaptor sa) {}
  public boolean isDistributed() {return true;}

  //----------------------------------------
  // utils

  public String
    getContextPath()
    {
      return getManager().getContextPath();
    }

  //----------------------------------------
  // change notification API

  protected static Map      _methodToInteger=new HashMap();
  protected static Method[] _integerToMethod=new Method[8];
  protected static Method   CREATE_SESSION;
  protected static Method   DESTROY_SESSION;
  protected static Method   TOUCH_SESSIONS;
  protected static Method   SET_LAST_ACCESSED_TIME;

  static
  {
    // this is absolutely horrible and will break if anyone changes
    // the shape of the interface - but it is a quick, easy and
    // efficient hack - so I am using it while I think of a better
    // way...
    try
    {
      int index=0;
      Method m=null;

      // class methods...
      m=CREATE_SESSION=AbstractReplicatedStore.class.getMethod("createSession", new Class[]{String.class, Long.TYPE, Integer.TYPE, Integer.TYPE});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      m=DESTROY_SESSION=AbstractReplicatedStore.class.getMethod("destroySession", new Class[]{String.class});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      m=TOUCH_SESSIONS=AbstractReplicatedStore.class.getMethod("touchSessions", new Class[]{String[].class, Long.TYPE});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      // instance methods...
      m=SET_LAST_ACCESSED_TIME=State.class.getMethod("setLastAccessedTime", new Class[]{Long.TYPE});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      m=State.class.getMethod("setMaxInactiveInterval", new Class[]{Integer.TYPE});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      m=State.class.getMethod("setAttribute", new Class[]{String.class, Object.class, Boolean.TYPE});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      m=State.class.getMethod("setAttributes", new Class[]{Map.class});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;

      m=State.class.getMethod("removeAttribute", new Class[]{String.class, Boolean.TYPE});
      _integerToMethod[index]=m;
      _methodToInteger.put(m.getName(), new Integer(index));
      index++;
    }
    catch (Exception e)
    {
      System.err.println("AbstractReplicatedStore: something went wrong building dispatch tables");
      e.printStackTrace(System.err);
    }
  }

  abstract protected void publish(String id, Method method, Object[] argInstances);

  protected void
    dispatch(String id, Integer methodId, Object[] argInstances)
    {
      try
      {
	AbstractReplicatedStore.setReplicating(true);

	Object target=null;
	if (id==null)
	{
	  // either this is a class method
	  target=this;
	}
	else
	{
	  // or an instance method..
	  synchronized (_subscribers){target=_subscribers.get(id);}
	}

	try
	{
	  Method method=_integerToMethod[methodId.intValue()];
	  if (target==null)
              _log.warn("null target for "+method);
          else
              method.invoke(target, argInstances);
	}
	catch (Exception e)
	{
	  _log.error("this should never happen - code version mismatch ?", e);
	}
      }
      finally
      {
	AbstractReplicatedStore.setReplicating(false);
      }
    }

  public void
    createSession(String id, long creationTime, int maxInactiveInterval, int actualMaxInactiveInterval)
    {
      if (_log.isTraceEnabled()) _log.trace("creating replicated session: "+id);
      State state=new LocalState(id, creationTime, maxInactiveInterval, actualMaxInactiveInterval);
      synchronized(_sessions) {_sessions.put(id, state);}

      if (AbstractReplicatedStore.getReplicating())
      {
	//	_log.info("trying to promote replicated session");
	getManager().getHttpSession(id); // should cause creation of corresponding InterceptorStack
      }
    }

  public void
    destroySession(String id)
    {
      if (_log.isTraceEnabled()) _log.trace("destroying replicated session: "+id);
      if(getManager().sessionExists(id))
          getManager().destroySession(getManager().getHttpSession(id));
      synchronized(_sessions) {_sessions.remove(id);}
    }

  public void
    touchSessions(String[] ids, long time)
    {
      //      _log.info("touching sessions...: "+ids);
     for (int i=0;i<ids.length;i++)
      {
	String id=ids[i];
	Object target;
	// I could synch the whole block. This is slower, but will not
	// hold up everything else...
	synchronized (_subscribers){target=_subscribers.get(id);}
	try
	{
	  ((StateInterceptor)target).setLastAccessedTime(time);
	}
	catch (Exception e)
	{
	  _log.warn("unable to touch session: "+id+" probably already removed");
	}
      }
    }

  //----------------------------------------
  // subscription - Listener management...

  protected Map _subscribers=new HashMap();

  public void
    subscribe(String id, Object o)
    {
      _log.trace("subscribing: "+id);
      synchronized (_subscribers) {_subscribers.put(id, o);}
    }

  public void
    unsubscribe(String id)
    {
      _log.trace("unsubscribing: "+id);
      synchronized (_subscribers) {_subscribers.remove(id);}
    }
}
