/**
 * MemCached Java client
 * Copyright (c) 2003
 * Richard 'toast' Russo <russor@msoe.edu>
 * http://people.msoe.edu/~russor/memcached
 *
 * Originally translated from Brad Fitzpatrick's <brad@danga.com> MemCached Perl client
 * See the memcached website:
 * http://www.danga.com/memcached/
 *
 * This module is Copyright (c) 2003 Richard Russo.
 * All rights reserved.
 * You may distribute under the terms of the GNU General Public License
 * This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND.
 *
 * @author  Richard 'toast' Russo <russor@msoe.edu>
 * @version 0.9.1
 */


package com.danga.MemCached;


import java.util.zip.*;
import java.util.*;
import java.util.Map.*;
import java.io.*;



/** This is a Java client for the memcached server available from
 *  <a href="http:/www.danga.com/memcached/">http://www.danga.com/memcached/</a>.*/
public class MemCachedClient {
    
    final int F_COMPRESSED = 2;
    final int F_SERIALIZED = 8;
    //using 8 (1 << 3) so other clients don't try to unpickle/unstore/whatever
    //things that are serialized... I don't think they'd like it. :)
    
    ArrayList buckets;
    HashMap host_dead;
    HashMap stats;
    HashMap sockets;
    boolean debug = false;
    boolean forceserial = false;
    
    String singlesock = null;
    double compress_savings = 0.20;
    boolean compress_enable = true;
    int compress_threshold = 1024; // FIXME: is this a reasonable default??
    
    /** Creates a new instance of MemCachedClient.
     *
     * By default, compression is enabled, with a threshold of 1024, and required
     * savings of 0.2; debug is disabled; forced serialization is disabled; and the
     * server list is empty.
     */
    public MemCachedClient() {
    }
    
    
    /** Sets the required change in size for value storage to use compression.
     * This rate is expressed as a decimal, for example 0.20 means that unless the data
     * becomes 20% smaller (it will then be at 80% of its original size), it will be
     * stored in uncompressed form.
     *
     * The value defaults to 0.20.
     * @param d required compression to store compressed data
     */
    public void set_compress_savings(double d) {
        compress_savings = d;
    }
    
    /** Enable storing compressed data, provided it meets the threshold and savings
     * requirements. If enabled, data will be stored in compressed form if it is
     * longer than the threshold length set with {@link #set_compress_threshold(int) set_compress_threshold()}
     * and compression results in a space savings of at least the rate set with
     * {@link #set_compress_savings(double) set_compress_savings()}.
     *
     * The default is that compression is enabled.
     *
     * Even if compression is disabled, compressed data will be automatically
     * decompressed.
     * @param b <CODE>true</CODE> to enable compression, <CODE>false</CODE> to disable compression
     */
    public void set_compress_enable(boolean b) {
        compress_enable = b;
    }
    
    /** Sets the required length for data to be considered for compression. If the
     * length of the data to be stored is not equal or larger than this value, it will
     * not be compressed.
     *
     * This defaults to 1024.
     * @param i required length of data to consider compression
     */
    public void set_compress_threshold(int i) {
        compress_threshold = i;
    }
    
    /** This function lets you tell the client to always serialize data.  This is mostly
     * useful for debugging; or if you don't want to store stringified numbers.
     * @param b True if you want to always serialize data, false if you want to let the client
     * decide.
     *
     */
    public void set_serial(boolean b) {
        forceserial = b;
    }
    
    /** Turns on or off debugging information.  If enabled, the client will print
     * status messages that may be helpful in tracking the progress of data through the
     * system.  If you're not having problems, this is probably not useful for you.
     * @param b <CODE>true</CODE>, if debugging messages are desired, <CODE>false</CODE> if debugging messages are not
     * desired
     */
    public void set_debug(boolean b) {
        debug = b;
    }
    
    /** This lets you determine if the client will serialize everything, or only data
     * types that it doesn't know to stringify
     * @return true, if the client is serializing everything
     */
    public boolean get_serial() {
        return forceserial;
    }
    
    /** Set the list of servers to use. All servers have an equal weight (1)
     * @param serverlist An array of servers to use; servers should be in the form ip:port.  Hostnames
     * are acceptable.
     */
    public void set_servers(String[] serverlist) {
        set_servers(serverlist, null);
    }
    
    /** Sets the list of servers, and their weights; For best results keep the weights
     * as low as possible. If the weightlist is shorter than the serverlist, remaining
     * servers are defaulted to a weight of 1.
     * @param serverlist A list of servers, in the form ip:port. Hostnames are also acceptable
     * @param weightlist A list of weights for the servers. Negative or zero values will be ignored, and
     * the default weight of 1 will be used instead
     *
     */
    public void set_servers(String[] serverlist, int[] weightlist) {
/* I'm just going to construct the bucket list here, since this is the only place
 * that the server list is going to change. The perl client constructs it elsewhere. */
        buckets = new ArrayList();
        host_dead = new HashMap();
        sockets = new HashMap();
        if (serverlist.length == 1) {
            singlesock = serverlist[0];
        } else {
            singlesock = null;
        }
        
        
        for (int i = 0; i < serverlist.length; ++i) {
            if (weightlist != null && weightlist.length >i) {
                for (int j = 0; j < weightlist[i]; ++j) {
                    buckets.add(serverlist[i]);
                }
            } else {
                buckets.add(serverlist[i]);
            }
        }
    }
    
    private SockIO get_sock(Object key) {
        if (singlesock != null) {
            return sock_to_host(singlesock);
        }
        if (key.getClass() == Integer.class) {
            return get_sock(((Integer)key).intValue());
        } else {
            return get_sock(key.toString());
        }
    }
    
    
    private SockIO get_sock(String key) {
        if (singlesock != null) {
            return sock_to_host(singlesock);
        }
        return get_sock(hashfunc(key));
    }
    
    private SockIO get_sock(int key) {
        if (singlesock != null) {
            return sock_to_host(singlesock);
        }
        if ( buckets.size() == 0) {
            return null;
        }
        
        int tries = 0;
        int hv = key;
        while (tries++ < 20) {
            String host = (String) buckets.get(hv % buckets.size());
            SockIO sock = sock_to_host(host);
            if (sock != null) {
                return sock;
            }
            hv += hashfunc("" + tries + key);  // stupid, but works
        }
        return null;
    }
    
    private SockIO sock_to_host(String host) {
        Date now = new Date();
        int tmp = host.indexOf(":");
        String[] ip ={ host.substring(0,tmp), host.substring(tmp+1)};
        
        if ((host_dead.containsKey(host) && now.before((Date)host_dead.get(host))) ||
        (host_dead.containsKey(ip[0]) && now.before((Date)host_dead.get(ip[0])))) {
            return null;
        }
        
        if (sockets.containsKey(host) && ((SockIO)sockets.get(host)).isConnected()) {
            return (SockIO) sockets.get(host);
        }
        
        SockIO sock;
        try {
            sock = new SockIO(ip[0], Integer.decode(ip[1]).intValue());
            
        } catch (Exception e) {
            sock = null;
        }
        if (sock != null) {
            sockets.put(host, sock);
            host_dead.remove(host);
            host_dead.remove(ip);
            return sock;
        }
        now = new Date();
        host_dead.put(host, new Date(now.getTime() + 60000 + (int)(java.lang.Math.random() * 10000)));
        host_dead.put(ip[0], new Date(now.getTime() + 60000 + (int)(java.lang.Math.random() * 10000)));
        if (debug) {
            System.out.println("MemCachedClient: marking " + host + " (" + ip[0] + ") dead\n");
        }
        return null;
    }
    
    private int hashfunc(String key) {
        int hash = 0;
        for (int i = 0; i < key.length(); ++i) {
            hash = hash*33 + key.charAt(i);
        }
        return hash;
    }
    
    /** Forget that servers were unreachable. This is useful if a network connection has
     * been restored, and servers that were recently unreachable have become reachable
     * again.
     */
    public void forget_dead_hosts() {
        host_dead.clear();
    }
    
    /** Disconnect from all memcached servers. */
    public void disconnect_all() {
        Iterator i = sockets.values().iterator();
        while (i.hasNext()) {
            ((SockIO)i.next()).close();
            i.remove();
        }
    }
    
    /* wouldn't it be nice if java had default parameters like c? */
    
    /** Deletes a key from the server; only the key is specified.
     * @param key the key to be removed
     * @return true, if the data was deleted successfully
     */
    public boolean delete(String key) {
        return delete(key,null, null);
    }
    /** Deletes a key from the server; the key, and a hash value are specified.
     * @return true, if the data was deleted successfully
     * @param hash used to determine which server is responsible for the specified key
     * @param key the key to be removed
     */
    public boolean delete(String key, int hash) {
        return delete(key,null,new Integer(hash));
    }
    /** Deletes a key from the server; the key, and a hash value are specified.
     * @return true, if the data was deleted successfully
     * @param hash used to determine which server is responsible for the specified key
     * @param key the key to be removed
     */
    public boolean delete(String key, Integer hash) {
        return delete(key, null, hash);
    }
    /** Deletes a key from the server; the key, and a delete time are specified. The item
     *  is immediately made non retrievable, however {@link #add(String, Object) add} and
     *  {@link #replace(String, Object) replace} will fail when used with the same key will
     *  fail, until the server reaches the specified time. However,
     *  {@link #set(String, Object) set} will succeed, and the new value will not
     *  be deleted.
     *
     * @return true, if the data was deleted successfully
     * @param expiry when to expire the record
     * @param key the key to be removed
     */
    
    public boolean delete(String key, Date expiry) {
        return delete(key,expiry,null);
    }
    /** Deletes a key from the server; the key, a delete time are specified, and a hash value
     *  are specified. The item is immediately made non retrievable, however
     *  {@link #add(String, Object) add} and {@link #replace(String, Object) replace}
     *  will fail when used with the same key will fail, until the server reaches the
     *  specified time. However, {@link #set(String, Object) set} will succeed,
     *  and the new value will not be deleted.
     *
     * @return true, if the data was deleted successfully
     * @param expiry when to expire the record.
     * @param hash used to determine which server is responsible for the specified key
     * @param key the key to be removed
     */
    public boolean delete(String key, Date expiry, int hash) {
        return delete(key, expiry, new Integer(hash));
    }
    
    /** Deletes a key from the server; the key, a delete time are specified, and a hash value
     *  are specified. The item is immediately made non retrievable, however
     *  {@link #add(String, Object) add} and {@link #replace(String, Object) replace}
     *  will fail when used with the same key will fail, until the server reaches the
     *  specified time. However, {@link #set(String, Object) set} will succeed,
     *  and the new value will not be deleted.
     *
     * @return true, if the data was deleted successfully
     * @param expiry when to expire the record.
     * @param hash used to determine which server is responsible for the specified key
     * @param key the key to be removed
     */
    public boolean delete(String key, Date expiry, Object hash) {
        SockIO sock;
        if (hash != null) {
            sock = get_sock(hash);
        } else {
            sock = get_sock(key);
        }
        
        if (sock == null) {
            return false;
        }
        String command = "delete " + key;
        if (expiry != null) {
            command = command + " " + expiry.getTime() / 1000;
        }
        command = command + "\r\n";
        
        try {
            sock.writeBytes(command);
            sock.flush();
            
            command = sock.readLine();
            if (command.equals("DELETED")) {
                return true;
            }
        } catch (IOException e) {
            sock.close();
        }
        return false;
    }
    
    /* Blah.... Default parameters would be _REALLY_ nice here */
    
    /** Stores data on the server; only the key and the value are specified.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @return true, if the data was successfully stored
     */
    public boolean set(String key, Object value) {
        return set("set", key, value, null, null);
    }
    /** Stores data on the server; the key, value, and a hash value are specified.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @return true, if the data was successfully stored
     */
    public boolean set(String key, Object value, int hash) {
        return set("set",key,value,null,new Integer(hash));
    }
    /** Stores data on the server; the key, value, and a hash value are specified.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @return true, if the data was successfully stored
     */
    public boolean set(String key, Object value, Integer hash) {
        return set("set",key,value, null, hash);
    }
    
    /** Stores data on the server; the key, value, and an expiration time are specified.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean set(String key, Object value, Date expiry) {
        return set("set",key,value,expiry,null);
    }
    /** Stores data on the server; the key, value, an expiration time, and a hash value are specified.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean set(String key, Object value, Date expiry, int hash) {
        return set("set",key,value, expiry, new Integer(hash));
    }
    /** Stores data on the server; the key, value, an expiration time, and a hash value are specified.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean set(String key, Object value, Date expiry, Object hash) {
        return set("set",key,value, expiry, hash);
    }
    
    
    /** Adds data to the server; only the key and the value are specified.
     *  If data already exists for this key on the server or if the key is being
     *  deleted, the specified value will not be stored.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @return true, if the data was successfully stored
     */
    public boolean add(String key, Object value) {
        return set("add", key, value, null, null);
    }
    /** Adds data to the server; the key, value, and a hash value are specified.
     *  If data already exists for this key on the server or if the key is being
     *  deleted, the specified value will not be stored.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @return true, if the data was successfully stored
     */
    public boolean add(String key, Object value, int hash) {
        return set("add",key,value,null,new Integer(hash));
    }
    /** Adds data to the server; the key, value, and a hash value are specified.
     *  If data already exists for this key on the server or if the key is being
     *  deleted, the specified value will not be stored.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @return true, if the data was successfully stored
     */
    public boolean add(String key, Object value, Integer hash) {
        return set("add",key,value, null, hash);
    }
    
    /** Adds data to the server; the key, value, and an expiration time are specified.
     *  If data already exists for this key on the server or if the key is being
     *  deleted, the specified value will not be stored.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean add(String key, Object value, Date expiry) {
        return set("add",key,value,expiry,null);
    }
    /** Adds data to the server; the key, value, an expiration time, and a hash value are specified.
     *  If data already exists for this key on the server or if the key is being
     *  deleted, the specified value will not be stored.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean add(String key, Object value, Date expiry, int hash) {
        return set("add",key,value, expiry, new Integer(hash));
    }
    /** Adds data to the server; the key, value, an expiration time, and a hash value are specified.
     *  If data already exists for this key on the server or if the key is being
     *  deleted, the specified value will not be stored.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean add(String key, Object value, Date expiry, Object hash) {
        return set("add",key,value, expiry, hash);
    }
    
    /** Updates data on the server; only the key and the value are specified.
     *  If data does not already exist for this key on the server, or if the key is being
     *  deleted, the specified value will not be stored.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @return true, if the data was successfully stored
     */
    public boolean replace(String key, Object value) {
        return set("replace", key, value, null, null);
    }
    /** Updates data on the server; the key, value, and a hash value are specified.
     *  If data does not already exist for this key on the server, or if the key is being
     *  deleted, the specified value will not be stored.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @return true, if the data was successfully stored
     */
    public boolean replace(String key, Object value, int hash) {
        return set("replace",key,value,null,new Integer(hash));
    }
    /** Updates data on the server; the key, value, and a hash value are specified.
     *  If data does not already exist for this key on the server, or if the key is being
     *  deleted, the specified value will not be stored.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @return true, if the data was successfully stored
     */
    public boolean replace(String key, Object value, Integer hash) {
        return set("replace",key,value, null, hash);
    }
    
    /** Updates data on the server; the key, value, and an expiration time are specified.
     *  If data does not already exist for this key on the server, or if the key is being
     *  deleted, the specified value will not be stored.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean replace(String key, Object value, Date expiry) {
        return set("replace",key,value,expiry,null);
    }
    /** Updates data on the server; the key, value, an expiration time, and a hash value are specified.
     *  If data does not already exist for this key on the server, or if the key is being
     *  deleted, the specified value will not be stored.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean replace(String key, Object value, Date expiry, int hash) {
        return set("replace",key,value, expiry, new Integer(hash));
    }
    /** Updates data on the server; the key, value, an expiration time, and a hash value are specified.
     *  If data does not already exist for this key on the server, or if the key is being
     *  deleted, the specified value will not be stored.
     *  The server will automatically delete the value when the expiration time has been reached.
     *  If the value is not a String or numeric type, or if {@link #set_serial(boolean) set_serial(true)}
     *  has been called; the data will be Serialized prior to storage.
     *
     *  If compression is enabled, and the data is longer than the compression threshold,
     *  and compresses by at least the compression savings, the data will be stored in
     *  compressed form.
     *
     * @param key key to store data under
     * @param value value to store
     * @param hash used to determine which server is responsible for the specified key
     * @param expiry when to expire the record
     * @return true, if the data was successfully stored
     */
    public boolean replace(String key, Object value, Date expiry, Object hash) {
        return set("replace",key,value, expiry, hash);
    }
    
    
    private boolean set(String cmdname, String key, Object value, Date expiry, Object hash) {
        SockIO sock;
        if (hash != null) {
            sock = get_sock(hash);
        } else {
            sock = get_sock(key);
        }
        
        if (sock == null) {
            return false;
        }
        
        if (expiry == null) {
            expiry = new Date(0);
        }
        int flags = 0;
        
        byte[] val;
        
        if (!forceserial && (value.getClass() == String.class ||
        value.getClass() == Double.class ||
        value.getClass() == Float.class ||
        value.getClass() == Integer.class ||
        value.getClass() == Long.class ||
        value.getClass() == Byte.class ||
        value.getClass() == Short.class)) {
            val = value.toString().getBytes();
        } else  {
            if (debug) {
                System.out.println("MemCache: Serializing " + value.getClass().getName());
            }
            flags|= F_SERIALIZED;
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            try {
                (new ObjectOutputStream(bos)).writeObject(value);
                val = bos.toByteArray();
            } catch (IOException e) {
                val = value.toString().getBytes();
            }
            
        }
        
        if (compress_enable && val.length > compress_threshold) {
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream(val.length);
                GZIPOutputStream gos = new GZIPOutputStream(bos);
                gos.write(val);
                gos.finish();
                
                
                if (bos.size() <= ( int)((1 - compress_savings) *(double) val.length)) {
                    val = bos.toByteArray();
                    flags|= F_COMPRESSED;
                }
                
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
        try {
            String cmd = cmdname + " " + key + " " + flags + " " +
            expiry.getTime() / 1000 + " " + val.length + "\r\n";
            sock.writeBytes(cmd);
            sock.write(val);
            sock.writeBytes("\r\n");
            sock.flush();
            
            String line = sock.readLine();
            if (debug) {
                System.out.println("MemCache: " + cmdname + " " + key +
                " = " + val + "(" + line + ")");
            }
            if (line.equals("STORED")) {
                return true;
            }
            
        } catch (IOException e) {
            sock.close();
        }
        return false;
        
        
        
    }
    
    
    /** Increment the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and does not check
     *  for overflow. Because Java lacks unsigned types, the value is returned as
     *  a 64-bit integer. The server will only increment a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long incr(String key) {
        return incrdecr("incr", key, 1, null);
    }
    /** Increment the value at the specified key by the specified increment,
     *  and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and does not check
     *  for overflow. Because Java lacks unsigned types, the value is returned as
     *  a 64-bit integer. The server will only increment a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param inc how much to increment by
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long incr(String key, long inc) {
        return incrdecr("incr", key, inc, null);
    }
    /** Increment the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and does not check
     *  for overflow. Because Java lacks unsigned types, the value is returned as
     *  a 64-bit integer. The server will only increment a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long incr(String key, Object hash) {
        return incrdecr("incr", key, 1, hash);
    }
    /** Increment the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and does not check
     *  for overflow. Because Java lacks unsigned types, the value is returned as
     *  a 64-bit integer. The server will only increment a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long incr(String key, int hash) {
        return incrdecr("incr", key, 1, new Integer(hash));
    }
    /** Increment the value at the specified key by the specified increment,
     *  and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and does not check
     *  for overflow. Because Java lacks unsigned types, the value is returned as
     *  a 64-bit integer. The server will only increment a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param inc how much to increment by
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long incr(String key, long inc, int hash) {
        return incrdecr("incr", key, inc, new Integer(hash));
    }
    /** Increment the value at the specified key by the specified increment,
     *  and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and does not check
     *  for overflow. Because Java lacks unsigned types, the value is returned as
     *  a 64-bit integer. The server will only increment a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param inc how much to increment by
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long incr(String key, long inc, Object hash) {
        return incrdecr("incr", key, inc, hash);
    }
    
    /** Decrement the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and checks for
     *  underflow. In the event of underflow, the result will be zero.  Because
     *  Java lacks unsigned types, the value is returned as a 64-bit integer.
     *  The server will only decrement a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long decr(String key) {
        return incrdecr("decr", key, 1, null);
    }
    /** Decrement the value at the specified key by the specified increment,
     *  and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and checks for
     *  underflow. In the event of underflow, the result will be zero.  Because
     *  Java lacks unsigned types, the value is returned as a 64-bit integer.
     *  The server will only decrement a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param inc how much to increment by
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long decr(String key, long inc) {
        return incrdecr("decr", key, inc, null);
    }
    /** Decrement the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and checks for
     *  underflow. In the event of underflow, the result will be zero.  Because
     *  Java lacks unsigned types, the value is returned as a 64-bit integer.
     *  The server will only decrement a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long decr(String key, Object hash) {
        return incrdecr("decr", key, 1, hash);
    }
    /** Decrement the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and checks for
     *  underflow. In the event of underflow, the result will be zero.  Because
     *  Java lacks unsigned types, the value is returned as a 64-bit integer.
     *  The server will only decrement a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long decr(String key, int hash) {
        return incrdecr("decr", key, 1, new Integer(hash));
    }
    
    /** Decrement the value at the specified key by the specified increment,
     *  and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and checks for
     *  underflow. In the event of underflow, the result will be zero.  Because
     *  Java lacks unsigned types, the value is returned as a 64-bit integer.
     *  The server will only decrement a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param inc how much to increment by
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long decr(String key, long inc, int hash) {
        return incrdecr("decr", key, inc, new Integer(hash));
    }
    /** Decrement the value at the specified key by 1, and then return it.
     *
     *  Note that the server uses a 32-bit unsigned integer, and checks for
     *  underflow. In the event of underflow, the result will be zero.  Because
     *  Java lacks unsigned types, the value is returned as a 64-bit integer.
     *  The server will only decrement a value if it already exists;
     *  if a value is not found, -1 will be returned.
     *
     * @param key key where the data is stored
     * @param inc how much to increment by
     * @param hash used to determine which server is responsible for the specified key
     * @return -1, if the key is not found, the value after incrementing otherwise
     */
    public long decr(String key, long inc, Object hash) {
        return incrdecr("decr", key, inc, hash);
    }
    
    private long incrdecr(String cmdname, String key, long inc, Object hash) {
        SockIO sock;
        if (hash != null) {
            sock = get_sock(hash);
        } else {
            sock = get_sock(key);
        }
        
        if (sock == null) {
            return -1;
        }
        
        try {
            sock.writeBytes(cmdname + " " + key + " " + inc + "\r\n");
            sock.flush();
            String tmp = sock.readLine();
            return Long.decode(tmp).longValue();
        } catch (IOException e) {
            sock.close();
        } catch (NumberFormatException e) {
        }
        
        return -1;
    }
    
    /** Retrieve a key from the server.
     *
     *  If the data was compressed or serialized when compressed, it will automatically
     *  be decompressed or serialized, as appropriate. (Inclusive or)
     *
     *  Non-serialized data will be returned as a string, so explicit conversion to
     *  numeric types will be necessary, if desired
     *
     * @param key key where data is stored
     * @return the object that was previously stored, or null if it was not previously stored
     */
    public Object get(String key) {
        return get(key, null);
    }
    
    /** Retrieve a key from the server, using a specific hash.
     *
     *  If the data was compressed or serialized when compressed, it will automatically
     *  be decompressed or serialized, as appropriate. (Inclusive or)
     *
     *  Non-serialized data will be returned as a string, so explicit conversion to
     *  numeric types will be necessary, if desired
     *
     * @param key key where data is stored
     * @param hash used to determine which server is responsible for the specified key
     * @return the object that was previously stored, or null if it was not previously stored
     */
    public Object get(String key, int hash) {
        return get(key, new Integer(hash));
    }
    
    /** Retrieve a key from the server, using a specific hash.
     *
     *  If the data was compressed or serialized when compressed, it will automatically
     *  be decompressed or serialized, as appropriate. (Inclusive or)
     *
     *  Non-serialized data will be returned as a string, so explicit conversion to
     *  numeric types will be necessary, if desired
     *
     * @param key key where data is stored
     * @param hash used to determine which server is responsible for the specified key
     * @return the object that was previously stored, or null if it was not previously stored
     */
    public Object get(String key, Object hash) {
        SockIO sock;
        if (hash != null) {
            sock = get_sock(hash);
        } else {
            sock = get_sock(key);
        }
        
        if (sock == null) {
            return null;
        }
        try {
            sock.writeBytes("get " + key + "\r\n");
        } catch (IOException e) {
            sock.close();
        }
        HashMap hm = new HashMap();
        load_items(sock, hm);
        if (debug) {
            Iterator i = hm.entrySet().iterator();
            while (i.hasNext()) {
                Entry e = (Entry)i.next();
                System.out.println("MemCache: got " + e.getKey() + " = " + e.getValue());
            }
        }
        return hm.get(key);
    }
    
    
    /** Retrieve multiple keys from the memcache.
     *
     *  This is recommended over repeated calls to {@link #get(String) get()}, since it
     *  is more efficient.
     * @return a hashmap with entries for each key is found by the server,
     *      keys that are not found are not entered into the hashmap, but attempting to
     *      retrieve them from the hashmap gives you null.
     * @param keys keys to retrieve
     */
    public HashMap get_multi(String[] keys) {
        return get_multi(keys, null);
    }
    
    /** Retrieve multiple keys from the memcache.
     *
     *  This is recommended over repeated calls to {@link #get(String) get()}, since it
     *  is more efficient.
     * @param keys keys to retrieve
     * @param hashes hash values used to determine which server to use for each key;
     *      if a hash is not provided for a key (either because there are more keys
     *      than hashes, or a hash value is specifically null) a hash will be computed.
     * @return a hashmap with entries for each key is found by the server,
     *      keys that are not found are not entered into the hashmap, but attempting to
     *      retrieve them from the hashmap gives you null.
     */
    public HashMap get_multi(String[] keys, Object[] hashes) {
        ArrayList socks = new ArrayList();
        HashMap sock_keys = new HashMap();
        for (int i = 0; i < keys.length; ++i) {
            SockIO sock;
            if (hashes!= null && hashes.length > i && hashes[i] != null) {
                sock = get_sock(hashes[i]);
            } else {
                sock = get_sock(keys[i]);
            }
            if (sock == null) {
                continue;
            }
            if (!sock_keys.containsKey(sock)) {
                sock_keys.put(sock, new StringBuffer());
                socks.add(sock);
            }
            ((StringBuffer)sock_keys.get(sock)).append(" " + keys[i]);
        }
        
        // Pass 1: send out requests
        
        ArrayList gather = new ArrayList();
        Iterator i = socks.iterator();
        while (i.hasNext()) {
            SockIO sock =null;
            try {
                sock = (SockIO) i.next();
                sock.writeBytes("get" + (StringBuffer)sock_keys.get(sock) + "\r\n");
                sock.flush();
                gather.add(sock);
            } catch (IOException e) {
                sock.close();
            }
        }
        
        HashMap ret = new HashMap();
        // Pass 2: get results
        i = gather.iterator();
        while (i.hasNext()) {
            load_items((SockIO) i.next(), ret);
        }
        
        if (debug) {
            i = ret.entrySet().iterator();
            while (i.hasNext()) {
                Entry e = (Entry)i.next();
                System.out.println("MemCache: got " + e.getKey() + " = " + e.getValue());
            }
        }
        return ret;
    }
    
    private void load_items(SockIO sock, HashMap hm) {
        try {
            while (true) {
                String line = sock.readLine();
                if (line == null) {
                    return;
                } else if (line.startsWith("VALUE")) {
                    StreamTokenizer st = new StreamTokenizer(new StringReader(line));
                    st.ordinaryChars(48, 57);
                    st.wordChars(48, 57); // add numbers to words
                    st.nextToken(); // skip VALUE token
                    st.nextToken();
                    String key = st.sval;
                    st.nextToken();
                    int flag = new Integer(st.sval).intValue();
                    st.nextToken();
                    int length = new Integer(st.sval).intValue();
                    byte[] buf = new byte[length];
                    sock.readFully(buf); // blocking read
                    
                    sock.readLine(); // clear out \r\n that should be left
                    // check for compression
                    Object o;
                    
                    if ((flag & F_COMPRESSED) != 0) {
                        try {
                            GZIPInputStream gzi = new GZIPInputStream(new ByteArrayInputStream(buf));
                            ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.length);
                            // read the input stream, and write to a byte array output stream since
                            // we have to read into a byte array, but we don't know how large it
                            // will need to be, and we don't want to resize it a bunch
                            
                            byte[] tmp = new byte[1024];
                            int count;
                            while ((count = gzi.read(tmp)) != -1) {
                                bos.write(tmp,0,count);
                            }
                            
                            buf = bos.toByteArray();
                        } catch (IOException e) {
                        }
                    }
                    if ((flag & F_SERIALIZED) != 0) {
                        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buf));
                        try {
                            o = ois.readObject();
                            if (debug) {
                                System.out.println("MemCache: Deserializing " + o.getClass().getName());
                            }
                        } catch (ClassNotFoundException e) {
                            o = new String(buf);
                        }
                    } else {
                        o = new String(buf);
                    }
                    hm.put(key, o);
                } else if (line.equals("END")) {
                    return;
                }
            }
        } catch (NumberFormatException e) {
            //this shouldn't happen...
            System.out.println("MemCache: The sever passed us bad numbers in get");
        } catch (IOException e) {
            sock.close();
        }
        
        
        
    }
    
}
