/*************************************************************************

"FreePastry" Peer-to-Peer Application Development Substrate 

Copyright 2002, Rice University. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

- Neither  the name  of Rice  University (RICE) nor  the names  of its
contributors may be  used to endorse or promote  products derived from
this software without specific prior written permission.

This software is provided by RICE and the contributors on an "as is"
basis, without any representations or warranties of any kind, express
or implied including, but not limited to, representations or
warranties of non-infringement, merchantability or fitness for a
particular purpose. In no event shall RICE or contributors be liable
for any direct, indirect, incidental, special, exemplary, or
consequential damages (including, but not limited to, procurement of
substitute goods or services; loss of use, data, or profits; or
business interruption) however caused and on any theory of liability,
whether in contract, strict liability, or tort (including negligence
or otherwise) arising in any way out of the use of this software, even
if advised of the possibility of such damage.

********************************************************************************/

package rice.p2p.util;

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

/**
 * XMLObjectInputStreamm is an extension of ObjectInputStreamm which provides
 * for deserialization for objects which have been converted to XML via a
 * XMLObjectOutputStream. This class supports all of the features of the
 * ObjectInputStreamm, including serialization support for any Java object graph
 * implementing the Serializable interface, support for Externalizable classes,
 * custom deserialization via the readObject() method, class evolution via the
 * readObjectNoData() method, alternate field reading mechanisms via the
 * readFields() method, support for the readUnshared method, and dynamic object
 * replacement via the readResolve() method. The format of the XML data to be
 * read in should conform to the JSX XML Schema, available online at
 * http://www.jsx.org/jsx.xsd. This class is designed to be able to read objects
 * serialized using JSX, however, this has not been fully tested and bugs may be
 * encountered.
 *
 * @version $Id: XMLObjectInputStream.java 3039 2006-02-07 12:20:12Z jstewart $
 * @author Alan Mislove
 */
public class XMLObjectInputStream extends ObjectInputStream {

  /**
   * The underlying reader, which parses the XML
   */
  protected XMLReader reader;

  /**
   * The hashtable of references, which is updated each time a new object is
   * read off of the stream.
   */
  protected Hashtable references;

  /**
   * The stack of objects which are currently being read off of the stream
   */
  protected Stack currentObjects;

  /**
   * The stack of class types which are being read off of the stream
   */
  protected Stack currentClasses;

  /**
   * The ReflectionFactory, which allows for prividged construction of objects
   */
  protected ReflectionFactory reflFactory = (ReflectionFactory) AccessController.doPrivileged(new sun.reflect.ReflectionFactory.GetReflectionFactoryAction());

  /**
   * The list of validation objects waiting for the entire object graph to be
   * read in
   */
  protected ValidationList vlist;

  /**
   * The depth at which we are currently at in the object tree
   */
  protected int depth;

  /**
   * The hashmap of readResolve methods, mapping Class->Method
   */
  protected static SoftHashMap READ_RESOLVES = new SoftHashMap();

  /**
   * The hashmap of readObject methods, mapping Class->Method
   */
  protected static SoftHashMap READ_OBJECTS = new SoftHashMap();

  /**
   * A cache of constructors, mapping classes to serialization constructors
   */
  protected static SoftHashMap CONSTRUCTORS = new SoftHashMap();

  /**
   * Constructor which reads data from the given input stream in order
   * deserialize objects. This constructor also reads the header from the
   * stream, and throws an IOException if the correct header is not seen.
   *
   * @param in The input stream to read data from
   * @exception IOException DESCRIBE THE EXCEPTION
   * @throws IOException If the stream header is corrupt
   */
  public XMLObjectInputStream(InputStream in) throws IOException {
    super();

    this.reader = new XMLReader(new InputStreamReader(in));
    this.currentObjects = new Stack();
    this.currentClasses = new Stack();
    this.references = new Hashtable();
    this.vlist = new ValidationList();
    this.depth = 0;

    readStreamHeader();
  }

  /**
   * Method which returns the Serializable constructor for the provided class.
   * This Constructor is the no-arg Constructor for the first non-Serializable
   * class in the class's superclass heirarchy. This method does not cache the
   * result, but does set the constructor to be accessible, regardless of the
   * Java security protection.
   *
   * @param c The class to fetch the constructor for
   * @return The SerializableConstructor value
   * @throws IOException If an error occurs
   * @throws NoSuchMethodException If the first non-Serializable class does not
   *      have a no-arg Constructor
   */
  protected Constructor getSerializableConstructor(Class c) throws IOException, NoSuchMethodException {
    Class initCl = c;

    while (Serializable.class.isAssignableFrom(initCl)) {
      initCl = initCl.getSuperclass();
    }

    Constructor cons = initCl.getDeclaredConstructor(new Class[0]);
    cons = reflFactory.newConstructorForSerialization(c, cons);
    cons.setAccessible(true);
    return cons;
  }

  /**
   * Method which returns the class object for class names written to the
   * stream. If the name represents a primitive, the the X.TYPE class is
   * returned, otherwise, Class.forName() is used.
   *
   * @param name The name of the class to return
   * @return The Class value
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Class getClass(String name) throws ClassNotFoundException {
    if (name.equals("int")) {
      return Integer.TYPE;
    } else if (name.equals("boolean")) {
      return Boolean.TYPE;
    } else if (name.equals("byte")) {
      return Byte.TYPE;
    } else if (name.equals("char")) {
      return Character.TYPE;
    } else if (name.equals("double")) {
      return Double.TYPE;
    } else if (name.equals("float")) {
      return Float.TYPE;
    } else if (name.equals("long")) {
      return Long.TYPE;
    } else if (name.equals("short")) {
      return Short.TYPE;
    } else {
      return Class.forName(name);
    }
  }

  /**
   * Method which returns a previously stored reference. If the reference cannot
   * be found, null is returned.
   *
   * @param reference The reference to use
   * @return The referenced object, or null if none can be found
   */
  protected Object getReference(String reference) {
    return references.get(reference);
  }


  // ----- ObjectInputStreamm Overriding Methods -----

  /**
   * Method which reads the XML header off of the stream. Usually, this is the
   * <?xml version="1.0"?> tag, but it may include processing instructions.
   *
   * @throws IOException If the stream header is corrupt
   */
  protected void readStreamHeader() throws IOException {
    reader.readHeader();
  }

  /**
   * Method which closes the underlying input stream for reading. Any subsequent
   * reads will throw an IOException.
   *
   * @throws IOException If an error occurs
   */
  public void close() throws IOException {
    reader.close();
  }

  /**
   * Method which resets the input stream, which removes the binding of all
   * previously stored references.
   *
   * @throws IOException If an error occurs
   */
  public void reset() throws IOException {
    references = new Hashtable();
    vlist.clear();
  }

  /**
   * Method which reads a byte from the underlying output stream. Simply calls
   * readByte()
   *
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   */
  public int read() throws IOException {
    return readByte();
  }

  /**
   * Method which reads a array of bytes from the underlying output stream.
   * Simply calls b[x] = readByte() on each of the array elements.
   *
   * @param b DESCRIBE THE PARAMETER
   * @param offset DESCRIBE THE PARAMETER
   * @param length DESCRIBE THE PARAMETER
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   */
  public int read(byte[] b, int offset, int length) throws IOException {
    reader.readStartTag("base64");
    byte[] bytes = reader.readBase64();

    int written = (length < bytes.length ? length : bytes.length);
    System.arraycopy(bytes, 0, b, offset, written);

    return written;
  }

  /**
   * Method which reads a byte from the underlying output stream. Simply calls
   * read()
   *
   * @param b DESCRIBE THE PARAMETER
   * @throws IOException If an error occurs
   */
  public void readFully(byte[] b) throws IOException {
    readFully(b, 0, b.length);
  }

  /**
   * Method which reads a byte from the underlying output stream. Simply calls
   * read()
   *
   * @param b DESCRIBE THE PARAMETER
   * @param offset DESCRIBE THE PARAMETER
   * @param length DESCRIBE THE PARAMETER
   * @throws IOException If an error occurs
   */
  public void readFully(byte[] b, int offset, int length) throws IOException {
    read(b, offset, length);
  }

  /**
   * Method which reads an unsigned byte from the underlying output stream.
   * Simply calls readByte()
   *
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   */
  public int readUnsignedByte() throws IOException {
    return readByte();
  }

  /**
   * Method which reads an unsigned short from the underlying output stream.
   * Simply calls readShort()
   *
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   */
  public int readUnsignedShort() throws IOException {
    return readShort();
  }

  /**
   * Method which reads an int from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public int readInt() throws IOException {
    reader.readStartTag("primitive");
    return readIntHelper();
  }

  /**
   * Method which reads a boolean from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public boolean readBoolean() throws IOException {
    reader.readStartTag("primitive");
    return readBooleanHelper();
  }

  /**
   * Method which reads a byte from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public byte readByte() throws IOException {
    reader.readStartTag("primitive");
    return readByteHelper();
  }

  /**
   * Method which reads a char from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public char readChar() throws IOException {
    reader.readStartTag("primitive");
    return readCharHelper();
  }

  /**
   * Method which reads a double from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public double readDouble() throws IOException {
    reader.readStartTag("primitive");
    return readDoubleHelper();
  }

  /**
   * Method which reads a float from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public float readFloat() throws IOException {
    reader.readStartTag("primitive");
    return readFloatHelper();
  }

  /**
   * Method which reads a long from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public long readLong() throws IOException {
    reader.readStartTag("primitive");
    return readLongHelper();
  }

  /**
   * Method which reads a short from the stream and returns the result.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public short readShort() throws IOException {
    reader.readStartTag("primitive");
    return readShortHelper();
  }

  /**
   * Method which reads a UTF-encoded String from the stream and returns the
   * result. This method simply calls readObject(), as all strings are UTF
   * encoded in XML.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   */
  public String readUTF() throws IOException {
    try {
      return (String) readObject();
    } catch (ClassNotFoundException e) {
      throw new IOException("ReadUTF caused " + e);
    }
  }

  /**
   * Method which is called by ObjectInputStreamm.readObject(), and reads the
   * next object from the stream, and returns the result. If the object (or any
   * of it's descendents) has a readResolve() method, that object is returned.
   *
   * @return The value from the stream
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If a class in the stream cannot be found
   */
  protected Object readObjectOverride() throws IOException, ClassNotFoundException {
    reader.readStartTag();

    Object result = readObjectHelper();

    if (depth == 0) {
      vlist.doCallbacks();
    }

    return result;
  }

  /**
   * Method which reads the next object from the stream and does not record a
   * reference to the object. This guarantees that in future references to this
   * object in the stream will throw an IOException.
   *
   * @return The next object from the stream, without recording a reference
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If a class in the stream cannot be found
   */
  public Object readUnshared() throws IOException, ClassNotFoundException {
    reader.readStartTag();

    Object result = readUnsharedHelper(false);

    if (depth == 0) {
      vlist.doCallbacks();
    }

    return result;
  }

  /**
   * Method which can be called by objects if they have a readObject() method.
   * This method initiates the default field deserialization mechanism, and sets
   * all of the object's fields from the stream. If this method is called, the
   * readFields() method CANNOT be called by the same object. However, one of
   * these two methods MUST be called in the context of a readObject().
   *
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If a class in the stream cannot be found
   * @throws NotActiveException If a object is not currently being read
   */
  public void defaultReadObject() throws IOException, ClassNotFoundException {
    if (currentObjects.peek() != null) {
      readFields(currentObjects.peek(), (Class) currentClasses.peek());
    } else {
      throw new NotActiveException("defaultReadObject called with empty stack!");
    }
  }

  /**
   * Method which can be called by objects if they have a readObject() method.
   * This method initiates the default field deserialization mechanism, and
   * reads all of the default fields in the stream. It does NOT set the field
   * values of the object, however, but instead returns the result to the Object
   * as a GetField object. It is explicitly the responsibility of the object to
   * set its fields appropriately. If this method is called, the
   * defaultReadObject() method CANNOT be called by the same object. However,
   * one of these two methods MUST be called in the context of a readObject().
   *
   * @return A GetField, representing all of this object's fields
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If a class in the stream cannot be found
   * @throws NotActiveException If a object is not currently being read
   */
  public ObjectInputStream.GetField readFields() throws IOException, ClassNotFoundException {
    if (currentObjects.peek() != null) {
      return readGetFields();
    } else {
      throw new NotActiveException("readFields called with empty stack!");
    }
  }

  /**
   * Register an object to be validated before the graph is returned. While
   * similar to resolveObject these validations are called after the entire
   * graph has been reconstituted. Typically, a readObject method will register
   * the object with the stream so that when all of the objects are restored a
   * final set of validations can be performed.
   *
   * @param obj the object to receive the validation callback.
   * @param prio controls the order of callbacks
   * @throws NotActiveException The stream is not currently reading objects so
   *      it is invalid to register a callback.
   * @throws InvalidObjectException The validation object is null.
   */
  public void registerValidation(ObjectInputValidation obj, int prio) throws NotActiveException, InvalidObjectException {
    if (currentObjects.peek() == null) {
      throw new NotActiveException("registerValidation called with empty stack!");
    }

    vlist.register(obj, prio);
  }

  /**
   * Method which returns a new instance of the provided class. It does this by
   * getting the class's serializable constructor (described in
   * getSerializableConstructor()) and using it to create a new object. Thus,
   * all of the Serializable superclasses of the object must be initialized from
   * the stream.
   *
   * @param c The class to create a new instance of
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   */
  protected Object newInstance(Class c) throws IOException {
    try {
      Constructor cons = (Constructor) CONSTRUCTORS.get(c);

      if (cons == null) {
        if (Externalizable.class.isAssignableFrom(c)) {
          cons = c.getDeclaredConstructor(new Class[0]);
        } else {
          cons = getSerializableConstructor(c);
        }

        cons.setAccessible(true);
        CONSTRUCTORS.put(c, cons);
      }

      return cons.newInstance(new Object[0]);
    } catch (InstantiationException e) {
      throw new IOException("Could not instanciate new class " + c);
    } catch (IllegalAccessException e) {
      throw new IOException("Could not instanciate new class " + c);
    } catch (InvocationTargetException e) {
      throw new IOException("Could not instanciate new class " + c);
    } catch (NoSuchMethodException e) {
      throw new IOException("Could not instanciate new class " + c);
    }
  }


  // ----- ObjectInputStreamm Referencing Methods -----

  /**
   * Method which adds a reference in the hashtable of references. Multiple
   * calls to this method will replace prior objects.
   *
   * @param reference The reference name to use
   * @param o The object to reference
   */
  protected void putReference(String reference, Object o) {
    references.put(reference, o);
  }

  // ----- Internal readX methods -----

  /**
   * Method which reads a primitive value from the stream and returns the String
   * representation to the callee for processing. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @param type The type of primitive which should be next
   * @return The String representation of the primitive
   * @throws IOException If an error occurs
   */
  protected String readPrimitive(String type) throws IOException {
    reader.assertStartTag("primitive");
    reader.assertAttribute("type", type);
    String value = reader.getAttribute("value");
    reader.readEndTag("primitive");

    return value;
  }

  /**
   * Method which reads an int from the stream. This method assumes that the XML
   * reader is currently on a primitive tag, and does consume the end primitive
   * tag.
   *
   * @return The int read from the stream
   * @throws IOException If an error occurs
   */
  protected int readIntHelper() throws IOException {
    return Integer.parseInt(readPrimitive("int"));
  }

  /**
   * Method which reads an boolean from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The boolean read from the stream
   * @throws IOException If an error occurs
   */
  protected boolean readBooleanHelper() throws IOException {
    return readPrimitive("boolean").equals("true");
  }

  /**
   * Method which reads an byte from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The byte read from the stream
   * @throws IOException If an error occurs
   */
  protected byte readByteHelper() throws IOException {
    return Byte.parseByte(readPrimitive("byte"));
  }

  /**
   * Method which reads an char from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The char read from the stream
   * @throws IOException If an error occurs
   */
  protected char readCharHelper() throws IOException {
    return readPrimitive("char").charAt(0);
  }

  /**
   * Method which reads an double from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The double read from the stream
   * @throws IOException If an error occurs
   */
  protected double readDoubleHelper() throws IOException {
    return Double.parseDouble(readPrimitive("double"));
  }

  /**
   * Method which reads an float from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The float read from the stream
   * @throws IOException If an error occurs
   */
  protected float readFloatHelper() throws IOException {
    return Float.parseFloat(readPrimitive("float"));
  }

  /**
   * Method which reads an long from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The long read from the stream
   * @throws IOException If an error occurs
   */
  protected long readLongHelper() throws IOException {
    return Long.parseLong(readPrimitive("long"));
  }

  /**
   * Method which reads an short from the stream. This method assumes that the
   * XML reader is currently on a primitive tag, and does consume the end
   * primitive tag.
   *
   * @return The short read from the stream
   * @throws IOException If an error occurs
   */
  protected short readShortHelper() throws IOException {
    return Short.parseShort(readPrimitive("short"));
  }

  /**
   * Method which reads an object from the stream. This method assumes that the
   * XML reader is currently on a start tag, and does consume the corresponding
   * end tag. If the next object represents a reference or null, then the
   * appropriate helper is called. Otherwise, readObjectUnshared() is called to
   * process the object.
   *
   * @return The object read from the stream
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readObjectHelper() throws IOException, ClassNotFoundException {
    reader.assertStartTag();
    depth++;
    Object result = null;

    if (reader.getStartTag().equals("reference")) {
      result = readReference();
    } else if (reader.getStartTag().equals("null")) {
      result = readNull();
    } else {
      result = readUnsharedHelper(true);
    }

    depth--;
    return result;
  }

  /**
   * Method which reads an object from the stream. This method assumes that the
   * XML reader is currently on a start tag, and does consume the corresponding
   * end tag. This method also assumes that the type of object which is being
   * read is *sharable*, even if it is not going to be shared. Thus, this method
   * can read objects of type String, Array, or Serializable.
   *
   * @param shared Whether or not to record a reference to this object
   * @return The object read from the stream
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readUnsharedHelper(boolean shared) throws IOException, ClassNotFoundException {
    reader.assertStartTag();

    if (reader.getStartTag().equals("string")) {
      return readString(shared);
    } else if (reader.getStartTag().equals("array")) {
      return readArray(shared);
    } else if (reader.getStartTag().equals("object")) {
      return readOrdinaryObject(shared);
    } else {
      throw new IOException("Unknown element name " + reader.getStartTag());
    }
  }

  /**
   * Method which reads a reference off of the stream, and looks the reference
   * up in the references table. If not corresponding object is found, an
   * IOException is thrown. This method expected that a reference tag has just
   * been read, and it does consume the end reference tag.
   *
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs or if a reference is not found
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readReference() throws IOException, ClassNotFoundException {
    reader.assertStartTag("reference");

    Object result = getReference(reader.getAttribute("idref"));

    if (result == null) {
      throw new IOException("Invalid reference " + reader.getAttribute("idref") + " found.");
    }

    reader.readEndTag("reference");

    return result;
  }

  /**
   * Method which reads a null item off of the stream. This method expected that
   * a null tag has just been read, and it does consume the end null tag.
   *
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readNull() throws IOException, ClassNotFoundException {
    reader.assertStartTag("null");
    reader.readEndTag("null");

    return null;
  }

  /**
   * Method which reads a string item off of the stream. This method expects
   * that a start string tag has just been read, an it does consume the end
   * start tag. If this string is to be shared, a reference is added to the
   * references tag.
   *
   * @param shared Whether or not to add this string to the references table
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readString(boolean shared) throws IOException, ClassNotFoundException {
    reader.assertStartTag("string");

    String result = new String(reader.getAttribute("value"));

    if (shared && (reader.getAttribute("id") != null)) {
      putReference(reader.getAttribute("id"), result);
    }

    reader.readEndTag("string");

    return result;
  }

  /**
   * Method which reads an ordinary object from the stream (not a String or
   * Array). This method assumes that the XML reader is currently on a start
   * tag, and does consume the corresponding end tag. This method first reads
   * the class type, and if it is Serializable, it constructs a new instance.
   * Then, if the object is shared and the id field is not null, it records a
   * reference to the object. If the object is Externalizable, the
   * readExternal() method is called and the object is returned. Otherwise, each
   * of the superclasses are read, from highest to lowest, using the readClass()
   * method. Lastly, if the object defines a readResolve() method, it is called
   * and the result is recorded and returned.
   *
   * @param shared Whether or not to record a reference to this object
   * @return The object read from the stream
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   * @throws NotSerializableException If the class to be deserialized is not
   *      Serializable
   */
  protected Object readOrdinaryObject(boolean shared) throws IOException, ClassNotFoundException {
    reader.assertStartTag("object");

    Class c = Class.forName(reader.getAttribute("class"));
    String id = reader.getAttribute("id");

    if (!Serializable.class.isAssignableFrom(c)) {
      throw new NotSerializableException(c.getName());
    }

    Object o = newInstance(c);

    if (shared && (id != null)) {
      putReference(id, o);
    }

    if (Externalizable.class.isAssignableFrom(c)) {
      ((Externalizable) o).readExternal(this);
      reader.step();
    } else {
      reader.step();

      while (!reader.isEndTag()) {
        readClass(o);
        reader.step();
      }
    }

    reader.assertEndTag("object");

    Method method = getReadResolve(c);

    try {
      if (method != null) {
        o = method.invoke(o, new Object[0]);

        if (shared && (id != null)) {
          putReference(id, o);
        }
      }
    } catch (IllegalAccessException e) {
      throw new IOException("ReadResolve caused " + e);
    } catch (InvocationTargetException e) {
      throw new IOException("ReadResolve caused " + e);
    }

    return o;
  }

  /**
   * Method which reads the information for one class for a given object from
   * the stream. This method assumes that the XML reader is currently on a start
   * declaredClass tag, and does consume the corresponding end declaredClass
   * tag. This method first reads the class type, and if it defines a
   * readObject() method, it is called. Otherwise, the fields are read in a
   * default manner. Any extra data not read in the stream is read and ignored.
   *
   * @param o The object we are reading the the class for
   * @return The object read from the stream
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readClass(Object o) throws IOException, ClassNotFoundException {
    reader.assertStartTag("declaredClass");

    Class c = Class.forName(reader.getAttribute("class"));

    Method method = getReadObject(c);

    if (method != null) {
      try {
        currentObjects.push(o);
        currentClasses.push(c);
        method.invoke(o, new Object[]{this});
        currentObjects.pop();
        currentClasses.pop();
        readUnreadOptionalData();
      } catch (InvocationTargetException e) {
        System.out.println(e.getTargetException().getMessage());
        e.getTargetException().printStackTrace();
        throw new IOException("InvocationTargetException thrown! " + e.getTargetException());
      } catch (IllegalAccessException e) {
        System.out.println(e.getMessage());
        e.printStackTrace();
        throw new IOException("IllegalAccessException thrown! " + e);
      }
    } else {
      readFields(o, c);
      readUnreadOptionalData();
    }

    reader.assertEndTag("declaredClass");

    return o;
  }

  /**
   * Method which reads any extra data from the stream which was not read by the
   * object. This method simply loops until it hits the appropriate end
   * declaredClass tag, and then returns. Thus, any data read by this method is
   * ignored. However, the objects here are deserialized as the stream may
   * reference them in the future.
   *
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected void readUnreadOptionalData() throws IOException, ClassNotFoundException {
    reader.step();

    while (!reader.isEndTag()) {
      if (reader.getStartTag().equals("primitive")) {
        reader.readEndTag("primitive");
      } else if (reader.getStartTag().equals("base64")) {
        reader.readEndTag("base64");
      } else {
        readObjectHelper();
      }

      reader.step();
    }
  }

  /**
   * Method which reads all of the field data from the stream, as readFields()
   * does, but instead of assigning the fields to the object, it returns them as
   * a GetField object. In this manner, the object itself can perform field
   * inititalization.
   *
   * @return The fields read from the stream
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected GetField readGetFields() throws IOException, ClassNotFoundException {
    reader.readStartTag("default");

    GetField g = new GetField();
    reader.step();

    while (!reader.isEndTag()) {
      readGetField(g);
      reader.step();
    }

    reader.assertEndTag("default");

    return g;
  }

  /**
   * Method which read a single field from the stream and places it in the
   * provided GetField object. This method assumes that the XML reader is on a
   * start tag, and that the item read has a field attribute.
   *
   * @param g The GetField object into which the field should be put
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected void readGetField(GetField g) throws IOException, ClassNotFoundException {
    reader.assertStartTag();

    String name = reader.getAttribute("field");

    if (name == null) {
      throw new IOException("Could not read field " + reader.getStartTag() + ", as field attribute was null!");
    }

    if (reader.getStartTag().equals("primitive")) {
      readPrimitiveGetField(g);
    } else {
      g.put(name, readObjectHelper());
    }
  }

  /**
   * Method which reads a primitive field from the stream, and places it in the
   * provided GetField object. This method assumes that the XML parser has just
   * read a primitive start tag, and that the tag has a field attribute. This
   * method does consume that end primitive tag.
   *
   * @param g The GetField object to put the primitive in
   * @exception ClassNotFoundException DESCRIBE THE EXCEPTION
   * @throws IOException If an error occurs
   */
  protected void readPrimitiveGetField(GetField g) throws IOException, ClassNotFoundException {
    reader.assertStartTag("primitive");
    String name = reader.getAttribute("field");

    if (name == null) {
      throw new IOException("Could not read primitive field " + reader.getAttribute("type") + ", as field attribute was null!");
    }

    Class c = getClass(reader.getAttribute("type"));

    if (c.equals(Integer.TYPE)) {
      g.put(name, readIntHelper());
    } else if (c.equals(Boolean.TYPE)) {
      g.put(name, readBooleanHelper());
    } else if (c.equals(Byte.TYPE)) {
      g.put(name, readByteHelper());
    } else if (c.equals(Character.TYPE)) {
      g.put(name, readCharHelper());
    } else if (c.equals(Double.TYPE)) {
      g.put(name, readDoubleHelper());
    } else if (c.equals(Float.TYPE)) {
      g.put(name, readFloatHelper());
    } else if (c.equals(Long.TYPE)) {
      g.put(name, readLongHelper());
    } else if (c.equals(Short.TYPE)) {
      g.put(name, readShortHelper());
    } else {
      throw new IllegalArgumentException("Field " + name + " is not primitive!");
    }
  }

  /**
   * Method which initiates the default field reading mechanism for the given
   * object's class. This method reads the start default tag, all of the fields,
   * and then reads the end default tag. It reads all of the fields using the
   * readField() method.
   *
   * @param o The object which is currently being read in
   * @param c The class to read the fields for
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected void readFields(Object o, Class c) throws IOException, ClassNotFoundException {
    reader.readStartTag("default");
    reader.step();

    while (!reader.isEndTag()) {
      readField(o, c);
      reader.step();
    }

    reader.assertEndTag("default");
  }

  /**
   * Method which reads a single field from the stream, and assignes it to the
   * provided object. If the field cannot be found in the given class, it is
   * deserialized and then ignored, in case it is referenced later in the
   * stream.
   *
   * @param o The object which is currently being read in
   * @param c The class to read the fields for
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected void readField(Object o, Class c) throws IOException, ClassNotFoundException {
    reader.assertStartTag();
    String field = reader.getAttribute("field");

    try {
      Field f = c.getDeclaredField(reader.getAttribute("field"));
      f.setAccessible(true);

      int mask = Modifier.STATIC;
      if ((f.getModifiers() & mask) != 0) {
        throw new NoSuchFieldException("Field read was static!");
      }

      if (reader.getStartTag().equals("primitive")) {
        readPrimitiveField(o, f);
      } else {
        f.set(o, readObjectHelper());
      }
    } catch (NoSuchFieldException e) {
      if (reader.getStartTag().equals("primitive")) {
        reader.readEndTag("primitive");
      } else {
        readObjectHelper();
      }
    } catch (IllegalAccessException e) {
      // skip IAE, which is very likely caused by setting a final field on < JVM 1.5
    } catch (IllegalArgumentException e) {
      System.err.println("COULD NOT SET FIELD " + field + " OF " + o.getClass().getName() + " - SKIPPING.  THIS SHOULD NOT HAPPEN!" + e);
      e.printStackTrace();
    }
  }

  /**
   * Method which reads a primitive field from the stream, and places it in the
   * given object. This method assumes that the XML parser has just read a
   * primitive start tag, and that the tag has a field attribute. This method
   * does consume that end primitive tag.
   *
   * @param o The object to put the primitive in
   * @param f The field representing the primitive about to be read
   * @exception IllegalAccessException DESCRIBE THE EXCEPTION
   * @throws IOException If an error occurs
   */
  protected void readPrimitiveField(Object o, Field f) throws IOException, IllegalAccessException {
    reader.assertStartTag("primitive");

    if (f.getType().equals(Integer.TYPE)) {
      f.setInt(o, readIntHelper());
    } else if (f.getType().equals(Boolean.TYPE)) {
      f.setBoolean(o, readBooleanHelper());
    } else if (f.getType().equals(Byte.TYPE)) {
      f.setByte(o, readByteHelper());
    } else if (f.getType().equals(Character.TYPE)) {
      f.setChar(o, readCharHelper());
    } else if (f.getType().equals(Double.TYPE)) {
      f.setDouble(o, readDoubleHelper());
    } else if (f.getType().equals(Float.TYPE)) {
      f.setFloat(o, readFloatHelper());
    } else if (f.getType().equals(Long.TYPE)) {
      f.setLong(o, readLongHelper());
    } else if (f.getType().equals(Short.TYPE)) {
      f.setInt(o, readShortHelper());
    } else {
      throw new IllegalArgumentException("Field " + f + " is not primitive!");
    }
  }

  /**
   * Method which reads an array off of the stream. This method expects that a
   * start array tag has just been read, and does consume the end array tag.
   * This method constructs an array of the provided base type, and then
   * recursively reads in all of the objects for the array.
   *
   * @param shared Whether or not to add this array to the references table
   * @return DESCRIBE THE RETURN VALUE
   * @throws IOException If an error occurs
   * @throws ClassNotFoundException If the class cannot be found
   */
  protected Object readArray(boolean shared) throws IOException, ClassNotFoundException {
    reader.assertStartTag("array");

    Class c = getClass(reader.getAttribute("base"));

    int length = Integer.valueOf(reader.getAttribute("length")).intValue();
    int dim = Integer.valueOf(reader.getAttribute("dim")).intValue();
    Object result = Array.newInstance(c, length);

    if (shared && (reader.getAttribute("id") != null)) {
      putReference(reader.getAttribute("id"), result);
    }

    for (int i = 0; i < length; i++) {
      if (c.equals(Integer.TYPE)) {
        Array.setInt(result, i, readInt());
      } else if (c.equals(Boolean.TYPE)) {
        Array.setBoolean(result, i, readBoolean());
      } else if (c.equals(Byte.TYPE)) {
        Array.setByte(result, i, readByte());
      } else if (c.equals(Character.TYPE)) {
        Array.setChar(result, i, readChar());
      } else if (c.equals(Double.TYPE)) {
        Array.setDouble(result, i, readDouble());
      } else if (c.equals(Float.TYPE)) {
        Array.setFloat(result, i, readFloat());
      } else if (c.equals(Long.TYPE)) {
        Array.setLong(result, i, readLong());
      } else if (c.equals(Short.TYPE)) {
        Array.setShort(result, i, readShort());
      } else {
        Array.set(result, i, readObject());
      }
    }

    reader.readEndTag("array");

    return result;
  }

  // ----- Internal helper methods -----


  /**
   * This method returns the readResolve() method of a given class, if such a
   * method exists. This method searches the class's heirarchy for a
   * readResolve() method which is assessible by the given class. If no such
   * method is found, null is returned.
   *
   * @param cl The class to find the readResolve() of
   * @return The method, or null if none was found
   */
  private static Method getReadResolve(Class cl) {
    synchronized (READ_RESOLVES) {
      if (READ_RESOLVES.containsKey(cl)) {
        return (Method) READ_RESOLVES.get(cl);
      }

      Method meth = null;
      Class defCl = cl;
      while (defCl != null) {
        try {
          meth = defCl.getDeclaredMethod("readResolve", new Class[0]);
          break;
        } catch (NoSuchMethodException ex) {
          defCl = defCl.getSuperclass();
        }
      }

      if (meth == null) {
        READ_RESOLVES.put(cl, null);
        return null;
      }

      meth.setAccessible(true);
      int mods = meth.getModifiers();
      if ((mods & (Modifier.STATIC | Modifier.ABSTRACT)) != 0) {
        READ_RESOLVES.put(cl, null);
        return null;
      } else if ((mods & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0) {
        READ_RESOLVES.put(cl, meth);
        return meth;
      } else if ((mods & Modifier.PRIVATE) != 0) {
        if (cl == defCl) {
          READ_RESOLVES.put(cl, meth);
          return meth;
        } else {
          READ_RESOLVES.put(cl, null);
          return null;
        }
      } else {
        READ_RESOLVES.put(cl, meth);
        return meth;
      }
    }
  }

  /**
   * This method returns the readResolve() method of a given class, if such a
   * method exists. This method searches the class's heirarchy for a
   * readResolve() method which is assessible by the given class. If no such
   * method is found, null is returned.
   *
   * @param cl The class to find the readResolve() of
   * @return The method, or null if none was found
   */
  private static Method getReadObject(Class cl) {
    synchronized (READ_OBJECTS) {
      if (READ_OBJECTS.containsKey(cl)) {
        return (Method) READ_OBJECTS.get(cl);
      }

      try {
        Method method = cl.getDeclaredMethod("readObject", new Class[]{ObjectInputStream.class});
        method.setAccessible(true);

        READ_OBJECTS.put(cl, method);
        return method;
      } catch (NoSuchMethodException e) {
        READ_OBJECTS.put(cl, null);
        return null;
      }
    }
  }


  // ----- Implementation of GetField -----

  /**
   * This class is an implementation of GetField which is compatible with the
   * XMLObjectInputStreamm. It works in the same manner as the
   * ObjectInputStreamm.GetField.
   *
   * @version $Id: pretty.settings 2305 2005-03-11 20:22:33Z jeffh $
   * @author jeffh
   */
  public class GetField extends ObjectInputStream.GetField {

    /**
     * DESCRIBE THE FIELD
     */
    protected HashMap primitives = new HashMap();

    /**
     * DESCRIBE THE FIELD
     */
    protected HashMap objects = new HashMap();

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public boolean get(String name, boolean value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Boolean) primitives.get(name)).booleanValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public byte get(String name, byte value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Byte) primitives.get(name)).byteValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public char get(String name, char value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Character) primitives.get(name)).charValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public double get(String name, double value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Double) primitives.get(name)).doubleValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public float get(String name, float value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Float) primitives.get(name)).floatValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public int get(String name, int value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Integer) primitives.get(name)).intValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public long get(String name, long value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Long) primitives.get(name)).longValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public short get(String name, short value) {
      if (primitives.get(name) == null) {
        return value;
      }

      return ((Short) primitives.get(name)).shortValue();
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public Object get(String name, Object value) {
      if (objects.get(name) == null) {
        return value;
      }

      return objects.get(name);
    }

    /**
     * Gets the ObjectStreamClass attribute of the GetField object
     *
     * @return The ObjectStreamClass value
     */
    public ObjectStreamClass getObjectStreamClass() {
      throw new UnsupportedOperationException("CANNOT GET THE OBJECT STREAM CLASS!");
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @return DESCRIBE THE RETURN VALUE
     */
    public boolean defaulted(String name) {
      return (!(primitives.containsKey(name) || objects.containsKey(name)));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, boolean value) {
      primitives.put(name, new Boolean(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, byte value) {
      primitives.put(name, new Byte(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, char value) {
      primitives.put(name, new Character(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, double value) {
      primitives.put(name, new Double(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, float value) {
      primitives.put(name, new Float(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, int value) {
      primitives.put(name, new Integer(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, long value) {
      primitives.put(name, new Long(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, short value) {
      primitives.put(name, new Short(value));
    }

    /**
     * DESCRIBE THE METHOD
     *
     * @param name DESCRIBE THE PARAMETER
     * @param value DESCRIBE THE PARAMETER
     */
    protected void put(String name, Object value) {
      objects.put(name, value);
    }
  }

  // ----- VALIDATION LIST IMPLEMENTATION ----

  /**
   * Prioritized list of callbacks to be performed once object graph has been
   * completely deserialized.
   *
   * @version $Id: pretty.settings 2305 2005-03-11 20:22:33Z jeffh $
   * @author jeffh
   */
  private static class ValidationList {

    /**
     * linked list of callbacks
     */
    private Callback list;

    /**
     * Creates new (empty) ValidationList.
     */
    ValidationList() {
    }

    /**
     * Registers callback. Throws InvalidObjectException if callback object is
     * null.
     *
     * @param obj DESCRIBE THE PARAMETER
     * @param priority DESCRIBE THE PARAMETER
     * @exception InvalidObjectException DESCRIBE THE EXCEPTION
     */
    void register(ObjectInputValidation obj, int priority) throws InvalidObjectException {
      if (obj == null) {
        throw new InvalidObjectException("null callback");
      }

      Callback prev = null;

      Callback cur = list;
      while (cur != null && priority < cur.priority) {
        prev = cur;
        cur = cur.next;
      }
      if (prev != null) {
        prev.next = new Callback(obj, priority, cur);
      } else {
        list = new Callback(obj, priority, list);
      }
    }

    /**
     * Invokes all registered callbacks and clears the callback list. Callbacks
     * with higher priorities are called first; those with equal priorities may
     * be called in any order. If any of the callbacks throws an
     * InvalidObjectException, the callback process is terminated and the
     * exception propagated upwards.
     *
     * @exception InvalidObjectException DESCRIBE THE EXCEPTION
     */
    void doCallbacks() throws InvalidObjectException {
      try {
        while (list != null) {
          list.obj.validateObject();
          list = list.next;
        }
      } catch (InvalidObjectException ex) {
        list = null;
        throw ex;
      }
    }

    /**
     * Resets the callback list to its initial (empty) state.
     */
    public void clear() {
      list = null;
    }

    /**
     * DESCRIBE THE CLASS
     *
     * @version $Id: pretty.settings 2305 2005-03-11 20:22:33Z jeffh $
     * @author jeffh
     */
    private static class Callback {
      final ObjectInputValidation obj;
      final int priority;
      Callback next;

      /**
       * Constructor for Callback.
       *
       * @param obj DESCRIBE THE PARAMETER
       * @param priority DESCRIBE THE PARAMETER
       * @param next DESCRIBE THE PARAMETER
       */
      Callback(ObjectInputValidation obj, int priority, Callback next) {
        this.obj = obj;
        this.priority = priority;
        this.next = next;
      }
    }
  }
}
