org.openxdata.sharedlib.client.view.FormRunnerView.java Source code

Java tutorial

Introduction

Here is the source code for org.openxdata.sharedlib.client.view.FormRunnerView.java

Source

package org.openxdata.sharedlib.client.view;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Vector;

import org.openxdata.sharedlib.client.OpenXdataConstants;
import org.openxdata.sharedlib.client.controller.QuestionChangeListener;
import org.openxdata.sharedlib.client.controller.SubmitListener;
import org.openxdata.sharedlib.client.locale.FormsConstants;
import org.openxdata.sharedlib.client.model.Calculation;
import org.openxdata.sharedlib.client.model.DynamicOptionDef;
import org.openxdata.sharedlib.client.model.FormDef;
import org.openxdata.sharedlib.client.model.OptionDef;
import org.openxdata.sharedlib.client.model.PageDef;
import org.openxdata.sharedlib.client.model.QuestionDef;
import org.openxdata.sharedlib.client.model.RepeatQtnsDef;
import org.openxdata.sharedlib.client.model.SkipRule;
import org.openxdata.sharedlib.client.model.ValidationRule;
import org.openxdata.sharedlib.client.util.FormUtil;
import org.openxdata.sharedlib.client.widget.CheckBoxWidget;
import org.openxdata.sharedlib.client.widget.DatePickerEx;
import org.openxdata.sharedlib.client.widget.DatePickerWidget;
import org.openxdata.sharedlib.client.widget.DateTimeWidget;
import org.openxdata.sharedlib.client.widget.EditListener;
import org.openxdata.sharedlib.client.widget.ListBoxWidget;
import org.openxdata.sharedlib.client.widget.RadioButtonWidget;
import org.openxdata.sharedlib.client.widget.RuntimeGroupWidget;
import org.openxdata.sharedlib.client.widget.RuntimeWidgetWrapper;
import org.openxdata.sharedlib.client.widget.TimeWidget;
import org.openxdata.sharedlib.client.widget.WidgetEx;
import org.openxdata.sharedlib.client.xforms.XformBuilder;
import org.openxdata.sharedlib.client.xforms.XformParser;
import org.openxdata.sharedlib.client.xforms.XformUtil;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DecoratedTabPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.xml.client.Element;
import com.google.gwt.xml.client.Node;
import com.google.gwt.xml.client.NodeList;
import com.google.gwt.xml.client.XMLParser;
import org.openxdata.sharedlib.client.model.QuestionType;

/**
 * This widgets is responsible for displaying a form at run time and lets user fill data 
 * as it controls the running of skip and validation rules.
 * 
 *  www.openxdata.org - Licensed as written in license.txt and original sources of this file and its authors are found in sources.txt.
 *
 */
public class FormRunnerView extends Composite
        implements SelectionHandler<Integer>, EditListener, QuestionChangeListener {

    private final char FIELD_SEPARATOR = '|'; //TODO These may need to be changed.
    private final char RECORD_SEPARATOR = '$';

    private FormsConstants constants = GWT.create(FormsConstants.class);

    public interface Images extends ClientBundle {
        ImageResource error();

        ImageResource loading();
    }

    /** Images reference where we get the error icon for widgets with errors. */
    public static final Images images = (Images) GWT.create(Images.class);

    /** The tabs where we lay various page panels. */
    protected DecoratedTabPanel tabs = new DecoratedTabPanel();

    /** The currently selected tab index. */
    protected int selectedTabIndex;

    /** The currently selected tab panel. */
    protected AbsolutePanel selectedPanel;

    /** The height of the currently selected tab panel. */
    protected String sHeight = "100%";

    /** Reference to the form definition. */
    protected FormDef formDef;

    /** Listener to form submit events. */
    protected SubmitListener submitListener;

    /** A map of parent binding text and one of the widgets that reference it. */
    protected HashMap<String, RuntimeWidgetWrapper> parentBindingWidgetMap;

    /** 
     * The first invalid widget. This is used when we validate more than one widget in a group
     * and at the end of the list we want to set focus to the first widget that we found invalid.
     */
    protected RuntimeWidgetWrapper firstInvalidWidget;

    /** 
     * Used when we are used as an embedded widget in another GWT application
     * and the user wants to control the space in which to embed us.
     */
    protected int embeddedHeightOffset = 0;

    /** A map of a questions and its list of Label widgets.
     *  Labels in this list are only those which have portions of its text
     *  that are to be replaced with answers from some questions
     *  which are the keys in this map.
     */
    protected HashMap<QuestionDef, List<Label>> labelMap;

    /** A map of a label widget and its text.
     *  Labels in this map are only those which have portions of its text
     *  that are to be replaced with answers from some questions.
     */
    protected HashMap<Label, String> labelText;

    /** A map of a label widget and its template text (eg ${/newform1/name}$) which is to be 
     * replaced by answers to some questions. Labels in this map are only those which have 
     * portions of its text that are to be replaced with answers from some questions.
     */
    protected HashMap<Label, String> labelReplaceText;

    /** A map of a question and its list of CheckBox widgets. */
    protected HashMap<QuestionDef, List<CheckBox>> checkBoxGroupMap;

    /** 
     * A map where the key widget's value change requires a list of other widgets to
     * run their validation rules. E.g key qtn: Total No of kids born and dependent qtn: how many are male?
     * Then the validation rule could be like no of male kids should be less than total
     * no of kids born. So whenever the total no changes, no of males should be revalidated.
     */
    protected HashMap<RuntimeWidgetWrapper, List<RuntimeWidgetWrapper>> validationWidgetsMap;

    /** A map of a questions and its list of RuntimeWidgetWrapper widgets.
     *  Widgets in this list are only those which should have values
     *  that are to be computed using answers from some questions
     *  which are the keys in this map.
     */
    protected HashMap<QuestionDef, List<RuntimeWidgetWrapper>> calcWidgetMap;

    /**
     * A map of filtered single select dynamic questions and their corresponding 
     * non label widgets. Only questions of single select dynamic which have the
     * widget filter property set are put in this list
     */
    protected HashMap<QuestionDef, RuntimeWidgetWrapper> filtDynOptWidgetMap;

    /** 
     * A reference to the login dialog to be used when user leaves the form open for long and the
     * server session times out. Because we do not want them lose their changes, when they try to
     * submit data, we use this dialog to logon the server.
     */
    private static LoginDialog loginDlg = new LoginDialog();

    /**
     * Reference to this very view. We need this static reference because the javascript login
     * callback method is static and will need this view to submit the data on successful login.
     */
    private static FormRunnerView formRunnerView;

    private List<RuntimeWidgetWrapper> externalSourceWidgets;
    private int externalSourceWidgetIndex = 0;

    /**
     * Constructs an instance of the form runner.
     *
     * @param images reference to images used in the application.
     */
    public FormRunnerView() {

        FormUtil.maximizeWidget(tabs);

        initWidget(tabs);
        tabs.addSelectionHandler(this);

        //This is needed for IE which does not seem to set the height properly.
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            public void execute() {
                setHeight(getHeight());
            }
        });

        formRunnerView = this;
    }

    /**
     * Reloads the form runner view
     * 
     * @param formDef the form definition to load.
     * @param layoutXml the form widget layout xml.
     * @param externalSourceWidgets a list of widgets which get their data from sources 
     *         external to the xform.
     */
    public void loadForm(FormDef formDef, String layoutXml, String javaScriptSrc,
            List<RuntimeWidgetWrapper> externalSourceWidgets, boolean previewMode) {
        FormUtil.initialize();

        if (previewMode) {
            //Here we must be in preview mode where we need to create a new copy of the formdef
            //such that we don't set preview values as default formdef values.
            if (formDef == null)
                this.formDef = null;
            else //set the document xml which we shall need for updating the model with question answers
                this.formDef = XformParser.copyFormDef(formDef);
        } else
            this.formDef = formDef;

        tabs.clear();
        if (formDef == null || layoutXml == null || layoutXml.trim().length() == 0) {
            addNewTab(constants.page() + "1");
            return;
        }

        loadLayout(layoutXml, externalSourceWidgets, getCalcQtnMappings(this.formDef));
        isValid(true);
        moveToFirstWidget();

        com.google.gwt.dom.client.Element script = DOM.getElementById("formtools_javascript");
        if (script != null)
            script.removeFromParent();

        if (javaScriptSrc != null) {
            Document document = Document.get();
            script = document.createElement("script");
            script.setAttribute("type", "text/javascript");
            script.setAttribute("id", "formtools_javascript");
            script.appendChild(document.createTextNode(javaScriptSrc));
            document.getElementsByTagName("head").getItem(0).appendChild(script);
        }

        this.externalSourceWidgets = externalSourceWidgets;
        externalSourceWidgetIndex = 0;
        if (externalSourceWidgets != null && externalSourceWidgets.size() > 0
                && FormUtil.getExternalSourceUrlSuffix() != null)
            fillExternalSourceWidget(externalSourceWidgets.get(externalSourceWidgetIndex++), null);
    }

    /**
     * Sets focus to the widget with the smallest tab index.
     */
    public void moveToFirstWidget() {
        moveToNextWidget(-1);
    }

    /**
     * Sets focus to the focusable widget whose tab index is next to a given index.
     * 
     * @param index the given tab index.
     */
    protected void moveToNextWidget(int index) {

        //If we have reached end of tab order, just wrap around to the first widget.
        if (index > selectedPanel.getWidgetCount() - 2)
            index = 0;

        while (++index < selectedPanel.getWidgetCount()) {
            if (((RuntimeWidgetWrapper) selectedPanel.getWidget(index)).setFocus())
                break;
        }
    }

    /**
     * Loads widgets in a given layout xml and populates a list of widgets whose source of
     * allowed option is external to the xform.
     * 
     * @param xml the layout xml.
     * @param externalSourceWidgets the list of external source widgets.
     */
    public void loadLayout(String xml, List<RuntimeWidgetWrapper> externalSourceWidgets,
            HashMap<QuestionDef, List<QuestionDef>> calcQtnMappings) {
        tabs.clear();

        if (formDef != null)
            formDef.clearChangeListeners();

        parentBindingWidgetMap = new HashMap<String, RuntimeWidgetWrapper>();
        labelMap = new HashMap<QuestionDef, List<Label>>();
        labelText = new HashMap<Label, String>();
        labelReplaceText = new HashMap<Label, String>();
        checkBoxGroupMap = new HashMap<QuestionDef, List<CheckBox>>();
        validationWidgetsMap = new HashMap<RuntimeWidgetWrapper, List<RuntimeWidgetWrapper>>();
        calcWidgetMap = new HashMap<QuestionDef, List<RuntimeWidgetWrapper>>();
        filtDynOptWidgetMap = new HashMap<QuestionDef, RuntimeWidgetWrapper>();

        //A list of widgets with validation rules.
        List<RuntimeWidgetWrapper> validationRuleWidgets = new ArrayList<RuntimeWidgetWrapper>();

        //A map of parent validation widgets keyed by their QuestionDef.
        //A parent validation widget is one whose QuestionDef is contained in any condition
        //of any validation rule.
        HashMap<QuestionDef, RuntimeWidgetWrapper> qtnParentValidationWidgetMap = new HashMap<QuestionDef, RuntimeWidgetWrapper>();

        //A list of questions for parent validation widgets.
        List<QuestionDef> parentValidationWidgetQtns = new ArrayList<QuestionDef>();

        initValidationWidgetsMap(parentValidationWidgetQtns);

        com.google.gwt.xml.client.Document doc = XMLParser.parse(xml);
        Element root = doc.getDocumentElement();
        NodeList pages = root.getChildNodes();
        for (int i = 0; i < pages.getLength(); i++) {
            if (pages.item(i).getNodeType() != Node.ELEMENT_NODE)
                continue;
            Element node = (Element) pages.item(i);

            addNewTab(node.getAttribute("Text"));
            WidgetEx.loadLabelProperties(node, new RuntimeWidgetWrapper(tabs.getTabBar(), images.error(), this));

            setWidth(node.getAttribute(WidgetEx.WIDGET_PROPERTY_WIDTH));
            setHeight(node.getAttribute(WidgetEx.WIDGET_PROPERTY_HEIGHT));
            setBackgroundColor(node.getAttribute(WidgetEx.WIDGET_PROPERTY_BACKGROUND_COLOR));

            loadPage(node.getChildNodes(), externalSourceWidgets, parentValidationWidgetQtns, validationRuleWidgets,
                    qtnParentValidationWidgetMap, calcQtnMappings);
        }

        setValidationWidgetsMap(validationRuleWidgets, qtnParentValidationWidgetMap);

        if (formDef != null) {
            fireSkipRules();
            doCalculations();
        }

        //For those widgets whose answers are now set, lets load their option lists.
        updateDynamicOptions();

        if (tabs.getWidgetCount() > 0)
            tabs.selectTab(0);
    }

    /**
     * Sets up the main panel widget.
     */
    protected void initPanel() {
        AbsolutePanel panel = new AbsolutePanel();
        FormUtil.maximizeWidget(panel);
        selectedPanel = panel;

        //This is needed for IE
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            public void execute() {
                setHeight(getHeight());
            }
        });
    }

    /**
     * Adds a new tab or page.
     * 
     * @param name the name of the page to add.
     */
    protected void addNewTab(String name) {
        initPanel();
        if (name == null)
            name = constants.page() + tabs.getWidgetCount();
        if (name.indexOf("</") > 0)
            name = name.substring(name.indexOf(">") + 1, name.indexOf("</"));
        tabs.add(selectedPanel, name);
        selectedPanel.setHeight(sHeight);
        selectedTabIndex = tabs.getWidgetCount() - 1;
        tabs.selectTab(selectedTabIndex);

        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            public void execute() {
                setHeight(getHeight());
            }
        });
    }

    /**
     * Loads widgets contained in a list of nodes for a page.
     * 
     * @param nodes the node list
     * @param externalSourceWidgets
     * @param validationQtns
     * @param validationWidgets
     * @param qtnWidgetMap a map keyed by the QuestionDef object for each loaded widget.
     */
    protected void loadPage(NodeList nodes, List<RuntimeWidgetWrapper> externalSourceWidgets,
            List<QuestionDef> validationQtns, List<RuntimeWidgetWrapper> validationWidgets,
            HashMap<QuestionDef, RuntimeWidgetWrapper> qtnWidgetMap,
            HashMap<QuestionDef, List<QuestionDef>> calcQtnMappings) {
        HashMap<Integer, RuntimeWidgetWrapper> widgets = new HashMap<Integer, RuntimeWidgetWrapper>();
        int maxTabIndex = 0;

        for (int i = 0; i < nodes.getLength(); i++) {
            if (nodes.item(i).getNodeType() != Node.ELEMENT_NODE)
                continue;
            try {
                Element node = (Element) nodes.item(i);
                int index = loadWidget(node, widgets, externalSourceWidgets, validationQtns, validationWidgets,
                        qtnWidgetMap, calcQtnMappings);
                if (index > maxTabIndex)
                    maxTabIndex = index;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        //We are adding widgets to the panel according to the tab index.
        for (int index = 0; index <= maxTabIndex; index++) {
            RuntimeWidgetWrapper widget = widgets.get(new Integer(index));
            if (widget != null) {
                selectedPanel.add(widget);
                FormUtil.setWidgetPosition(widget, widget.getLeft(), widget.getTop());
            }
        }
    }

    protected int loadWidget(Element node, HashMap<Integer, RuntimeWidgetWrapper> widgets,
            List<RuntimeWidgetWrapper> externalSourceWidgets, List<QuestionDef> validationQtns,
            List<RuntimeWidgetWrapper> validationWidgets, HashMap<QuestionDef, RuntimeWidgetWrapper> qtnWidgetMap,
            HashMap<QuestionDef, List<QuestionDef>> calcQtnMappings) {
        RuntimeWidgetWrapper parentWrapper = null;

        String left = node.getAttribute(WidgetEx.WIDGET_PROPERTY_LEFT);
        String top = node.getAttribute(WidgetEx.WIDGET_PROPERTY_TOP);
        String s = node.getAttribute(WidgetEx.WIDGET_PROPERTY_WIDGETTYPE);
        int tabIndex = (node.getAttribute(WidgetEx.WIDGET_PROPERTY_TABINDEX) != null
                ? Integer.parseInt(node.getAttribute(WidgetEx.WIDGET_PROPERTY_TABINDEX))
                : 0);

        QuestionDef questionDef = null;
        String binding = node.getAttribute(WidgetEx.WIDGET_PROPERTY_BINDING);
        String parentBinding = node.getAttribute(WidgetEx.WIDGET_PROPERTY_PARENTBINDING);
        if (binding != null && binding.trim().length() > 0) {
            questionDef = formDef.getQuestion(binding);
            if (questionDef != null)
                questionDef.setAnswer(questionDef.getDefaultValue()); //Just incase we are refreshing and had already set the answer
        }

        RuntimeWidgetWrapper wrapper = null;
        boolean wrapperSet = false;
        Widget widget = null;
        if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_RADIOBUTTON)) {
            widget = new RadioButtonWidget(parentBinding, node.getAttribute(WidgetEx.WIDGET_PROPERTY_TEXT));

            if (parentBindingWidgetMap.get(parentBinding) == null)
                wrapperSet = true;

            parentWrapper = getParentBindingWrapper(widget, parentBinding);
            ((RadioButton) widget).setTabIndex(tabIndex);

            if (wrapperSet) {
                wrapper = parentWrapper;
                questionDef = formDef.getQuestion(parentBinding);
            }
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_CHECKBOX)) {
            widget = new CheckBoxWidget(node.getAttribute(WidgetEx.WIDGET_PROPERTY_TEXT));
            if (parentBindingWidgetMap.get(parentBinding) == null)
                wrapperSet = true;

            parentWrapper = getParentBindingWrapper(widget, parentBinding);
            ((CheckBox) widget).setTabIndex(tabIndex);

            String defaultValue = parentWrapper.getQuestionDef().getDefaultValue();
            if (defaultValue != null && defaultValue.contains(binding))
                ((CheckBox) widget).setValue(true);

            if (wrapperSet) {
                wrapper = parentWrapper;
                questionDef = formDef.getQuestion(parentBinding);
            }

        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_BUTTON)) {
            widget = new Button(node.getAttribute(WidgetEx.WIDGET_PROPERTY_TEXT));
            ((Button) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_LISTBOX)) {
            widget = new ListBoxWidget(false);
            ((ListBox) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_TEXTAREA)) {
            widget = new TextArea();
            ((TextArea) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_DATEPICKER)) {
            widget = new DatePickerWidget();
            ((DatePickerEx) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_DATETIME)) {
            widget = new DateTimeWidget();
            ((DateTimeWidget) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_TIME)) {
            widget = new TimeWidget();
            ((TimeWidget) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_TEXTBOX)) {
            widget = new TextBox();
            if (questionDef != null && (questionDef.getDataType() == QuestionType.NUMERIC
                    || questionDef.getDataType() == QuestionType.DECIMAL))
                FormUtil.allowNumericOnly((TextBox) widget, questionDef.getDataType() == QuestionType.DECIMAL);
            ((TextBox) widget).setTabIndex(tabIndex);
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_LABEL)) {
            String text = node.getAttribute(WidgetEx.WIDGET_PROPERTY_TEXT);
            widget = new Label(text);

            int pos1 = text.indexOf("${");
            int pos2 = text.indexOf("}$");
            if (pos1 > -1 && pos2 > -1 && (pos2 > pos1)) {
                String varname = text.substring(pos1 + 2, pos2);
                labelText.put((Label) widget, text);
                labelReplaceText.put((Label) widget, "${" + varname + "}$");

                ((Label) widget).setText(text.replace("${" + varname + "}$", ""));
                if (varname.startsWith("/" + formDef.getBinding() + "/"))
                    varname = varname.substring(("/" + formDef.getBinding() + "/").length(), varname.length());

                QuestionDef qtnDef = formDef.getQuestion(varname);
                List<Label> labels = labelMap.get(qtnDef);
                if (labels == null) {
                    labels = new ArrayList<Label>();
                    labelMap.put(qtnDef, labels);
                }
                labels.add((Label) widget);
            }
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_GROUPBOX)
                || s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_REPEATSECTION)) {
            RepeatQtnsDef repeatQtnsDef = null;
            if (questionDef != null)
                repeatQtnsDef = questionDef.getRepeatQtnsDef();

            boolean repeated = false;
            String value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_REPEATED);
            if (value != null && value.trim().length() > 0)
                repeated = (value.equals(WidgetEx.REPEATED_TRUE_VALUE));

            widget = new RuntimeGroupWidget(images, formDef, repeatQtnsDef, this, repeated);
            ((RuntimeGroupWidget) widget).loadWidgets(formDef, node.getChildNodes(), externalSourceWidgets,
                    calcQtnMappings, calcWidgetMap, filtDynOptWidgetMap);
            copyLabelMap(((RuntimeGroupWidget) widget).getLabelMap());
            copyLabelText(((RuntimeGroupWidget) widget).getLabelText());
            copyLabelReplaceText(((RuntimeGroupWidget) widget).getLabelReplaceText());
            copyCheckBoxGroupMap(((RuntimeGroupWidget) widget).getCheckBoxGroupMap());
            copyCalcWidgetMap(((RuntimeGroupWidget) widget).getCalcWidgetMap());
            copyFiltDynOptWidgetMap(((RuntimeGroupWidget) widget).getFiltDynOptWidgetMap());
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_IMAGE)) {
            widget = new Image();
            String xpath = binding;
            if (!xpath.startsWith(formDef.getBinding()))
                xpath = "/" + formDef.getBinding() + "/" + binding;
            ((Image) widget).setUrl(
                    URL.encode(FormUtil.getMultimediaUrl() + "?formId=" + formDef.getId() + "&xpath=" + xpath));
        } else if (s.equalsIgnoreCase(WidgetEx.WIDGET_TYPE_VIDEO_AUDIO) && questionDef != null) {
            String answer = questionDef.getAnswer();
            if (answer != null && answer.trim().length() != 0) {
                widget = new HTML();
                if (binding != null && binding.trim().length() > 0) {
                    String xpath = binding;
                    if (!xpath.startsWith(formDef.getBinding()))
                        xpath = "/" + formDef.getBinding() + "/" + binding;

                    String extension = "";
                    String contentType = "&contentType=video/3gpp";
                    if (questionDef.getDataType() == QuestionType.AUDIO)
                        contentType = "&contentType=audio/3gpp";

                    contentType += "&name=" + questionDef.getBinding() + ".3gp";

                    ((HTML) widget).setHTML("<a href="
                            + URL.encode(FormUtil.getMultimediaUrl() + extension + "?formId=" + formDef.getId()
                                    + "&xpath=" + xpath + contentType)
                            + ">" + node.getAttribute(WidgetEx.WIDGET_PROPERTY_TEXT) + "</a>");
                }
            }
        } else
            return tabIndex;

        if (!wrapperSet) {
            wrapper = new RuntimeWidgetWrapper(widget, images.error(), this);

            if (parentWrapper != null) { //Check box or radio button
                if (!parentWrapper.getQuestionDef().isVisible())
                    wrapper.setVisible(false);
                if (!parentWrapper.getQuestionDef().isEnabled())
                    wrapper.setEnabled(false);
                if (parentWrapper.getQuestionDef().isLocked())
                    wrapper.setLocked(true);
            }
        }

        boolean loadWidget = true;

        String value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_HELPTEXT);
        if (value != null && value.trim().length() > 0)
            wrapper.setTitle(value);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_WIDTH);
        if (value != null && value.trim().length() > 0)
            wrapper.setWidth(value);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_HEIGHT);
        if (value != null && value.trim().length() > 0)
            wrapper.setHeight(value);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_EXTERNALSOURCE);
        if (value != null && value.trim().length() > 0)
            wrapper.setExternalSource(value);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_DISPLAYFIELD);
        if (value != null && value.trim().length() > 0)
            wrapper.setDisplayField(value);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_FILTERFIELD);
        if (value != null && value.trim().length() > 0)
            wrapper.setFilterField(value);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_ID);
        if (value != null && value.trim().length() > 0)
            wrapper.setId(value);

        if (questionDef != null) {
            if (questionDef.getDataType() == QuestionType.LIST_EXCLUSIVE_DYNAMIC) {
                questionDef.setOptions(null); //may have been set by the preview
                if (wrapper.getFilterField() != null && wrapper.getFilterField().trim().length() > 0)
                    filtDynOptWidgetMap.put(questionDef, wrapper);
            }

            wrapper.setQuestionDef(questionDef, false);
            ValidationRule validationRule = formDef.getValidationRule(questionDef);
            wrapper.setValidationRule(validationRule);
            if (validationRule != null && questionDef.getDataType() == QuestionType.REPEAT)
                questionDef.setAnswer("0");

            if (validationQtns.contains(questionDef) && isValidationWidget(wrapper)) {
                validationWidgetsMap.put(wrapper, new ArrayList<RuntimeWidgetWrapper>());
                qtnWidgetMap.put(questionDef, wrapper);
            }

            if (validationRule != null && isValidationWidget(wrapper))
                validationWidgets.add(wrapper);
        }

        if (parentBinding != null)
            wrapper.setParentBinding(parentBinding);

        if (binding != null)
            wrapper.setBinding(binding);

        if (parentWrapper != null)
            parentWrapper.addChildWidget(wrapper);

        value = node.getAttribute(WidgetEx.WIDGET_PROPERTY_VALUEFIELD);
        if (value != null && value.trim().length() > 0) {
            wrapper.setValueField(value);

            if (externalSourceWidgets != null && wrapper.getExternalSource() != null
                    && wrapper.getDisplayField() != null && (wrapper.getWrappedWidget() instanceof TextBox
                            || wrapper.getWrappedWidget() instanceof ListBox)
                    && questionDef != null) {

                if (!(questionDef.getDataType() == QuestionType.LIST_EXCLUSIVE
                        || questionDef.getDataType() == QuestionType.LIST_MULTIPLE)) {
                    questionDef.setDataType(QuestionType.LIST_EXCLUSIVE);
                }

                externalSourceWidgets.add(wrapper);
                loadWidget = false;

                wrapper.addSuggestBoxChangeEvent();
            }
        }

        if (loadWidget)
            wrapper.loadQuestion();

        wrapper.setExternalSourceDisplayValue();

        WidgetEx.loadLabelProperties(node, wrapper);

        wrapper.setTabIndex(tabIndex);

        if (tabIndex > 0)
            widgets.put(new Integer(tabIndex), wrapper);
        else
            selectedPanel.add(wrapper);

        FormUtil.setWidgetPosition(wrapper, left, top);

        if (widget instanceof Button && binding != null) {
            if (binding.equals("submit")) {
                ((Button) widget).addClickHandler(new ClickHandler() {
                    public void onClick(ClickEvent event) {
                        submit();
                    }
                });
            } else if (binding.equals("cancel")) {
                ((Button) widget).addClickHandler(new ClickHandler() {
                    public void onClick(ClickEvent event) {
                        onCancel();
                    }
                });
            }
        }

        if (wrapper.isEditable() && questionDef != null)
            updateCalcWidgetMapping(wrapper, calcQtnMappings, calcWidgetMap);

        return tabIndex;
    }

    /**
     * Check if a widget can have a validation rule.
     * 
     * @param wrapper the widget
     * @return true if it can have, else false.
     */
    private boolean isValidationWidget(RuntimeWidgetWrapper wrapper) {
        return !((wrapper.getWrappedWidget() instanceof Label) || (wrapper.getWrappedWidget() instanceof HTML)
                || (wrapper.getWrappedWidget() instanceof Hyperlink)
                || (wrapper.getWrappedWidget() instanceof Button));
    }

    /**
     * Gets a widget that has a given parent binding value as that of a given widget.
     * 
     * @param widget the given widget.
     * @param parentBinding the parent binding value.
     * @return the widget that has the same parent binding.
     */
    protected RuntimeWidgetWrapper getParentBindingWrapper(Widget widget, String parentBinding) {
        RuntimeWidgetWrapper parentWrapper = parentBindingWidgetMap.get(parentBinding);
        if (parentWrapper == null) {
            QuestionDef qtn = formDef.getQuestion(parentBinding);
            if (qtn != null) {
                parentWrapper = new RuntimeWidgetWrapper(widget, images.error(), this);
                parentWrapper.setQuestionDef(qtn, true);
                parentBindingWidgetMap.put(parentBinding, parentWrapper);
                qtn.addChangeListener(this);
                List<CheckBox> list = new ArrayList<CheckBox>();
                list.add((CheckBox) widget);
                checkBoxGroupMap.put(qtn, list);
            }
        } else
            checkBoxGroupMap.get(parentWrapper.getQuestionDef()).add((CheckBox) widget);

        return parentWrapper;
    }

    /**
     * Submits form data to the server.
     */
    protected void submit() {

        //Before calling the submit listener, we first check if the user is authenticated
        //The authentication will call us back and tell us whether to proceed with the
        //data submission or display the login dialog box.
        if (formDef != null)
            FormUtil.isAuthenticated();
    }

    /**
     * Called when one clicks the submit button on the form to submit form data.
     */
    public void onSubmit() {
        submit();
    }

    /**
     * Called when one clicks the cancel button on the form, meaning that they have
     * changed their mind about submitting the form.
     */
    public void onCancel() {
        if (Window.confirm(constants.cancelFormPrompt()))
            submitListener.onCancel();
    }

    /**
     * Does the actual submission of form data to the submit listener.
     */
    private void submitData() {
        if (formDef.getDoc() == null)
            XformBuilder.fromFormDef2Xform(formDef);

        saveValues();

        if (!isValid(false))
            return;

        String xml = XformUtil.getInstanceDataDoc(formDef.getDoc()).toString();
        xml = FormUtil.formatXml(xml);
        submitListener.onSubmit(xml);
    }

    /**
     * Checks if form data has validation errors.
     * 
     * @param fireValueChanged set to true to fire the value changed event, else false.
     * @return true if form has no validation errors, else false.
     */
    protected boolean isValid(boolean fireValueChanged) {
        boolean valid = true;
        int pageNo = -1;
        firstInvalidWidget = null;
        for (int index = 0; index < tabs.getWidgetCount(); index++) {
            if (!isValid((AbsolutePanel) tabs.getWidget(index), fireValueChanged)) {
                valid = false;
                if (pageNo == -1)
                    pageNo = index;
            }
        }

        if (!valid) {
            tabs.selectTab(pageNo);
            assert (firstInvalidWidget != null);
            firstInvalidWidget.setFocus();
        }
        return valid;
    }

    /**
     * Checks if widgets on a panel or page have validation errors.
     * 
     * @param panel the panel whose widgets to check.
     * @param fireValueChanged set to true to fire the value changed event, else false.
     * @return true if the panel widgets have no errors, else false.
     */
    private boolean isValid(AbsolutePanel panel, boolean fireValueChanged) {
        boolean valid = true;
        for (int index = 0; index < panel.getWidgetCount(); index++) {
            RuntimeWidgetWrapper widget = (RuntimeWidgetWrapper) panel.getWidget(index);
            if (!widget.isValid()) {
                valid = false;
                if (firstInvalidWidget == null && widget.isFocusable())
                    firstInvalidWidget = widget.getInvalidWidget();
            }

            if (fireValueChanged && widget.getQuestionDef() != null)
                onValueChanged(widget);
        }
        return valid;
    }

    /**
     * Saves form answers from widgets, on all pages,
     *  to their corresponding model QuestionDef objects.
     */
    protected void saveValues() {
        for (int index = 0; index < tabs.getWidgetCount(); index++)
            savePageValues((AbsolutePanel) tabs.getWidget(index));
    }

    /**
     * Saves page answers from widgets to their corresponding model QuestionDef objects.
     * 
     * @param panel the panel whose widgets to save.
     */
    protected void savePageValues(AbsolutePanel panel) {
        for (int index = 0; index < panel.getWidgetCount(); index++)
            ((RuntimeWidgetWrapper) panel.getWidget(index)).saveValue(formDef);
    }

    /**
     * @see org.openxdata.sharedlib.client.widget.EditListener#onValueChanged(org.openxdata.sharedlib.client.widget.RuntimeWidgetWrapper)
     */
    public void onValueChanged(RuntimeWidgetWrapper widget) {
        if (!widget.isEditable())
            return;

        onValueChanged(widget.getQuestionDef());
        fireParentQtnValidationRules(widget);
    }

    /**
     * Called when the value or answer of a question changes.
     * 
     * @param questionDef the question definition object.
     */
    private void onValueChanged(QuestionDef questionDef) {
        fireSkipRules();
        updateDynamicOptions(questionDef);

        List<Label> labels = labelMap.get(questionDef);
        if (labels != null) {
            for (Widget widget : labels) {
                String replaceText = questionDef.getAnswer();
                if (replaceText == null)
                    replaceText = "";
                ((Label) widget).setText(labelText.get(widget).replace(labelReplaceText.get(widget), replaceText));
            }
        }

        List<RuntimeWidgetWrapper> widgets = calcWidgetMap.get(questionDef);
        if (widgets != null) {

            for (RuntimeWidgetWrapper widget : widgets) {
                Calculation calculation = formDef.getCalculation(widget.getQuestionDef());
                String calcExpression = replaceCalcExpression(calculation.getCalculateExpression(),
                        widget.getQuestionDef()); //calcExpression.replace(binding, answer);

                QuestionType type = widget.getQuestionDef().getDataType();
                String answer = calcExpression;

                if (calculation.getCalculateExpression().trim().indexOf(' ') > 0) {
                    if (type == QuestionType.NUMERIC) {
                        try {
                            answer = "" + FormUtil.evaluateIntExpression(calcExpression);
                        } catch (Exception ex) {
                            answer = FormUtil.evaluateStringExpression(calcExpression);
                        }
                    } else if (type == QuestionType.DECIMAL) {
                        try {
                            answer = "" + FormUtil.evaluateDoubleExpression(calcExpression);
                        } catch (Exception ex) {
                            answer = FormUtil.evaluateStringExpression(calcExpression);
                        }
                    } else {
                        try {
                            answer = FormUtil.evaluateStringExpression(calcExpression);
                        } catch (Exception ex) {
                            answer = "" + FormUtil.evaluateDoubleExpression(calcExpression);
                        }
                    }
                }

                widget.setAnswer(answer);
                widget.isValid(); // TODO May need to fire change event instead
                onValueChanged(widget);
            }
        }

        List<CheckBox> list = checkBoxGroupMap.get(questionDef);
        if (list != null) {
            for (CheckBox checkBox : list)
                ((RuntimeWidgetWrapper) checkBox.getParent().getParent()).isValid();
        }
    }

    /**
     * @see com.google.gwt.event.logical.shared.SelectionHandler#onSelection(SelectionEvent)
     */
    public void onSelection(SelectionEvent<Integer> event) {
        selectedTabIndex = event.getSelectedItem();
        selectedPanel = (AbsolutePanel) tabs.getWidget(selectedTabIndex);
        moveToFirstWidget();
    }

    /**
     * Sets the listener to form submission events.
     * 
     * @param submitListener reference to the listener.
     */
    public void setSubmitListener(SubmitListener submitListener) {
        this.submitListener = submitListener;
    }

    /**
     * Checks whether the form is being previewed.
     * 
     * @return true if yes, else false.
     */
    public boolean isPreviewing() {
        return tabs.getWidgetCount() > 0;
    }

    /**
     * @see org.openxdata.sharedlib.client.widget.EditListener#onMoveToNextWidget(com.google.gwt.user.client.ui.Widget)
     */
    public void onMoveToNextWidget(Widget widget) {
        if (widget.getParent().getParent() != null) {
            if (widget.getParent().getParent() instanceof RuntimeGroupWidget) {
                //Non repeating widgets in a group box
                if (((RuntimeGroupWidget) widget.getParent().getParent()).onMoveToNextWidget(widget))
                    return;
                else
                    widget = widget.getParent().getParent().getParent().getParent();
            } else if (widget.getParent().getParent().getParent() instanceof RuntimeGroupWidget) {
                //Repeating widgets.
                if (((RuntimeGroupWidget) widget.getParent().getParent().getParent()).onMoveToNextWidget(widget))
                    return;
                else
                    widget = widget.getParent().getParent().getParent().getParent().getParent();
            }
        }

        int index = selectedPanel.getWidgetIndex(widget);
        moveToNextWidget(index);
    }

    /**
     * @see org.openxdata.sharedlib.client.widget.EditListener#onMoveToPrevWidget(com.google.gwt.user.client.ui.Widget)
     */
    public void onMoveToPrevWidget(Widget widget) {
        boolean moved = false;

        if (widget.getParent().getParent() instanceof RuntimeGroupWidget) {
            if (((RuntimeGroupWidget) widget.getParent().getParent()).onMoveToPrevWidget(widget))
                return;
            else
                widget = widget.getParent().getParent().getParent().getParent();
        }

        int index = selectedPanel.getWidgetIndex(widget);
        while (--index > 0) {
            if (((RuntimeWidgetWrapper) selectedPanel.getWidget(index)).setFocus()) {
                moved = true;
                break;
            }
        }

        if (!moved && this.selectedTabIndex > 0)
            tabs.selectTab(--selectedTabIndex);
    }

    /**
     * Checks all skip logic and does the appropriate action for the affected widgets.
     */
    protected void fireSkipRules() {
        Vector<SkipRule> rules = formDef.getSkipRules();
        if (rules != null) {
            for (int i = 0; i < rules.size(); i++) {
                SkipRule rule = (SkipRule) rules.elementAt(i);
                rule.fire(formDef);
            }
        }
    }

    protected void doCalculations() {
        Vector<Calculation> calculations = formDef.getCalculations();
        if (calculations != null) {
            for (int i = 0; i < calculations.size(); i++) {

            }
        }
    }

    /**
     * Clears the window.
     */
    public void clear() {
        tabs.clear();
    }

    /**
     * This function is called when one switches between forms in the tree view
     */
    public void setFormDef(FormDef formDef) {
        if (this.formDef != formDef) {
            tabs.clear();
            addNewTab(constants.page() + "1");
        }
    }

    /**
     * Sets the height offset to be used for the form when embedded in a GWT application.
     * 
     * @param offset
     */
    public void setEmbeddedHeightOffset(int offset) {
        embeddedHeightOffset = offset;
    }

    /**
     * Updates the list of options for a question whose list of options depends
     * on the select option for a given question.
     * 
     * @param questionDef the question whose selected value determines the list of
     *                  allowed options for another question.
     */
    private void updateDynamicOptions(QuestionDef questionDef) {
        QuestionType type = questionDef.getDataType();
        if (!(type == QuestionType.LIST_EXCLUSIVE || type == QuestionType.LIST_EXCLUSIVE_DYNAMIC))
            return;

        //Get the dynamic option definition where this question is the parent.
        DynamicOptionDef dynamicOptionDef = formDef.getDynamicOptions(questionDef.getId());
        if (dynamicOptionDef == null)
            return;

        //Get the question definition for the child/dependent question.
        final QuestionDef childQuestionDef = formDef.getQuestion(dynamicOptionDef.getQuestionId());
        if (childQuestionDef == null)
            return;

        childQuestionDef.setOptionList(null);

        //Get the selected option/answer in the parent question
        OptionDef optionDef = questionDef.getOptionWithValue(questionDef.getAnswer());
        if (optionDef != null) {
            RuntimeWidgetWrapper widget = filtDynOptWidgetMap.get(childQuestionDef);
            if (widget == null) {
                //Set the now possible answers for the child question as per the parent's selected value.
                List<OptionDef> optionList = dynamicOptionDef.getOptionList(optionDef.getId());
                childQuestionDef.setOptionList(optionList);
            } else {
                //TODO Need to show progress window.
                //widget values come from an external filtered source.
                fillExternalSourceWidget(widget, questionDef.getAnswer());
                return;
            }
        }

        //do it recursively until when no more dependent questions.
        onValueChanged(childQuestionDef);
    }

    /**
     * Updates dynamic selection lists in all pages of the form to their values as determined
     * by the selected option of their parent questions.
     */
    private void updateDynamicOptions() {
        if (formDef.getPages() == null)
            return;

        for (byte i = 0; i < formDef.getPages().size(); i++) {
            PageDef pageDef = (PageDef) formDef.getPages().elementAt(i);
            for (byte j = 0; j < pageDef.getQuestions().size(); j++)
                updateDynamicOptions((QuestionDef) pageDef.getQuestions().elementAt(j));
        }
    }

    /**
     * Copies from a given label map to our class level one.
     * 
     * @param labelMap the label map to copy from.
     */
    private void copyLabelMap(HashMap<QuestionDef, List<Label>> labelMap) {
        Iterator<Entry<QuestionDef, List<Label>>> iterator = labelMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<QuestionDef, List<Label>> entry = iterator.next();
            this.labelMap.put(entry.getKey(), entry.getValue());
        }
    }

    private void copyCalcWidgetMap(HashMap<QuestionDef, List<RuntimeWidgetWrapper>> calcWidgetMap) {
        Iterator<Entry<QuestionDef, List<RuntimeWidgetWrapper>>> iterator = calcWidgetMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<QuestionDef, List<RuntimeWidgetWrapper>> entry = iterator.next();
            this.calcWidgetMap.put(entry.getKey(), entry.getValue());
        }
    }

    private void copyFiltDynOptWidgetMap(HashMap<QuestionDef, RuntimeWidgetWrapper> filtDynOptWidgetMap) {
        Iterator<Entry<QuestionDef, RuntimeWidgetWrapper>> iterator = filtDynOptWidgetMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<QuestionDef, RuntimeWidgetWrapper> entry = iterator.next();
            this.filtDynOptWidgetMap.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Copies from a given label text map to our class level one.
     * 
     * @param labelText the label text map to copy from.
     */
    private void copyLabelText(HashMap<Label, String> labelText) {
        Iterator<Entry<Label, String>> iterator = labelText.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<Label, String> entry = iterator.next();
            this.labelText.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Copies from a given label replace text map to our class level one.
     * 
     * @param labelReplaceText the label replace text map to copy from.
     */
    private void copyLabelReplaceText(HashMap<Label, String> labelReplaceText) {
        Iterator<Entry<Label, String>> iterator = labelReplaceText.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<Label, String> entry = iterator.next();
            this.labelReplaceText.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Copies from a given check box group map to our class level one.
     * 
     * @param labelMap the check box group map to copy from.
     */
    private void copyCheckBoxGroupMap(HashMap<QuestionDef, List<CheckBox>> labelMap) {
        Iterator<Entry<QuestionDef, List<CheckBox>>> iterator = labelMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<QuestionDef, List<CheckBox>> entry = iterator.next();
            this.checkBoxGroupMap.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Gets the background color of the selected page.
     * 
     * @return the background color.
     */
    public String getBackgroundColor() {
        if (selectedPanel == null)
            return "";
        return DOM.getStyleAttribute(selectedPanel.getElement(), "backgroundColor");
    }

    /**
     * Gets the widget of the selected page.
     * 
     * @return the width in pixels.
     */
    public String getWidth() {
        if (selectedPanel == null)
            return "";
        return DOM.getStyleAttribute(selectedPanel.getElement(), "width");
    }

    /**
     * Gets the height of the selected page.
     * 
     * @return the height in pixes.
     */
    public String getHeight() {
        if (selectedPanel == null)
            return "";
        return DOM.getStyleAttribute(selectedPanel.getElement(), "height");
    }

    /**
     * Sets the background color of the selected page.
     * 
     * @param backgroundColor the background color.
     */
    public void setBackgroundColor(String backgroundColor) {
        try {
            if (selectedPanel != null)
                DOM.setStyleAttribute(selectedPanel.getElement(), "backgroundColor", backgroundColor);
        } catch (Exception ex) {
        }
    }

    /**
     * Sets the width of the currently selected tab panel.
     * 
     * @param widget the widget to set in pixels.
     */
    public void setWidth(String width) {
        try {
            if (selectedPanel != null)
                DOM.setStyleAttribute(selectedPanel.getElement(), "width", width);
        } catch (Exception ex) {
        }
    }

    /**
     * Sets the height of the selected widget panel.
     * 
     * @param height the height to set in pixels.
     */
    public void setHeight(String height) {
        try {
            if (height != null && height.trim().length() > 0 && !height.equals("100%"))
                sHeight = height;
            if (selectedPanel != null)
                DOM.setStyleAttribute(selectedPanel.getElement(), "height", sHeight);
        } catch (Exception ex) {
        }
    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onEnabledChanged(QuestionDef, boolean)
     */
    public void onEnabledChanged(QuestionDef sender, boolean enabled) {
        List<CheckBox> list = checkBoxGroupMap.get(sender);
        if (list == null)
            return;

        for (CheckBox checkBox : list) {
            checkBox.setEnabled(enabled);
            if (!enabled)
                checkBox.setValue(false);
        }
    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onVisibleChanged(QuestionDef, boolean)
     */
    public void onVisibleChanged(QuestionDef sender, boolean visible) {
        List<CheckBox> list = checkBoxGroupMap.get(sender);
        if (list == null)
            return;

        for (CheckBox checkBox : list) {
            checkBox.setVisible(visible);
            if (!visible)
                checkBox.setValue(false);
        }
    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onRequiredChanged(QuestionDef, boolean)
     */
    public void onRequiredChanged(QuestionDef sender, boolean required) {

    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onLockedChanged(QuestionDef, boolean)
     */
    public void onLockedChanged(QuestionDef sender, boolean locked) {

    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onBindingChanged(QuestionDef, String)
     */
    public void onBindingChanged(QuestionDef sender, String newValue) {

    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onDataTypeChanged(QuestionDef, int)
     */
    public void onDataTypeChanged(QuestionDef sender, QuestionType dataType) {

    }

    /**
     * @see org.openxdata.sharedlib.client.controller.QuestionChangeListener#onOptionsChanged(QuestionDef, List)
     */
    public void onOptionsChanged(QuestionDef sender, List<OptionDef> optionList) {

    }

    /**
     * This method is called from javascript after a user has made an attempt to log on the server.
     * 
     * @param authenticated true o
     */
    public static void authenticationCallback(boolean authenticated) {

        if (authenticated) {
            loginDlg.hide();
            formRunnerView.submitData();
        } else
            loginDlg.center();
    }

    /**
     * This method is called from javascript to submit form data.
     */
    public static void submitForm() {
        formRunnerView.submit();
    }

    /**
     * @see org.openxdata.sharedlib.client.widget.EditListener#onRowAdded(org.openxdata.sharedlib.client.widget.RuntimeWidgetWrapper)
     */
    public void onRowAdded(RuntimeWidgetWrapper rptWidget, int increment) {

        //Get the current bottom y position of the repeat widget.
        int bottomYpos = getBottomYPos(rptWidget);

        for (int index = 0; index < selectedPanel.getWidgetCount(); index++) {
            RuntimeWidgetWrapper currentWidget = (RuntimeWidgetWrapper) selectedPanel.getWidget(index);
            if (currentWidget == rptWidget)
                continue;

            int top = currentWidget.getTopInt();
            if (top >= bottomYpos)
                currentWidget.setTopInt(top + increment);
        }

        DOM.setStyleAttribute(selectedPanel.getElement(), "height",
                getHeightInt() + increment + OpenXdataConstants.UNITS);

        setParentHeight(true, rptWidget, increment);
    }

    private void setParentHeight(boolean increase, RuntimeWidgetWrapper rptWidget, int change) {
        Widget parent = rptWidget.getParent();
        while (parent != null) {
            if (parent instanceof RuntimeGroupWidget) {
                RuntimeWidgetWrapper wrapper = (RuntimeWidgetWrapper) ((RuntimeGroupWidget) parent).getParent()
                        .getParent();
                int height = wrapper.getHeightInt();
                wrapper.setHeight((increase ? height + change : height - change) + OpenXdataConstants.UNITS);
            } else if (parent instanceof FormRunnerView)
                return;

            parent = parent.getParent();
        }
    }

    private int getBottomYPos(RuntimeWidgetWrapper rptWidget) {
        int bottomYpos = rptWidget.getTopInt() + rptWidget.getHeightInt();

        Widget parent = rptWidget.getParent();
        while (parent != null) {
            if (parent instanceof RuntimeGroupWidget) {
                RuntimeWidgetWrapper wrapper = (RuntimeWidgetWrapper) ((RuntimeGroupWidget) parent).getParent()
                        .getParent();
                if (selectedPanel.getWidgetIndex(wrapper) != -1) {
                    bottomYpos = wrapper.getTopInt() + wrapper.getHeightInt();
                    break;
                }
            } else if (parent instanceof FormRunnerView)
                break;

            parent = parent.getParent();
        }

        return bottomYpos;
    }

    /**
     * @see org.openxdata.sharedlib.client.widget.EditListener#onRowRemoved(org.openxdata.sharedlib.client.widget.RuntimeWidgetWrapper)
     */
    public void onRowRemoved(RuntimeWidgetWrapper rptWidget, int decrement) {

        //Get the current bottom y position of the repeat widget.
        int bottomYpos = getBottomYPos(rptWidget);

        //Move widgets which are below the bottom of the repeat widget.
        for (int index = 0; index < selectedPanel.getWidgetCount(); index++) {
            RuntimeWidgetWrapper currentWidget = (RuntimeWidgetWrapper) selectedPanel.getWidget(index);
            if (currentWidget == rptWidget)
                continue;

            int top = currentWidget.getTopInt();
            if (top >= bottomYpos)
                currentWidget.setTopInt(top - decrement);
        }

        DOM.setStyleAttribute(selectedPanel.getElement(), "height",
                getHeightInt() - decrement + OpenXdataConstants.UNITS);

        setParentHeight(false, rptWidget, decrement);
    }

    /**
     * Gets the height of the currently selected page.
     * 
     * @return the height.
     */
    private int getHeightInt() {
        return FormUtil.convertDimensionToInt(DOM.getStyleAttribute(selectedPanel.getElement(), "height"));
    }

    /**
     * Fires all validation rules that are dependent on the value of a certain widget.
     * In other wards the value in this widget is referenced in one or more conditions
     * or one or more validation rules. These are cross field validation rules. eg Total no
     * of kids should be less than those that are born as male.
     * 
     * @param widget the widget whose value change requires re firing of other rules.
     */
    private void fireParentQtnValidationRules(RuntimeWidgetWrapper widget) {
        List<RuntimeWidgetWrapper> widgets = validationWidgetsMap.get(widget);
        if (widgets == null)
            return;

        for (RuntimeWidgetWrapper wgt : widgets)
            wgt.isValid();
    }

    /**
     * 
     * @param parentValidationWidgetQtns
     */
    private void initValidationWidgetsMap(List<QuestionDef> parentValidationWidgetQtns) {
        int count = formDef.getValidationRuleCount();
        for (int index = 0; index < count; index++) {
            ValidationRule rule = formDef.getValidationRuleAt(index);
            List<QuestionDef> qtns = rule.getValueQuestions(formDef);
            for (QuestionDef questionDef : qtns) {
                if (!parentValidationWidgetQtns.contains(questionDef))
                    parentValidationWidgetQtns.add(questionDef);
            }
        }
    }

    /**
     * 
     * @param validationRuleWidgets a list of widgets with validation rules
     * @param qtnParentValidationWidgetMap
     */
    private void setValidationWidgetsMap(List<RuntimeWidgetWrapper> validationRuleWidgets,
            HashMap<QuestionDef, RuntimeWidgetWrapper> qtnParentValidationWidgetMap) {
        for (RuntimeWidgetWrapper widget : validationRuleWidgets) {
            ValidationRule rule = widget.getValidationRule();
            List<QuestionDef> qtns = rule.getValueQuestions(formDef);
            for (QuestionDef questionDef : qtns) {
                RuntimeWidgetWrapper wgt = qtnParentValidationWidgetMap.get(questionDef);
                if (wgt == null)
                    continue;

                List<RuntimeWidgetWrapper> widgets = validationWidgetsMap.get(wgt);
                if (widgets == null)
                    continue;

                widgets.add(widget);
            }
        }
    }

    /**
     * Processes global keyboard events.
     * 
     * @param event the event.
     * @return false if processed, else true.
     */
    public boolean handleKeyBoardEvent(Event event) {
        if (event.getCtrlKey() && event.getKeyCode() == 'S') {
            onSubmit();

            //Returning false such that firefox does not try to save the page.
            return false;
        }

        return true;
    }

    public static HashMap<QuestionDef, List<QuestionDef>> getCalcQtnMappings(FormDef formDef) {
        HashMap<QuestionDef, List<QuestionDef>> calcQtnMappings = new HashMap<QuestionDef, List<QuestionDef>>();

        String qtnBinding, formBinding = "/" + formDef.getBinding() + "/";
        for (int index = 0; index < formDef.getCalculationCount(); index++) {
            Calculation calculation = formDef.getCalculationAt(index);
            String expression = calculation.getCalculateExpression();

            int pos = expression.indexOf(formBinding);
            while (pos > -1) {
                int pos2 = expression.indexOf(' ', pos);
                if (pos2 > -1)
                    qtnBinding = expression.substring(pos, pos2);
                else
                    qtnBinding = expression.substring(pos);

                qtnBinding = qtnBinding.substring(formBinding.length());

                QuestionDef questionDef = formDef.getQuestion(qtnBinding);
                if (questionDef != null) {
                    List<QuestionDef> qtns = calcQtnMappings.get(questionDef);
                    if (qtns == null) {
                        qtns = new ArrayList<QuestionDef>();
                        calcQtnMappings.put(questionDef, qtns);
                    }

                    QuestionDef qtnDef = formDef.getQuestion(calculation.getQuestionId());
                    if (!qtns.contains(qtnDef))
                        qtns.add(qtnDef);
                }

                if (pos2 > -1)
                    pos = expression.indexOf(formBinding, pos2 + 1);
                else
                    break;
            }
        }

        return calcQtnMappings;
    }

    public static void updateCalcWidgetMapping(RuntimeWidgetWrapper widget,
            HashMap<QuestionDef, List<QuestionDef>> calcQtnMappings,
            HashMap<QuestionDef, List<RuntimeWidgetWrapper>> calcWidgetMap) {

        Iterator<Entry<QuestionDef, List<QuestionDef>>> iterator = calcQtnMappings.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<QuestionDef, List<QuestionDef>> entry = iterator.next();
            if (!entry.getValue().contains(widget.getQuestionDef()))
                continue;

            List<RuntimeWidgetWrapper> widgets = calcWidgetMap.get(entry.getKey());
            if (widgets == null) {
                widgets = new ArrayList<RuntimeWidgetWrapper>();
                calcWidgetMap.put(entry.getKey(), widgets);
            }

            widgets.add(widget);
        }
    }

    private String replaceCalcExpression(String calcExpression, QuestionDef questionDef) {

        String expression = calcExpression;

        String qtnBinding, formBinding = "/" + formDef.getBinding() + "/";

        int pos = expression.indexOf(formBinding);
        while (pos > -1) {
            int pos2 = expression.indexOf(' ', pos);
            if (pos2 > -1)
                qtnBinding = expression.substring(pos, pos2);
            else
                qtnBinding = expression.substring(pos);

            qtnBinding = qtnBinding.substring(formBinding.length());

            QuestionDef qtnDef = formDef.getQuestion(qtnBinding);
            if (qtnDef == null)
                return "";

            expression = expression.replace(formBinding + qtnBinding,
                    getCalcExpressionAnswer(questionDef.getDataType(), qtnDef, calcExpression));

            if (pos2 > -1)
                pos = expression.indexOf(formBinding);
            else
                break;
        }

        return expression;
    }

    private String getCalcExpressionAnswer(QuestionType type, QuestionDef questionDef, String calcExpression) {
        String answer = questionDef.getAnswer();
        if (answer == null || answer.trim().length() == 0) {
            if (type == QuestionType.NUMERIC || type == QuestionType.DECIMAL)
                answer = "0";
            else if (type == QuestionType.DATE || type == QuestionType.DATE_TIME || type == QuestionType.DATE_TIME)
                answer = "";
            else
                answer = calcExpression.trim().indexOf(' ') > 0 ? "''" : "";
        }

        QuestionType questionType = questionDef.getDataType();
        if (questionType == QuestionType.NUMERIC || questionType == QuestionType.DECIMAL)
            return answer;
        else if (answer != null && answer.trim().length() > 0 && calcExpression.trim().indexOf(' ') > 0
                && !answer.equals("''"))
            return "'" + answer + "'";

        return answer;
    }

    /**
     * Fills a widget with a list of possible values/answers from a source 
     * external to the xform. e.g database,web services, etc.
     * 
     * @param widget the widget whose possible answers to fill.
     * @param filterValue the filter value for those that are filtered.
     */
    private void fillExternalSourceWidget(final RuntimeWidgetWrapper widget, final String filterValue) {

        String url = FormUtil.getHostPageBaseURL();
        url += FormUtil.getExternalSourceUrlSuffix();
        url += WidgetEx.WIDGET_PROPERTY_EXTERNALSOURCE + "=" + widget.getExternalSource();
        url += "&" + WidgetEx.WIDGET_PROPERTY_DISPLAYFIELD + "=" + widget.getDisplayField();
        url += "&" + WidgetEx.WIDGET_PROPERTY_VALUEFIELD + "=" + widget.getValueField();

        String filterField = widget.getFilterField();
        if (filterField != null && filterField.trim().length() > 0) {

            if (filtDynOptWidgetMap.get(widget.getQuestionDef()) != null
                    && (filterValue == null || filterValue.trim().length() == 0)) {
                fillNextExternalSourceWidget();
                return;
            }

            url += "&FilterField=" + filterField + "&FilterValue=";

            if (filterValue == null)
                url += "IS NULL";
            else
                url += filterValue;
        }

        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));

        try {
            builder.sendRequest(null, new RequestCallback() {
                public void onResponseReceived(Request request, Response response) {
                    String text = response.getText();
                    if (filterValue == null) {
                        //loading for the first time
                        fillWidgetValues(text);
                        fillNextExternalSourceWidget();
                    } else {
                        //loading later after filter.
                        fillWidgetValues(text, widget);
                        onValueChanged(widget.getQuestionDef());
                    }
                }

                public void onError(Request request, Throwable exception) {
                    FormUtil.displayException(exception);

                    if (filterValue == null)
                        fillNextExternalSourceWidget();
                    else
                        onValueChanged(widget.getQuestionDef());
                }
            });
        } catch (RequestException ex) {
            FormUtil.displayException(ex);

            if (filterValue == null)
                fillNextExternalSourceWidget();
            else
                onValueChanged(widget.getQuestionDef());
        }
    }

    private void fillNextExternalSourceWidget() {
        if (externalSourceWidgetIndex < externalSourceWidgets.size())
            fillExternalSourceWidget(externalSourceWidgets.get(externalSourceWidgetIndex++), null);
        else {
            externalSourceWidgets.clear();
            externalSourceWidgetIndex = 0;
        }
    }

    private void fillWidgetValues(String text) {
        RuntimeWidgetWrapper widget = externalSourceWidgets.get(externalSourceWidgetIndex - 1);
        fillWidgetValues(text, widget);
        updateDynamicOptions(widget.getQuestionDef());
    }

    private void fillWidgetValues(String text, RuntimeWidgetWrapper widget) {
        if (text == null)
            return;

        QuestionDef questionDef = widget.getQuestionDef();
        questionDef.clearOptions();

        String displayField = null, valueField = null;
        int beginIndex = 0;
        int pos = text.indexOf(FIELD_SEPARATOR, beginIndex);
        while (pos > 0) {
            displayField = text.substring(beginIndex, pos);

            beginIndex = pos + 1;
            pos = text.indexOf(RECORD_SEPARATOR, beginIndex);
            if (pos > 0) {
                valueField = text.substring(beginIndex, pos);
                questionDef.addOption(
                        new OptionDef(questionDef.getOptionCount() + 1, displayField, valueField, questionDef));
                beginIndex = pos + 1;
                pos = text.indexOf(FIELD_SEPARATOR, beginIndex);
            } else {
                valueField = text.substring(beginIndex);
                questionDef.addOption(
                        new OptionDef(questionDef.getOptionCount() + 1, displayField, valueField, questionDef));
            }
        }

        widget.loadQuestion();
    }
}