/*
* 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;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.NoSuchElementException;
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.condition.Condition;
import org.openrosa.client.jr.core.model.condition.Constraint;
import org.openrosa.client.jr.core.model.condition.EvaluationContext;
import org.openrosa.client.jr.core.model.condition.IConditionExpr;
import org.openrosa.client.jr.core.model.condition.IFunctionHandler;
import org.openrosa.client.jr.core.model.condition.Recalculate;
import org.openrosa.client.jr.core.model.condition.Triggerable;
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.data.helper.Selection;
import org.openrosa.client.jr.core.model.instance.FormInstance;
import org.openrosa.client.jr.core.model.instance.InvalidReferenceException;
import org.openrosa.client.jr.core.model.instance.TreeElement;
import org.openrosa.client.jr.core.model.instance.TreeReference;
import org.openrosa.client.jr.core.model.utils.QuestionPreloader;
import org.openrosa.client.jr.core.services.locale.Localizable;
import org.openrosa.client.jr.core.services.locale.Localizer;
import org.openrosa.client.jr.core.services.storage.IMetaData;
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.ExtWrapList;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapListPoly;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapNullable;
import org.openrosa.client.jr.core.util.externalizable.PrototypeFactory;
import org.openrosa.client.jr.model.xform.XPathReference;
/**
* Definition of a form. This has some meta data about the form definition and a
* collection of groups together with question branching or skipping rules.
*
* @author Daniel Kayiwa, Drew Roos
*
*/
public class FormDef implements IFormElement, Localizable, Persistable, IMetaData {
public static final String STORAGE_KEY = "FORMDEF";
public static final int TEMPLATING_RECURSION_LIMIT = 10;
private Vector children;// <IFormElement>
/** A collection of group definitions. */
private int id;
/** The numeric unique identifier of the form definition on the local device */
private String title;
/** The display title of the form. */
private String name;
/**
* A unique external name that is used to identify the form between machines
*/
private Localizer localizer;
public Vector triggerables; // <Triggerable>; this list is topologically ordered, meaning for any tA and tB in
//the list, where tA comes before tB, evaluating tA cannot depend on any result from evaluating tB
private boolean triggerablesInOrder; //true if triggerables has been ordered topologically (DON'T DELETE ME EVEN THOUGH I'M UNUSED)
private FormInstance instance;
private Vector outputFragments; // <IConditionExpr> contents of <output>
// tags that serve as parameterized
// arguments to captions
public HashMap triggerIndex; // <TreeReference, Vector<Triggerable>>
private HashMap conditionRepeatTargetIndex; // <TreeReference, Condition>;
// associates repeatable
// nodes with the Condition
// that determines their
// relevancy
public EvaluationContext exprEvalContext;
private QuestionPreloader preloader = new QuestionPreloader();
/**
*
*/
public FormDef() {
setID(-1);
setChildren(null);
triggerables = new Vector();
triggerablesInOrder = true;
triggerIndex = new HashMap();
conditionRepeatTargetIndex = new HashMap();
setEvaluationContext(new EvaluationContext());
outputFragments = new Vector();
}
// ---------- child elements
public void addChild(IFormElement fe) {
this.children.addElement(fe);
}
public IFormElement getChild(int i) {
if (i < this.children.size())
return (IFormElement) this.children.elementAt(i);
throw new ArrayIndexOutOfBoundsException(
"FormDef: invalid child index: " + i + " only "
+ children.size() + " children");
}
public IFormElement getChild(FormIndex index) {
IFormElement element = this;
while (index != null && index.isInForm()) {
element = element.getChild(index.getLocalIndex());
index = index.getNextLevel();
}
return element;
}
/**
* Dereference the form index and return a Vector of all interstitial nodes
* (top-level parent first; index target last)
*
* Ignore 'new-repeat' node for now; just return/stop at ref to
* yet-to-be-created repeat node (similar to repeats that already exist)
*
* @param index
* @return
*/
public Vector explodeIndex(FormIndex index) {
Vector indexes = new Vector();
Vector multiplicities = new Vector();
Vector elements = new Vector();
collapseIndex(index, indexes, multiplicities, elements);
return elements;
}
// take a reference, find the instance node it refers to (factoring in
// multiplicities)
/**
* @param index
* @return
*/
public TreeReference getChildInstanceRef(FormIndex index) {
Vector indexes = new Vector();
Vector multiplicities = new Vector();
Vector elements = new Vector();
collapseIndex(index, indexes, multiplicities, elements);
return getChildInstanceRef(elements, multiplicities);
}
/**
* @param elements
* @param multiplicities
* @return
*/
public TreeReference getChildInstanceRef(Vector elements, Vector multiplicities) {
if (elements.size() == 0)
return null;
// get reference for target element
TreeReference ref = FormInstance.unpackReference(((IFormElement) elements.lastElement()).getBind()).clone();
for (int i = 0; i < ref.size(); i++) {
ref.setMultiplicity(i, 0);
}
// fill in multiplicities for repeats along the way
for (int i = 0; i < elements.size(); i++) {
IFormElement temp = (IFormElement) elements.elementAt(i);
if (temp instanceof GroupDef && ((GroupDef) temp).getRepeat()) {
TreeReference repRef = FormInstance.unpackReference(temp.getBind());
if (repRef.isParentOf(ref, false)) {
int repMult = ((Integer) multiplicities.elementAt(i)).intValue();
ref.setMultiplicity(repRef.size() - 1, repMult);
} else {
return null; // question/repeat hierarchy is not consistent
// with instance instance and bindings
}
}
}
return ref;
}
public void setLocalizer(Localizer l) {
if (this.localizer != null) {
this.localizer.unregisterLocalizable(this);
}
this.localizer = l;
if (this.localizer != null) {
this.localizer.registerLocalizable(this);
}
}
// don't think this should ever be called(!)
public IDataReference getBind() {
throw new RuntimeException("method not implemented");
}
public void setValue(IAnswerData data, TreeReference ref) {
setValue(data, ref, instance.resolveReference(ref));
}
public void setValue(IAnswerData data, TreeReference ref, TreeElement node) {
setAnswer(data, node);
triggerTriggerables(ref);
}
public void setAnswer(IAnswerData data, TreeReference ref) {
setAnswer(data, instance.resolveReference(ref));
}
public void setAnswer(IAnswerData data, TreeElement node) {
node.setAnswer(data);
}
/**
* Deletes the inner-most repeat that this node belongs to and returns the
* corresponding FormIndex. Behavior is currently undefined if you call this
* method on a node that is not contained within a repeat.
*
* @param index
* @return
*/
public FormIndex deleteRepeat(FormIndex index) {
Vector indexes = new Vector();
Vector multiplicities = new Vector();
Vector elements = new Vector();
collapseIndex(index, indexes, multiplicities, elements);
// loop backwards through the elements, removing objects from each
// vector, until we find a repeat
// TODO: should probably check to make sure size > 0
for (int i = elements.size() - 1; i >= 0; i--) {
IFormElement e = (IFormElement) elements.elementAt(i);
if (e instanceof GroupDef && ((GroupDef) e).getRepeat()) {
break;
} else {
indexes.removeElementAt(i);
multiplicities.removeElementAt(i);
elements.removeElementAt(i);
}
}
// build new formIndex which includes everything
// up to the node we're going to remove
FormIndex newIndex = buildIndex(indexes, multiplicities, elements);
TreeReference deleteRef = getChildInstanceRef(newIndex);
TreeElement deleteElement = instance.resolveReference(deleteRef);
TreeReference parentRef = deleteRef.getParentRef();
TreeElement parentElement = instance.resolveReference(parentRef);
int childMult = deleteElement.getMult();
parentElement.removeChild(deleteElement);
// update multiplicities of other child nodes
for (int i = 0; i < parentElement.getNumChildren(); i++) {
TreeElement child = parentElement.getChildAt(i);
if (child.getMult() > childMult) {
child.setMult(child.getMult() - 1);
}
}
triggerTriggerables(parentRef);
return newIndex;
}
public void createNewRepeat(FormIndex index) throws InvalidReferenceException {
TreeReference destRef = getChildInstanceRef(index);
TreeElement template = instance.getTemplate(destRef);
instance.copyNode(template, destRef);
preloadInstance(instance.resolveReference(destRef));
triggerTriggerables(destRef); // trigger conditions that depend on the creation of this new node
initializeTriggerables(destRef); // initialize conditions for the node (and sub-nodes)
}
public boolean canCreateRepeat(TreeReference repeatRef) {
Condition c = (Condition) conditionRepeatTargetIndex.get(repeatRef.genericize());
if (c != null) {
return c.evalBool(instance, new EvaluationContext(exprEvalContext, repeatRef));
} /* else check # child constraints of parent
} */
return true;
}
public void copyItemsetAnswer(QuestionDef q, TreeElement targetNode, IAnswerData data) throws InvalidReferenceException{
ItemsetBinding itemset = q.getDynamicChoices();
TreeReference targetRef = targetNode.getRef();
TreeReference destRef = itemset.getDestRef().contextualize(targetRef);
Vector<Selection> selections = null;
Vector<String> selectedValues = new Vector<String>();
if (data instanceof SelectMultiData) {
selections = (Vector<Selection>)data.getValue();
} else if (data instanceof SelectOneData) {
selections = new Vector<Selection>();
selections.addElement((Selection)data.getValue());
}
if (itemset.valueRef != null) {
for (int i = 0; i < selections.size(); i++) {
selectedValues.addElement(selections.elementAt(i).choice.getValue());
}
}
//delete existing dest nodes that are not in the answer selection
HashMap<String, TreeElement> existingValues = new HashMap<String, TreeElement>();
Vector<TreeReference> existingNodes = getInstance().expandReference(destRef);
for (int i = 0; i < existingNodes.size(); i++) {
TreeElement node = getInstance().resolveReference(existingNodes.elementAt(i));
if (itemset.valueRef != null) {
String value = itemset.getRelativeValue().evalReadable(this.getInstance(), new EvaluationContext(exprEvalContext, node.getRef()));
if (selectedValues.contains(value)) {
existingValues.put(value, node); //cache node if in selection and already exists
}
}
//delete from target
targetNode.removeChild(node);
}
//copy in nodes for new answer; preserve ordering in answer
for (int i = 0; i < selections.size(); i++) {
Selection s = selections.elementAt(i);
SelectChoice ch = s.choice;
TreeElement cachedNode = null;
if (itemset.valueRef != null) {
String value = ch.getValue();
if (existingValues.containsKey(value)) {
cachedNode = existingValues.get(value);
}
}
if (cachedNode != null) {
cachedNode.setMult(i);
targetNode.addChild(cachedNode);
} else {
getInstance().copyItemsetNode(ch.copyNode, destRef, this);
}
}
triggerTriggerables(destRef); // trigger conditions that depend on the creation of these new nodes
initializeTriggerables(destRef); // initialize conditions for the node (and sub-nodes)
//not 100% sure this will work since destRef is ambiguous as the last step, but i think it's supposed to work
}
/**
* Add a Condition to the form's Collection.
*
* @param condition
* the condition to be set
*/
public Triggerable addTriggerable(Triggerable t) {
int existingIx = triggerables.indexOf(t);
if (existingIx >= 0) {
//one node may control access to many nodes; this means many nodes effectively have the same condition
//let's identify when conditions are the same, and store and calculate it only once
//note, if the contextRef is unnecessarily deep, the condition will be evaluated more times than needed
//perhaps detect when 'identical' condition has a shorter contextRef, and use that one instead?
return (Triggerable)triggerables.elementAt(existingIx);
} else {
triggerables.addElement(t);
triggerablesInOrder = false;
Vector triggers = t.getTriggers();
for (int i = 0; i < triggers.size(); i++) {
TreeReference trigger = (TreeReference) triggers.elementAt(i);
if (!triggerIndex.containsKey(trigger)) {
triggerIndex.put(trigger, new Vector());
}
Vector triggered = (Vector) triggerIndex.get(trigger);
if (!triggered.contains(t)) {
triggered.addElement(t);
}
}
if (t instanceof Condition) {
// droos 5/14: this this might be a bug? what if we encounter
// the same condition again, but the targets
// have since changed? we'll return the original condition
// (above), and not update this index
Vector targets = t.getTargets();
for (int i = 0; i < targets.size(); i++) {
TreeReference target = (TreeReference) targets.elementAt(i);
if (instance.getTemplate(target) != null) {
conditionRepeatTargetIndex.put(target, (Condition) t);
}
}
}
return t;
}
}
public void finalizeTriggerables () {
Vector partialOrdering = new Vector();
for (int i = 0; i < triggerables.size(); i++) {
Triggerable t = (Triggerable)triggerables.elementAt(i);
Vector deps = new Vector();
if (t.canCascade()) {
for (int j = 0; j < t.getTargets().size(); j++) {
TreeReference target = (TreeReference)t.getTargets().elementAt(j);
Vector triggered = (Vector)triggerIndex.get(target);
if (triggered != null) {
for (int k = 0; k < triggered.size(); k++) {
Triggerable u = (Triggerable)triggered.elementAt(k);
if (!deps.contains(u))
deps.addElement(u);
}
}
}
}
for (int j = 0; j < deps.size(); j++) {
Triggerable u = (Triggerable)deps.elementAt(j);
Triggerable[] edge = {t, u};
partialOrdering.addElement(edge);
}
}
Vector vertices = new Vector();
for (int i = 0; i < triggerables.size(); i++)
vertices.addElement(triggerables.elementAt(i));
triggerables.removeAllElements();
while (vertices.size() > 0) {
//determine root nodes
Vector roots = new Vector();
for (int i = 0; i < vertices.size(); i++) {
roots.addElement(vertices.elementAt(i));
}
for (int i = 0; i < partialOrdering.size(); i++) {
Triggerable[] edge = (Triggerable[])partialOrdering.elementAt(i);
roots.removeElement(edge[1]);
}
//if no root nodes while graph still has nodes, graph has cycles
if (roots.size() == 0) {
throw new RuntimeException("Cannot create partial ordering of triggerables due to dependency cycle. Why wasn't this caught during parsing?");
}
//remove root nodes and edges originating from them
for (int i = 0; i < roots.size(); i++) {
Triggerable root = (Triggerable)roots.elementAt(i);
triggerables.addElement(root);
vertices.removeElement(root);
}
for (int i = partialOrdering.size() - 1; i >= 0; i--) {
Triggerable[] edge = (Triggerable[])partialOrdering.elementAt(i);
if (roots.contains(edge[0]))
partialOrdering.removeElementAt(i);
}
}
triggerablesInOrder = true;
}
public void initializeTriggerables() {
initializeTriggerables(TreeReference.rootRef());
}
/**
* Walks the current set of conditions, and evaluates each of them with the
* current context.
*/
private void initializeTriggerables(TreeReference rootRef) {
TreeReference genericRoot = rootRef.genericize();
Vector applicable = new Vector();
for (int i = 0; i < triggerables.size(); i++) {
Triggerable t = (Triggerable)triggerables.elementAt(i);
for (int j = 0; j < t.getTargets().size(); j++) {
TreeReference target = (TreeReference)t.getTargets().elementAt(j);
if (genericRoot.isParentOf(target, false)) {
applicable.addElement(t);
break;
}
}
}
evaluateTriggerables(applicable, rootRef);
}
// ref: unambiguous ref of node that just changed
public void triggerTriggerables(TreeReference ref) {
// turn unambiguous ref into a generic ref
TreeReference genericRef = ref.genericize();
// get conditions triggered by this node
Vector triggered = (Vector)triggerIndex.get(genericRef);
if (triggered == null)
return;
Vector triggeredCopy = new Vector();
for (int i = 0; i < triggered.size(); i++)
triggeredCopy.addElement(triggered.elementAt(i));
evaluateTriggerables(triggeredCopy, ref);
}
private void evaluateTriggerables(Vector tv, TreeReference anchorRef) {
//add all cascaded triggerables to queue
for (int i = 0; i < tv.size(); i++) {
Triggerable t = (Triggerable)tv.elementAt(i);
if (t.canCascade()) {
for (int j = 0; j < t.getTargets().size(); j++) {
TreeReference target = (TreeReference)t.getTargets().elementAt(j);
Vector triggered = (Vector)triggerIndex.get(target);
if (triggered != null) {
for (int k = 0; k < triggered.size(); k++) {
Triggerable u = (Triggerable)triggered.elementAt(k);
if (!tv.contains(u))
tv.addElement(u);
}
}
}
}
}
//'triggerables' is topologically-ordered by dependencies, so evaluate the triggerables in 'tv'
//in the order they appear in 'triggerables'
for (int i = 0; i < triggerables.size(); i++) {
Triggerable t = (Triggerable)triggerables.elementAt(i);
if (tv.contains(t)) {
evaluateTriggerable(t, anchorRef);
}
}
}
private void evaluateTriggerable(Triggerable t, TreeReference anchorRef) {
TreeReference contextRef = t.contextRef.contextualize(anchorRef);
Vector v = instance.expandReference(contextRef);
for (int i = 0; i < v.size(); i++) {
EvaluationContext ec = new EvaluationContext(exprEvalContext, (TreeReference)v.elementAt(i));
t.apply(instance, ec, this);
}
}
public boolean evaluateConstraint(TreeReference ref, IAnswerData data) {
if (data == null)
return true;
TreeElement node = instance.resolveReference(ref);
Constraint c = node.getConstraint();
if (c == null)
return true;
EvaluationContext ec = new EvaluationContext(exprEvalContext, ref);
ec.isConstraint = true;
ec.candidateValue = data;
return c.constraint.eval(instance, ec);
}
/**
* @param ec
* The new Evaluation Context
*/
public void setEvaluationContext(EvaluationContext ec) {
initEvalContext(ec);
this.exprEvalContext = ec;
}
private void initEvalContext(EvaluationContext ec) {
if (!ec.getFunctionHandlers().containsKey("jr:itext")) {
final FormDef f = this;
ec.addFunctionHandler(new IFunctionHandler() {
public String getName() {
return "jr:itext";
}
public Object eval(Object[] args) {
String textID = (String) args[0];
try {
String text = f.getLocalizer().getText(textID);
return text == null ? "[itext:" + textID + "]" : text;
} catch (NoSuchElementException nsee) {
return "[nolocale]";
}
}
public Vector getPrototypes() {
Class[] proto = { String.class };
Vector v = new Vector();
v.addElement(proto);
return v;
}
public boolean rawArgs() {
return false;
}
public boolean realTime() {
return false;
}
});
}
}
public String fillTemplateString(String template, TreeReference contextRef) {
HashMap args = new HashMap();
int depth = 0;
Vector outstandingArgs = Localizer.getArgs(template);
while (outstandingArgs.size() > 0) {
for (int i = 0; i < outstandingArgs.size(); i++) {
String argName = (String) outstandingArgs.elementAt(i);
if (!args.containsKey(argName)) {
int ix = -1;
try {
ix = Integer.parseInt(argName);
} catch (NumberFormatException nfe) {
System.err.println("Warning: expect arguments to be numeric [" + argName + "]");
}
if (ix < 0 || ix >= outputFragments.size())
continue;
IConditionExpr expr = (IConditionExpr) outputFragments.elementAt(ix);
String value = expr.evalReadable(this.getInstance(), new EvaluationContext(exprEvalContext, contextRef));
args.put(argName, value);
}
}
template = Localizer.processArguments(template, args);
outstandingArgs = Localizer.getArgs(template);
depth++;
if (depth >= TEMPLATING_RECURSION_LIMIT) {
throw new RuntimeException("Dependency cycle in <output>s; recursion limit exceeded!!");
}
}
return template;
}
/**
* Identify the itemset in the backend model, and create a set of SelectChoice
* objects at the current question reference based on the data in the model.
*
* Will modify the itemset binding to contain the relevant choices
*
* @param itemset The binding for an itemset, where the choices will be populated
* @param curQRef A reference to the current question's element, which will be
* used to determine the values to be chosen from.
*/
public void populateDynamicChoices (ItemsetBinding itemset, TreeReference curQRef) {
Vector<SelectChoice> choices = new Vector<SelectChoice>();
Vector<TreeReference> matches = itemset.nodesetExpr.evalNodeset(this.getInstance(),
new EvaluationContext(exprEvalContext, itemset.contextRef.contextualize(curQRef)));
for (int i = 0; i < matches.size(); i++) {
TreeReference item = matches.elementAt(i);
String label = itemset.labelExpr.evalReadable(this.getInstance(), new EvaluationContext(exprEvalContext, item));
String value = null;
TreeElement copyNode = null;
if (itemset.copyMode) {
copyNode = this.getInstance().resolveReference(itemset.copyRef.contextualize(item));
}
if (itemset.valueRef != null) {
value = itemset.valueExpr.evalReadable(this.getInstance(), new EvaluationContext(exprEvalContext, item));
}
// SelectChoice choice = new SelectChoice(labelID,labelInnerText,value,isLocalizable);
SelectChoice choice = new SelectChoice(label, value != null ? value : "dynamic:" + i, itemset.labelIsItext);
choice.setIndex(i);
if (itemset.copyMode)
choice.copyNode = copyNode;
choices.addElement(choice);
}
if (choices.size() == 0) {
throw new RuntimeException("dynamic select question has no choices! [" + itemset.nodesetRef + "]");
}
itemset.setChoices(choices, this.getLocalizer());
}
/**
* @return the preloads
*/
public QuestionPreloader getPreloader() {
return preloader;
}
/**
* @param preloads
* the preloads to set
*/
public void setPreloader(QuestionPreloader preloads) {
this.preloader = preloads;
}
/*
* (non-Javadoc)
*
* @see
* org.javarosa.core.model.utils.Localizable#localeChanged(java.lang.String,
* org.javarosa.core.model.utils.Localizer)
*/
public void localeChanged(String locale, Localizer localizer) {
for (Enumeration e = children.elements(); e.hasMoreElements();) {
((IFormElement) e.nextElement()).localeChanged(locale, localizer);
}
}
public String toString() {
return getTitle();
}
/**
* Preload the Data Model with the preload values that are enumerated in the
* data bindings.
*/
public void preloadInstance(TreeElement node) {
// if (node.isLeaf()) {
IAnswerData preload = null;
if (node.getPreloadHandler() != null) {
preload = preloader.getQuestionPreload(node.getPreloadHandler(),
node.getPreloadParams());
}
if (preload != null) { // what if we want to wipe out a value in the
// instance?
node.setAnswer(preload);
}
// } else {
if (!node.isLeaf()) {
for (int i = 0; i < node.getNumChildren(); i++) {
TreeElement child = node.getChildAt(i);
if (child.getMult() != TreeReference.INDEX_TEMPLATE)
// don't preload templates; new repeats are preloaded as they're created
preloadInstance(child);
}
}
// }
}
public boolean postProcessInstance() {
return postProcessInstance(instance.getRoot());
}
/**
* Iterate over the form's data bindings, and evaluate all post procesing
* calls.
*
* @return true if the instance was modified in any way. false otherwise.
*/
private boolean postProcessInstance(TreeElement node) {
// we might have issues with ordering, for example, a handler that writes a value to a node,
// and a handler that does something external with the node. if both handlers are bound to the
// same node, we need to make sure the one that alters the node executes first. deal with that later.
// can we even bind multiple handlers to the same node currently?
// also have issues with conditions. it is hard to detect what conditions are affected by the actions
// of the post-processor. normally, it wouldn't matter because we only post-process when we are exiting
// the form, so the result of any triggered conditions is irrelevant. however, if we save a form in the
// interim, post-processing occurs, and then we continue to edit the form. it seems like having conditions
// dependent on data written during post-processing is a bad practice anyway, and maybe we shouldn't support it.
if (node.isLeaf()) {
if (node.getPreloadHandler() != null) {
return preloader.questionPostProcess(node, node.getPreloadHandler(), node.getPreloadParams());
} else {
return false;
}
} else {
boolean instanceModified = false;
for (int i = 0; i < node.getNumChildren(); i++) {
TreeElement child = node.getChildAt(i);
if (child.getMult() != TreeReference.INDEX_TEMPLATE)
instanceModified |= postProcessInstance(child);
}
return instanceModified;
}
}
/**
* Reads the form definition object from the supplied stream.
*
* Requires that the instance has been set to a prototype of the instance that
* should be used for deserialization.
*
* @param dis
* - the stream to read from.
* @throws IOException
* @throws InstantiationException
* @throws IllegalAccessException
*/
public void readExternal(DataInputStream dis, PrototypeFactory pf) throws IOException, DeserializationException {
setID(ExtUtil.readInt(dis));
setName(ExtUtil.nullIfEmpty(ExtUtil.readString(dis)));
setTitle((String) ExtUtil.read(dis, new ExtWrapNullable(String.class), pf));
setChildren((Vector) ExtUtil.read(dis, new ExtWrapListPoly(), pf));
setInstance((FormInstance) ExtUtil.read(dis, FormInstance.class, pf));
setLocalizer((Localizer) ExtUtil.read(dis, new ExtWrapNullable(Localizer.class), pf));
Vector vcond = (Vector) ExtUtil.read(dis, new ExtWrapList(Condition.class), pf);
for (Enumeration e = vcond.elements(); e.hasMoreElements(); )
addTriggerable((Condition) e.nextElement());
Vector vcalc = (Vector) ExtUtil.read(dis, new ExtWrapList(Recalculate.class), pf);
for (Enumeration e = vcalc.elements(); e.hasMoreElements();)
addTriggerable((Recalculate) e.nextElement());
finalizeTriggerables();
outputFragments = (Vector) ExtUtil.read(dis, new ExtWrapListPoly(), pf);
}
/**
* meant to be called after deserialization and initialization of handlers
*
* @param newInstance
* true if the form is to be used for a new entry interaction,
* false if it is using an existing IDataModel
*/
public void initialize(boolean newInstance) {
if (newInstance) {// only preload new forms (we may have to revisit
// this)
preloadInstance(instance.getRoot());
}
initializeTriggerables();
if (getLocalizer() != null && getLocalizer().getLocale() == null) {
getLocalizer().setToDefault();
}
}
/**
* Writes the form definition object to the supplied stream.
*
* @param dos
* - the stream to write to.
* @throws IOException
*/
public void writeExternal(DataOutputStream dos) throws IOException {
ExtUtil.writeNumeric(dos, getID());
ExtUtil.writeString(dos, ExtUtil.emptyIfNull(getName()));
ExtUtil.write(dos, new ExtWrapNullable(getTitle()));
ExtUtil.write(dos, new ExtWrapListPoly(getChildren()));
ExtUtil.write(dos, instance);
ExtUtil.write(dos, new ExtWrapNullable(localizer));
Vector conditions = new Vector();
Vector recalcs = new Vector();
for (int i = 0; i < triggerables.size(); i++) {
Triggerable t = (Triggerable) triggerables.elementAt(i);
if (t instanceof Condition) {
conditions.addElement(t);
} else if (t instanceof Recalculate) {
recalcs.addElement(t);
}
}
ExtUtil.write(dos, new ExtWrapList(conditions));
ExtUtil.write(dos, new ExtWrapList(recalcs));
ExtUtil.write(dos, new ExtWrapListPoly(outputFragments));
}
public void collapseIndex(FormIndex index, Vector indexes, Vector multiplicities, Vector elements) {
if (!index.isInForm()) {
return;
}
IFormElement element = this;
while (index != null) {
int i = index.getLocalIndex();
element = element.getChild(i);
indexes.addElement(new Integer(i));
multiplicities.addElement(new Integer(index.getInstanceIndex() == -1 ? 0 : index.getInstanceIndex()));
elements.addElement(element);
index = index.getNextLevel();
}
}
public FormIndex buildIndex(Vector indexes, Vector multiplicities, Vector elements) {
FormIndex cur = null;
Vector curMultiplicities = new Vector();
for(int j = 0; j < multiplicities.size() ; ++j) {
curMultiplicities.addElement(multiplicities.elementAt(j));
}
Vector curElements = new Vector();
for(int j = 0; j < elements.size() ; ++j) {
curElements.addElement(elements.elementAt(j));
}
for (int i = indexes.size() - 1; i >= 0; i--) {
int ix = ((Integer) indexes.elementAt(i)).intValue();
int mult = ((Integer) multiplicities.elementAt(i)).intValue();
//TODO: ... No words. Just fix it.
TreeReference ref = (TreeReference)((XPathReference)((IFormElement)elements.elementAt(i)).getBind()).getReference();
if (!(elements.elementAt(i) instanceof GroupDef && ((GroupDef) elements.elementAt(i)).getRepeat())) {
mult = -1;
}
cur = new FormIndex(cur, ix, mult,getChildInstanceRef(curElements,curMultiplicities));
curMultiplicities.removeElementAt(curMultiplicities.size() - 1);
curElements.removeElementAt(curElements.size() - 1);
}
return cur;
}
public FormIndex incrementIndex(FormIndex index) {
Vector indexes = new Vector();
Vector multiplicities = new Vector();
Vector elements = new Vector();
if (index.isEndOfFormIndex()) {
return index;
} else if (index.isBeginningOfFormIndex()) {
if (children == null || children.size() == 0) {
return FormIndex.createEndOfFormIndex();
}
} else {
collapseIndex(index, indexes, multiplicities, elements);
}
incrementHelper(indexes, multiplicities, elements);
if (indexes.size() == 0) {
return FormIndex.createEndOfFormIndex();
} else {
return buildIndex(indexes, multiplicities, elements);
}
}
private void incrementHelper(Vector indexes, Vector multiplicities, Vector elements) {
int i = indexes.size() - 1;
boolean exitRepeat = false;
if (i == -1 || elements.elementAt(i) instanceof GroupDef) {
// current index is group or repeat or the top-level form
boolean descend = true;
if (i >= 0) {
// find out whether we're on a repeat, and if so, whether the
// specified instance actually exists
GroupDef group = (GroupDef) elements.elementAt(i);
if (group.getRepeat()) {
if (instance.resolveReference(getChildInstanceRef(elements, multiplicities)) == null) {
descend = false; // repeat instance does not exist; do
// not descend into it
exitRepeat = true;
}
}
}
if (descend) {
indexes.addElement(new Integer(0));
multiplicities.addElement(new Integer(0));
elements.addElement((i == -1 ? this : (IFormElement) elements.elementAt(i)).getChild(0));
return;
}
}
while (i >= 0) {
// if on repeat, increment to next repeat EXCEPT when we're on a
// repeat instance that does not exist and was not created
// (repeat-not-existing can only happen at lowest level; exitRepeat
// will be true)
if (!exitRepeat && elements.elementAt(i) instanceof GroupDef && ((GroupDef) elements.elementAt(i)).getRepeat()) {
multiplicities.setElementAt(new Integer(((Integer) multiplicities.elementAt(i)).intValue() + 1), i);
return;
}
IFormElement parent = (i == 0 ? this : (IFormElement) elements.elementAt(i - 1));
int curIndex = ((Integer) indexes.elementAt(i)).intValue();
// increment to the next element on the current level
if (curIndex + 1 >= parent.getChildren().size()) {
// at the end of the current level; move up one level and start
// over
indexes.removeElementAt(i);
multiplicities.removeElementAt(i);
elements.removeElementAt(i);
i--;
exitRepeat = false;
} else {
indexes.setElementAt(new Integer(curIndex + 1), i);
multiplicities.setElementAt(new Integer(0), i);
elements.setElementAt(parent.getChild(curIndex + 1), i);
return;
}
}
}
public FormIndex decrementIndex(FormIndex index) {
Vector indexes = new Vector();
Vector multiplicities = new Vector();
Vector elements = new Vector();
if (index.isBeginningOfFormIndex()) {
return index;
} else if (index.isEndOfFormIndex()) {
if (children == null || children.size() == 0) {
return FormIndex.createBeginningOfFormIndex();
}
} else {
collapseIndex(index, indexes, multiplicities, elements);
}
decrementHelper(indexes, multiplicities, elements);
if (indexes.size() == 0) {
return FormIndex.createBeginningOfFormIndex();
} else {
return buildIndex(indexes, multiplicities, elements);
}
}
private void decrementHelper(Vector indexes, Vector multiplicities, Vector elements) {
int i = indexes.size() - 1;
if (i != -1) {
int curIndex = ((Integer) indexes.elementAt(i)).intValue();
int curMult = ((Integer) multiplicities.elementAt(i)).intValue();
if (curMult > 0) {
// set node to previous repetition of current element
multiplicities.setElementAt(new Integer(curMult - 1), i);
} else if (curIndex > 0) {
// set node to previous element
indexes.setElementAt(new Integer(curIndex - 1), i);
multiplicities.setElementAt(new Integer(0), i);
elements.setElementAt((i == 0 ? this : (IFormElement) elements.elementAt(i - 1)).getChild(curIndex - 1), i);
if (setRepeatNextMultiplicity(elements, multiplicities))
return;
} else {
// at absolute beginning of current level; index to parent
indexes.removeElementAt(i);
multiplicities.removeElementAt(i);
elements.removeElementAt(i);
return;
}
}
IFormElement element = (i < 0 ? this : (IFormElement) elements.elementAt(i));
while (!(element instanceof QuestionDef)) {
int subIndex = element.getChildren().size() - 1;
element = element.getChild(subIndex);
indexes.addElement(new Integer(subIndex));
multiplicities.addElement(new Integer(0));
elements.addElement(element);
if (setRepeatNextMultiplicity(elements, multiplicities))
return;
}
}
private boolean setRepeatNextMultiplicity(Vector elements, Vector multiplicities) {
// find out if node is repeatable
TreeReference nodeRef = getChildInstanceRef(elements, multiplicities);
TreeElement node = instance.resolveReference(nodeRef);
if (node == null || node.repeatable) { // node == null if there are no
// instances of the repeat
int mult;
if (node == null) {
mult = 0; // no repeats; next is 0
} else {
String name = node.getName();
TreeElement parentNode = instance.resolveReference(nodeRef.getParentRef());
mult = parentNode.getChildMultiplicity(name);
}
multiplicities.setElementAt(new Integer(mult), multiplicities.size() - 1);
return true;
} else {
return false;
}
}
/*
* (non-Javadoc)
*
* @see org.javarosa.core.model.IFormElement#getDeepChildCount()
*/
public int getDeepChildCount() {
int total = 0;
Enumeration e = children.elements();
while (e.hasMoreElements()) {
total += ((IFormElement) e.nextElement()).getDeepChildCount();
}
return total;
}
public void registerStateObserver(FormElementStateListener qsl) {
// NO. (Or at least not yet).
}
public void unregisterStateObserver(FormElementStateListener qsl) {
// NO. (Or at least not yet).
}
public Vector getChildren() {
return children;
}
public void setChildren(Vector children) {
this.children = (children == null ? new Vector() : children);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getID() {
return id;
}
public void setID(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Localizer getLocalizer() {
return localizer;
}
public FormInstance getInstance() {
return instance;
}
public void setInstance(FormInstance instance) {
if (instance.getFormId() != -1 && getID() != instance.getFormId()) {
System.err.println("Warning: assigning incompatible instance (type " + instance.getFormId() + ") to a formdef (type " + getID() + ")");
}
instance.setFormId(getID());
this.instance = instance;
attachControlsToInstanceData();
}
public Vector getOutputFragments() {
return outputFragments;
}
public void setOutputFragments(Vector outputFragments) {
this.outputFragments = outputFragments;
}
public HashMap getMetaData() {
HashMap metadata = new HashMap();
String[] fields = getMetaDataFields();
for (int i = 0; i < fields.length; i++) {
try{
metadata.put(fields[i], getMetaData(fields[i]));
}catch(NullPointerException npe){
if(getMetaData(fields[i])==null){
System.out.println("ERROR! XFORM MUST HAVE A NAME!");
npe.printStackTrace();
}
}
}
return metadata;
}
public Object getMetaData(String fieldName) {
if (fieldName.equals("DESCRIPTOR")) {
return name;
} if (fieldName.equals("XMLNS")) {
return ExtUtil.emptyIfNull(instance.schema);
} else {
throw new IllegalArgumentException();
}
}
public String[] getMetaDataFields() {
return new String[] {"DESCRIPTOR","XMLNS"};
}
/**
* Link a deserialized instance back up with its parent FormDef. this allows select/select1 questions to be
* internationalizable in chatterbox, and (if using CHOICE_INDEX mode) allows the instance to be serialized
* to xml
*/
public void attachControlsToInstanceData () {
attachControlsToInstanceData(instance.getRoot());
}
private void attachControlsToInstanceData (TreeElement node) {
for (int i = 0; i < node.getNumChildren(); i++) {
attachControlsToInstanceData(node.getChildAt(i));
}
IAnswerData val = node.getValue();
Vector selections = null;
if (val instanceof SelectOneData) {
selections = new Vector();
selections.addElement(val.getValue());
} else if (val instanceof SelectMultiData) {
selections = (Vector)val.getValue();
}
if (selections != null) {
QuestionDef q = findQuestionByRef(node.getRef(), this);
if (q == null) {
throw new RuntimeException("FormDef.attachControlsToInstanceData: can't find question to link");
}
if (q.getDynamicChoices() != null) {
//droos: i think we should do something like initializing the itemset here, so that default answers
//can be linked to the selectchoices. however, there are complications. for example, the itemset might
//not be ready to be evaluated at form initialization; it may require certain questions to be answered
//first. e.g., if we evaluate an itemset and it has no choices, the xform engine will throw an error
//itemset TODO
}
for (int i = 0; i < selections.size(); i++) {
Selection s = (Selection)selections.elementAt(i);
s.attachChoice(q);
}
}
}
public static QuestionDef findQuestionByRef (TreeReference ref, IFormElement fe) {
if (fe instanceof FormDef) {
ref = ref.genericize();
}
if (fe instanceof QuestionDef) {
QuestionDef q = (QuestionDef)fe;
TreeReference bind = FormInstance.unpackReference(q.getBind());
return (ref.equals(bind) ? q : null);
} else {
for (int i = 0; i < fe.getChildren().size(); i++) {
QuestionDef ret = findQuestionByRef(ref, fe.getChild(i));
if (ret != null)
return ret;
}
return null;
}
}
/**
* Appearance isn't a valid attribute for form, but this method must be included
* as a result of conforming to the IFormElement interface.
*/
public String getAppearanceAttr () {
throw new RuntimeException("This method call is not relevant for FormDefs getAppearanceAttr ()");
}
/**
* Appearance isn't a valid attribute for form, but this method must be included
* as a result of conforming to the IFormElement interface.
*/
public void setAppearanceAttr (String appearanceAttr) {
throw new RuntimeException("This method call is not relevant for FormDefs setAppearanceAttr()");
}
/**
* Not applicable here.
*/
public String getLabelInnerText() {
return null;
}
/**
* Not applicable
*/
public String getTextID() {
return null;
}
/**
* Not applicable
*/
public void setTextID(String textID) {
throw new RuntimeException("This method call is not relevant for FormDefs [setTextID()]");
}
}