/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * XMLSerialization.java
 * Copyright (C) 2004-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core.xml;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import weka.core.RevisionHandler;
import weka.core.RevisionUtils;
import weka.core.Utils;
import weka.core.Version;

/**
 * With this class objects can be serialized to XML instead into a binary
 * format. It uses introspection (cf. beans) to retrieve the data from the given
 * object, i.e. it can only access beans-conform fields automatically.
 * <p>
 * The generic approach of writing data as XML can be overriden by adding custom
 * methods for reading/writing in a derived class (cf. <code>m_Properties</code>, <code>m_CustomMethods</code>).<br>
 * Custom read and write methods must have the same signature (and also be
 * <code>public</code>!) as the <code>readFromXML</code> and
 * <code>writeToXML</code> methods. Methods that apply to the naming rule
 * <code>read + property name</code> are added automatically to the list of
 * methods by the method
 * <code>XMLSerializationMethodHandler.addMethods(...)</code>.
 * <p>
 * Other properties that are not conform the bean set/get-methods have to be
 * processed manually in a derived class (cf.
 * <code>readPostProcess(Object)</code>, <code>writePostProcess(Object)</code>).
 * <p>
 * For a complete XML serialization/deserialization have a look at the
 * <code>KOML</code> class.
 * <p>
 * If a stored class has a constructor that takes a String to initialize (e.g.
 * String or Double) then the content of the tag will used for the constructor,
 * e.g. from
 * 
 * <pre>
 * &lt;object name="name" class="String" primitive="no"&gt;Smith&lt;/object&gt;
 * </pre>
 * 
 * "Smith" will be used to instantiate a String object as constructor argument.
 * <p>
 * 
 * @see KOML
 * @see #fromXML(Document)
 * @see #toXML(Object)
 * @see #m_Properties
 * @see #m_CustomMethods
 * @see #readPostProcess(Object)
 * @see #writePostProcess(Object)
 * @see #readFromXML(Element)
 * @see #writeToXML(Element, Object, String)
 * 
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 9413 $
 */
public class XMLSerialization implements RevisionHandler {

  /** for debugging purposes only */
  protected static boolean DEBUG = false;

  /**
   * the node that is currently processed, in case of writing the parent node
   * (something might go wrong writing the new child) and in case of reading the
   * actual node that is tried to process
   */
  protected Element m_CurrentNode = null;

  /** the tag for an object */
  public final static String TAG_OBJECT = "object";

  /** the version attribute */
  public final static String ATT_VERSION = XMLDocument.ATT_VERSION;

  /** the tag for the name */
  public final static String ATT_NAME = XMLDocument.ATT_NAME;

  /** the tag for the class */
  public final static String ATT_CLASS = "class";

  /** the tag whether primitive or not (yes/no) */
  public final static String ATT_PRIMITIVE = "primitive";

  /** the tag whether array or not (yes/no) */
  public final static String ATT_ARRAY = "array";

  /** the tag whether null or not (yes/no) */
  public final static String ATT_NULL = "null";

  /** the value "yes" for the primitive and array attribute */
  public final static String VAL_YES = XMLDocument.VAL_YES;

  /** the value "no" for the primitive and array attribute */
  public final static String VAL_NO = XMLDocument.VAL_NO;

  /** the value of the name for the root node */
  public final static String VAL_ROOT = "__root__";

  /** the root node of the XML document */
  public final static String ROOT_NODE = TAG_OBJECT;

  /**
   * default value for attribute ATT_PRIMITIVE
   * 
   * @see #ATT_PRIMITIVE
   */
  public final static String ATT_PRIMITIVE_DEFAULT = VAL_NO;

  /**
   * default value for attribute ATT_ARRAY
   * 
   * @see #ATT_ARRAY
   */
  public final static String ATT_ARRAY_DEFAULT = VAL_NO;

  /**
   * default value for attribute ATT_NULL
   * 
   * @see #ATT_NULL
   */
  public final static String ATT_NULL_DEFAULT = VAL_NO;

  /** the DOCTYPE for the serialization */
  public final static String DOCTYPE = "<!" + XMLDocument.DTD_DOCTYPE + " "
      + ROOT_NODE + "\n" + "[\n" + "   <!" + XMLDocument.DTD_ELEMENT + " "
      + TAG_OBJECT + " (" + XMLDocument.DTD_PCDATA + XMLDocument.DTD_SEPARATOR
      + TAG_OBJECT + ")" + XMLDocument.DTD_ZERO_OR_MORE + ">\n" + "   <!"
      + XMLDocument.DTD_ATTLIST + " " + TAG_OBJECT + " " + ATT_NAME + "      "
      + XMLDocument.DTD_CDATA + " " + XMLDocument.DTD_REQUIRED + ">\n"
      + "   <!" + XMLDocument.DTD_ATTLIST + " " + TAG_OBJECT + " " + ATT_CLASS
      + "     " + XMLDocument.DTD_CDATA + " " + XMLDocument.DTD_REQUIRED
      + ">\n" + "   <!" + XMLDocument.DTD_ATTLIST + " " + TAG_OBJECT + " "
      + ATT_PRIMITIVE + " " + XMLDocument.DTD_CDATA + " \""
      + ATT_PRIMITIVE_DEFAULT + "\">\n" + "   <!" + XMLDocument.DTD_ATTLIST
      + " " + TAG_OBJECT + " " + ATT_ARRAY + "     " + XMLDocument.DTD_CDATA
      + " \"" + ATT_ARRAY_DEFAULT
      + "\">   <!-- the dimensions of the array; no=0, yes=1 -->\n" + "   <!"
      + XMLDocument.DTD_ATTLIST + " " + TAG_OBJECT + " " + ATT_NULL + "      "
      + XMLDocument.DTD_CDATA + " \"" + ATT_NULL_DEFAULT + "\">\n" + "   <!"
      + XMLDocument.DTD_ATTLIST + " " + TAG_OBJECT + " " + ATT_VERSION + "   "
      + XMLDocument.DTD_CDATA + " \"" + Version.VERSION + "\">\n" + "]\n" + ">";

  /**
   * List of fully qualified property names to suppress any warning messages for
   */
  public final static List<String> SUPPRESS_PROPERTY_WARNINGS = new ArrayList<String>();

  /** the XMLDocument that performs the transformation to and fro XML */
  protected XMLDocument m_Document = null;

  /** for handling properties (ignored/allowed) */
  protected PropertyHandler m_Properties = null;

  /** for handling custom read/write methods */
  protected XMLSerializationMethodHandler m_CustomMethods = null;

  /**
   * for overriding class names (Class &lt;-&gt; Classname (String))
   * 
   * @see #overrideClassname(Object)
   */
  protected Hashtable<Class, String> m_ClassnameOverride = null;

  /**
   * initializes the serialization
   * 
   * @throws Exception if initialization fails
   */
  public XMLSerialization() throws Exception {
    super();
    clear();
  }

  /**
   * used for debugging purposes, i.e. only if DEBUG is set to true. needs a
   * newly generated Throwable instance to get the method/line from
   * 
   * @param t a throwable instance, generated in the calling method
   * @param msg a message to pring
   * @see #DEBUG
   */
  protected void trace(Throwable t, String msg) {
    if ((DEBUG) && (t.getStackTrace().length > 0)) {
      System.out.println("trace: " + t.getStackTrace()[0] + ": " + msg);
    }
  }

  /**
   * generates internally a new XML document and clears also the IgnoreList and
   * the mappings for the Read/Write-Methods
   * 
   * @throws Exception if something goes wrong
   */
  public void clear() throws Exception {
    m_Document = new XMLDocument();
    m_Document.setValidating(true);
    m_Document.newDocument(DOCTYPE, ROOT_NODE);

    m_Properties = new PropertyHandler();
    m_CustomMethods = new XMLSerializationMethodHandler(this);

    m_ClassnameOverride = new Hashtable<Class, String>();
    // java.io.File is sometimes represented as another class:
    // - Win32: sun.awt.shell.Win32ShellFolder2
    // - Linux: sun.awt.shell.DefaultShellFolder
    // -> we set it to "java.io.File"
    m_ClassnameOverride.put(java.io.File.class, java.io.File.class.getName());

    setVersion(Version.VERSION);

    m_CurrentNode = null;
  }

  /**
   * sets the given version string in the XML document
   * 
   * @param version the new version string
   */
  private void setVersion(String version) {
    Document doc;

    doc = m_Document.getDocument();
    doc.getDocumentElement().setAttribute(ATT_VERSION, version);
  }

  /**
   * returns the WEKA version with which the serialized object was created
   * 
   * @return the current version
   * @see Version
   */
  public String getVersion() {
    Document doc;
    String result;

    doc = m_Document.getDocument();
    result = doc.getDocumentElement().getAttribute(ATT_VERSION);

    return result;
  }

  /**
   * Checks the version in the current Document with the one of the current
   * release. If the version differ, a warning is printed.
   */
  private void checkVersion() {
    String versionStr;
    Version version;

    version = new Version();
    versionStr = getVersion();
    if (versionStr.equals(""))
      System.out.println("WARNING: has no version!");
    else if (version.isOlder(versionStr))
      System.out.println("WARNING: loading a newer version (" + versionStr
          + " > " + Version.VERSION + ")!");
    else if (version.isNewer(versionStr))
      System.out.println("NOTE: loading an older version (" + versionStr
          + " < " + Version.VERSION + ")!");
  }

  /**
   * returns a hashtable with PropertyDescriptors that have "get" and "set"
   * methods indexed by the property name.
   * 
   * @see java.beans.PropertyDescriptor
   * @param o the object to retrieve the descriptors from
   * @return the PropertyDescriptors indexed by name of the property
   * @throws Exception if the introspection fails
   */
  protected Hashtable getDescriptors(Object o) throws Exception {
    BeanInfo info;
    PropertyDescriptor[] desc;
    int i;
    Hashtable<String, PropertyDescriptor> result;

    result = new Hashtable<String, PropertyDescriptor>();

    info = Introspector.getBeanInfo(o.getClass());
    desc = info.getPropertyDescriptors();
    for (i = 0; i < desc.length; i++) {
      // get AND set method?
      if ((desc[i].getReadMethod() != null)
          && (desc[i].getWriteMethod() != null)) {
        // in ignore list, i.e. a general ignore without complete path?
        if (m_Properties.isIgnored(desc[i].getDisplayName()))
          continue;

        // in ignore list of the class?
        if (m_Properties.isIgnored(o, desc[i].getDisplayName()))
          continue;

        // not an allowed property
        if (!m_Properties.isAllowed(o, desc[i].getDisplayName()))
          continue;

        result.put(desc[i].getDisplayName(), desc[i]);
      }
    }

    return result;
  }

  /**
   * returns the path of the "name" attribute from the root down to this node
   * (including it).
   * 
   * @param node the node to get the path for
   * @return the complete "name" path of this node
   */
  protected String getPath(Element node) {
    String result;

    result = node.getAttribute(ATT_NAME);

    while (node.getParentNode() != node.getOwnerDocument()) {
      node = (Element) node.getParentNode();
      result = node.getAttribute(ATT_NAME) + "." + result;
    }

    return result;
  }

  /**
   * returns either <code>VAL_YES</code> or <code>VAL_NO</code> depending on the
   * value of <code>b</code>
   * 
   * @param b the boolean to turn into a string
   * @return the value in string representation
   */
  protected String booleanToString(boolean b) {
    if (b)
      return VAL_YES;
    else
      return VAL_NO;
  }

  /**
   * turns the given string into a boolean, if a positive number is given, then
   * zero is considered FALSE, every other number TRUE; the empty string is also
   * considered being FALSE
   * 
   * @param s the string to turn into a boolean
   * @return the string as boolean
   */
  protected boolean stringToBoolean(String s) {
    if (s.equals(""))
      return false;
    else if (s.equals(VAL_YES))
      return true;
    else if (s.equalsIgnoreCase("true"))
      return true;
    else if (s.replaceAll("[0-9]*", "").equals(""))
      return (Integer.parseInt(s) != 0);
    else
      return false;
  }

  /**
   * appends a new node to the parent with the given parameters (a non-array)
   * 
   * @param parent the parent of this node. if it is <code>null</code> the
   *          document root element is used
   * @param name the name of the node
   * @param classname the classname for this node
   * @param primitive whether it is a primitve data type or not (i.e. an object)
   * @return the generated node
   */
  protected Element addElement(Element parent, String name, String classname,
      boolean primitive) {
    return addElement(parent, name, classname, primitive, 0);
  }

  /**
   * appends a new node to the parent with the given parameters
   * 
   * @param parent the parent of this node. if it is <code>null</code> the
   *          document root element is used
   * @param name the name of the node
   * @param classname the classname for this node
   * @param primitive whether it is a primitve data type or not (i.e. an object)
   * @param array the dimensions of the array (0 if not an array)
   * @return the generated node
   */
  protected Element addElement(Element parent, String name, String classname,
      boolean primitive, int array) {
    return addElement(parent, name, classname, primitive, array, false);
  }

  /**
   * appends a new node to the parent with the given parameters
   * 
   * @param parent the parent of this node. if it is <code>null</code> the
   *          document root element is used
   * @param name the name of the node
   * @param classname the classname for this node
   * @param primitive whether it is a primitve data type or not (i.e. an object)
   * @param array the dimensions of the array (0 if not an array)
   * @param isnull whether it is null
   * @return the generated node
   */
  protected Element addElement(Element parent, String name, String classname,
      boolean primitive, int array, boolean isnull) {
    Element result;

    if (parent == null)
      result = m_Document.getDocument().getDocumentElement();
    else
      result = (Element) parent.appendChild(m_Document.getDocument()
          .createElement(TAG_OBJECT));

    // attributes
    // mandatory attributes:
    result.setAttribute(ATT_NAME, name);
    result.setAttribute(ATT_CLASS, classname);

    // add following attributes only if necessary, i.e., different from default:
    if (!booleanToString(primitive).equals(ATT_PRIMITIVE_DEFAULT))
      result.setAttribute(ATT_PRIMITIVE, booleanToString(primitive));

    // multi-dimensional array?
    if (array > 1) {
      result.setAttribute(ATT_ARRAY, Integer.toString(array));
    }
    // backwards compatible: 0 -> no array ("no"), 1 -> 1-dim. array ("yes")
    else {
      if (!booleanToString(array == 1).equals(ATT_ARRAY_DEFAULT))
        result.setAttribute(ATT_ARRAY, booleanToString(array == 1));
    }

    if (!booleanToString(isnull).equals(ATT_NULL_DEFAULT))
      result.setAttribute(ATT_NULL, booleanToString(isnull));

    return result;
  }

  /**
   * if the class of the given object (or one of its ancestors) is stored in the
   * classname override hashtable, then the override name is returned otherwise
   * the classname of the given object.
   * 
   * @param o the object to check for overriding its classname
   * @return if overridden then the classname stored in the hashtable, otherwise
   *         the classname of the given object
   * @see #m_ClassnameOverride
   */
  protected String overrideClassname(Object o) {
    Enumeration enm;
    String result;
    Class currentCls;

    result = o.getClass().getName();

    // check overrides
    enm = m_ClassnameOverride.keys();
    while (enm.hasMoreElements()) {
      currentCls = (Class) enm.nextElement();
      if (currentCls.isInstance(o)) {
        result = m_ClassnameOverride.get(currentCls);
        break;
      }
    }

    return result;
  }

  /**
   * if the given classname is stored in the classname override hashtable, then
   * the override name is returned otherwise the given classname. <b>Note:</b>
   * in contrast to <code>overrideClassname(Object)</code> does this method only
   * look for exact name matches. The other method checks whether the class of
   * the given object is a subclass of any of the stored overrides.
   * 
   * @param classname the classname to check for overriding
   * @return if overridden then the classname stored in the hashtable, otherwise
   *         the given classname
   * @see #m_ClassnameOverride
   * @see #overrideClassname(Object)
   */
  protected String overrideClassname(String classname) {
    Enumeration enm;
    String result;
    Class currentCls;

    result = classname;

    // check overrides
    enm = m_ClassnameOverride.keys();
    while (enm.hasMoreElements()) {
      currentCls = (Class) enm.nextElement();
      if (currentCls.getName().equals(classname)) {
        result = m_ClassnameOverride.get(currentCls);
        break;
      }
    }

    return result;
  }

  /**
   * returns a property descriptor if possible, otherwise <code>null</code>
   * 
   * @param className the name of the class to get the descriptor for
   * @param displayName the name of the property
   * @return the descriptor if available, otherwise <code>null</code>
   */
  protected PropertyDescriptor determineDescriptor(String className,
      String displayName) {
    PropertyDescriptor result;

    result = null;

    try {
      result = new PropertyDescriptor(displayName, Class.forName(className));
    } catch (Exception e) {
      result = null;
    }

    return result;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeBooleanToXML(Element parent, boolean o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Boolean.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Boolean(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeByteToXML(Element parent, byte o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Byte.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Byte(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeCharToXML(Element parent, char o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Character.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Character(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeDoubleToXML(Element parent, double o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Double.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Double(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeFloatToXML(Element parent, float o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Float.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Float(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeIntToXML(Element parent, int o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Integer.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Integer(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeLongToXML(Element parent, long o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Long.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Long(o).toString()));

    return node;
  }

  /**
   * adds the given primitive to the DOM structure.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the primitive to describe in XML
   * @param name the name of the primitive
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   */
  protected Element writeShortToXML(Element parent, short o, String name)
      throws Exception {
    Element node;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    m_CurrentNode = parent;

    node = addElement(parent, name, Short.TYPE.getName(), true);
    node.appendChild(node.getOwnerDocument().createTextNode(
        new Short(o).toString()));

    return node;
  }

  /**
   * checks whether the innermost class is a primitive class (handles
   * multi-dimensional arrays)
   * 
   * @param c the array class to inspect
   * @return whether the array consists of primitive elements
   */
  protected boolean isPrimitiveArray(Class c) {
    if (c.getComponentType().isArray())
      return isPrimitiveArray(c.getComponentType());
    else
      return c.getComponentType().isPrimitive();
  }

  /**
   * adds the given Object to a DOM structure. (only public due to reflection).<br>
   * <b>Note:</b> <code>overrideClassname(Object)</code> is not invoked in case
   * of arrays, since the array class could be a superclass, whereas the
   * elements of the array can be specialized subclasses. In case of an array
   * the method <code>overrideClassname(String)</code> is invoked, which
   * searches for an exact match of the classname in the override hashtable.
   * 
   * @param parent the parent of this object, e.g. the class this object is a
   *          member of
   * @param o the Object to describe in XML
   * @param name the name of the object
   * @return the node that was created
   * @throws Exception if the DOM creation fails
   * @see #overrideClassname(Object)
   * @see #overrideClassname(String)
   * @see #m_ClassnameOverride
   */
  public Element writeToXML(Element parent, Object o, String name)
      throws Exception {
    String classname;
    Element node;
    Hashtable memberlist;
    Enumeration enm;
    Object member;
    String memberName;
    Method method;
    PropertyDescriptor desc;
    boolean primitive;
    int array;
    int i;
    Object obj;
    String tmpStr;

    node = null;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), name);

    // special handling of null-objects
    if (o == null) {
      node = addElement(parent, name, "" + null, false, 0, true);
      return node;
    }

    // used for overriding the classname
    obj = null;

    // get information about object
    array = 0;
    if (o.getClass().isArray())
      array = Utils.getArrayDimensions(o);
    if (array > 0) {
      classname = Utils.getArrayClass(o.getClass()).getName();
      primitive = isPrimitiveArray(o.getClass());
    } else {
      // try to get property descriptor to determine real class
      // (for primitives the getClass() method returns the corresponding
      // Object-Class!)
      desc = null;
      if (parent != null)
        desc = determineDescriptor(parent.getAttribute(ATT_CLASS), name);

      if (desc != null)
        primitive = desc.getPropertyType().isPrimitive();
      else
        primitive = o.getClass().isPrimitive();

      // for primitives: retrieve primitive type, otherwise the object's real
      // class. For non-primitives we can't use the descriptor, since that
      // might only return an interface as class!
      if (primitive) {
        classname = desc.getPropertyType().getName();
      } else {
        obj = o;
        classname = o.getClass().getName();
      }
    }

    // fix class/primitive if parent is array of primitives, thanks to
    // reflection the elements of the array are objects and not primitives!
    if ((parent != null) && (!parent.getAttribute(ATT_ARRAY).equals(""))
        && (!parent.getAttribute(ATT_ARRAY).equals(VAL_NO))
        && (stringToBoolean(parent.getAttribute(ATT_PRIMITIVE)))) {
      primitive = true;
      classname = parent.getAttribute(ATT_CLASS);
      obj = null;
    }

    // perhaps we need to override the classname
    if (obj != null)
      classname = overrideClassname(obj); // for non-arrays
    else
      classname = overrideClassname(classname); // for arrays

    // create node for current object
    node = addElement(parent, name, classname, primitive, array);

    // array? -> save as child with 'name="<index>"'
    if (array > 0) {
      for (i = 0; i < Array.getLength(o); i++) {
        invokeWriteToXML(node, Array.get(o, i), Integer.toString(i));
      }
    }
    // non-array
    else {
      // primitive? -> only toString()
      if (primitive) {
        node.appendChild(node.getOwnerDocument().createTextNode(o.toString()));
      }
      // object
      else {
        // process recursively members of this object
        memberlist = getDescriptors(o);
        // if no get/set methods -> we assume it has String-Constructor
        if (memberlist.size() == 0) {
          if (!o.toString().equals("")) {
            tmpStr = o.toString();
            // these five entities are recognized by every XML processor
            // see http://www.xml.com/pub/a/2001/03/14/trxml10.html
            tmpStr = tmpStr.replaceAll("&", "&amp;").replaceAll("\"", "&quot;")
                .replaceAll("'", "&apos;").replaceAll("<", "&lt;")
                .replaceAll(">", "&gt;");
            // in addition, replace some other entities as well
            tmpStr = tmpStr.replaceAll("\n", "&#10;").replaceAll("\r", "&#13;")
                .replaceAll("\t", "&#9;");

            if (o instanceof java.io.File) {
              // hack to force separators to be always saved as /
              tmpStr = tmpStr.replace('\\', '/');
            }

            node.appendChild(node.getOwnerDocument().createTextNode(tmpStr));
          }
        } else {
          enm = memberlist.keys();
          while (enm.hasMoreElements()) {
            memberName = enm.nextElement().toString();

            // in ignore list?
            if ((m_Properties.isIgnored(memberName))
                || (m_Properties.isIgnored(getPath(node) + "." + memberName))
                || (m_Properties.isIgnored(o, getPath(node) + "." + memberName)))
              continue;

            // is it allowed?
            if (!m_Properties.isAllowed(o, memberName))
              continue;

            desc = (PropertyDescriptor) memberlist.get(memberName);
            method = desc.getReadMethod();
            member = method.invoke(o, (Object[]) null);
            invokeWriteToXML(node, member, memberName);
          }
        }
      }
    }

    return node;
  }

  /**
   * either invokes a custom method to write a specific property/class or the
   * standard method <code>writeToXML(Element,Object,String)</code>
   * 
   * @param parent the parent XML node
   * @param o the object's content will be added as children to the given parent
   *          node
   * @param name the name of the object
   * @return the node that was created
   * @throws Exception if invocation or turning into XML fails
   */
  protected Element invokeWriteToXML(Element parent, Object o, String name)
      throws Exception {
    Method method;
    Class[] methodClasses;
    Object[] methodArgs;
    boolean array;
    Element node;
    boolean useDefault;

    node = null;
    method = null;
    useDefault = false;

    m_CurrentNode = parent;

    // default, if null
    if (o == null)
      useDefault = true;

    try {
      if (!useDefault) {
        array = o.getClass().isArray();

        // display name?
        if (m_CustomMethods.write().contains(name))
          method = m_CustomMethods.write().get(o.getClass());
        else
        // class?
        if ((!array) && (m_CustomMethods.write().contains(o.getClass())))
          method = m_CustomMethods.write().get(o.getClass());
        else
          method = null;

        useDefault = (method == null);
      }

      // custom
      if (!useDefault) {
        methodClasses = new Class[3];
        methodClasses[0] = Element.class;
        methodClasses[1] = Object.class;
        methodClasses[2] = String.class;
        methodArgs = new Object[3];
        methodArgs[0] = parent;
        methodArgs[1] = o;
        methodArgs[2] = name;
        node = (Element) method.invoke(this, methodArgs);
      }
      // standard
      else {
        node = writeToXML(parent, o, name);
      }
    } catch (Exception e) {
      if (DEBUG)
        e.printStackTrace();

      if (m_CurrentNode != null) {
        System.out.println("Happened near: " + getPath(m_CurrentNode));
        // print it only once!
        m_CurrentNode = null;
      }
      System.out.println("PROBLEM (write): " + name);

      throw (Exception) e.fillInStackTrace();
    }

    return node;
  }

  /**
   * enables derived classes to due some pre-processing on the objects, that's
   * about to be serialized. Right now it only returns the object.
   * 
   * @param o the object that is serialized into XML
   * @return the possibly altered object
   * @throws Exception if post-processing fails
   */
  protected Object writePreProcess(Object o) throws Exception {
    return o;
  }

  /**
   * enables derived classes to add other properties to the DOM tree, e.g. ones
   * that do not apply to the get/set convention of beans. only implemented with
   * empty method body.
   * 
   * @param o the object that is serialized into XML
   * @throws Exception if post-processing fails
   */
  protected void writePostProcess(Object o) throws Exception {
  }

  /**
   * extracts all accesible properties from the given object
   * 
   * @param o the object to turn into an XML representation
   * @return the generated DOM document
   * @throws Exception if XML generation fails
   */
  public XMLDocument toXML(Object o) throws Exception {
    clear();
    invokeWriteToXML(null, writePreProcess(o), VAL_ROOT);
    writePostProcess(o);
    return m_Document;
  }

  /**
   * returns a descriptor for a given objet by providing the name
   * 
   * @param o the object the get the descriptor for
   * @param name the display name of the descriptor
   * @return the Descriptor, if found, otherwise <code>null</code>
   * @throws Exception if introsepction fails
   */
  protected PropertyDescriptor getDescriptorByName(Object o, String name)
      throws Exception {
    PropertyDescriptor result;
    PropertyDescriptor[] desc;
    int i;

    result = null;

    desc = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();
    for (i = 0; i < desc.length; i++) {
      if (desc[i].getDisplayName().equals(name)) {
        result = desc[i];
        break;
      }
    }

    return result;
  }

  /**
   * returns the associated class for the given name
   * 
   * @param name the name of the class to return a Class object for
   * @return the class if it could be retrieved
   * @throws Exception if it class retrieval fails
   */
  protected Class determineClass(String name) throws Exception {
    Class result;

    if (name.equals(Boolean.TYPE.getName()))
      result = Boolean.TYPE;
    else if (name.equals(Byte.TYPE.getName()))
      result = Byte.TYPE;
    else if (name.equals(Character.TYPE.getName()))
      result = Character.TYPE;
    else if (name.equals(Double.TYPE.getName()))
      result = Double.TYPE;
    else if (name.equals(Float.TYPE.getName()))
      result = Float.TYPE;
    else if (name.equals(Integer.TYPE.getName()))
      result = Integer.TYPE;
    else if (name.equals(Long.TYPE.getName()))
      result = Long.TYPE;
    else if (name.equals(Short.TYPE.getName()))
      result = Short.TYPE;
    else
      result = Class.forName(name);

    return result;
  }

  /**
   * returns an Object representing the primitive described by the given node.
   * Here we use a trick to return an object even though its a primitive: by
   * creating a primitive array with reflection of length 1, setting the
   * primtive value as real object and then returning the "object" at position 1
   * of the array.
   * 
   * @param node the node to return the value as "primitive" object
   * @return the primitive as "pseudo" object
   * @throws Exception if the instantiation of the array fails or any of the
   *           String conversions fails
   */
  protected Object getPrimitive(Element node) throws Exception {
    Object result;
    Object tmpResult;
    Class cls;

    cls = determineClass(node.getAttribute(ATT_CLASS));
    tmpResult = Array.newInstance(cls, 1);

    if (cls == Boolean.TYPE)
      Array.set(tmpResult, 0, new Boolean(XMLDocument.getContent(node)));
    else if (cls == Byte.TYPE)
      Array.set(tmpResult, 0, new Byte(XMLDocument.getContent(node)));
    else if (cls == Character.TYPE)
      Array.set(tmpResult, 0, new Character(XMLDocument.getContent(node)
          .charAt(0)));
    else if (cls == Double.TYPE)
      Array.set(tmpResult, 0, new Double(XMLDocument.getContent(node)));
    else if (cls == Float.TYPE)
      Array.set(tmpResult, 0, new Float(XMLDocument.getContent(node)));
    else if (cls == Integer.TYPE)
      Array.set(tmpResult, 0, new Integer(XMLDocument.getContent(node)));
    else if (cls == Long.TYPE)
      Array.set(tmpResult, 0, new Long(XMLDocument.getContent(node)));
    else if (cls == Short.TYPE)
      Array.set(tmpResult, 0, new Short(XMLDocument.getContent(node)));
    else
      throw new Exception("Cannot get primitive for class '" + cls.getName()
          + "'!");

    result = Array.get(tmpResult, 0);

    return result;
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public boolean readBooleanFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Boolean) getPrimitive(node)).booleanValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public byte readByteFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Byte) getPrimitive(node)).byteValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public char readCharFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Character) getPrimitive(node)).charValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public double readDoubleFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Double) getPrimitive(node)).doubleValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public float readFloatFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Float) getPrimitive(node)).floatValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public int readIntFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Integer) getPrimitive(node)).intValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public long readLongFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Long) getPrimitive(node)).longValue();
  }

  /**
   * builds the primitive from the given DOM node.
   * 
   * @param node the associated XML node
   * @return the primitive created from the XML description
   * @throws Exception if instantiation fails
   */
  public short readShortFromXML(Element node) throws Exception {
    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    return ((Short) getPrimitive(node)).shortValue();
  }

  /**
   * adds the specific node to the object via a set method
   * 
   * @param o the object to set a property
   * @param name the name of the object for which to set a property (only for
   *          information reasons)
   * @param child the value of the property to add
   * @return the provided object, but augmented by the child
   * @throws Exception if something goes wrong
   */
  public Object readFromXML(Object o, String name, Element child)
      throws Exception {
    Object result;
    Hashtable descriptors;
    PropertyDescriptor descriptor;
    String methodName;
    Method method;
    Object[] methodArgs;
    Object tmpResult;
    Class paramClass;

    result = o;
    descriptors = getDescriptors(result);
    methodName = child.getAttribute(ATT_NAME);

    // in ignore list?
    if (m_Properties.isIgnored(getPath(child)))
      return result;

    // in ignore list of class?
    if (m_Properties.isIgnored(result, getPath(child)))
      return result;

    // is it allowed?
    if (!m_Properties.isAllowed(result, methodName))
      return result;

    descriptor = (PropertyDescriptor) descriptors.get(methodName);

    // unknown property?
    if (descriptor == null) {
      if (!m_CustomMethods.read().contains(methodName)
          && !SUPPRESS_PROPERTY_WARNINGS.contains(name + "." + methodName))
        System.out.println("WARNING: unknown property '" + name + "."
            + methodName + "'!");
      return result;
    }

    method = descriptor.getWriteMethod();
    methodArgs = new Object[1];
    tmpResult = invokeReadFromXML(child);
    paramClass = method.getParameterTypes()[0];

    // array?
    if (paramClass.isArray()) {
      // no data?
      if (Array.getLength(tmpResult) == 0)
        return result;
      methodArgs[0] = tmpResult;
    }
    // non-array
    else {
      methodArgs[0] = tmpResult;
    }

    method.invoke(result, methodArgs);

    return result;
  }

  /**
   * returns an array with the dimensions of the array stored in XML
   * 
   * @param node the node to determine the dimensions for
   * @return the dimensions of the array
   */
  protected int[] getArrayDimensions(Element node) {
    Vector<Element> children;
    Vector<Integer> tmpVector;
    int[] tmp;
    int[] result;
    int i;

    // have we reached the innermost dimension?
    if (stringToBoolean(node.getAttribute(ATT_ARRAY)))
      children = XMLDocument.getChildTags(node);
    else
      children = null;

    if (children != null) {
      tmpVector = new Vector<Integer>();

      if (children.size() > 0) {
        // are children also arrays?
        tmp = getArrayDimensions(children.get(0));

        // further dimensions
        if (tmp != null) {
          for (i = tmp.length - 1; i >= 0; i--)
            tmpVector.add(new Integer(tmp[i]));
        }

        // add current dimension
        tmpVector.add(0, new Integer(children.size()));
      } else {
        tmpVector.add(new Integer(0));
      }

      // generate result
      result = new int[tmpVector.size()];
      for (i = 0; i < result.length; i++)
        result[i] = tmpVector.get(tmpVector.size() - i - 1).intValue();
    } else {
      result = null;
    }

    return result;
  }

  /**
   * builds the object from the given DOM node. (only public due to reflection)
   * 
   * @param node the associated XML node
   * @return the instance created from the XML description
   * @throws Exception if instantiation fails
   */
  public Object readFromXML(Element node) throws Exception {
    String classname;
    String name;
    boolean primitive;
    boolean array;
    boolean isnull;
    Class<?> cls;
    Vector<Element> children;
    Object result;
    int i;
    Constructor constructor;
    Class[] methodClasses;
    Object[] methodArgs;
    Element child;

    // for debugging only
    if (DEBUG)
      trace(new Throwable(), node.getAttribute(ATT_NAME));

    m_CurrentNode = node;

    result = null;

    name = node.getAttribute(ATT_NAME);
    classname = node.getAttribute(ATT_CLASS);
    primitive = stringToBoolean(node.getAttribute(ATT_PRIMITIVE));
    array = stringToBoolean(node.getAttribute(ATT_ARRAY));
    isnull = stringToBoolean(node.getAttribute(ATT_NULL));

    // special handling of null
    if (isnull)
      return result;

    children = XMLDocument.getChildTags(node);
    cls = determineClass(classname);

    // array
    if (array) {
      result = Array.newInstance(cls, getArrayDimensions(node));
      for (i = 0; i < children.size(); i++) {
        child = children.get(i);
        Array.set(result, Integer.parseInt(child.getAttribute(ATT_NAME)),
            invokeReadFromXML(child));
      }
    }
    // non-array
    else {
      // primitive/String-constructor
      if (children.size() == 0) {
        // primitive
        if (primitive) {
          result = getPrimitive(node);
        }
        // assumed String-constructor
        else {
          methodClasses = new Class[1];
          methodClasses[0] = String.class;
          methodArgs = new Object[1];
          methodArgs[0] = XMLDocument.getContent(node);
          try {
            constructor = cls.getConstructor(methodClasses);
            result = constructor.newInstance(methodArgs);
          } catch (Exception e) {
            // if it's not a class with String constructor, let's try standard
            // constructor
            try {
              result = cls.newInstance();
            } catch (Exception e2) {
              // sorry, can't instantiate!
              result = null;
              System.out.println("ERROR: Can't instantiate '" + classname
                  + "'!");
            }
          }
        }
      }
      // normal get/set methods
      else {
        result = cls.newInstance();
        for (i = 0; i < children.size(); i++)
          result = readFromXML(result, name, children.get(i));
      }
    }

    return result;
  }

  /**
   * either invokes a custom method to read a specific property/class or the
   * standard method <code>readFromXML(Element)</code>
   * 
   * @param node the associated XML node
   * @return the instance created from the XML description
   * @throws Exception if instantiation fails
   */
  protected Object invokeReadFromXML(Element node) throws Exception {
    Method method;
    Class[] methodClasses;
    Object[] methodArgs;
    boolean array;
    boolean useDefault;

    useDefault = false;
    method = null;
    m_CurrentNode = node;

    try {
      // special handling of null values
      if (stringToBoolean(node.getAttribute(ATT_NULL)))
        useDefault = true;

      if (!useDefault) {
        array = stringToBoolean(node.getAttribute(ATT_ARRAY));

        // display name?
        if (m_CustomMethods.read().contains(node.getAttribute(ATT_NAME)))
          method = m_CustomMethods.read().get(node.getAttribute(ATT_NAME));
        else
        // class name?
        if ((!array)
            && (m_CustomMethods.read().contains(determineClass(node
                .getAttribute(ATT_CLASS)))))
          method = m_CustomMethods.read().get(
              determineClass(node.getAttribute(ATT_CLASS)));
        else
          method = null;

        useDefault = (method == null);
      }

      // custom method
      if (!useDefault) {
        methodClasses = new Class[1];
        methodClasses[0] = Element.class;
        methodArgs = new Object[1];
        methodArgs[0] = node;
        return method.invoke(this, methodArgs);
      }
      // standard
      else {
        return readFromXML(node);
      }
    } catch (Exception e) {
      if (DEBUG)
        e.printStackTrace();

      if (m_CurrentNode != null) {
        System.out.println("Happened near: " + getPath(m_CurrentNode));
        // print it only once!
        m_CurrentNode = null;
      }
      System.out.println("PROBLEM (read): " + node.getAttribute("name"));

      throw (Exception) e.fillInStackTrace();
    }
  }

  /**
   * additional pre-processing can happen in derived classes before the actual
   * reading from XML (working on the raw XML). right now it does nothing with
   * the document.
   * 
   * @param document the document to pre-process
   * @return the processed object
   * @throws Exception if post-processing fails
   */
  protected Document readPreProcess(Document document) throws Exception {
    return document;
  }

  /**
   * additional post-processing can happen in derived classes after reading from
   * XML. right now it only returns the object as it is.
   * 
   * @param o the object to perform some additional processing on
   * @return the processed object
   * @throws Exception if post-processing fails
   */
  protected Object readPostProcess(Object o) throws Exception {
    return o;
  }

  /**
   * returns the given DOM document as an instance of the specified class
   * 
   * @param document the parsed DOM document representing the object
   * @return the XML as object
   * @throws Exception if object instantiation fails
   */
  public Object fromXML(Document document) throws Exception {
    if (!document.getDocumentElement().getNodeName().equals(ROOT_NODE))
      throw new Exception("Expected '" + ROOT_NODE
          + "' as root element, but found '"
          + document.getDocumentElement().getNodeName() + "'!");
    m_Document.setDocument(readPreProcess(document));
    checkVersion();
    return readPostProcess(invokeReadFromXML(m_Document.getDocument()
        .getDocumentElement()));
  }

  /**
   * parses the given XML string (can be XML or a filename) and returns an
   * Object generated from the representation
   * 
   * @param xml the xml to parse (if "<?xml" is not found then it is considered
   *          a file)
   * @return the generated instance
   * @throws Exception if something goes wrong with the parsing
   */
  public Object read(String xml) throws Exception {
    return fromXML(m_Document.read(xml));
  }

  /**
   * parses the given file and returns a DOM document
   * 
   * @param file the XML file to parse
   * @return the parsed DOM document
   * @throws Exception if something goes wrong with the parsing
   */
  public Object read(File file) throws Exception {
    return fromXML(m_Document.read(file));
  }

  /**
   * parses the given stream and returns a DOM document
   * 
   * @param stream the XML stream to parse
   * @return the parsed DOM document
   * @throws Exception if something goes wrong with the parsing
   */
  public Object read(InputStream stream) throws Exception {
    return fromXML(m_Document.read(stream));
  }

  /**
   * parses the given reader and returns a DOM document
   * 
   * @param reader the XML reader to parse
   * @return the parsed DOM document
   * @throws Exception if something goes wrong with the parsing
   */
  public Object read(Reader reader) throws Exception {
    return fromXML(m_Document.read(reader));
  }

  /**
   * writes the given object into the file
   * 
   * @param file the filename to write to
   * @param o the object to serialize as XML
   * @throws Exception if something goes wrong with the parsing
   */
  public void write(String file, Object o) throws Exception {
    toXML(o).write(file);
  }

  /**
   * writes the given object into the file
   * 
   * @param file the filename to write to
   * @param o the object to serialize as XML
   * @throws Exception if something goes wrong with the parsing
   */
  public void write(File file, Object o) throws Exception {
    toXML(o).write(file);
  }

  /**
   * writes the given object into the stream
   * 
   * @param stream the filename to write to
   * @param o the object to serialize as XML
   * @throws Exception if something goes wrong with the parsing
   */
  public void write(OutputStream stream, Object o) throws Exception {
    toXML(o).write(stream);
  }

  /**
   * writes the given object into the writer
   * 
   * @param writer the filename to write to
   * @param o the object to serialize as XML
   * @throws Exception if something goes wrong with the parsing
   */
  public void write(Writer writer, Object o) throws Exception {
    toXML(o).write(writer);
  }

  /**
   * for testing only. if the first argument is a filename with ".xml" as
   * extension it tries to generate an instance from the XML description and
   * does a <code>toString()</code> of the generated object.
   */
  public static void main(String[] args) throws Exception {
    if (args.length > 0) {
      // read xml and print
      if (args[0].toLowerCase().endsWith(".xml")) {
        System.out.println(new XMLSerialization().read(args[0]).toString());
      }
      // read binary and print generated XML
      else {
        // read
        FileInputStream fi = new FileInputStream(args[0]);
        ObjectInputStream oi = new ObjectInputStream(
            new BufferedInputStream(fi));
        Object o = oi.readObject();
        oi.close();
        // print to stdout
        // new XMLSerialization().write(System.out, o);
        new XMLSerialization().write(new BufferedOutputStream(
            new FileOutputStream(args[0] + ".xml")), o);
        // print to binary file
        FileOutputStream fo = new FileOutputStream(args[0] + ".exp");
        ObjectOutputStream oo = new ObjectOutputStream(
            new BufferedOutputStream(fo));
        oo.writeObject(o);
        oo.close();
      }
    }
  }

  /**
   * Returns the revision string.
   * 
   * @return the revision
   */
  @Override
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 9413 $");
  }
}
