/*
* 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.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
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.IDataReference;
import org.openrosa.client.jr.core.model.data.IAnswerData;
import org.openrosa.client.jr.core.model.instance.utils.ITreeVisitor;
import org.openrosa.client.jr.core.model.util.restorable.Restorable;
import org.openrosa.client.jr.core.model.util.restorable.RestoreUtils;
import org.openrosa.client.jr.core.model.utils.IInstanceVisitor;
import org.openrosa.client.jr.core.services.storage.Persistable;
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.ExtWrapMap;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapNullable;
import org.openrosa.client.jr.core.util.externalizable.PrototypeFactory;
/**
* This class represents the xform model instance
*/
public class FormInstance implements Persistable, Restorable {
public static final String STORAGE_KEY = "FORMDATA";
/** The root of this tree */
private TreeElement root = new TreeElement();
// represents '/'; always has one and only one child -- the top-level
// instance data node
// this node is never returned or manipulated directly
/** The name for this data model */
private String name;
/** The integer Id of the model */
private int id;
/** The ID of the form that this is a model for */
private int formId;
/** The date that this model was taken and recorded */
private Date dateSaved;
public String schema;
public String formVersion;
public String uiVersion;
private HashMap namespaces = new HashMap();
public FormInstance() {
}
/**
* Creates a new data model using the root given.
*
* @param root
* The root of the tree for this data model.
*/
public FormInstance(TreeElement root) {
setID(-1);
setFormId(-1);
setRoot(root);
}
/**
* Sets the root element of this Model's tree
*
* @param root
* The root of the tree for this data model.
*/
public void setRoot(TreeElement topLevel) {
root = new TreeElement();
if (topLevel != null)
root.addChild(topLevel);
}
/**
* TODO: confusion between root and its first child?
*
* @return This model's root tree element
*/
public TreeElement getRoot() {
if (root.getNumChildren() == 0)
throw new RuntimeException("root node has no children");
return root.getChildAt(0);
}
// throws classcastexception if not using XPathReference
public static TreeReference unpackReference(IDataReference ref) {
return (TreeReference) ref.getReference();
}
public TreeReference copyNode(TreeReference from, TreeReference to) throws InvalidReferenceException {
if (!from.isAbsolute()) {
throw new InvalidReferenceException("Source reference must be absolute for copying", from);
}
TreeElement src = resolveReference(from);
if (src == null) {
throw new InvalidReferenceException("Null Source reference while attempting to copy node", from);
}
return copyNode(src, to).getRef();
}
// for making new repeat instances; 'from' and 'to' must be unambiguous
// references EXCEPT 'to' may be ambiguous at its final step
// return true is successfully copied, false otherwise
public TreeElement copyNode(TreeElement src, TreeReference to) throws InvalidReferenceException {
if (!to.isAbsolute())
throw new InvalidReferenceException("Destination reference must be absolute for copying", to);
// strip out dest node info and get dest parent
String dstName = to.getNameLast();
int dstMult = to.getMultLast();
TreeReference toParent = to.getParentRef();
TreeElement parent = resolveReference(toParent);
if (parent == null) {
throw new InvalidReferenceException("Null parent reference whle attempting to copy", toParent);
}
if (!parent.isChildable()) {
throw new InvalidReferenceException("Invalid Parent Node: cannot accept children.", toParent);
}
if (dstMult == TreeReference.INDEX_UNBOUND) {
dstMult = parent.getChildMultiplicity(dstName);
} else if (parent.getChild(dstName, dstMult) != null) {
throw new InvalidReferenceException("Destination already exists!", to);
}
TreeElement dest = src.deepCopy(false);
dest.setName(dstName);
dest.multiplicity = dstMult;
parent.addChild(dest);
return dest;
}
public void copyItemsetNode (TreeElement copyNode, TreeReference destRef, FormDef f) throws InvalidReferenceException {
TreeElement templateNode = getTemplate(destRef);
TreeElement newNode = copyNode(templateNode, destRef);
newNode.populateTemplate(copyNode, f);
}
// don't think this is used anymore
public IAnswerData getDataValue(IDataReference questionReference) {
TreeElement element = resolveReference(questionReference);
if (element != null) {
return element.getValue();
} else {
return null;
}
}
// take a ref that unambiguously refers to a single node and return that node
// return null if ref is ambiguous, node does not exist, ref is relative, or ref is '/'
// can be used to retrieve template nodes
public TreeElement resolveReference(TreeReference ref) {
if (!ref.isAbsolute()){
return null;
}
TreeElement node = root;
for (int i = 0; i < ref.size(); i++) {
String name = ref.getName(i);
int mult = ref.getMultiplicity(i);
if (mult == TreeReference.INDEX_UNBOUND) {
if (node.getChildMultiplicity(name) == 1) {
mult = 0;
} else {
// reference is not unambiguous
node = null;
break;
}
}
node = node.getChild(name, mult);
if (node == null)
break;
}
return (node == root ? null : node); // never return a reference to '/'
}
// same as resolveReference but return a vector containing all interstitial
// nodes: top-level instance data node first, and target node last
// returns null in all the same situations as resolveReference EXCEPT ref
// '/' will instead return empty vector
public Vector explodeReference(TreeReference ref) {
if (!ref.isAbsolute())
return null;
Vector nodes = new Vector();
TreeElement cur = root;
for (int i = 0; i < ref.size(); i++) {
String name = ref.getName(i);
int mult = ref.getMultiplicity(i);
if (mult == TreeReference.INDEX_UNBOUND) {
if (cur.getChildMultiplicity(name) == 1) {
mult = 0;
} else {
// reference is not unambiguous
return null;
}
}
if (cur != root) {
nodes.addElement(cur);
}
cur = cur.getChild(name, mult);
if (cur == null) {
return null;
}
}
return nodes;
}
public Vector expandReference(TreeReference ref) {
return expandReference(ref, false);
}
// take in a potentially-ambiguous ref, and return a vector of refs for all nodes that match the passed-in ref
// meaning, search out all repeated nodes that match the pattern of the passed-in ref
// every ref in the returned vector will be unambiguous (no index will ever be INDEX_UNBOUND)
// does not return template nodes when matching INDEX_UNBOUND, but will match templates when INDEX_TEMPLATE is explicitly set
// return null if ref is relative, otherwise return vector of refs (but vector will be empty is no refs match)
// '/' returns {'/'}
// can handle sub-repetitions (e.g., {/a[1]/b[1], /a[1]/b[2], /a[2]/b[1]})
public Vector expandReference(TreeReference ref, boolean includeTemplates) {
if (!ref.isAbsolute())
return null;
Vector v = new Vector();
expandReference(ref, root, v, includeTemplates);
return v;
}
// recursive helper function for expandReference
// sourceRef: original path we're matching against
// node: current node that has matched the sourceRef thus far
// templateRef: explicit path that refers to the current node
// refs: Vector to collect matching paths; if 'node' is a target node that
// matches sourceRef, templateRef is added to refs
private void expandReference(TreeReference sourceRef, TreeElement node, Vector refs, boolean includeTemplates) {
int depth = node.getDepth();
if (depth == sourceRef.size()) {
refs.addElement(node.getRef());
} else if (node.getNumChildren() > 0) {
String name = sourceRef.getName(depth);
int mult = sourceRef.getMultiplicity(depth);
Vector children = new Vector();
if (mult == TreeReference.INDEX_UNBOUND) {
int count = node.getChildMultiplicity(name);
for (int i = 0; i < count; i++) {
TreeElement child = node.getChild(name, i);
if (child != null) {
children.addElement(child);
} else {
throw new IllegalStateException(); // missing/non-sequential
// nodes
}
}
if (includeTemplates) {
TreeElement template = node.getChild(name, TreeReference.INDEX_TEMPLATE);
if (template != null) {
children.addElement(template);
}
}
} else {
TreeElement child = node.getChild(name, mult);
if (child != null)
children.addElement(child);
}
for (Enumeration e = children.elements(); e.hasMoreElements();) {
expandReference(sourceRef, (TreeElement)e.nextElement(), refs, includeTemplates);
}
}
}
// retrieve the template node for a given repeated node ref may be ambiguous
// return null if node is not repeatable
// assumes templates are built correctly and obey all data model validity rules
public TreeElement getTemplate(TreeReference ref) {
TreeElement node = getTemplatePath(ref);
return (node == null ? null : node.repeatable ? node : null);
}
public TreeElement getTemplatePath(TreeReference ref) {
if (!ref.isAbsolute())
return null;
TreeElement node = root;
for (int i = 0; i < ref.size(); i++) {
String name = ref.getName(i);
TreeElement newNode = node.getChild(name, TreeReference.INDEX_TEMPLATE);
if (newNode == null)
newNode = node.getChild(name, 0);
if (newNode == null)
return null;
node = newNode;
}
return node;
}
// determine if nodes are homogeneous, meaning their descendant structure is 'identical' for repeat purposes
// identical means all children match, and the children's children match, and so on
// repeatable children are ignored; as they do not have to exist in the same quantity for nodes to be homogeneous
// however, the child repeatable nodes MUST be verified amongst themselves for homogeneity later
// this function ignores the names of the two nodes
public static boolean isHomogeneous(TreeElement a, TreeElement b) {
if (a.isLeaf() && b.isLeaf()) {
return true;
} else if (a.isChildable() && b.isChildable()) {
// verify that every (non-repeatable) node in a exists in b and vice
// versa
for (int k = 0; k < 2; k++) {
TreeElement n1 = (k == 0 ? a : b);
TreeElement n2 = (k == 0 ? b : a);
for (int i = 0; i < n1.getNumChildren(); i++) {
TreeElement child1 = n1.getChildAt(i);
if (child1.repeatable)
continue;
TreeElement child2 = n2.getChild(child1.getName(), 0);
if (child2 == null)
return false;
if (child2.repeatable)
throw new RuntimeException("shouldn't happen");
}
}
// compare children
for (int i = 0; i < a.getNumChildren(); i++) {
TreeElement childA = a.getChildAt(i);
if (childA.repeatable)
continue;
TreeElement childB = b.getChild(childA.getName(), 0);
if (!isHomogeneous(childA, childB))
return false;
}
return true;
} else {
return false;
}
}
/**
* Resolves a binding to a particular question data element
*
* @param binding
* The binding representing a particular question
* @return A QuestionDataElement corresponding to the binding provided. Null
* if none exists in this tree.
*/
public TreeElement resolveReference(IDataReference binding) {
return resolveReference(unpackReference(binding));
}
public void accept(IInstanceVisitor visitor) {
visitor.visit(this);
if (visitor instanceof ITreeVisitor) {
root.accept((ITreeVisitor) visitor);
}
}
public void setDateSaved(Date dateSaved) {
this.dateSaved = dateSaved;
}
public void setFormId(int formId) {
this.formId = formId;
}
public Date getDateSaved() {
return this.dateSaved;
}
public int getFormId() {
return this.formId;
}
/*
* (non-Javadoc)
*
* @see
* org.javarosa.core.services.storage.utilities.Externalizable#readExternal
* (java.io.DataInputStream)
*/
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
id = ExtUtil.readInt(in);
formId = ExtUtil.readInt(in);
name = (String) ExtUtil.read(in, new ExtWrapNullable(String.class), pf);
schema = (String) ExtUtil.read(in, new ExtWrapNullable(String.class), pf);
dateSaved = (Date) ExtUtil.read(in, new ExtWrapNullable(Date.class), pf);
namespaces = (HashMap)ExtUtil.read(in, new ExtWrapMap(String.class, String.class));
setRoot((TreeElement) ExtUtil.read(in, TreeElement.class, pf));
}
/*
* (non-Javadoc)
*
* @see
* org.javarosa.core.services.storage.utilities.Externalizable#writeExternal
* (java.io.DataOutputStream)
*/
public void writeExternal(DataOutputStream out) throws IOException {
ExtUtil.writeNumeric(out, id);
ExtUtil.writeNumeric(out, formId);
ExtUtil.write(out, new ExtWrapNullable(name));
ExtUtil.write(out, new ExtWrapNullable(schema));
ExtUtil.write(out, new ExtWrapNullable(dateSaved));
ExtUtil.write(out, new ExtWrapMap(namespaces));
ExtUtil.write(out, getRoot());
}
public String getName() {
return name;
}
/**
* Sets the name of this datamodel instance
*
* @param name
* The name to be used
*/
public void setName(String name) {
this.name = name;
}
public int getID() {
return id;
}
public void setID(int id) {
this.id = id;
}
public TreeReference addNode(TreeReference ambigRef) {
TreeReference ref = ambigRef.clone();
if (createNode(ref) != null) {
return ref;
} else {
return null;
}
}
public TreeReference addNode(TreeReference ambigRef, IAnswerData data, int dataType) {
TreeReference ref = ambigRef.clone();
TreeElement node = createNode(ref);
if (node != null) {
if (dataType >= 0) {
node.dataType = dataType;
}
node.setValue(data);
return ref;
} else {
return null;
}
}
/*
* create the specified node in the tree, creating all intermediary nodes at
* each step, if necessary. if specified node already exists, return null
*
* creating a duplicate node is only allowed at the final step. it will be
* done if the multiplicity of the last step is ALL or equal to the count of
* nodes already there
*
* at intermediate steps, the specified existing node is used; if
* multiplicity is ALL: if no nodes exist, a new one is created; if one node
* exists, it is used; if multiple nodes exist, it's an error
*
* return the newly-created node; modify ref so that it's an unambiguous ref
* to the node
*/
private TreeElement createNode(TreeReference ref) {
TreeElement node = root;
for (int k = 0; k < ref.size(); k++) {
String name = ref.getName(k);
int count = node.getChildMultiplicity(name);
int mult = ref.getMultiplicity(k);
TreeElement child;
if (k < ref.size() - 1) {
if (mult == TreeReference.INDEX_UNBOUND) {
if (count > 1) {
return null; // don't know which node to use
} else {
// will use existing (if one and only one) or create new
mult = 0;
ref.setMultiplicity(k, 0);
}
}
// fetch
child = node.getChild(name, mult);
if (child == null) {
if (mult == 0) {
// create
child = new TreeElement(name, count);
node.addChild(child);
ref.setMultiplicity(k, count);
} else {
return null; // intermediate node does not exist
}
}
} else {
if (mult == TreeReference.INDEX_UNBOUND || mult == count) {
if (k == 0 && root.getNumChildren() != 0) {
return null; // can only be one top-level node, and it
// already exists
}
if (!node.isChildable()) {
return null; // current node can't have children
}
// create new
child = new TreeElement(name, count);
node.addChild(child);
ref.setMultiplicity(k, count);
} else {
return null; // final node must be a newly-created node
}
}
node = child;
}
return node;
}
public void addNamespace(String prefix, String URI) {
namespaces.put(prefix, URI);
}
public String[] getNamespacePrefixes() {
String[] prefixes = new String[namespaces.size()];
int i = 0;
for(Iterator en = namespaces.keySet().iterator() ; en.hasNext(); ) {
prefixes[i] = (String)en.next();
++i;
}
return prefixes;
}
public String getNamespaceURI(String prefix) {
return (String)namespaces.get(prefix);
}
public String getRestorableType() {
return "form";
}
// TODO: include whether form was sent already (or restrict always to unsent
// forms)
public FormInstance exportData() {
FormInstance dm = RestoreUtils.createDataModel(this);
RestoreUtils.addData(dm, "name", name);
RestoreUtils.addData(dm, "form-id", new Integer(formId));
RestoreUtils.addData(dm, "saved-on", dateSaved,
Constants.DATATYPE_DATE_TIME);
RestoreUtils.addData(dm, "schema", schema);
/////////////
throw new RuntimeException("FormInstance.exportData(): must be updated to use new transport layer");
// ITransportManager tm = TransportManager._();
// boolean sent = (tm.getModelDeliveryStatus(id, true) == TransportMessage.STATUS_DELIVERED);
// RestoreUtils.addData(dm, "sent", new Boolean(sent));
/////////////
// for (Enumeration e = namespaces.keys(); e.hasMoreElements(); ) {
// String key = (String)e.nextElement();
// RestoreUtils.addData(dm, "namespace/" + key, namespaces.get(key));
// }
//
// RestoreUtils.mergeDataModel(dm, this, "data");
// return dm;
}
public void templateData(FormInstance dm, TreeReference parentRef) {
RestoreUtils.applyDataType(dm, "name", parentRef, String.class);
RestoreUtils.applyDataType(dm, "form-id", parentRef, Integer.class);
RestoreUtils.applyDataType(dm, "saved-on", parentRef,
Constants.DATATYPE_DATE_TIME);
RestoreUtils.applyDataType(dm, "schema", parentRef, String.class);
RestoreUtils.applyDataType(dm, "sent", parentRef, Boolean.class);
// don't touch data for now
}
public void importData(FormInstance dm) {
name = (String) RestoreUtils.getValue("name", dm);
formId = ((Integer) RestoreUtils.getValue("form-id", dm)).intValue();
dateSaved = (Date) RestoreUtils.getValue("saved-on", dm);
schema = (String) RestoreUtils.getValue("schema", dm);
boolean sent = RestoreUtils.getBoolean(RestoreUtils
.getValue("sent", dm));
TreeElement names = dm.resolveReference(RestoreUtils.absRef("namespace", dm));
if (names != null) {
for (int i = 0; i < names.getNumChildren(); i++) {
TreeElement child = names.getChildAt(i);
String name = child.getName();
Object value = RestoreUtils.getValue("namespace/" + name, dm);
if (value != null){
namespaces.put(name, value);
}
}
}
/////////////
throw new RuntimeException("FormInstance.importData(): must be updated to use new transport layer");
// if (sent) {
// ITransportManager tm = TransportManager._();
// tm.markSent(id, false);
// }
/////////////
// IStorageUtility forms = StorageManager.getStorage(FormDef.STORAGE_KEY);
// FormDef f = (FormDef)forms.read(formId);
// setRoot(processSavedDataModel(dm.resolveReference(RestoreUtils.absRef("data", dm)), f.getDataModel(), f));
}
public TreeElement processSaved(FormInstance template, FormDef f) {
TreeElement fixedInstanceRoot = template.getRoot().deepCopy(true);
TreeElement incomingRoot = root.getChildAt(0);
if (!fixedInstanceRoot.getName().equals(incomingRoot.getName()) || incomingRoot.getMult() != 0) {
throw new RuntimeException("Saved form instance to restore does not match form definition");
}
fixedInstanceRoot.populate(incomingRoot, f);
return fixedInstanceRoot;
}
public FormInstance clone () {
FormInstance cloned = new FormInstance(this.getRoot().deepCopy(true));
cloned.setID(this.getID());
cloned.setFormId(this.getFormId());
cloned.setName(this.getName());
cloned.setDateSaved(this.getDateSaved());
cloned.schema = this.schema;
cloned.formVersion = this.formVersion;
cloned.uiVersion = this.uiVersion;
cloned.namespaces = new HashMap();
for (Iterator e = this.namespaces.keySet().iterator(); e.hasNext(); ) {
Object key = e.next();
cloned.namespaces.put(key, this.namespaces.get(key));
}
return cloned;
}
}