// 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.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.Location; import org.openmrs.Patient; import org.openmrs.PersonAttribute; import org.openmrs.api.LocationService; import org.openmrs.api.PatientService; import org.openmrs.api.context.Context; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.api.Creatable; import org.openmrs.module.webservices.rest.web.resource.api.Deletable; import org.openmrs.module.webservices.rest.web.resource.api.Listable; import org.openmrs.module.webservices.rest.web.resource.api.Retrievable; import org.openmrs.module.webservices.rest.web.resource.api.Searchable; import org.openmrs.module.webservices.rest.web.resource.api.Updatable; import org.openmrs.module.webservices.rest.web.response.ResponseException; import org.projectbuendia.openmrs.webservices.rest.RestController; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; /** * REST API for locations (places where patients can be located). * <p/> * <p>Expected behavior: * <ul> * <li>GET /location returns all locations ({@link #getAll(RequestContext)}) * <li>GET /location/[UUID] returns a single location ({@link #retrieve(String, RequestContext)}) * <li>POST /location adds a location ({@link #create(SimpleObject, RequestContext)} * <li>POST /location/[UUID] updates a location ({@link #update(String, SimpleObject, * RequestContext)}) * <li>DELETE /location/[UUID] deletes a location ({@link #delete(String, String, RequestContext)}) * </ul> * <p/> * <p>Each operation accepts and returns locations in the following JSON form: * <p/> * <pre> * { * "uuid": “12345678-1234-1234-1234-123456789abc", * "names": { * “en”: “Kailahun”, * “fr”: “Kailahun” // (if other locales are available in the future) * } * "parent_uuid": “87654321-4321-4321-4321-cba9876543210" // parent location * } * </pre> * <p/> * <p>If an error occurs, the response will be in the form: * <pre> * { * "error": { * "message": "[error message]", * "code": "[breakpoint]", * "detail": "[stack trace]" * } * } * </pre> */ @Resource(name = RestController.REST_VERSION_1_AND_NAMESPACE + "/locations", supportedClass = Location.class, supportedOpenmrsVersions = "1.10.*,1.11.*") public class LocationResource implements Listable, Searchable, Retrievable, Creatable, Updatable, Deletable { // Known locations. // The root location. public static final String ROOT_UUID = "3449f5fe-8e6b-4250-bcaa-fca5df28ddbf"; public static final String TRIAGE_UUID = "3f75ca61-ec1a-4739-af09-25a84e3dd237"; // TODO/generalize: The facility name should not be hardcoded here. private static final String ROOT_NAME = "ROOT LOCATION"; // The hard-coded zones. These are (name, UUID) pairs, and are children of // the root location. TODO/generalize: Consider generalizing these zones // as well. They may be common in infectious disease deployments but don't // make sense for all situations. private static final String[][] ZONE_NAMES_AND_UUIDS = { {"Triage Zone", TRIAGE_UUID}, {"Suspected Zone", "2f1e2418-ede6-481a-ad80-b9939a7fde8e"}, {"Probable Zone", "3b11e7c8-a68a-4a5f-afb3-a4a053592d0e"}, {"Confirmed Zone", "b9038895-9c9d-4908-9e0d-51fd535ddd3c"}, {"Morgue", "4ef642b9-9843-4d0d-9b2b-84fe1984801f"}, {"Discharged", "d7ca63c3-6ea0-4357-82fd-0910cc17a2cb"}, }; private static Log log = LogFactory.getLog(PatientResource.class); static final RequestLogger logger = RequestLogger.LOGGER; private final LocationService locationService; public LocationResource() { locationService = Context.getLocationService(); Location root = getRootLocation(locationService); ensureZonesExist(locationService, root); } private static Location getRootLocation(LocationService service) { Location location = service.getLocationByUuid(ROOT_UUID); if (location == null) { log.info("Creating root location"); location = new Location(); location.setName(ROOT_NAME); location.setUuid(ROOT_UUID); location.setDescription(ROOT_NAME); service.saveLocation(location); } return location; } private static void ensureZonesExist(LocationService service, Location root) { for (String[] nameAndUuid : ZONE_NAMES_AND_UUIDS) { String name = nameAndUuid[0]; String uuid = nameAndUuid[1]; Location zone = service.getLocationByUuid(uuid); if (zone == null) { log.info("Creating zone location " + name); zone = new Location(); zone.setName(name); zone.setUuid(uuid); zone.setDescription(name); zone.setParentLocation(root); service.saveLocation(zone); } } } @Override public Object create(SimpleObject request, RequestContext context) throws ResponseException { try { logger.request(context, this, "create", request); Object result = createInner(request); logger.reply(context, this, "create", result); return result; } catch (Exception e) { logger.error(context, this, "create", e); throw e; } } private Object createInner(SimpleObject request) throws ResponseException { if (request.containsKey("uuid")) { throw new InvalidObjectDataException("\"uuid\" key is specified but not allowed"); } String parentUuid = (String) request.get("parent_uuid"); if (parentUuid == null) { throw new InvalidObjectDataException("Required \"parent_uuid\" key is missing"); } Location parent = locationService.getLocationByUuid(parentUuid); if (parent == null) { throw new InvalidObjectDataException("No parent location found with UUID " + parentUuid); } Location location = new Location(); updateNames(request, location); location.setParentLocation(parent); return locationService.saveLocation(location); } private void updateNames(SimpleObject request, Location location) { Map names = (Map) request.get("names"); if (names == null || names.isEmpty()) { throw new InvalidObjectDataException("No name specified for new location"); } // TODO(nfortescue): work out if locations can be localized. String name = (String) names.values().iterator().next(); if (name.isEmpty()) { throw new InvalidObjectDataException("Empty name specified for new location"); } Location duplicate = locationService.getLocation(name); if (duplicate != null) { throw new InvalidObjectDataException( String.format("Another location already has the name \"%s\"", name)); } location.setName(name); location.setDescription(name); } @Override public SimpleObject getAll(RequestContext context) throws ResponseException { try { logger.request(context, this, "getAll"); SimpleObject result = getAllInner(); logger.reply(context, this, "getAll", result); return result; } catch (Exception e) { logger.error(context, this, "getAll", e); throw e; } } @Override public SimpleObject search(RequestContext context) throws ResponseException { try { logger.request(context, this, "search"); SimpleObject result = searchInner(context); logger.reply(context, this, "search", null); return result; } catch (Exception e) { logger.error(context, this, "search", e); throw e; } } @Override public Object retrieve(String uuid, RequestContext context) throws ResponseException { try { logger.request(context, this, "retrieve", uuid); Object result = retrieveInner(uuid); logger.reply(context, this, "retrieve", result); return result; } catch (Exception e) { logger.error(context, this, "retrieve", e); throw e; } } private Object retrieveInner(String uuid) throws ResponseException { Location location = locationService.getLocationByUuid(uuid); return location == null ? null : locationToJson(location); } private SimpleObject locationToJson(Location location) { SimpleObject result = new SimpleObject(); if (location == null) { throw new NullPointerException(); } result.add("uuid", location.getUuid()); Location parentLocation = location.getParentLocation(); if (parentLocation != null) { result.add("parent_uuid", parentLocation.getUuid()); } SimpleObject names = new SimpleObject(); names.add("en", location.getDisplayString()); result.add("names", names); return result; } @Override public List<Representation> getAvailableRepresentations() { return Arrays.asList(Representation.DEFAULT); } @Override public Object update(String uuid, SimpleObject request, RequestContext context) throws ResponseException { try { logger.request(context, this, "update", uuid + ", " + request); Object result = updateInner(uuid, request); logger.reply(context, this, "update", result); return result; } catch (Exception e) { logger.error(context, this, "update", e); throw e; } } private Object updateInner(String uuid, SimpleObject request) throws ResponseException { Location existing = locationService.getLocationByUuid(uuid); if (existing == null) { throw new InvalidObjectDataException("No location found with UUID " + uuid); } updateNames(request, existing); Location location = locationService.saveLocation(existing); return locationToJson(location); } @Override public String getUri(Object instance) { Location location = (Location) instance; Resource res = getClass().getAnnotation(Resource.class); return RestConstants.URI_PREFIX + res.name() + "/" + location.getUuid(); } @Override public void delete(String uuid, String reason, RequestContext context) throws ResponseException { try { logger.request(context, this, "update", uuid + ", " + reason); deleteInner(uuid); logger.reply(context, this, "update", null); } catch (Exception e) { logger.error(context, this, "update", e); throw e; } } private SimpleObject getAllInner() throws ResponseException { ArrayList<SimpleObject> jsonResults = new ArrayList<>(); // A new fetch is needed to sort out the hibernate cache. Location root = locationService.getLocationByUuid(ROOT_UUID); if (root == null) { throw new IllegalStateException( "Top-level location not found, expected UUID: " + ROOT_UUID); } addRecursively(root, jsonResults); SimpleObject list = new SimpleObject(); list.add("results", jsonResults); return list; } private void addRecursively(Location location, ArrayList<SimpleObject> results) { if (location.isRetired()) return; results.add(locationToJson(location)); for (Location child : location.getChildLocations()) { addRecursively(child, results); } } private SimpleObject searchInner(RequestContext requestContext) throws ResponseException { return getAll(requestContext); } private void deleteInner(String uuid) throws ResponseException { if (ROOT_UUID.equals(uuid)) { throw new InvalidObjectDataException("Cannot delete the root location"); } for (String[] nameAndUuid : ZONE_NAMES_AND_UUIDS) { if (nameAndUuid[1].equals(uuid)) { throw new InvalidObjectDataException( "Cannot delete the zone \"" + nameAndUuid[0] + "\""); } } Location location = locationService.getLocationByUuid(uuid); if (location == null) { throw new InvalidObjectDataException("No location found with UUID " + uuid); } deleteLocationRecursively(location); } private void deleteLocationRecursively(Location location) { // We can't rely on database constraints to fail when deleting a // location, as locations are only stored as strings. Checking all // the patient attributes and child locations is really slow, and // slower than it need be, but deleting locations should be rare. PatientService patientService = Context.getPatientService(); for (Patient patient : patientService.getAllPatients()) { Set<PersonAttribute> attributes = patient.getAttributes(); for (PersonAttribute attribute : attributes) { if (attribute.getValue().equals(location.getUuid())) { throw new InvalidObjectDataException( String.format("Cannot delete the location \"%s\"" + " because it has patients assigned to it", location.getDisplayString())); } } } for (Location child : location.getChildLocations()) { deleteLocationRecursively(child); } locationService.purgeLocation(location); } }