/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.controls;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.JOptionPane;
import org.opensourcephysics.tools.Resource;
import org.opensourcephysics.tools.ResourceLoader;

/**
 * This is a basic xml control for storing data.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class XMLControlElement implements XMLControl {
  // static constants
  public static final int ALWAYS_DECRYPT = 0;
  public static final int PASSWORD_DECRYPT = 3;
  public static final int NEVER_DECRYPT = 5;
  // static fields
  public static int compactArraySize = 0;
  protected static String encoding = "UTF-8";                             //$NON-NLS-1$
  
  // instance fields
  protected String className = "java.lang.Object";                        //$NON-NLS-1$ //changed by W. Christian
  protected Class<?> theClass = null;
  protected String name;
  protected Map<String, Integer> counts = new HashMap<String, Integer>(); // maps numbered names to counts
  protected Object object;
  protected XMLProperty parent;
  protected int level;
  protected ArrayList<String> propNames = new ArrayList<String>();
  protected ArrayList<XMLProperty> props = new ArrayList<XMLProperty>();
  protected BufferedReader input;
  protected BufferedWriter output;
  public boolean canWrite;
  protected boolean valid = false;
  protected boolean readFailed = false;
  protected String version;
  protected String doctype = "osp10.dtd";                                 //$NON-NLS-1$
  private String basepath;
  private String password;
  private int decryptPolicy = ALWAYS_DECRYPT;

  /**
   * Constructs an empty control for the Object class.
   */
  public XMLControlElement() {

  /** empty block */
  }

  /**
   * Constructs an empty control for the specified class.
   *
   * @param type the class.
   */
  public XMLControlElement(Class<?> type) {
    this();
    setObjectClass(type);
  }

  /**
   * Constructs and loads a control with the specified object.
   *
   * @param obj the object.
   */
  public XMLControlElement(Object obj) {
    this();
    setObjectClass(obj.getClass());
    saveObject(obj);
  }

  /**
   * Constructs a control with the specified parent.
   *
   * @param parent the parent.
   */
  public XMLControlElement(XMLProperty parent) {
    this();
    this.parent = parent;
    level = parent.getLevel();
  }

  /**
   * Constructs a control and reads the specified input.
   * Input may be a file name or an xml string
   *
   * @param input the input string
   */
  public XMLControlElement(String input) {
    this();
    if(input.startsWith("<?xml")) { //$NON-NLS-1$
      readXML(input);
    } else {
      read(input);
    }
  }

  /**
   * Constructs a copy of the specified XMLControl.
   *
   * @param control the XMLControl to copy.
   */
  public XMLControlElement(XMLControl control) {
    this();
    readXML(control.toXML());
  }

  /**
   * Locks the control's interface. Values sent to the control will not
   * update the display until the control is unlocked. Not implemented.
   *
   * @param lock boolean
   */
  public void setLockValues(boolean lock) {

  /** empty block */
  }

  /**
   * Sets a property with the specified name and boolean value.
   *
   * @param name the name
   * @param value the boolean value
   */
  public void setValue(String name, boolean value) {
    if(name==null) {
      return;
    }
    setXMLProperty(name, "boolean", String.valueOf(value)); //$NON-NLS-1$
  }

  /**
   * Sets a property with the specified name and double value.
   *
   * @param name the name
   * @param value the double value
   */
  public void setValue(String name, double value) {
    if(name==null) {
      return;
    }
    setXMLProperty(name, "double", String.valueOf(value)); //$NON-NLS-1$
  }

  /**
   * Sets a property with the specified name and int value.
   *
   * @param name the name
   * @param value the int value
   */
  public void setValue(String name, int value) {
    if(name==null) {
      return;
    }
    setXMLProperty(name, "int", String.valueOf(value)); //$NON-NLS-1$
  }

  /**
   * Sets a property with the specified name and object value.
   *
   * @param name the name
   * @param obj the object
   */
  public void setValue(String name, Object obj) {
    if(name==null) {
      return;
    }
    // clear the property if obj is null
    if(obj==null) {
      Iterator<XMLProperty> it = props.iterator();
      while(it.hasNext()) {
        XMLProperty prop = it.next();
        if(name.equals(prop.getPropertyName())) {
          it.remove();
          propNames.remove(name);
          break;
        }
      }
      return;
    }
    String type = XML.getDataType(obj);
    if(type!=null) {
      if(type.equals("int")||type.equals("double")) { //$NON-NLS-1$ //$NON-NLS-2$
        obj = obj.toString();
      }
      setXMLProperty(name, type, obj);
    }
  }

  /**
   * Gets the boolean value of the specified named property.
   *
   * @param name the name
   * @return the boolean value, or false if none found
   */
  public boolean getBoolean(String name) {
    XMLProperty prop = getXMLProperty(name);
    if((prop!=null)&&prop.getPropertyType().equals("boolean")) {       //$NON-NLS-1$
      return "true".equals(prop.getPropertyContent().get(0));          //$NON-NLS-1$
    } else if((prop!=null)&&prop.getPropertyType().equals("string")) { //$NON-NLS-1$
      return "true".equals(prop.getPropertyContent().get(0));          //$NON-NLS-1$
    }
    return false;
  }

  /**
   * Gets the double value of the specified named property.
   *
   * @param name the name
   * @return the double value, or Double.NaN if none found
   */
  public double getDouble(String name) {
    XMLProperty prop = getXMLProperty(name);
    if((prop!=null)&&(prop.getPropertyType().equals("double")||prop.getPropertyType().equals("int"))) { //$NON-NLS-1$ //$NON-NLS-2$
      return Double.parseDouble((String) prop.getPropertyContent().get(0));
    }
    return Double.NaN;
  }

  /**
   * Gets the int value of the specified named property.
   *
   * @param name the name
   * @return the int value, or Integer.MIN_VALUE if none found
   */
  public int getInt(String name) {
    XMLProperty prop = getXMLProperty(name);
    if((prop!=null)&&prop.getPropertyType().equals("int")) {           //$NON-NLS-1$
      return Integer.parseInt((String) prop.getPropertyContent().get(0));
    } else if((prop!=null)&&prop.getPropertyType().equals("object")) { //$NON-NLS-1$
      XMLControl control = (XMLControl) prop.getPropertyContent().get(0);
      if(control.getObjectClass()==OSPCombo.class) {
        OSPCombo combo = (OSPCombo) control.loadObject(null);
        return combo.getSelectedIndex();
      }
    }
    return Integer.MIN_VALUE;
  }

  /**
   * Gets the string value of the specified named property.
   *
   * @param name the name
   * @return the string value, or null if none found
   */
  public String getString(String name) {
    XMLProperty prop = getXMLProperty(name);
    if((prop!=null)&&prop.getPropertyType().equals("string")) { //$NON-NLS-1$
      String content = (String) prop.getPropertyContent().get(0);
      if(content.indexOf(XML.CDATA_PRE)!=-1) {
        content = content.substring(content.indexOf(XML.CDATA_PRE)+XML.CDATA_PRE.length(), content.indexOf(XML.CDATA_POST));
      }
      return content;
    } else if(name.equals("basepath")&&(getRootControl()!=null)) {     //$NON-NLS-1$
      return getRootControl().basepath;
    } else if((prop!=null)&&prop.getPropertyType().equals("object")) { //$NON-NLS-1$
      XMLControl control = (XMLControl) prop.getPropertyContent().get(0);
      if(control.getObjectClass()==OSPCombo.class) {
        OSPCombo combo = (OSPCombo) control.loadObject(null);
        return combo.toString();
      }
    }
    return null;
  }

  /**
   * Gets the object value of the specified named property.
   *
   * @param name the name
   * @return the object, or null if not found
   */
  public Object getObject(String name) {
    XMLProperty prop = getXMLProperty(name);
    if(prop!=null) {
      String type = prop.getPropertyType();
      if(type.equals("object")) {            //$NON-NLS-1$
        return objectValue(prop);
      } else if(type.equals("array")) {      //$NON-NLS-1$
        return arrayValue(prop);
      } else if(type.equals("collection")) { //$NON-NLS-1$
        return collectionValue(prop);
      } else if(type.equals("int")) {        //$NON-NLS-1$
        return new Integer(intValue(prop));
      } else if(type.equals("double")) {     //$NON-NLS-1$
        return new Double(doubleValue(prop));
      } else if(type.equals("boolean")) {    //$NON-NLS-1$
        return new Boolean(booleanValue(prop));
      } else if(type.equals("string")) {     //$NON-NLS-1$
        return stringValue(prop);
      }
    }
    return null;
  }

  /**
   * Gets the set of property names.
   *
   * @return a set of names
   */
  public Collection<String> getPropertyNames() {
    synchronized(propNames) {
      return new ArrayList<String>(propNames);
    }
  }

  /**
   * Gets the type of the specified property. Returns null if the property
   * is not found.
   *
   * @param name the property name
   * @return the type
   */
  public String getPropertyType(String name) {
    XMLProperty prop = getXMLProperty(name);
    if(prop!=null) {
      return prop.getPropertyType();
    }
    return null;
  }

  /**
   * Sets the password. Files are encrypted when the password is non-null.
   *
   * @param pass the password or phrase
   */
  public void setPassword(String pass) {
    password = pass;
    if(getObjectClass()!=Cryptic.class) {
      setValue("xml_password", pass); //$NON-NLS-1$
    }
  }

  /**
   * Gets the password.
   *
   * @return the password
   */
  public String getPassword() {
    if(password==null) {
      password = getString("xml_password"); //$NON-NLS-1$
    }
    return password;
  }

  /**
   * Sets the verify password flag.
   *
   * @param true to require password verification for file decryption
   */
  public void setDecryptPolicy(int policy) {
    if(policy==NEVER_DECRYPT) {
      decryptPolicy = NEVER_DECRYPT;
    } else if(policy==PASSWORD_DECRYPT) {
      decryptPolicy = PASSWORD_DECRYPT;
    } else {
      decryptPolicy = ALWAYS_DECRYPT;
    }
  }

  /**
   * Reads data into this control from a named source.
   *
   * @param name the name
   * @return the path of the opened document or null if failed
   */
  public String read(String name) {
    OSPLog.finest("reading "+name); //$NON-NLS-1$
    Resource res = ResourceLoader.getResource(name);
    if(res!=null) {
      read(res.openReader());
      String path = XML.getDirectoryPath(name);
      if(!path.equals("")) { //$NON-NLS-1$
        ResourceLoader.addSearchPath(path);
        basepath = path;
      } else {
        basepath = XML.getDirectoryPath(res.getAbsolutePath());
      }
      File file = res.getFile();
      canWrite = ((file!=null)&&file.canWrite());
      return res.getAbsolutePath();
    }
    readFailed = true;
    return null;
  }

  /**
   * Reads the control from an xml string.
   *
   * @param xml the xml string
   */
  public void readXML(String xml) {
    input = new BufferedReader(new StringReader(xml));
    readInput();
    if(!failedToRead()) {
      canWrite = false;
    }
  }

  /**
   * Reads the control from a Reader.
   *
   * @param in the Reader
   */
  public void read(Reader in) {
    if(in instanceof BufferedReader) {
      input = (BufferedReader) in;
    } else {
      input = new BufferedReader(in);
    }
    readInput();
    try {
      input.close();
    } catch(IOException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Reads data into this control from a named source if the source
   * specifies the same class as the current className.
   *
   * @param name the name
   * @param type the class
   * @return the path of the opened document or null if failed
   */
  public String readForClass(String name, Class<?> type) {
    Resource res = ResourceLoader.getResource(name);
    if(res==null) {
      return null;
    }
    input = new BufferedReader(res.openReader());
    if(!isInputForClass(type)) {
      return null;
    }
    return read(name);
  }

  /**
   * Reads this control from an xml string if the xml specifies the
   * same class as the current className.
   *
   * @param xml the xml string
   * @param type the class
   * @return true if successfully read
   */
  public boolean readXMLForClass(String xml, Class<?> type) {
    input = new BufferedReader(new StringReader(xml));
    if(!isInputForClass(type)) {
      return false;
    }
    readXML(xml);
    return !readFailed;
  }

  /**
   * Returns true if the most recent read operation failed.
   *
   * @return <code>true</code> if the most recent read operation failed
   */
  public boolean failedToRead() {
    return readFailed;
  }

  /**
   * Writes this control as an xml file with the specified name.
   *
   * @param fileName the file name
   * @return the path of the saved document or null if failed
   */
  public String write(String fileName) {
    canWrite = true;
    int n = fileName.lastIndexOf("/"); //$NON-NLS-1$
    if(n<0) {
      n = fileName.lastIndexOf("\\"); //$NON-NLS-1$
    }
    if(n>0) {
      String dir = fileName.substring(0, n+1);
      File file = new File(dir);
      if(!file.exists()&&!file.mkdir()) {
        canWrite = false;
        return null;
      }
    }
    try {
      File file = new File(fileName);
      if(file.exists()&&!file.canWrite()) {
    		JOptionPane.showMessageDialog(null, 
    				ControlsRes.getString("Dialog.ReadOnly.Message"),  //$NON-NLS-1$
    				ControlsRes.getString("Dialog.ReadOnly.Title"),  //$NON-NLS-1$
    				JOptionPane.PLAIN_MESSAGE);
        canWrite = false;
        return null;
      }
      FileOutputStream stream = new FileOutputStream(file);
      java.nio.charset.Charset charset = java.nio.charset.Charset.forName(encoding);
      write(new OutputStreamWriter(stream, charset));
      // add search path to ResourceLoader
      if(file.exists()) {
        String path = XML.getDirectoryPath(file.getCanonicalPath());
        ResourceLoader.addSearchPath(path);
      }
      // write dtd if valid
      if(isValid()) {
        // replace xml file name with dtd file name
        if(fileName.indexOf("/")!=-1) {                                                //$NON-NLS-1$
          fileName = fileName.substring(0, fileName.lastIndexOf("/")+1)+getDoctype();  //$NON-NLS-1$
        } else if(fileName.indexOf("\\")!=-1) {                                        //$NON-NLS-1$
          fileName = fileName.substring(0, fileName.lastIndexOf("\\")+1)+getDoctype(); //$NON-NLS-1$
        } else {
          fileName = doctype;
        }
        writeDocType(new FileWriter(fileName));
      }
      if(file.exists()) {
        return XML.getAbsolutePath(file);
      }
    } catch(IOException ex) {
      canWrite = false;
      OSPLog.warning(ex.getMessage());
    }
    return null;
  }

  /**
   * Writes this control to a Writer.
   *
   * @param out the Writer
   */
  public void write(Writer out) {
    try {
      output = new BufferedWriter(out);
      String xml = toXML();
      // if password-protected, encrypt the xml string and save the cryptic
      if(getPassword()!=null) {
        Cryptic cryptic = new Cryptic(xml);
        XMLControl control = new XMLControlElement(cryptic);
        xml = control.toXML();
      }
      output.write(xml);
      output.flush();
      output.close();
    } catch(IOException ex) {
      OSPLog.info(ex.getMessage());
    }
  }

  /**
   * Writes the DTD to a Writer.
   *
   * @param out the Writer
   */
  public void writeDocType(Writer out) {
    try {
      output = new BufferedWriter(out);
      output.write(XML.getDTD(getDoctype()));
      output.flush();
      output.close();
    } catch(IOException ex) {
      OSPLog.info(ex.getMessage());
    }
  }

  /**
   * Returns this control as an xml string.
   *
   * @return the xml string
   */
  public String toXML() {
    return toString();
  }

  /**
   * Sets the valid property.
   *
   * @param valid <code>true</code> to write the DTD and DocType
   */
  public void setValid(boolean valid) {
    this.valid = valid;
  }

  /**
   * Gets the valid property. When true, this writes the DTD and defines
   * the DocType when writing an xml document. Note: the presence or absense
   * of the DocType header and DTD has no effect on the read() methods--this
   * will always read a well-formed osp document and ignore a non-osp document.
   *
   * @return <code>true</code> if this is valid
   */
  public boolean isValid() {
    return valid&&(XML.getDTD(getDoctype())!=null);
  }

  /**
   * Sets the version.
   *
   * @param vers the version data
   */
  public void setVersion(String vers) {
    version = vers;
  }

  /**
   * Gets the version. May return null.
   *
   * @return the version
   */
  public String getVersion() {
    return version;
  }

  /**
   * Sets the doctype. Not yet implemented since only one doctype is defined.
   *
   * @param name the doctype resource name
   */
  public void setDoctype(String name) {
    if(XML.getDTD(name)!=null) {
      // check that name is accepted, etc
      // could make acceptable names be public String constants?
    }
  }

  /**
   * Gets the doctype. May return null.
   *
   * @return the doctype
   */
  public String getDoctype() {
    return doctype;
  }

  /**
   * Sets the class of the object for which this element stores data.
   *
   * @param type the <code>Class</code> of the object
   */
  public void setObjectClass(Class<?> type) {
    if((object!=null)&&!type.isInstance(object)) {
      throw new RuntimeException(object+" "+ControlsRes.getString("XMLControlElement.Exception.NotInstanceOf")+" "+type); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    }
    className = type.getName();
    theClass = type;
  }

  /**
   * Gets the class of the object for which this element stores data.
   *
   * @return the <code>Class</code> of the object
   */
  public Class<?> getObjectClass() {
    if(className==null) {
      return null;
    }
    if((theClass!=null)&&theClass.getName().equals(className)) {
      return theClass;
    }
    theClass = null;
    try {
      theClass = Class.forName(className);
    } catch(ClassNotFoundException ex) {

    /** empty block */
    }
    ClassLoader loader = XML.getClassLoader();
    if((loader!=null)&&(theClass==null)) {
      try {
        theClass = loader.loadClass(className);
      } catch(ClassNotFoundException ex) {

      /** empty block */
      }
    }
    return theClass;
  }

  /**
   * Gets the name of the object class for which this element stores data.
   *
   * @return the object class name
   */
  public String getObjectClassName() {
    return className;
  }

  /**
   * Saves an object's data in this element.
   *
   * @param obj the object to save.
   */
  public void saveObject(Object obj) {
    if(obj==null) {
      obj = object;
    }
    Class<?> type = getObjectClass();
    if((type==null)||type.equals(Object.class)) {
      if(obj==null) {
        return;
      }
      type = obj.getClass();
    }
    if(type.isInstance(obj)) {
      object = obj;
      className = obj.getClass().getName();
      clearValues();
      XML.ObjectLoader loader = XML.getLoader(type);
      loader.saveObject(this, obj);
    }
  }

  /**
   * Loads an object with data from this element. This asks the user for
   * approval and review before importing data from mismatched classes.
   *
   * @param obj the object to load
   * @return the loaded object
   */
  public Object loadObject(Object obj) {
    return loadObject(obj, false, false);
  }

  /**
   * Loads an object with data from this element. This asks the user to
   * review data from mismatched classes before importing it.
   *
   * @param obj the object to load
   * @param autoImport true to automatically import data from mismatched classes
   * @return the loaded object
   */
  public Object loadObject(Object obj, boolean autoImport) {
    return loadObject(obj, autoImport, false);
  }

  /**
   * Loads an object with data from this element.
   *
   * @param obj the object to load
   * @param autoImport true to automatically import data from mismatched classes
   * @param importAll true to import all importable data
   * @return the loaded object
   */
  public Object loadObject(Object obj, boolean autoImport, boolean importAll) {
    Class<?> type = getObjectClass();
    if(type==null) {
      if(obj!=null) {
        if(!autoImport) {
          int result = JOptionPane.showConfirmDialog(null, ControlsRes.getString("XMLControlElement.Dialog.UnknownClass.Message")+" \""+className+"\""+XML.NEW_LINE //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            +ControlsRes.getString("XMLControlElement.Dialog.MismatchedClass.Query")+" \""+obj.getClass().getName()+"\"", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
              ControlsRes.getString("XMLControlElement.Dialog.MismatchedClass.Title"), //$NON-NLS-1$
                JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
          if(result!=JOptionPane.YES_OPTION) {
            return obj;
          }
        }
        if(!importInto(obj, importAll)) {
          return obj;
        }
        type = obj.getClass();
      } else {
        return null;
      }
    }
    try {
      if(XML.getLoader(type).getClass()==XML.getLoader(obj.getClass()).getClass()) {
        autoImport = true;
        importAll = true;
      }
    } catch(Exception ex) {

    /** empty block */
    }
    if((obj!=null)&&!type.isInstance(obj)) {
      if(!autoImport) {
        int result = JOptionPane.showConfirmDialog(null, ControlsRes.getString("XMLControlElement.Dialog.MismatchedClass.Message")+" \""+type.getName()+"\""+XML.NEW_LINE //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
          +ControlsRes.getString("XMLControlElement.Dialog.MismatchedClass.Query")+" \""+obj.getClass().getName()+"\"", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            ControlsRes.getString("XMLControlElement.Dialog.MismatchedClass.Title"), //$NON-NLS-1$
              JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
        if(result!=JOptionPane.YES_OPTION) {
          return obj;
        }
      }
      if(!importInto(obj, importAll)) {
        return obj;
      }
      type = obj.getClass();
    }
    XML.ObjectLoader loader = XML.getLoader(type);
    if(obj==null) { // if obj is null, try to create a new one
      if(object==null) {
        object = loader.createObject(this);
      }
      obj = object;
    }
    if(obj==null) {
      return null; // unable to create new obj
    }
    if(type.isInstance(obj)) {
      obj = loader.loadObject(this, obj);
      object = obj;
    }
    return obj;
  }

  /**
   * Clears all properties.
   */
  public void clearValues() {
    props.clear();
    propNames.clear();
  }

  /**
   * Method required by the Control interface.
   *
   * @param s the string
   */
  public void println(String s) {
    System.out.println(s);
  }

  /**
   * Method required by the Control interface.
   */
  public void println() {
    System.out.println();
  }

  /**
   * Method required by the Control interface.
   *
   * @param s the string
   */
  public void print(String s) {
    System.out.print(s);
  }

  /**
   * Method required by the Control interface.
   */
  public void clearMessages() {

  /** empty block */
  }

  /**
   * Method required by the Control interface.
   *
   * @param s the string
   */
  public void calculationDone(String s) {

  /** empty block */
  }

  /**
   * Gets the property name.
   *
   * @return a name
   */
  public String getPropertyName() {
    XMLProperty parent = getParentProperty();
    // if no class name, return parent name
    if(className==null) {
      if(parent==null) {
        return "null";                                              //$NON-NLS-1$
      }
      return parent.getPropertyName();
    }
    // else if array or collection item, return numbered class name
    else if (this.isArrayOrCollectionItem()) {
      if (this.name==null) {
        // add numbering or name property
      	String myName = this.getString("name"); //$NON-NLS-1$
      	if (myName!=null && !"".equals(myName)) { //$NON-NLS-1$
          name = className.substring(className.lastIndexOf(".")+1); //$NON-NLS-1$
      		name += " \""+myName+"\""; //$NON-NLS-1$ //$NON-NLS-2$
      	}
      	else {
	        XMLProperty root = this;
	        while(root.getParentProperty()!=null) {
	          root = root.getParentProperty();
	        }
	        if(root instanceof XMLControlElement) {
	          XMLControlElement rootControl = (XMLControlElement) root;
	          name = className.substring(className.lastIndexOf(".")+1); //$NON-NLS-1$
	          name = rootControl.addNumbering(name);
	        }
      	}
      }
      return ""+name;                                          //$NON-NLS-1$
    }
    // else if this has a parent, return its name
    else if(parent!=null) {
      return parent.getPropertyName();
      // else return the short class name
    } else {
      return className.substring(className.lastIndexOf(".")+1);     //$NON-NLS-1$
    }
  }

  /**
   * Gets the property type.
   *
   * @return the type
   */
  public String getPropertyType() {
    return "object"; //$NON-NLS-1$
  }

  /**
   * Gets the property class.
   *
   * @return the class
   */
  public Class<?> getPropertyClass() {
    return getObjectClass();
  }

  /**
   * Gets the immediate parent property, if any.
   *
   * @return the parent
   */
  public XMLProperty getParentProperty() {
    return parent;
  }

  /**
   * Gets the level of this property relative to the root.
   *
   * @return a non-negative integer
   */
  public int getLevel() {
    return level;
  }

  /**
   * Gets the property content of this control.
   *
   * @return a list of XMLProperties
   */
  public List<Object> getPropertyContent() {
    return new ArrayList<Object>(props);
  }

  /**
   * Gets the named XMLControl child of this property. May return null.
   *
   * @param name the property name
   * @return the XMLControl
   */
  public XMLControl getChildControl(String name) {
    XMLControl[] children = getChildControls();
    for(int i = 0; i<children.length; i++) {
      if(children[i].getPropertyName().equals(name)) {
        return children[i];
      }
    }
    return null;
  }

  /**
   * Gets the XMLControl children of this property. The returned array has
   * length for type "object" = 1, "collection" and "array" = 0+, other
   * types = 0.
   *
   * @return an XMLControl array
   */
  public XMLControl[] getChildControls() {
    ArrayList<XMLControl> list = new ArrayList<XMLControl>();
    Iterator<XMLProperty> it = props.iterator();
    while(it.hasNext()) {
      XMLProperty prop = it.next();
      if(prop.getPropertyType().equals("object")) { //$NON-NLS-1$
        list.add((XMLControl) prop.getPropertyContent().get(0));
      }
    }
    return list.toArray(new XMLControl[0]);
  }

  /**
   * Gets the root control.
   *
   * @return the root control
   */
  public XMLControlElement getRootControl() {
    if(parent==null) {
      return this;
    }
    XMLProperty prop = parent;
    while(prop.getParentProperty()!=null) {
      prop = prop.getParentProperty();
    }
    if(prop instanceof XMLControlElement) {
      return(XMLControlElement) prop;
    }
    return null;
  }

  /**
   * Appends numbering to a specified name. Increments the number each time
   * this is called for the same name.
   *
   * @param name the name
   * @return the name with appended numbering
   */
  public String addNumbering(String name) {
    Integer count = counts.get(name);
    if(count==null) {
      count = new Integer(0);
    }
    count = new Integer(count.intValue()+1);
    counts.put(name, count);
    return name+" "+count.toString(); //$NON-NLS-1$
  }

  /**
   * This does nothing since the property type is "object".
   *
   * @param stringValue the string value of a primitive or string property
   */
  public void setValue(String stringValue) {

  /** empty block */
  }

  /**
   * Returns the string xml representation.
   *
   * @return the string xml representation
   */
  public String toString() {
    StringBuffer xml = new StringBuffer(""); //$NON-NLS-1$
    // write the header if this is the top level
    if(getLevel()==0) {
      xml.append("<?xml version=\"1.0\" encoding=\""+encoding+"\"?>");       //$NON-NLS-1$ //$NON-NLS-2$
      if(isValid()) {
        xml.append(XML.NEW_LINE+"<!DOCTYPE object SYSTEM \""+doctype+"\">"); //$NON-NLS-1$ //$NON-NLS-2$
      }
    }
    // write the opening tag
    xml.append(XML.NEW_LINE+indent(getLevel())+"<object class=\""+className+"\""); //$NON-NLS-1$ //$NON-NLS-2$
    // write the version if this is the top level
    if((version!=null)&&(getLevel()==0)) {
      xml.append(" version=\""+version+"\""); //$NON-NLS-1$ //$NON-NLS-2$
    }
    // write the property content and closing tag
    if(props.isEmpty()) {
      xml.append("/>");                                        //$NON-NLS-1$
    } else {
      xml.append(">");                                         //$NON-NLS-1$
      Iterator<XMLProperty> it = props.iterator();
      while(it.hasNext()) {
        xml.append(it.next().toString());
      }
      xml.append(XML.NEW_LINE+indent(getLevel())+"</object>"); //$NON-NLS-1$
    }
    return xml.toString();
  }

  // ____________________________ static methods _________________________________

  /**
   * Returns a list of objects of a specified class within this control.
   *
   * @param type the Class
   * @return the list of objects
   */
  public <T> List<T> getObjects(Class<T> type) {
    return getObjects(type, false);
  }

  /**
   * Returns a list of objects of a specified class within this control.
   *
   * @param type the Class
   * @param useChooser true to allow user to choose
   * @return the list of objects
   */
  public <T> List<T> getObjects(Class<T> type, boolean useChooser) {
    java.util.List<XMLProperty> props;
    if(useChooser) {
      String name = type.getName();
      name = name.substring(name.lastIndexOf(".")+1);                                                                                                                                                                           //$NON-NLS-1$
      // select objects using an xml tree chooser
      XMLTreeChooser chooser = new XMLTreeChooser(ControlsRes.getString("XMLControlElement.Chooser.SelectObjectsOfClass.Title"), ControlsRes.getString("XMLControlElement.Chooser.SelectObjectsOfClass.Label")+" "+name, null); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
      props = chooser.choose(this, type);
    } else {
      // select all objects of desired type using an xml tree
      XMLTree tree = new XMLTree(this);
      tree.setHighlightedClass(type);
      tree.selectHighlightedProperties();
      props = tree.getSelectedProperties();
    }
    List<T> objects = new ArrayList<T>();
    Iterator<XMLProperty> it = props.iterator();
    while(it.hasNext()) {
      XMLControl prop = (XMLControl) it.next();
      objects.add(type.cast(prop.loadObject(null)));
    }
    return objects;
  }

  /**
   * Returns a copy of this control.
   *
   * @return a clone
   */
  public Object clone() {
    return new XMLControlElement(this);
  }

  // ____________________________ private methods _________________________________

  /**
   * Determines if this is (the child of) an array or collection item.
   *
   * @return true if this is an array or collection item
   */
  private boolean isArrayOrCollectionItem() {
    XMLProperty parent = getParentProperty();
    if(parent!=null) {
      parent = parent.getParentProperty();
      return((parent!=null)&&("arraycollection".indexOf(parent.getPropertyType())>=0)); //$NON-NLS-1$
    }
    return false;
  }

  /**
   * Prepares this control for importing into the specified object.
   *
   * @param obj the importing object
   * @param importAll true to import all
   * @return <code>true</code> if the data is imported
   */
  private boolean importInto(Object obj, boolean importAll) {
    // get the list of importable properties
    XMLControl control = new XMLControlElement(obj);
    Collection<String> list = control.getPropertyNames();
    list.retainAll(this.getPropertyNames());
    // add property values
    Collection<String> names = new ArrayList<String>();
    Collection<Object> values = new ArrayList<Object>();
    for(Iterator<XMLProperty> it = props.iterator(); it.hasNext(); ) {
      XMLProperty prop = it.next();
      String propName = prop.getPropertyName();
      if(!list.contains(propName)) {
        continue;
      }
      names.add(propName);                          // keeps names in same order as values
      if(prop.getPropertyType().equals("object")) { //$NON-NLS-1$
        values.add(prop.getPropertyClass().getSimpleName());
      } else {
        values.add(prop.getPropertyContent().get(0));
      }
    }
    // choose the properties to import
    ListChooser chooser = new ListChooser(ControlsRes.getString("XMLControlElement.Chooser.ImportObjects.Title"), //$NON-NLS-1$
      ControlsRes.getString("XMLControlElement.Chooser.ImportObjects.Label")); //$NON-NLS-1$
    if(names.isEmpty()||importAll||chooser.choose(names, names, values)) {
      // names list now contains property names to keep
      Iterator<XMLProperty> it = props.iterator();
      while(it.hasNext()) {
        XMLProperty prop = it.next();
        if(!names.contains(prop.getPropertyName())) {
          it.remove();
          propNames.remove(prop.getPropertyName());
        }
      }
      // add object properties not in the names list to this control
      Iterator<String> it2 = control.getPropertyNames().iterator();
      while(it2.hasNext()) {
        String name = it2.next();
        if(names.contains(name)) {
          continue;
        }
        String propType = control.getPropertyType(name);
        if(propType.equals("int")) {            //$NON-NLS-1$
          setValue(name, control.getInt(name));
        } else if(propType.equals("double")) {  //$NON-NLS-1$
          setValue(name, control.getDouble(name));
        } else if(propType.equals("boolean")) { //$NON-NLS-1$
          setValue(name, control.getBoolean(name));
        } else if(propType.equals("string")) {  //$NON-NLS-1$
          setValue(name, control.getString(name));
        } else {
          setValue(name, control.getObject(name));
        }
      }
      return true;
    }
    return false;
  }

  /**
   * Sets a property.
   *
   * @param name the name
   * @param type the type
   * @param value the value
   */
  private void setXMLProperty(String name, String type, Object value) {
    // remove any previous property with the same name
    int i = -1;
    if(propNames.contains(name)) {
      Iterator<XMLProperty> it = props.iterator();
      while(it.hasNext()) {
        i++;
        XMLProperty prop = it.next();
        if(prop.getPropertyName().equals(name)) {
          it.remove();
          break;
        }
      }
    } else {
      propNames.add(name);
    }
    if(i>-1) {
      props.add(i, new XMLPropertyElement(this, name, type, value));
    } else {
      props.add(new XMLPropertyElement(this, name, type, value));
    }
  }

  /**
   * Gets a named property. May return null.
   *
   * @param name the name
   * @return the XMLProperty
   */
  private XMLProperty getXMLProperty(String name) {
    if(name==null) {
      return null;
    }
    Iterator<XMLProperty> it = props.iterator();
    while(it.hasNext()) {
      XMLProperty prop = it.next();
      if(name.equals(prop.getPropertyName())) {
        return prop;
      }
    }
    return null;
  }

  /**
   * Reads this control from the current input.
   */
  private void readInput() {
    readFailed = false;
    try {
      // get document root opening tag line
      String openingTag = input.readLine();
      while((openingTag!=null)&&(openingTag.indexOf("<object")==-1)) { //$NON-NLS-1$
        openingTag = input.readLine();
      }
      // read this element from the root
      if(openingTag!=null) {
        // get version, if any
        String xml = openingTag;
        int i = xml.indexOf("version=");                               //$NON-NLS-1$
        if(i!=-1) {
          xml = xml.substring(i+9);
          version = xml.substring(0, xml.indexOf("\""));               //$NON-NLS-1$
        }
        readObject(this, openingTag);
      } else {
        readFailed = true;
        return;
      }
    } catch(Exception ex) {
      readFailed = true;
      OSPLog.warning("Failed to read xml: "+ex.getMessage());          //$NON-NLS-1$
      return;
    }
    // if object class is Cryptic, decrypt and inspect
    if(Cryptic.class.equals(getObjectClass())) {
      Cryptic cryptic = (Cryptic) loadObject(null);
      // get the decrypted xml
      String xml = cryptic.decrypt();
      // return if decrypted xml is not readable by a test control
      XMLControl test = new XMLControlElement(xml);
      if(test.failedToRead()) {
        return;
      }
      // keep current password for possible verification needs
      String pass = password;
      // get the password from the test control
      password = test.getString("xml_password");       //$NON-NLS-1$
      // return if decrypt policy is NEVER or unverified PASSWORD
      switch(decryptPolicy) {
         case NEVER_DECRYPT :
           return;
         case PASSWORD_DECRYPT :
           if((password!=null)&&!password.equals("")&& //$NON-NLS-1$
                        !password.equals(pass)) {
             if(!Password.verify(password, null)) {
               return;
             }
           }
      }
      // otherwise read the decrypted xml into this control
      clearValues();
      object = null;
      className = Object.class.getName();
      theClass = null;
      readXML(xml);
    }
  }

  /**
   * Checks to see if the input is for the specified class.
   */
  private boolean isInputForClass(Class<?> type) {
    try {
      // get document root tag
      String xml = input.readLine();
      while((xml!=null)&&(xml.indexOf("<object")==-1)) {        //$NON-NLS-1$
        xml = input.readLine();
      }
      // check class name
      if(xml!=null) {
        xml = xml.substring(xml.indexOf("class=")+7);           //$NON-NLS-1$
        String className = xml.substring(0, xml.indexOf("\"")); //$NON-NLS-1$
        if(className.equals(type.getName())) {
          return true;
        }
      }
    } catch(Exception ex) {
      ex.printStackTrace();
    }
    return false;
  }

  /**
   * Reads the current input into an XMLcontrolElement.
   *
   * @param control the control to load
   * @param xml the xml opening tag line
   * @return the loaded element
   * @throws IOException
   */
  private XMLControlElement readObject(XMLControlElement control, String xml) throws IOException {
    control.clearValues();
    // set class name
    xml = xml.substring(xml.indexOf("class=")+7); //$NON-NLS-1$
    String className = xml.substring(0, xml.indexOf("\"")); //$NON-NLS-1$
    // workaround for media package name change
    int i = className.lastIndexOf(".");                     //$NON-NLS-1$
    if(i>-1) {
      String packageName = className.substring(0, i);
      if(packageName.endsWith("org.opensourcephysics.media")) { //$NON-NLS-1$
        className = packageName+".core"+className.substring(i); //$NON-NLS-1$
      }
    }
    control.className = className;
    // look for closing object tag on same line
    if(xml.indexOf("/>")!=-1) { //$NON-NLS-1$
      input.readLine();
      return control;
    }
    // read and process input lines
    XMLProperty prop = control;
    xml = input.readLine();
    while(xml!=null) {
      // closing object tag
      if(xml.indexOf("</object>")!=-1) {      //$NON-NLS-1$
        input.readLine();
        return control;
      }
      // opening property tag
      else if(xml.indexOf("<property")!=-1) { //$NON-NLS-1$
        XMLProperty child = readProperty(new XMLPropertyElement(prop), xml);
        control.props.add(child);
        control.propNames.add(child.getPropertyName());
      }
      xml = input.readLine();
    }
    return control;
  }

  /**
   * Reads the current input into a property element.
   *
   * @param prop the property element to load
   * @param xml the xml opening tag line
   * @return the loaded property element
   * @throws IOException
   */
  private XMLPropertyElement readProperty(XMLPropertyElement prop, String xml) throws IOException {
    // set property name
    prop.name = xml.substring(xml.indexOf("name=")+6, xml.indexOf("type=")-2); //$NON-NLS-1$ //$NON-NLS-2$
    // set property type
    xml = xml.substring(xml.indexOf("type=")+6);                               //$NON-NLS-1$
    prop.type = xml.substring(0, xml.indexOf("\""));                           //$NON-NLS-1$
    // set property content and className
    if(prop.type.equals("array")||prop.type.equals("collection")) {                            //$NON-NLS-1$ //$NON-NLS-2$
      xml = xml.substring(xml.indexOf("class=")+7);                                            //$NON-NLS-1$
      String className = xml.substring(0, xml.indexOf("\""));                                  //$NON-NLS-1$
      // workaround for media package name change
      int i = className.lastIndexOf(".");                                                      //$NON-NLS-1$
      if(i>-1) {
        String packageName = className.substring(0, i);
        if(packageName.endsWith("org.opensourcephysics.media")) {                              //$NON-NLS-1$
          className = packageName+".core"+className.substring(i);                              //$NON-NLS-1$
        }
      }
      prop.className = className;
      if(xml.indexOf("/>")!=-1) {                                                              // property closing tag on same line //$NON-NLS-1$
        return prop;
      }
      xml = input.readLine();
      while(xml.indexOf("<property")!=-1) {                                                    //$NON-NLS-1$
        prop.content.add(readProperty(new XMLPropertyElement(prop), xml));
        xml = input.readLine();
      }
    } else if(prop.type.equals("object")) {                                                    //$NON-NLS-1$
      XMLControlElement control = readObject(new XMLControlElement(prop), input.readLine());
      prop.content.add(control);
      prop.className = control.className;
    } else {                                                                                   // int, double, boolean or string types
      if(xml.indexOf(XML.CDATA_PRE)!=-1) {
        String s = xml.substring(xml.indexOf(XML.CDATA_PRE));
        while(s.indexOf(XML.CDATA_POST+"</property>")==-1) {                                   // look for end tag //$NON-NLS-1$
          s += XML.NEW_LINE+input.readLine();
        }
        xml = s.substring(0, s.indexOf(XML.CDATA_POST+"</property>")+XML.CDATA_POST.length()); //$NON-NLS-1$
      } else {
        String s = xml.substring(xml.indexOf(">")+1);                                          //$NON-NLS-1$
        while(s.indexOf("</property>")==-1) {                                                  // look for end tag //$NON-NLS-1$
          s += XML.NEW_LINE+input.readLine();
        }
        xml = s.substring(0, s.indexOf("</property>"));                                        //$NON-NLS-1$
      }
      prop.content.add(xml);
    }
    return prop;
  }

  /**
   * Returns a space for indentation.
   *
   * @param level the indent level
   * @return the space
   */
  private String indent(int level) {
    String space = ""; //$NON-NLS-1$
    for(int i = 0; i<XML.INDENT*level; i++) {
      space += " "; //$NON-NLS-1$
    }
    return space;
  }

  /**
   * Returns the object value of the specified property. May return null.
   *
   * @param prop the property
   * @return the array
   */
  private Object objectValue(XMLProperty prop) {
    if(!prop.getPropertyType().equals("object")) { //$NON-NLS-1$
      return null;
    }
    XMLControl control = (XMLControl) prop.getPropertyContent().get(0);
    return control.loadObject(null);
  }

  /**
   * Returns the double value of the specified property.
   *
   * @param prop the property
   * @return the value
   */
  private double doubleValue(XMLProperty prop) {
    if(!prop.getPropertyType().equals("double")) { //$NON-NLS-1$
      return Double.NaN;
    }
    return Double.parseDouble((String) prop.getPropertyContent().get(0));
  }

  /**
   * Returns the double value of the specified property.
   *
   * @param prop the property
   * @return the value
   */
  private int intValue(XMLProperty prop) {
    if(!prop.getPropertyType().equals("int")) { //$NON-NLS-1$
      return Integer.MIN_VALUE;
    }
    return Integer.parseInt((String) prop.getPropertyContent().get(0));
  }

  /**
   * Returns the boolean value of the specified property.
   *
   * @param prop the property
   * @return the value
   */
  private boolean booleanValue(XMLProperty prop) {
    return prop.getPropertyContent().get(0).equals("true"); //$NON-NLS-1$
  }

  /**
   * Returns the string value of the specified property.
   *
   * @param prop the property
   * @return the value
   */
  private String stringValue(XMLProperty prop) {
    if(!prop.getPropertyType().equals("string")) { //$NON-NLS-1$
      return null;
    }
    String content = (String) prop.getPropertyContent().get(0);
    if(content.indexOf(XML.CDATA_PRE)!=-1) {
      content = content.substring(content.indexOf(XML.CDATA_PRE)+XML.CDATA_PRE.length(), content.indexOf(XML.CDATA_POST));
    }
    return content;
  }

  /**
   * Returns the array value of the specified property. May return null.
   *
   * @param prop the property
   * @return the array
   */
  private Object arrayValue(XMLProperty prop) {
    if(!prop.getPropertyType().equals("array")) { //$NON-NLS-1$
      return null;
    }
    Class<?> componentType = prop.getPropertyClass().getComponentType();
    List<?> content = prop.getPropertyContent();
    // if no content, return a zero-length array
    if(content.isEmpty()) {
      return Array.newInstance(componentType, 0);
    }
    // determine the format from the first item
    XMLProperty first = (XMLProperty) content.get(0);
    if(first.getPropertyName().equals("array")) { //$NON-NLS-1$
      // create the array from an array string
      Object obj = first.getPropertyContent().get(0);
      if(obj instanceof String) {
        return arrayValue((String) obj, componentType);
      }
      return null;
    }
    // create the array from a list of properties
    // determine the length of the array
    XMLProperty last = (XMLProperty) content.get(content.size()-1);
    String index = last.getPropertyName();
    int n = Integer.parseInt(index.substring(1, index.indexOf("]"))); //$NON-NLS-1$
    // create the array
    Object array = Array.newInstance(componentType, n+1);
    // populate the array
    Iterator<?> it = content.iterator();
    while(it.hasNext()) {
      XMLProperty next = (XMLProperty) it.next();
      index = next.getPropertyName();
      n = Integer.parseInt(index.substring(1, index.indexOf("]"))); //$NON-NLS-1$
      String type = next.getPropertyType();
      if(type.equals("object")) {                                   //$NON-NLS-1$
        Array.set(array, n, objectValue(next));
      } else if(type.equals("int")) {                               //$NON-NLS-1$
        int val = intValue(next);
        if(Object.class.isAssignableFrom(componentType)) {
          Array.set(array, n, new Integer(val));
        } else {
          Array.setInt(array, n, val);
        }
      } else if(type.equals("double")) {                            //$NON-NLS-1$
        double val = doubleValue(next);
        if(Object.class.isAssignableFrom(componentType)) {
          Array.set(array, n, new Double(val));
        } else {
          Array.setDouble(array, n, val);
        }
      } else if(type.equals("boolean")) {                           //$NON-NLS-1$
        boolean val = booleanValue(next);
        if(Object.class.isAssignableFrom(componentType)) {
          Array.set(array, n, new Boolean(val));
        } else {
          Array.setBoolean(array, n, val);
        }
      } else if(type.equals("string")) {                            //$NON-NLS-1$
        Array.set(array, n, stringValue(next));
      } else if(type.equals("array")) {                             //$NON-NLS-1$
        Array.set(array, n, arrayValue(next));
      } else if(type.equals("collection")) {                        //$NON-NLS-1$
        Array.set(array, n, collectionValue(next));
      }
    }
    return array;
  }

  /**
   * Returns the array value of the specified array string. May return null.
   * An array string must start and end with braces and contain only
   * int, double and boolean types.
   *
   * @param arrayString the array string
   * @param componentType the component type of the array
   * @return the array
   */
  private Object arrayValue(String arrayString, Class<?> componentType) {
    if(!(arrayString.startsWith("{")&&arrayString.endsWith("}"))) { //$NON-NLS-1$ //$NON-NLS-2$
      return null;
    }
    // trim the outer braces
    String trimmed = arrayString.substring(1, arrayString.length()-1);
    if(componentType.isArray()) {
      // create and collect the array elements from substrings
      ArrayList<Object> list = new ArrayList<Object>();
      ArrayList<Boolean> isNull = new ArrayList<Boolean>();
      Class<?> arrayType = componentType.getComponentType();
      int i = trimmed.indexOf("{"); //$NON-NLS-1$
      int j = trimmed.indexOf("}"); //$NON-NLS-1$
      int k = trimmed.indexOf(","); //$NON-NLS-1$
      while(j>0) {
        if (k<i) { // first comma is before opening brace
        	isNull.add(true);
          trimmed = trimmed.substring(k+1);
        }
        else {
	        String nextArray = trimmed.substring(i, j+1);
	        Object obj = arrayValue(nextArray, arrayType);
	        list.add(obj);
        	isNull.add(false);
	        trimmed = trimmed.substring(j+1);
	        if (trimmed.startsWith(",")) // comma following closing brace //$NON-NLS-1$
		        trimmed = trimmed.substring(1);
        }
        i = trimmed.indexOf("{"); //$NON-NLS-1$
        j = trimmed.indexOf("}"); //$NON-NLS-1$
        k = trimmed.indexOf(","); //$NON-NLS-1$
      }
      // look for trailing null elements
      while (k>-1) {
      	isNull.add(true);
        trimmed = trimmed.substring(k+1);
        k = trimmed.indexOf(","); //$NON-NLS-1$
      }
      if (trimmed.length()>0) { // last element (after final comma) is null
      	isNull.add(true);      	
      }
      // create the array
      Object array = Array.newInstance(componentType, isNull.size());
      // populate the array
      Boolean[] hasNoElement = isNull.toArray(new Boolean[0]);
      Iterator<Object> it = list.iterator();
      for (int n=0; n<hasNoElement.length; n++) {
        if (!hasNoElement[n] && it.hasNext()) {
          Object obj = it.next();
          Array.set(array, n, obj);
        }
      }
      return array;
    }
    // collect element substrings separated by commas
    ArrayList<String> list = new ArrayList<String>();
    while(!trimmed.equals("")) {    //$NON-NLS-1$
      int i = trimmed.indexOf(","); //$NON-NLS-1$
      if(i>-1) {
        list.add(trimmed.substring(0, i));
        trimmed = trimmed.substring(i+1);
      } else {
        list.add(trimmed);
        break;
      }
    }
    // create the array
    Object array = Array.newInstance(componentType, list.size());
    // populate the array
    Iterator<String> it = list.iterator();
    int n = 0;
    while(it.hasNext()) {
      if(componentType==Integer.TYPE) {
        int i = Integer.parseInt(it.next());
        Array.setInt(array, n++, i);
      } else if(componentType==Double.TYPE) {
        double x = Double.parseDouble(it.next());
        Array.setDouble(array, n++, x);
      } else if(componentType==Boolean.TYPE) {
        boolean bool = it.next().equals("true"); //$NON-NLS-1$
        Array.setBoolean(array, n++, bool);
      }
    }
    return array;
  }

  /**
   * Returns the collection value of the specified property. May return null.
   *
   * @param prop the property
   * @return the array
   */
  @SuppressWarnings("unchecked")
  private Object collectionValue(XMLProperty prop) {
    if(!prop.getPropertyType().equals("collection")) { //$NON-NLS-1$
      return null;
    }
    Class<?> classType = prop.getPropertyClass();
    try {
      // create the collection
      Collection<Object> c = (Collection<Object>) classType.newInstance();
      List<Object> content = prop.getPropertyContent();
      // populate the array
      Iterator<Object> it = content.iterator();
      while(it.hasNext()) {
        XMLProperty next = (XMLProperty) it.next();
        String type = next.getPropertyType();
        if(type.equals("object")) {            //$NON-NLS-1$
          c.add(objectValue(next));
        } else if(type.equals("string")) {     //$NON-NLS-1$
          c.add(stringValue(next));
        } else if(type.equals("array")) {      //$NON-NLS-1$
          c.add(arrayValue(next));
        } else if(type.equals("collection")) { //$NON-NLS-1$
          c.add(collectionValue(next));
        }
      }
      return c;
    } catch(Exception ex) {
      ex.printStackTrace();
    }
    return null;
  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
