/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache;

import org.jboss.cache.eviction.LRUPolicy;
import org.jboss.cache.interceptors.Interceptor;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.lock.*;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.util.NestedRuntimeException;
import org.jboss.naming.NonSerializableFactory;
import org.jgroups.*;
import org.jgroups.blocks.GroupRequest;
import org.jgroups.blocks.MethodCall;
import org.jgroups.blocks.RpcDispatcher;
import org.jgroups.util.Rsp;
import org.jgroups.util.RspList;
import org.jgroups.util.Util;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.naming.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;


/**
 * A tree-like structure that is replicated across several members. Updates will
 * be multicast to all group members reliably and in the same order. User has the
 * option to set transaction isolation level now (e.g., <code>SERIALIZABLE</code>, or
 * <code>REPEATABLE_READ</code>.
 *
 * @author Bela Ban
 * @author Ben Wang
 * @version $Id: TreeCache.java,v 1.79.2.15 2004/09/23 18:42:24 bwang00 Exp $
 * @jmx.mbean extends="org.jboss.system.ServiceMBean"
 * <p/>
 * todo: emit notifications only on commit. bad case: emit notif about node creation,
 * // then rollback TX
 */
public class TreeCache extends ServiceMBeanSupport implements TreeCacheMBean, Cloneable, MembershipListener {
   protected Node root=new Node(SEPARATOR, Fqn.fromString(SEPARATOR), null, null);
   protected Vector listeners=new Vector();
   protected JChannel channel=null;
   protected Address  local_addr=null;

   /** Am I the coordinator ? */
   protected boolean coordinator=false;

   protected String cluster_name="TreeCache-Group";
   protected String cluster_props=null;
   protected Vector members=new Vector();
   protected RpcDispatcher disp=null;
   protected LockManager lock_mgr=new LockManagerImpl();
   protected MessageListener ml=new MessageListenerAdaptor();
   protected long state_fetch_timeout=5000;
   protected long sync_repl_timeout=30000;
   protected boolean use_repl_queue=false;
   protected int repl_queue_max_elements=1000;
   protected long repl_queue_interval=5000;

   /** only used internally, no setters or getters */
   protected boolean locking=true;

   protected boolean fetch_state_on_startup=true;
   protected long lock_acquisition_timeout=10000;
   protected String eviction_policy_class=null;
   protected TreeCacheListener eviction_policy_provider = null;
   protected int cache_mode=LOCAL;
   public static Method put_data_method=null;
   public static Method put_data_erase_method=null;
   public static Method put_key_val_method=null;
   public static Method remove_node_method=null;
   public static Method remove_key_method=null;
   public static Method remove_data_method=null;
   public static Method prepare_method=null;
   public static Method commit_method=null;
   public static Method rollback_method=null;
   public static Method replicate_method=null;
   public static Method replicate_all_method=null;
   static        Method addChild_method=null;
   public static Method getKeyValueMethod=null;
   public static Method getNodeMethod=null;
   public static Method getKeysMethod=null;
   public static Method getChildrenNamesMethod=null;
   static LinkedList    crud_methods=new LinkedList();
   protected boolean isStateSet=false;
   protected Object stateLock=new Object();
   // Distinguish the node operation type. Use for locking granularity
   public static final int NODE_READ=1;
   public static final int NODE_WRITE=2;
   protected IsolationLevel isolationLevel=IsolationLevel.REPEATABLE_READ;
   protected Map thread2GtxMap_=new HashMap();
   // Eviction policy configuration in xml Element
   protected Element evictConfig_ = null;


   public MessageListener getMessageListener() {
      return ml;
   }

   /** {@link #invokeMethod(MethodCall)} will dispatch to this chain of interceptors.
    * In the future, this will be replaced with JBossAop. This is a first step towards refactoring JBossCache.
    */
   protected Interceptor interceptor_chain=null;

   /**
    * Interceptor which handles invocations of {@link #_replicate(MethodCall)}. Any such method
    * invocation is forwarded to the invoke_handler.<br/>
    * This will go away in the future, as we're moving replication functionality into the
    * ReplicationInterceptor itself
    */
   protected Replicatable replication_handler=null;

   /**
    * Maintains (a) mapping between Transaction and GlobalTransactions and
    * (b) between GlobalTransactions and TransactionEntries
    */
   protected TransactionTable tx_table=new TransactionTable();

   /**
    * Method to acquire a TransactionManager. By default we use JBossTransactionManagerLookup. Has
    * to be set before calling {@link #start()}
    */
   protected TransactionManagerLookup tm_lookup=null;

   /**
    * Class of the implementation of TransactionManagerLookup
    */
   protected String tm_lookup_class=null;

   /**
    * Used to get the Transaction associated with the current thread
    */
   protected TransactionManager tm=null;

   /** The fully qualified name of the CacheLoader (has to implement the CacheLoader interface) */
   protected String cache_loader_class=null;

   /** A reference to the CacheLoader. If null, we don't have a CachedLoader */
   protected CacheLoader cache_loader=null;

   /** The resource from which to configure the CacheLoader */
   protected String cache_loader_config=null;

   /** Are the CacheLoaders sharing the same resource or not ? */
   protected boolean cache_loader_shared=true;

   /** List<Fqn> of nodes to preload (if cache loader is enabled) */
   protected List cache_loader_preload=null;

   /** Fetches the transient state.
    * Attribute fetch_cache_on_startup has to be true */
   protected boolean cache_loader_fetch_transient_state=true;

   /** Fetches the entire persistent state from the underlying CacheLoader. Only used if cache_loader_shared=false.
    * Attribute fetch_cache_on_startup has to be true */
   protected boolean cache_loader_fetch_persistent_state=true;

   /**
    * When persistent state is retrieved, should the contents of the persistent store be cleared before setting the
    * new state, or should it be a union of the existing and new state ?
    */
   // protected boolean cache_loader_overwrite_persistent_state=true;

   /** synchronous or asynchrous commit phase ? */
   protected boolean sync_commit_phase=false;

   /** synchronous or asynchrous rollback phase ? */
   protected boolean sync_rollback_phase=false;

   protected String jndi_name=null;

   /**
    * Queue used to replicate updates when mode is repl-async
    */
   protected ReplicationQueue repl_queue=null;

   public static final String SEPARATOR="/";

   /**
    * Entries in the cache are by default local; ie. not replicated
    */
   public static final int LOCAL=1;

   /**
    * Entries in the cache are by default replicated (asynchronously)
    */
   public static final int REPL_ASYNC=2;

   /**
    * Entries in the cache are by default replicated (synchronously)
    */
   public static final int REPL_SYNC=3;

   static final String JNDI_PREFIX="/JBossCache/";

   static final String UNINITIALIZED="jboss:internal:uninitialized";

   static {
      try {
         put_data_method=TreeCache.class.getMethod("_put",
                                                   new Class[]{GlobalTransaction.class,
                                                               Fqn.class,
                                                               Map.class,
                                                               boolean.class});
         put_data_erase_method=TreeCache.class.getMethod("_put",
                                                         new Class[]{GlobalTransaction.class,
                                                                     Fqn.class,
                                                                     Map.class,
                                                                     boolean.class,
                                                                     boolean.class});
         put_key_val_method=TreeCache.class.getMethod("_put",
                                                      new Class[]{GlobalTransaction.class,
                                                                  Fqn.class,
                                                                  Object.class,
                                                                  Object.class,
                                                                  boolean.class});
         remove_node_method=TreeCache.class.getMethod("_remove",
                                                      new Class[]{GlobalTransaction.class,
                                                                  Fqn.class,
                                                                  boolean.class});
         remove_key_method=TreeCache.class.getMethod("_remove",
                                                     new Class[]{GlobalTransaction.class,
                                                                 Fqn.class,
                                                                 Object.class,
                                                                 boolean.class});
         remove_data_method=TreeCache.class.getMethod("_removeData",
                                                      new Class[]{GlobalTransaction.class,
                                                                  Fqn.class,
                                                                  boolean.class});
         prepare_method=TreeCache.class.getMethod("prepare",
                                                  new Class[]{GlobalTransaction.class,
                                                              List.class,
                                                              Address.class,
                                                              boolean.class});
         commit_method=TreeCache.class.getMethod("commit",
                                                 new Class[]{GlobalTransaction.class});
         rollback_method=TreeCache.class.getMethod("rollback",
                                                   new Class[]{GlobalTransaction.class});
         addChild_method=TreeCache.class.getMethod("_addChild",
                                                   new Class[]{GlobalTransaction.class,
                                                               Fqn.class, Object.class, Node.class});
         getKeyValueMethod=TreeCache.class.getMethod("_get",
                                                     new Class[]{Fqn.class, Object.class, boolean.class});
         getNodeMethod=TreeCache.class.getMethod("_get", new Class[]{Fqn.class});
         getKeysMethod=TreeCache.class.getMethod("_getKeys", new Class[]{Fqn.class});
         getChildrenNamesMethod=TreeCache.class.getMethod("_getChildrenNames", new Class[]{Fqn.class});
         replicate_method=TreeCache.class.getMethod("_replicate", new Class[]{MethodCall.class});
         replicate_all_method=TreeCache.class.getMethod("_replicate", new Class[]{List.class});
      }
      catch(NoSuchMethodException ex) {
         ex.printStackTrace();
         throw new ExceptionInInitializerError(ex.toString());
      }

      crud_methods.add(put_data_method);
      crud_methods.add(put_data_erase_method);
      crud_methods.add(put_key_val_method);
      crud_methods.add(remove_node_method);
      crud_methods.add(remove_key_method);
      crud_methods.add(remove_data_method);
   }

   public static boolean isCrudMethod(Method m) {
      return m == null? false : crud_methods.contains(m);
   }

   /**
    * Creates a channel with the given properties. Connects to the channel, then creates a PullPushAdapter
    * and starts it
    */
   public TreeCache(String cluster_name, String props, long state_fetch_timeout) throws Exception {
      super();
      if(cluster_name != null)
         this.cluster_name=cluster_name;
      if(props != null)
         this.cluster_props=props;
      this.state_fetch_timeout=state_fetch_timeout;
   }

   public TreeCache() throws Exception {
      super();
   }

   /**
    * Expects an already connected channel. Creates a PullPushAdapter and starts it
    */
   public TreeCache(JChannel channel) throws Exception {
      super();
      this.channel=channel;
   }

   /**
    * @return
    * @jmx.managed-attribute access="read-only"
    */
   public Object getLocalAddress() {
      return channel != null ? channel.getLocalAddress() : null;
   }

   /**
    * @return
    * @jmx.managed-attribute access="read-only"
    */
   public Vector getMembers() {
      return members;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public boolean isCoordinator() {
      return coordinator;
   }

   /**
    * Get the name of the replication group
    * @jmx.managed-attribute
    */
   public String getClusterName() {
      return cluster_name;
   }

   /**
    * Set the name of the replication group
    *
    * @jmx.managed-attribute
    */
   public void setClusterName(String name) {
      cluster_name=name;
   }

   /**
    * Get the cluster properties (e.g. the protocol stack specification in case of JGroups)
    *
    * @jmx.managed-attribute
    */
   public String getClusterProperties() {
      return cluster_props;
   }

   /**
    * Set the cluster properties. If the cache is to use the new properties,
    * it has to be redeployed
    *
    * @param cluster_props The properties for the cluster (JGroups)
    * @jmx.managed-attribute
    */
   public void setClusterProperties(String cluster_props) {
      this.cluster_props=cluster_props;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public String getInterceptorChain() {
      String retval=printInterceptorChain(interceptor_chain);
      if(retval == null || retval.length() == 0)
         return "<empty>";
      else
         return retval;
   }


   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public String getCacheLoaderClass() {
      return cache_loader_class;
   }

   /**
    *
    * @param cache_loader_class
    * @jmx.managed-attribute
    */
   public void setCacheLoaderClass(String cache_loader_class) {
      this.cache_loader_class=cache_loader_class;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public String getCacheLoaderConfig() {
      return cache_loader_config;
   }

   /**
    *
    * @param cache_loader_config
    * @jmx.managed-attribute
    */
   public void setCacheLoaderConfig(String cache_loader_config) {
      this.cache_loader_config=cache_loader_config;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public CacheLoader getCacheLoader() {
      return cache_loader;
   }

   /**
    *
    * @param cache_loader
    * @jmx.managed-attribute
    */
   public void setCacheLoader(CacheLoader cache_loader) {
      this.cache_loader=cache_loader;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public boolean getCacheLoaderShared() {
      return cache_loader_shared;
   }

   /**
    *
    * @param shared
    * @jmx.managed-attribute
    */
   public void setCacheLoaderShared(boolean shared) {
      this.cache_loader_shared=shared;
   }


   /**
    *
    * @param list
    * @jmx.managed-attribute
    */
   public void setCacheLoaderPreload(String list) {
      if(list == null) return;
      ArrayList l;
      StringTokenizer st=new StringTokenizer(list, ",");
      String tok;
      Fqn    fqn;
      l=new ArrayList();
      while(st.hasMoreTokens()) {
         tok=st.nextToken();
         fqn=Fqn.fromString(tok.trim());
         l.add(fqn);
      }
      if(l.size() > 0)
         this.cache_loader_preload=l;
   }



   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public String getCacheLoaderPreload() {
      return cache_loader_preload != null? cache_loader_preload.toString() : null;
   }

   /**
    *
    * @param flag
    * @jmx.managed-attribute
    */
   public void setCacheLoaderFetchPersistentState(boolean flag) {
      cache_loader_fetch_persistent_state=flag;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public boolean getCacheLoaderFetchPersistentState() {
      return cache_loader_fetch_persistent_state;
   }

   /**
    *
    * @param flag
    * @jmx.managed-attribute
    */
   public void setCacheLoaderFetchTransientState(boolean flag) {
      cache_loader_fetch_transient_state=flag;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public boolean getCacheLoaderFetchTransientState() {
      return cache_loader_fetch_transient_state;
   }


   /**
    * @return
    * @jmx.managed-attribute
    */
   public boolean getSyncCommitPhase() {
      return sync_commit_phase;
   }

   /**
    *
    * @param sync_commit_phase
    * @jmx.managed-attribute
    */
   public void setSyncCommitPhase(boolean sync_commit_phase) {
      this.sync_commit_phase=sync_commit_phase;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public boolean getSyncRollbackPhase() {
      return sync_rollback_phase;
   }

   /**
    *
    * @param sync_rollback_phase
    * @jmx.managed-attribute
    */
   public void setSyncRollbackPhase(boolean sync_rollback_phase) {
      this.sync_rollback_phase=sync_rollback_phase;
   }


   /**
    * Setup eviction policy configuration
    *
    * @jmx.managed-attribute access="write-only"
    */
   public void setEvictionPolicyConfig(Element config) {
      evictConfig_ = config;
      log.info("setEvictionPolicyConfig(): " +config);
   }

   /**
    */
   public Element getEvictionPolicyConfig() {
      return evictConfig_;
   }

   /**
    * Convert a list of elements to the JG property string
    *
    * @jmx.managed-attribute access="write-only"
    */
   public void setClusterConfig(Element config) {
      StringBuffer buffer=new StringBuffer();
      NodeList stack=config.getChildNodes();
      int length=stack.getLength();

      for(int s=0; s < length; s++) {
         org.w3c.dom.Node node=stack.item(s);
         if(node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE)
            continue;

         Element tag=(Element)node;
         String protocol=tag.getTagName();
         buffer.append(protocol);
         NamedNodeMap attrs=tag.getAttributes();
         int attrLength=attrs.getLength();
         if(attrLength > 0)
            buffer.append('(');
         for(int a=0; a < attrLength; a++) {
            Attr attr=(Attr)attrs.item(a);
            String name=attr.getName();
            String value=attr.getValue();
            buffer.append(name);
            buffer.append('=');
            buffer.append(value);
            if(a < attrLength - 1)
               buffer.append(';');
         }
         if(attrLength > 0)
            buffer.append(')');
         buffer.append(':');
      }
      // Remove the trailing ':'
      buffer.setLength(buffer.length() - 1);
      setClusterProperties(buffer.toString());
      log.info("setClusterConfig(): setting cluster properties " +
              "from xml to: " + cluster_props);
   }




   /**
    * Get the max time to wait until the initial state is retrieved.
    * This is used in a replicating cache: when a new cache joins the cluster,
    * it needs to acquire the (replicated) state of the other members to
    * initialize itself. If no state has been received within <tt>timeout</tt>
    * milliseconds, the map will be empty.
    *
    * @return long Number of milliseconds to wait for the state. 0 means to
    *         wait forever.
    * @jmx.managed-attribute
    */
   public long getInitialStateRetrievalTimeout() {
      return state_fetch_timeout;
   }

   /**
    * Set the initial state transfer timeout
    * (see {@link #getInitialStateRetrievalTimeout()})
    *
    * @jmx.managed-attribute
    */
   public void setInitialStateRetrievalTimeout(long timeout) {
      state_fetch_timeout=timeout;
   }

   /**
    * Returns the current caching mode. Valid values are
    * <ul>
    * <li>LOCAL
    * <li>REPL_ASYNC
    * <li>REPL_SYNC
    * <ul>
    *
    * @return String The caching mode
    * @jmx.managed-attribute
    */
   public String getCacheMode() {
      return mode2String(cache_mode);
   }

   public int getCacheModeInternal() {
      return cache_mode;
   }

   private String mode2String(int mode) {
      switch(mode) {
         case LOCAL:
            return "LOCAL";
         case REPL_ASYNC:
            return "REPL_ASYNC";
         case REPL_SYNC:
            return "REPL_SYNC";
         default:
            throw new RuntimeException("setCacheMode(): caching mode " + mode + " is invalid");
      }
   }

   /**
    * Sets the default caching mode)
    *
    * @jmx.managed-attribute
    */
   public void setCacheMode(String mode) throws Exception {
      int m=string2Mode(mode);
      setCacheMode(m);
   }


   /**
    * Sets the default cache mode. Valid arguments are
    * <ol>
    * <li>TreeCache.LOCAL
    * <li>TreeCache.REPL_ASYNC
    * <li>TreeCache.REPL_SYNC
    * </ol>
    *
    * @param mode
    */
   public void setCacheMode(int mode) {
      if(mode == LOCAL || mode == REPL_ASYNC || mode == REPL_SYNC)
         this.cache_mode=mode;
      else
         throw new IllegalArgumentException("setCacheMode(): caching mode " + mode + " is invalid");
   }


   /**
    * Returns the default max timeout after which synchronous replication calls
    * return.
    *
    * @return long Number of milliseconds after which a sync repl call must
    *         return. 0 means to wait forever
    * @jmx.managed-attribute
    */
   public long getSyncReplTimeout() {
      return sync_repl_timeout;
   }

   /**
    * Sets the default maximum wait time for synchronous replication to receive all
    * results
    *
    * @jmx.managed-attribute
    */
   public void setSyncReplTimeout(long timeout) {
      sync_repl_timeout=timeout;
   }

   /**
    * @return
    * @jmx.managed-attribute
    */
   public boolean getUseReplQueue() {
      return use_repl_queue;
   }

   /**
    * @param flag
    * @jmx.managed-attribute
    */
   public void setUseReplQueue(boolean flag) {
      use_repl_queue=flag;
      if(flag) {
         if(repl_queue == null) {
            repl_queue=new ReplicationQueue(this, repl_queue_interval, repl_queue_max_elements);
            if(repl_queue_interval >= 0)
               repl_queue.start();
         }
      }
      else {
         if(repl_queue != null) {
            repl_queue.stop();
            repl_queue=null;
         }
      }
   }

   /**
    * @return
    * @jmx.managed-attribute
    */
   public long getReplQueueInterval() {
      return repl_queue_interval;
   }

   /**
    * @param interval
    * @jmx.managed-attribute
    */
   public void setReplQueueInterval(long interval) {
      this.repl_queue_interval=interval;
      if(repl_queue != null)
         repl_queue.setInterval(interval);
   }

   /**
    * @return
    * @jmx.managed-attribute
    */
   public int getReplQueueMaxElements() {
      return repl_queue_max_elements;
   }

   /**
    * @param max_elements
    * @jmx.managed-attribute
    */
   public void setReplQueueMaxElements(int max_elements) {
      this.repl_queue_max_elements=max_elements;
      if(repl_queue != null)
         repl_queue.setMax_elements(max_elements);
   }


   public ReplicationQueue getReplQueue() {
      return repl_queue;
   }

   /**
    * Returns the transaction isolation level.
    *
    * @jmx.managed-attribute
    */
   public String getIsolationLevel() {
      return isolationLevel.toString();
   }

   /**
    * Set the transaction isolation level. This determines the locking strategy to be used
    *
    * @jmx.managed-attribute
    */
   public void setIsolationLevel(String level) {
      IsolationLevel tmp_level=IsolationLevel.stringToIsolationLevel(level);

      if(tmp_level == null) {
         throw new IllegalArgumentException("TreeCache.setIsolationLevel(): level \"" + level + "\" is invalid");
      }
      setIsolationLevel(tmp_level);
   }

   /**
    * @param level
    */
   public void setIsolationLevel(IsolationLevel level) {
      isolationLevel=level;
      if(isolationLevel != IsolationLevel.NONE)
         locking=true;
      else
         locking=false;
      LockStrategyFactory.setIsolationLevel(level);
   }

   /**
    * @return
    * @jmx.managed-attribute
    */
   public boolean getFetchStateOnStartup() {
      return fetch_state_on_startup;
   }


   /**
    * @param flag
    * @jmx.managed-attribute
    */
   public void setFetchStateOnStartup(boolean flag) {
      fetch_state_on_startup=flag;
   }


   /**
    * Default max time to wait for a lock. If the lock cannot be acquired
    * within this time, a LockingException will be thrown.
    *
    * @return long Max number of milliseconds to wait for a lock to be acquired
    * @jmx.managed-attribute
    */
   public long getLockAcquisitionTimeout() {
      return lock_acquisition_timeout;
   }

   /**
    * Set the max time for lock acquisition. A value of 0 means to wait forever (not recomended).
    * Note that lock acquisition timeouts may be removed in the future when we
    * have deadlock detection.
    *
    * @param timeout
    * @jmx.managed-attribute
    */
   public void setLockAcquisitionTimeout(long timeout) {
      this.lock_acquisition_timeout=timeout;
   }



   /**
    * Returns the name of the cache eviction policy (must be an implementation
    * of EvictionPolicy)
    *
    * @return Fully qualified name of a class implementing the EvictionPolicy
    *         interface
    * @jmx.managed-attribute
    */
   public String getEvictionPolicyClass() {
      return eviction_policy_class;
   }

   /**
    * Sets the classname of the eviction policy
    *
    * @jmx.managed-attribute
    */
   public void setEvictionPolicyClass(String eviction_policy_class) {
      if(eviction_policy_class == null || eviction_policy_class.length() ==0)
         return;
      try {
         this.eviction_policy_class=eviction_policy_class;
         eviction_policy_provider =(TreeCacheListener)
               getClass().getClassLoader().loadClass(eviction_policy_class).newInstance();
         this.addTreeCacheListener(eviction_policy_provider );
      }
      catch(Throwable t) {
         log.error("setEvictionPolicyClass(): failed creating instance of  " + eviction_policy_class, t);
      }
   }

   /**
    * Obtain eviction thread (if any) wake up interval in seconds
    *
    * @jmx.managed-attribute
    */
   public int getEvictionThreadWakeupIntervalSeconds() {
      if( eviction_policy_provider == null ) return -1;
      else
         return ((LRUPolicy)eviction_policy_provider).getWakeupIntervalSeconds();
   }
   /**
    * Currently not used.
    *
    * @param lm
    */
   public void setLockManager(LockManager lm) {
      if(lm != null)
         this.lock_mgr=lm;
   }

   /**
    * Sets the lock manager. The empty constructor will be called. The class
    * needs to implement the <tt>LockManager</tt> interface. Currently not used.
    *
    * @param class_name
    */
   public void setLockManagerClass(String class_name) {
      if(class_name == null)
         return;
      try {
         LockManager new_lm;
         new_lm=(LockManager)getClass().getClassLoader().loadClass(class_name).newInstance();
         this.lock_mgr=new_lm;
      }
      catch(Throwable t) {
         log.error("setLockManager(): failed creating instance of  " + class_name, t);
      }
   }

   /**
    * Sets the TransactionManagerLookup object
    *
    * @param l
    * @jmx.managed-attribute
    */
   public void setTransactionManagerLookup(TransactionManagerLookup l) {
      this.tm_lookup=l;
   }


   /**
    * @return
    * @jmx.managed-attribute
    */
   public String getTransactionManagerLookupClass() {
      return tm_lookup_class;
   }

   /**
    * Sets the class of the TransactionManagerLookup impl. This will attempt to create an
    * instance, and will throw an exception if this fails.
    *
    * @param cl
    * @throws Exception
    * @jmx.managed-attribute
    */
   public void setTransactionManagerLookupClass(String cl) throws Exception {
      this.tm_lookup_class=cl;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public TransactionManager getTransactionManager() {
      return tm;
   }

   /**
    *
    * @return
    * @jmx.managed-attribute
    */
   public String getJndiName() {
      return jndi_name;
   }

   /**
    *
    * @param jndi_name
    * @jmx.managed-attribute
    */
   public void setJndiName(String jndi_name) throws NamingException {
      if(this.jndi_name != null && jndi_name != null && !this.jndi_name.equals(jndi_name))
         unbindFromJndi(this.jndi_name);
      this.jndi_name=jndi_name;
      bindToJndi();
   }


   public void setReplicationHandler(Replicatable handler) {
      replication_handler=handler;
   }

   public Replicatable getReplicationHandler() {
      return replication_handler;
   }

   /**
    * Fetch the group state from the current coordinator. If successful,
    * this will trigger setState().
    *
    * @jmx.managed-operation
    */
   public void fetchState(long timeout) throws ChannelClosedException, ChannelNotConnectedException {
      if(channel == null)
         throw new ChannelNotConnectedException();
      boolean rc=channel.getState(null, timeout);
      if(rc)
         log.info("fetchState(): state was retrieved successfully");
      else
         log.info("fetchState(): state could not be retrieved (first member)");
   }

   /**
    * Takes the entire transient state and stores all of it in persistent storage using
    * the CacheLoader (if set). This will overwrite the state already in the persistent backend store.
    * @throws Exception
    */
   public void dumpStateToCacheLoader() throws Exception {
      throw new UnsupportedOperationException("not yet implemented");
   }

   /**
    * @param listener
    * @jmx.managed-operation
    */
   public void addTreeCacheListener(TreeCacheListener listener) {
      if(!listeners.contains(listener))
         listeners.addElement(listener);
   }

   /**
    * @param listener
    * @jmx.managed-operation
    */
   public void removeTreeCacheListener(TreeCacheListener listener) {
      listeners.removeElement(listener);
   }

   /* --------------------------- MBeanSupport ------------------------- */

   /**
    *
    * @throws Exception
    * @jmx.managed-operation
    */
   public void createService() throws Exception {
      ;
   }

   /**
    * @jmx.managed-operation
    */
   public void destroyService() {
      ;
   }


   /**
    *
    * @throws Exception
    * @jmx.managed-operation
    */
   public void startService() throws Exception {
      if(this.tm_lookup == null && this.tm_lookup_class != null) {
         Class clazz=Thread.currentThread().getContextClassLoader().loadClass(this.tm_lookup_class);
         this.tm_lookup=(TransactionManagerLookup)clazz.newInstance();
      }

      try {
         if(tm_lookup != null)
            tm=tm_lookup.getTransactionManager();
         else
            log.warn("No transaction manager lookup class has been defined. TX will be null");
      }
      catch(Exception e) {
         log.debug("failed looking up TransactionManager, will not use transactions", e);
      }

      // Create the interceptors in the correct order (later to be defined in XML file)
      createInterceptorChain();

      // Create the cache loader (needs to be created regardless of mode (local/replicated). This method will
      // be a no-op if no cache loader has been configured
      try {
         createCacheLoader();
      }
      catch(Throwable t) {
         log.error("failed creating the cache loader", t);
      }

      try {
         createEvictionPolicy();
      }
      catch(Throwable t) {
         log.error("failed creating the eviction policy", t);
      }

      switch(cache_mode) {
         case LOCAL:
            log.info("cache mode is local, will not create the channel");
            break;
         case REPL_SYNC:
         case REPL_ASYNC:
            log.info("cache mode is " + mode2String(cache_mode));
            if(channel != null) { // already started
               log.info("channel is already running");
               return;
            }
            if(cluster_props == null) {
               cluster_props=getDefaultProperties();
               log.debug("setting cluster properties to default value");
            }

            try {
               channel=new JChannel(cluster_props);
               if(log.isTraceEnabled())
                  log.trace("cache properties: " + cluster_props);
               channel.setOpt(Channel.GET_STATE_EVENTS, Boolean.TRUE);
               disp=new RpcDispatcher(channel, ml, this, this);
               channel.connect(cluster_name);
               local_addr=channel.getLocalAddress();
               if(fetch_state_on_startup) {
                  fetchStateOnStartup();
               }
            }
            catch (Exception ex) {
               notifyCacheStopped();   // in case something goes wrong here...
            }
            break;
         default:
            throw new IllegalArgumentException("cache mode " + cache_mode + " is invalid");
      }

      coordinator=determineCoordinator();
      notifyCacheStarted();
   }

   private void createEvictionPolicy() {
      // Configure if eviction policy is set
      if(eviction_policy_provider != null)
         ((LRUPolicy)eviction_policy_provider).configure(this);
   }

   /**
    * Assembles the interceptor stack. Presence and order of interceptors is determined by looking at
    * the cache configuration. In the future, this will be configured through XML.
    * Creates either:
    * <pre>
    * CallInterceptor
    * [ReplicationInterceptor]
    * [CacheLoaderInterceptor]
    *
    * or
    *
    * CallInterceptor
    * [CacheLoaderInterceptor]
    * [ReplicationInterceptor]
    * </pre>
    * CallInterceptor is always present at the top, the other two may or may not be present
    */
   protected void createInterceptorChain() throws IllegalAccessException, InstantiationException, ClassNotFoundException {
      Interceptor call_interceptor=null;
      Interceptor repl_interceptor=null;
      Interceptor cache_loader_interceptor=null;
      Interceptor tx_interceptor=null;
      Interceptor first=null;

      call_interceptor=createInterceptor("org.jboss.cache.interceptors.CallInterceptor");
      call_interceptor.setCache(this);

      if(cache_mode != LOCAL) {
         repl_interceptor=createInterceptor("org.jboss.cache.interceptors.ReplicationInterceptor");
         repl_interceptor.setCache(this);
      }

      if(tm != null) {
         tx_interceptor=createInterceptor("org.jboss.cache.interceptors.TransactionInterceptor");
         tx_interceptor.setCache(this);
      }

      if(cache_loader_class != null || cache_loader != null) {
         cache_loader_interceptor=createInterceptor("org.jboss.cache.interceptors.CacheLoaderInterceptor");
         cache_loader_interceptor.setCache(this);
      }

      if(tx_interceptor != null) {
         if(first == null)
            first=tx_interceptor;
         else
            addInterceptor(first, tx_interceptor);
      }

      if(cache_loader_interceptor != null && cache_loader_shared) {
         if(first == null)
            first=cache_loader_interceptor;
         else
            addInterceptor(first, cache_loader_interceptor);
      }

      if(repl_interceptor != null) {
         if(first == null)
            first=repl_interceptor;
         else
            addInterceptor(first, repl_interceptor);
      }

      if(cache_loader_interceptor != null && !cache_loader_shared) {
         if(first == null)
            first=cache_loader_interceptor;
         else
            addInterceptor(first, cache_loader_interceptor);
      }

      if(call_interceptor != null) {
         if(first == null)
            first=call_interceptor;
         else
            addInterceptor(first, call_interceptor);
      }

      interceptor_chain=first;
      if(log.isInfoEnabled())
         log.info("interceptor chain is:\n" + printInterceptorChain(first));
   }

   private String printInterceptorChain(Interceptor i) {
      StringBuffer sb=new StringBuffer();
      if(i != null) {
         if(i.getNext() != null) {
            sb.append(printInterceptorChain(i.getNext())).append("\n");
         }
         sb.append(i.getClass());
      }
      return sb.toString();
   }


   /** Adds an interceptor at the end of the chain */
   void addInterceptor(Interceptor first, Interceptor i) {
      if(first == null)
         return;
      do {
         if(first.getNext() != null)
            first=first.getNext();
         else
            break;
      }
      while(first != null); // findbugs has a false positive for an NPE here...
      first.setNext(i);
   }


   Interceptor createInterceptor(String classname) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
      Class clazz=getClass().getClassLoader().loadClass(classname);
      return (Interceptor)clazz.newInstance();
   }


   /**
    * Copied from Cluster codebase
    */
   void bindToJndi() throws NamingException {
      Context ctx=null;

      if(jndi_name == null || jndi_name.trim().length() == 0)
         return;

      try {
         ctx=new InitialContext();
         if(ctx == null) {
            log.info("no JNDI found (assuming we're running standalone");
            return;
         }

         NonSerializableFactory.bind(jndi_name, this);
         Name n=ctx.getNameParser("").parse(jndi_name);
         while(n.size() > 1) {
            String ctxName=n.get(0);
            try {
               ctx=(Context)ctx.lookup(ctxName);
            }
            catch(NameNotFoundException e) {
               log.debug("creating Subcontext" + ctxName);
               ctx=ctx.createSubcontext(ctxName);
            }
            n=n.getSuffix(1);
         }

         // The helper class NonSerializableFactory uses address type nns, we go on to
         // use the helper class to bind the service object in JNDI
         //
         StringRefAddr addr=new StringRefAddr("nns", jndi_name);
         Reference ref=new Reference(this.getClass().getName(), addr, NonSerializableFactory.class.getName(), null);
         ctx.bind(n.get(0), ref);
      }
      finally {
         if(ctx != null)
            ctx.close();
      }
   }

   void unbindFromJndi(String key) throws NamingException {
      Context ctx=null;
      try {
         ctx=new InitialContext();
         if(ctx == null) {
            log.info("no JNDI found (assuming we're running standalone");
            return;
         }
         ctx.unbind(key);
         NonSerializableFactory.unbind(key);
      }
      finally {
         ctx.close();
      }
   }

   /** Creates an instance of a CacheLoader if and only if
    * <ul>
    * <li>The CacheLoader has not yet been created
    * <li>cache_loader_class is set
    * <li>The CacheLoader is shared and we are <em>not</em> the coordinator (only the coordinator
    *     is supposed to have a CacheLoader)
    * </ul>
    * @throws Exception
    */
   protected void createCacheLoader() throws Exception {
      if(cache_loader == null && cache_loader_class != null) {
         Class cl=Thread.currentThread().getContextClassLoader().loadClass(cache_loader_class);
         cache_loader=(CacheLoader)cl.newInstance();
         cache_loader.setConfig(cache_loader_config);
         cache_loader.setCache(this);
         cache_loader.create();
         cache_loader.start();
         if(cache_loader_preload != null) {
            for(Iterator it=cache_loader_preload.iterator(); it.hasNext();) {
               Fqn fqn=(Fqn)it.next();
               preload(fqn, true, true);
            }
         }
      }
   }

   /**
    * Loads the indicated Fqn, plus all parents recursively from the CacheLoader. If now CacheLoader is present,
    * this is a no-op
    * @param fqn
    * @throws Exception
    * @jmx.managed-operation
    */
   public void load(String fqn) throws Exception {
      if(cache_loader != null)
         preload(Fqn.fromString(fqn), true, true);
   }

   void preload(Fqn fqn, boolean preload_parents, boolean preload_children) throws Exception {

      // 1. Load the attributes first
      this.get(fqn, "bla");

      // 2. Then load the parents
      if(preload_parents) {
         Fqn tmp_fqn=new Fqn();
         for(int i=0; i < fqn.size()-1; i++) {
            tmp_fqn=new Fqn(tmp_fqn, fqn.get(i));
            this.get(tmp_fqn, "bla");
         }
      }

      if(preload_children == false)
         return;

      // 3. Then recursively for all child nodes, preload them as well
      Set children=cache_loader.getChildrenNames(fqn);
      if(children == null)
         return;
      for(Iterator it=children.iterator(); it.hasNext();) {
         String child_name=(String)it.next();
         Fqn child_fqn=new Fqn(fqn, child_name);
         preload(child_fqn, false, true);
      }
   }

   void destroyCacheLoader() {
      if(cache_loader != null) {
         cache_loader.stop();
         cache_loader.destroy();
         cache_loader=null;
      }
   }

   protected boolean determineCoordinator() {
      if(channel == null)
         return false;
      Object local_addr=getLocalAddress();
      if(local_addr == null)
         return false;
      View view=channel.getView();
      if(view == null) return false;
      ViewId vid=view.getVid();
      if(vid == null) return false;
      Object coord=vid.getCoordAddress();
      if(coord == null) return false;
      if(local_addr.equals(coord))
         return true;
      else
         return false;
   }

   protected void fetchStateOnStartup() throws Exception {
      long start, stop;
      synchronized(stateLock) {
         isStateSet=false;
         start=System.currentTimeMillis();
         boolean rc=channel.getState(null, state_fetch_timeout);
         if(rc) {
            while(!isStateSet) {
               try {
                  stateLock.wait();
               }
               catch(InterruptedException iex) {
               }
            }
            stop=System.currentTimeMillis();
            log.info("state was retrieved successfully (in " + (stop-start) + " milliseconds");
         }
         else
            log.info("state could not be retrieved (must be first member in group)");
      }
   }

   /**
    * @jmx.managed-operation
    */
   public void stopService() {
      if(channel != null) {
         log.info("stopService(): closing the channel");
         channel.close();
         channel=null;
      }
      if(disp != null) {
         log.info("stopService(): stopping the dispatcher");
         disp.stop();
         disp=null;
      }
      if(members != null && members.size() > 0)
         members.clear();

      if(repl_queue != null)
         repl_queue.stop();

      destroyCacheLoader();

      if(jndi_name!= null) {
         try {
            unbindFromJndi(this.jndi_name);
         }
         catch(NamingException e) {
            log.error("failed unbinding from JNDI", e);
         }
      }

      notifyCacheStopped();
   }

   /* ----------------------- End of MBeanSupport ----------------------- */

   /**
    * @param fqn fqn String name to retrieve from cache
    * @return Node corresponding to the fqn. Null if does not exist. No guarantees wrt replication,
    * cache loading are given if the underlying node is modified
    */
   public Node get(String fqn) throws CacheException {
      return get(Fqn.fromString(fqn));
   }

   /**
    * @param fqn fqn instance to retrieve from cache
    * @return Node corresponding to the fqn. Null if does not exist. No guarantees wrt replication,
    * cache loading are given if the underlying node is modified
    */
   public Node get(Fqn fqn) throws CacheException {
      MethodCall m=new MethodCall(getNodeMethod, new Object[]{fqn});
      return (Node)invokeMethod(m);
   }

   public Node _get(Fqn fqn) throws CacheException {
      try {
         return findNode(fqn, NODE_READ);
      }
      finally {
         // release locks since no tx context
         if(locking == true && getCurrentTransaction() == null)
            releaseLocks(null, fqn);
      }
   }


   /**
    * @param fqn
    * @return
    * @jmx.managed-operation
    */
   public Set getKeys(String fqn) throws CacheException {
      return getKeys(Fqn.fromString(fqn));
   }

   /**
    * @param fqn
    * @return A Set<String> of keys. This is an unmodifiable set, any attempt to modify it will throw
    * an exception. Returns null if the node is not found, or the node has no attributes
    * @jmx.managed-operation
    */
   public Set getKeys(Fqn fqn) throws CacheException {
      MethodCall m=new MethodCall(getKeysMethod, new Object[]{fqn});
      return (Set)invokeMethod(m);
   }


   public Set _getKeys(Fqn fqn) throws CacheException {
      Set retval=null;
      try {
         Node n=findNode(fqn, NODE_READ);
         if(n == null)
            return null;

         if(n.containsKey(UNINITIALIZED))
            loadAttributesFromCacheLoader(n);
         retval=n.getDataKeys();
         return retval != null? Collections.unmodifiableSet(retval) : null;
      }
      finally {
         // release locks since no tx context
         if(locking == true && getCurrentTransaction() == null)
            releaseLocks(null, fqn);
      }
   }

   /**
    * Finds a node given its name and returns the value associated with a given key in its <code>data</code>
    * map. Returns null if the node was not found in the tree or the key was not found in the hashmap.
    *
    * @param fqn The fully qualified name of the node.
    * @param key The key.
    * @jmx.managed-operation
    */
   public Object get(String fqn, Object key) throws CacheException {
      return get(Fqn.fromString(fqn), key);
   }


   /**
    * Finds a node given its name and returns the value associated with a given key in its <code>data</code>
    * map. Returns null if the node was not found in the tree or the key was not found in the hashmap.
    *
    * @param fqn The fully qualified name of the node.
    * @param key The key.
    * @jmx.managed-operation
    */
   public Object get(Fqn fqn, Object key) throws CacheException {
      return get(fqn, key, true);
   }

   public Object _get(Fqn fqn, Object key, boolean sendNodeEvent) throws CacheException {
      if(log.isDebugEnabled())
         log.debug("get(" + ", \"" + fqn + "\", " + key + ", \"" +sendNodeEvent +"\")");
      try {
         Node n=findNode(fqn, NODE_READ);
         if(n == null) return null;

         if(n.containsKey(UNINITIALIZED))
            loadAttributesFromCacheLoader(n);

         if(sendNodeEvent)
            notifyNodeVisisted(fqn);
         return n.get(key);
      }
      finally {
         // release locks since no tx context
         if(locking == true && getCurrentTransaction() == null)
            releaseLocks(null, fqn);
      }
   }


   protected Object get(Fqn fqn, Object key, boolean sendNodeEvent) throws CacheException {
      MethodCall m=new MethodCall(getKeyValueMethod, new Object[]{fqn, key, new Boolean(sendNodeEvent)});
      return invokeMethod(m);
   }

   /**
    * Like <code>get()</code> method but without triggering a node visit event. This is used
    * to prevent refresh of the cache data in the eviction policy.
    * @param fqn
    * @param key
    * @return
    */
   public Object peek(Fqn fqn, Object key) throws CacheException {
      return get(fqn, key, false);
   }



   /**
    * Checks whether a given node exists in the tree
    *
    * @param fqn The fully qualified name of the node
    * @return boolean Whether or not the node exists
    * @jmx.managed-operation
    */
   public boolean exists(String fqn) {
      return exists(Fqn.fromString(fqn));
   }


   /**
    * Checks whether a given node exists in the tree. Does not acquire any locks in doing so (result may be dirty read)
    * @param fqn The fully qualified name of the node
    * @return boolean Whether or not the node exists
    * @jmx.managed-operation
    */
   public boolean exists(Fqn fqn) {
      Node n=getInternal(fqn);
      return n != null;
   }

   /**
    * Gets node without attempt to load it from CacheLoader if not present
    * @param fqn
    * @return
    */
   protected Node getInternal(Fqn fqn) {
      if(fqn == null || fqn.size() == 0) return root;
      Node n=root, retval=null;
      Object obj;
      for(int i=0; i < fqn.size(); i++) {
         obj=fqn.get(i);
         n=n.getChild(obj);
         if(n == null)
            return null;
         else
            retval=n;
      }
      return retval;
   }


   /**
    *
    * @param fqn
    * @param key
    * @return
    * @jmx.managed-operation
    */
   public boolean exists(String fqn, Object key) {
      return exists(Fqn.fromString(fqn), key);
   }


   /**
    * Checks whether a given key exists in the given node. Does not interact with CacheLoader, so the behavior is
    * different from {@link #get(Fqn,Object)}
    * @param fqn The fully qualified name of the node
    * @param key
    * @return boolean Whether or not the node exists
    * @jmx.managed-operation
    */
   public boolean exists(Fqn fqn, Object key) {
      Node n=getInternal(fqn);
      if(n == null)
         return false;
      else
         return n.containsKey(key);
    }


   /**
    * Adds a new node to the tree and sets its data. If the node doesn not yet exist, it will be created.
    * Also, parent nodes will be created if not existent. If the node already has data, then the new data
    * will override the old one. If the node already existed, a nodeModified() notification will be generated.
    * Otherwise a nodeCreated() motification will be emitted.
    *
    * @param fqn  The fully qualified name of the new node
    * @param data The new data. May be null if no data should be set in the node.
    * @jmx.managed-operation
    */
   public void put(String fqn, Map data) throws CacheException {
      put(Fqn.fromString(fqn), data);
   }

   /**
    * Adds a new node to the tree and sets its data. If the node doesn not yet exist, it will be created.
    * Also, parent nodes will be created if not existent. If the node already has data, then the new data
    * will override the old one. If the node already existed, a nodeModified() notification will be generated.
    * Otherwise a nodeCreated() motification will be emitted.
    *
    * @param fqn  The fully qualified name of the new node
    * @param data The new data. May be null if no data should be set in the node.
    * @jmx.managed-operation
    */
   public void put(Fqn fqn, Map data) throws CacheException {
      GlobalTransaction tx=getCurrentTransaction();
      MethodCall m=new MethodCall(put_data_method, new Object[]{tx, fqn, data, Boolean.TRUE});
      invokeMethod(m);
   }

   /**
    * Adds a key and value to a given node. If the node doesn't exist, it will be created. If the node
    * already existed, a nodeModified() notification will be generated. Otherwise a
    * nodeCreated() motification will be emitted.
    *
    * @param fqn   The fully qualified name of the node
    * @param key   The key
    * @param value The value
    * @return Object The previous value (if any), if node was present
    * @jmx.managed-operation
    */
   public Object put(String fqn, Object key, Object value) throws CacheException {
      return put(Fqn.fromString(fqn), key, value);
   }

   /**
    * Adds a key and value to a given node. If the node doesn't exist, it will be created. If the node
    * already existed, a nodeModified() notification will be generated. Otherwise a
    * nodeCreated() motification will be emitted.
    *
    * @param fqn   The fully qualified name of the node
    * @param key   The key
    * @param value The value
    * @return Object The previous value (if any), if node was present
    * @jmx.managed-operation
    */
   public Object put(Fqn fqn, Object key, Object value) throws CacheException {
      GlobalTransaction tx=getCurrentTransaction();
      MethodCall m=new MethodCall(put_key_val_method, new Object[]{tx, fqn, key, value, Boolean.TRUE});
      return invokeMethod(m);
   }

   /**
    * Removes the node from the tree.
    *
    * @param fqn The fully qualified name of the node.
    * @jmx.managed-operation
    */
   public void remove(String fqn) throws CacheException {
      remove(Fqn.fromString(fqn));
   }

   /**
    * Removes the node from the tree.
    *
    * @param fqn The fully qualified name of the node.
    * @jmx.managed-operation
    */
   public void remove(Fqn fqn) throws CacheException {
      GlobalTransaction tx=getCurrentTransaction();
      MethodCall m=new MethodCall(remove_node_method, new Object[]{tx, fqn, Boolean.TRUE});
      invokeMethod(m);
   }

   /**
    * Called by eviction policy provider. Note that eviction is done only in local mode,
    * that is, it doesn't replicate the node removal. This is will cause the replcation nodes
    * not synchronizing, but it is ok since user is supposed to add the node again when get is
    * null. After that, the contents will be in sync.
    * @param fqn Will remove everythign assoicated with this fqn.
    * @throws CacheException
    * @jmx.managed-operation
    */
   public void evict(Fqn fqn) throws CacheException {
      if(!exists(fqn)) return;   // node does not exist. Maybe it has been recursively removed.
      // use remove method now if there is a child node. Otherwise, it is removed
      boolean create_undo_ops = false;
      boolean sendNodeEvent = false;
      boolean eviction=true;
      if(hasChild(fqn)) {
         _removeData(null, fqn, create_undo_ops, sendNodeEvent, eviction);
      }
      else {
         _remove(null, fqn, create_undo_ops, sendNodeEvent, eviction);
      }
   }


   /**
    * Evicts a key/value pair from a node's attributes. Note that this is <em>local</em>, will not be replicated.
    * @param fqn
    * @param key
    * @throws CacheException
    * @jmx.managed-operation
    */
   public void evict(Fqn fqn, Object key) throws CacheException {
      if(!exists(fqn)) return;
      boolean create_undo_ops = false;
      boolean sendNodeEvent = false;
      boolean eviction=true;
      _removeData(null, fqn, create_undo_ops, sendNodeEvent, eviction);
   }




   /**
    * Removes <code>key</code> from the node's hashmap
    *
    * @param fqn The fullly qualified name of the node
    * @param key The key to be removed
    * @return The previous value, or null if none was associated with the given key
    * @jmx.managed-operation
    */
   public Object remove(String fqn, Object key) throws CacheException {
      return remove(Fqn.fromString(fqn), key);
   }

   /**
    * Removes <code>key</code> from the node's hashmap
    *
    * @param fqn The fullly qualified name of the node
    * @param key The key to be removed
    * @return The previous value, or null if none was associated with the given key
    * @jmx.managed-operation
    */
   public Object remove(Fqn fqn, Object key) throws CacheException {
      GlobalTransaction tx=getCurrentTransaction();
      MethodCall m=new MethodCall(remove_key_method, new Object[]{tx, fqn, key, Boolean.TRUE});
      return invokeMethod(m);
   }

   /**
    *
    * @param fqn
    * @throws CacheException
    * @jmx.managed-operation
    */
   public void removeData(String fqn) throws CacheException {
      removeData(Fqn.fromString(fqn));
   }

   /**
    *
    * @param fqn
    * @throws CacheException
    * @jmx.managed-operation
    */
   public void removeData(Fqn fqn) throws CacheException {
      GlobalTransaction tx=getCurrentTransaction();
      MethodCall m=new MethodCall(remove_data_method, new Object[]{tx, fqn, Boolean.TRUE});
      invokeMethod(m);
   }

   /**
    * Force-releases all locks in this node and the entire subtree
    *
    * @param fqn
    * @jmx.managed-operation
    */
   public void releaseAllLocks(String fqn) {
      releaseAllLocks(Fqn.fromString(fqn));
   }

   /**
    * Force-releases all locks in this node and the entire subtree
    *
    * @param fqn
    * @jmx.managed-operation
    */
   public void releaseAllLocks(Fqn fqn) {
      Node n;

      try {
         n=findNode(fqn, false, false, // doesn't acquire any locks
                    null, NODE_READ);
         if(n == null) {
            log.error("releaseAllLocks(): node " + fqn + " not found");
            return;
         }
         n.releaseAllForce();
      }
      catch(Throwable t) {
         log.error("releaseAllLocks(): failed", t);
      }
   }

   /**
    * Prints a representation of the node defined by <code>fqn</code>.
    * Output includes name, fqn and data.
    *
    * @jmx.managed-operation
    */
   public String print(String fqn) {
      return print(Fqn.fromString(fqn));
   }

   /**
    * Prints a representation of the node defined by <code>fqn</code>.
    * Output includes name, fqn and data.
    *
    * @jmx.managed-operation
    */
   public String print(Fqn fqn) {
      try {
         Node n=findNode(fqn, false, false, // doesn't acquire any locks
                         null, NODE_READ);
         if(n == null) return null;
         return n.toString();
      }
      catch(Throwable t) {
         return null;
      }
   }


   /**
    * Returns all children of a given node
    *
    * @param fqn The fully qualified name of the node
    * @return Set A list of child names (as Strings)
    * @jmx.managed-operation
    */
   public Set getChildrenNames(String fqn) throws CacheException {
      return getChildrenNames(Fqn.fromString(fqn));
   }

   /**
    * Returns all children of a given node
    *
    * @param fqn The fully qualified name of the node
    * @return Set A list of child names (as Strings). Must <em>not</em> be modified because this would
    * modify the underlying node directly (will throw an exception if modification is attempted). Returns null
    * of the parent node was not found, or if there are no children
    * @jmx.managed-operation
    */
   public Set getChildrenNames(Fqn fqn) throws CacheException {
      MethodCall m=new MethodCall(getChildrenNamesMethod, new Object[]{fqn});
      return (Set)invokeMethod(m);
   }

   public Set _getChildrenNames(Fqn fqn) throws CacheException {
      try {
         Node n=findNode(fqn, NODE_READ);
         if(n == null) return null;
         Map m=n.getChildren();

         // if null try the CacheLoader first (if registered)
         if(m == null && cache_loader != null) {
            Set children_names=null;
            try {
               children_names=cache_loader.getChildrenNames(fqn);
            }
            catch(Exception e) {
               log.error("failed getting the children names for " + fqn + " from the cache loader", e);
            }
            if(children_names != null) {
               // create one Node per child, don't load attributes yet (marked as UNINITIALIZED)
               for(Iterator it=children_names.iterator(); it.hasNext();) {
                  String child_name=(String)it.next();
                  Fqn child_fqn=new Fqn(n.getFqn(), child_name);
                  n.createChild(child_name, child_fqn, n, UNINITIALIZED, null);
               }
               return Collections.unmodifiableSet(children_names); // todo: change to preload nodes into memory
            }
         }

         if(m != null)
            return Collections.unmodifiableSet(m.keySet());
         else
            return null;
      }
      finally {
         if(locking == true && getCurrentTransaction() == null) {
            releaseLocks(null, fqn);
         }
      }
   }



   public boolean hasChild(Fqn fqn) {
      if(fqn == null) return false;

      Node n=root;
      Object obj;
      for(int i=0; i < fqn.size(); i++) {
         obj=fqn.get(i);
         n=n.getChild(obj);
         if(n == null)
            return false;
      }
      return n.hasChild();
   }

   /**
    * @return
    * @jmx.managed-operation
    */
   public String toString() {
      StringBuffer sb=new StringBuffer();
      int indent=0;
      Map children;

      children=root.getChildren();
      if(children != null && children.size() > 0) {
         Collection nodes=children.values();
         for(Iterator it=nodes.iterator(); it.hasNext();) {
            ((Node)it.next()).print(sb, indent);
            sb.append("\n");
         }
      }
      else
         sb.append(SEPARATOR);
      return sb.toString();
   }

   /**
    * @return
    * @jmx.managed-operation
    */
   public String printDetails() {
      StringBuffer sb=new StringBuffer();
      int indent=0;
      Map children;

      children=root.getChildren();
      if(children != null && children.size() > 0) {
         Collection nodes=children.values();
         for(Iterator it=nodes.iterator(); it.hasNext();) {
            ((Node)it.next()).printDetails(sb, indent);
            sb.append("\n");
         }
      }
      else
         sb.append(SEPARATOR);
      return sb.toString();
   }

   /**
    * @return
    * @jmx.managed-operation
    */
   public String printLockInfo() {
      StringBuffer sb=new StringBuffer("\n");
      int indent=0;
      Map children;

      children=root.getChildren();
      if(children != null && children.size() > 0) {
         Collection nodes=children.values();
         for(Iterator it=nodes.iterator(); it.hasNext();) {
            ((Node)it.next()).printLockInfo(sb, indent);
            sb.append("\n");
         }
      }
      else
         sb.append(SEPARATOR);
      return sb.toString();
   }

   /**
    * Gets the number of read or write locks held across the entire tree
    * @return
    */
   public int getNumberOfLocksHeld() {
      return numLocks(root);
   }


   int numLocks(Node n) {
      int num=0;
      Map children;
      if(n.isLocked())
         num++;
      if((children=n.getChildren()) != null) {
         for(Iterator it=children.values().iterator(); it.hasNext();) {
            num+=numLocks((Node)it.next());
         }
      }
      return num;
   }


   /**
    * Prints the entire configuration (all variables). Uses reflection to do so
    */
   public String printInfo() {
      StringBuffer sb=new StringBuffer();
      Class cl=getClass();
      Field[] fields=cl.getDeclaredFields();

      for(int i=0; i < fields.length; i++) {
         Field f=fields[i];
         sb.append(f.getName()).append(": ");
         try {
            sb.append(f.get(this));
         }
         catch(Exception e) {
            e.printStackTrace();
         }
         sb.append("\n");
      }

      return sb.toString();
   }

   /* ---------------------- Remote method calls -------------------- */

   public List callRemoteMethods(Vector mbrs, MethodCall method_call,
                                 boolean synchronous, boolean exclude_self, long timeout)
           throws Exception {
      RspList rsps;
      Rsp rsp;
      List retval;
      Vector validMembers;
      int mode=synchronous ? GroupRequest.GET_ALL : GroupRequest.GET_NONE;

      if(disp == null)
         return null;

      validMembers=mbrs != null ? new Vector(mbrs) : new Vector(this.members);
      if(exclude_self && validMembers != null && validMembers.size() > 0) {
         Object local_addr=getLocalAddress();
         if(local_addr != null)
            validMembers.remove(local_addr);
      }
      if(validMembers.size() == 0) {
         if(log.isDebugEnabled())
            log.debug("callRemoteMethods(): destination list is empty, discarding call");
         return null;
      }

      if(log.isDebugEnabled())
         log.debug("callRemoteMethods(): valid members are " + validMembers);

      rsps=disp.callRemoteMethods(validMembers, method_call, mode, timeout);
      if(rsps == null)
         return null;
      retval=new ArrayList(rsps.size());
      for(int i=0; i < rsps.size(); i++) {
         rsp=(Rsp)rsps.elementAt(i);
         if(rsp.wasSuspected() || !rsp.wasReceived())
            retval.add(new TimeoutException("rsp=" + rsp));
         else
            retval.add(rsp.getValue());
      }
      return retval;
   }


   /**
    * @param members
    * @param method
    * @param args
    * @param synchronous
    * @param exclude_self
    * @param timeout
    * @return
    * @throws Exception
    * @jmx.managed-operation
    */
   public List callRemoteMethods(Vector members, Method method, Object[] args,
                                 boolean synchronous, boolean exclude_self, long timeout)
           throws Exception {
      return callRemoteMethods(members, new MethodCall(method, args), synchronous, exclude_self, timeout);
   }

   /**
    * @param members
    * @param method_name
    * @param types
    * @param args
    * @param synchronous
    * @param exclude_self
    * @param timeout
    * @return
    * @throws Exception
    * @jmx.managed-operation
    */
   public List callRemoteMethods(Vector members, String method_name,
                                 Class[] types, Object[] args,
                                 boolean synchronous, boolean exclude_self, long timeout)
           throws Exception {
      Method method=getClass().getMethod(method_name, types);
      return callRemoteMethods(members, method, args, synchronous, exclude_self, timeout);
   }
   /* -------------------- End Remote method calls ------------------ */


   /* --------------------- Callbacks -------------------------- */

   /**
    * Does the real work. Needs to acquire locks if accessing nodes, depending on
    * the value of <tt>locking</tt>. If run inside a transaction, needs to (a) add
    * newly acquired locks to {@link TransactionEntry}'s lock list, (b) add nodes
    * that were created to {@link TransactionEntry}'s node list and (c) create
    * {@link Modification}s and add them to {@link TransactionEntry}'s modification
    * list and (d) create compensating modifications to undo the changes in case
    * of a rollback
    *
    * @param fqn
    * @param data
    * @param create_undo_ops If true, undo operations will be created (default is true).
    *                        Otherwise they will not be created (used by rollback()).
    * @jmx.managed-operation
    */
   public void _put(GlobalTransaction tx, String fqn, Map data, boolean create_undo_ops)
           throws CacheException {
      _put(tx, Fqn.fromString(fqn), data, create_undo_ops);
   }


   /**
    * Does the real work. Needs to acquire locks if accessing nodes, depending on
    * the value of <tt>locking</tt>. If run inside a transaction, needs to (a) add
    * newly acquired locks to {@link TransactionEntry}'s lock list, (b) add nodes
    * that were created to {@link TransactionEntry}'s node list and (c) create
    * {@link Modification}s and add them to {@link TransactionEntry}'s modification
    * list and (d) create compensating modifications to undo the changes in case
    * of a rollback
    *
    * @param fqn
    * @param data
    * @param create_undo_ops If true, undo operations will be created (default is true).
    *                        Otherwise they will not be created (used by rollback()).
    * @jmx.managed-operation
    */
   public void _put(GlobalTransaction tx, Fqn fqn, Map data, boolean create_undo_ops)
           throws CacheException {
      _put(tx, fqn, data, create_undo_ops, false);
   }


   /**
    * Does the real work. Needs to acquire locks if accessing nodes, depending on
    * the value of <tt>locking</tt>. If run inside a transaction, needs to (a) add
    * newly acquired locks to {@link TransactionEntry}'s lock list, (b) add nodes
    * that were created to {@link TransactionEntry}'s node list and (c) create
    * {@link Modification}s and add them to {@link TransactionEntry}'s modification
    * list and (d) create compensating modifications to undo the changes in case
    * of a rollback
    *
    * @param fqn
    * @param data
    * @param create_undo_ops If true, undo operations will be created (default is true).
    * @param erase_contents  Clear the existing hashmap before putting the new data into it
    *                        Otherwise they will not be created (used by rollback()).
    * @jmx.managed-operation
    */
   public void _put(GlobalTransaction tx, Fqn fqn, Map data, boolean create_undo_ops, boolean erase_contents)
           throws CacheException {
      Node n;
      MethodCall undo_op=null;
      Map old_data;

      if(log.isDebugEnabled())
         log.debug("_put(" + tx + ", \"" + fqn + "\", " + data + ")");

      try {
         // Find the node. This will lock it (if <tt>locking</tt> is true) and
         // add the temporarily created parent nodes to the TX's node list if tx != null)
         n=findNode(fqn, true, locking, tx, NODE_WRITE);
         if(n == null) {
            log.error("_put(): node " + fqn + " not found");
            return;
         }

         // create a compensating method call (reverting the effect of
         // this modification) and put it into the TX's undo list.
         if(tx != null && create_undo_ops) {
            // TODO even if n is brand new, getData can have empty value instead. Need to fix.
            if((old_data=n.getData()) == null) {
               undo_op=new MethodCall(remove_data_method,
                       new Object[]{tx, fqn, Boolean.FALSE});
            }
            else {
               undo_op=new MethodCall(put_data_erase_method,
                       new Object[]{tx, fqn,
                                    new HashMap(old_data),
                                    Boolean.FALSE,
                                    Boolean.TRUE}); //erase previous hashmap contents
            }
         }

         if(erase_contents) {
            n.put(data, true);
         }
         else {
            if(cache_loader != null) {
               loadAttributesFromCacheLoader(n); // lazy-load the contents before the put()
            }
            n.put(data);
         }

         if(tx != null && create_undo_ops) {
            // 1. put undo-op in TX' undo-operations list (needed to rollback TX)
            tx_table.addUndoOperation(tx, undo_op);
         }
         notifyNodeModified(fqn);
      }
      finally { // we always unlock when no TX is present (lock-update-unlock)
         if(tx == null && locking == true)
            releaseLocks(null, fqn);
      }
   }


   /**
    * @param fqn
    * @param key
    * @param value
    * @return Previous value (if any)
    * @jmx.managed-operation
    */
   public Object _put(GlobalTransaction tx, String fqn, Object key, Object value,  boolean create_undo_ops)
         throws CacheException {
      return _put(tx, Fqn.fromString(fqn), key, value, create_undo_ops);
   }


   /**
    * @param fqn
    * @param key
    * @param value
    * @return Previous value (if any)
    * @jmx.managed-operation
    */
   public Object _put(GlobalTransaction tx, Fqn fqn, Object key, Object value, boolean create_undo_ops)
         throws CacheException {
      Node n=null;
      MethodCall undo_op=null;
      Object old_value=null;

      if(log.isDebugEnabled())
         log.debug("_put(" + tx + ", \"" + fqn + "\", " + key + ", " + value + ")");

      try {
         // Find the node. This will lock it (if <tt>locking</tt> is true) and
         // add the temporarily created parent nodes to the TX's node list if tx != null)
         n=findNode(fqn, true, locking, tx, NODE_WRITE);
         if(n == null) {
            log.error("_put(): node " + fqn + " not found");
            return null;
         }

         old_value=n.get(key);
         if(n.containsKey(UNINITIALIZED) && cache_loader != null)
            loadAttributesFromCacheLoader(n);

         n.put(key, value); //not really necessary because CacheLoaderInterceptor already does this

         // create a compensating method call (reverting the effect of
         // this modification) and put it into the TX's undo list.
         if(tx != null && create_undo_ops) {
            if(old_value == null) {
               undo_op=new MethodCall(remove_key_method,
                       new Object[]{tx, fqn, key, Boolean.FALSE});
            }
            else {
               undo_op=new MethodCall(put_key_val_method,
                       new Object[]{tx, fqn, key, old_value,
                                    Boolean.FALSE});
            }
            // 1. put undo-op in TX' undo-operations list (needed to rollback TX)
            tx_table.addUndoOperation(tx, undo_op);
         }

         notifyNodeModified(fqn);
         return old_value;
      }
      finally { // we always unlock when no TX is present (lock-update-unlock)
         if(tx == null && locking == true)
            releaseLocks(null, fqn);
      }
   }

   /**
    * @param fqn
    * @jmx.managed-operation
    */
   public void _remove(GlobalTransaction tx, String fqn, boolean create_undo_ops) throws CacheException {
      _remove(tx, Fqn.fromString(fqn), create_undo_ops);
   }

   /**
    * @param fqn
    * @jmx.managed-operation
    */
   public void _remove(GlobalTransaction tx, Fqn fqn, boolean create_undo_ops) throws CacheException {
      _remove(tx, fqn, create_undo_ops, true);
   }

   public void _remove(GlobalTransaction tx, Fqn fqn, boolean create_undo_ops, boolean sendNodeEvent)
           throws CacheException {
      _remove(tx, fqn, create_undo_ops, sendNodeEvent, false);
   }

   /**
    * Remove a node
    * @param tx
    * @param fqn
    * @param create_undo_ops
    * @param sendNodeEvent
    */
   public void _remove(GlobalTransaction tx, Fqn fqn, boolean create_undo_ops, boolean sendNodeEvent, boolean eviction)
         throws CacheException {
      Node n, parent_node;
      MethodCall undo_op=null;

      if(log.isDebugEnabled())
         log.debug("_remove(" + tx + ", \"" + fqn + "\")");

      if(fqn.size() == 0) {
         Set children=getChildrenNames(fqn);
         if(children != null) {
            Object[] kids=children.toArray();

            for(int i=0; i < kids.length; i++) {
               String s=(String)kids[i];
               Fqn tmp=new Fqn(fqn, s);
               try {
                  _remove(tx, tmp, create_undo_ops, true, eviction);
               }
               catch(Exception e) {
                  log.error("failure removing node " + tmp);
               }
            }
         }
         return;
      }

      try {
         // Find the node. This will lock it (if <tt>locking</tt> is true) and
         // add the temporarily created parent nodes to the TX's node list if tx != null)
         n=findNode(fqn, true, locking, tx, NODE_WRITE);
         if(n == null) {
            log.error("_remove(): node " + fqn + " not found");
            return;
         }
         parent_node=n.getParent();

         // release all locks for n
         if(locking) {
            if(tx != null)
               n.releaseAll(tx);
            else
               n.releaseAll(getGlobalTransactionFromThread());
         }

         // remove subtree from parent
         parent_node.removeChild(n.getName());

         // create a compensating method call (reverting the effect of
         // this modification) and put it into the TX's undo list.
         if(tx != null && create_undo_ops && n != null && eviction == false) {
            undo_op=new MethodCall(addChild_method, new Object[]{tx, parent_node.getFqn(), n.getName(), n});

            // 1. put undo-op in TX' undo-operations list (needed to rollback TX)
            tx_table.addUndoOperation(tx, undo_op);
         }

         if(sendNodeEvent)
            notifyNodeRemoved(fqn);
         else
            notifyNodeEvicted(fqn);
      }
      finally { // we always unlock when no TX is present (lock-update-unlock)
         if(tx == null && locking == true)
            releaseLocks(null, fqn);
      }
   }

   /**
    * @param fqn
    * @param key
    * @return Object
    * @jmx.managed-operation
    */
   public Object _remove(GlobalTransaction tx, String fqn, Object key, boolean create_undo_ops)
           throws CacheException {
      return _remove(tx, Fqn.fromString(fqn), key, create_undo_ops);
   }

   /**
    * @param fqn
    * @param key
    * @return Object
    * @jmx.managed-operation
    */
   public Object _remove(GlobalTransaction tx, Fqn fqn, Object key, boolean create_undo_ops)
           throws CacheException {
      return _remove(tx, fqn, key, create_undo_ops, false); // din't node remove notification event
   }

   public Object _remove(GlobalTransaction tx, Fqn fqn, Object key, boolean create_undo_ops, boolean sendNodeEvent)
           throws CacheException {
      Node n=null;
      MethodCall undo_op=null;
      Object old_value=null;

      if(log.isDebugEnabled())
         log.debug("_remove(" + tx + ", \"" + fqn + "\", " + key + ")");

      try {
         // Find the node. This will lock it (if <tt>locking</tt> is true) and
         // add the temporarily created parent nodes to the TX's node list if tx != null)
         n=findNode(fqn, true, locking, tx, NODE_WRITE);
         if(n == null) {
            log.error("_remove(): node " + fqn + " not found");
            return null;
         }
         old_value=n.remove(key);

         // create a compensating method call (reverting the effect of
         // this modification) and put it into the TX's undo list.
         if(tx != null && create_undo_ops && old_value != null) {
            undo_op=new MethodCall(put_key_val_method,
                    new Object[]{tx, fqn, key, old_value,
                                 Boolean.FALSE});
            // 1. put undo-op in TX' undo-operations list (needed to rollback TX)
            tx_table.addUndoOperation(tx, undo_op);
         }

         if(sendNodeEvent)
            notifyNodeRemoved(fqn);
         return old_value;
      }
      finally { // we always unlock when no TX is present (lock-update-unlock)
         if(tx == null && locking == true)
            releaseLocks(tx, fqn);
      }
   }

   /**
    * @param fqn
    */
   public void _removeData(GlobalTransaction tx, String fqn, boolean create_undo_ops)
           throws CacheException {
      _removeData(tx, Fqn.fromString(fqn), create_undo_ops);
   }

   /**
    * @param fqn
    */
   public void _removeData(GlobalTransaction tx, Fqn fqn, boolean create_undo_ops)
           throws CacheException {
      _removeData(tx, fqn, create_undo_ops, true);
   }


   public void _removeData(GlobalTransaction tx, Fqn fqn, boolean create_undo_ops, boolean sendNodeEvent)
           throws CacheException {
      _removeData(tx, fqn, create_undo_ops, sendNodeEvent, false);
   }

   public void _removeData(GlobalTransaction tx, Fqn fqn, boolean create_undo_ops, boolean sendNodeEvent, boolean eviction)
           throws CacheException {
      Node n=null;
      MethodCall undo_op=null;
      Map old_data=null;

      if(log.isTraceEnabled())
         log.trace("_removeData(" + tx + ", \"" + fqn + "\")");

      try {
         // Find the node. This will lock it (if <tt>locking</tt> is true) and
         // add the temporarily created parent nodes to the TX's node list if tx != null)
         n=findNode(fqn, true, locking, tx, NODE_WRITE);
         if(n == null) {
            log.error("_removeData(): node " + fqn + " not found");
            return;
         }

         // create a compensating method call (reverting the effect of
         // this modification) and put it into the TX's undo list.
         if(tx != null && create_undo_ops && (old_data=n.getData()) != null && !eviction) {
            undo_op=new MethodCall(put_data_method, new Object[]{tx, fqn, new HashMap(old_data), Boolean.FALSE});
         }

         n.clear();
         if(eviction)
            n.put(UNINITIALIZED, null); // required by cache loader to subsequently load the element again

         if(sendNodeEvent) {
            notifyNodeVisisted(fqn);
         }
         else { // FIXME Bela did this so GUI view can refresh the view after node is evicted. But this breaks eviction policy, especially AOP!!!!
            if(eviction)
               notifyNodeEvicted(fqn);
            else
               notifyNodeModified(fqn); // todo: merge these 2 notifications back into 1 !
         }

         // put undo-op in TX' undo-operations list (needed to rollback TX)
         if(tx != null && create_undo_ops) {
            tx_table.addUndoOperation(tx, undo_op);
         }
      }
      finally { // we always unlock when no TX is present (lock-update-unlock)
         if(tx == null && locking == true)
            releaseLocks(null, fqn);
      }
   }

   /**
    * Compensating method to {@link #_remove(GlobalTransaction,Fqn,boolean)}. This
    * is package-private on purpose because it is only supposed to be called inside
    * the same VM (by {@link #rollback(GlobalTransaction)}).
    *
    * @param parent_fqn
    * @param child_name
    * @param old_node
    */
   void _addChild(GlobalTransaction tx, String parent_fqn, Object child_name, Node old_node)
           throws CacheException {
      _addChild(tx, Fqn.fromString(parent_fqn), child_name, old_node);
   }

   /**
    * Compensating method to {@link #_remove(GlobalTransaction,Fqn,boolean)}. This
    * is package-private on purpose because it is only supposed to be called inside
    * the same VM (by {@link #rollback(GlobalTransaction)}).
    *
    * @param parent_fqn
    * @param child_name
    * @param old_node
    */
   public void _addChild(GlobalTransaction tx, Fqn parent_fqn, Object child_name, Node old_node)
           throws CacheException {
      if(log.isDebugEnabled())
         log.debug("_addChild(" + tx + ", \"" + parent_fqn + "\", \"" + child_name +
                 "\")");

      if(parent_fqn == null || child_name == null || old_node == null) {
         log.error("_addChild(): parent_fqn or child_name or node was null");
         return;
      }
      Node tmp=findNode(parent_fqn, false, false, tx, NODE_WRITE);
      if(tmp == null) {
         log.error("_addChild(): node " + parent_fqn + " not found");
         return;
      }

// TODO This is bit weird????
      if(locking) {
         GlobalTransaction gtx=null;
         if(tx != null)
            gtx=tx;
         else
            gtx=getGlobalTransactionFromThread();

         old_node.releaseAll(gtx);
         old_node.acquireAll(gtx, lock_acquisition_timeout, Node.LOCK_TYPE_WRITE);
      }
      tmp.addChild(child_name, old_node);
   }


   /**
    * Invoked by the ReplicationInterceptor of other nodes in the cluster, to replicate their changes across the
    * cluster. Needs to be forwarded to the ReplicationInterceptor in this interceptor chain.<br/>
    * This method will later be moved entirely into the ReplicationInterceptor.
    * @param method_call
    * @return
    * @throws Throwable
    */
   public Object _replicate(MethodCall method_call) throws Throwable {
      if(replication_handler != null) {
         return replication_handler.replicate(method_call);
      }
      else {
         throw new UnsupportedOperationException("no replication handler is installed");
      }
   }

   public void _replicate(List method_calls) throws Throwable {
      if(replication_handler != null) {
         replication_handler.replicate(method_calls);
      }
      else {
         throw new UnsupportedOperationException("no replication handler is installed");
      }
   }


   /* ----------------- End of  Callbacks ---------------------- */


   /* ----------------- 2-phase commit protocol----------------- */


   /**
    * @param tx            The global transaction under which all updates are coordinated across
    *                      a cluster
    * @param modifications A list of {@link MethodCall} objects
    * @param coordinator   The address of the member who coordinates this 2pc protocol
    * @param commit        Commit or rollback immediately ?
    * @throws Exception
    */
   public void prepare(GlobalTransaction tx, List modifications, Address coordinator,
                       boolean commit) throws Exception {
      MethodCall method_call;
      Object retval;
      boolean success=true;

      // discard own message. should never happen because runPreparePhase() calls
      // every member except self !
      if(coordinator != null && coordinator.equals(getLocalAddress())) {
         log.warn("prepare(): received my own message (will discard it)");
         return;
      }

      // 1. Create new entry for TX in tx_table, the modifications below will need this entry to add
      //    their modifications under the GlobalTx key
      if(tx_table.get(tx) == null) {
         TransactionEntry ent=new TransactionEntry();

         // we don't need the local TX in prepare/commit (bela Nov 13 2003)
         // ent.setTransaction(getLocalTransaction());
         tx_table.put(tx, ent);
      }

      if(log.isDebugEnabled())
         log.debug("prepare(): run prepare ");
      // 2. Run all MethodCalls in modification list
      try {
         if(modifications != null) {
            for(Iterator it=modifications.iterator(); it.hasNext();) {
               method_call=(MethodCall)it.next();
               try {
                  retval=method_call.invoke(this);
               }
               catch(Throwable t) {
                  log.error("method invocation failed", t);
                  retval=t;
               }
               if(retval != null && retval instanceof Exception) {
                  success=false;
                  throw (Exception)retval;
               }
               else {
                  // add the modification to the TX's modification list.
                  tx_table.addModification(tx, method_call);
               }
            }
         }
      }
      finally {
         if(commit) {
            if(success)
               commit(tx);
            else
               rollback(tx);
         }
      }
   }


   public void addModification(GlobalTransaction gtx, MethodCall method_call) {
      tx_table.addModification(gtx, method_call);
   }




   /**
    * Remove all locks held by <tt>tx</tt>, remove the transaction from the
    * transaction table
    *
    * @param tx
    */
   public void commit(GlobalTransaction tx) {
      if(log.isDebugEnabled())
         log.debug("commit(): called to commit cache with gtx " + tx);

      TransactionEntry entry=tx_table.get(tx);
      if(entry == null) {
         log.error("commit(): entry for transaction " + tx + " not found");
         return;
      }

      // Let's do it in stack style, LIFO
      List list=entry.getLocks();
      for(int i=list.size() - 1; i >= 0; i--) {
         IdentityLock lock=(IdentityLock)list.get(i);
         if(log.isDebugEnabled())
            log.debug("commit(): releasing lock " + lock);
         lock.release(tx);
      }

      if(log.isDebugEnabled())
         log.debug("commit(): removing local transaction key " + entry.getTransaction());
      tx_table.remove(entry.getTransaction());
      tx_table.remove(tx);
   }

   /**
    * Revert all changes made inside this TX: invoke all method calls of the undo-ops
    * list. Then release all locks and remove the TX from the transaction table.
    * <ol>
    * <li>Revert all modifications done in the current TX<li/>
    * <li>Release all locks held by the current TX</li>
    * <li>Remove all temporary nodes created by the current TX</li>
    * </ol>
    *
    * @param tx
    */
   public void rollback(GlobalTransaction tx) {
      List undo_ops;
      TransactionEntry entry=tx_table.get(tx);
      MethodCall undo_op;
      Object retval;
      Fqn node_name;

      if(log.isDebugEnabled())
         log.debug("rollback(): called to rollback cache");

      if(entry == null) {
         log.error("rollback(): entry for transaction " + tx +
                 " not found (transaction  has possibly already been rolled back)");
         return;
      }

      // 1. Revert the modifications by running the undo-op list in reverse. This
      //    *cannot* throw any exceptions !
      undo_ops=entry.getUndoOperations();
      for(ListIterator it=undo_ops.listIterator(undo_ops.size()); it.hasPrevious();) {
         undo_op=(MethodCall)it.previous();
         try {
            retval=undo_op.invoke(this);
            if(retval != null && retval instanceof Throwable) {
               log.error("rollback(): undo operation failed, error=" + retval);
            }
         }
         catch(Throwable t) {
            log.error("rollback(): undo operation failed", t);
         }
      }

      // 2. Remove all temporary nodes. Need to do it backward since node is LIFO.
      for(ListIterator it=entry.getNodes().listIterator(entry.getNodes().size());
          it.hasPrevious();) {
         node_name=(Fqn)it.previous();
         try {
            _remove(tx, node_name, false);
         }
         catch(Throwable t) {
            log.error("rollback(): failed removing node \"" + node_name + "\"", t);
         }
      }


      // 3. Finally, release all locks held by this TX
      // Let's do it in stack style, LIFO
      // Note that the lock could have been released already so don't panic.
      List list=entry.getLocks();
      for(int i=list.size() - 1; i >= 0; i--) {
         IdentityLock lock=(IdentityLock)list.get(i);
         if(log.isDebugEnabled())
            log.debug("rollback(): releasing lock " + lock);
         lock.release(tx);
      }

      tx_table.remove(entry.getTransaction());
      tx_table.remove(tx);
   }

   /* -------------- End of 2-phase commit protocol------------- */


   /*-------------------- MessageListener ----------------------*/

   class MessageListenerAdaptor implements MessageListener {

      /**
       * Callback (no-op)
       *
       * @param msg
       */
      public void receive(Message msg) {
         ;
      }

      /**
       * Return a copy of the current cache (tree). It actually returns a 2 element array of byte[], element 0
       * being the transient state (or null) and element 1 being the persistent state (or null)
       */
      public byte[] getState() {
         GlobalTransaction tx;
         boolean fetch_persistent_state=cache_loader != null &&
               cache_loader_shared == false &&
               cache_loader_fetch_persistent_state;
         byte[] transient_state=null;
         byte[] persistent_state=null;
         byte[][] states=new byte[2][];
         byte[] retval=null;
         boolean locked=false;

         tx=getCurrentTransaction();
         if(tx == null) tx=getGlobalTransactionFromThread();

         states[0]=states[1]=null;
         try {
            if(cache_loader_fetch_transient_state) {
               log.info("locking the tree to obtain transient state");
               root.acquireAll(tx, state_fetch_timeout, Node.LOCK_TYPE_READ);
               locked=true;
               transient_state=Util.objectToByteBuffer(root);
               states[0]=transient_state;
               log.info("returning the transient state (" + transient_state.length + " bytes)");
            }
         }
         catch(Throwable t) {
            log.error("failed getting the transient state", t);
         }
         try {
            if(fetch_persistent_state) {
               if(!locked) {
                  log.info("locking the tree to obtain persistent state");
                  root.acquireAll(tx, state_fetch_timeout, Node.LOCK_TYPE_READ);
                  locked=true;
               }
               log.info("getting the persistent state");
               persistent_state=cache_loader.loadEntireState();
               states[1]=persistent_state;
            }
         }
         catch(Throwable t) {
            log.error("failed getting the persistent state", t);
         }

         try {
            retval=Util.objectToByteBuffer(states);
            return retval;
         }
         catch(Throwable t) {
            log.error("failed serializing transient and persistent state", t);
            return retval;
         }

         finally {
            root.releaseAll(tx);
         }
      }


      public void setState(byte[] new_state) {
         try {
            _setState(new_state);
         }
         finally {
            synchronized(stateLock) {
               // Notify wait that state has been set.
               stateLock.notifyAll();
            }
         }
      }

      /**
       * Set the cache (tree) to this value. The new_state is a byt[][] array, element 0 is the transient state
       * (or null) , and element 1 is the persistent state (or null)
       */
      void _setState(byte[] new_state) {
         Node new_root=null, old_root=null;
         Object obj;
         GlobalTransaction tx=null;
         byte[][] states=null;
         byte[] transient_state=null;
         byte[] persistent_state=null;
         boolean locked=false;

         if(new_state == null) {
            log.info("new cache is null (maybe first member in cluster)");
            return;
         }

         // 1. Unserialize the states into transient and persistent state
         try {
            log.info("received the state (size=" + new_state.length + " bytes)");
            states=(byte[][])Util.objectFromByteBuffer(new_state);
            transient_state=states[0];
            persistent_state=states[1];
            if(transient_state != null)
               log.info("transient state: " + transient_state.length + " bytes");
            if(persistent_state != null)
               log.info("persistent state: " + persistent_state.length + " bytes");
            tx=getCurrentTransaction();
            if(tx == null) tx=getGlobalTransactionFromThread();
         }
         catch(Throwable t) {
            log.error("failed unserializing state", t);
         }

         // 2. If transient state is available: set it (lock tree first)
         if(transient_state != null) {
            try {
               log.info("setting transient state");
               obj=Util.objectFromByteBuffer(transient_state);
               new_root=(Node)obj;
               log.info("locking the old tree");
               root.acquireAll(tx, state_fetch_timeout, Node.LOCK_TYPE_WRITE);
               locked=true;
               log.info("setState(): locking the old tree was successful");
               old_root=root;
               root=new_root;
               log.info("setting the transient state was successful");
               notifyAllNodesCreated(root);
            }
            catch(Throwable t) {
               log.error("failed setting transient state", t);
            }
         }

         // 3. Set the persistent state
         if(persistent_state != null) {
            if(cache_loader == null) {
               log.error("cache loader is null, cannot set persistent state");
            }
            else {
               try {
                  if(!locked) {
                     log.info("setState(): locking the old tree");
                     root.acquireAll(tx, state_fetch_timeout, Node.LOCK_TYPE_WRITE);
                     old_root=root; // to unlock later
                     locked=true;
                     root=new Node(SEPARATOR, Fqn.fromString(SEPARATOR), null, null);
                     log.info("setState(): locking the old tree was successful");
                  }
                  log.info("setting the persistent state");
                  // cache_loader.remove(Fqn.fromString("/"));
                  cache_loader.storeEntireState(persistent_state);
                  log.info("setting the persistent state was successful");
               }
               catch(Throwable t) {
                  log.error("failed setting persistent state", t);
               }
            }
         }

         if(old_root != null) {
            log.info("forcing release of all locks in old tree");
            try {old_root.releaseAllForce();} catch(Throwable t) {log.error("failed releasing locks", t);}
         }

         isStateSet=true;
      }



   }



   /*-------------------- End of MessageListener ----------------------*/


   /*----------------------- MembershipListener ------------------------*/

   public void viewAccepted(View new_view) {
      Vector new_mbrs=new_view.getMembers();

      // todo: if MergeView, fetch and reconcile state from coordinator
      // actually maybe this is best left up to the application ? we just notify them and let
      // the appl handle it ?

      log.info("viewAccepted(): new members: " + new_mbrs);
      if(new_mbrs != null) {
         members.removeAllElements();
         members.addAll(new_view.getMembers());
         notifyViewChange(new_view);
      }

      if(cache_loader_shared) {
         // boolean old_coord=coordinator;
         coordinator=determineCoordinator();
      }
   }


   /**
    * Called when a member is suspected
    */
   public void suspect(Address suspected_mbr) {
      ;
   }


   /**
    * Block sending and receiving of messages until viewAccepted() is called
    */
   public void block() {
   }

   /*------------------- End of MembershipListener ----------------------*/


   /* ------------------------------ Private methods --------------------------- */

   /**
    * Returns the transaction associated with the current thread. We get the initial context
    * and a reference to the TransactionManager to get the transaction. This method is used
    * by {@link #getCurrentTransaction()}
    *
    * @return
    */
   protected Transaction getLocalTransaction() {
      if(tm == null) {
         return null;
      }
      try {
         return tm.getTransaction();
      }
      catch(Throwable t) {
         return null;
      }
   }

   /**
    * Get the transaction associated with the current thread and maps it onto a
    * {@link GlobalTransaction}. The mapping is maintained in the <tt>tx_map</tt> table.
    * If a local transaction exists, but doesn't yet have a mapping to a GlobalTransaction,
    * a new GlobalTransaction will be created and mapped to the local transaction.
    *
    * @return A GlobalTransaction, or null if no (local) transaction was associated with
    *         the current thread
    */
   public GlobalTransaction getCurrentTransaction() {
      GlobalTransaction gtx=null;
      Transaction tx;
      Address addr=(Address)getLocalAddress();

      if((tx=getLocalTransaction()) == null) { // no transaction is associated with the current thread
         return null;
      }

      gtx=tx_table.get(tx);
      if(gtx == null) {
         gtx=GlobalTransaction.create(addr);
         tx_table.put(tx, gtx);
         TransactionEntry ent=new TransactionEntry();
         ent.setTransaction(tx);
         tx_table.put(gtx, ent);
      }
      return gtx;
   }

   public GlobalTransaction getCurrentTransaction(Transaction tx) {
        GlobalTransaction gtx=null;
        Address addr=(Address)getLocalAddress();

        gtx=tx_table.get(tx);
        if(gtx == null) {
           gtx=GlobalTransaction.create(addr);
           tx_table.put(tx, gtx);
           TransactionEntry ent=new TransactionEntry();
           ent.setTransaction(tx);
           tx_table.put(gtx, ent);
        }
        return gtx;
   }



   /**
    * Invokes a method against this object. Contains the logger_ic for handling the various
    * use cases, e.g. mode (local, repl_async, repl_sync), transaction (yes or no) and
    * locking (yes or no).
    *
    * @param m
    * @return
    * @throws CacheException
    */
   protected Object invokeMethod(MethodCall m) throws CacheException {
      if(log.isTraceEnabled()) log.trace("calling method " + m.getName());
      try {
         return interceptor_chain.invoke(m);
      }
      catch(Throwable t) {
         if(t instanceof CacheException)
            throw (CacheException)t;
         throw new NestedRuntimeException(t);
      }
   }



   protected String printArgs(Object[] args) {
      StringBuffer sb=new StringBuffer();
      boolean first=true;
      if(args != null && args.length > 0) {
         for(int i=0; i < args.length; i++) {
            if(!first)
               sb.append(", ");
            else
               first=false;
            if(args[i] instanceof String) sb.append("\"");
            sb.append(args[i]);
            if(args[i] instanceof String) sb.append("\"");
         }
      }
      return sb.toString();
   }

   /**
    * Finds a node given a fully qualified name. If intermediary nodes don't exist, they
    * will be created (if <tt>create_if_not_exists</tt> is true).
    * If <tt>lock_node</tt> is true, locks will be acquired for each node
    * (with <tt>tx</tt> as owner).<br>
    * Whenever nodes are created, and the global transaction is not null, the created
    * nodes have to be added to the transaction's {@link TransactionEntry#nodes}
    * field.<br>
    * When a lock is acquired on a node, a reference to the lock has to be added to the list
    * of locked nodes in the {@link TransactionEntry#locks} field.
    * <p>This operation will also applies different locking to the tree nodes, depending on
    * <tt>operation_type</tt>. If it is <tt>read</tt> type, all nodes will be acquired with
    * read lock. Otherwise, the operation is <tt>write</tt> type, all parent nodes will be acquired
    * with read lock while the destination node acquires write lock.</p>
    *
    * @param fqn                  Fully qualified name for the corresponding node.
    * @param create_if_not_exists Flag to indicate whether to create a new sub tree node if
    *                             not yet existed. Note that this applies only when lock_type is 1, i.e., read only.
    * @param lock_node            Boolean flag to decide whether to lock the node.
    * @param tx                   GlobaTransaction intance used to associated with this transaction.
    * @param operation_type       Integer to indicate whether this operation is for read or write
    *                             intention. Values are <tt>NODE_READ</tt> and <tt>NODE_WRITE</tt>.
    * @return
    */
   protected Node findNode(Fqn fqn, boolean create_if_not_exists, boolean lock_node,
                           GlobalTransaction tx, int operation_type) throws CacheException {
      Node n, child_node=null;
      Object child_name;
      Fqn tmp_fqn=new Fqn(), copy;

      if(fqn == null) return null;
      int treeNodeSize=fqn.size();
      // GlobalTransaction gtx = null; // bela March 19 2004


      if(treeNodeSize == 0)
         return root;
      n=root;

      for(int i=0; i < treeNodeSize; i++) {
         child_name=fqn.get(i);
         tmp_fqn=new Fqn(tmp_fqn, child_name);
         child_node=n.getChild(child_name);

         // try CacheLoader if node is not in cache
         if(child_node == null && cache_loader != null) {
            try {
               if(cache_loader.exists(fqn)) {
                  child_node=n.createChild(child_name, tmp_fqn, n, UNINITIALIZED, null);
                  notifyNodeLoaded(tmp_fqn);
               }
            }
            catch(Throwable t) {
               log.error("failure loading node " + fqn + " from CacheLoader", t);
            }
         }

         if(child_node == null) {
            if(create_if_not_exists) {
               copy=(Fqn)tmp_fqn.clone();
               child_node=n.createChild(child_name, copy, n);
               if(tx != null) {
                  // add the node name to the list maintained for the current tx
                  // (needed for abort/rollback of transaction)
                  tx_table.addNode(tx, (Fqn)tmp_fqn.clone());
               }
               notifyNodeCreated(copy);
            }
            else {
               return null;
            }
         }

         // if(lock_node) {
         if(lock_node && tx != null) { // bela March 19 2004 (read CVS commit comment for details)
//            if(tx == null) {
//               gtx=getGlobalTransactionFromThread();
//            } else {
//               gtx = tx;
//            }
            // bela March 19 2004

            try {
               // Write lock only for destination node in write request.
               if(operation_type == NODE_WRITE && i == (treeNodeSize - 1)) {
                  child_node.acquire(tx, lock_acquisition_timeout, Node.LOCK_TYPE_WRITE);
               }
               else {
                  child_node.acquire(tx, lock_acquisition_timeout, Node.LOCK_TYPE_READ);
               }
            }
            catch(CacheException cex) {
               releaseLocks(tx, fqn); // This is intentional. If tx is null, releaseLocks will understand it.
               throw cex;
            }

            if(tx != null) {
               // add the lock to the list of locks maintained for this transaction
               // (needed for release of locks on commit or rollback)
               tx_table.addLock(tx, child_node.getImmutableLock());
            }
         }
         n=child_node;
      }
      return child_node;
   }



   /**
    * This method finds a node given a fully qualified name. It <em>never</em> creates
    * intermediary nodes.
    *
    * @param fqn
    * @param lock_type Lock type. Read lock --1, write lock --2.
    * @return
    */
   protected Node findNode(String fqn, int lock_type) throws CacheException {
      return findNode(Fqn.fromString(fqn), false, locking, getCurrentTransaction(), lock_type);
   }

   /**
    * This method finds a node given a fully qualified name. It <em>never</em> creates
    * intermediary nodes.
    *
    * @param fqn
    * @param lock_type Lock type. Read lock --1, write lock --2.
    * @return
    */
   protected Node findNode(Fqn fqn, int lock_type) throws CacheException {
      return findNode(fqn, false, locking, getCurrentTransaction(), lock_type);
   }

   /**
    * Obtain a GlobalTransaction instance based on current thread id.
    * Note that once a gtx is mapped the a thread instance, it will never
    * be removed from the map. In other words, the same thread will use
    * the same gtx instance during the cache life cycle.
    *
    * @return GlobalTransaction instance
    */
   protected GlobalTransaction getGlobalTransactionFromThread() {
      Object currentThread=Thread.currentThread();
      GlobalTransaction gtx=(GlobalTransaction)thread2GtxMap_.get(currentThread);
      if(gtx == null) {
         Address ipAddress=(Address)getLocalAddress();
         gtx=GlobalTransaction.create(ipAddress);
         thread2GtxMap_.put(currentThread, gtx);
      }
      return gtx;
   }


   /**
    *
    * @return
    */
   public GlobalTransaction getGlobalTransactionFromLocalTransaction(Transaction tx) {
      return tx_table.get(tx);
   }

   public TransactionEntry getTransactionEntry(GlobalTransaction gtx) {
      return tx_table.get(gtx);
   }

   /**
    * Releases the locks acquired outside of a transaction
    *
    * @param fqn
    */
   protected void releaseLocks(GlobalTransaction tx, Fqn fqn) {
      Node n, child_node=null;
      Object child_name;

      if(fqn == null) return;
      n=root;

      // TODO tx is kind of confusing here. Need to consolidate it later.
      if(tx == null) { // Use current thread to identify the GlobalTransaction
         tx=getGlobalTransactionFromThread();
      }

      java.util.Stack stack=new java.util.Stack();
      for(int i=0; i < fqn.size(); i++) {
         child_name=fqn.get(i);
         child_node=n.getChild(child_name);
         if(child_node == null) {
            return;
         }
         else {
            stack.push(child_node);
         }
         n=child_node;
      }

      while(!stack.empty()) {
         Node node=(Node)stack.pop();
         node.release(tx);
      }
   }

   /**
    * Returns the data hashmap for a given node. This method can only be used by callers that are inside
    * the same package. The reason is that callers must not modify the return value, as these modifications
    * would not be replicated, thus rendering the replicas inconsistent.
    *
    * @param fqn The fully qualified name of the node
    * @return Map A cloned data hashmap for the given node
    */
   Map getData(String fqn) throws CacheException {
      Node n=findNode(fqn, NODE_READ);

      if(n == null) return null;
      return n.getData();
   }


   protected void loadAttributesFromCacheLoader(Node n) {
      if(cache_loader == null || n == null || !n.containsKey(UNINITIALIZED))
         return;

      // lazily load attributes before the get()
      try {
         Map m=cache_loader.get(n.getFqn());
         n.remove(UNINITIALIZED); // all attributes have been loaded
         n.put(m);
      }
      catch(Throwable t) {
         log.error("failure when lazy loading attributes of " + n.getFqn(), t);
      }
   }


   protected void notifyNodeCreated(Fqn fqn) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).nodeCreated(fqn);
   }

    protected void notifyNodeLoaded(Fqn fqn) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).nodeLoaded(fqn);
   }

   protected void notifyNodeRemoved(Fqn fqn) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).nodeRemoved(fqn);
   }

   protected void notifyNodeEvicted(Fqn fqn) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).nodeEvicted(fqn);
   }

   protected void notifyNodeModified(Fqn fqn) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).nodeModified(fqn);
   }

   protected void notifyNodeVisisted(Fqn fqn) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).nodeVisited(fqn);
   }

   protected void notifyCacheStarted() {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).cacheStarted(this);
   }

   protected void notifyCacheStopped() {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).cacheStopped(this);
   }

   protected void notifyViewChange(View v) {
      for(int i=0; i < listeners.size(); i++)
         ((TreeCacheListener)listeners.elementAt(i)).viewChange(v);
   }

   /**
    * Generates NodeAdded notifications for all nodes of the tree. This is called whenever the tree is
    * initially retrieved (state transfer)
    */
   protected void notifyAllNodesCreated(Node curr) {
      Node n;
      Map children;

      if(curr == null) return;
      notifyNodeCreated(curr.fqn);
      if((children=curr.getChildren()) != null) {
         for(Iterator it=children.values().iterator(); it.hasNext();) {
            n=(Node)it.next();
            notifyAllNodesCreated(n);
         }
      }
   }

   protected String getDefaultProperties() {
      return "UDP(mcast_addr=224.0.0.36;mcast_port=55566;ip_ttl=32;" +
              "mcast_send_buf_size=150000;mcast_recv_buf_size=80000):" +
              "PING(timeout=2000;num_initial_members=3):" +
              "MERGE2(min_interval=5000;max_interval=10000):" +
              "FD_SOCK:" +
              "VERIFY_SUSPECT(timeout=1500):" +
              "pbcast.NAKACK(gc_lag=50;max_xmit_size=8192;retransmit_timeout=600,1200,2400,4800):" +
              "UNICAST(timeout=600,1200,2400,4800):" +
              "pbcast.STABLE(desired_avg_gossip=20000):" +
              "FRAG(frag_size=8192;down_thread=false;up_thread=false):" +
              "pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;" +
              "shun=false;print_local_addr=true):" +
              "pbcast.STATE_TRANSFER";
   }


   protected int string2Mode(String mode) {
      if(mode == null) return -1;
      String m=mode.toLowerCase().trim();
      if(m.equals("local"))
         return LOCAL;
      else
         if(m.equals("repl_async") || m.equals("repl-async"))
            return REPL_ASYNC;
         else
            if(m.equals("repl_sync") || m.equals("repl-sync"))
               return REPL_SYNC;
            else
               return -1;
   }


   public int numberOfModifications(GlobalTransaction gtx) {
      TransactionEntry entry;
      if(gtx == null)
         return -1;
      entry=tx_table.get(gtx);
      List modifications=entry != null ? entry.getModifications() : null;
      return modifications != null ? modifications.size() : -1;
   }




   static void help() {
      System.out.println("TreeCache [-help] [-cluster_props <properties>] [-mode <mode>]");
      System.out.println("(mode: local (default), repl_async, repl_sync");
   }

   public static class MyListener implements TreeCacheListener {

      public void nodeCreated(Fqn fqn) {
         System.out.println("** node added: " + fqn);
      }

      public void nodeRemoved(Fqn fqn) {
         System.out.println("** node removed: " + fqn);
      }

      public void nodeModified(Fqn fqn) {
         System.out.println("** node modified: " + fqn);
      }

      public void nodeLoaded(Fqn fqn) {
         System.out.println("** node loaded: " + fqn);
      }

      public void nodeEvicted(Fqn fqn) {
         System.out.println("*** node evicted: " + fqn);
      }

      public void nodeVisited(Fqn fqn) {
      }

      public void cacheStarted(TreeCache cache) {
      }

      public void cacheStopped(TreeCache cache) {
      }

      public void viewChange(View new_view) {
         System.out.println("** view change: " + new_view);
      }
   }


}


