Package org.jdesktop.wonderland.modules.sharedstate.client

Source Code of org.jdesktop.wonderland.modules.sharedstate.client.SharedMapImpl$VersionedValue

/**
* Open Wonderland
*
* Copyright (c) 2011, Open Wonderland Foundation, All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* The Open Wonderland Foundation designates this particular file as
* subject to the "Classpath" exception as provided by the Open Wonderland
* Foundation in the License file that accompanied this code.
*/

/**
* Project Wonderland
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied
* this code.
*/
package org.jdesktop.wonderland.modules.sharedstate.client;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.jdesktop.wonderland.client.cell.Cell;
import org.jdesktop.wonderland.client.cell.ChannelComponent;
import org.jdesktop.wonderland.client.comms.OKErrorResponseListener;
import org.jdesktop.wonderland.common.cell.messages.CellMessage;
import org.jdesktop.wonderland.common.messages.MessageID;
import org.jdesktop.wonderland.common.messages.ResponseMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedBoolean;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedByte;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedChar;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedData;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedDouble;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedFloat;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedInteger;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedLong;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedMap;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedShort;
import org.jdesktop.wonderland.modules.sharedstate.common.SharedString;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.ChangeValueMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.MapRequestMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.MapResponseMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.GetRequestMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.GetResponseMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.PutRequestMessage;
import org.jdesktop.wonderland.modules.sharedstate.common.messages.RemoveRequestMessage;

/**
* Implementation of the SharedMap interface.  This map is lazy, meaning it
* doesn't load the value for a key unless you explicitly request the value.
* At the time a request is made, the map queries the server for the
* given value.
* <p>
* In addition to lazily requesting values, this implementation also snoops
* for values. Whenever a value changes, the server sends a message to all
* clients.  When the client receives one of these notifications, it populates
* the relevant entry.
* <p>
* Although this implementation implements Map, it is implemented underneath
* using a list as the primary storage, and a HashMap of the list elements
* as secondary storage.  This is similar in concept to LinkedHashMap, but
* with correct Iterator semantics for remote operation. Iterators of this
* implementation never fail, they always return a valid
* iteration over the list that was correct at some point in time.
* <p>
* Both Iterators over this map, as well as the ContainsKey and ContainsValue
* method make requests to the server for values that aren't known, so are
* quite expensive.  Use with caution.
*
* @author jkaplan
*/
class SharedMapImpl implements SharedMapCli {
    private static final Logger logger =
            Logger.getLogger(SharedMap.class.getName());

    /** the unique name of this map */
    private String name;

    /** channel to communicate on */
    private ChannelComponent channel;

    /** the clientID of this client */
    private BigInteger localSenderID;

    /** whether or not the map is initialized */
    private boolean initializing = false;

    /** The backing map */
    private VersionedMap backing = new VersionedMap();

    /** An executor for requests */
    private ExecutorService executor = Executors.newCachedThreadPool();

    /** the types of version supported */
    private enum VersionType { LOCAL, REMOTE };

    /** a lock to protected large changes to the values map */
    private final Object valuesLock = new Object();

    /** listeners */
    private List<ListenerRecord> listeners =
            new CopyOnWriteArrayList<ListenerRecord>();

    /**
     * Create a new shared map
     * @return
     */
    public SharedMapImpl(String name, Cell cell, ChannelComponent channel) {
        this.name = name;
        this.channel = channel;

        // get our sender ID from the current session
        this.localSenderID = cell.getCellCache().getSession().getID();
    }

    public String getName() {
        return name;
    }

    public synchronized int size() {
        checkInit();
        return backing.size();
    }

    public boolean isEmpty() {
        checkInit();
        return backing.isEmpty();
    }

    public <T extends SharedData> T get(String key, Class<T> type) {
        return (T) get(key);
    }

    public synchronized SharedData get(Object key) {
        checkInit();

        // see if we have the key at all
        VersionedValue val = backing.get(key);
        if (val == null) {
            return null;
        }

        // get the value from the holder. If this value is being requested,
        // this call will block until it is returned
        return val.get();
    }

    public boolean containsKey(Object key) {
        checkInit();
        return backing.containsKey(key);
    }

    public boolean containsValue(Object value) {
        checkInit();

        for (VersionedValue h : backing.values()) {
            if (value.equals(h.get())) {
                return true;
            }
        }
       
        return false;
    }

    public SharedData put(final String key, final SharedData value) {
        checkInit();

        // put the new value into the map
        final VersionedResult res = backing.putLocal(key, value);
       
        // send the request for a change, and revert if there is an error
        channel.send(new PutRequestMessage(getName(), key, value),
                new OKErrorResponseListener()
        {
            @Override
            public void onSuccess(MessageID messageID) {
                // do nothing
            }

            @Override
            public void onFailure(MessageID messageID, String message,
                                  Throwable cause)
            {
                // revert
                backing.revertPut(key, res.getLocalVersion());
            }
         });

         // return the previous value
         return res.getPrevValue();
    }

    public SharedData remove(Object keyObj) {
        checkInit();

        final String key = (String) keyObj;

        // remove the value from the map
        SharedData prev = backing.removeLocal(key);

        // send the request for a change, and revert if there is an error
        channel.send(new RemoveRequestMessage(getName(), key),
                new OKErrorResponseListener()
        {
            @Override
            public void onSuccess(MessageID messageID) {
                // do nothing
            }

            @Override
            public void onFailure(MessageID messageID, String message,
                                  Throwable cause)
            {
                backing.revertRemove(key);
            }
        });

        // change the value to an internal one
        return prev;
    }

    public void putAll(Map<? extends String, ? extends SharedData> m) {
        checkInit();
        for (Entry<? extends String, ? extends SharedData> e : m.entrySet()) {
            put(e.getKey(), e.getValue());
        }
    }

    public void clear() {
        checkInit();
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public Set<String> keySet() {
        checkInit();
        return backing.keySet();
    }

    public Collection<SharedData> values() {
        checkInit();

        // XXX TODO: this should be backed by the real list XXX
        Collection<SharedData> out = new ArrayList<SharedData>(backing.size());
        for (VersionedValue val : backing.values()) {
            out.add(val.get());
        }

        return out;
    }

    public Set<Entry<String, SharedData>> entrySet() {
        checkInit();

        // XXX TODO: this should be backed by the real list XXX
        Set<Entry<String, SharedData>> out =
                new LinkedHashSet<Entry<String, SharedData>>();
        for (Entry<String, VersionedValue> e : backing.entrySet()) {
            out.add(new SharedDataEntry(e.getKey(), e.getValue().get()));
        }

        return out;
    }

    public void addSharedMapListener(SharedMapListenerCli listener) {
        addSharedMapListener(null, listener);
    }

    public void removeSharedMapListener(SharedMapListenerCli listener) {
        removeSharedMapListener(null, listener);
    }

    public void addSharedMapListener(String propRegex,
                                     SharedMapListenerCli listener)
    {
        listeners.add(new ListenerRecord(propRegex, listener));
    }

    public void removeSharedMapListener(String propRegex,
                                        SharedMapListenerCli listener)
    {
        listeners.remove(new ListenerRecord(propRegex, listener));
    }
   
    public String getString(String key) {
        SharedString value = get(key, SharedString.class);
        return (value == null ? null : value.getValue());
    }

    public void putString(String key, String value) {
        put(key, SharedString.valueOf(value));
    }

    public boolean getBoolean(String key) {
        SharedBoolean value = get(key, SharedBoolean.class);
        return (value == null ? false : value.getValue());
    }

    public void putBoolean(String key, boolean value) {
        put(key, SharedBoolean.valueOf(value));
    }

    public byte getByte(String key) {
        SharedByte value = get(key, SharedByte.class);
        return (value == null ? 0 : value.getValue());
    }

    public void putByte(String key, byte value) {
        put(key, SharedByte.valueOf(value));
    }

    public char getChar(String key) {
        SharedChar value = get(key, SharedChar.class);
        return (value == null ? 0 : value.getValue());
    }

    public void putChar(String key, char value) {
        put(key, SharedChar.valueOf(value));
    }

    public short getShort(String key) {
        SharedShort value = get(key, SharedShort.class);
        return (value == null ? 0 : value.getValue());
    }

    public void putShort(String key, short value) {
        put(key, SharedShort.valueOf(value));
    }

    public int getInt(String key) {
        SharedInteger value = get(key, SharedInteger.class);
        return (value == null ? 0 : value.getValue());
    }

    public void putInt(String key, int value) {
        put(key, SharedInteger.valueOf(value));
    }

    public long getLong(String key) {
        SharedLong value = get(key, SharedLong.class);
        return (value == null ? 0 : value.getValue());
    }

    public void putLong(String key, long value) {
        put(key, SharedLong.valueOf(value));
    }

    public float getFloat(String key) {
        SharedFloat value = get(key, SharedFloat.class);
        return (value == null ? 0f : value.getValue());
    }

    public void putFloat(String key, float value) {
        put(key, SharedFloat.valueOf(value));
    }

    public double getDouble(String key) {
        SharedDouble value = get(key, SharedDouble.class);
        return (value == null ? 0.0 : value.getValue());
    }

    public void putDouble(String key, double value) {
        put(key, SharedDouble.valueOf(value));
    }

    void handleMessage(ChangeValueMessage cvm) {
        VersionedValue prev;

        synchronized (valuesLock) {
            switch (cvm.getAction()) {
                case PUT:
                    backing.putRemote(cvm.getSenderID(),
                                      cvm.getPropertyName(),
                                      cvm.getVersion(),
                                      cvm.getPropertyValue());
                    break;
                case REMOVE:
                    backing.removeRemote(cvm.getSenderID(),
                                         cvm.getPropertyName(),
                                         cvm.getVersion());
                    break;
                default:
                    logger.warning("Unsupported operation: " + cvm.getAction());
                    break;
            }
        }
    }

    /**
     * Wait for the map to finish initializing
     */
    synchronized void waitForInit() throws InterruptedException {
        // if we are not already initializing, start now
        if (!isInitialized() && !initializing) {
            initializing = true;
           
            // read the initial data for this map in a new thread
            executor.submit(new Runnable() {
                public void run() {
                    doInit();
                }
            });
        }

        // wait for initialization to finish
        while (!isInitialized()) {
            wait();
        }
    }

    /**
     * Initialize the map by reading in all the keys. This method blocks
     * until the server has sent us the keys for this map.
     */
    private void doInit() {
        try {
            ResponseMessage rm = channel.sendAndWait(new MapRequestMessage(getName()));
            if (rm instanceof MapResponseMessage) {
                MapResponseMessage mrm = (MapResponseMessage) rm;
                backing.initialize(mrm.getVersion(), mrm.getKeys());
            } else {
                throw new IllegalStateException("Bad response to map request: " +
                                                rm.getClass().getName());
            }

        } catch (InterruptedException ie) {
            // oh well
        } finally {
            // no matter what, now is good time to wake up all the listeners
            synchronized (this) {
                initializing = false;
                notifyAll();
            }
        }
    }

    /**
     * Check to make sure the map is initialized before doing anything
     */
    private synchronized void checkInit() {
        if (!isInitialized()) {
            throw new IllegalStateException("Not initialized");
        }
    }

    private synchronized boolean isInitialized() {
        return backing.isInitialized();
    }
   
    class ValueGetter implements Callable<VersionedValue> {
        private String propName;

        public ValueGetter(String propName) {
            this.propName = propName;
        }

        public VersionedValue call() throws InterruptedException {
            CellMessage m = new GetRequestMessage(getName(), propName);
            ResponseMessage rm = channel.sendAndWait(m);
            if (rm instanceof GetResponseMessage) {
                GetResponseMessage grm = (GetResponseMessage) rm;
                return new ImmediateVersionedValue(VersionType.REMOTE,
                                                   grm.getVersion(), grm.getData());
            } else {
                throw new IllegalStateException("Invalid response to value " +
                        "request: " + rm.getClass());
            }
        }
    }

    class VersionedMap extends ConcurrentHashMap<String, VersionedValue> {
        /**
         * A map from key to values to revert to.  This map is used to support
         * reverting local versions back to the original version if something
         * fails.  A null value in this map means that on reversion, the given
         * key should be removed from the map.
         */
        private final Map<String, VersionedValue> reversions =
                                    new LinkedHashMap<String, VersionedValue>();
       
        /**
         * Whether or not this maps has been initialized.  On initialization,
         * a list of all the keys in the map are passed in.
         */
        private boolean initialized = false;

        /** the next local version number */
        private long nextLocalVersion = 0;

        /**
         * Whether or not the map has been initialized
         * @return true if the map is initialized, or false if not
         */
        public synchronized boolean isInitialized() {
            return initialized;
        }

        /**
         * Initialize this map with the given list of keys
         * @param version the version that the map should be initialized to
         * @param keys the list of keys to set
         */
        public synchronized void initialize(long version, Collection<String> keys) {
            // create a map with all keys set to either the value from
            // the reversions map (if the value was previously initialized)
            // or a lazy getter for the value if not
            Map<String, VersionedValue> vals =
                    new LinkedHashMap<String, VersionedValue>();
            for (String key : keys) {
                VersionedValue cur = reversions.get(key);

                // there are three cases here:
                // 1. there is no current value, in which case we want to add
                // a lazy getter
                // 2. there is a current value, in which case, we want to add
                // the value itself
                // 3. there is a current value, but get() returns null.  This
                // means the value represents a removal from the map.  We only
                // want to apply this removal if its version number is
                // *later* than the one we have

                if (cur != null && cur.get() != null) {
                    // add the value in
                    vals.put(key, cur);
                } else if (cur == null || cur.getVersion() < version) {
                    // add a lazy value
                    vals.put(key, new FutureVersionedValue(version, key));
                } else {
                    // do nothing -- this should only happen when the version
                    // of a removal is greater than the version we have.  In
                    // this case, we want to skip the key altogether
                }
            }

            // clear the reversions list
            reversions.clear();

            // apply the changes all at once to the underlying map
            super.putAll(vals);
           
            // we are now initialized
            initialized = true;
        }

        /**
         * Put a value from a local source.  Only valid after the map has
         * been initialized.
         * @param key the key to put
         * @param value the value to put
         * @return two pieces of information: the version of the new data that
         * was added to the map (that must be passed in during a revert) and
         * the previous value of this key
         */
        public synchronized VersionedResult putLocal(String key,
                                                     SharedData value)
        {
            // make sure we have been initialized
            if (!isInitialized()) {
                throw new IllegalStateException("Map not initialized");
            }

            // get the version for this action
            long localVersion = nextLocalVersion();

            // create a new local object to put in the map
            VersionedValue val = new ImmediateVersionedValue(VersionType.LOCAL,
                                                           localVersion, value);

            // add this new, local value to the map, and simultaneously get
            // the current version to revert to if necessary
            VersionedValue prev = super.put(key, val);

            // add the previous version to the reversion list
            addToReversionList(key, prev);

            // store the previous value, and then clear the result so
            // anyone waiting on the previous value will get this replacement
            SharedData prevVal = null;
            if (prev != null) {
                prevVal = prev.peek();
                prev.clear(value);
            }

            // return the result and newly created local version number
            return new VersionedResult(localVersion, prevVal);
        }

        /**
         * Put a value from a remote source.  Make sure the version is later
         * than the current version.  Also, if there is any reversion
         * information for this key in the map, remove it.
         * @param senderID the id of the intiator of this change
         * @param key the key to put
         * @param version the version of the value
         * @param data the data to put
         */
        public void putRemote(BigInteger senderID, String key, long version,
                              SharedData data)
        {
            // create a versioned object for this value
            VersionedValue val = new ImmediateVersionedValue(VersionType.REMOTE,
                                                             version, data);
            VersionedValue prev = null;

            synchronized (this) {
                // if the map is not yet initialized, just add this value to the
                // reversion list.  The reversion list will be processed in the
                // initialize() method above.
                if (!isInitialized()) {
                    reversions.put(key, val);
                    return;
                }

                // see if we have an existing version of this value in the map
                prev = get(key);
                if (prev != null && (compare(val, prev) < 0)) {
                    // older version -- skip the update
                    return;
                }

                // we are ok to replace the value
                super.put(key, val);

                // a local remote value always overwrites a remote one, so make
                // sure to clear this value from the reversions map.  The value
                // in the reversion map is also the previous value for this
                // key (before our local put).  If this key exists in the
                // reversion list, use the value on the reversion list
                // as the previous value (even if it is null).
                boolean inReversionList = reversions.containsKey(key);
                VersionedValue revert = reversions.remove(key);
                if (inReversionList) {
                    prev = revert;
                }
            }

            // notify listeners
            SharedData prevData = (prev == null) ? null : prev.peek();
            firePropertyChange(senderID, key, prevData, data);
        }

        /**
         * Revert a key that was put to the previous value.  The reversion will
         * only take place if the current value in the map has a local version
         * number,which is the same as the number that is passed in.  Reversion
         * may cause the value to be removed from the map if the value was
         * previously empty.
         * @param key the key to rever
         * @param localVersion the version number of the change we are reverting
         */
        public void revertPut(String key, long localVersion) {
            VersionedValue cur = null;
            VersionedValue revert = null;

            synchronized (this) {
                cur = get(key);

                // make sure we are OK to revert -- the current value must exist
                // and have a local version number equal to the one that was
                // passed in
                if (cur == null ||
                        cur.getVersionType() != VersionType.LOCAL ||
                        cur.getVersion() != localVersion)
                {
                    return;
                }

                // get the proper version from the reversion list
                revert = reversions.remove(key);
                if (revert == null) {
                    // if there is no value on the reversions list, it means we
                    // should remove the given value from the map on revert
                    super.remove(key);
                } else {
                    // revert
                    super.put(key, revert);
                }
            }

            // notify listeners
            SharedData curData = (cur == null) ? null : cur.peek();
            SharedData revData = (revert == null) ? null : revert.peek();
            firePropertyChange(localSenderID, key, curData, revData);
        }

        /**
         * Remove a value from a local source. Store the previous value in
         * case of a reversion.
         * @param key the key to remove
         * @return the previous value of the key, or null if there wasn't one
         */
        public synchronized SharedData removeLocal(String key) {
            // remove the current value from the map
            VersionedValue prev = super.remove(key);

            // add the previous version to the reversion list
            addToReversionList(key, prev);

            // get the value to return before clearing the result
            SharedData out = null;
            if (prev != null) {
                out = prev.peek();

                // clear the previous value, so anyone waiting on it will get
                // this replacement
                prev.clear(null);
            }

            // return the previous value
            return out;
        }

        /**
         * Remove a value from a remote source.  Clear the reversion list
         * for this value.
         * @param senderID the id of the initiator of this change
         * @param key the property to remove
         * @param version the version of the removal
         */
        public void removeRemote(BigInteger senderID, String key, long version) {
            // create a versioned object for this value
            VersionedValue val = new ImmediateVersionedValue(VersionType.REMOTE,
                                                             version, null);
            VersionedValue prev = null;

            synchronized (this) {
                // if the map is not yet initialized, add a null value to the
                // reversion list.  Applying this value will happen in the
                // initialize() method above.
                if (!isInitialized()) {
                    reversions.put(key, val);
                    return;
                }

                // see if we have an existing version of this value in the map
                prev = get(key);
                if (prev != null && (compare(val, prev) < 0)) {
                    // older version -- skip the update
                    return;
                }

                // we are ok to remove the value
                super.remove(key);

                // a local remote value always overwrites a remote one, so make
                // sure to clear this value from the reversions map.  The value
                // in the reversion map is also the previous value for this
                // key (before our local put).  If this key exists in the
                // reversion list, use the value on the reversion list
                // as the previous value (even if it is null).
                boolean inReversionList = reversions.containsKey(key);
                VersionedValue revert = reversions.remove(key);
                if (inReversionList) {
                    prev = revert;
                }
            }

            // notify listeners
            SharedData prevData = (prev == null) ? null : prev.peek();
            firePropertyChange(senderID, key, prevData, null);
        }

         /**
         * Revert a key that was removed to the previous value.  The reversion
         * will only take place if there is no current value in the map and
         * the reversion map has an entry for the given value.
         * @param key the key to revert
         */
        public void revertRemove(String key) {
            VersionedValue cur = null;
            VersionedValue revert = null;

            synchronized (this) {
                cur = get(key);

                // make sure we are OK to revert -- the current value must exist
                // and have a local version number equal to the one that was
                // passed in
                if (cur != null) {
                    return;
                }

                // get the proper version from the reversion list
                revert = reversions.remove(key);
                if (revert != null) {
                    super.put(key, revert);
                }
            }
           
            // notify listeners
            SharedData curData = (cur == null) ? null : cur.peek();
            SharedData revData = (revert == null) ? null : revert.peek();
            firePropertyChange(localSenderID, key, curData, revData);
        }

        /**
         * Add a value to the reversion list
         */
        private void addToReversionList(String key, VersionedValue prev) {
            // there are three possible cases here:
            // 1. the previous value is null, so make sure that no value is
            // on the reversion list
            // 2. the previous value is a remote value, so add that value to
            // the reversion list
            // 3. the previous version is local, in which case the reversion
            // list should already contain the correct version to revert to
            if (prev == null || prev.getVersionType() == VersionType.REMOTE) {
            
                // it is either case 1. or 2., so we should put the value on
                // the list. Putting a null on the list is fine -- if we revert
                // it will simply remove the value from the underlying map
                reversions.put(key, prev);
            }
        }

        /**
         * Versioned maps don't support putAll()
         */
        @Override
        public void putAll(Map<? extends String, ? extends VersionedValue> m) {
            throw new UnsupportedOperationException("putAll() not supported");
        }

        /**
         * Versioned maps don't support the clear operation
         */
        @Override
        public void clear() {
            throw new UnsupportedOperationException("Clear not supported");
        }

        /**
         * Return true of one value has a newer version than another.
         * @param other the other value to compare
         * @return -1 if the first value is smaller than the second, 0
         * if the two values are the same, or 1 if the second value is larger
         */
        public int compare(VersionedValue val1, VersionedValue val2) {
            // first see, if one value is local and the other remote.  Remote
            // values are always greater than local values
            if (val1.getVersionType() == VersionType.LOCAL &&
                    val2.getVersionType() == VersionType.REMOTE) {
                return -1;
            } else if (val1.getVersionType() == VersionType.REMOTE &&
                    val2.getVersionType() == VersionType.LOCAL) {
                return 1;
            }

            // at this point, we know both values are either local or remote
            // so we can just compare the version numbers
            if (val1.getVersion() < val2.getVersion()) {
                return -1;
            } else if (val2.getVersion() > val1.getVersion()) {
                return 1;
            }

            // they are equal
            return 0;
        }

        /**
         * Fire a property change event. It is a good idea to make sure you
         * don't hold any locks before calling this method, since it calls
         * outside this class.
         */
        private void firePropertyChange(BigInteger senderID, String key,
                                        SharedData oldValue, SharedData newValue)
        {
            for (ListenerRecord lr : listeners) {
                if (lr.matches(key)) {
                    lr.getListener().propertyChanged(new SharedMapEventCli(
                                                     SharedMapImpl.this,
                                                     senderID,key, oldValue,
                                                     newValue));
                }
            }
        }

        /**
         * Get the next local version number
         */
        private synchronized long nextLocalVersion() {
            return nextLocalVersion++;
        }
    }

    class VersionedResult {
        private long localVersion;
        private SharedData prevValue;

        public VersionedResult(long localVersion, SharedData prevValue) {
            this.localVersion = localVersion;
            this.prevValue = prevValue;
        }

        public long getLocalVersion() {
            return localVersion;
        }

        public SharedData getPrevValue() {
            return prevValue;
        }
    }

    /**
     * Holds a value in the map.
     */
    interface VersionedValue {
        /**
         * Get the type of version
         * @return the version type
         */
        public VersionType getVersionType();

        /**
         * Get the version associated with this value
         * @return the version
         */
        public long getVersion();

        /**
         * Get the value.  This method blocks until data is available.
         * @return the value.
         */
        public SharedData get();

        /**
         * Get the value but don't block. Return null if the data is not
         * immediately available
         * @return the value,  or null if the value isn't available
         */
        public SharedData peek();

        /**
         * Remove any value assocated with this holder.  Stop any requests
         * in progress.
         * @param newVal the new value to return to anyone waiting on a get
         * @return the value that was removed, or null if no value was removed.
         */
        public SharedData clear(SharedData newVal);
    }

    class FutureVersionedValue implements VersionedValue {
        private long version;
        private String key;

        private Future<VersionedValue> future;
        private SharedData replacement;
       
        public FutureVersionedValue(long version, String key) {
            this.key = key;
            this.version = version;
        }

        public VersionType getVersionType() {
            return VersionType.REMOTE;
        }

        public synchronized long getVersion() {
            return version;
        }

        synchronized void setVersion(long version) {
            this.version = version;
        }

        public SharedData get() {
            SharedData out = null;

            // if the future does not yet exist, create it
            synchronized (this) {
                if (future == null) {
                    future = executor.submit(new ValueGetter(key));
                }
            }

            try {
                // this should return an immediate value, otherwise we could
                // wait again...
                VersionedValue res = future.get();
                out = res.get();
                setVersion(res.getVersion());
            } catch (InterruptedException ie) {
                logger.log(Level.WARNING, "Get value interrupted", ie);
            } catch (ExecutionException ee) {
                logger.log(Level.WARNING, "Error getting value", ee);
            } catch (CancellationException ce) {
                // the request was cancelled, see if it was replaced
                out = getReplacement();
            }

            return out;
        }

        public SharedData peek() {
            if (future != null && future.isDone()) {
                return get();
            } else {
                return null;
            }
        }

        public SharedData clear(SharedData newVal) {
            SharedData out = null;
            setReplacement(newVal);

            if (future != null) {
                if (future.isDone()) {
                    out = get();
                } else {
                    future.cancel(true);
                   
                    synchronized (this) {
                        future = null;
                    }
                }
            }
           
            return out;
        }
       
        synchronized void setReplacement(SharedData replacement) {
            this.replacement = replacement;
        }
       
        synchronized SharedData getReplacement() {
            return replacement;
        }
    }

    class ImmediateVersionedValue implements VersionedValue {
        private VersionType type;
        private long version;
        private SharedData value;

        public ImmediateVersionedValue(VersionType type, long version,
                                       SharedData value)
        {
            this.type = type;
            this.version = version;
            this.value = value;
        }

        public VersionType getVersionType() {
            return type;
        }

        public long getVersion() {
            return version;
        }

        public SharedData get() {
            return value;
        }

        public SharedData peek() {
            return get();
        }

        public SharedData clear(SharedData newVal) {
            return value;
        }
    }

    class SharedDataEntry implements Entry<String, SharedData> {
        private String key;
        private SharedData value;

        public SharedDataEntry(String key, SharedData value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public SharedData getValue() {
            return value;
        }

        public SharedData setValue(SharedData value) {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    }

    class ListenerRecord {
        private String propRegex;
        private Pattern pattern;
        private SharedMapListenerCli listener;

        public ListenerRecord(String propRegex, SharedMapListenerCli listener) {
            this.propRegex = propRegex;
            this.listener = listener;

            if (propRegex != null) {
                this.pattern = Pattern.compile(propRegex);
            }
        }

        public String getPropRegex() {
            return propRegex;
        }

        public SharedMapListenerCli getListener() {
            return listener;
        }

        public boolean matches(String key) {
            if (pattern == null) {
                return true;
            }
           
            return pattern.matcher(key).matches();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ListenerRecord other = (ListenerRecord) obj;
            if ((this.propRegex == null) ? (other.propRegex != null) : !this.propRegex.equals(other.propRegex)) {
                return false;
            }
            if (this.listener != other.listener && (this.listener == null || !this.listener.equals(other.listener))) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 23 * hash + (this.propRegex != null ? this.propRegex.hashCode() : 0);
            hash = 23 * hash + (this.listener != null ? this.listener.hashCode() : 0);
            return hash;
        }
    }
}
TOP

Related Classes of org.jdesktop.wonderland.modules.sharedstate.client.SharedMapImpl$VersionedValue

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.