/* * Copyright (C) 2009 JavaRosa * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.openrosa.client.jr.core.model.instance; import java.io.IOException; import java.util.Enumeration; import java.util.Vector; import org.openrosa.client.java.io.DataInputStream; import org.openrosa.client.java.io.DataOutputStream; import org.openrosa.client.jr.core.model.Constants; import org.openrosa.client.jr.core.model.FormDef; import org.openrosa.client.jr.core.model.FormElementStateListener; import org.openrosa.client.jr.core.model.condition.Constraint; import org.openrosa.client.jr.core.model.data.IAnswerData; import org.openrosa.client.jr.core.model.data.SelectMultiData; import org.openrosa.client.jr.core.model.data.SelectOneData; import org.openrosa.client.jr.core.model.instance.utils.CompactInstanceWrapper; import org.openrosa.client.jr.core.model.instance.utils.ITreeVisitor; import org.openrosa.client.jr.core.model.util.restorable.RestoreUtils; import org.openrosa.client.jr.core.util.externalizable.DeserializationException; import org.openrosa.client.jr.core.util.externalizable.ExtUtil; import org.openrosa.client.jr.core.util.externalizable.ExtWrapList; import org.openrosa.client.jr.core.util.externalizable.ExtWrapNullable; import org.openrosa.client.jr.core.util.externalizable.ExtWrapTagged; import org.openrosa.client.jr.core.util.externalizable.Externalizable; import org.openrosa.client.jr.core.util.externalizable.PrototypeFactory; /** * An element of a FormInstance. * * TreeElements represent an XML node in the instance. It may either have a value (e.g., <name>Drew</name>), * a number of TreeElement children (e.g., <meta><device /><timestamp /><user_id /></meta>), or neither (e.g., * <empty_node />) * * @author Clayton Sims * */ public class TreeElement implements Externalizable { private String name; // can be null only for hidden root node public int multiplicity; // see TreeReference for special values private TreeElement parent; public boolean repeatable; //public boolean isAttribute; for when we support xml attributes as data nodes private IAnswerData value; private Vector children = new Vector(); /* model properties */ public int dataType = Constants.DATATYPE_NULL; //TODO public boolean required = false;// TODO private Constraint constraint = null; private String preloadHandler = null; private String preloadParams = null; private boolean relevant = true; private boolean enabled = true; // inherited properties private boolean relevantInherited = true; private boolean enabledInherited = true; private Vector observers; private Vector attributes = new Vector(); /** * TreeElement with null name and 0 multiplicity? (a "hidden root" node?) */ public TreeElement() { this(null, TreeReference.DEFAULT_MUTLIPLICITY); } public TreeElement(String name) { this(name, TreeReference.DEFAULT_MUTLIPLICITY); } public TreeElement(String name, int multiplicity) { this.name = name; this.multiplicity = multiplicity; this.parent = null; } public boolean isLeaf() { return (children.size() == 0); } public boolean isChildable() { return (value == null); } public void setValue(IAnswerData value) { if (isLeaf()) { this.value = value; } else { throw new RuntimeException("Can't set data value for node that has children!"); } } public TreeElement getChild(String name, int multiplicity) { if (name.equals(TreeReference.NAME_WILDCARD)) { return (TreeElement) this.children.elementAt(multiplicity); //droos: i'm suspicious of this } else { for (int i = 0; i < this.children.size(); i++) { TreeElement child = (TreeElement) this.children.elementAt(i); if (name.equals(child.getName()) && child.getMult() == multiplicity) { return child; } } } return null; } /** * * Get all the child nodes of this element, with specific name * * @param name * @return */ public Vector getChildrenWithName(String name) { return getChildrenWithName(name, false); } private Vector getChildrenWithName(String name, boolean includeTemplate) { Vector v = new Vector(); for (int i = 0; i < this.children.size(); i++) { TreeElement child = (TreeElement) this.children.elementAt(i); if ((child.getName().equals(name) || name.equals(TreeReference.NAME_WILDCARD)) && (includeTemplate || child.multiplicity != TreeReference.INDEX_TEMPLATE)) v.addElement(child); } return v; } public int getNumChildren() { return this.children.size(); } public TreeElement getChildAt (int i) { return (TreeElement)children.elementAt(i); } /** * Add a child to this element * * @param child */ public void addChild(TreeElement child) { addChild(child, false); } private void addChild(TreeElement child, boolean checkDuplicate) { if (!isChildable()) { throw new RuntimeException("Can't add children to node that has data value!"); } if (child.multiplicity == TreeReference.INDEX_UNBOUND) { throw new RuntimeException("Cannot add child with an unbound index!"); } if (checkDuplicate) { TreeElement existingChild = getChild(child.name, child.multiplicity); if (existingChild != null) { throw new RuntimeException("Attempted to add duplicate child!"); } } // try to keep things in order int i = children.size(); if (child.getMult() == TreeReference.INDEX_TEMPLATE) { TreeElement anchor = getChild(child.getName(), 0); if (anchor != null) i = children.indexOf(anchor); } else { TreeElement anchor = getChild(child.getName(), (child.getMult() == 0 ? TreeReference.INDEX_TEMPLATE : child.getMult() - 1)); if (anchor != null) i = children.indexOf(anchor) + 1; } children.insertElementAt(child, i); child.setParent(this); child.setRelevant(isRelevant(), true); child.setEnabled(isEnabled(), true); } public void removeChild(TreeElement child) { children.removeElement(child); } public void removeChild(String name, int multiplicity) { TreeElement child = getChild(name, multiplicity); if (child != null) { removeChild(child); } } public void removeChildren(String name) { removeChildren(name, false); } public void removeChildren(String name, boolean includeTemplate) { Vector v = getChildrenWithName(name, includeTemplate); for (int i = 0; i < v.size(); i++) { removeChild((TreeElement) v.elementAt(i)); } } public void removeChildAt(int i) { children.removeElementAt(i); } public int getChildMultiplicity(String name) { return getChildrenWithName(name, false).size(); } public TreeElement shallowCopy() { TreeElement newNode = new TreeElement(name, multiplicity); newNode.parent = parent; newNode.repeatable = repeatable; newNode.dataType = dataType; newNode.relevant = relevant; newNode.required = required; newNode.enabled = enabled; newNode.constraint = constraint; newNode.preloadHandler = preloadHandler; newNode.preloadParams = preloadParams; newNode.setAttributesFromSingleStringVector(getSingleStringAttributeVector()); if (value != null) { newNode.value = value.clone(); } newNode.children = children; return newNode; } public TreeElement deepCopy(boolean includeTemplates) { TreeElement newNode = shallowCopy(); newNode.children = new Vector(); for (int i = 0; i < children.size(); i++) { TreeElement child = (TreeElement) children.elementAt(i); if (includeTemplates || child.getMult() != TreeReference.INDEX_TEMPLATE) { newNode.addChild(child.deepCopy(includeTemplates)); } } return newNode; } /* ==== MODEL PROPERTIES ==== */ // factoring inheritance rules public boolean isRelevant() { return relevantInherited && relevant; } // factoring in inheritance rules public boolean isEnabled() { return enabledInherited && enabled; } /* ==== SPECIAL SETTERS (SETTERS WITH SIDE-EFFECTS) ==== */ public boolean setAnswer(IAnswerData answer) { if (value != null || answer != null) { setValue(answer); alertStateObservers(FormElementStateListener.CHANGE_DATA); return true; } else { return false; } } public void setRequired(boolean required) { if (this.required != required) { this.required = required; alertStateObservers(FormElementStateListener.CHANGE_REQUIRED); } } public void setRelevant(boolean relevant) { setRelevant(relevant, false); } private void setRelevant(boolean relevant, boolean inherited) { boolean oldRelevancy = isRelevant(); if (inherited) { this.relevantInherited = relevant; } else { this.relevant = relevant; } if (isRelevant() != oldRelevancy) { for (int i = 0; i < children.size(); i++) { ((TreeElement) children.elementAt(i)).setRelevant(isRelevant(), true); } alertStateObservers(FormElementStateListener.CHANGE_RELEVANT); } } public void setEnabled(boolean enabled) { setEnabled(enabled, false); } public void setEnabled(boolean enabled, boolean inherited) { boolean oldEnabled = isEnabled(); if (inherited) { this.enabledInherited = enabled; } else { this.enabled = enabled; } if (isEnabled() != oldEnabled) { for (int i = 0; i < children.size(); i++) { ((TreeElement) children.elementAt(i)).setEnabled(isEnabled(), true); } alertStateObservers(FormElementStateListener.CHANGE_ENABLED); } } /* ==== OBSERVER PATTERN ==== */ public void registerStateObserver(FormElementStateListener qsl) { if (observers == null) observers = new Vector(); if (!observers.contains(qsl)) { observers.addElement(qsl); } } public void unregisterStateObserver(FormElementStateListener qsl) { if (observers != null) { observers.removeElement(qsl); if (observers.isEmpty()) observers = null; } } public void unregisterAll() { observers = null; } public void alertStateObservers(int changeFlags) { if (observers != null) { for (Enumeration e = observers.elements(); e.hasMoreElements();) ((FormElementStateListener) e.nextElement()) .formElementStateChanged(this, changeFlags); } } /* ==== VISITOR PATTERN ==== */ /** * Visitor pattern acceptance method. * * @param visitor * The visitor traveling this tree */ public void accept(ITreeVisitor visitor) { visitor.visit(this); Enumeration en = children.elements(); while (en.hasMoreElements()) { ((TreeElement) en.nextElement()).accept(visitor); } } /* * ==== HARD-CODED ATTRIBUTES (delete once we support writable attributes) * ==== */ /** * Returns the number of attributes of this element. */ public int getAttributeCount() { return attributes.size(); } /** * get namespace of attribute at 'index' in the vector * * @param index * @return String */ public String getAttributeNamespace(int index) { return ((String[]) attributes.elementAt(index))[0]; } /** * get name of attribute at 'index' in the vector * * @param index * @return String */ public String getAttributeName(int index) { return ((String[]) attributes.elementAt(index))[1]; } /** * get value of attribute at 'index' in the vector * * @param index * @return String */ public String getAttributeValue(int index) { return ((String[]) attributes.elementAt(index))[2]; } /** * get value of attribute with namespace:name' in the vector * * @param index * @return String */ public String getAttributeValue(String namespace, String name) { for (int i = 0; i < getAttributeCount(); i++) { if (name.equals(getAttributeName(i)) && (namespace == null || namespace.equals(getAttributeNamespace(i)))) { return getAttributeValue(i); } } return null; } /** * Sets the given attribute; a value of null removes the attribute * * * */ public void setAttribute(String namespace, String name, String value) { if (namespace == null) namespace = ""; for (int i = attributes.size() - 1; i >= 0; i--) { String[] attribut = (String[]) attributes.elementAt(i); if (attribut[0].equals(namespace) && attribut[1].equals(name)) { if (value == null) { attributes.removeElementAt(i); } else { attribut[2] = value; } return; } } attributes.addElement(new String[] { namespace, name, value }); } /** * A method for producing a vector of single strings - from the current * attribute vector of string [] arrays. * * @return */ public Vector getSingleStringAttributeVector() { Vector strings = new Vector(); if (attributes.size() == 0) return null; else { for (int i = 0; i < this.attributes.size(); i++) { String[] array = (String[]) attributes.elementAt(i); if (array[0] == null || array[0] == "") strings.addElement(new String(array[1] + "=" + array[2])); else strings.addElement(new String(array[0] + ":" + array[1] + "=" + array[2])); } return strings; } } /** * Method to repopulate the attribute vector from a vector of singleStrings * * @param attStrings */ public void setAttributesFromSingleStringVector(Vector attStrings) { this.attributes = new Vector(); if (attStrings != null) { for (int i = 0; i < attStrings.size(); i++) { addSingleAttribute(i, attStrings); } } } private void addSingleAttribute(int i, Vector attStrings) { String att = (String) attStrings.elementAt(i); String[] array = new String[3]; int start = 0; // get namespace int pos = -1; // Clayton Sims - Jun 1, 2009 : Updated this code: // We want to find the _last_ possible ':', not the // first one. Namespaces can have URLs in them. //int pos = att.indexOf(":"); while(att.indexOf(":",pos+1) != -1) { pos = att.indexOf(":",pos+1); } if (pos == -1) { array[0] = null; start = 0; } else { array[0] = att.substring(start, pos); start = ++pos; } // get attribute name pos = att.indexOf("="); array[1] = att.substring(start, pos); start = ++pos; array[2] = att.substring(start); this.setAttribute(array[0], array[1], array[2]); } /* ==== SERIALIZATION ==== */ /* * TODO: * * this new serialization scheme is kind of lame. ideally, we shouldn't have * to sub-class TreeElement at all; we should have an API that can * seamlessly represent complex data model objects (like weight history or * immunizations) as if they were explicity XML subtrees underneath the * parent TreeElement * * failing that, we should wrap this scheme in an ExternalizableWrapper */ /* * (non-Javadoc) * * @see * org.javarosa.core.services.storage.utilities.Externalizable#readExternal * (java.io.DataInputStream) */ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { name = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); multiplicity = ExtUtil.readInt(in); repeatable = ExtUtil.readBool(in); value = (IAnswerData) ExtUtil.read(in, new ExtWrapNullable(new ExtWrapTagged()), pf); // children = ExtUtil.nullIfEmpty((Vector)ExtUtil.read(in, new // ExtWrapList(TreeElement.class), pf)); // Jan 22, 2009 - csims@dimagi.com // old line: children = ExtUtil.nullIfEmpty((Vector)ExtUtil.read(in, new // ExtWrapList(TreeElement.class), pf)); // New Child deserialization // 1. read null status as boolean // 2. read number of children // 3. for i < number of children // 3.1 if read boolean true , then create TreeElement and deserialize // directly. // 3.2 if read boolean false then create tagged element and deserialize // child if (!ExtUtil.readBool(in)) { // 1. children = null; } else { children = new Vector(); // 2. int numChildren = (int) ExtUtil.readNumeric(in); // 3. for (int i = 0; i < numChildren; ++i) { boolean normal = ExtUtil.readBool(in); TreeElement child; if (normal) { // 3.1 child = new TreeElement(); child.readExternal(in, pf); } else { // 3.2 child = (TreeElement) ExtUtil.read(in, new ExtWrapTagged(), pf); } child.setParent(this); children.addElement(child); } } // end Jan 22, 2009 dataType = ExtUtil.readInt(in); relevant = ExtUtil.readBool(in); required = ExtUtil.readBool(in); enabled = ExtUtil.readBool(in); relevantInherited = ExtUtil.readBool(in); enabledInherited = ExtUtil.readBool(in); constraint = (Constraint) ExtUtil.read(in, new ExtWrapNullable( Constraint.class), pf); preloadHandler = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); preloadParams = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); Vector attStrings = ExtUtil.nullIfEmpty((Vector) ExtUtil.read(in, new ExtWrapList(String.class), pf)); setAttributesFromSingleStringVector(attStrings); } /* * (non-Javadoc) * * @see * org.javarosa.core.services.storage.utilities.Externalizable#writeExternal * (java.io.DataOutputStream) */ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, ExtUtil.emptyIfNull(name)); ExtUtil.writeNumeric(out, multiplicity); ExtUtil.writeBool(out, repeatable); ExtUtil.write(out, new ExtWrapNullable(value == null ? null : new ExtWrapTagged(value))); // Jan 22, 2009 - csims@dimagi.com // old line: ExtUtil.write(out, new // ExtWrapList(ExtUtil.emptyIfNull(children))); // New Child serialization // 1. write null status as boolean // 2. write number of children // 3. for all child in children // 3.1 if child type == TreeElement write boolean true , then serialize // directly. // 3.2 if child type != TreeElement, write boolean false, then tagged // child if (children == null) { // 1. ExtUtil.writeBool(out, false); } else { // 1. ExtUtil.writeBool(out, true); // 2. ExtUtil.writeNumeric(out, children.size()); // 3. Enumeration en = children.elements(); while (en.hasMoreElements()) { TreeElement child = (TreeElement) en.nextElement(); if (child.getClass() == TreeElement.class) { // 3.1 ExtUtil.writeBool(out, true); child.writeExternal(out); } else { // 3.2 ExtUtil.writeBool(out, false); ExtUtil.write(out, new ExtWrapTagged(child)); } } } // end Jan 22, 2009 ExtUtil.writeNumeric(out, dataType); ExtUtil.writeBool(out, relevant); ExtUtil.writeBool(out, required); ExtUtil.writeBool(out, enabled); ExtUtil.writeBool(out, relevantInherited); ExtUtil.writeBool(out, enabledInherited); ExtUtil.write(out, new ExtWrapNullable(constraint)); // TODO: inefficient for repeats ExtUtil.writeString(out, ExtUtil.emptyIfNull(preloadHandler)); ExtUtil.writeString(out, ExtUtil.emptyIfNull(preloadParams)); Vector attStrings = getSingleStringAttributeVector(); ExtUtil.write(out, new ExtWrapList(ExtUtil.emptyIfNull(attStrings))); } //rebuilding a node from an imported instance // there's a lot of error checking we could do on the received instance, but it's // easier to just ignore the parts that are incorrect public void populate(TreeElement incoming, FormDef f) { if (this.isLeaf()) { // check that incoming doesn't have children? IAnswerData value = incoming.getValue(); if (value == null) { this.setValue(null); } else if (this.dataType == Constants.DATATYPE_TEXT || this.dataType == Constants.DATATYPE_NULL) { this.setValue(value); // value is a StringData } else { String textVal = (String) value.getValue(); IAnswerData typedVal = RestoreUtils.xfFact.parseData(textVal, this.dataType, this.getRef(), f); this.setValue(typedVal); } } else { Vector names = new Vector(); for (int i = 0; i < this.getNumChildren(); i++) { TreeElement child = this.getChildAt(i); if (!names.contains(child.getName())) { names.addElement(child.getName()); } } // remove all default repetitions from skeleton data model (_preserving_ templates, though) for (int i = 0; i < this.getNumChildren(); i++) { TreeElement child = this.getChildAt(i); if (child.repeatable && child.getMult() != TreeReference.INDEX_TEMPLATE) { this.removeChildAt(i); i--; } } // make sure ordering is preserved (needed for compliance with xsd schema) if (this.getNumChildren() != names.size()) { throw new RuntimeException("sanity check failed"); } for (int i = 0; i < this.getNumChildren(); i++) { TreeElement child = this.getChildAt(i); String expectedName = (String) names.elementAt(i); if (!child.getName().equals(expectedName)) { TreeElement child2 = null; int j; for (j = i + 1; j < this.getNumChildren(); j++) { child2 = this.getChildAt(j); if (child2.getName().equals(expectedName)) { break; } } if (j == this.getNumChildren()) { throw new RuntimeException("sanity check failed"); } this.removeChildAt(j); this.children.insertElementAt(child2, i); } } // java i hate you so much for (int i = 0; i < this.getNumChildren(); i++) { TreeElement child = this.getChildAt(i); Vector newChildren = incoming.getChildrenWithName(child.getName()); if (child.repeatable) { for (int k = 0; k < newChildren.size(); k++) { TreeElement newChild = child.deepCopy(true); newChild.setMult(k); this.children.insertElementAt(newChild, i + k + 1); newChild.populate((TreeElement)newChildren.elementAt(k), f); } i += newChildren.size(); } else { if (newChildren.size() == 0) { child.setRelevant(false); } else { child.populate((TreeElement)newChildren.elementAt(0), f); } } } } } //this method is for copying in the answers to an itemset. the template node of the destination //is used for overall structure (including data types), and the itemset source node is used for //raw data. note that data may be coerced across types, which may result in type conversion error //very similar in structure to populate() public void populateTemplate(TreeElement incoming, FormDef f) { if (this.isLeaf()) { IAnswerData value = incoming.getValue(); if (value == null) { this.setValue(null); } else { Class classType = CompactInstanceWrapper.classForDataType(this.dataType); if (classType == null) { throw new RuntimeException("data type [" + value.getClass().getName() + "] not supported inside itemset"); } else if (true /*classType.isAssignableFrom(value.getClass())*/ && !(value instanceof SelectOneData || value instanceof SelectMultiData)) { this.setValue(value); } else { String textVal = RestoreUtils.xfFact.serializeData(value); IAnswerData typedVal = RestoreUtils.xfFact.parseData(textVal, this.dataType, this.getRef(), f); this.setValue(typedVal); } } } else { for (int i = 0; i < this.getNumChildren(); i++) { TreeElement child = this.getChildAt(i); Vector newChildren = incoming.getChildrenWithName(child.getName()); if (child.repeatable) { for (int k = 0; k < newChildren.size(); k++) { TreeElement template = f.getInstance().getTemplate(child.getRef()); TreeElement newChild = template.deepCopy(false); newChild.setMult(k); this.children.insertElementAt(newChild, i + k + 1); newChild.populateTemplate((TreeElement)newChildren.elementAt(k), f); } i += newChildren.size(); } else { child.populateTemplate((TreeElement)newChildren.elementAt(0), f); } } } } //return the tree reference that corresponds to this tree element public TreeReference getRef () { TreeElement elem = this; TreeReference ref = TreeReference.selfRef(); while (elem != null) { TreeReference step; if (elem.name != null) { step = TreeReference.selfRef(); step.add(elem.name, elem.multiplicity); } else { step = TreeReference.rootRef(); } ref = ref.parent(step); elem = elem.parent; } return ref; } public int getDepth () { TreeElement elem = this; int depth = 0; while (elem.name != null) { depth++; elem = elem.parent; } return depth; } public String getPreloadHandler() { return preloadHandler; } public Constraint getConstraint() { return constraint; } public void setPreloadHandler(String preloadHandler) { this.preloadHandler = preloadHandler; } public void setConstraint(Constraint constraint) { this.constraint = constraint; } public String getPreloadParams() { return preloadParams; } public void setPreloadParams(String preloadParams) { this.preloadParams = preloadParams; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getMult() { return multiplicity; } public void setMult(int multiplicity) { this.multiplicity = multiplicity; } public void setParent (TreeElement parent) { this.parent = parent; } public TreeElement getParent () { return parent; } public IAnswerData getValue() { return value; } }