Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BAH-460 Add search similar patients API [WIP] #33

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
import org.apache.commons.lang.StringUtils;
import org.openmrs.module.webservices.rest.web.RequestContext;

import java.sql.Timestamp;
import java.util.Date;
import java.util.Map;

public class PatientSearchParameters {
private final String MIDNIGHT = "00:00:00";

private Boolean filterPatientsByLocation;
private String identifier;
private String name;
private String gender;
private Date birthdate;
private String addressFieldName;
private String addressFieldValue;
private Integer start;
Expand Down Expand Up @@ -43,6 +49,8 @@ public PatientSearchParameters(RequestContext context) {
} else {
this.setAddressFieldName("city_village");
}
this.setGender(context.getParameter("gender"));
this.setBirthdate(context.getParameter("birthdate"));
this.setAddressFieldValue(context.getParameter("addressFieldValue"));
Map parameterMap = context.getRequest().getParameterMap();
this.setAddressSearchResultFields((String[]) parameterMap.get("addressSearchResultsConfig"));
Expand Down Expand Up @@ -71,6 +79,26 @@ public void setName(String name) {
this.name = name;
}

public String getGender() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add this field here we also need to make sure it's respected by existing API methods that use PatientSearchParameters.
(It would be okay to skip the gender field when searching for similar patients, if your timeline to finish this is tight.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djazayeri what do you mean by respected?

return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public void setBirthdate(String birthdate) {
if(StringUtils.isEmpty(birthdate)) {
return;
}

this.birthdate = Timestamp.valueOf(setToMidnight(birthdate));
}

public Date getBirthdate() {
return birthdate;
}

public String getAddressFieldName() {
return addressFieldName;
}
Expand Down Expand Up @@ -174,4 +202,12 @@ public void setFilterOnAllIdentifiers(Boolean filterOnAllIdentifiers) {
public Boolean getFilterOnAllIdentifiers() {
return filterOnAllIdentifiers;
}

private String setToMidnight(String birthdate) {
if(StringUtils.isNotEmpty(birthdate) && birthdate.matches("\\d{4}-\\d{2}-\\d{2}")) {
return birthdate + " " + MIDNIGHT;
} else {
return birthdate;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.bahmni.module.bahmnicore.contract.patient.mapper;

import java.util.Objects;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.bahmni.module.bahmnicore.contract.patient.response.PatientResponse;
Expand All @@ -21,11 +20,13 @@

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;

public class PatientResponseMapper {
private PatientResponse patientResponse;
private VisitService visitService;
Expand All @@ -38,8 +39,8 @@ public PatientResponseMapper(VisitService visitService, BahmniVisitLocationServi
}

public PatientResponse map(Patient patient, String loginLocationUuid, String[] searchResultFields, String[] addressResultFields, Object programAttributeValue) {
List<String> patientSearchResultFields = searchResultFields != null ? Arrays.asList(searchResultFields) : new ArrayList<>();
List<String> addressSearchResultFields = addressResultFields != null ? Arrays.asList(addressResultFields) : new ArrayList<>();
List<String> patientSearchResultFields = searchResultFields != null ? asList(searchResultFields) : new ArrayList<>();
List<String> addressSearchResultFields = addressResultFields != null ? asList(addressResultFields) : new ArrayList<>();

Integer visitLocationId = bahmniVisitLocationService.getVisitLocation(loginLocationUuid).getLocationId();
List<Visit> activeVisitsByPatient = visitService.getActiveVisitsByPatient(patient);
Expand All @@ -55,7 +56,9 @@ public PatientResponse map(Patient patient, String loginLocationUuid, String[] s
patientResponse.setFamilyName(patient.getFamilyName());
patientResponse.setGender(patient.getGender());
PatientIdentifier primaryIdentifier = patient.getPatientIdentifier();
patientResponse.setIdentifier(primaryIdentifier.getIdentifier());
if(primaryIdentifier != null) {
patientResponse.setIdentifier(primaryIdentifier.getIdentifier());
}
patientResponse.setPatientProgramAttributeValue(programAttributeValue);

mapExtraIdentifiers(patient, primaryIdentifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.openmrs.Patient;
import org.openmrs.RelationshipType;

import java.util.Date;
import java.util.List;

public interface PatientDao {
Expand All @@ -19,6 +20,8 @@ List<PatientResponse> getPatientsUsingLuceneSearch(String identifier, String nam
String programAttributeFieldName, String[] addressSearchResultFields,
String[] patientSearchResultFields, String loginLocationUuid, Boolean filterPatientsByLocation, Boolean filterOnAllIdentifiers);

List<PatientResponse> getSimilarPatientsUsingLuceneSearch(String name, String gender, Date birthdate, String loginLocationUuid, Integer length);

public Patient getPatient(String identifier);

public List<Patient> getPatients(String partialIdentifier, boolean shouldMatchExactPatientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,26 @@
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientIdentifierType;
import org.openmrs.Person;
import org.openmrs.PersonName;
import org.openmrs.RelationshipType;
import org.openmrs.api.context.Context;
import org.openmrs.api.db.hibernate.HibernatePatientDAO;
import org.openmrs.api.db.hibernate.PersonLuceneQuery;
import org.openmrs.api.db.hibernate.search.LuceneQuery;
import org.openmrs.module.bahmniemrapi.visitlocation.BahmniVisitLocationServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static java.util.stream.Collectors.reducing;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;

@Repository
Expand Down Expand Up @@ -88,18 +94,74 @@ public List<PatientResponse> getPatientsUsingLuceneSearch(String identifier, Str
List<PatientResponse> patientResponses = patientIdentifiers.stream()
.map(patientIdentifier -> {
Patient patient = patientIdentifier.getPatient();
if(!uniquePatientIds.contains(patient.getPatientId())) {
PatientResponse patientResponse = patientResponseMapper.map(patient, loginLocationUuid, patientSearchResultFields, addressSearchResultFields,
programAttributes.get(patient.getPatientId()));
uniquePatientIds.add(patient.getPatientId());
return patientResponse;
}else
return null;
return toPatientResponse(patientResponseMapper, patient, loginLocationUuid, addressSearchResultFields, patientSearchResultFields, programAttributes, uniquePatientIds);
}).filter(Objects::nonNull)
.collect(toList());
return patientResponses;
}

@Override
public List<PatientResponse> getSimilarPatientsUsingLuceneSearch(String name, String gender, Date birthdate, String loginLocationUuid, Integer length) {
PatientResponseMapper patientResponseMapper = new PatientResponseMapper(Context.getVisitService(),new BahmniVisitLocationServiceImpl(Context.getLocationService()));
List<Patient> patients = getPatientsByNameGenderAndBirthdate(name, gender, birthdate, length);
List<PatientResponse> patientResponses = patients.stream()
.map(patient -> {return patientResponseMapper.map(patient, loginLocationUuid, null, null,null);}).filter(Objects::nonNull)
.collect(toList());
return patientResponses;
}

private PatientResponse toPatientResponse(PatientResponseMapper patientResponseMapper, Patient patient, String loginLocationUuid, String[] addressSearchResultFields, String[] patientSearchResultFields, Map<Object, Object> programAttributes, Set<Integer> uniquePatientIds) {
if(!uniquePatientIds.contains(patient.getPatientId())) {
PatientResponse patientResponse = patientResponseMapper.map(patient, loginLocationUuid, patientSearchResultFields, addressSearchResultFields,
programAttributes.get(patient.getPatientId()));
uniquePatientIds.add(patient.getPatientId());
return patientResponse;
} else {
return null;
}
}

private List<Patient> getPatientsByNameGenderAndBirthdate(String name, String gender, Date birthdate, Integer length) {
if(isAllNullOrEmpty(name, gender, birthdate)) {
return new ArrayList<>();
}

HibernatePatientDAO patientDAO = new HibernatePatientDAO();
patientDAO.setSessionFactory(sessionFactory);
String query = LuceneQuery.escapeQuery(name);
PersonLuceneQuery personLuceneQuery = new PersonLuceneQuery(sessionFactory);
LuceneQuery<PersonName> nameQuery = personLuceneQuery.getPatientNameQuery(query, false);
List<Patient> patients = nameQuery.list().stream()
.filter(patient -> patient.getPreferred() && checkGender(patient.getPerson(), gender)
&& checkBirthdate(patient.getPerson(), birthdate)
)
.limit(length)
.map(patient -> new Patient(patient.getPerson()))
.collect(toList());
return patients;
}

private Boolean isAllNullOrEmpty(String name, String gender, Date birthdate) {
return (name == null || name.trim().isEmpty()) && (gender == null || gender.isEmpty()) && birthdate == null;
}


private Boolean checkGender(Person person, String gender) {
if(gender != null && !gender.isEmpty()){
return gender.equals(person.getGender());
} else {
return true;
}
}

private Boolean checkBirthdate(Person person, Date birthdate) {
if(birthdate != null) {
return birthdate.equals(person.getBirthdate());
} else {
return true;
}
}

private List<PatientIdentifier> getPatientIdentifiers(String identifier, Boolean filterOnAllIdentifiers, Integer offset, Integer length) {
FullTextSession fullTextSession = Search.getFullTextSession(sessionFactory.getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity(PatientIdentifier.class).get();
Expand All @@ -108,7 +170,7 @@ private List<PatientIdentifier> getPatientIdentifiers(String identifier, Boolean
.wildcard().onField("identifierAnywhere").matching("*" + identifier.toLowerCase() + "*").createQuery();
org.apache.lucene.search.Query nonVoidedIdentifiers = queryBuilder.keyword().onField("voided").matching(false).createQuery();
org.apache.lucene.search.Query nonVoidedPatients = queryBuilder.keyword().onField("patient.voided").matching(false).createQuery();

List<String> identifierTypeNames = getIdentifierTypeNames(filterOnAllIdentifiers);

BooleanJunction identifierTypeShouldJunction = queryBuilder.bool();
Expand All @@ -132,7 +194,7 @@ private List<PatientIdentifier> getPatientIdentifiers(String identifier, Boolean
fullTextQuery.setMaxResults(length);
return (List<PatientIdentifier>) fullTextQuery.list();
}

private List<String> getIdentifierTypeNames(Boolean filterOnAllIdentifiers) {
List<String> identifierTypeNames = new ArrayList<>();
addIdentifierTypeName(identifierTypeNames,"bahmni.primaryIdentifierType");
Expand Down Expand Up @@ -179,7 +241,7 @@ private boolean isValidAddressField(String addressFieldName) {
"LOWER (TABLE_NAME) ='person_address' and LOWER(COLUMN_NAME) IN " +
"( :personAddressField)";
Query queryToGetAddressFields = sessionFactory.getCurrentSession().createSQLQuery(query);
queryToGetAddressFields.setParameterList("personAddressField", Arrays.asList(addressFieldName.toLowerCase()));
queryToGetAddressFields.setParameterList("personAddressField", asList(addressFieldName.toLowerCase()));
List list = queryToGetAddressFields.list();
return list.size() > 0;
}
Expand All @@ -201,7 +263,7 @@ private List<Integer> getPersonAttributeIds(String[] patientAttributes) {
String query = "select person_attribute_type_id from person_attribute_type where name in " +
"( :personAttributeTypeNames)";
Query queryToGetAttributeIds = sessionFactory.getCurrentSession().createSQLQuery(query);
queryToGetAttributeIds.setParameterList("personAttributeTypeNames", Arrays.asList(patientAttributes));
queryToGetAttributeIds.setParameterList("personAttributeTypeNames", asList(patientAttributes));
List list = queryToGetAttributeIds.list();
return (List<Integer>) list;
}
Expand Down Expand Up @@ -229,7 +291,7 @@ public List<Patient> getPatients(String patientIdentifier, boolean shouldMatchEx
}

Patient patient = getPatient(patientIdentifier);
List<Patient> result = (patient == null ? new ArrayList<Patient>() : Arrays.asList(patient));
List<Patient> result = (patient == null ? new ArrayList<Patient>() : asList(patient));
return result;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.bahmni.module.bahmnicore.service;

import org.bahmni.module.bahmnicore.contract.patient.PatientSearchParameters;
import org.bahmni.module.bahmnicore.contract.patient.mapper.PatientResponseMapper;
import org.bahmni.module.bahmnicore.contract.patient.response.PatientConfigResponse;
import org.bahmni.module.bahmnicore.contract.patient.response.PatientResponse;
import org.openmrs.Patient;
import org.openmrs.Person;
import org.openmrs.RelationshipType;

import java.util.List;
Expand All @@ -15,6 +17,8 @@ public interface BahmniPatientService {

List<PatientResponse> luceneSearch(PatientSearchParameters searchParameters);

List<PatientResponse> searchSimilarPatients(PatientSearchParameters searchParameters);

public List<Patient> get(String partialIdentifier, boolean shouldMatchExactPatientId);

public List<RelationshipType> getByAIsToB(String aIsToB);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@Service
@Lazy //to get rid of cyclic dependencies
public class BahmniPatientServiceImpl implements BahmniPatientService {
private static final int SIMILAR_PATIENT_RESULT_LENGTH = 5;
private PersonService personService;
private ConceptService conceptService;
private PatientDao patientDao;
Expand Down Expand Up @@ -83,6 +84,15 @@ public List<PatientResponse> luceneSearch(PatientSearchParameters searchParamete
searchParameters.getFilterPatientsByLocation(), searchParameters.getFilterOnAllIdentifiers());
}

@Override
public List<PatientResponse> searchSimilarPatients(PatientSearchParameters searchParameters) {
return patientDao.getSimilarPatientsUsingLuceneSearch(searchParameters.getName(),
searchParameters.getGender(),
searchParameters.getBirthdate(),
searchParameters.getLoginLocationUuid(),
SIMILAR_PATIENT_RESULT_LENGTH);
}

@Override
public List<Patient> get(String partialIdentifier, boolean shouldMatchExactPatientId) {
return patientDao.getPatients(partialIdentifier, shouldMatchExactPatientId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.bahmni.module.bahmnicore.contract.patient;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.openmrs.module.webservices.rest.web.RequestContext;

import javax.servlet.http.HttpServletRequest;
import java.sql.Timestamp;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;


public class PatientSearchParametersTest {

@Mock
RequestContext requestContext;

@Mock
HttpServletRequest request;

@Before
public void setup() {
initMocks(this);
when(requestContext.getRequest()).thenReturn(request);
}

@Test
public void shouldIgnoreEmptyBirthdate () {
when(requestContext.getParameter("birthdate")).thenReturn("");
PatientSearchParameters patientSearchParameters = new PatientSearchParameters(requestContext);

assertNull(patientSearchParameters.getBirthdate());
}

@Test
public void shouldParseBirthdateFromStringAndSetToMidnight () {
when(requestContext.getParameter("birthdate")).thenReturn("1983-01-30");
PatientSearchParameters patientSearchParameters = new PatientSearchParameters(requestContext);

assertEquals(Timestamp.valueOf("1983-01-30 00:00:00"), patientSearchParameters.getBirthdate());
}
}
Loading