package com.radicaldynamic.groupinform.xform; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.StringTokenizer; import java.util.UUID; import java.util.regex.Pattern; import com.mycila.xmltool.XMLTag; import com.radicaldynamic.groupinform.application.Collect; import android.util.Log; public class Field { private static String t = "Field: "; // Any attributes found on this element private Map<String, String> attributes = new HashMap<String, String>(); // Any children (other fields) of this one, e.g., groups and repeats (or items for a select or select1 field) private ArrayList<Field> children = new ArrayList<Field>(); private String type; // The XML element name of this field (e.g., group, input, etc.) private String location; // The XML element location of this node (e.g., *[2]/*[1]) private String xpath; // Value of the "ref" or "nodeset" attribute (if any) private Field parent; // The parent of this field item (null if at top of form hierarchy) // For select or select1 fields (also see "children" attribute) private String itemValue = ""; // Any value assigned to this node (if it is an item) private boolean itemDefault = false; // Whether this value should be preselected private FieldText label = new FieldText(); // Any label assigned to this field private FieldText hint = new FieldText(); // Any hint assigned to this field private Bind bind = new Bind(); private Instance instance = new Instance(); private boolean active = false; // Used to determine which field is "active" in form builder navigation private boolean empty = false; // This is an "empty" or new field and requires further initialization private boolean newField = false; // Whether this field is new and should be added to the (control) field state list private boolean saved = false; // Whether changes to a field that has been loaded into the field editor were saved /* * For fields instantiated by the form builder */ public Field() { empty = true; newField = true; } // For fields instantiated from entries in <h:body> public Field(XMLTag tag, ArrayList<Bind> binds, String instanceRoot, Field parent) { final String tt = t + "Field(): "; if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "created new " + tag.getCurrentTagName() + " field at " + tag.getCurrentTagLocation()); setType(tag.getCurrentTagName()); setLocation(tag.getCurrentTagLocation()); if (parent != null) { setParent(parent); } // Read in attributes (includes "ref" to instance data output) for (String s : tag.getAttributeNames()) { attributes.put(s, tag.getAttribute(s)); /* * Special handling for the ref attribute to make it easy to access. * * Note that repeat elements have a nodeset attribute rather than a ref * but that both are essentially equivalent. We need to remember this * when it comes time to write out the XML. */ if (s.equals(XForm.Attribute.REFERENCE) || s.equals(XForm.Attribute.NODESET) || s.equals(XForm.Attribute.BIND)) { String xpath = tag.getAttribute(s); // If this reference is not to an itext translation then it must be to an instance/bind if (Pattern.matches("^jr:.*", xpath)) { // FIXME: is this sanity check required? } else { // If the reference is not literal then make it so if (!Pattern.matches("^/.*", xpath)) { String ref = determineXPath(parent, instanceRoot, xpath); if (ref.length() > 0) { if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, tt + "changed non-literal XPath from " + xpath + " to " + ref); xpath = ref; } } /* * Iterate through the known list of binds and form a relationship * with those that have the same XPath as the current field. */ Iterator<Bind> it = binds.iterator(); while (it.hasNext()) { Bind b = it.next(); // If a bind with a nodeset identical to this ref exists, associate it with this field if (b.getXPath().equals(xpath)) { if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, t + "bind with nodeset " + b.getXPath() + " associated to field at " + getLocation()); setBind(b); // Not all binds will have an associated type but our code (may) expect them to if (b.getType() == null) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "bind for " + b.getXPath() + " missing an explicit type (setting to string if input)"); if (getType().equals("input")) b.setType("string"); } // No point in looking further, right? break; } } } setXPath(xpath); } } } public Map<String, String> getAttributes() { return attributes; } public ArrayList<Field> getChildren() { return children; } /* * If this field is a group, containing exactly one field which is a repeat then return it. * Else, return null. This should only be used on fields that are known to be repeated groups. */ public Field getRepeat() { if (type.equals("group") && children.size() == 1 && children.get(0).getType().equals("repeat")) { return children.get(0); } else { return null; } } public void setLabel(String label) { if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, t + "setting label for " + type + " at " + location); this.label = new FieldText(label); } // If you want a human readable textual string then you need to perform .toString() on the result public FieldText getLabel() { return label; } public void setHint(String hint) { if (Collect.Log.VERBOSE) Log.v(Collect.LOGTAG, t + "setting hint for " + type + " at " + location); this.hint = new FieldText(hint); } // If you want a human readable textual string then you need to perform .toString() on the result public FieldText getHint() { return hint; } public void setType(String type) { this.type = type; } public String getType() { return type; } public void setLocation(String location) { this.location = location; } public String getLocation() { return location; } public void setItemValue(String itemValue) { this.itemValue = itemValue; } public String getItemValue() { return itemValue; } public void setItemDefault(boolean itemDefault) { this.itemDefault = itemDefault; } public boolean isItemDefault() { return itemDefault; } public void setXPath(String xpath) { this.xpath = xpath; } public String getXPath() { return xpath; } public boolean hasXPath() { if (xpath != null && xpath.length() > 0) return true; else return false; } public void setBind(Bind bind) { this.bind = bind; } public Bind getBind() { return bind; } public void setActive(boolean active) { this.active = active; } public boolean isActive() { return active; } public void setParent(Field parent) { this.parent = parent; } public Field getParent() { return parent; } public void setInstance(Instance instance) { this.instance = instance; } public Instance getInstance() { return instance; } public void setEmpty(boolean empty) { this.empty = empty; } public boolean isEmpty() { return empty; } public void setSaved(boolean saved) { this.saved = saved; } public boolean isSaved() { return saved; } public void setNewField(boolean newField) { this.newField = newField; } public boolean isNewField() { return newField; } /* * Traverse the tree upwards until an XPath can be determined */ private String determineXPath(Field parent, String instanceRoot, String xpath) { if (parent == null) { xpath = File.separator + instanceRoot + File.separator + xpath; } else { if (Field.isRepeatedGroup(parent)) { xpath = parent.getRepeat().getXPath() + File.separator + xpath; } else { xpath = determineXPath(parent.getParent(), instanceRoot, xpath); } } return xpath; } /* * Returns true if the field it has been passed is a repeated group, otherwise false */ public static boolean isRepeatedGroup(Field f) { if (f != null && f.getType().equals("group") && f.getChildren().size() == 1 && f.getChildren().get(0).getType().equals("repeat")) return true; else return false; } /* * Creates a suitable instance field name from the label. This should only be used on new fields. */ public static String makeFieldName(FieldText label) { final StringTokenizer st = new StringTokenizer(label.toString(), " ", true); final StringBuilder sb = new StringBuilder(); String name = ""; while (st.hasMoreTokens()) { String token = st.nextToken(); token = String.format("%s%s", Character.toUpperCase(token.charAt(0)), token.substring(1)); sb.append(token); } name = sb.toString().replaceAll("\\s", "").replaceAll("[^a-zA-Z0-9]", ""); // Just in case the label did not have anything in it from which to generate a sane field name if (name.length() == 0) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "unable to construct field name from getLabel().toString() of " + label.toString()); // Get rid of - characters (not valid in XML tag names) name = UUID.randomUUID().toString().replaceAll("[^a-zA-Z0-9]", ""); } return name; } }