package org.openrosa.client.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import org.openrosa.client.xforms.ItemsetBuilder;
import org.openrosa.client.util.FormUtil;
import org.openrosa.client.xforms.ItemsetUtil;
import org.openrosa.client.xforms.XformConstants;
import com.google.gwt.xml.client.Element;
/**
* This is the definition of options lists for a question whose list of options
* depends on the selected option for another question. The question whose options
* lists are defined by this object should be of type Single Select Dynamic.
*
* @author daniel
*
*/
public class DynamicOptionDef implements Serializable{
/** The question whose values are determined by or dependent on the answer of another
* (parent) question. In other wards this question must be of type Single Select Dynamic.
* This is the question we refer to as the child in this relationship.
*/
private int questionId;
/** A map between each parent option and a list of possible options for the dependent question above. */
private HashMap<Integer,List<OptionDef>> parentToChildOptions;
/**
* This is not persisted but rather used only during design mode
* to ensure that we have unique option ids.
*/
private static int nextOptionId = 1;
/** An instance child node for this dynamic selection list definition object.
* This node is the "<dynamiclist>" which is a child of <xf:instance id="question2">
* where the questionId member variable is for a question whose binding is "question2".
*/
private Element dataNode;
/**
* Creates a new instance of the dynamic option definition.
*/
public DynamicOptionDef(){
}
/**
* Creates a copy of a dynamic option definition object.
*
* @param dynamicOptionDef the dynamic option definition object to copy.
* @param questionDef the question definition object referenced by this dynamic
* option definition object.
*/
public DynamicOptionDef(DynamicOptionDef dynamicOptionDef, QuestionDef questionDef){
setQuestionId(dynamicOptionDef.getQuestionId()); //same as questionDef.getId()
parentToChildOptions = new HashMap<Integer,List<OptionDef>>();
Iterator<Entry<Integer,List<OptionDef>>> iterator = dynamicOptionDef.getParentToChildOptions().entrySet().iterator();
while(iterator.hasNext()){
Entry<Integer,List<OptionDef>> entry = iterator.next();
List<OptionDef> list = entry.getValue();
List<OptionDef> newList = new ArrayList<OptionDef>();
for(int index = 0; index < list.size(); index++)
newList.add(new OptionDef(list.get(index),questionDef));
parentToChildOptions.put(new Integer(entry.getKey()), newList);
}
}
public int getQuestionId() {
return questionId;
}
public void setQuestionId(int questionId) {
this.questionId = questionId;
}
public HashMap<Integer, List<OptionDef>> getParentToChildOptions() {
return parentToChildOptions;
}
public void setParentToChildOptions(HashMap<Integer, List<OptionDef>> parentToChildOptions) {
this.parentToChildOptions = parentToChildOptions;
}
/**
* Gets the list of options for a particular parent question selected option.
*
* @param optionId the identifier of the parent question selected option.
* @return the option list.
*/
public List<OptionDef> getOptionList(Integer optionId){
if(parentToChildOptions == null)
return null;
return parentToChildOptions.get(optionId);
}
/**
* Gets the entire list of options referenced by this dynamic option definition object.
*
* @return the options list.
*/
public List<OptionDef> getOptions(){
if(parentToChildOptions == null)
return null;
List<OptionDef> options = new ArrayList<OptionDef>();
Iterator<Entry<Integer,List<OptionDef>>> iterator = parentToChildOptions.entrySet().iterator();
while(iterator.hasNext()){
List<OptionDef> list = iterator.next().getValue();
for(int index = 0; index < list.size(); index++)
options.add(list.get(index));
}
return options;
}
/**
* Sets the list of options for a given option in a parent question.
*
* @param optionId the identifier of the parent question option.
* @param list the option list.
*/
public void setOptionList(Integer optionId, List<OptionDef> list){
if(parentToChildOptions == null)
parentToChildOptions = new HashMap<Integer, List<OptionDef>>();
parentToChildOptions.put(optionId, list);
}
/**
* Removes the list of options for a given parent question option.
*
* @param optionId the identifier of the parent question option.
*/
public void removeOptionList(Integer optionId){
parentToChildOptions.remove(optionId);
}
/**
* Gets the next available option id.
*
* @return the option id.
*/
public int getNextOptionId() {
return nextOptionId;
}
/**
* Get the next available option id with a flag which tells whether to increment the counter.
*
* @param increment set to true to increment the counter by one.
* @return the option id.
*/
public int getNextOptionId(boolean increment) {
return nextOptionId++;
}
/**
* Sets the value of the next option id.
*
* @param nextOptionId the option id value.
*/
public void setNextOptionId(int nextOptionId) {
this.nextOptionId = nextOptionId;
}
/**
* Gets the size of the parent to child option mappings.
*
* @return the numeric size.
*/
public int size(){
if(parentToChildOptions == null)
return 0;
return parentToChildOptions.size();
}
/**
* Sets the xforms document node that has data for this object.
*
* @param node the xforms node.
*/
public void setDataNode(Element node){
this.dataNode = node;
}
/**
* Updates the xforms document with the current changes in the
* dynamic option definition object.
*
* @param formDef the form definition object.
* @param parentQuestionDef the parent question whose options determine the list
* of options that this object contains.
*/
public void updateDoc(FormDef formDef, QuestionDef parentQuestionDef){
//if(parentToChildOptions == null)
// return;
if(dataNode == null)
ItemsetBuilder.fromDynamicOptionDef2Xform(formDef.getDoc(),this,parentQuestionDef,formDef);
else{
//Update the nodeset child instance id
QuestionDef questionDef = formDef.getQuestion(questionId);
if(questionDef == null || questionDef.getFirstOptionNode() == null)
return;
String nodeset = questionDef.getFirstOptionNode().getAttribute(XformConstants.ATTRIBUTE_NAME_NODESET);
if(nodeset == null)
return;
if(nodeset.trim().length() == 0 && questionDef.getFirstOptionNode() != null) //this should probably be removed.
questionDef.getFirstOptionNode().setAttribute(XformConstants.ATTRIBUTE_NAME_NODESET, "instance('"+ questionDef.getQuestionID()+"')/item[@parent=instance('"+formDef.getQuestionID()+"')/"+parentQuestionDef.getQuestionID()+"]");
String instanceId = ItemsetUtil.getChildInstanceId(nodeset); //this should probably be removed
if(!(instanceId == null || instanceId.equals(questionDef.getQuestionID()))){
nodeset = nodeset.replace("'"+instanceId+"'", "'"+questionDef.getQuestionID()+"'");
questionDef.getFirstOptionNode().setAttribute(XformConstants.ATTRIBUTE_NAME_NODESET, nodeset);
}
//Update the nodeset parent instance id
instanceId = ItemsetUtil.getParentQuestionBindId(nodeset);
if(!(instanceId == null || instanceId.equals(parentQuestionDef.getQuestionID()))){
nodeset = nodeset.replace("')/"+instanceId+"]", "')/"+parentQuestionDef.getQuestionID()+"]");
questionDef.getFirstOptionNode().setAttribute(XformConstants.ATTRIBUTE_NAME_NODESET, nodeset);
}
//update the nodeset form instance id
instanceId = ItemsetUtil.getFormInstanceId(nodeset);
if(!(instanceId == null || instanceId.equals(formDef.getQuestionID()))){
nodeset = nodeset.replace("'"+instanceId+"'", "'"+formDef.getQuestionID()+"'");
questionDef.getFirstOptionNode().setAttribute(XformConstants.ATTRIBUTE_NAME_NODESET, nodeset);
}
//Update the instance id
if(dataNode.getParentNode() != null)
((Element)dataNode.getParentNode()).setAttribute(XformConstants.ATTRIBUTE_NAME_ID, questionDef.getQuestionID());
ItemsetBuilder.updateDynamicOptionDef(formDef, parentQuestionDef, this);
}
}
/**
* Gets the xforms document node that has data for this object.
*
* @return the xforms node.
*/
public Element getDataNode(){
return dataNode;
}
/**
* Gets an option with a particular value or binding.
*
* @param value the option value or binding.
* @return the option.
*/
public OptionDef getOptionWithValue(String value){
if(parentToChildOptions == null || value == null)
return null;
Iterator<Entry<Integer,List<OptionDef>>> iterator = parentToChildOptions.entrySet().iterator();
while(iterator.hasNext()){
OptionDef optionDef = getOptionWithValue(iterator.next().getValue(),value);
if(optionDef != null)
return optionDef;
}
return null;
}
/**
* Gets an option with a particular text.
*
* @param text the option text.
* @return the option.
*/
public OptionDef getOptionWithText(String text){
if(parentToChildOptions == null || text == null)
return null;
Iterator<Entry<Integer,List<OptionDef>>> iterator = parentToChildOptions.entrySet().iterator();
while(iterator.hasNext()){
OptionDef optionDef = getOptionWithText(iterator.next().getValue(),text);
if(optionDef != null)
return optionDef;
}
return null;
}
/**
* Gets an option with a particular text from an options list.
*
* @param options the options list.
* @param text the text of the option to get.
* @return the option.
*/
private OptionDef getOptionWithText(List<OptionDef> options, String text){
List list = (List)options;
for(int i=0; i<list.size(); i++){
OptionDef optionDef = (OptionDef)list.get(i);
if(optionDef.getText().equals(text))
return optionDef;
}
return null;
}
/**
* Gets an option with a particular value or binding from an options list.
*
* @param options the options list.
* @param value the value or binding of the option to get.
* @return the option.
*/
private OptionDef getOptionWithValue(List<OptionDef> options, String value){
List list = (List)options;
for(int i=0; i<list.size(); i++){
OptionDef optionDef = (OptionDef)list.get(i);
if(optionDef.getQuestionID().equals(value))
return optionDef;
}
return null;
}
/**
* Gets an option with a particular id.
*
* @param id the option id.
* @return the option.
*/
public OptionDef getOptionWithId(int id){
if(parentToChildOptions == null)
return null;
Iterator<Entry<Integer,List<OptionDef>>> iterator = parentToChildOptions.entrySet().iterator();
while(iterator.hasNext()){
OptionDef optionDef = getOptionWithId(iterator.next().getValue(),id);
if(optionDef != null)
return optionDef;
}
return null;
}
/**
* Gets an option with a particular id from an options list.
*
* @param options the options list.
* @param id the id of the option to get.
* @return the option.
*/
private OptionDef getOptionWithId(List<OptionDef> options, int id){
List list = (List)options;
for(int i=0; i<list.size(); i++){
OptionDef optionDef = (OptionDef)list.get(i);
if(optionDef.getId() == id)
return optionDef;
}
return null;
}
/**
* Builds the locale xpath xpressions and their text values for this question.
*
* @param formDef the form definition object that this object belongs to.
* @param parentLangNode the parent language node we are building onto.
*/
public void buildLanguageNodes(FormDef formDef, Element parentLangNode){
if(parentToChildOptions == null)
return;
if(dataNode == null)
return;
QuestionDef questionDef = formDef.getQuestion(questionId);
if(questionDef == null)
return;
if(dataNode.getParentNode() == null)
return;
String xpath = FormUtil.getNodePath(dataNode.getParentNode());
String id = ((Element)dataNode.getParentNode()).getAttribute(XformConstants.ATTRIBUTE_NAME_ID);
if(id != null && id.trim().length() > 0)
xpath += "[@" + XformConstants.ATTRIBUTE_NAME_ID + "='" + questionDef.getQuestionID() + "']";
xpath += "/" + FormUtil.getNodeName(dataNode);
Iterator<Entry<Integer,List<OptionDef>>> iterator = parentToChildOptions.entrySet().iterator();
while(iterator.hasNext()){
List<OptionDef> list = iterator.next().getValue();
for(int index = 0; index < list.size(); index++)
list.get(index).buildLanguageNodes(xpath,formDef.getDoc(), parentLangNode);
}
}
/**
* Updates this dynamicOptionDef (as the main from the refresh source) with the parameter one
*
* @param dstFormDef the destination form definition object.
* @param srcFormDef the old form definition object to copy from.
* @param newDynOptionDef the new dynamic option definition object that we are building.
* @param srcDynOptionDef the old dynamic option definition object that we are copying from.
* @param newParentQtnDef the new parent question definition object.
* @param oldParentQtnDef the old parent definition object.
* @param oldChildQtnDef the old child definition object.
* @param newChildQtnDef the new child definition object.
*
*/
public void refresh(FormDef dstFormDef, FormDef srcFormDef,DynamicOptionDef newDynOptionDef, DynamicOptionDef srcDynOptionDef, QuestionDef newParentQtnDef, QuestionDef oldParentQtnDef, QuestionDef oldChildQtnDef, QuestionDef newChildQtnDef){
parentToChildOptions = new HashMap<Integer,List<OptionDef>>();
Iterator<Entry<Integer,List<OptionDef>>> iterator = srcDynOptionDef.getParentToChildOptions().entrySet().iterator();
while(iterator.hasNext()){
Entry<Integer,List<OptionDef>> entry = iterator.next();
OptionDef optionDef = oldParentQtnDef.getOption(entry.getKey());
if(optionDef == null)
continue; //how can this be????.
optionDef = newParentQtnDef.getOptionWithValue(optionDef.getQuestionID());
if(optionDef == null)
continue; //possibly option deleted.
List<OptionDef> newList = refreshList(newDynOptionDef,entry.getValue(),newParentQtnDef, oldParentQtnDef, oldChildQtnDef, newChildQtnDef);
if(newList.size() > 0)
parentToChildOptions.put(new Integer(optionDef.getId()), newList);
}
}
private List<OptionDef> refreshList(DynamicOptionDef dynamicOptionDef, List<OptionDef> list, QuestionDef newParentQtnDef, QuestionDef oldParentQtnDef, QuestionDef oldChildQtnDef, QuestionDef newChildQtnDef){
List<OptionDef> newList = new ArrayList<OptionDef>();
for(int index = 0; index < list.size(); index++){
OptionDef oldOptionDef = list.get(index);
OptionDef newOptionDef = newParentQtnDef.getOptionWithValue(oldOptionDef.getQuestionID());
if(newOptionDef == null){
//We do not want to lose options we had created before refresh.
//The user should manually delete them after a refresh, if they don't want them.
//TODO The new optiondef may need a new id
newOptionDef = new OptionDef(oldOptionDef,newChildQtnDef);
newOptionDef.setId(dynamicOptionDef.getNextOptionId());
}
else
newOptionDef.setText(oldOptionDef.getText());
newList.add(newOptionDef);
}
return newList;
}
}