/* * 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 @SuppressWarnings("javadoc") public static final int ALWAYS_DECRYPT = 0; @SuppressWarnings("javadoc") public static final int PASSWORD_DECRYPT = 3; @SuppressWarnings("javadoc") public static final int NEVER_DECRYPT = 5; // static fields @SuppressWarnings("javadoc") 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; @SuppressWarnings("javadoc") 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), false); //$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), false); //$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), false); //$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) { setValue(name, obj, XMLPropertyElement.defaultWriteNullFinalArrayElements); } /** * Sets a property with the specified name and object value. * * @param name the name * @param obj the object * @param writeNullFinalElement true to write a final null array element (if needed) */ public void setValue(String name, Object obj, boolean writeNullFinalElement) { 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; } if (obj instanceof Boolean) { setValue(name, ((Boolean)obj).booleanValue()); 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, writeNullFinalElement); } } /** * 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") //$NON-NLS-1$ || prop.getPropertyType().equals("int") //$NON-NLS-1$ || prop.getPropertyType().equals("string"))) { //$NON-NLS-1$ try { return Double.parseDouble((String) prop.getPropertyContent().get(0)); } catch(Exception ex) { return Double.NaN; } } 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$ || prop.getPropertyType().equals("string"))) { //$NON-NLS-1$ try { return Integer.parseInt((String) prop.getPropertyContent().get(0)); } catch(Exception ex) { return Integer.MIN_VALUE; } } 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 decryption policy. * * @param policy the decryption policy: NEVER_DECRYPT, PASSWORD_DECRYPT or ALWAYS_DECRYPT */ 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.mkdirs()) { 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 an XML property. * * @param name the name * @param type the type * @param value the value * @param writeNullFinalArrayElement true to write a final null array element (if needed) */ private void setXMLProperty(String name, String type, Object value, boolean writeNullFinalArrayElement) { // 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, writeNullFinalArrayElement)); } else { props.add(new XMLPropertyElement(this, name, type, value, writeNullFinalArrayElement)); } } /** * 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$ // add XMLControl unless value is null if (xml.indexOf(">null</property")==-1) { //$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; } if (prop.getPropertyContent().isEmpty()) 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 = indexOfClosingBrace(trimmed, i); int k = trimmed.indexOf(","); //$NON-NLS-1$ while(j>0) { // if (k<i) { // first comma is before opening brace if (k>-1 && 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$ j = indexOfClosingBrace(trimmed, i); 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; } /** * Returns the index of the closing brace corresponding to the opening * brace at the given index in an array string. * * @param arrayString the array string * @param indexOfOpeningBrace the index of the opening brace * @return the index of the closing brace */ private int indexOfClosingBrace(String arrayString, int indexOfOpeningBrace) { int pointer = indexOfOpeningBrace+1; int n = 1; // count up/down for opening/closing braces int opening = arrayString.indexOf("{", pointer); //$NON-NLS-1$ int closing = arrayString.indexOf("}", pointer); //$NON-NLS-1$ while (n>0) { if (opening>-1 && opening<closing) { n++; pointer = opening+1; opening = arrayString.indexOf("{", pointer); //$NON-NLS-1$ } else if (closing>-1) { n--; pointer = closing+1; closing = arrayString.indexOf("}", pointer); //$NON-NLS-1$ } else return -1; } return pointer-1; } } /* * 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 */