/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache;


import org.jgroups.*;
import org.jgroups.blocks.ReplicationManager;
import org.jgroups.blocks.ReplicationReceiver;
import org.jgroups.log.Trace;
import org.jgroups.util.Rsp;
import org.jgroups.util.RspList;
import org.jgroups.util.Util;
import org.jboss.system.ServiceMBeanSupport;

import java.util.*;


/**
 * Uses <a href="http://www.jgroups.com">JGroups</a> to implement a replicated
 * cache. At the same time it is an MBean, which means we can use it in JBoss.
 * Refer to {@link Cache} for a detailed description.
 * todo Notify CacheListeners on _put(), _putAll(), _remove() and _clear()
 * todo Create and connect channel only if mode != transient (createService(),
 * startService())
 * todo Start a new transaction when any of the update methods (put(),
 * putAll(), remove() or clear()) are called without a transaction associated
 * with the current thread.
 * @author Bela Ban
 * @version $Revision: 1.1.2.1 $
 */
public class CacheImpl extends ServiceMBeanSupport implements CacheImplMBean {
    private boolean trace=log != null ? log.isTraceEnabled() : false;
    private boolean debug=log != null ? log.isDebugEnabled() : false;
    private boolean info=log != null ? log.isInfoEnabled() : false;

    Channel channel=null;
    ReplicationManager repl_mgr=null;
    MessageListener msg_listener=new MessageListenerAdapter();
    ReplicationReceiver repl_receiver=new ReplicationReceiverAdapter();


    /** Contains the cache entries (keys={@link CacheKey}s,
     * values={@link CacheValue}s). The keys will
     * be added according in sorted order according to their timestamp. This
     * allows the eviction policy handler to quickly remove e.g. the oldest
     * items.
     */
    TreeMap map=new TreeMap();

    /** Name of the cluster. All caches with the same name will form a cluster */
    String cluster_name="Cache-Group";

    /** The name of the MBean service */
    String mbean_name="jboss:service=CacheService";

    /** JGroups protocol stack properties */
    String cluster_properties=null;

    /** Max number of entries */
    int max_capacity=10000;

    /** Max time in milliseconds to retrieve the initial state */
    long state_timeout=20000;

    /** Max number of milliseconds to wait for all responses on a synchronous replication
     * until the call returns.
     */
    long sync_repl_timeout=5000;

    /** Max number of millseconds to acquire a lock */
    long lock_acquisition_timeout=10000;

    /** Max number of miliseconds a lock is held. If this is exceeded the lock wil be automatically released.
     * Note that this is currently not used.
     */
    long lock_lease_timeout=60 * 1000;

    /** The default caching mode is asynchronous replication */
    int caching_mode=Options.REPL_ASYNC;

    /** The fully qualified name of an eviction policy handler, needs to implement EvictionPolicy */
    String eviction_policy_classname=null;

    /** The eviction policy handler used for this cache */
    EvictionPolicy eviction_policy=null;

    LockingPolicy locking_policy=null;

    /** A list of CacheListeners */
    List cache_listeners=new ArrayList();


    /**
     * Constructor for CacheImpl.
     */
    public CacheImpl() {
        super();
    }


    /**
     * @see org.jboss.system.ServiceMBean#getName()
     */
    public String getName() {
        return mbean_name;
    }

    public void setName(String name) {
        this.mbean_name=name;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getClusterName()
     */
    public String getClusterName() {
        return cluster_name;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setClusterName(java.lang.String)
     */
    public void setClusterName(String name) {
        if(name != null) cluster_name=name;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getClusterProperties()
     */
    public String getClusterProperties() {
        return cluster_properties;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setClusterProperties(String)
     */
    public void setClusterProperties(String cluster_properties) {
        if(cluster_properties != null)
            this.cluster_properties=cluster_properties;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getMaxCapacity()
     */
    public int getMaxCapacity() {
        return max_capacity;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setMaxCapacity(int)
     */
    public void setMaxCapacity(int max_capacity) {
        this.max_capacity=max_capacity;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getInitialStateRetrievalTimeout()
     */
    public long getInitialStateRetrievalTimeout() {
        return state_timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setInitialStateRetrievalTimeout(long)
     */
    public void setInitialStateRetrievalTimeout(long timeout) {
        state_timeout=timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getCachingMode()
     */
    public int getCachingMode() {
        return caching_mode;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setCachingMode(int)
     */
    public void setCachingMode(int mode) {
        this.caching_mode=mode;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getSyncReplTimeout()
     */
    public long getSyncReplTimeout() {
        return sync_repl_timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setSyncReplTimeout(long)
     */
    public void setSyncReplTimeout(long timeout) {
        this.sync_repl_timeout=timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getLockAcquisitionTimeout()
     */
    public long getLockAcquisitionTimeout() {
        return lock_acquisition_timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setLockAcquisitionTimeout(long)
     */
    public void setLockAcquisitionTimeout(long timeout) {
        this.lock_acquisition_timeout=timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#getLockLeaseTimeout()
     */
    public long getLockLeaseTimeout() {
        return lock_lease_timeout;
    }

    /**
     * @see org.jboss.cache.CacheImplMBean#setLockLeaseTimeout(long)
     */
    public void setLockLeaseTimeout(long timeout) {
        this.lock_lease_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
     */
    public String getEvictionPolicyClass() {
        return eviction_policy_classname;
    }

    /** Sets the classname of the eviction policy */
    public void setEvictionPolicyClass(String eviction_policy_classname) {
        if(eviction_policy_classname != null)
            this.eviction_policy_classname=eviction_policy_classname;

        // dump the old policy, create the new one:
        if(eviction_policy != null) {
            try {
                eviction_policy.stop();
                eviction_policy.destroy();
            }
            catch(Throwable ex) {
            }
        }
        try {
            eviction_policy=createEvictionPolicy(eviction_policy_classname);
        }
        catch(Throwable ex) {
            log.error("CacheImpl.setEvictionPolicyClass(): failed creating eviction policy instance", ex);
            this.eviction_policy_classname=null;
        }
    }


    /**
     * Returns the locking_policy.
     * @return LockingPolicy
     */
    public LockingPolicy getLockingPolicy() {
        return locking_policy;
    }

    /**
     * TODO: maybe we should rather give the name of the class. Or, if people
     * wish to give the instance directly, change evitiction policy
     * instantiation as well.
     * <br/>Sets the locking_policy.
     * @param locking_policy The locking_policy to set
     */
    public void setLockingPolicy(LockingPolicy locking_policy) {
        this.locking_policy=locking_policy;
    }


    /**
     * @see org.jboss.cache.Cache#addListener(org.jboss.cache.CacheListener)
     */
    public void addListener(CacheListener l) {
        if(l != null && !cache_listeners.contains(l))
            cache_listeners.add(l);
    }

    /**
     * @see org.jboss.cache.Cache#removeListener(org.jboss.cache.CacheListener)
     */
    public void removeListener(CacheListener l) {
        if(l != null)
            cache_listeners.remove(l);
    }

    /**
     * Currently not used. May be removed shortly
     * @see org.jboss.cache.Cache#configure(java.lang.Object)
     */
    public void configure(Object properties) {
        // TODO: implement
    }

    /**
     *
     * @see org.jboss.cache.Cache#size()
     */
    public int size() {
        // TODO: implement locking. somebody else might be locking the cache at the same time
        // Do we want to add a dirtySize() method ?
        return map.size();
    }

    /**
     * @see org.jboss.cache.Cache#isEmpty()
     */
    public boolean isEmpty() {
        // TODO: implement locking
        return map.isEmpty();
    }

    /**
     * @see org.jboss.cache.Cache#containsKey(java.lang.Object)
     */
    public boolean containsKey(Object key) {
        // TODO: implement locking
        return map.containsKey(new CacheKey(key));
    }

    /**
     * @see org.jboss.cache.Cache#get(java.lang.Object)
     */
    public Object get(Object key) {
        // TODO: implement locking (somebody else might have a write-lock on this item)

        return map.get(new CacheKey(key));
    }

    /**
     * Returns a <em>copy</em> of the keys in the cache.<br/>
     * TODO: implement locking
     * @return Set of Objects
     * @see org.jboss.cache.Cache#keySet()
     */
    public Set keySet() {
        // TODO: implement locking. Define semantics of iterating over changing cache keys

        CacheKey k;
        Set s=new TreeSet();
        for(Iterator it=map.keySet().iterator(); it.hasNext();) {
            k=(CacheKey)it.next();
            s.add(k.getKey());
        }
        return s;
    }

    /**
     * Returns a <em>copy</em> of the values in the cache<br/>
     *  TODO:  implement locking
     * @return Collection of Objects
     * @see org.jboss.cache.Cache#values()
     */
    public Collection values() {
        // TODO: implement locking. Define semantics of iterating over changing cache values

        CacheValue v;
        Collection retval=new LinkedList();
        for(Iterator it=map.values().iterator(); it.hasNext();) {
            v=(CacheValue)it.next();
            retval.add(v.getValue());
        }
        return retval;
    }

    /**
     * Returns a copy of all cache entries<br/>
     * TODO: implement locking<br/>
     * TODO: provide own implementation of Map.Entry; the current approach is
     * copy-heavy
     * @see org.jboss.cache.Cache#entrySet()
     */
    public Set entrySet() {
        // TODO: implement locking. Define semantics of iterating over changing cache entries

        TreeMap copy=(TreeMap)map.clone();
        Map.Entry entry;
        for(Iterator it=map.entrySet().iterator(); it.hasNext();) {
            entry=(Map.Entry)it.next();
            copy.put(entry.getKey(), entry.getValue());
        }
        return copy.entrySet();
    }


    /**
     * @see org.jboss.cache.Cache#clear()
     */
    public void clear() throws CacheException, TimeoutException, LockingException {
        clear(getDefaultOptions());
    }

    /**
     * @see org.jboss.cache.Cache#clear(org.jboss.cache.Options)
     */
    public void clear(Options update_options) throws CacheException, TimeoutException, LockingException {
        Update update=null;
        if(update_options == null) {
            if(trace)
                log.trace("clear(): update_options was null, using default oprions");
            update_options=getDefaultOptions();
        }
        update=new Update(Update.CLEAR, update_options);
        handleUpdate(update);
    }

    /**
     * @see org.jboss.cache.Cache#put(java.lang.Object, java.lang.Object)
     */
    public Object put(Object key, Object value) throws CacheException, TimeoutException, LockingException {
        return put(key, value, getDefaultOptions());
    }

    /**
     * @see org.jboss.cache.Cache#put(java.lang.Object, java.lang.Object, org.jboss.cache.Options)
     */
    public Object put(Object key, Object value, Options update_options)
            throws CacheException, TimeoutException, LockingException {
        Update update=null;
        if(update_options == null) {
            if(trace)
                log.trace("put(): update_options was null, using default oprions");
            update_options=getDefaultOptions();
        }
        update=new Update(Update.PUT, key, value, update_options);
        return handleUpdate(update);
    }


    /**
     * @see org.jboss.cache.Cache#putAll(java.util.Map)
     */
    public void putAll(Map m) throws CacheException, TimeoutException, LockingException {
        putAll(m, getDefaultOptions());
    }


    /**
     * @see org.jboss.cache.Cache#putAll(java.util.Map, org.jboss.cache.Options)
     */
    public void putAll(Map m, Options update_options) throws CacheException, TimeoutException, LockingException {
        Update update=null;
        if(update_options == null) {
            if(trace)
                log.trace("putAll(): update_options was null, using default oprions");
            update_options=getDefaultOptions();
        }
        update=new Update(Update.PUT_ALL, map, update_options);
        handleUpdate(update);
    }


    /**
     * @see org.jboss.cache.Cache#remove(java.lang.Object)
     */
    public Object remove(Object key) throws CacheException, TimeoutException, LockingException {
        return remove(key, getDefaultOptions());
    }


    /**
     * @see org.jboss.cache.Cache#remove(java.lang.Object, org.jboss.cache.Options)
     */
    public Object remove(Object key, Options update_options)
            throws CacheException, TimeoutException, LockingException {
        Update update=null;
        if(update_options == null) {
            if(trace)
                log.trace("remove(): update_options was null, using default oprions");
            update_options=getDefaultOptions();
        }
        update=new Update(Update.REMOVE, key, update_options);
        return handleUpdate(update);
    }

    /* ---------------------------- Interface XAResource --------------------------- */
    /**
     *
     * @param xid
     * @param flags
     * @throws javax.transaction.xa.XAException
     */
    public void start(javax.transaction.xa.Xid xid, int flags)
            throws javax.transaction.xa.XAException {
    }

    public void end(javax.transaction.xa.Xid xid, int flags)
            throws javax.transaction.xa.XAException {
    }

    public int prepare(javax.transaction.xa.Xid xid)
            throws javax.transaction.xa.XAException {
        return 0;
    }

    public void commit(javax.transaction.xa.Xid xid, boolean onePhase)
            throws javax.transaction.xa.XAException {
    }

    public void rollback(javax.transaction.xa.Xid xid)
            throws javax.transaction.xa.XAException {
    }

    public void forget(javax.transaction.xa.Xid xid)
            throws javax.transaction.xa.XAException {
    }

    public javax.transaction.xa.Xid[] recover(int flag)
            throws javax.transaction.xa.XAException {
        return new javax.transaction.xa.Xid[0];
    }

    public boolean isSameRM(javax.transaction.xa.XAResource xaRes)
            throws javax.transaction.xa.XAException {
        return false;
    }

    public int getTransactionTimeout() throws javax.transaction.xa.XAException {
        return 0;
    }

    public boolean setTransactionTimeout(int seconds)
            throws javax.transaction.xa.XAException {
        return false;
    }
    /* ------------------------ End of Interface XAResource ------------------------ */






    /**
     * @see org.jboss.cache.Cache#setTransience(java.lang.Object, boolean)
     */
    public void setTransience(Object key, boolean transient_entry) throws KeyNotExistsException {
    }

    /**
     * @see org.jboss.cache.Cache#getTransience(java.lang.Object)
     */
    public boolean getTransience(Object key) throws KeyNotExistsException {
        return false;
    }


    /**
     * @see org.jboss.system.ServiceMBeanSupport#createService()
     */
    public void createService() throws Exception {
        trace=log.isTraceEnabled();
        debug=log.isDebugEnabled();
        info=log.isInfoEnabled();
        if(cluster_properties == null) {
            log.warn("CacheImpl.createService(): cluster properties not set; using default properties");
            cluster_properties="UDP(mcast_addr=230.1.2.3;mcast_port=25000;ip_ttl=32;" +
                    "mcast_send_buf_size=64000;mcast_recv_buf_size=80000):" +
                    "PING(timeout=2000;num_initial_members=3):" +
                    "MERGE2(min_interval=5000;max_interval=10000):" +
                    "FD:" +
                    "VERIFY_SUSPECT(timeout=1500):" +
                    "pbcast.STABLE(desired_avg_gossip=20000):" +
                    "pbcast.NAKACK(gc_lag=50;retransmit_timeout=800,1200,2400,4800):" +
                    "UNICAST(timeout=800,1200,2400,4800):" +
                    "FRAG(frag_size=8096;down_thread=false;up_thread=false):" +
                    "pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;" +
                    "shun=false;print_local_addr=true):" +
                    "pbcast.STATE_TRANSFER";
        }

        if(eviction_policy_classname != null && eviction_policy_classname.trim().length() > 0) {
            eviction_policy=createEvictionPolicy(eviction_policy_classname);
            eviction_policy.setCache(this);
            eviction_policy.create();
        }

        // TODO: set locking policy to default value

        Trace.init(); // TODO: replace with log4j adapter (similar to what Scott did)

        if(channel == null) {
            channel=new JChannel(cluster_properties);
            channel.setOpt(Channel.GET_STATE_EVENTS, new Boolean(true));
        }
        else {
            log.warn("CacheImpl.createService(): channel was not null (should have been null); " +
                     "channel was not created");
        }

        if(debug)
            log.debug("CacheImpl.createService(): channel was created with properties: " + cluster_properties);
        log.info("CacheImpl.createService(): service was created");
    }

    /**
     * @see org.jboss.system.ServiceMBeanSupport#destroyService()
     */
    public void destroyService() throws Exception {
        cluster_properties=null;
        if(eviction_policy != null) {
            eviction_policy.destroy();
            eviction_policy=null;
        }

        if(repl_mgr != null) {
            repl_mgr.stop();
            repl_mgr=null;
        }

        if(channel != null) {
            channel.close(); // will disconnect first if still connected
            channel=null;
        }
        log.info("CacheImpl.createService(): service was destroyed");
    }

    /**
     * @see org.jboss.system.ServiceMBeanSupport#startService()
     */
    public void startService() throws Exception {
        trace=log.isTraceEnabled();
        debug=log.isDebugEnabled();
        info=log.isInfoEnabled();
        if(eviction_policy != null)
            eviction_policy.start();

        if(channel == null)
            throw new Exception("CacheImpl.startService(): channel was null (cannot connect it)");

        repl_mgr=new ReplicationManager(channel, msg_listener, null, repl_receiver);
        channel.connect(cluster_name);

        if(channel.getState(null, state_timeout)) {
            log.info("CacheImpl.startService(): state was retrieved successfully");
        }
        else {
            log.info("CacheImpl.startService(): state could not be retrieved (first member)");
        }

        log.info("CacheImpl.createService(): service was started");
    }

    /**
     * @see org.jboss.system.ServiceMBeanSupport#stopService()
     */
    public void stopService() throws Exception {
        if(eviction_policy != null)
            eviction_policy.stop();
        if(repl_mgr != null) {
            repl_mgr.stop();
            repl_mgr=null;
        }
        if(channel != null) {
            channel.disconnect();
            if(info)
                log.info("CacheImpl.stopService(): channel was disconnected");
        }

        log.info("CacheImpl.stopService(): service was stopped");
    }


    /**
     * Internal access to the underlying hashmap. Maybe the methods that allow
     * access to the internal representation of the cache will be moved into
     * their own class.
     * @param key
     * @return
     */
    public CacheKey _get(Object key) {

        // TODO: check for locking
        return (CacheKey)map.get(key);
    }

    /**
     * Provides access to the underlying cache structure<br/>
     * TODO: move to accessor class
     * @return Set of {@link CacheKey}s
     */
    public Set _keySet() {
        return map.keySet();
    }

    /**
     * Provides access to the underlying cache<br/>
     * TODO: move to accessor class
     * @return Collection of {@link CacheValue}s
     */
    public Collection _values() {
        return map.values();
    }

    /**
     * Applies the change locally. May throw a LockingException if somebody else
     * has the write lock on this entry and we fail to acquire the lock within
     * <tt>lock_acquisition_timeout</tt> milliseconds.
     * @param key The key
     * @param value The value
     * @return Object The old value, or null if there was no existing entry
     * @throws LockingException Thrown if lock cannot be acquired within a
     * certain time.
     */
    public Object _put(Object key, Object value) throws LockingException {

        // TODO: check for lock (might be locked by another transaction)
        CacheKey old_key=null;
        old_key=(CacheKey)map.put(new CacheKey(key), new CacheValue(value));
        return old_key != null ? old_key.getKey() : null;
    }


    public Object _put(CacheKey key, CacheValue value) throws LockingException {
        //      TODO: check for lock (might be locked by another transaction)
        CacheKey old_key;
        old_key=(CacheKey)map.put(key, value);
        return old_key != null ? old_key.getKey() : null;
    }


    /**
     * Method _clear.
     */
    public void _clear() {

        // TODO: check for lock (might be locked by another transaction)
        map.clear();
    }

    /**
     *
     * @param key
     * @return Object
     */
    public Object _remove(Object key) {

        // TODO: check for lock (might be locked by another transaction)
        CacheKey old_key=null;
        old_key=(CacheKey)map.remove(key);
        return old_key != null ? old_key.getKey() : null;
    }

    /**
     * Method _putAll.
     * @param map
     */
    public void _putAll(Map map) {
        // TODO: check for lock (might be locked by another transaction)
        map.putAll(map);
    }


    /**
     * Method getDefaultOptions.
     * @return Options
     */
    public Options getDefaultOptions() {
        return new Options(caching_mode, sync_repl_timeout, lock_acquisition_timeout, lock_lease_timeout, false);
    }



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

    /**
     * Handles all updates (put(), putAll(), remove() and clear()). Depending on
     * the mode (transient, replication), the call will be applied locally
     * (transient) or sent to all receivers (replication). If the call is
     * replicated and synchronous, this method will process return values, e.g.
     * search for Exceptions or timeouts as return values.
     * @param update The update. Guaranteed to be non-null
     * @return Object An object if the original method needs a return value.
     * Otherwise the caller will simply discard this return value.
     */
    Object handleUpdate(Update update) throws CacheException, TimeoutException, LockingException {
        Options opts=update != null ? update.getUpdateOptions() : null;
        if(opts == null) {
            log.error("handleUpdate(): options == null");
            return null;
        }

        if(opts.getCachingMode() == Options.TRANSIENT)
            return handleTransientUpdate(update);
        else
            return handleReplicatedUpdate(update);
    }


    /**
     * Handles a transient update. Update is purely local, but still needs to
     * observe locked entries. For example, when another node has an entry X
     * locked, this update will have to wait until that entry is released (or
     * a timeout occurred).
     * @param update Update object. Guaranteed to be non-null
     * @return Object
     * @throws TimeoutException
     * @throws LockingException
     */
    Object handleTransientUpdate(Update update) throws TimeoutException, LockingException {
        switch(update.getOperation()) {
            case Update.PUT:
                return _put(new CacheKey(update.getKey()), new CacheValue(update.getValue(), true));
            case Update.PUT_ALL:
                _putAll(update.getMap());
            case Update.REMOVE:
                return _remove(update.getKey());
            case Update.CLEAR:
                _clear();
            default :
                log.error("handleTransientUpdate(): operation unknown (" + update.getOperation() + ")");
                return null;
        }
    }


    /**
     * Handles a replicated update. Will broadcast the update to all nodes
     * (using org.jgroups.blocks.ReplicationManager) and then wait for all
     * responses (if synchronous), or return immediately (if asynchronous). If
     * synchronous, all responses will be checked for exceptions. If all
     * responses haven't been received within the timeout, we will throw a
     * TimeoutException. If the call is synchronous with locking, and one of the
     * responses contains a LockingException, we will throw a LockingException
     * (possibly bundling all LockingExceptions received).
     * @param update
     * @return Object
     * @throws TimeoutException
     * @throws LockingException
     */
    Object handleReplicatedUpdate(Update update) throws CacheException, TimeoutException, LockingException {
        RspList rsps;
        byte[] data=null;
        Options opts;
        boolean synchronous=true;
        Object retval=null;

        if(repl_mgr == null)
            throw new CacheException("handleReplicatedUpdate(): replication manager is null, cannot replicate update");

        try {
            data=Util.objectToByteBuffer(update);
        }
        catch(Throwable t) {
            throw new CacheException("handleReplicatedUpdate(): error serializing update", t);
        }

        opts=update.getUpdateOptions();
        synchronous=opts.getCachingMode() == Options.REPL_SYNC;

        if(update.getKey() != null)
            retval=get(update.getKey());

        rsps=repl_mgr.send(null, // send to the group address
                           data,
                           synchronous,
                           opts.getSyncReplTimeout(),
                           update.getTransaction(),
                           null, // lock info not currently used
                           opts.getLockAcquisitionTimeout(),
                           opts.getLockLeaseTimeout(),
                           update.getTransaction() != null);

        //only check for results if the call was synchronous
        if(synchronous) {
            if(rsps == null) // this should *not* happen
                log.error("handleReplicatedUpdate(): response list was null");
            else
                checkResults(rsps); // will check for exceptions
        }

        return retval;
    }

    /**
     * Checks whether responses from members contain exceptions or timeouts.
     * Throws an exception if that is the case
     * @param rsps
     * @throws TimeoutException
     * @throws LockingException
     */
    void checkResults(RspList rsps) throws TimeoutException, LockingException {
        Map ml=null;
        List ll=null;
        LockingException l=null;
        TimeoutException t=null;
        Rsp rsp;

        for(int i=0; i < rsps.size(); i++) {
            rsp=(Rsp)rsps.elementAt(i);

            // check for exceptions
            if(rsp.getValue() != null && rsp.getValue() instanceof Throwable) {
                if(l == null) {
                    l=new LockingException(ml=new HashMap());
                }
                ml.put(rsp.getSender(), rsp.getValue());
            }

            if(l != null)
                throw l;

            // check for timeouts
            if(rsp.wasReceived() == false) {
                if(t == null)
                    t=new TimeoutException(ll=new ArrayList());
                ll.add(rsp.getSender());
            }
        }

        if(t != null)
            throw t;
    }


    /**
     *
     * @param classname
     * @return EvictionPolicy
     * @throws Exception
     */
    EvictionPolicy createEvictionPolicy(String classname) throws Exception {
        ClassLoader loader=Thread.currentThread().getContextClassLoader();
        Class cl=loader.loadClass(classname);
        return (EvictionPolicy)cl.newInstance();
    }

    /* ---------------------------- End of Private methods --------------------------- */


    /**
     * All updates are received by this class.
     *
     * @author Bela Ban
     * @version $Revision: 1.1.2.1 $
     */
    class ReplicationReceiverAdapter implements ReplicationReceiver {

        /**
         * @see org.jgroups.blocks.ReplicationReceiver#commit(org.jgroups.blocks.Xid)
         */
        public void commit(org.jgroups.blocks.Xid arg0) {
            ;
        }


        public Object receive(org.jgroups.blocks.Xid transaction, byte[] data, byte[] lock_info,
                              long lock_acquisition_timeout, long lock_lease_timeout,
                              boolean use_locks)
                throws org.jgroups.blocks.LockingException,
                org.jgroups.blocks.UpdateException {

            Update update=null;

            if(trace) {
                StringBuffer sb=new StringBuffer();
                sb.append("receive(): ");
                if(data != null)
                    sb.append(" data=").append(data.length).append(" bytes");
                sb.append(", use_locks=").append(use_locks);
                if(use_locks) {
                    if(transaction != null)
                        sb.append("transaction=").append(transaction);
                    sb.append(", lock_acquisition_timeout=").append(lock_acquisition_timeout);
                    sb.append("lock_lease_timeout=").append(lock_lease_timeout);
                }
                log.trace(sb.toString());
            }

            if(data == null) {
                log.error("receive(): data is null; cannot apply update");
                return null;
            }

            // TODO: check whether we really don't need locks (I believe we do)
            if(!use_locks) {
                try {
                    return handleTransientUpdate(update);
                }
                catch(Throwable t) {
                    log.error("receive()", t);
                    return t;
                }
            }
            else {
                // TODO: implement locking updates
            }

            return null;
        }

        /**
         * @see org.jgroups.blocks.ReplicationReceiver#rollback(org.jgroups.blocks.Xid)
         */
        public void rollback(org.jgroups.blocks.Xid arg0) {
            ;
        }

    }


    /**
     * This class handles initial state transfers to new members.
     *
     * @author Bela Ban
     * @version $Revision: 1.1.2.1 $
     */
    class MessageListenerAdapter implements MessageListener {

        public void receive(Message msg) {
            ;
        }

        /**
         * TODO: use read lock on entire hashmap while making copy
         */
        public byte[] getState() {
            Map copy=(Map)map.clone();

            try {
                return Util.objectToByteBuffer(copy);
            }
            catch(Throwable ex) {
                log.error("getState(): exception marshalling state", ex);
                return null;
            }
        }

        /**
         * TODO: use write lock on entire hashmap to set state
         */
        public void setState(byte[] state) {
            Map new_copy;

            try {
                new_copy=(Map)Util.objectFromByteBuffer(state);
                if(new_copy == null)
                    return;
            }
            catch(Throwable ex) {
                log.error("setState(): exception unmarshalling state", ex);
                return;
            }

            map.clear(); // remove all elements
            map.putAll(new_copy);
            log.info("setState(): hashmap has " + map.size() + " items");
        }
    }


    class MembershipListenerAdapter implements MembershipListener {

        /**
         * @see org.jgroups.MembershipListener#viewAccepted(org.jgroups.View)
         */
        public void viewAccepted(View view) {
// TODO: replace by log.info()
            System.out.println("new view: " + view);
        }

        /**
         * @see org.jgroups.MembershipListener#suspect(org.jgroups.Address)
         */
        public void suspect(org.jgroups.Address arg0) {
        }

        /**
         * @see org.jgroups.MembershipListener#block()
         */
        public void block() {
        }
    }

}
