<commit_before>/*
 * 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.javarosa.xform.parse;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.javarosa.core.model.Constants;
import org.javarosa.core.model.DataBinding;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.GroupDef;
import org.javarosa.core.model.IDataReference;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.QuestionDef;
import org.javarosa.core.model.condition.Condition;
import org.javarosa.core.model.condition.Constraint;
import org.javarosa.core.model.condition.Recalculate;
import org.javarosa.core.model.condition.Triggerable;
import org.javarosa.core.model.instance.DataModelTree;
import org.javarosa.core.model.instance.TreeElement;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.model.util.restorable.Restorable;
import org.javarosa.core.model.util.restorable.RestoreUtils;
import org.javarosa.core.services.locale.Localizer;
import org.javarosa.core.services.locale.TableLocaleSource;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import org.javarosa.core.util.externalizable.PrototypeFactoryDeprecated;
import org.javarosa.model.xform.XPathReference;
import org.javarosa.xform.util.IXFormBindHandler;
import org.javarosa.xform.util.XFormAnswerDataParser;
import org.javarosa.xpath.XPathConditional;
import org.javarosa.xpath.parser.XPathSyntaxException;
import org.kxml2.io.KXmlParser;
import org.kxml2.kdom.Document;
import org.kxml2.kdom.Element;
import org.kxml2.kdom.Node;
import org.xmlpull.v1.XmlPullParser;

/* droos: i think we need to start storing the contents of the <bind>s in the formdef again */

/**
 * Provides conversion from xform to epihandy object model and vice vasa.
 *
 * @author <NAME>
 * @author <NAME>
 *
 */
public class XFormParser {
	public static final String NAMESPACE_JAVAROSA = "http://openrosa.org/javarosa";

	private static Hashtable topLevelHandlers;
	private static Hashtable groupLevelHandlers;
	private static Hashtable typeMappings;
	private static PrototypeFactoryDeprecated modelPrototypes;

	/** IXFormBindHandler */
	private static Vector bindHandlers;

	/* THIS CLASS IS NOT THREAD-SAFE */
	//state variables -- not a good idea since this class is static, but that's not really a good idea either, now is it
	private static boolean modelFound;
	private static Hashtable bindingsByID;
	private static Vector bindings; //DataBinding
	private static Vector repeats; //TreeReference
	private static Vector selectOnes; //TreeReference
	private static Vector selectMultis; //TreeReference
	private static Element instanceNode; //top-level data node of the instance; saved off so it can be processed after the <bind>s
	private static String defaultNamespace;
	
	private static DataModelTree repeatTree; //pseudo-data model tree that describes the repeat structure of the instance;
										     //useful during instance processing and validation
	
	static {
		initProcessingRules();
		initTypeMappings();
		modelPrototypes = new PrototypeFactoryDeprecated();
		bindHandlers = new Vector();
	}

	/**
	 * Default Constructor
	 *
	 */
	public XFormParser(){

	}

	private static void initProcessingRules () {
		IElementHandler title = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseTitle(f, e); } };
		IElementHandler meta = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseMeta(f, e); } };
		IElementHandler model = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseModel(f, e); } };
		IElementHandler input = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_INPUT); } };
		IElementHandler select = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_SELECT_MULTI); } };
		IElementHandler select1 = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_SELECT_ONE); } };
		IElementHandler group = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseGroup((IFormElement)parent, e, f, CONTAINER_GROUP); } };
		IElementHandler repeat = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseGroup((IFormElement)parent, e, f, CONTAINER_REPEAT); } };
		IElementHandler groupLabel = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseGroupLabel(f, (GroupDef)parent, e); } };
		IElementHandler trigger = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_TRIGGER); } };
		IElementHandler upload = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseUpload((IFormElement)parent, e, f, Constants.CONTROL_UPLOAD); } };

		groupLevelHandlers = new Hashtable();
		groupLevelHandlers.put("input", input);
		groupLevelHandlers.put("select", select);
		groupLevelHandlers.put("select1", select1);
		groupLevelHandlers.put("group", group);
		groupLevelHandlers.put("repeat", repeat);
		groupLevelHandlers.put("trigger", trigger); //multi-purpose now; need to dig deeper
		groupLevelHandlers.put(Constants.XFTAG_UPLOAD, upload);

		topLevelHandlers = new Hashtable();
		for (Enumeration en = groupLevelHandlers.keys(); en.hasMoreElements(); ) {
			String key = (String)en.nextElement();
			topLevelHandlers.put(key, groupLevelHandlers.get(key));
		}
		topLevelHandlers.put("model", model);
		topLevelHandlers.put("title", title);
		topLevelHandlers.put("meta", meta);

		groupLevelHandlers.put("label", groupLabel);
	}

	private static void initTypeMappings () {
		typeMappings = new Hashtable();
		typeMappings.put("string", new Integer(Constants.DATATYPE_TEXT));               //xsd:
		typeMappings.put("integer", new Integer(Constants.DATATYPE_INTEGER));           //xsd:
		typeMappings.put("int", new Integer(Constants.DATATYPE_INTEGER));               //xsd:
		typeMappings.put("decimal", new Integer(Constants.DATATYPE_DECIMAL));           //xsd:
		typeMappings.put("double", new Integer(Constants.DATATYPE_DECIMAL));            //xsd:
		typeMappings.put("float", new Integer(Constants.DATATYPE_DECIMAL));             //xsd:
		typeMappings.put("dateTime", new Integer(Constants.DATATYPE_DATE_TIME));        //xsd:
		typeMappings.put("date", new Integer(Constants.DATATYPE_DATE));                 //xsd:
		typeMappings.put("time", new Integer(Constants.DATATYPE_TIME));                 //xsd:
		typeMappings.put("gYear", new Integer(Constants.DATATYPE_UNSUPPORTED));         //xsd:
		typeMappings.put("gMonth", new Integer(Constants.DATATYPE_UNSUPPORTED));        //xsd:
		typeMappings.put("gDay", new Integer(Constants.DATATYPE_UNSUPPORTED));          //xsd:
		typeMappings.put("gYearMonth", new Integer(Constants.DATATYPE_UNSUPPORTED));    //xsd:
		typeMappings.put("gMonthDay", new Integer(Constants.DATATYPE_UNSUPPORTED));     //xsd:
		typeMappings.put("boolean", new Integer(Constants.DATATYPE_UNSUPPORTED));       //xsd:
		typeMappings.put("base64Binary", new Integer(Constants.DATATYPE_UNSUPPORTED));  //xsd:
		typeMappings.put("hexBinary", new Integer(Constants.DATATYPE_UNSUPPORTED));     //xsd:
		typeMappings.put("anyURI", new Integer(Constants.DATATYPE_UNSUPPORTED));        //xsd:
		typeMappings.put("listItem", new Integer(Constants.DATATYPE_CHOICE));           //xforms:
		typeMappings.put("listItems", new Integer(Constants.DATATYPE_CHOICE_LIST));	    //xforms:	
		typeMappings.put("select1", new Integer(Constants.DATATYPE_CHOICE));	        //non-standard	
		typeMappings.put("select", new Integer(Constants.DATATYPE_CHOICE_LIST));        //non-standard
		typeMappings.put("geopoint", new Integer(Constants.DATATYPE_GEOPOINT));         //non-standard
		typeMappings.put("barcode", new Integer(Constants.DATATYPE_BARCODE));           //non-standard
        typeMappings.put("binary", new Integer(Constants.DATATYPE_BINARY));             //non-standard
	    
	}
	
	private static void initBindHandlers() {
		Enumeration en = bindHandlers.elements();
		while(en.hasMoreElements()) {
			IXFormBindHandler handler = (IXFormBindHandler)en.nextElement();
			handler.init();
			
		}
	}
	private static void processBindHandlers(FormDef formDef) {
		Enumeration en = bindHandlers.elements();
		while(en.hasMoreElements()) {
			IXFormBindHandler handler = (IXFormBindHandler)en.nextElement();
			handler.postProcess(formDef);
			
		}
	}

	private static void initStateVars () {
		modelFound = false;
		bindingsByID = new Hashtable();
		bindings = new Vector();
		repeats = new Vector();
		selectOnes = new Vector();
		selectMultis = new Vector();
		instanceNode = null;
		repeatTree = null;
		defaultNamespace = null;
	}

	public static FormDef getFormDef(Reader reader) {
		System.out.println("Parsing form...");
		
		Document doc = getXMLDocument(reader);
		try {
			reader.close();
		} catch (IOException e) {
			System.out.println("Error closing reader");
			e.printStackTrace();
		}
		
		if (doc != null) {
			try {
				return getFormDef(doc);
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		} else {
			return null;
		}
	}

	public static Document getXMLDocument(Reader reader){
		Document doc = new Document();

		try{
			KXmlParser parser = new KXmlParser();
			parser.setInput(reader);
			parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
			
			doc.parse(parser);
		} catch(Exception e){
			//#if debug.output==verbose || debug.output==exception
			System.err.println("XML Syntax Error!");
			e.printStackTrace();
			//#endif

			return null;
		}

		return doc;
	}

	public static FormDef getFormDef(Document doc){
		FormDef formDef = new FormDef();
		
		initBindHandlers();
		initStateVars();

		defaultNamespace = doc.getRootElement().getNamespaceUri(null);
		
		parseElement(formDef, doc.getRootElement(), formDef, topLevelHandlers);
		
		collapseRepeatGroups(formDef);
		
		if(instanceNode != null) {
			parseInstance(formDef, instanceNode);
		}

		initStateVars();

		processBindHandlers(formDef);
		return formDef;
	}

	private static final int CONTAINER_GROUP = 1;
	private static final int CONTAINER_REPEAT = 2;

	private static void parseElement (FormDef f, Element e, Object parent, Hashtable handlers) { //,
//			boolean allowUnknownElements, boolean allowText, boolean recurseUnknown) {
		String name = e.getName();

		IElementHandler eh = (IElementHandler)handlers.get(name);
		if (eh != null) {
			eh.handle(f, e, parent);
		} else {
			if (!name.equals("html") && !name.equals("head") && !name.equals("body")) {
				//#if debug.output==verbose
				System.err.println("XForm Parse: Unrecognized element [" + name	+ "]. Ignoring and processing children...");
				//#endif
			}
			for (int i = 0; i < e.getChildCount(); i++) {
				if (e.getType(i) == Element.ELEMENT) {
					parseElement(f, e.getElement(i), parent, handlers);
				}
			}
		}
	}

	private static void parseTitle (FormDef f, Element e) {
		String title = getXMLText(e, true);
		System.out.println("Title: \"" + title + "\"");
		f.setTitle(title);
		if(f.getName() == null) {
			//Jan 9, 2009 - ctsims
			//We don't really want to allow for forms without
			//some unique ID, so if a title is available, use
			//that.
			f.setName(title);
		}
	}
	
	private static void parseMeta (FormDef f, Element e) {
		int attributes = e.getAttributeCount();
		for(int i = 0 ; i < attributes ; ++i) {
			String name = e.getAttributeName(i);
			String value = e.getAttributeValue(i);
			if("name".equals(name)) {
				f.setName(value);
			}
		}
	}

	//for ease of parsing, we assume a model comes before the controls, which isn't necessarily mandated by the xforms spec
	private static void parseModel (FormDef f, Element e) {
		if (modelFound) {
			//#if debug.output==verbose
			System.err.println("Multiple models not supported. Ignoring subsequent models.");
			//#endif
			return;
		}
		modelFound = true;

		for (int i = 0; i < e.getChildCount(); i++) {
			
			int type = e.getType(i);
			Element child = (type == Node.ELEMENT ? e.getElement(i) : null);
			String childName = (child != null ? child.getName() : null);
			
			if ("itext".equals(childName)) {
				parseIText(f, child);
			} else if ("instance".equals(childName)) {
				//we save parsing the instance node until the end, giving us the information we need about
				//binds and data types and such
				saveInstanceNode(child);
			} else if ("bind".equals(childName)) { //<instance> must come before <bind>s
				parseBind(f, child);
			} else { //invalid model content
				if (type == Node.ELEMENT) {
					throw new XFormParseException("Unrecognized top-level tag [" + childName + "] found within <model>");
				} else if (type == Node.TEXT && getXMLText(e, i, true).length() != 0) {
					throw new XFormParseException("Unrecognized text content found within <model>: \"" + getXMLText(e, i, true) + "\"");					
				}
			}
			
			if(child == null || "bind".equals(childName) || "itext".equals(childName)) {
				//<NAME> - Jun 17, 2009 - This code is used when the stinginess flag
				//is set for the build. It dynamically wipes out old model nodes once they're
				//used. This is sketchy if anything else plans on touching the nodes.
				//This code can be removed once we're pull-parsing
				//#if org.javarosa.xform.stingy
				//# e.removeChild(i);
				//# --i;
				//#endif
			}
		}
		
		
	}

	private static void saveInstanceNode (Element instance) {
		if (instanceNode != null) {
			System.err.println("Multiple instances not supported. Ignoring subsequent instances.");
			return;
		}
			
		for (int i = 0; i < instance.getChildCount(); i++) {
			if (instance.getType(i) == Node.ELEMENT) {
				if (instanceNode != null) {
					throw new XFormParseException("XForm Parse: <instance> has more than one child element");
				} else {
					instanceNode = instance.getElement(i);
				}
			}
		}
	}
	
	private static int serialQuestionID = 1;
	
	protected static QuestionDef parseUpload(IFormElement parent, Element e, FormDef f,
			int controlUpload) {
		QuestionDef question = parseControl(parent, e, f, controlUpload);
		String mediaType = e.getAttributeValue(null, "mediatype");
		if ("image/*".equals(mediaType)) {
			// NOTE: this could be further expanded. 
			question.setControlType(Constants.CONTROL_IMAGE_CHOOSE);
		}
		else if("audio/*".equals(mediaType))
		{
            question.setControlType(Constants.CONTROL_AUDIO_CAPTURE);
        } 
		else if ("video/*".equals(mediaType)) 
		{
            question.setControlType(Constants.CONTROL_VIDEO_CAPTURE);
        }
        return question;
    }
	
	protected static QuestionDef parseControl (IFormElement parent, Element e, FormDef f, int controlType) {
		QuestionDef question = new QuestionDef();
		question.setID(serialQuestionID++); //until we come up with a better scheme
		IDataReference dataRef = null;
		boolean refFromBind = false;
		
		String ref = e.getAttributeValue(null, "ref");
		String bind = e.getAttributeValue(null, "bind");
		
		if (bind != null) {
			DataBinding binding = (DataBinding)bindingsByID.get(bind);
			if (binding == null) {
				throw new XFormParseException("XForm Parse: invalid binding ID '" + bind + "'");
			}
			dataRef = binding.getReference();
			refFromBind = true;
		} else if (ref != null) {
			dataRef = new XPathReference(ref);
		} else {
			if (controlType == Constants.CONTROL_TRIGGER) {
				//TODO: special handling for triggers? also, not all triggers created equal
			} else {
				throw new XFormParseException("XForm Parse: input control with neither 'ref' nor 'bind'");
			}
		}

		if (dataRef != null) {
			if (!refFromBind) {
				dataRef = getAbsRef(dataRef, parent);
			}
			question.setBind(dataRef);
			
			if (controlType == Constants.CONTROL_SELECT_ONE) {
				selectOnes.addElement((TreeReference)dataRef.getReference());
			} else if (controlType == Constants.CONTROL_SELECT_MULTI) {
				selectMultis.addElement((TreeReference)dataRef.getReference());
			}
		}		

		boolean isSelect = (controlType == Constants.CONTROL_SELECT_MULTI || controlType == Constants.CONTROL_SELECT_ONE);
		question.setControlType(controlType);
		question.setAppearanceAttr(e.getAttributeValue(null, "appearance"));

		for (int i = 0; i < e.getChildCount(); i++) {
			int type = e.getType(i);
			Element child = (type == Node.ELEMENT ? e.getElement(i) : null);
			String childName = (child != null ? child.getName() : null);

			if ("label".equals(childName)) {
				parseQuestionLabel(f, question, child);
			} else if ("hint".equals(childName)) {
				parseHint(f, question, child);
			} else if (isSelect && "item".equals(childName)) {
				parseItem(f, question, child);
			}
		}
		if (isSelect) {
			if (question.getSelectItemIDs().size() == 0) {
				throw new XFormParseException("Select question has no choices");
			}
			question.localizeSelectMap(null);		
		}
			
		parent.addChild(question);
		return question;
	}

	private static void parseQuestionLabel (FormDef f, QuestionDef q, Element e) {
		String label = getLabel(e, f);
		String ref = e.getAttributeValue("", "ref");

		if (ref != null) {
			if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
				String textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

				verifyTextMappings(f, textRef, "Question <label>", true);
				q.setLongTextID(textRef + ";long", null);
				q.setShortTextID(textRef + ";short", null);
			} else {
				throw new RuntimeException("malformed ref [" + ref + "] for <label>");
			}
		} else {
			q.setLongText(label);
			q.setShortText(label);
		}
	}

	private static void parseGroupLabel (FormDef f, GroupDef g, Element e) {
		if (g.getRepeat())
			return; //ignore child <label>s for <repeat>; the appropriate <label> must be in the wrapping <group>
		
		String label = getLabel(e, f);
		String ref = e.getAttributeValue("", "ref");

		if (ref != null) {
			if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
				String textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

				verifyTextMappings(f, textRef, "Group <label>", true);
				g.setLongTextID(textRef + ";long", null);
				g.setShortTextID(textRef + ";short", null);
			} else {
				throw new RuntimeException("malformed ref [" + ref + "] for <label>");
			}
		} else {
			g.setLongText(label);
			g.setShortText(label);
		}
	}
	
	private static String getLabel (Element e, FormDef f) {
		boolean outputFound = false;
		boolean otherStuffFound = false;
		Vector outputs = new Vector();
		for (int i = 0; i < e.getChildCount(); i++) {
			if (e.getType(i) != Node.TEXT) {
				if (e.getType(i) == Node.ELEMENT && "output".equals(e.getElement(i).getName())) {
					outputFound = true;
					outputs.addElement(new Integer(i));
				} else {
					otherStuffFound = true;
				}
			}
		}
		
		if (outputFound && !otherStuffFound) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < outputs.size() + 1; i++) {
				int ixStart = (i == 0 ? 0 : ((Integer)outputs.elementAt(i - 1)).intValue() + 1);
				if (ixStart < e.getChildCount()) {
					String piece = getXMLText(e, ixStart, false);
					if (piece != null) {
						sb.append(piece);
					}
				}
				
				if (i < outputs.size()) {
					int ix = ((Integer)outputs.elementAt(i)).intValue();
					sb.append("${" + parseOutput(e.getElement(ix), f) + "}");
				}
			}
			return sb.toString().trim();
		} else {
			return getXMLText(e, true);
		}
	}	
	
	private static String parseOutput (Element e, FormDef f) {
		String xpath = e.getAttributeValue(null, "ref");
		if (xpath == null) {
			xpath = e.getAttributeValue(null, "value");
		}
		if (xpath == null) {
			throw new XFormParseException("XForm Parse: <output> without 'ref' or 'value'");	
		}
		
		XPathConditional expr = null;
		try {
			expr = new XPathConditional(xpath);
		} catch (XPathSyntaxException xse) {
			//#if debug.output==verbose
			System.err.println("Invalid XPath expression [" + xpath + "]!");
			return "";
			//#endif
		}

		int index = -1;
		if (f.getOutputFragments().contains(expr)) {
			index = f.getOutputFragments().indexOf(expr);
		} else {
			index = f.getOutputFragments().size();
			f.getOutputFragments().addElement(expr);
		}
		return String.valueOf(index);
	}
	
	private static void parseHint (FormDef f, QuestionDef q, Element e) {
		String hint = getXMLText(e, true);
		String ref = e.getAttributeValue("", "ref");

		if (ref != null) {
			if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
				String textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

				verifyTextMappings(f, textRef, "<hint>", false);
				q.setHelpTextID(textRef, null);
			} else {
				throw new RuntimeException("malformed ref [" + ref + "] for <hint>");
			}
		} else {
			q.setHelpText(hint);
		}
	}

	private static void parseItem (FormDef f, QuestionDef q, Element e) {
		String label = null;
		String textRef = null;
		String value = null;

		for (int i = 0; i < e.getChildCount(); i++) {
			int type = e.getType(i);
			Element child = (type == Node.ELEMENT ? e.getElement(i) : null);
			String childName = (child != null ? child.getName() : null);

			if ("label".equals(childName)) {
				label = getXMLText(child, true);
				String ref = child.getAttributeValue("", "ref");

				if (ref != null) {
					if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
						textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

						verifyTextMappings(f, textRef, "Item <label>", false);
					} else {
						throw new XFormParseException("malformed ref [" + ref + "] for <item>");
					}
				}
			} else if ("value".equals(childName)) {
				value = getXMLText(child, true);
				
				if (value.length() > 32) {
					System.err.println("WARNING: choice value [" + value + "] is too long; max. suggested length 32 chars");<commit_msg>make max choice value length a defined constant<commit_after>/*
 * 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.javarosa.xform.parse;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.javarosa.core.model.Constants;
import org.javarosa.core.model.DataBinding;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.GroupDef;
import org.javarosa.core.model.IDataReference;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.QuestionDef;
import org.javarosa.core.model.condition.Condition;
import org.javarosa.core.model.condition.Constraint;
import org.javarosa.core.model.condition.Recalculate;
import org.javarosa.core.model.condition.Triggerable;
import org.javarosa.core.model.instance.DataModelTree;
import org.javarosa.core.model.instance.TreeElement;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.model.util.restorable.Restorable;
import org.javarosa.core.model.util.restorable.RestoreUtils;
import org.javarosa.core.services.locale.Localizer;
import org.javarosa.core.services.locale.TableLocaleSource;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import org.javarosa.core.util.externalizable.PrototypeFactoryDeprecated;
import org.javarosa.model.xform.XPathReference;
import org.javarosa.xform.util.IXFormBindHandler;
import org.javarosa.xform.util.XFormAnswerDataParser;
import org.javarosa.xpath.XPathConditional;
import org.javarosa.xpath.parser.XPathSyntaxException;
import org.kxml2.io.KXmlParser;
import org.kxml2.kdom.Document;
import org.kxml2.kdom.Element;
import org.kxml2.kdom.Node;
import org.xmlpull.v1.XmlPullParser;

/* droos: i think we need to start storing the contents of the <bind>s in the formdef again */

/**
 * Provides conversion from xform to epihandy object model and vice vasa.
 *
 * @author <NAME>
 * @author <NAME>
 *
 */
public class XFormParser {
	public static final String NAMESPACE_JAVAROSA = "http://openrosa.org/javarosa";

	private static Hashtable topLevelHandlers;
	private static Hashtable groupLevelHandlers;
	private static Hashtable typeMappings;
	private static PrototypeFactoryDeprecated modelPrototypes;

	/** IXFormBindHandler */
	private static Vector bindHandlers;

	/* THIS CLASS IS NOT THREAD-SAFE */
	//state variables -- not a good idea since this class is static, but that's not really a good idea either, now is it
	private static boolean modelFound;
	private static Hashtable bindingsByID;
	private static Vector bindings; //DataBinding
	private static Vector repeats; //TreeReference
	private static Vector selectOnes; //TreeReference
	private static Vector selectMultis; //TreeReference
	private static Element instanceNode; //top-level data node of the instance; saved off so it can be processed after the <bind>s
	private static String defaultNamespace;
	
	private static DataModelTree repeatTree; //pseudo-data model tree that describes the repeat structure of the instance;
										     //useful during instance processing and validation
	
	static {
		initProcessingRules();
		initTypeMappings();
		modelPrototypes = new PrototypeFactoryDeprecated();
		bindHandlers = new Vector();
	}

	/**
	 * Default Constructor
	 *
	 */
	public XFormParser(){

	}

	private static void initProcessingRules () {
		IElementHandler title = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseTitle(f, e); } };
		IElementHandler meta = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseMeta(f, e); } };
		IElementHandler model = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseModel(f, e); } };
		IElementHandler input = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_INPUT); } };
		IElementHandler select = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_SELECT_MULTI); } };
		IElementHandler select1 = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_SELECT_ONE); } };
		IElementHandler group = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseGroup((IFormElement)parent, e, f, CONTAINER_GROUP); } };
		IElementHandler repeat = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseGroup((IFormElement)parent, e, f, CONTAINER_REPEAT); } };
		IElementHandler groupLabel = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseGroupLabel(f, (GroupDef)parent, e); } };
		IElementHandler trigger = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseControl((IFormElement)parent, e, f, Constants.CONTROL_TRIGGER); } };
		IElementHandler upload = new IElementHandler () {
			public void handle (FormDef f, Element e, Object parent) { parseUpload((IFormElement)parent, e, f, Constants.CONTROL_UPLOAD); } };

		groupLevelHandlers = new Hashtable();
		groupLevelHandlers.put("input", input);
		groupLevelHandlers.put("select", select);
		groupLevelHandlers.put("select1", select1);
		groupLevelHandlers.put("group", group);
		groupLevelHandlers.put("repeat", repeat);
		groupLevelHandlers.put("trigger", trigger); //multi-purpose now; need to dig deeper
		groupLevelHandlers.put(Constants.XFTAG_UPLOAD, upload);

		topLevelHandlers = new Hashtable();
		for (Enumeration en = groupLevelHandlers.keys(); en.hasMoreElements(); ) {
			String key = (String)en.nextElement();
			topLevelHandlers.put(key, groupLevelHandlers.get(key));
		}
		topLevelHandlers.put("model", model);
		topLevelHandlers.put("title", title);
		topLevelHandlers.put("meta", meta);

		groupLevelHandlers.put("label", groupLabel);
	}

	private static void initTypeMappings () {
		typeMappings = new Hashtable();
		typeMappings.put("string", new Integer(Constants.DATATYPE_TEXT));               //xsd:
		typeMappings.put("integer", new Integer(Constants.DATATYPE_INTEGER));           //xsd:
		typeMappings.put("int", new Integer(Constants.DATATYPE_INTEGER));               //xsd:
		typeMappings.put("decimal", new Integer(Constants.DATATYPE_DECIMAL));           //xsd:
		typeMappings.put("double", new Integer(Constants.DATATYPE_DECIMAL));            //xsd:
		typeMappings.put("float", new Integer(Constants.DATATYPE_DECIMAL));             //xsd:
		typeMappings.put("dateTime", new Integer(Constants.DATATYPE_DATE_TIME));        //xsd:
		typeMappings.put("date", new Integer(Constants.DATATYPE_DATE));                 //xsd:
		typeMappings.put("time", new Integer(Constants.DATATYPE_TIME));                 //xsd:
		typeMappings.put("gYear", new Integer(Constants.DATATYPE_UNSUPPORTED));         //xsd:
		typeMappings.put("gMonth", new Integer(Constants.DATATYPE_UNSUPPORTED));        //xsd:
		typeMappings.put("gDay", new Integer(Constants.DATATYPE_UNSUPPORTED));          //xsd:
		typeMappings.put("gYearMonth", new Integer(Constants.DATATYPE_UNSUPPORTED));    //xsd:
		typeMappings.put("gMonthDay", new Integer(Constants.DATATYPE_UNSUPPORTED));     //xsd:
		typeMappings.put("boolean", new Integer(Constants.DATATYPE_UNSUPPORTED));       //xsd:
		typeMappings.put("base64Binary", new Integer(Constants.DATATYPE_UNSUPPORTED));  //xsd:
		typeMappings.put("hexBinary", new Integer(Constants.DATATYPE_UNSUPPORTED));     //xsd:
		typeMappings.put("anyURI", new Integer(Constants.DATATYPE_UNSUPPORTED));        //xsd:
		typeMappings.put("listItem", new Integer(Constants.DATATYPE_CHOICE));           //xforms:
		typeMappings.put("listItems", new Integer(Constants.DATATYPE_CHOICE_LIST));	    //xforms:	
		typeMappings.put("select1", new Integer(Constants.DATATYPE_CHOICE));	        //non-standard	
		typeMappings.put("select", new Integer(Constants.DATATYPE_CHOICE_LIST));        //non-standard
		typeMappings.put("geopoint", new Integer(Constants.DATATYPE_GEOPOINT));         //non-standard
		typeMappings.put("barcode", new Integer(Constants.DATATYPE_BARCODE));           //non-standard
        typeMappings.put("binary", new Integer(Constants.DATATYPE_BINARY));             //non-standard
	    
	}
	
	private static void initBindHandlers() {
		Enumeration en = bindHandlers.elements();
		while(en.hasMoreElements()) {
			IXFormBindHandler handler = (IXFormBindHandler)en.nextElement();
			handler.init();
			
		}
	}
	private static void processBindHandlers(FormDef formDef) {
		Enumeration en = bindHandlers.elements();
		while(en.hasMoreElements()) {
			IXFormBindHandler handler = (IXFormBindHandler)en.nextElement();
			handler.postProcess(formDef);
			
		}
	}

	private static void initStateVars () {
		modelFound = false;
		bindingsByID = new Hashtable();
		bindings = new Vector();
		repeats = new Vector();
		selectOnes = new Vector();
		selectMultis = new Vector();
		instanceNode = null;
		repeatTree = null;
		defaultNamespace = null;
	}

	public static FormDef getFormDef(Reader reader) {
		System.out.println("Parsing form...");
		
		Document doc = getXMLDocument(reader);
		try {
			reader.close();
		} catch (IOException e) {
			System.out.println("Error closing reader");
			e.printStackTrace();
		}
		
		if (doc != null) {
			try {
				return getFormDef(doc);
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		} else {
			return null;
		}
	}

	public static Document getXMLDocument(Reader reader){
		Document doc = new Document();

		try{
			KXmlParser parser = new KXmlParser();
			parser.setInput(reader);
			parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
			
			doc.parse(parser);
		} catch(Exception e){
			//#if debug.output==verbose || debug.output==exception
			System.err.println("XML Syntax Error!");
			e.printStackTrace();
			//#endif

			return null;
		}

		return doc;
	}

	public static FormDef getFormDef(Document doc){
		FormDef formDef = new FormDef();
		
		initBindHandlers();
		initStateVars();

		defaultNamespace = doc.getRootElement().getNamespaceUri(null);
		
		parseElement(formDef, doc.getRootElement(), formDef, topLevelHandlers);
		
		collapseRepeatGroups(formDef);
		
		if(instanceNode != null) {
			parseInstance(formDef, instanceNode);
		}

		initStateVars();

		processBindHandlers(formDef);
		return formDef;
	}

	private static final int CONTAINER_GROUP = 1;
	private static final int CONTAINER_REPEAT = 2;

	private static void parseElement (FormDef f, Element e, Object parent, Hashtable handlers) { //,
//			boolean allowUnknownElements, boolean allowText, boolean recurseUnknown) {
		String name = e.getName();

		IElementHandler eh = (IElementHandler)handlers.get(name);
		if (eh != null) {
			eh.handle(f, e, parent);
		} else {
			if (!name.equals("html") && !name.equals("head") && !name.equals("body")) {
				//#if debug.output==verbose
				System.err.println("XForm Parse: Unrecognized element [" + name	+ "]. Ignoring and processing children...");
				//#endif
			}
			for (int i = 0; i < e.getChildCount(); i++) {
				if (e.getType(i) == Element.ELEMENT) {
					parseElement(f, e.getElement(i), parent, handlers);
				}
			}
		}
	}

	private static void parseTitle (FormDef f, Element e) {
		String title = getXMLText(e, true);
		System.out.println("Title: \"" + title + "\"");
		f.setTitle(title);
		if(f.getName() == null) {
			//Jan 9, 2009 - ctsims
			//We don't really want to allow for forms without
			//some unique ID, so if a title is available, use
			//that.
			f.setName(title);
		}
	}
	
	private static void parseMeta (FormDef f, Element e) {
		int attributes = e.getAttributeCount();
		for(int i = 0 ; i < attributes ; ++i) {
			String name = e.getAttributeName(i);
			String value = e.getAttributeValue(i);
			if("name".equals(name)) {
				f.setName(value);
			}
		}
	}

	//for ease of parsing, we assume a model comes before the controls, which isn't necessarily mandated by the xforms spec
	private static void parseModel (FormDef f, Element e) {
		if (modelFound) {
			//#if debug.output==verbose
			System.err.println("Multiple models not supported. Ignoring subsequent models.");
			//#endif
			return;
		}
		modelFound = true;

		for (int i = 0; i < e.getChildCount(); i++) {
			
			int type = e.getType(i);
			Element child = (type == Node.ELEMENT ? e.getElement(i) : null);
			String childName = (child != null ? child.getName() : null);
			
			if ("itext".equals(childName)) {
				parseIText(f, child);
			} else if ("instance".equals(childName)) {
				//we save parsing the instance node until the end, giving us the information we need about
				//binds and data types and such
				saveInstanceNode(child);
			} else if ("bind".equals(childName)) { //<instance> must come before <bind>s
				parseBind(f, child);
			} else { //invalid model content
				if (type == Node.ELEMENT) {
					throw new XFormParseException("Unrecognized top-level tag [" + childName + "] found within <model>");
				} else if (type == Node.TEXT && getXMLText(e, i, true).length() != 0) {
					throw new XFormParseException("Unrecognized text content found within <model>: \"" + getXMLText(e, i, true) + "\"");					
				}
			}
			
			if(child == null || "bind".equals(childName) || "itext".equals(childName)) {
				//<NAME> - Jun 17, 2009 - This code is used when the stinginess flag
				//is set for the build. It dynamically wipes out old model nodes once they're
				//used. This is sketchy if anything else plans on touching the nodes.
				//This code can be removed once we're pull-parsing
				//#if org.javarosa.xform.stingy
				//# e.removeChild(i);
				//# --i;
				//#endif
			}
		}
		
		
	}

	private static void saveInstanceNode (Element instance) {
		if (instanceNode != null) {
			System.err.println("Multiple instances not supported. Ignoring subsequent instances.");
			return;
		}
			
		for (int i = 0; i < instance.getChildCount(); i++) {
			if (instance.getType(i) == Node.ELEMENT) {
				if (instanceNode != null) {
					throw new XFormParseException("XForm Parse: <instance> has more than one child element");
				} else {
					instanceNode = instance.getElement(i);
				}
			}
		}
	}
	
	private static int serialQuestionID = 1;
	
	protected static QuestionDef parseUpload(IFormElement parent, Element e, FormDef f,
			int controlUpload) {
		QuestionDef question = parseControl(parent, e, f, controlUpload);
		String mediaType = e.getAttributeValue(null, "mediatype");
		if ("image/*".equals(mediaType)) {
			// NOTE: this could be further expanded. 
			question.setControlType(Constants.CONTROL_IMAGE_CHOOSE);
		}
		else if("audio/*".equals(mediaType))
		{
            question.setControlType(Constants.CONTROL_AUDIO_CAPTURE);
        } 
		else if ("video/*".equals(mediaType)) 
		{
            question.setControlType(Constants.CONTROL_VIDEO_CAPTURE);
        }
        return question;
    }
	
	protected static QuestionDef parseControl (IFormElement parent, Element e, FormDef f, int controlType) {
		QuestionDef question = new QuestionDef();
		question.setID(serialQuestionID++); //until we come up with a better scheme
		IDataReference dataRef = null;
		boolean refFromBind = false;
		
		String ref = e.getAttributeValue(null, "ref");
		String bind = e.getAttributeValue(null, "bind");
		
		if (bind != null) {
			DataBinding binding = (DataBinding)bindingsByID.get(bind);
			if (binding == null) {
				throw new XFormParseException("XForm Parse: invalid binding ID '" + bind + "'");
			}
			dataRef = binding.getReference();
			refFromBind = true;
		} else if (ref != null) {
			dataRef = new XPathReference(ref);
		} else {
			if (controlType == Constants.CONTROL_TRIGGER) {
				//TODO: special handling for triggers? also, not all triggers created equal
			} else {
				throw new XFormParseException("XForm Parse: input control with neither 'ref' nor 'bind'");
			}
		}

		if (dataRef != null) {
			if (!refFromBind) {
				dataRef = getAbsRef(dataRef, parent);
			}
			question.setBind(dataRef);
			
			if (controlType == Constants.CONTROL_SELECT_ONE) {
				selectOnes.addElement((TreeReference)dataRef.getReference());
			} else if (controlType == Constants.CONTROL_SELECT_MULTI) {
				selectMultis.addElement((TreeReference)dataRef.getReference());
			}
		}		

		boolean isSelect = (controlType == Constants.CONTROL_SELECT_MULTI || controlType == Constants.CONTROL_SELECT_ONE);
		question.setControlType(controlType);
		question.setAppearanceAttr(e.getAttributeValue(null, "appearance"));

		for (int i = 0; i < e.getChildCount(); i++) {
			int type = e.getType(i);
			Element child = (type == Node.ELEMENT ? e.getElement(i) : null);
			String childName = (child != null ? child.getName() : null);

			if ("label".equals(childName)) {
				parseQuestionLabel(f, question, child);
			} else if ("hint".equals(childName)) {
				parseHint(f, question, child);
			} else if (isSelect && "item".equals(childName)) {
				parseItem(f, question, child);
			}
		}
		if (isSelect) {
			if (question.getSelectItemIDs().size() == 0) {
				throw new XFormParseException("Select question has no choices");
			}
			question.localizeSelectMap(null);		
		}
			
		parent.addChild(question);
		return question;
	}

	private static void parseQuestionLabel (FormDef f, QuestionDef q, Element e) {
		String label = getLabel(e, f);
		String ref = e.getAttributeValue("", "ref");

		if (ref != null) {
			if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
				String textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

				verifyTextMappings(f, textRef, "Question <label>", true);
				q.setLongTextID(textRef + ";long", null);
				q.setShortTextID(textRef + ";short", null);
			} else {
				throw new RuntimeException("malformed ref [" + ref + "] for <label>");
			}
		} else {
			q.setLongText(label);
			q.setShortText(label);
		}
	}

	private static void parseGroupLabel (FormDef f, GroupDef g, Element e) {
		if (g.getRepeat())
			return; //ignore child <label>s for <repeat>; the appropriate <label> must be in the wrapping <group>
		
		String label = getLabel(e, f);
		String ref = e.getAttributeValue("", "ref");

		if (ref != null) {
			if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
				String textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

				verifyTextMappings(f, textRef, "Group <label>", true);
				g.setLongTextID(textRef + ";long", null);
				g.setShortTextID(textRef + ";short", null);
			} else {
				throw new RuntimeException("malformed ref [" + ref + "] for <label>");
			}
		} else {
			g.setLongText(label);
			g.setShortText(label);
		}
	}
	
	private static String getLabel (Element e, FormDef f) {
		boolean outputFound = false;
		boolean otherStuffFound = false;
		Vector outputs = new Vector();
		for (int i = 0; i < e.getChildCount(); i++) {
			if (e.getType(i) != Node.TEXT) {
				if (e.getType(i) == Node.ELEMENT && "output".equals(e.getElement(i).getName())) {
					outputFound = true;
					outputs.addElement(new Integer(i));
				} else {
					otherStuffFound = true;
				}
			}
		}
		
		if (outputFound && !otherStuffFound) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < outputs.size() + 1; i++) {
				int ixStart = (i == 0 ? 0 : ((Integer)outputs.elementAt(i - 1)).intValue() + 1);
				if (ixStart < e.getChildCount()) {
					String piece = getXMLText(e, ixStart, false);
					if (piece != null) {
						sb.append(piece);
					}
				}
				
				if (i < outputs.size()) {
					int ix = ((Integer)outputs.elementAt(i)).intValue();
					sb.append("${" + parseOutput(e.getElement(ix), f) + "}");
				}
			}
			return sb.toString().trim();
		} else {
			return getXMLText(e, true);
		}
	}	
	
	private static String parseOutput (Element e, FormDef f) {
		String xpath = e.getAttributeValue(null, "ref");
		if (xpath == null) {
			xpath = e.getAttributeValue(null, "value");
		}
		if (xpath == null) {
			throw new XFormParseException("XForm Parse: <output> without 'ref' or 'value'");	
		}
		
		XPathConditional expr = null;
		try {
			expr = new XPathConditional(xpath);
		} catch (XPathSyntaxException xse) {
			//#if debug.output==verbose
			System.err.println("Invalid XPath expression [" + xpath + "]!");
			return "";
			//#endif
		}

		int index = -1;
		if (f.getOutputFragments().contains(expr)) {
			index = f.getOutputFragments().indexOf(expr);
		} else {
			index = f.getOutputFragments().size();
			f.getOutputFragments().addElement(expr);
		}
		return String.valueOf(index);
	}
	
	private static void parseHint (FormDef f, QuestionDef q, Element e) {
		String hint = getXMLText(e, true);
		String ref = e.getAttributeValue("", "ref");

		if (ref != null) {
			if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
				String textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

				verifyTextMappings(f, textRef, "<hint>", false);
				q.setHelpTextID(textRef, null);
			} else {
				throw new RuntimeException("malformed ref [" + ref + "] for <hint>");
			}
		} else {
			q.setHelpText(hint);
		}
	}

	private static void parseItem (FormDef f, QuestionDef q, Element e) {
		final int MAX_VALUE_LEN = 32;
		
		String label = null;
		String textRef = null;
		String value = null;

		for (int i = 0; i < e.getChildCount(); i++) {
			int type = e.getType(i);
			Element child = (type == Node.ELEMENT ? e.getElement(i) : null);
			String childName = (child != null ? child.getName() : null);

			if ("label".equals(childName)) {
				label = getXMLText(child, true);
				String ref = child.getAttributeValue("", "ref");

				if (ref != null) {
					if (ref.startsWith("jr:itext('") && ref.endsWith("')")) {
						textRef = ref.substring("jr:itext('".length(), ref.indexOf("')"));

						verifyTextMappings(f, textRef, "Item <label>", false);
					} else {
						throw new XFormParseException("malformed ref [" + ref + "] for <item>");
					}
				}
			} else if ("value".equals(childName)) {
				value = getXMLText(child, true);
				
				if (value.length() > MAX_VALUE_LEN) {
					System.err.println("WARNING: choice value [" + value + "] is too long; max. suggested length " + MAX_VALUE_LEN + " chars");
{'fraction_non_alphanumeric': 0.13429403648627933, 'fraction_numerical': 0.0022776548914828845, 'mean_word_length': 3.8463171301209935, 'pattern_counts': {'":': 0, '<': 69, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 10}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5626785', 'n_tokens_mistral': 16110, 'n_tokens_neox': 14189, 'n_words': 3648}