// Copyright 2015 The Project Buendia Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at: http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distrib- // uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES // OR CONDITIONS OF ANY KIND, either express or implied. See the License for // specific language governing permissions and limitations under the License. package org.openmrs.projectbuendia.webservices.rest; import org.openmrs.Concept; import org.openmrs.ConceptDatatype; import org.openmrs.Encounter; import org.openmrs.EncounterRole; import org.openmrs.EncounterType; import org.openmrs.Location; import org.openmrs.Obs; import org.openmrs.Order; import org.openmrs.Patient; import org.openmrs.Provider; import org.openmrs.User; import org.openmrs.api.ConceptService; import org.openmrs.api.EncounterService; import org.openmrs.api.ObsService; import org.openmrs.api.context.Context; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.projectbuendia.Utils; import org.openmrs.projectbuendia.VisitObsValue; import javax.annotation.Nullable; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; /** Utility for adding observations parsed from JSON. */ public class ObservationsHandler { /** * For consistency these should be accepted as XForm instances, but as a * short-term fix we allow observations to be expressed in this JSON format: * <pre> * "observations": [ * { * "question_uuid": "xxxx-...', * # then ONE of the following three answer_* fields: * "answer_date": "2013-01-30" * "answer_number": 40 * "answer_uuid": "xxxx-...." * }, ... * ] * </pre> */ private static final String KEY_QUESTION_UUID = "question_uuid"; private static final String KEY_ANSWER = "answer_value"; /** * Adds a new encounter with the given observations and orders. * @param observations a list of observations * @param orderUuids a list of order UUIDs * @param patient the patient for whom to add the encounter * @param encounterTime the time of the encounter * @param changeMessage a message to be recorded with the observation * @param encounterTypeName the OpenMRS name for the encounter type, configured in OpenMRS * @param locationUuid the UUID of the location where the encounter happened * @param entererUuid optional. The UUID of the provider who entered the encounter. */ public static Encounter addEncounter(List observations, List orderUuids, Patient patient, Date encounterTime, String changeMessage, String encounterTypeName, String locationUuid, @Nullable String entererUuid) { // OpenMRS will reject the encounter if the time is in the past, even if // the client's clock is off by only one millisecond; work around this. encounterTime = Utils.fixEncounterDateTime(encounterTime); EncounterService encounterService = Context.getEncounterService(); final Location location = Context.getLocationService().getLocationByUuid(locationUuid); if (location == null) { throw new InvalidObjectDataException("Location not found: " + locationUuid); } EncounterType encounterType = encounterService.getEncounterType(encounterTypeName); if (encounterType == null) { throw new InvalidObjectDataException("Encounter type not found: " + encounterTypeName); } @Nullable User enterer = Utils.getUserFromProviderUuid(entererUuid); List<Obs> obsList = new ArrayList<>(); if (observations != null) { for (Object observation : observations) { Obs obs = jsonObservationToObs(observation, patient, encounterTime, location); obs.setCreator(enterer); obsList.add(obs); } } if (orderUuids != null) { for (Object item : orderUuids) { Obs obs = orderUuidToObs((String) item, patient, encounterTime, location); obs.setCreator(enterer); obsList.add(obs); } } // Write the encounter and all the observations to the database. Encounter encounter = new Encounter(); encounter.setEncounterDatetime(encounterTime); encounter.setPatient(patient); encounter.setLocation(location); encounter.setEncounterType(encounterType); // Maybe set provider Provider provider = Utils.getProviderFromUser(enterer); if (provider != null) { EncounterRole encounterRole = Context.getEncounterService() .getEncounterRoleByUuid(EncounterRole.UNKNOWN_ENCOUNTER_ROLE_UUID); encounter.setProvider(encounterRole, provider); } encounter = encounterService.saveEncounter(encounter); ObsService obsService = Context.getObsService(); for (Obs obs : obsList) { if (obs != null) { encounter.addObs(obs); obsService.saveObs(obs, changeMessage); } } return encounter; } public static Obs jsonObservationToObs(Object jsonObservation, Patient patient, Date encounterTime, Location location) { Map observationObject = (Map) jsonObservation; String questionUuid = (String) observationObject.get(KEY_QUESTION_UUID); ConceptService conceptService = Context.getConceptService(); Concept questionConcept = conceptService.getConceptByUuid(questionUuid); if (questionConcept == null) { throw new InvalidObjectDataException("Question concept not found: " + questionUuid); } Obs obs = new Obs(patient, questionConcept, encounterTime, location); String answer = (String) observationObject.get(KEY_ANSWER); ConceptDatatype conceptDatatype = questionConcept.getDatatype(); try { // OpenMRS's setValueAsString method parses strings using the local server timezone, // instead of GMT. This can result in date values getting shifted. We work around this // by intercepting datatypes that are dates, formatting using a UTC formatter, and // storing the value directly. if (conceptDatatype.isDate()) { obs.setValueDate(Utils.YYYYMMDD_UTC_FORMAT.parse(answer)); } else if (conceptDatatype.isTime() || conceptDatatype.isDateTime()) { throw new RuntimeException("Timestamps aren't supported yet."); } else { obs.setValueAsString(answer); } } catch (ParseException e) { throw new InvalidObjectDataException("Couldn't parse value '" + answer + "'."); } return obs; } private static Obs orderUuidToObs(String orderUuid, Patient patient, Date encounterTime, Location location) { Order order = Context.getOrderService().getOrderByUuid(orderUuid); if (order == null) { throw new InvalidObjectDataException("Order not found: " + orderUuid); } Obs obs = new Obs(patient, DbUtil.getOrderExecutedConcept(), encounterTime, location); obs.setOrder(order); obs.setValueNumeric(1d); return obs; } public static SimpleObject obsToJson(Obs obs) { SimpleObject object = new SimpleObject() .add("uuid", obs.getUuid()) .add("voided", obs.isVoided()); if (obs.isVoided()) { return object; } object .add("patient_uuid", obs.getPerson().getUuid()) .add("encounter_uuid", obs.getEncounter().getUuid()) .add("concept_uuid", obs.getConcept().getUuid()) .add("timestamp", Utils.toIso8601(obs.getObsDatetime())); Provider provider = Utils.getProviderFromUser(obs.getCreator()); object.add("enterer_uuid", provider != null ? provider.getUuid() : null); boolean isExecutedOrder = DbUtil.getOrderExecutedConcept().equals(obs.getConcept()) && obs.getOrder() != null; if (isExecutedOrder) { // As far as the client knows, a chain of orders is represented by the root order's // UUID, so we have to work back through the chain or orders to get the root UUID. // Normally, the client will only ever supply observations for the root order ID, but // in the event that an order is marked as executed on the server (for example) we don't // want that to mean that an order execution gets missed. object.add("value", Utils.getRootOrder(obs.getOrder()).getUuid()); } else { object.add("value", ObservationsHandler.obsValueToString(obs)); } return object; } public static String obsValueToString(Obs obs) { return VisitObsValue.visit( obs, new VisitObsValue.ObsValueVisitor<String>() { @Override public String visitCoded(Concept value) { return value.getUuid(); } @Override public String visitNumeric(Double value) { return "" + value; } @Override public String visitBoolean(Boolean value) { return "" + value; } @Override public String visitText(String value) { return value; } @Override public String visitDate(Date value) { return Utils.YYYYMMDD_UTC_FORMAT.format(value); } @Override public String visitDateTime(Date value) { return Utils.toIso8601(value); } }); } }