package com.radicaldynamic.groupinform.xform;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Iterator;
import android.util.Log;
import com.mycila.xmltool.XMLDoc;
import com.mycila.xmltool.XMLTag;
import com.radicaldynamic.groupinform.R;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.utilities.TranslationSortByDefault;
public final class FormWriter
{
private static final String t = "FormWriter: ";
public static final String CONTENT_TYPE = "text/xml";
private static XMLTag mFormTag;
private static String mDefaultPrefix;
private static String mInstanceRoot;
private static String mInstanceRootId;
@SuppressWarnings("serial")
public static class FormSanityException extends Exception
{
FormSanityException(String s)
{
super(s);
}
}
@SuppressWarnings("serial")
public static class GroupHasNoChildrenException extends FormSanityException
{
GroupHasNoChildrenException(String s)
{
super(s);
}
}
public static byte[] writeXml(String headTitle, String instanceRoot, String instanceRootId) throws GroupHasNoChildrenException
{
try {
// Retrieve and load a template XForm file (this makes it easier than hardcoding a new one from scratch)
InputStream xis = Collect.getInstance().getResources().openRawResource(R.raw.xform_template);
mFormTag = XMLDoc.from(xis, false);
xis.close();
} catch (IOException e) {
// Ignore xis.close() exceptions
e.printStackTrace();
}
mDefaultPrefix = mFormTag.getPefix(XForm.Value.XMLNS_XFORMS);
mInstanceRoot = instanceRoot;
mInstanceRootId = instanceRootId;
// Insert the title of this form into the XML
mFormTag.gotoRoot().gotoTag("h:head/h:title").setText(FieldText.encodeXMLEntities(headTitle));
// Either write out translations or remove the unused itext tag
if (Collect.getInstance().getFormBuilderState().getTranslations().size() > 0) {
if (!mFormTag.gotoRoot().gotoTag("h:head/%1$s:model", mDefaultPrefix).hasTag("%1$s:itext", mDefaultPrefix)) {
mFormTag.gotoRoot().gotoTag("h:head/%1$s:model", mDefaultPrefix).addTag(mDefaultPrefix + ":itext");
}
writeTranslations(null);
} else {
if (mFormTag.gotoRoot().gotoTag("h:head/%1$s:model", mDefaultPrefix).hasTag("%1$s:itext", mDefaultPrefix)) {
mFormTag.gotoRoot().gotoTag("h:head/%1$s:model/%1$s:itext", mDefaultPrefix).delete();
}
}
writeInstance(null);
writeBinds();
writeBody(null);
// Return XML for consumption
return mFormTag.toBytes();
}
private static void writeBody(Field incomingField) throws GroupHasNoChildrenException
{
Iterator<Field> it;
if (incomingField == null) {
it = Collect.getInstance().getFormBuilderState().getFields().iterator();
mFormTag.gotoRoot().gotoTag("h:body");
} else {
it = incomingField.getChildren().iterator();
}
while (it.hasNext()) {
Field field = it.next();
mFormTag.addTag(field.getType());
// Support for repeat (nodeset) references as well as regular references
if (field.hasXPath()) {
if (field.getType().equals("repeat")) {
mFormTag.getCurrentTag().setAttribute(XForm.Attribute.NODESET, field.getXPath());
} else {
mFormTag.getCurrentTag().setAttribute(XForm.Attribute.REFERENCE, field.getXPath());
}
}
// Multiple field types
if (field.getAttributes().containsKey(XForm.Attribute.APPEARANCE))
mFormTag.getCurrentTag().setAttribute(XForm.Attribute.APPEARANCE, field.getAttributes().get(XForm.Attribute.APPEARANCE));
// Upload control fields only
if (field.getAttributes().containsKey(XForm.Attribute.MEDIA_TYPE))
mFormTag.getCurrentTag().setAttribute(XForm.Attribute.MEDIA_TYPE, field.getAttributes().get(XForm.Attribute.MEDIA_TYPE));
// If the label does not reference an itext translation then attempt to output a regular label
if (field.getLabel().getRef() == null) {
// We don't gotoParent() after adding a tag with a text value (see longer comment in writeInstance)
if (field.getLabel().toString().length() > 0) {
/*
* Special support for labels that take advantage of XForm output from instance fields. E.g.,
* <label>review widget. is your email still <output value="/widgets/regex"/>?</label>
*/
mFormTag.addTag(XMLDoc.from("<label>" + FieldText.encodeXMLEntities(field.getLabel().toString().replace("xmlns=\"http://www.w3.org/2002/xforms\" ", "")) + "</label>", false));
}
} else {
mFormTag.addTag("label").addAttribute(XForm.Attribute.REFERENCE, "jr:itext('" + field.getLabel().getRef() + "')").gotoParent();
}
// Do the same for hints
if (field.getHint().getRef() == null) {
if (field.getHint().toString().length() > 0) {
mFormTag.addTag(XMLDoc.from("<hint>" + FieldText.encodeXMLEntities(field.getHint().toString().replace("xmlns=\"http://www.w3.org/2002/xforms\" ", "")) + "</hint>", false));
}
} else {
mFormTag.addTag("hint").addAttribute(XForm.Attribute.REFERENCE, "jr:itext('" + field.getHint().getRef() + "')").gotoParent();
}
// Special support for item control fields
if (field.getType().equals("item")) {
if (field.getItemValue() == null)
mFormTag.addTag("value").setText("");
else
mFormTag.addTag("value").setText(field.getItemValue());
}
// Sanity check to make sure groups have children
if (field.getType().equals("group")) {
if (Field.isRepeatedGroup(field) && field.getRepeat().getChildren().size() == 0) {
throw new GroupHasNoChildrenException("The repeated group \"" + field.getLabel() + "\" does not contain any fields.\n\nYou must create a field within this group or remove it before saving the form.");
} else if (field.getChildren().size() == 0) {
throw new GroupHasNoChildrenException("The group \"" + field.getLabel() + "\" does not contain any fields.\n\nYou must create a field within this group or remove it before saving the form.");
}
}
writeBody(field);
mFormTag.gotoParent();
}
}
private static void writeBinds()
{
Iterator<Bind> it = Collect.getInstance().getFormBuilderState().getBinds().iterator();
mFormTag.gotoRoot().gotoTag("h:head/%1$s:model", mDefaultPrefix);
while (it.hasNext()) {
Bind bind = it.next();
if (bind.hasUnhandledAttribute())
if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "bind " + bind.getXPath() + " has unhandled attributes that will not be written; data will be lost!");
mFormTag.addTag("bind").addAttribute("nodeset", bind.getXPath());
// The following are conditional attributes
if (bind.getType() != null) mFormTag.getCurrentTag().setAttribute("type", bind.getType());
if (bind.isReadonly()) mFormTag.getCurrentTag().setAttribute("readonly", bind.getReadonly());
if (bind.isRequired()) mFormTag.getCurrentTag().setAttribute("required", bind.getRequired());
if (bind.getPreload() != null && bind.getPreloadParams() != null) {
mFormTag.getCurrentTag().setAttribute("jr:preload", bind.getPreload());
mFormTag.getCurrentTag().setAttribute("jr:preloadParams", bind.getPreloadParams());
}
if (bind.getConstraint() != null) mFormTag.getCurrentTag().setAttribute("constraint", bind.getConstraint());
if (bind.getConstraintMsg() != null) mFormTag.getCurrentTag().setAttribute("jr:constraintMsg", bind.getConstraintMsg());
if (bind.getRelevant() != null) mFormTag.getCurrentTag().setAttribute("relevant", bind.getRelevant());
if (bind.getCalculate() != null) mFormTag.addAttribute("calculate", bind.getCalculate());
// Make sure the next bind is inserted at the same level as this one
mFormTag.gotoParent();
}
}
private static void writeInstance(Instance incomingInstance)
{
Iterator<Instance> it;
if (incomingInstance == null) {
it = Collect.getInstance().getFormBuilderState().getInstance().iterator();
// Initialize the instance root (only done once)
mFormTag.gotoRoot().gotoTag("h:head/%1$s:model/%1$s:instance", mDefaultPrefix);
mFormTag.addTag(mInstanceRoot).addAttribute("id", mInstanceRootId);
} else {
it = incomingInstance.getChildren().iterator();
}
while (it.hasNext()) {
Instance instance = it.next();
if (instance.getChildren().isEmpty()) {
/*
* For some reason unknown to me we can only call gotoParent() when adding
* an empty tag. Calling it after adding an empty tag OR a tag with a text
* value causes the nesting to get screwed up. This doesn't make any sense
* to me. It might be a bug with xmltool.
*/
if (instance.getDefaultValue().length() == 0) {
mFormTag.addTag(instance.getName());
mFormTag.gotoParent();
} else {
mFormTag.addTag(instance.getName()).setText(instance.getDefaultValue());
}
} else {
// Likely a repeated data set
mFormTag.addTag(instance.getName()).addAttribute(XForm.Attribute.JR_TEMPLATE, "");
writeInstance(instance);
mFormTag.gotoParent();
}
}
}
private static void writeTranslations(Translation i18n)
{
Iterator<Translation> it;
if (i18n == null) {
Collections.sort(Collect.getInstance().getFormBuilderState().getTranslations(), new TranslationSortByDefault());
it = Collect.getInstance().getFormBuilderState().getTranslations().iterator();
} else {
it = i18n.getTexts().iterator();
}
while (it.hasNext()) {
Translation t = it.next();
if (t.isGroup()) {
// Only write out sets that have translations
if (!t.getTexts().isEmpty()) {
mFormTag.gotoRoot().gotoTag("h:head/%1$s:model/%1$s:itext", mDefaultPrefix);
mFormTag.addTag("translation").addAttribute(XForm.Attribute.LANGUAGE, t.getLang());
writeTranslations(t);
}
} else {
// Only write out translations that have content
if (t.getValue() instanceof String) {
mFormTag
.addTag("text").addAttribute(XForm.Attribute.ID, t.getId())
.addTag("value").setText(t.getValue())
.gotoParent();
}
}
}
}
}