// 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.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Patient;
import org.openmrs.Person;
import org.openmrs.PersonName;
import org.openmrs.Provider;
import org.openmrs.User;
import org.openmrs.api.PersonService;
import org.openmrs.api.ProviderService;
import org.openmrs.api.UserService;
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.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.response.ObjectNotFoundException;
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;
/**
* Resource for users (note that users are stored as Providers, Persons, and Users, but only
* Providers will be returned by List calls).
* <p/>
* <p>Expected behavior:
* <ul>
* <li>GET /user returns all users ({@link #getAll(RequestContext)})
* <li>GET /user/[UUID] returns a single user ({@link #retrieve(String, RequestContext)})
* <li>GET /user?q=[QUERY] returns users whose full name contains the query string
* ({@link #search(RequestContext)})
* <li>POST /user creates a new user ({@link #create(SimpleObject, RequestContext)})
* </ul>
* <p/>
* <p>All GET operations return User resources in the following JSON form:
* <pre>
* {
* user_id: "5a382-9", // UUID for the user
* full_name: "John Smith", // constructed from given and family name
* given_name: "John",
* family_name: "Smith"
* }
* </pre>
* <p/>
* <p>User creation expects a slightly different format:
* <pre>
* {
* user_name: "jsmith", // user id which can be used to log into OpenMRS
* password: "Password123", // must have > 8 characters, at least 1 digit and 1 uppercase
* given_name: "John",
* family_name: "Smith"
* }
* </pre>
* <p/>
* <p>If an error occurs, the response will contain the following:
* <pre>
* {
* "error": {
* "message": "[error message]",
* "code": "[breakpoint]",
* "detail": "[stack trace]"
* }
* }
* </pre>
*/
@Resource(name = RestController.REST_VERSION_1_AND_NAMESPACE + "/users", supportedClass = Provider
.class, supportedOpenmrsVersions = "1.10.*,1.11.*")
public class UserResource implements Listable, Searchable, Retrievable, Creatable {
// JSON property names
private static final String USER_ID = "user_id";
private static final String USER_NAME = "user_name";
private static final String FULL_NAME = "full_name"; // Ignored on create.
private static final String FAMILY_NAME = "family_name";
private static final String GIVEN_NAME = "given_name";
private static final String PASSWORD = "password";
// Sentinel for unknown values
private static final String UNKNOWN = "(UNKNOWN)";
// Defaults for guest account
private static final String GUEST_FULL_NAME = "Guest User";
private static final String GUEST_GIVEN_NAME = "Guest";
private static final String GUEST_FAMILY_NAME = "User";
private static final String GUEST_USER_NAME = "guest";
private static final String GUEST_PASSWORD = "Password123";
private static final String[] REQUIRED_FIELDS = {USER_NAME, GIVEN_NAME, PASSWORD};
private static final Object guestAddLock = new Object();
private static Log log = LogFactory.getLog(UserResource.class);
static final RequestLogger logger = RequestLogger.LOGGER;
private final PersonService personService;
private final ProviderService providerService;
private final UserService userService;
public UserResource() {
personService = Context.getPersonService();
providerService = Context.getProviderService();
userService = Context.getUserService();
}
/** Returns all Providers. */
@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;
}
}
/** Returns all Providers. */
private SimpleObject getAllInner() throws ResponseException {
List<Provider> providers;
// Returning providers is not a thread-safe operation as it may add the guest user
// to the database, which is not idempotent.
synchronized (this) {
providers = providerService.getAllProviders(false); // omit retired
addGuestIfNotPresent(providers);
}
return getSimpleObjectWithResults(providers);
}
/** Creates a Provider named "Guest User" if one doesn't already exist. */
private void addGuestIfNotPresent(List<Provider> providers) {
boolean guestFound = false;
for (Provider provider : providers) {
// TODO/robustness: Use a fixed UUID instead of searching for
// anything with a matching name.
if (provider.getName().equals(GUEST_FULL_NAME)) {
guestFound = true;
break;
}
}
if (!guestFound) {
SimpleObject guestDetails = new SimpleObject();
guestDetails.put(GIVEN_NAME, GUEST_GIVEN_NAME);
guestDetails.put(FAMILY_NAME, GUEST_FAMILY_NAME);
guestDetails.put(USER_NAME, GUEST_USER_NAME);
guestDetails.put(PASSWORD, GUEST_PASSWORD);
synchronized (guestAddLock) {
// Fetch again to avoid duplication in case another thread has
// added Guest User, but use the UserService for the check to
// avoid Hibernate cache issues.
User guestUser = userService.getUserByUsername(GUEST_USER_NAME);
if (guestUser == null) {
providers.add(createFromSimpleObject(guestDetails));
}
}
}
}
/**
* Converts a list of Providers into a SimpleObject in the form
* {"results": [...]} with an array of SimpleObjects, one for each Provider.
*/
private SimpleObject getSimpleObjectWithResults(List<Provider> providers) {
List<SimpleObject> jsonResults = new ArrayList<>();
for (Provider provider : providers) {
jsonResults.add(providerToJson(provider));
}
SimpleObject list = new SimpleObject();
list.add("results", jsonResults);
return list;
}
/** Adds a new Provider (with associated User and Person). */
private Provider createFromSimpleObject(SimpleObject simpleObject) {
checkRequiredFields(simpleObject, REQUIRED_FIELDS);
// TODO: Localize full name construction
String fullName = simpleObject.get(GIVEN_NAME) + " " + simpleObject.get(FAMILY_NAME);
Person person = new Person();
PersonName personName = new PersonName();
personName.setGivenName((String) simpleObject.get(GIVEN_NAME));
personName.setFamilyName((String) simpleObject.get(FAMILY_NAME));
person.addName(personName);
person.setGender(UNKNOWN); // This is required, even though it serves no purpose here.
personService.savePerson(person);
User user = new User();
user.setPerson(person);
user.setName(fullName);
user.setUsername((String) simpleObject.get(USER_NAME));
userService.saveUser(user, (String) simpleObject.get(PASSWORD));
Provider provider = new Provider();
provider.setPerson(person);
provider.setName(fullName);
providerService.saveProvider(provider);
log.info("Created user " + fullName);
return provider;
}
/** Builds a SimpleObject describing the given Provider. */
private SimpleObject providerToJson(Provider provider) {
SimpleObject jsonForm = new SimpleObject();
if (provider != null) {
jsonForm.add(USER_ID, provider.getUuid());
jsonForm.add(FULL_NAME, provider.getName());
Person person = provider.getPerson();
if (person != null) {
jsonForm.add(GIVEN_NAME, person.getGivenName());
jsonForm.add(FAMILY_NAME, person.getFamilyName());
}
}
return jsonForm;
}
/** Throws an exception if the given SimpleObject is missing any required fields. */
private void checkRequiredFields(SimpleObject simpleObject, String[] requiredFields) {
List<String> missingFields = new ArrayList<>();
for (String requiredField : requiredFields) {
if (!simpleObject.containsKey(requiredField)) {
missingFields.add(requiredField);
}
}
if (!missingFields.isEmpty()) {
throw new InvalidObjectDataException(
"JSON object lacks required fields: " + StringUtils.join(missingFields, ","));
}
}
/** Adds a new Provider. */
@Override
public Object create(SimpleObject obj, RequestContext context) throws ResponseException {
try {
logger.request(context, this, "create", obj);
Object result = createInner(obj);
logger.reply(context, this, "create", result);
return result;
} catch (Exception e) {
logger.error(context, this, "create", e);
throw e;
}
}
private Object createInner(SimpleObject simpleObject) throws ResponseException {
return providerToJson(createFromSimpleObject(simpleObject));
}
@Override public String getUri(Object instance) {
Patient patient = (Patient) instance;
Resource res = getClass().getAnnotation(Resource.class);
return RestConstants.URI_PREFIX + res.name() + "/" + patient.getUuid();
}
/** Returns a specific Provider. */
@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 {
Provider provider = providerService.getProviderByUuid(uuid);
if (provider == null) {
throw new ObjectNotFoundException();
}
return providerToJson(provider);
}
@Override public List<Representation> getAvailableRepresentations() {
return Arrays.asList(Representation.DEFAULT);
}
/** Searches for Providers whose names contain the 'q' parameter. */
@Override public SimpleObject search(RequestContext context) throws ResponseException {
try {
logger.request(context, this, "search");
SimpleObject result = searchInner(context);
logger.reply(context, this, "search", result);
return result;
} catch (Exception e) {
logger.error(context, this, "search", e);
throw e;
}
}
/** Searches for Providers whose names contain the 'q' parameter. */
private SimpleObject searchInner(RequestContext requestContext) throws ResponseException {
// Partial string query for searches.
String query = requestContext.getParameter("q");
// Retrieve all patients and filter the list based on the query.
List<Provider> filteredProviders = new ArrayList<>();
// Perform a substring search on username.
for (Provider provider : providerService.getAllProviders(false)) {
if (StringUtils.containsIgnoreCase(provider.getName(), query)) {
filteredProviders.add(provider);
}
}
addGuestIfNotPresent(filteredProviders);
return getSimpleObjectWithResults(filteredProviders);
}
}