package org.activityinfo.server.endpoint.odk; import com.google.api.client.util.Maps; import com.google.appengine.api.images.Image; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import com.google.inject.Inject; import org.activityinfo.legacy.shared.command.CreateLocation; import org.activityinfo.legacy.shared.command.result.VoidResult; import org.activityinfo.model.auth.AuthenticatedUser; import org.activityinfo.model.form.FormClass; import org.activityinfo.model.form.FormField; import org.activityinfo.model.form.FormInstance; import org.activityinfo.model.legacy.KeyGenerator; import org.activityinfo.model.resource.ResourceId; import org.activityinfo.model.type.FieldType; import org.activityinfo.model.type.FieldValue; import org.activityinfo.model.type.ReferenceType; import org.activityinfo.model.type.ReferenceValue; import org.activityinfo.model.type.geo.GeoPoint; import org.activityinfo.model.type.geo.GeoPointType; import org.activityinfo.model.type.image.ImageRowValue; import org.activityinfo.model.type.image.ImageValue; import org.activityinfo.model.type.primitive.TextValue; import org.activityinfo.server.authentication.ServerSideAuthProvider; import org.activityinfo.server.command.DispatcherSync; import org.activityinfo.server.command.ResourceLocatorSync; import org.activityinfo.server.database.hibernate.EntityManagerProvider; import org.activityinfo.server.database.hibernate.entity.Activity; import org.activityinfo.server.database.hibernate.entity.User; import org.activityinfo.server.endpoint.odk.xform.LegacyXFormInstance; import org.activityinfo.server.endpoint.odk.xform.XFormInstance; import org.activityinfo.server.endpoint.odk.xform.XFormInstanceImpl; import org.activityinfo.service.blob.BlobFieldStorageService; import org.activityinfo.service.blob.BlobId; import org.w3c.dom.Element; import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import static com.google.appengine.api.images.ImagesServiceFactory.makeImage; import static com.google.common.base.Optional.absent; import static com.google.common.base.Optional.of; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.CREATED; import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.activityinfo.model.legacy.CuidAdapter.GPS_FIELD; import static org.activityinfo.model.legacy.CuidAdapter.LOCATION_FIELD; import static org.activityinfo.model.legacy.CuidAdapter.LOCATION_NAME_FIELD; import static org.activityinfo.model.legacy.CuidAdapter.field; import static org.activityinfo.model.legacy.CuidAdapter.getLegacyIdFromCuid; import static org.activityinfo.model.legacy.CuidAdapter.locationInstanceId; import static org.activityinfo.model.legacy.CuidAdapter.newLegacyFormInstanceId; import static org.activityinfo.server.endpoint.odk.OdkFieldValueParserFactory.fromFieldType; import static org.activityinfo.server.endpoint.odk.OdkHelper.isLocation; @Path("/submission") public class FormSubmissionResource { private static final Logger LOGGER = Logger.getLogger(FormSubmissionResource.class.getName()); final private DispatcherSync dispatcher; final private ResourceLocatorSync locator; final private AuthenticationTokenService authenticationTokenService; final private ServerSideAuthProvider authProvider; // Necessary for 2.8 XForms, remove afterwards final private EntityManagerProvider entityManager; // Necessary for 2.8 XForms, remove afterwards final private BlobFieldStorageService blobFieldStorageService; final private InstanceIdService instanceIdService; final private SubmissionArchiver submissionArchiver; @Inject public FormSubmissionResource(DispatcherSync dispatcher, ResourceLocatorSync locator, AuthenticationTokenService authenticationTokenService, ServerSideAuthProvider authProvider, // Necessary for 2.8 XForms, remove afterwards EntityManagerProvider entityManager, // Necessary for 2.8 XForms, remove afterwards BlobFieldStorageService blobFieldStorageService, InstanceIdService instanceIdService, SubmissionArchiver submissionArchiver) { this.dispatcher = dispatcher; this.locator = locator; this.authenticationTokenService = authenticationTokenService; this.authProvider = authProvider; // Necessary for 2.8 XForms, remove afterwards this.entityManager = entityManager; // Necessary for 2.8 XForms, remove afterwards this.blobFieldStorageService = blobFieldStorageService; this.instanceIdService = instanceIdService; this.submissionArchiver = submissionArchiver; } @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_XML) public Response submit(byte bytes[]) { final boolean legacy; // Necessary for 2.8 XForms, remove afterwards AuthenticatedUser user; XFormInstance instance; FormClass formClass; try { instance = new XFormInstanceImpl(bytes); user = authenticationTokenService.authenticate(instance.getAuthenticationToken()); formClass = locator.getFormClass(instance.getFormClassId()); } catch (IllegalStateException illegalStateException) { // Necessary for 2.8 XForms, remove afterwards if ("Cannot find element userID".equals(illegalStateException.getMessage())) { LOGGER.log(Level.INFO, "User ID not found, trying to parse submission as legacy form instance"); instance = new LegacyXFormInstance(bytes); int formClassId = getLegacyIdFromCuid(instance.getFormClassId()); User owner = entityManager.get().find(Activity.class, formClassId).getDatabase().getOwner(); authProvider.set(owner); user = new AuthenticatedUser("", (int) owner.getId(), "@"); formClass = locator.getFormClass(instance.getFormClassId()); } else throw illegalStateException; // Legacy code ends here } legacy = instance instanceof LegacyXFormInstance; // Necessary for 2.8 XForms, remove afterwards ResourceId formId = newLegacyFormInstanceId(formClass.getId()); FormInstance formInstance = new FormInstance(formId, formClass.getId()); String instanceId = instance.getId(); LOGGER.log(Level.INFO, "Saving XForm " + instance.getId() + " as " + formId); for (FormField formField : formClass.getFields()) { Optional<Element> element = instance.getFieldContent(formField.getId()); if (element.isPresent()) { formInstance.set(formField.getId(), tryParse(formInstance, formField, element.get(), legacy)); } else if (isLocation(formClass, formField)) { FieldType fieldType = formField.getType(); Optional<Element> gpsField = instance.getFieldContent(field(formClass.getId(), GPS_FIELD)); Optional<Element> nameField = instance.getFieldContent(field(formClass.getId(), LOCATION_NAME_FIELD)); if (fieldType instanceof ReferenceType && nameField.isPresent()) { ResourceId locationFieldId = field(formClass.getId(), LOCATION_FIELD); int newLocationId = new KeyGenerator().generateInt(); ResourceId locationFormClassId = Iterables.getOnlyElement(((ReferenceType) fieldType).getRange()); int locationTypeId = getLegacyIdFromCuid(locationFormClassId); FieldValue fieldValue = new ReferenceValue(locationInstanceId(newLocationId)); String name = OdkHelper.extractText(nameField.get()); Optional<GeoPoint> geoPoint = parseLocation(gpsField, legacy); formInstance.set(locationFieldId, fieldValue); createLocation(newLocationId, locationTypeId, name, geoPoint); } } } if (!instanceIdService.exists(instanceId)) { for (FieldValue fieldValue : formInstance.getFieldValueMap().values()) { if (fieldValue instanceof ImageValue) { persistImageData(user, instance, (ImageValue) fieldValue); } } locator.persist(formInstance); instanceIdService.submit(instanceId); } // Backup the original XForm in case something went wrong with processing submissionArchiver.backup(formClass.getId(), formId, ByteSource.wrap(bytes)); return Response.status(CREATED).build(); } private FieldValue tryParse(FormInstance formInstance, FormField formField, Element element, boolean legacy) { try { OdkFieldValueParser odkFieldValueParser = fromFieldType(formField.getType(), legacy); return odkFieldValueParser.parse(element); } catch (Exception e) { String text = OdkHelper.extractText(element); if (text == null) { LOGGER.log(Level.SEVERE, "Malformed Element in form instance prevents parsing", e); } else if (!text.equals("")) { LOGGER.log(Level.WARNING, "Can't parse form instance contents, storing as text", e); formInstance.set(formField.getId(), TextValue.valueOf(text)); } } return null; } private Optional<GeoPoint> parseLocation(Optional<Element> element, boolean legacy) { Preconditions.checkNotNull(element); if (element.isPresent()) { try { OdkFieldValueParser odkFieldValueParser = fromFieldType(GeoPointType.INSTANCE, legacy); return of((GeoPoint) odkFieldValueParser.parse(element.get())); } catch (Exception e) { LOGGER.log(Level.WARNING, "Can't parse form submission location data", e); } } return absent(); } private void persistImageData(AuthenticatedUser user, XFormInstance instance, ImageValue fieldValue) { ImageRowValue imageRowValue = fieldValue.getValues().get(0); if (imageRowValue.getFilename() != null) { try { BodyPart bodyPart = ((XFormInstanceImpl) instance).findBodyPartByFilename(imageRowValue.getFilename()); Image image = makeImage(ByteStreams.toByteArray(bodyPart.getInputStream())); String contentDisposition = bodyPart.getDisposition(); String mimeType = bodyPart.getContentType(); ByteSource byteSource = ByteSource.wrap(image.getImageData()); imageRowValue.setMimeType(mimeType); imageRowValue.setHeight(image.getHeight()); imageRowValue.setWidth(image.getWidth()); blobFieldStorageService.put(user, contentDisposition, mimeType, new BlobId(imageRowValue.getBlobId()), byteSource); } catch (MessagingException messagingException) { LOGGER.log(Level.SEVERE, "Unable to parse input", messagingException); throw new WebApplicationException(Response.status(BAD_REQUEST).build()); } catch (IOException ioException) { LOGGER.log(Level.SEVERE, "Could not write image to GCS", ioException); throw new WebApplicationException(Response.status(SERVICE_UNAVAILABLE).build()); } } } private VoidResult createLocation(int id, int locationTypeId, String name, Optional<GeoPoint> geoPoint) { Preconditions.checkNotNull(name, geoPoint); Map<String, Object> properties = Maps.newHashMap(); properties.put("id", id); properties.put("locationTypeId", locationTypeId); properties.put("name", name); if (geoPoint.isPresent()) { properties.put("latitude", geoPoint.get().getLatitude()); properties.put("longitude", geoPoint.get().getLongitude()); } return dispatcher.execute(new CreateLocation(properties)); } }