/**
* 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.module.htmlformentry.element;
import org.openmrs.Location;
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientIdentifierType;
import org.openmrs.Person;
import org.openmrs.PersonAddress;
import org.openmrs.PersonName;
import org.openmrs.api.IdentifierNotUniqueException;
import org.openmrs.api.context.Context;
import org.openmrs.messagesource.MessageSourceService;
import org.openmrs.module.htmlformentry.FormEntryContext;
import org.openmrs.module.htmlformentry.FormEntryContext.Mode;
import org.openmrs.module.htmlformentry.FormEntrySession;
import org.openmrs.module.htmlformentry.FormSubmissionError;
import org.openmrs.module.htmlformentry.HtmlFormEntryUtil;
import org.openmrs.module.htmlformentry.ValidationException;
import org.openmrs.module.htmlformentry.action.FormSubmissionControllerAction;
import org.openmrs.module.htmlformentry.comparator.OptionComparator;
import org.openmrs.module.htmlformentry.widget.AddressWidget;
import org.openmrs.module.htmlformentry.widget.DateWidget;
import org.openmrs.module.htmlformentry.widget.DropdownWidget;
import org.openmrs.module.htmlformentry.widget.ErrorWidget;
import org.openmrs.module.htmlformentry.widget.HiddenFieldWidget;
import org.openmrs.module.htmlformentry.widget.NameWidget;
import org.openmrs.module.htmlformentry.widget.NumberFieldWidget;
import org.openmrs.module.htmlformentry.widget.Option;
import org.openmrs.module.htmlformentry.widget.TextFieldWidget;
import org.openmrs.module.htmlformentry.widget.Widget;
import org.openmrs.util.OpenmrsUtil;
import org.openmrs.validator.PatientIdentifierValidator;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Holds the widgets used to represent Patient Details, and serves as both the
* HtmlGeneratorElement and the FormSubmissionControllerAction for Patient
* Details.
*/
public class PatientDetailSubmissionElement implements HtmlGeneratorElement, FormSubmissionControllerAction {
public static final String FIELD_PERSON_NAME = "name";
public static final String FIELD_BIRTH_DATE_OR_AGE = "birthDateOrAge";
public static final String FIELD_BIRTH_DATE = "birthDate";
public static final String FIELD_AGE = "age";
public static final String FIELD_GENDER = "gender";
public static final String FIELD_IDENTIFIER = "identifier";
public static final String FIELD_IDENTIFIER_LOCATION = "identifierLocation";
public static final String FIELD_ADDRESS = "address";
private NameWidget nameWidget;
private ErrorWidget nameErrorWidget;
private DropdownWidget genderWidget;
private ErrorWidget genderErrorWidget;
private Widget ageWidget;
private ErrorWidget ageErrorWidget;
private Widget birthDateWidget;
private ErrorWidget birthDateErrorWidget;
private Widget identifierTypeWidget;
private Widget identifierTypeValueWidget;
private ErrorWidget identifierTypeValueErrorWidget;
private Widget identifierLocationWidget;
private ErrorWidget identifierLocationErrorWidget;
private AddressWidget addressWidget;
private boolean required;
public PatientDetailSubmissionElement(FormEntryContext context, Map<String, String> attributes) {
createElement(context, attributes);
}
public void createElement(FormEntryContext context, Map<String, String> attributes) {
String field = attributes.get("field");
Patient existingPatient = context.getExistingPatient();
// Required attribute defaults to true if not specified
required = ! "false".equalsIgnoreCase(attributes.get("required"));
if (FIELD_PERSON_NAME.equalsIgnoreCase(field)) {
nameWidget = new NameWidget();
nameErrorWidget = new ErrorWidget();
createWidgets(context, nameWidget, nameErrorWidget,
existingPatient != null && existingPatient.getPersonName() != null ? existingPatient.getPersonName() : null);
}
else if (FIELD_GENDER.equalsIgnoreCase(field)) {
MessageSourceService msg = Context.getMessageSourceService();
genderWidget = new DropdownWidget();
genderErrorWidget = new ErrorWidget();
genderWidget.addOption(new Option(msg.getMessage("Patient.gender.male"), "M", false));
genderWidget.addOption(new Option(msg.getMessage("Patient.gender.female"), "F", false));
createWidgets(context, genderWidget, genderErrorWidget, existingPatient != null ? existingPatient.getGender() : null);
}
else if (FIELD_AGE.equalsIgnoreCase(field)) {
ageWidget = new NumberFieldWidget(0d, 200d, false);
ageErrorWidget = new ErrorWidget();
createWidgets(context, ageWidget, ageErrorWidget, existingPatient != null ? existingPatient.getAge() : null);
}
else if (FIELD_BIRTH_DATE.equalsIgnoreCase(field)) {
birthDateWidget = new DateWidget();
birthDateErrorWidget = new ErrorWidget();
createWidgets(context, birthDateWidget, birthDateErrorWidget, existingPatient != null ? existingPatient.getBirthdate() : null);
}
else if (FIELD_BIRTH_DATE_OR_AGE.equalsIgnoreCase(field)) {
ageWidget = new NumberFieldWidget(0d, 200d, false);
ageErrorWidget = new ErrorWidget();
createWidgets(context, ageWidget, ageErrorWidget, existingPatient != null ? existingPatient.getAge() : null);
birthDateWidget = new DateWidget();
birthDateErrorWidget = new ErrorWidget();
createWidgets(context, birthDateWidget, birthDateErrorWidget, existingPatient != null ? existingPatient.getBirthdate() : null);
}
else if (FIELD_IDENTIFIER.equalsIgnoreCase(field)) {
PatientIdentifierType idType = HtmlFormEntryUtil.getPatientIdentifierType(attributes.get("identifierTypeId"));
identifierTypeValueWidget = new TextFieldWidget();
identifierTypeValueErrorWidget = new ErrorWidget();
String initialValue = null;
if (existingPatient != null) {
if (idType == null) {
if (existingPatient.getPatientIdentifier() != null) {
initialValue = existingPatient.getPatientIdentifier().getIdentifier();
}
} else {
if (existingPatient.getPatientIdentifier(idType) != null) {
initialValue = existingPatient.getPatientIdentifier(idType).getIdentifier();
}
}
}
createWidgets(context, identifierTypeValueWidget, identifierTypeValueErrorWidget, initialValue);
if (idType != null) {
identifierTypeWidget = new HiddenFieldWidget();
createWidgets(context, identifierTypeWidget, null, idType.getId().toString());
}
else {
identifierTypeWidget = new DropdownWidget();
List<PatientIdentifierType> patientIdentifierTypes = HtmlFormEntryUtil.getPatientIdentifierTypes();
for (PatientIdentifierType patientIdentifierType : patientIdentifierTypes) {
((DropdownWidget) identifierTypeWidget).addOption(new Option(patientIdentifierType.getName(), patientIdentifierType
.getPatientIdentifierTypeId().toString(), false));
}
createWidgets(context, identifierTypeWidget, null,
existingPatient != null && existingPatient.getPatientIdentifier() != null ? existingPatient.getPatientIdentifier()
.getIdentifierType().getId() : null);
}
}
else if (FIELD_IDENTIFIER_LOCATION.equalsIgnoreCase(field)) {
identifierLocationWidget = new DropdownWidget();
identifierLocationErrorWidget = new ErrorWidget();
Location defaultLocation = existingPatient != null
&& existingPatient.getPatientIdentifier() != null ? existingPatient.getPatientIdentifier().getLocation() : null;
defaultLocation = defaultLocation == null ? context.getDefaultLocation() : defaultLocation;
identifierLocationWidget.setInitialValue(defaultLocation);
List<Option> locationOptions = new ArrayList<Option>();
for(Location location:Context.getLocationService().getAllLocations()) {
Option option = new Option(location.getName(), location.getId().toString(), location.equals(defaultLocation));
locationOptions.add(option);
}
Collections.sort(locationOptions, new OptionComparator());
// if initialValueIsSet=false, no initial/default location, hence this shows the 'select input' field as first option
boolean initialValueIsSet = !(defaultLocation == null);
((DropdownWidget) identifierLocationWidget).addOption(new Option(Context.getMessageSourceService().getMessage("htmlformentry.chooseALocation"), "", !initialValueIsSet));
if (!locationOptions.isEmpty()) {
for(Option option: locationOptions){
((DropdownWidget) identifierLocationWidget).addOption(option);
}
}
createWidgets(context, identifierLocationWidget, identifierLocationErrorWidget, defaultLocation);
}
else if (FIELD_ADDRESS.equalsIgnoreCase(field)) {
addressWidget = new AddressWidget();
createWidgets(context, addressWidget, null, existingPatient != null ? existingPatient.getPersonAddress() : null);
}
}
private void createWidgets(FormEntryContext context, Widget fieldWidget, ErrorWidget errorWidget, Object initialValue) {
context.registerWidget(fieldWidget);
if (errorWidget != null) {
context.registerErrorWidget(fieldWidget, errorWidget);
}
if (initialValue != null && StringUtils.hasText(initialValue.toString())) {
fieldWidget.setInitialValue(initialValue);
}
}
@Override
public String generateHtml(FormEntryContext context) {
StringBuilder sb = new StringBuilder();
MessageSourceService mss = Context.getMessageSourceService();
if (nameWidget != null) {
sb.append(nameWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW)
sb.append(nameErrorWidget.generateHtml(context));
}
if (genderWidget != null) {
sb.append(genderWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW)
sb.append(genderErrorWidget.generateHtml(context));
}
if (birthDateWidget != null) {
if (ageWidget != null) {
sb.append(mss.getMessage("Person.birthdate")).append(" ");
}
sb.append(birthDateWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW)
sb.append(birthDateErrorWidget.generateHtml(context));
}
if (ageWidget != null) {
if (birthDateWidget != null) {
sb.append(" ").append(mss.getMessage("Person.age.or")).append(" ");
}
sb.append(ageWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW)
sb.append(ageErrorWidget.generateHtml(context));
}
if (identifierTypeValueWidget != null) {
sb.append(identifierTypeValueWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW) {
// if value is required
if (required) {
sb.append("<span class='required'>*</span>");
}
sb.append(" ");
sb.append(identifierTypeValueErrorWidget.generateHtml(context));
}
}
if (identifierTypeWidget != null) {
if (identifierTypeValueWidget instanceof DropdownWidget){
sb.append(" ").append(mss.getMessage("PatientIdentifier.identifierType")).append(" ");
}
sb.append(identifierTypeWidget.generateHtml(context)).append(" ");
}
if (identifierLocationWidget != null) {
sb.append(identifierLocationWidget.generateHtml(context));
if (context.getMode() != Mode.VIEW)
sb.append(identifierLocationErrorWidget.generateHtml(context));
}
if (addressWidget != null) {
sb.append(addressWidget.generateHtml(context));
}
return sb.toString();
}
@Override
public void handleSubmission(FormEntrySession session, HttpServletRequest request) {
Patient patient = (Patient) session.getSubmissionActions().getCurrentPerson();
if (patient == null) {
throw new RuntimeException("Programming exception: person shouldn't be null");
}
FormEntryContext context = session.getContext();
if (nameWidget != null) {
PersonName name = (PersonName) nameWidget.getValue(context, request);
if (patient != null) {
if (!name.isPreferred()) {
PersonName currentPreferredName = context.getExistingPatient().getPersonName();
if (currentPreferredName != null){
currentPreferredName.setPreferred(false);
currentPreferredName.setVoided(true);
}
}
}
// HACK: we need to set the date created and uuid here as a hack around a hibernate flushing issue (see saving the Patient in FormEntrySession applyActions())
if (name.getDateCreated() == null) {
name.setDateCreated(new Date());
}
if (name.getUuid() == null) {
name.setUuid(UUID.randomUUID().toString());
}
name.setPreferred(true);
patient.addName(name);
}
if (genderWidget != null) {
String value = (String) genderWidget.getValue(context, request);
patient.setGender(value);
}
if (ageWidget != null) {
Double value = (Double) ageWidget.getValue(context, request);
if (value != null)
calculateBirthDate(patient, null, value);
}
if (birthDateWidget != null) {
Date value = (Date) birthDateWidget.getValue(context, request);
if (value != null) {
calculateBirthDate(patient, value, null);
}
}
if (identifierTypeValueWidget != null && identifierTypeWidget != null) {
String identifier = (String) identifierTypeValueWidget.getValue(context, request);
PatientIdentifierType identifierType = getIdentifierType((String) identifierTypeWidget.getValue(context, request));
// Look for an existing identifier of this type
PatientIdentifier patientIdentifier = patient.getPatientIdentifier(identifierType);
if (StringUtils.hasText(identifier)) {
// No existing identifier of this type, so create new
if (patientIdentifier == null) {
patientIdentifier = new PatientIdentifier();
patientIdentifier.setIdentifierType(identifierType);
// HACK: we need to set the date created and uuid here as a hack around a hibernate flushing issue (see saving the Patient in FormEntrySession applyActions())
patientIdentifier.setDateChanged(new Date());
patientIdentifier.setUuid(UUID.randomUUID().toString());
// For 1.9+ onwards patients require a preferred identifier
if (patient.getPatientId() == null) {
patientIdentifier.setPreferred(true);
}
patient.addIdentifier(patientIdentifier);
}
if (!identifier.equals(patientIdentifier.getIdentifier()) || !identifierType.equals(patientIdentifier.getIdentifierType())) {
validateIdentifier(identifierType.getId(), identifier);
}
patientIdentifier.setIdentifier(identifier);
}
else if (patientIdentifier != null) {
// If this field is not required, then we interpret a blank value as a request to avoid any existing identifier
session.getSubmissionActions().getIdentifiersToVoid().add(patientIdentifier);
}
}
//
// TODO current behavior always updates location of the preferred identifier rather than
// a specific identifier type being edited by the identifier widget. But identifier location
// widget isn't aware of the identifier type widget
//
if (identifierLocationWidget != null) {
PatientIdentifier patientIdentifier = patient.getPatientIdentifier();
if (patientIdentifier == null) {
patientIdentifier = new PatientIdentifier();
patient.addIdentifier(patientIdentifier);
}
Object locationString = identifierLocationWidget.getValue(context, request);
Location location = (Location) HtmlFormEntryUtil.convertToType(locationString.toString().trim(), Location.class);
patientIdentifier.setLocation(location);
patientIdentifier.setPreferred(true);
}
if (addressWidget != null) {
PersonAddress personAddress = (PersonAddress) addressWidget.getValue(context, request);
if (context.getMode() == Mode.EDIT) {
if (!personAddress.isPreferred()) {
PersonAddress currentPreferredAddress = context.getExistingPatient().getPersonAddress();
currentPreferredAddress.setPreferred(false);
currentPreferredAddress.setVoided(true);
}
}
personAddress.setPreferred(true);
patient.addAddress(personAddress);
}
session.getSubmissionActions().setPatientUpdateRequired(true);
}
private void validateIdentifier(Integer identifierType, String identifier) {
if (identifierType != null && identifier != null) {
try {
PatientIdentifier pi = new PatientIdentifier();
pi.setIdentifier(identifier);
pi.setIdentifierType(getIdentifierType(identifierType.toString()));
// note that this is a bit of a hack; we can't call the PatientIdentifierValidator.validateIdentifier(identifier) method
// because it (as of 1.8) also tests to make sure that an identifier has a location associated with it; when validating an
// individual identifier widget, there will be no location because the location is collected in another widget
PatientIdentifierValidator.validateIdentifier(pi.getIdentifier(), pi.getIdentifierType());
if (Context.getPatientService().isIdentifierInUseByAnotherPatient(pi)) {
throw new IdentifierNotUniqueException(Context.getMessageSourceService().getMessage(
"PatientIdentifier.error.notUniqueWithParameter", new Object[] { pi.getIdentifier() },
Context.getLocale()));
}
}
catch (Exception e) {
throw new ValidationException(e.getMessage());
}
}
}
private PatientIdentifierType getIdentifierType(String id) {
PatientIdentifierType patientIdentifierType = Context.getPatientService().getPatientIdentifierType(new Integer(id));
if (patientIdentifierType == null) {
throw new RuntimeException("Invalid identifierTypeId given " + id);
}
return patientIdentifierType;
}
@SuppressWarnings("unused")
@Override
public Collection<FormSubmissionError> validateSubmission(FormEntryContext context, HttpServletRequest request) {
List<FormSubmissionError> ret = new ArrayList<FormSubmissionError>();
List<FormSubmissionError> ageOrBirthdDateErrorMessage = new ArrayList<FormSubmissionError>();
validateMandatoryField(context, request, genderWidget, genderErrorWidget, ret);
if (required) {
validateMandatoryField(context, request, identifierTypeValueWidget, identifierTypeValueErrorWidget, ret);
}
validateMandatoryField(context, request, identifierLocationWidget, identifierLocationErrorWidget, ret);
if(nameWidget != null){
PersonName personName = nameWidget.getValue(context, request);
if(!StringUtils.hasText(personName.getGivenName()) || !StringUtils.hasText(personName.getFamilyName())){
ret.add(new FormSubmissionError(context.getFieldName(nameErrorWidget), Context.getMessageSourceService().getMessage("htmlformentry.error.name.required")));
}
}
if (ageWidget != null && validateMandatoryField(context, request, ageWidget, ageErrorWidget, ageOrBirthdDateErrorMessage)) {
try {
Number value = (Number) ageWidget.getValue(context, request);
}
catch (Exception e) {
ret.add(new FormSubmissionError(context.getFieldName(ageErrorWidget), e.getMessage()));
}
}
if (birthDateWidget != null
&& validateMandatoryField(context, request, birthDateWidget, birthDateErrorWidget, ageOrBirthdDateErrorMessage)) {
try {
if (birthDateWidget.getValue(context, request) != null
&& OpenmrsUtil.compare((Date) birthDateWidget.getValue(context, request), new Date()) > 0) {
ret.add(new FormSubmissionError(context.getFieldName(birthDateErrorWidget), Context.getMessageSourceService().getMessage(
"htmlformentry.error.cannotBeInFuture")));
}
}
catch (Exception e) {
ret.add(new FormSubmissionError(context.getFieldName(birthDateErrorWidget), e.getMessage()));
}
}
if (ageWidget != null && birthDateWidget != null) {
if (ageOrBirthdDateErrorMessage.size() > 1) {
ret.add(new FormSubmissionError(context.getFieldName(ageErrorWidget), Context.getMessageSourceService().getMessage(
"Person.birthdate.required")));
}
}
else {
ret.addAll(ageOrBirthdDateErrorMessage);
}
if (identifierTypeWidget != null && identifierTypeValueWidget != null) {
String identifierTypeId = (String) identifierTypeWidget.getValue(context, request);
String identifierValue = (String) identifierTypeValueWidget.getValue(context, request);
if(StringUtils.hasText(identifierValue)){
try {
validateIdentifier(Integer.getInteger(identifierTypeId), identifierValue);
}
catch (Exception e) {
ret.add(new FormSubmissionError(context.getFieldName(identifierTypeValueErrorWidget), e.getMessage()));
}
}
}
return ret;
}
private boolean validateMandatoryField(FormEntryContext context, HttpServletRequest request, Widget widget, ErrorWidget errorWidget,
List<FormSubmissionError> ret) {
try {
if (widget != null) {
Object obj = widget.getValue(context, request);
if (obj == null || (obj instanceof String && !StringUtils.hasText((String) obj))) {
throw new Exception("htmlformentry.error.required");
}
}
}
catch (Exception ex) {
ret.add(new FormSubmissionError(context.getFieldName(errorWidget), Context.getMessageSourceService().getMessage(ex.getMessage())));
return false;
}
return true;
}
/**
* If there's a birthdate specified
*/
private void calculateBirthDate(Person person, Date date, Double age) {
Date birthdate = null;
boolean birthdateEstimated = false;
if (date != null) {
birthdate = date;
//if you have a previous date that's marked as estimated and date does not change --> keep it that way
//if you have a previous date that's marked as estimated but date changes --> not estimated
//if new --> not estimated
//if existing and not estimated --> not estimated
birthdateEstimated = person.getBirthdate() != null && person.getBirthdateEstimated() != null && person.getBirthdate().equals(date) ? person.getBirthdateEstimated() : false;
}
else if (age != null) {
try {
Double ageRemainder = BigDecimal.valueOf(age).subtract(BigDecimal.valueOf(Math.floor(age))).doubleValue();
if (ageRemainder.equals(Double.valueOf(0)))
person.setBirthdateFromAge(age.intValue(), new Date()); //default to usual behavior from core
else { //a decimal was entered
Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DAY_OF_MONTH, - Double.valueOf((ageRemainder * 365)).intValue()); //if patient is 2.2 years old, patient was 2.0 years 2.2 - (.2*365) days ago
c.add(Calendar.YEAR, -1 * Double.valueOf(Math.floor(age)).intValue());
birthdate = c.getTime();
}
birthdateEstimated = true;
}
catch (NumberFormatException e) {
throw new RuntimeException("Error getting date from age", e);
}
} else {
throw new IllegalArgumentException("You must provide either an age or a birthdate for this patient.");
}
if (birthdate != null)
person.setBirthdate(birthdate);
person.setBirthdateEstimated(birthdateEstimated);
}
}