package org.activityinfo.server.endpoint.odk.build;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.activityinfo.io.xform.form.Bind;
import org.activityinfo.io.xform.form.BindingType;
import org.activityinfo.io.xform.form.Body;
import org.activityinfo.io.xform.form.BodyElement;
import org.activityinfo.io.xform.form.Instance;
import org.activityinfo.io.xform.form.InstanceElement;
import org.activityinfo.io.xform.form.Model;
import org.activityinfo.io.xform.form.Translation;
import org.activityinfo.io.xform.form.XForm;
import org.activityinfo.model.form.FormClass;
import org.activityinfo.model.form.FormField;
import org.activityinfo.model.resource.ResourceId;
import org.activityinfo.model.type.ParametrizedFieldType;
import org.activityinfo.model.type.geo.GeoPointType;
import org.activityinfo.model.type.primitive.TextType;
import org.activityinfo.server.endpoint.odk.OdkField;
import org.activityinfo.server.endpoint.odk.OdkFormFieldBuilder;
import org.activityinfo.server.endpoint.odk.OdkFormFieldBuilderFactory;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static org.activityinfo.model.legacy.CuidAdapter.END_DATE_FIELD;
import static org.activityinfo.model.legacy.CuidAdapter.GPS_FIELD;
import static org.activityinfo.model.legacy.CuidAdapter.LOCATION_NAME_FIELD;
import static org.activityinfo.model.legacy.CuidAdapter.START_DATE_FIELD;
import static org.activityinfo.model.legacy.CuidAdapter.field;
import static org.activityinfo.server.endpoint.odk.OdkHelper.isLocation;
/**
* Constructs an XForm from a FormClass
*/
public class XFormBuilder {
private OdkFormFieldBuilderFactory factory;
private String userId;
private FormClass formClass;
private List<OdkField> fields;
private ResourceId startDateFieldId;
private ResourceId endDateFieldId;
private Set<ResourceId> dateFields;
private ResourceId locationNameFieldId;
private ResourceId gpsFieldId;
private XPathBuilder xPathBuilder;
private XForm xform;
public XFormBuilder(OdkFormFieldBuilderFactory factory) {
this.factory = factory;
}
public XFormBuilder setUserId(String userId) {
this.userId = userId;
return this;
}
public XForm build(FormClass formClass) {
this.formClass = formClass;
startDateFieldId = field(formClass.getId(), START_DATE_FIELD);
endDateFieldId = field(formClass.getId(), END_DATE_FIELD);
dateFields = Sets.newHashSet(startDateFieldId, endDateFieldId);
locationNameFieldId = field(formClass.getId(), LOCATION_NAME_FIELD);
gpsFieldId = field(formClass.getId(), GPS_FIELD);
fields = createFieldBuilders(formClass);
xPathBuilder = new XPathBuilder(fields);
xform = new XForm();
xform.getHead().setTitle(formClass.getLabel());
xform.getHead().setModel(createModel());
xform.setBody(createBody());
return xform;
}
private List<OdkField> createFieldBuilders(FormClass formClass) {
fields = new ArrayList<>();
for (FormField field : formClass.getFields()) {
if(field.isVisible() && isValid(field)) {
OdkFormFieldBuilder builder = factory.get(field.getType());
if (builder != null) {
fields.add(new OdkField(field, builder));
}
}
}
return fields;
}
private boolean isValid(FormField field) {
if(!(field.getType() instanceof ParameterizedType)) {
return true;
}
ParametrizedFieldType type = (ParametrizedFieldType) field.getType();
return type.isValid();
}
private Model createModel() {
Model model = new Model();
model.getItext().getTranslations().add(Translation.defaultTranslation());
model.setInstance(createInstance());
model.getBindings().addAll(createBindings());
return model;
}
private Instance createInstance() {
InstanceElement data = new InstanceElement("data");
data.setId(formClass.getId().asString());
data.addChild(
new InstanceElement("meta",
new InstanceElement("instanceID"),
new InstanceElement("userID", userId)));
data.addChild(new InstanceElement("field_" + startDateFieldId.asString()));
data.addChild(new InstanceElement("field_" + endDateFieldId.asString()));
for (OdkField field : fields) {
if (isLocation(formClass, field.getModel())) {
data.addChild(new InstanceElement("field_" + locationNameFieldId.asString()));
data.addChild(new InstanceElement("field_" + gpsFieldId.asString()));
} else if (!dateFields.contains(field.getModel().getId())) {
data.addChild(new InstanceElement(field.getRelativeFieldName()));
}
}
return new Instance(data);
}
private Collection<Bind> createBindings() {
List<Bind> bindings = Lists.newArrayList();
bindings.add(instanceIdBinding());
bindings.add(startDate());
bindings.add(endDate());
for (OdkField field : fields) {
// As a transitional hack, populate the startDate and endDate of the "activity"
// with the start/end date of interview
if (isLocation(formClass, field.getModel())) {
bindings.add(locationNameField());
bindings.add(gpsField());
} else if (!dateFields.contains(field.getModel().getId())) {
Bind bind = new Bind();
bind.setNodeSet(field.getAbsoluteFieldName());
bind.setType(field.getBuilder().getModelBindType());
if (field.getModel().isReadOnly()) {
bind.setReadonly(XPathBuilder.TRUE);
}
//TODO Fix this
//bind.calculate = formField.getExpression();
bind.setRelevant(xPathBuilder.build(field.getModel().getRelevanceConditionExpression()));
if (field.getModel().isRequired()) {
bind.setRequired(XPathBuilder.TRUE);
}
bindings.add(bind);
}
}
return bindings;
}
private Body createBody() {
Body body = new Body();
for (OdkField field : fields) {
if (isLocation(formClass, field.getModel())) {
body.addElement(createPresentationElement(locationName(field.getModel())));
body.addElement(createPresentationElement(gps(field.getModel())));
} else if (field.getModel().isVisible() && !dateFields.contains(field.getModel().getId())) {
body.addElement(createPresentationElement(field));
}
}
return body;
}
private BodyElement createPresentationElement(OdkField formField) {
return formField.getBuilder().createBodyElement(
formField.getAbsoluteFieldName(),
formField.getModel().getLabel(),
formField.getModel().getDescription());
}
private Bind instanceIdBinding() {
Bind bind = new Bind();
bind.setNodeSet("/data/meta/instanceID");
bind.setType(BindingType.STRING);
bind.setReadonly(XPathBuilder.TRUE);
bind.setCalculate("concat('uuid:',uuid())");
return bind;
}
private Bind startDate() {
Bind bind = new Bind();
bind.setNodeSet("/data/field_" + startDateFieldId.asString());
bind.setType(BindingType.DATETIME);
bind.setPreload("timestamp");
bind.setPreloadParams("start");
return bind;
}
private Bind endDate() {
Bind bind = new Bind();
bind.setNodeSet("/data/field_" + endDateFieldId.asString());
bind.setReadonly(XPathBuilder.TRUE);
bind.setPreload("timestamp");
bind.setPreloadParams("end");
return bind;
}
private Bind locationNameField() {
Bind bind = new Bind();
bind.setNodeSet("/data/field_" + locationNameFieldId.asString());
bind.setType(BindingType.STRING);
bind.setRequired(XPathBuilder.TRUE);
return bind;
}
private Bind gpsField() {
Bind bind = new Bind();
bind.setNodeSet("/data/field_" + gpsFieldId.asString());
bind.setType(BindingType.GEOPOINT);
return bind;
}
private OdkField locationName(FormField original) {
FormField formField = new FormField(locationNameFieldId);
formField.setType(TextType.INSTANCE);
formField.setLabel(original.getLabel());
return new OdkField(formField, factory.get(formField.getType()));
}
private OdkField gps(FormField original) {
FormField formField = new FormField(gpsFieldId);
formField.setType(GeoPointType.INSTANCE);
formField.setLabel("GPS coordinates (" + original.getLabel() + ")");
return new OdkField(formField, factory.get(formField.getType()));
}
}