package org.activityinfo.server.command.handler;
import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.activityinfo.legacy.shared.command.UpdateFormClass;
import org.activityinfo.legacy.shared.command.result.CommandResult;
import org.activityinfo.legacy.shared.command.result.VoidResult;
import org.activityinfo.legacy.shared.exception.CommandException;
import org.activityinfo.model.form.FormClass;
import org.activityinfo.model.form.FormField;
import org.activityinfo.model.legacy.CuidAdapter;
import org.activityinfo.model.resource.Resource;
import org.activityinfo.model.resource.ResourceId;
import org.activityinfo.model.resource.Resources;
import org.activityinfo.model.type.Cardinality;
import org.activityinfo.model.type.NarrativeType;
import org.activityinfo.model.type.barcode.BarcodeType;
import org.activityinfo.model.type.enumerated.EnumItem;
import org.activityinfo.model.type.enumerated.EnumType;
import org.activityinfo.model.type.expr.CalculatedFieldType;
import org.activityinfo.model.type.number.QuantityType;
import org.activityinfo.model.type.primitive.BooleanType;
import org.activityinfo.model.type.primitive.TextType;
import org.activityinfo.server.database.hibernate.entity.*;
import javax.persistence.EntityManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;
public class UpdateFormClassHandler implements CommandHandler<UpdateFormClass> {
private static final int MIN_GZIP_BYTES = 1024 * 5;
private static final Logger LOGGER = Logger.getLogger(UpdateFormClassHandler.class.getName());
private final PermissionOracle permissionOracle;
private final Provider<EntityManager> entityManager;
@Inject
public UpdateFormClassHandler(Provider<EntityManager> entityManager, PermissionOracle permissionOracle) {
this.entityManager = entityManager;
this.permissionOracle = permissionOracle;
}
@Override
public CommandResult execute(UpdateFormClass cmd, User user) throws CommandException {
int activityId = CuidAdapter.getLegacyIdFromCuid(cmd.getFormClassId());
Activity activity = entityManager.get().find(Activity.class, activityId);
permissionOracle.assertDesignPrivileges(activity.getDatabase(), user);
FormClass formClass = validateFormClass(cmd.getJson());
// Update the activity table with the JSON value
String json = cmd.getJson();
if(json.length() > MIN_GZIP_BYTES) {
activity.setGzFormClass(compressJson(json));
activity.setFormClass(null);
} else {
activity.setFormClass(json);
activity.setGzFormClass(null);
}
// we should not set it instead of user (looks very weird for end user if mode is changed because of some backend function)
// activity.setClassicView(false);
if (cmd.isSyncActivityEntities()) {
syncEntities(activity, formClass);
} else {
entityManager.get().persist(activity);
}
return new VoidResult();
}
private byte[] compressJson(String json) {
try {
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
GZIPOutputStream gzOut = new GZIPOutputStream(byteArrayOut);
OutputStreamWriter writer = new OutputStreamWriter(gzOut, Charsets.UTF_8);
writer.write(json);
writer.close();
byte[] bytes = byteArrayOut.toByteArray();
LOGGER.log(Level.INFO, "FormClass GZipped json size = " + bytes.length);
return bytes;
} catch(IOException e) {
throw new RuntimeException(e);
}
}
private FormClass validateFormClass(String json) {
try {
Resource resource = Resources.fromJson(json);
FormClass formClass = FormClass.fromResource(resource);
return formClass;
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Invalid FormClass json: " + e.getMessage(), e);
throw new CommandException();
}
}
/**
* Synchronize this FormClass representation with the legacy indicators and attributes
* format. We need to maintain a dual-write layer until the transition from indicators and
* attributes is complete.
*
*/
private void syncEntities(Activity activity, FormClass formClass) {
activity.setName(formClass.getLabel());
List<FormFieldEntity> fields = new ArrayList<>();
fields.addAll(activity.getIndicators());
fields.addAll(activity.getAttributeGroups());
Map<ResourceId, FormFieldEntity> entityMap = Maps.newHashMap();
for(FormFieldEntity field : fields) {
entityMap.put(field.getFieldId(), field);
}
Set<ResourceId> builtinFields = Sets.newHashSet();
for(int fieldIndex : CuidAdapter.BUILTIN_FIELDS) {
builtinFields.add(CuidAdapter.field(formClass.getId(), fieldIndex));
}
int sortOrder = 1;
for(FormField field : formClass.getFields()) {
if(!builtinFields.contains(field.getId())) {
FormFieldEntity fieldEntity = entityMap.get(field.getId());
if (fieldEntity == null) {
createNewEntity(activity, field, sortOrder);
} else {
updateEntity(fieldEntity, field, sortOrder);
entityMap.remove(field.getId());
}
sortOrder++;
}
}
// delete any entities that were not matched to FormFields
for(FormFieldEntity entity : entityMap.values()) {
entity.delete();
}
}
private void createNewEntity(Activity activity, FormField field, int sortOrder) {
if(field.getType() instanceof EnumType) {
createAttributeGroup(activity, field, sortOrder);
} else {
createIndicator(activity, field, sortOrder);
}
}
private void updateEntity(FormFieldEntity fieldEntity, FormField field, int sortOrder) {
if(fieldEntity instanceof AttributeGroup) {
updateAttributeGroup((AttributeGroup) fieldEntity, field, sortOrder);
} else {
updateIndicator((Indicator)fieldEntity, field, sortOrder);
}
}
private void createIndicator(Activity activity, FormField field, int sortOrder) {
Indicator indicator = new Indicator();
indicator.setId(CuidAdapter.getLegacyIdFromCuid(field.getId()));
indicator.setActivity(activity);
updateIndicatorProperties(indicator, field, sortOrder);
entityManager.get().persist(indicator);
}
private void updateIndicator(Indicator indicator, FormField field, int sortOrder) {
updateIndicatorProperties(indicator, field, sortOrder);
}
private void updateIndicatorProperties(Indicator indicator, FormField field, int sortOrder) {
indicator.setName(truncate(field.getLabel(), 255));
indicator.setMandatory(field.isRequired());
indicator.setDescription(field.getDescription());
indicator.setSortOrder(sortOrder);
indicator.setNameInExpression(field.getCode());
indicator.setSkipExpression(field.getRelevanceConditionExpression());
indicator.setCalculatedAutomatically(field.getType() instanceof CalculatedFieldType);
if (field.getType() instanceof QuantityType) {
indicator.setType(QuantityType.TYPE_CLASS.getId());
indicator.setUnits(((QuantityType) field.getType()).getUnits());
} else if(field.getType() instanceof NarrativeType) {
indicator.setType(NarrativeType.TYPE_CLASS.getId());
} else if (field.getType() instanceof BooleanType) {
indicator.setType(BooleanType.TYPE_CLASS.getId());
} else if (field.getType() instanceof CalculatedFieldType) {
CalculatedFieldType type = (CalculatedFieldType) field.getType();
indicator.setType(QuantityType.TYPE_CLASS.getId());
indicator.setExpression(type.getExpression().getExpression());
} else if (field.getType() instanceof BarcodeType) {
indicator.setType(TextType.TYPE_CLASS.getId());
} else {
indicator.setType(field.getType().getTypeClass().getId());
}
}
private String truncate(String label, int maxLength) {
if(label.length() > maxLength) {
return label.substring(0, maxLength);
} else {
return label;
}
}
private FormFieldEntity createAttributeGroup(Activity activity, FormField field, int sortOrder) {
EnumType type = (EnumType) field.getType();
AttributeGroup group = new AttributeGroup();
group.setId(CuidAdapter.getLegacyIdFromCuid(field.getId()));
updateAttributeGroupProperties(group, field, sortOrder);
entityManager.get().persist(group);
activity.getAttributeGroups().add(group);
updateAttributes(group, type);
return group;
}
private void updateAttributeGroup(AttributeGroup group, FormField field, int sortOrder) {
updateAttributeGroupProperties(group, field, sortOrder);
updateAttributes(group, (EnumType) field.getType());
}
private void updateAttributeGroupProperties(AttributeGroup group, FormField field, int sortOrder) {
group.setName(truncate(field.getLabel(), 255));
group.setMandatory(field.isRequired());
group.setMultipleAllowed(((EnumType) field.getType()).getCardinality() == Cardinality.MULTIPLE);
group.setSortOrder(sortOrder);
}
private void updateAttributes(AttributeGroup group, EnumType type) {
Map<ResourceId, Attribute> attributeMap = new HashMap<>();
for(Attribute attribute : group.getAttributes()) {
attributeMap.put(attribute.getResourceId(), attribute);
}
// add/update present attributes
int sortOrder = 1;
for(EnumItem item : type.getValues()) {
Attribute attribute = attributeMap.get(item.getId());
if(attribute == null) {
attribute = new Attribute();
attribute.setGroup(group);
attribute.setId(CuidAdapter.getLegacyIdFromCuid(item.getId()));
attribute.setName(truncate(item.getLabel(), 255));
attribute.setSortOrder(sortOrder);
entityManager.get().persist(attribute);
group.getAttributes().add(attribute);
} else {
// update properties
attribute.setName(item.getLabel());
attribute.setSortOrder(sortOrder);
}
sortOrder++;
}
// remove deleted
Set<ResourceId> deleted = Sets.newHashSet(attributeMap.keySet());
for(EnumItem item : type.getValues()) {
deleted.remove(item.getId());
}
for (ResourceId deletedAttribute : deleted) {
attributeMap.get(deletedAttribute).delete();
}
}
}