Objlist.java

/*
 * $RCSfile: Objlist.java,v $
* $Id: Objlist.java,v 1.6 1998/11/30 02:21:07 devnull Exp $
* by Lee Wilson, http://www.ad1440.net/~devnull
* Development started on 1998 10 01
* (c) Devnull Software, LLC. (http://www.devnullsoftware.com)
*/

package com.devnullsoftware.objlist;

import java.io.*;
import java.util.*;
import java.lang.reflect.*;

/**
  A growable List class that stores Objects and allows searches on both
  primary and/or secondary indices.

  @author Dave Olafson
  @author Lee Wilson
*/
public class Objlist implements Serializable {
  //-------------------------------------------------------------------
  // Global Constants
  //
  static final long serialVersionUID = 7639128011386996065L;


  //-------------------------------------------------------------------
  // Instance Variables
  //
  /**
    Debug flag.  If set to true, debugging statements will be output.
  */
  public static boolean debug = false;
  /**
    The Hashtable where all objects are stored.  Keyed by each object's
    toString() value
  */
  private Hashtable objects = null;
  /**
    The Object Indices.  Each key is a string representation of an integer
    index value.  The object associated with the key is a String that is the
    key to an object in the objects Hashtable.
  */
  private Hashtable indices = null;
  /**
    The reverse-pointers to the Object Indices.
    Each key is the object's toString() value and each value stored is
    the index number of the object.
  */
  private Hashtable reverseIndices = null;
  /**
    A very-mutable and fairly unreliable indication of the current index.
    Since it can be changed by many methods, it is not extremely safe to 
    rely on it between method calls.  It is mainly useful for iterators.
  */
  private int currentIndex = 0;
  /**
    The index of the next Object added to the Objlist.
  */
  private int nextIndex = 0;
  /**
    Extra lookup Indexes
  */
  private Hashtable secondaryIndex = null;


  //-------------------------------------------------------------------
  // Constructors
  //
  /**
    Creates a new Objlist with a default size of 100.
  */
  public Objlist ()  { 
    init ();
  }


  /**
    Creates a new Objlist copied from the Objlist passed in.  Useful for
    creating list iterators.

    @param from Objlist to be copied.
    @return a new Objlist.
  */
  public Objlist (Objlist from)  { 
    init();
    if (from != null)  {
      objects = (Hashtable)from.objects.clone();
      indices = (Hashtable)from.indices.clone();
      reverseIndices = (Hashtable)from.reverseIndices.clone();
      currentIndex = from.currentIndex;
      nextIndex = from.nextIndex;
    }
  }


  /**
    Creates a new Objlist copied from the array passed in.

    @param from array to be copied.
    @return a new Objlist.
  */
  public Objlist (Object[] from)  {
    init ();
    Object  tmp [] = new Object [from.length];
    System.arraycopy (from, 0, tmp, 0, from.length);
    for (int i = 0; i < tmp.length; i++)  {
      try  {
	add(tmp[i]);
      }
      catch (Exception e)  {
	e.printStackTrace();
      }
    }
  }


  /** 
    Common initializer stuff shared between constructors
  */
  private void init ()  {
    objects = new Hashtable();
    indices = new Hashtable();
    reverseIndices = new Hashtable();
    secondaryIndex = new Hashtable ();
  }



  //-------------------------------------------------------------------
  // Accessors & Mutators
  //
  /**
    Gets the current Index number.
  */
  public int getIndex ()  {
    return (currentIndex);
  }


  /**
    Gets the number of items currently stored in the list.
  */
  public int getNumItems ()  {
    return objects.size();
  }


  /**
    Sets the index to the value given.  Dangerous to rely on this since other
    internal routines in this class will change the index without warning.

    @param newIndex the new Index # to set the list to.   
  */
  protected void setIndex (int newIndex)  { 
    currentIndex = newIndex;
  }


  /**
    Adds a secondary Index.  Normally used internally only.
  */
  private Hashtable addIndex (String indexName)  {
    Hashtable h = new Hashtable();
    secondaryIndex.put (indexName, h);
    return h;
  }


  /**
    Converts the Objects in the list to Strings and puts them in an array
    of strings.  Only the "id" of the Objects are returned (i.e. the output
    of the Object.toString() function).  Not guaranteed to come out in any
    particular order.
  */
  public String [] stringArray () { 
    String s[] = new String [objects.size()];
    int i = 0;
    for (Enumeration e = objects.elements(); e.hasMoreElements(); )  {
      s[i++] = (String)e.nextElement().toString();
    }
    return (s);
  }


  /**
    Converts the Objects in the list to Strings and puts them in an array
    of strings.  Only the "id" of the Objects are returned (i.e. the output
    of the Object.toString() function).  Not guaranteed to come out in any
    particular order.
  */
  public String [] stringArrayKeys () { 
    String s[] = new String [objects.size()];
    int i = 0;
    for (Enumeration e = objects.keys(); e.hasMoreElements(); )  {
      s[i++] = (String)e.nextElement().toString();
    }
    return (s);
  }


  /**
    Converts the Objects in the list to Strings and puts them in an array
    of strings.  Only the "id" of the Objects are returned (i.e. the output
    of the Object.toString() function).  The order of the array will be the
    order in which the objects were put in.
  */
  public String [] orderedStringArray () { 
    String s[] = new String [objects.size()];
    int i = 0;
    int nextNum = 0;
    for (i = 0; i < nextIndex; i++)  {
      Object obj = index(i);
      if (obj != null)  {
	s[nextNum++] = obj.toString();
      }
    }
    return (s);
  }


  //-------------------------------------------------------------------
  // Security
  //


  //-------------------------------------------------------------------
  // Methods
  //
  /**
    Adds a new Object to the list.  If the Object implements the 
    IndexedData interface, it will poll the object for it's secondary
    indexes and add them as well.
    
    @param obj Object to be put on the list.
    @exception NullPointerException
               if obj is null.
    @return same Object that was passed in.
  */
  public Object add (Object obj)
    throws NullPointerException,
           NoSuchFieldException,
           IllegalAccessException
  { 
    if (obj == null)  {
      throw (new NullPointerException
             ("cannot add a null object to an Objlist"));
    }
    indices.put(Integer.toString(nextIndex), obj.toString());
    reverseIndices.put(obj.toString(), Integer.toString(nextIndex));
    objects.put(obj.toString(), obj);
    nextIndex++;
    if (obj instanceof IndexedData)  {
      IndexedData indexedObj = (IndexedData)obj;
      String [] indexNames = indexedObj.getIndices(this);
      for (int i = 0; i < indexNames.length; i++)  {
        Hashtable h = (Hashtable)secondaryIndex.get(indexNames[i]);
        if (h == null)  {
          h = addIndex(indexNames[i]);
        }
        Class c = indexedObj.getClass();
        String indexKey = null;
	boolean found = false;
	Field f = null;
	while (!found)  {
	  try  {
	    f = c.getDeclaredField(indexNames[i]);
	    found = true;
	  }
	  catch (NoSuchFieldException e)  {
	    c = c.getSuperclass();
	  }
	  catch (NullPointerException e)  {
	    break;
	  }
	}
	Object value = f.get(indexedObj);
	if (value != null)  {
	  indexKey = value.toString();
	}
        if (indexKey != null)  {
          h.put(indexKey, obj.toString());
        }
      }  
    }  
    return (obj);   
  }


  /**
    Adds a new Object to the list.  If an object that is similar to the added
    object is already in the list, it will be overwritten by the new data.
    
    @param obj Object to be put on the list.
    @return same Object that was passed in.
  */
  public Object overwrite (Object obj) { 
    Object oldobj = find (obj.toString());
    while (oldobj != null)  {
      deleteObj (oldobj);
      oldobj = find (obj.toString());
    }
    try  {
      add(obj);
    }
    catch (Exception e)  {
      e.printStackTrace();
    }
    return (obj);   
  }


  /**
    updates secondary index for an Object.  This is necessary if the objects
    value which is used as a secondary index lookups changes.  

    @param obj Object which lists need to be updated for.
    @param indexName index which is being updated.
    @param String oldKey the old value of the field which was used as a key
    @param String newKey the new value of the field which will be used as a key
    @exception IndexesNotSupportedException
               if the object does not implement the IndexedData interface
    @exception NullPointerException
               if the object passed in is null
    @exception IllegalArgumentException
               if the object is not already in the list
  */
  public void updateIndex (Object obj,
			   String indexName,
			   String oldKey,
			   String newKey)
    throws IndexesNotSupportedException,
           NullPointerException,
           IllegalArgumentException,
           NoSuchFieldException,
           IllegalAccessException
  { 
    if (obj == null)  {
      throw (new NullPointerException
             ("cannot add a null object to an Objlist"));
    }
    if (indexName == null)  {
      throw (new NullPointerException
             ("indexName cannot be null"));
    }
    if (newKey == null)  {
      throw (new NullPointerException
             ("newKey cannot be null"));
    }
    if (!(obj instanceof IndexedData))  {
      throw (new IndexesNotSupportedException());
    }
    if (obj != find(obj.toString()))  {
      throw (new IllegalArgumentException
             ("obj must already be in the Objlist in order to update"));
    }
    Hashtable h = (Hashtable)secondaryIndex.get(indexName);
    if (h != null)  {
      if (oldKey != null)  {
	h.remove(oldKey);
      }
      h.put(newKey, obj.toString());
    }
  }  


  /**
    updates all secondary indices for an Object.  This is necessary if the
    objects values which are used as secondary index lookups change.  

    @param obj Object which lists need to be updated for
    @exception IndexesNotSupportedException
               if the object does not implement the IndexedData interface
    @exception NullPointerException
               if the object passed in is null
    @exception IllegalArgumentException
               if the object is not already in the list
  */
  public void updateAllIndices (Object obj)
    throws IndexesNotSupportedException,
           NullPointerException,
           IllegalArgumentException,
           IllegalAccessException
  { 
    if (obj == null)  {
      throw (new NullPointerException
             ("cannot add a null object to an Objlist"));
    }
    if (!(obj instanceof IndexedData))  {
      throw (new IndexesNotSupportedException());
    }
    if (obj != find(obj.toString()))  {
      throw (new IllegalArgumentException
             ("obj must already be in the Objlist in order to update"));
    }
    IndexedData indexedObj = (IndexedData)obj;
    String [] indexNames = indexedObj.getIndices();
    for (int i = 0; i < indexNames.length; i++)  {
      Hashtable h = (Hashtable)secondaryIndex.get(indexNames[i]);
      if (h == null)  {
        h = addIndex(indexNames[i]);
      }
      Class c = indexedObj.getClass();
      String indexKey = null;
      boolean found = false;
      Field f = null;
      while (!found)  {
	try  {
	  f = c.getDeclaredField(indexNames[i]);
	  found = true;
	}
	catch (NoSuchFieldException e)  {
	  c = c.getSuperclass();
	}
      }
      Object value = f.get(indexedObj);
      if (value != null)  {
	indexKey = value.toString();
      }
      if (indexKey != null)  {
        h.remove(indexKey);
        h.put(indexKey, obj.toString());
      }
    }  
  }  


  /**
    Deletes an Object from the list.

    @param id The id of the Object to be deleted.  In the case of Objects 
              that are not strings, the id is the result of the
              Object.toString() method.
  */
  public void delete (String id) { 
    if (id == null)  {
      return;
    }
    Object obj = find (id);

    Object delobj = reverseIndices.remove(id);
    while (delobj != null)  {
      Object delobj2 = indices.remove(delobj);
      delobj = reverseIndices.remove(id);
    }
    delobj = objects.remove(id);
    while (delobj != null)  {
      delobj = objects.remove(id);
    }
  }


  /**
    Deletes the object that is the current index.  Very dangerous to use
    externally since the index is rarely guaranteed to be set to a specific
    value and can change internally without notice.
  */
  protected void deleteCurrentIndex ()  { 
    Object obj = index(currentIndex);
    deleteObj(obj);
  }


  /**
    Deletes an Object from the list.

    @param obj The Object to be deleted from the list.  Note that this does
               not destroy the Object, just the list's reference to it.
  */
  public boolean deleteObj (Object obj) { 
    boolean found = false;
    if (obj == null)  {
      return false;
    }
    Object delobj = reverseIndices.remove(obj.toString());
    while (delobj != null)  {
      Object delobj2 = indices.remove(delobj);
      delobj = reverseIndices.remove(obj.toString());      
      found = true;
    }
    delobj = objects.remove(obj.toString());
    while (delobj != null)  {
      delobj = objects.remove(obj.toString());
      found = true;
    }
    if (getNumItems() == 0)  {
      nextIndex = 0;
    }
    return found;
  }


  /**
    Retrieves Object at the index given. 

    @param Index the index number to retrieve the Object from
    @return the Object asked for, or null if no Object stored at that index
      location.
  */
  public Object index (int Index)  {
    currentIndex = Index;
    Object keyobj = indices.get(Integer.toString(Index));
    if (keyobj != null)  {
      return (find(keyobj.toString()));
    }
    else  {
      return null;
    }
  }


  /**
    Retrieves the first Object from the Objlist.
    @return Object or null if list is empty.
  */
  public Object first ()  {
    currentIndex = -1;
    return (next());
  }

  /**
    Gives the first index number in the list that contains a valid Object.
    @return number or -1 if list is empty.
  */
  public int firstIndex ()  {
    currentIndex = -1;
    return (findIndex(next()));
  }


  /**
    Retrieves the last Object stored in the Objlist.
    @return Object or null if list is empty.
  */
  public Object last ()  {
    currentIndex = nextIndex;
    return (prev());
  }


  /**
    Gives the last index number in the list that stores a valid Object.
    @return number or -1 if list is empty.
  */
  public int lastIndex ()  {
    currentIndex = nextIndex;
    return (findIndex(prev()));
  }


  /**
    Retrieves the next Object from the list.  Uses the index number set by
    routines in this class and is not necessarily guaranteed to be what the
    user thinks.  Used mainly for iterators.  Also increments the index itself
    in preparation for the next call to this routine.
    
    @return Object or null if there is no object at or past current index.
  */
  public Object next ()  {
    Object keyobj = null;
    while (keyobj == null)  {
      keyobj = indices.get(Integer.toString(++currentIndex));
      if (currentIndex >= nextIndex)  {
	break;
      }
    }
    if (keyobj != null)  {
      return (objects.get(keyobj));
    }
    return null;
  }


  /**
    Given an object, finds it in the list and returns the Object that comes 
    after it.  

    @param obj the Object to find in the list.
    @return Object or null if the Object given was the last in the list or
    not found.
  */
  public Object next (Object obj)  {
    currentIndex = findIndex(obj);
    if (currentIndex < 0)   {
      return null;
    }
    return (next());
  }


  /**
    Retrieves the previous Object from the list.  Uses the index number set by
    routines in this class and is not necessarily guaranteed to be what the
    user thinks.  Used mainly for iterators.  Also decrements the index itself
    in preparation for the next call to this routine.
    
    @return Object or null if there is no object previous to the current index.
  */
  public Object prev ()  {
    Object keyobj = null;
    while (keyobj == null)  {
      keyobj = indices.get(Integer.toString(--currentIndex));
      if (currentIndex < 0)  {
	break;
      }
    }
    if (keyobj != null)  {
      return (objects.get(keyobj));
    }
    return null;
  }

  
  /**
    Given an object, finds it in the list and returns the Object that comes 
    before it.  

    @param obj the Object to find in the list.
    @return Object or null if the Object given was the first in the list or
    not found.
  */
  public Object prev (Object obj)  {
    currentIndex = findIndex(obj);
    if (currentIndex < 0)  {
      return null;
    }
    return (prev());
  }


  /**
    Finds the Object identified by the string passed in.
    
    @param id The id of the Object to be deleted.  In the case of Objects 
              that are not strings, the id is the result of the
              Object.toString() method.
    @return Object or null if the Object is not found in the list.
  */
  public Object find (String id)  {
    if (id == null)  {
      return null;
    }
    return (objects.get(id));
  }


  /**
    Finds the object using the secondary index identified by the
    string passed in.

    @param key the secondary key used to look up the item.
    @param indexName the name of the secondary index to use for looking
           up the item.
    @exception NullPointerException
               if key or indexName is null.
    @exception IllegalArgumentException
               if the Objects in this Objlist do not implement the
               IndexedData interface.
    @return Object or null if the Object was not found in the list.
  */
  public Object find (String key, String indexName)
    throws NullPointerException, IllegalArgumentException
      {
    if (key == null)  {
      throw (new NullPointerException
             ("cannot perform a find with a null key"));
    }
    if (indexName == null)  {
      throw (new NullPointerException
             ("cannot perform a find with a null indexName"));
    }
    if (!secondaryIndex.containsKey(indexName))  {
      throw (new IllegalArgumentException
             ("Secondary Index "+indexName+" does not exist"));
    }
    Hashtable h = (Hashtable)secondaryIndex.get(indexName);
    String realKey = (String)h.get(key);
    System.out.println ("realkey == "+realKey);
    return ( find(realKey) );
  }


  /**
    Finds the Index number associated with a particular object.

    @param obj The Object to search for.
    @return number or -1 if the Object is not found in the list.
  */
  public int findIndex (Object obj)  {
    if (obj == null)  {
      return -1;
    }
    String ind = (String)reverseIndices.get(obj.toString());
    if (ind == null)  {
      return -1;
    }
    try  {
      int i = Integer.parseInt(ind);
      return (i);
    }
    catch (Exception e)  {
      return -1;
    }
  }
}