/**
* The contents of this file are subject to the OpenMRS Public License
* Version 1.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://license.openmrs.org
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* Copyright (C) OpenMRS, LLC. All Rights Reserved.
*/
package org.openmrs.web.controller.patient;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Concept;
import org.openmrs.Location;
import org.openmrs.Obs;
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientIdentifierType;
import org.openmrs.PatientIdentifierType.LocationBehavior;
import org.openmrs.Person;
import org.openmrs.PersonAddress;
import org.openmrs.PersonAttribute;
import org.openmrs.PersonName;
import org.openmrs.Relationship;
import org.openmrs.RelationshipType;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.util.LocationUtility;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.validator.PatientValidator;
import org.openmrs.web.WebConstants;
import org.openmrs.web.controller.person.PersonFormController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.WebRequest;
/**
* This controller is used for the "mini"/"new"/"short" patient form. Only
* key/important attributes for the patient are displayed and allowed to be
* edited
*
* @see org.openmrs.web.controller.patient.PatientFormController
*/
@Controller
public class ShortPatientFormController {
private static final Log log = LogFactory.getLog(ShortPatientFormController.class);
private static final String SHORT_PATIENT_FORM_URL = "/admin/patients/shortPatientForm";
private static final String FIND_PATIENT_PAGE = "findPatient";
private static final String PATIENT_DASHBOARD_URL = "/patientDashboard.form";
@Autowired
PatientValidator patientValidator;
/**
* @return
*/
@RequestMapping(method = RequestMethod.GET, value = SHORT_PATIENT_FORM_URL)
public void showForm() {
}
@ModelAttribute("patientModel")
public ShortPatientModel getPatientModel(@RequestParam(value = "patientId", required = false) Integer patientId,
ModelMap model, WebRequest request) {
Patient patient = null;
if (patientId != null) {
try {
patient = Context.getPatientService().getPatient(patientId);
}
catch (ClassCastException ex) {
// we're promoting an existing Person to a full Patient
// this will be handled in the next lines
}
if (patient == null) {
Person toPromote = Context.getPersonService().getPerson(patientId);
if (toPromote == null)
throw new IllegalArgumentException("No patient or person with the given id");
patient = new Patient(toPromote);
}
} else {
// we may have some details to add to a blank patient
patient = new Patient();
String name = request.getParameter("addName");
if (!StringUtils.isBlank(name)) {
String gender = request.getParameter("addGender");
String date = request.getParameter("addBirthdate");
String age = request.getParameter("addAge");
PersonFormController.getMiniPerson(patient, name, gender, date, age);
}
}
// if we have an existing personName, cache the original name so that we
// can use it to
// track changes in givenName, middleName, familyName, will also use
// it to restore the original values
if (patient.getPersonName() != null && patient.getPersonName().getId() != null)
model.addAttribute("personNameCache", PersonName.newInstance(patient.getPersonName()));
else
model.addAttribute("personNameCache", new PersonName());
// cache a copy of the person address for comparison in case the name is
// edited
if (patient.getPersonAddress() != null && patient.getPersonAddress().getId() != null)
model.addAttribute("personAddressCache", patient.getPersonAddress().clone());
else
model.addAttribute("personAddressCache", new PersonAddress());
String propCause = Context.getAdministrationService().getGlobalProperty("concept.causeOfDeath");
Concept conceptCause = Context.getConceptService().getConcept(propCause);
String causeOfDeathOther = "";
if (conceptCause != null && patient.getPatientId() != null) {
List<Obs> obssDeath = Context.getObsService().getObservationsByPersonAndConcept(patient, conceptCause);
if (obssDeath.size() == 1) {
Obs obsDeath = obssDeath.iterator().next();
causeOfDeathOther = obsDeath.getValueText();
if (causeOfDeathOther == null) {
log.debug("cod is null, so setting to empty string");
causeOfDeathOther = "";
} else {
log.debug("cod is valid: " + causeOfDeathOther);
}
} else {
log.debug("obssDeath is wrong size: " + obssDeath.size());
}
} else {
log.debug("No concept cause found");
}
// end get 'other' cause of death
model.addAttribute("causeOfDeathOther", causeOfDeathOther);
return new ShortPatientModel(patient);
}
@ModelAttribute("locations")
public List<Location> getLocations() {
return Context.getLocationService().getAllLocations();
}
@ModelAttribute("defaultLocation")
public Location getDefaultLocation() {
return (LocationUtility.getUserDefaultLocation() != null) ? LocationUtility.getUserDefaultLocation()
: LocationUtility.getDefaultLocation();
}
@ModelAttribute("identifierTypes")
public List<PatientIdentifierType> getIdentifierTypes() {
return Context.getPatientService().getAllPatientIdentifierTypes();
}
@ModelAttribute("identifierLocationUsed")
public boolean getIdentifierLocationUsed() {
List<PatientIdentifierType> pits = Context.getPatientService().getAllPatientIdentifierTypes();
boolean identifierLocationUsed = false;
for (PatientIdentifierType pit : pits) {
if (pit.getLocationBehavior() == null || pit.getLocationBehavior() == LocationBehavior.REQUIRED) {
identifierLocationUsed = true;
}
}
return identifierLocationUsed;
}
/**
* Handles the form submission by validating the form fields and saving it
* to the DB
*
* @param request
* the webRequest object
* @param patientModel
* the modelObject containing the patient info collected from the
* form fields
* @param result
* @param status
* @return the view to forward to
* @should pass if all the form data is valid
* @should create a new patient
* @should send the user back to the form in case of validation errors
* @should void a name and replace it with a new one if it is changed to a
* unique value
* @should void an address and replace it with a new one if it is changed to
* a unique value
* @should add a new name if the person had no names
* @should add a new address if the person had none
* @should ignore a new address that was added and voided at same time
* @should set the cause of death as none a coded concept
* @should set the cause of death as a none coded concept
* @should void the cause of death obs that is none coded
*/
@RequestMapping(method = RequestMethod.POST, value = SHORT_PATIENT_FORM_URL)
public String saveShortPatient(WebRequest request, @ModelAttribute("personNameCache") PersonName personNameCache,
@ModelAttribute("personAddressCache") PersonAddress personAddressCache,
@ModelAttribute("patientModel") ShortPatientModel patientModel, BindingResult result) {
if (Context.isAuthenticated()) {
// First do form validation so that we can easily bind errors to
// fields
new ShortPatientFormValidator().validate(patientModel, result);
if (result.hasErrors())
return SHORT_PATIENT_FORM_URL;
Patient patient = null;
patient = getPatientFromFormData(patientModel);
Errors patientErrors = new BindException(patient, "patient");
patientValidator.validate(patient, patientErrors);
if (patientErrors.hasErrors()) {
// bind the errors to the patientModel object by adding them to
// result since this is not a patient object
// so that spring doesn't try to look for getters/setters for
// Patient in ShortPatientModel
for (ObjectError error : patientErrors.getAllErrors())
result.reject(error.getCode(), error.getArguments(), "Validation errors found");
return SHORT_PATIENT_FORM_URL;
}
// check if name/address were edited, void them and replace them
boolean foundChanges = hasPersonNameOrAddressChanged(patient, personNameCache, personAddressCache);
try {
patient = Context.getPatientService().savePatient(patient);
request.setAttribute(WebConstants.OPENMRS_MSG_ATTR, Context.getMessageSourceService().getMessage(
"Patient.saved"), WebRequest.SCOPE_SESSION);
// TODO do we really still need this, besides ensuring that the
// cause of death is provided?
// process and save the death info
saveDeathInfo(patientModel, request);
if (!patient.getVoided()) {
// save the relationships to the database
Map<String, Relationship> relationships = getRelationshipsMap(patientModel, request);
for (Relationship relationship : relationships.values()) {
// if the user added a person to this relationship, save
// it
if (relationship.getPersonA() != null && relationship.getPersonB() != null)
Context.getPersonService().saveRelationship(relationship);
}
}
}
catch (APIException e) {
log.error("Error occurred while attempting to save patient", e);
request.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, Context.getMessageSourceService().getMessage(
"Patient.save.error"), WebRequest.SCOPE_SESSION);
// TODO revert the changes and send them back to the form
// don't send the user back to the form because the created
// person name/addresses
// will be recreated over again if the user attempts to resubmit
if (!foundChanges)
return SHORT_PATIENT_FORM_URL;
}
return "redirect:" + PATIENT_DASHBOARD_URL + "?patientId=" + patient.getPatientId();
}
return FIND_PATIENT_PAGE;
}
/**
* Convenience method that gets the data from the patientModel
*
* @param patientModel
* the modelObject holding the form data
* @return the patient object that has been populated with input from the
* form
*/
private Patient getPatientFromFormData(ShortPatientModel patientModel) {
Patient patient = patientModel.getPatient();
PersonName personName = patientModel.getPersonName();
if (personName != null) {
personName.setPreferred(true);
patient.addName(personName);
}
PersonAddress personAddress = patientModel.getPersonAddress();
if (personAddress != null) {
if (personAddress.isVoided() && StringUtils.isBlank(personAddress.getVoidReason())) {
personAddress.setVoidReason(Context.getMessageSourceService().getMessage("general.default.voidReason"));
}
// don't add an address that is being created and at the
// same time being removed
else if (!(personAddress.isVoided() && personAddress.getPersonAddressId() == null)) {
personAddress.setPreferred(true);
patient.addAddress(personAddress);
}
}
// add all the existing identifiers and any new ones.
for (PatientIdentifier id : patientModel.getIdentifiers()) {
// skip past the new ones removed from the user interface(may be
// they were invalid
// and the user changed their mind about adding them and they
// removed them)
if (id.getPatientIdentifierId() == null && id.isVoided())
continue;
patient.addIdentifier(id);
}
// add the person attributes
for (PersonAttribute formAttribute : patientModel.getPersonAttributes())
patient.addAttribute(formAttribute);
return patient;
}
/**
* Creates a map of string of the form 3b, 3a and the actual person
* Relationships
*
* @param person
* the patient/person whose relationships to return
* @param request
* the webRequest Object
* @return map of strings matched against actual relationships
*/
@ModelAttribute("relationshipsMap")
private Map<String, Relationship> getRelationshipsMap(@ModelAttribute("patientModel") ShortPatientModel patientModel,
WebRequest request) {
Person person = patientModel.getPatient();
Map<String, Relationship> relationshipMap = new LinkedHashMap<String, Relationship>();
// gp is in the form "3a, 7b, 4a"
String relationshipsString = Context.getAdministrationService().getGlobalProperty(
OpenmrsConstants.GLOBAL_PROPERTY_NEWPATIENTFORM_RELATIONSHIPS, "");
relationshipsString = relationshipsString.trim();
if (relationshipsString.length() > 0) {
String[] showRelations = relationshipsString.split(",");
// iterate over strings like "3a"
for (String showRelation : showRelations) {
showRelation = showRelation.trim();
boolean aIsToB = true;
if (showRelation.endsWith("b")) {
aIsToB = false;
}
// trim out the trailing a or b char
String showRelationId = showRelation.replace("a", "");
showRelationId = showRelationId.replace("b", "");
RelationshipType relationshipType = Context.getPersonService().getRelationshipType(
Integer.valueOf(showRelationId));
// flag to know if we need to create a stub relationship
boolean relationshipFound = false;
if (person != null && person.getPersonId() != null) {
if (aIsToB) {
List<Relationship> relationships = Context.getPersonService().getRelationships(null, person,
relationshipType);
if (relationships.size() > 0) {
relationshipMap.put(showRelation, relationships.get(0));
relationshipFound = true;
}
} else {
List<Relationship> relationships = Context.getPersonService().getRelationships(person, null,
relationshipType);
if (relationships.size() > 0) {
relationshipMap.put(showRelation, relationships.get(0));
relationshipFound = true;
}
}
}
// if no relationship was found, create a stub one now
if (relationshipFound == false) {
Relationship relationshipStub = new Relationship();
relationshipStub.setRelationshipType(relationshipType);
if (aIsToB)
relationshipStub.setPersonB(person);
else
relationshipStub.setPersonA(person);
relationshipMap.put(showRelation, relationshipStub);
}
// check the request to see if a parameter exists in there
// that matches to the user desired relation. Overwrite
// any previous data if found
String submittedPersonId = request.getParameter(showRelation);
if (submittedPersonId != null && submittedPersonId.length() > 0) {
Person submittedPerson = Context.getPersonService().getPerson(Integer.valueOf(submittedPersonId));
if (aIsToB)
relationshipMap.get(showRelation).setPersonA(submittedPerson);
else
relationshipMap.get(showRelation).setPersonB(submittedPerson);
}
}
}
return relationshipMap;
}
/**
* Processes the death information for a deceased patient and save it to the
* database
*
* @param patientModel
* the modelObject containing the patient info collected from the
* form fields
* @param request
* webRequest object
*/
private void saveDeathInfo(ShortPatientModel patientModel, WebRequest request) {
// update the death reason
if (patientModel.getPatient().getDead()) {
log.debug("Patient is dead, so let's make sure there's an Obs for it");
// need to make sure there is an Obs that represents the
// patient's cause of death, if applicable
String codProp = Context.getAdministrationService().getGlobalProperty("concept.causeOfDeath");
Concept causeOfDeath = Context.getConceptService().getConcept(codProp);
if (causeOfDeath != null) {
List<Obs> obssDeath = Context.getObsService().getObservationsByPersonAndConcept(patientModel.getPatient(),
causeOfDeath);
if (obssDeath != null) {
if (obssDeath.size() > 1) {
log.warn("Multiple causes of death (" + obssDeath.size() + ")? Shouldn't be...");
} else {
Obs obsDeath = null;
if (obssDeath.size() == 1) {
// already has a cause of death - let's edit
// it.
log.debug("Already has a cause of death, so changing it");
obsDeath = obssDeath.iterator().next();
} else {
// no cause of death obs yet, so let's make
// one
log.debug("No cause of death yet, let's create one.");
obsDeath = new Obs();
obsDeath.setPerson(patientModel.getPatient());
obsDeath.setConcept(causeOfDeath);
}
// put the right concept and (maybe) text in this obs
Concept currCause = patientModel.getPatient().getCauseOfDeath();
if (currCause == null) {
// set to NONE
log.debug("Current cause is null, attempting to set to NONE");
String noneConcept = Context.getAdministrationService().getGlobalProperty("concept.none");
currCause = Context.getConceptService().getConcept(noneConcept);
}
if (currCause != null) {
log.debug("Current cause is not null, setting to value_coded");
obsDeath.setValueCoded(currCause);
obsDeath.setValueCodedName(currCause.getName());
Date dateDeath = patientModel.getPatient().getDeathDate();
if (dateDeath == null)
dateDeath = new Date();
obsDeath.setObsDatetime(dateDeath);
// check if this is an "other" concept - if
// so, then we need to add value_text
String otherConcept = Context.getAdministrationService().getGlobalProperty(
"concept.otherNonCoded");
Concept conceptOther = Context.getConceptService().getConcept(otherConcept);
if (conceptOther != null) {
if (conceptOther.equals(currCause)) {
// seems like this is an other
// concept - let's try to get the
// "other" field info
String otherInfo = request.getParameter("patient.causeOfDeath_other");
if (otherInfo == null)
otherInfo = "";
log.debug("Setting value_text as " + otherInfo);
obsDeath.setValueText(otherInfo);
} else {
log.debug("New concept is NOT the OTHER concept, so setting to blank");
obsDeath.setValueText("");
}
} else {
log.debug("Don't seem to know about an OTHER concept, so deleting value_text");
obsDeath.setValueText("");
}
if (StringUtils.isBlank(obsDeath.getVoidReason()))
obsDeath.setVoidReason(Context.getMessageSourceService().getMessage(
"general.default.changeReason"));
Context.getObsService().saveObs(obsDeath, obsDeath.getVoidReason());
} else {
log.debug("Current cause is still null - aborting mission");
}
}
}
} else {
log.debug("Cause of death is null - should not have gotten here without throwing an error on the form.");
}
}
}
/**
* Convenience method that checks if the person name or person address have
* been changed, should void the old person name/address and create a new
* one with the changes.
*
* @param patient
* the patient
* @param personNameCache
* the cached copy of the person name
* @param personAddressCache
* the cached copy of the person address
* @return true if the personName or personAddress was edited otherwise
* false
*/
private boolean hasPersonNameOrAddressChanged(Patient patient, PersonName personNameCache,
PersonAddress personAddressCache) {
boolean foundChanges = false;
PersonName personName = patient.getPersonName();
if (personNameCache.getId() != null) {
// if the existing persoName has been edited
if (!getPersonNameString(personName).equalsIgnoreCase(getPersonNameString(personNameCache))) {
if (log.isDebugEnabled())
log.debug("Voiding person name with id: " + personName.getId() + " and replacing it with a new one: "
+ personName.toString());
foundChanges = true;
// create a new one and copy the changes to it
PersonName newName = PersonName.newInstance(personName);
newName.setPersonNameId(null);
newName.setUuid(null);
newName.setChangedBy(null);// just in case it had a value
newName.setDateChanged(null);
newName.setCreator(Context.getAuthenticatedUser());
newName.setDateCreated(new Date());
// restore the given,middle and familyName, then void the old
// name
personName.setGivenName(personNameCache.getGivenName());
personName.setMiddleName(personNameCache.getMiddleName());
personName.setFamilyName(personNameCache.getFamilyName());
personName.setPreferred(false);
personName.setVoided(true);
personName.setVoidReason(Context.getMessageSourceService().getMessage("general.voidReasonWithArgument",
new Object[] { newName.toString() }, "Voided because it was edited to: " + newName.toString(),
Context.getLocale()));
// add the created name
patient.addName(newName);
}
}
PersonAddress personAddress = patient.getPersonAddress();
if (personAddress != null) {
if (personAddressCache.getId() != null) {
// if the existing personAddress has been edited
if (!personAddress.isBlank() && !personAddressCache.isBlank()
&& !personAddress.toString().equalsIgnoreCase(personAddressCache.toString())) {
if (log.isDebugEnabled())
log.debug("Voiding person address with id: " + personAddress.getId()
+ " and replacing it with a new one: " + personAddress.toString());
foundChanges = true;
// create a new one and copy the changes to it
PersonAddress newAddress = (PersonAddress) personAddress.clone();
newAddress.setPersonAddressId(null);
newAddress.setUuid(null);
newAddress.setChangedBy(null);// just in case it had a value
newAddress.setDateChanged(null);
newAddress.setCreator(Context.getAuthenticatedUser());
newAddress.setDateCreated(new Date());
// restore address fields that are checked for changes and
// void the address
personAddress.setAddress1(personAddressCache.getAddress1());
personAddress.setAddress2(personAddressCache.getAddress2());
personAddress.setAddress3(personAddressCache.getAddress3());
personAddress.setCityVillage(personAddressCache.getCityVillage());
personAddress.setCountry(personAddressCache.getCountry());
personAddress.setCountyDistrict(personAddressCache.getCountyDistrict());
personAddress.setStateProvince(personAddressCache.getStateProvince());
personAddress.setPostalCode(personAddressCache.getPostalCode());
personAddress.setLatitude(personAddressCache.getLatitude());
personAddress.setLongitude(personAddressCache.getLongitude());
personAddress.setPreferred(false);
personAddress.setVoided(true);
personAddress.setVoidReason(Context.getMessageSourceService().getMessage(
"general.voidReasonWithArgument", new Object[] { newAddress.toString() },
"Voided because it was edited to: " + newAddress.toString(), Context.getLocale()));
// Add the created one
patient.addAddress(newAddress);
}
}
}
return foundChanges;
}
/**
* Convenience method that transforms a person name to a string while
* ignoring null and blank values, the returned string only contains the
* givenName, middleName and familyName
*
* @param name
* the person name to transform
* @return the transformed string ignoring blanks and nulls
*/
public static String getPersonNameString(PersonName name) {
ArrayList<String> tempName = new ArrayList<String>();
if (StringUtils.isNotBlank(name.getGivenName()))
tempName.add(name.getGivenName().trim());
if (StringUtils.isNotBlank(name.getMiddleName()))
tempName.add(name.getMiddleName().trim());
if (StringUtils.isNotBlank(name.getFamilyName()))
tempName.add(name.getFamilyName().trim());
return StringUtils.join(tempName, " ");
}
}