package org.jcon.util.lock;

import org.jcon.util.ArraySorter;
import java.util.Hashtable;
import java.util.Vector;

/**
 * Provides a pool of lockable objects with built in
 * deadlock avoidance using "resource ordering". To use
 * this class effectively:
 * <p>
 * 1. Add keyed objects to the pool with add().
 * <p>
 * 2. Lock and retreive one or more objects with
 * lockObjects(), which returns a LockSet.
 * <p>
 * 3. Use the LockSet for a transaction.
 * <p>
 * 4. When finished return the LockSet with unlock().
 * <p> <p>
 * This class is threadsafe, since its intended use is for
 * multiple threads needing access to the objects in
 * the pool. We have attempted to allow for the objects
 * to be local or remote, and to allow all lock subsystem
 * class fields to pass through serialization robustly.
 * <p>
 * This class is designed to be part of a greater pool,
 * with some objects requiring exclusive use and others
 * available on a shared basis. For example you might have
 * class ResourcePool with methods addExclusive(key, object)
 * and addShared(key, object). The exclusive method would
 * add to a LockPool instance, while the shared method
 * would add to a Hashtable.
 * <p> <pre>
 * Later we may add methods:
 * - removeObject(Object object)
 * - lockIfAvailable(String[] keys)
 * - boolean isLocked(String key)
 * - boolean containsKey(Object key)
 * - boolean containsObject(Object object)
 * - Enumeration getKeys()
 * - void removeAll()
 * - setLockSetTimeout(...)
 * - addLockPoolListener(LockPoolListener listener)
 * </pre>
 * @author Jack Harich
 */
public class LockPool implements java.io.Serializable {

//---------- Package Fields ------------------------------
static final boolean DEBUGGING = true; // false for production

//---------- Private Fields ------------------------------
// Key = key, Object = Resource with object
private Hashtable resources = new Hashtable();

// Key = String is id, Object = LockSet checked out
private Hashtable lockSets = new Hashtable();

private long lastLockSetID;    // Simple approach for now
private long lastResourceSort; // Sufficient

//---------- Public Methods ------------------------------
/**
 * Adds the unique key and object to the pool. Throws an
 * IllegalArgumentException if the key or object is
 * already in the pool.
 */
public synchronized void add(Object key, Object object) {
    if (resources.containsKey(key)) {
        throw new IllegalArgumentException("The key '" +
            key + "' is already in this pool.");

    } else if (resources.contains(object)) {
        throw new IllegalArgumentException("The object '" +
            object + "' is already in this pool.");

    } else {
        Resource resource = new Resource();
        resource.setKey(key);
        resource.setObject(object);
        resource.setSort(createResourceSort());
        resources.put(key, resource);
    }
}
/**
 * Removes the key and its object from the pool,
 * returning the object removed or null if none. No effect
 * if the pool does not contain the key. Throws an
 * IllegalStateException if the object is locked.
 */
public synchronized Object removeKey(Object key) {
    Resource resource = (Resource)resources.get(key);
    if (resource == null) {
        return null;
    } else if (resource.isLocked()) {
        throw new IllegalStateException("The object with key '" +
            key + "' is locked.");
    } else {
        return resources.remove(key);
    }
}
/**
 * Locks the keyed objects and returns a LockSet containing
 * those objects for exclusive use by the client, such as
 * for a transaction.
 * <p>
 * This method locks the objects one-by-one, blocking if
 * one of the objects is
 * already locked. The block will proceed when the object
 * is unlocked, and will block again if another lock is
 * encountered. When this method returns we guarentee that
 * all objects in the LockSet are locked. This provides
 * exclusive use of these objects to the LockSet holder,
 * assuming the same objects are not available outside
 * the LockPool.
 * <p>
 * Throws an IllegalArgumentException and releases LockSet
 * locks if a key is not found, since this usually
 * indicates a programming error, and the lockSet holder
 * is probably assuming the LockSet is complete.
 * <p>
 * Null and duplicate keys are ignored. For example if keys
 * contains <code> { A, B, null, A } </code> then the
 * LockSet returned contains only A and B. This allows
 * keys preparation logic more flexibility and makes lock
 * pool use more robust.
 */
public synchronized LockSet lockObjects(Object[] keys) {

    Object[] uniqueKeys = getUniqueKeys(keys);
    Resource[] sortedResources = getSortedResources(uniqueKeys);

    // Build lockSet, locking as we go
    String id = createLockSetID();
    LockSet lockSet = new LockSet(id);
    for (int i = 0; i < sortedResources.length; i++) {
        Resource resource = sortedResources[i];

        // Wait if locked. Reliable since we are the only
        // instance locking or unlocking the resources.
        while (resource.isLocked()) {
            try {
                wait();
            } catch(InterruptedException ex) {
                print(".lockObjects() - Interrupted, should not happen");
                ex.printStackTrace();
                // And continue
            }
        }
        print(".lockObjects() ------ Locking " + resource.getKey());
        resource.setLocked(true);
        // Locked, so proceed
        lockSet.add(resource);
    }
    lockSets.put(id, lockSet);
    return lockSet;
}
/**
 * A convenience method that is the same as lockObjects()
 * except it locks a single object.
 */
 public synchronized LockSet lockObject(Object key) {
    return lockObjects( new Object[] { key } );
 }
/**
 * Unlocks the objects in the lockSet, after which they
 * are now available for use.
 * <p>
 * The client MUST take extreme care to unlock a lockSet
 * after use, especially if exceptions are encountered.
 * A reasonable approach is to call lockObjects() in a try
 * block, use the obtained lockSet in that block, and call
 * unlock() in the finally block.
 * <p>
 * Throws an IllegalStateException if the lockSet is
 * already unlocked, so you should only unlock a lockSet once.
 */
public synchronized void unlock(LockSet lockSet) {
    String id = lockSet.getID();
    if (! lockSets.containsKey(id)) {
        throw new IllegalStateException("LockSet with id '" +
            id + "' is not locked.");
    } else {
        lockSet.unlockAll();
        lockSets.remove(id);
        notifyAll();
    }
}
//---------- Private Methods -----------------------------
// Returns the resources, sorted in a consistent manner
// This is "resource ordering" to avoid deadlock
private Resource[] getSortedResources(Object[] keys) {

    // Populate sortedResources, sorts
    Resource[] sortedResources = new Resource[keys.length];
    String[] sorts = new String[keys.length];

    for (int i = 0; i < keys.length; i++) {
        Resource resource = (Resource)resources.get(keys[i]);
        if (resource == null) {
            throw new IllegalArgumentException("The key '" +
                keys[i] + "' is not in this pool.");
        }
        sortedResources[i] = resource;
        sorts[i] = resource.getSort();
    }
    // Sort
    ArraySorter.sortObjectsByASCII(sorts, sortedResources);
    return sortedResources;
}
// Removes null and duplicate keys, returns unique keys
// Duplicate key would cause deadlock
private Object[] getUniqueKeys(Object[] keys) {
    Vector uniqueVector = new Vector();
    // Build uniqueVector
    for (int i = 0; i < keys.length; i++) {
        Object key = keys[i];
        if (key == null) continue;
        // Duplicate check
        boolean unique = true;
        for (int j = i + 1; j < keys.length; j++) {
            //print(" - " + key + " versus " + keys[j]);
            if (key.equals(keys[j])) {
                unique = false;
                continue;
            }
        }
        if (unique) uniqueVector.addElement(key);
    }
    // Use uniqueVector to return uniqueKeys
    Object[] uniqueKeys = new Object[uniqueVector.size()];
    uniqueVector.copyInto(uniqueKeys);
    return uniqueKeys;
}
private String createResourceSort() {
    return String.valueOf(lastResourceSort++);
}
private String createLockSetID() {
    return String.valueOf(lastLockSetID++);
}
//--- Std
private static void print(String text) {
    if (DEBUGGING) System.out.println("LockPool" + text);
}


} // End class
