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

FM2-347: Add support for _has for ServiceRequest and Observation #529

Closed
wants to merge 9 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,12 @@
*/
package org.openmrs.module.fhir2.api;

import java.util.HashSet;

import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.openmrs.module.fhir2.api.search.param.ServiceRequestSearchParams;

public interface FhirServiceRequestService extends FhirService<ServiceRequest> {

IBundleProvider searchForServiceRequests(ReferenceAndListParam patientReference, TokenAndListParam code,
ReferenceAndListParam encounterReference, ReferenceAndListParam participantReference, DateRangeParam occurrence,
TokenAndListParam uuid, DateRangeParam lastUpdated, HashSet<Include> includes);
IBundleProvider searchForServiceRequests(ServiceRequestSearchParams serviceRequestSearchParams);

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,35 @@

import static org.hibernate.criterion.Restrictions.and;
import static org.hibernate.criterion.Restrictions.or;
import static org.hibernate.criterion.Subqueries.propertyIn;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.HasAndListParam;
import ca.uhn.fhir.rest.param.HasParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import lombok.AccessLevel;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Criteria;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hl7.fhir.r4.model.Observation;
import org.openmrs.ConceptClass;
import org.openmrs.EncounterProvider;
import org.openmrs.Obs;
import org.openmrs.Obs.Status;
import org.openmrs.TestOrder;
import org.openmrs.module.fhir2.FhirConstants;
import org.openmrs.module.fhir2.api.dao.FhirServiceRequestDao;
Expand All @@ -30,6 +48,7 @@

@Component
@Setter(AccessLevel.PACKAGE)
@Slf4j
public class FhirServiceRequestDaoImpl extends BaseFhirDao<TestOrder> implements FhirServiceRequestDao<TestOrder> {

@Override
Expand All @@ -45,6 +64,9 @@ protected void setupSearchParams(Criteria criteria, SearchParameterMap theParams
entry.getValue().forEach(
param -> handleEncounterReference(criteria, (ReferenceAndListParam) param.getParam(), "e"));
break;
case FhirConstants.HAS_SEARCH_HANDLER:
entry.getValue().forEach(param -> handleHasAndListParam(criteria, (HasAndListParam) param.getParam()));
break;
case FhirConstants.PATIENT_REFERENCE_SEARCH_HANDLER:
entry.getValue().forEach(patientReference -> handlePatientReference(criteria,
(ReferenceAndListParam) patientReference.getParam(), "patient"));
Expand Down Expand Up @@ -89,4 +111,295 @@ private Optional<Criterion> handleDateRange(DateRangeParam dateRangeParam) {
handleDate("autoExpireDate", dateRangeParam.getUpperBound())))))))));
}

/**
* Handle _has parameters that are passed in to constrain the ServiceRequest resource on properties
* of dependent resources
*/
private void handleHasAndListParam(Criteria criteria, HasAndListParam hasAndListParam) {
if (criteria == null) {
log.warn("handleHasAndListParam called without criteria.");
return;
}

if (hasAndListParam == null) {
log.warn("handleHasAndListParam called without param or param list.");
return;
}

hasAndListParam.getValuesAsQueryTokens().forEach(hasOrListParam -> {
List<HasParam> queryTokens = hasOrListParam.getValuesAsQueryTokens();
if (queryTokens.isEmpty()) {
return;
}

// Making the assumption that any "orListParams" match everything except for the value
HasParam hasParam = queryTokens.get(0);
Set<String> values = queryTokens.stream().map(HasParam::getParameterValue).collect(Collectors.toSet());

switch (hasParam.getTargetResourceType()) {
case FhirConstants.OBSERVATION:
handleHasObservation(criteria, hasParam, values);
break;
default:
log.warn("_has parameter not supported: " + hasParam.getQueryParameterQualifier());
}
});
}

private void handleHasObservation(Criteria criteria, HasParam hasParam, Set<String> values) {
String projection;
switch (hasParam.getReferenceFieldName()) {
case Observation.SP_BASED_ON:
projection = "order";
break;
default:
log.warn("Failed to add has constraint for non-existent reference " + hasParam.getReferenceFieldName());
// ensure no entries are found
criteria.add(Restrictions.isNull("id"));
return;
}

String parameterName = hasParam.getParameterName();
String parameterValue = hasParam.getParameterValue();
DetachedCriteria observationCriteria = createObservationCriteria(projection, parameterName, parameterValue);

criteria.add(propertyIn("id", observationCriteria));
}

/**
* Creates a detached criteria representing the search for the parameterName with a value
* representing the given parameterValue using the given projection.
* <p>
* The available FHIR parameters will be converted where applicable, however some parameters can not
* be represented:
* <ul>
* <li><code>Observation.SP_SPECIMEN</code>: a close match would be accessionIdentifier but wouldn't
* represent the entire specimen</li>
* <li><code>Observation.SP_FOCUS</code>, <code>Observation.SP_DERIVED_FROM</code>,
* <code>Observation.SP_METHOD</code>, <code>Observation.SP_DATA_ABSENT_REASON</code>,
* <code>Observation.SP_DEVICE</code>: no meaningful mapping in OpenMRS</li>
* <li>SP_COMPONENT_* (like <code>Observation.SP_COMPONENT_CODE</code>) and SP_COMBO_* (like
* <code>Observation.SP_COMBO_VALUE_QUANTITY</code>): OpenMRS does not support Components yet</li>
* <li><code>Observation.SP_CODE_VALUE_STRING</code>, <code>Observation.SP_PART_OF</code>,
* <code>Observation.SP_CODE_VALUE_DATE</code>, <code>Observation.SP_CODE_VALUE_QUANTITY</code>,
* <code>Observation.SP_CODE_VALUE_CONCEPT</code>: no meaningful mappings could be found during
* development. This may or may not change in the future.</li>
* </ul>
* Unrepresentable parameters will return a detached criteria containing no matches.
* </p>
*
* @param projection the string determining the projection of the detached criteria
* @param parameterName the string representing the parameter to be searched for
* @param parameterValue the string containing a representation of the value to be compared with
* @return the detached criteria representing the observation criteria
*/
// the detached criteria in this file should all return a subquery of ids to make usage of them consistent
private DetachedCriteria createObservationCriteria(String projection, String parameterName, String parameterValue) {
DetachedCriteria observationQuery = DetachedCriteria.forClass(Obs.class);
observationQuery.setProjection(Projections.property(projection));

if (parameterName == null) {
// just check for existence of any observation if no further propertyname is given
return observationQuery;
}

switch (parameterName) {
case Observation.SP_DATE:
addSimpleDateSearch(observationQuery, "obsDatetime", parameterValue);
break;

case Observation.SP_PATIENT:
case Observation.SP_SUBJECT:
addReferenceSearchByUuid(observationQuery, "person", parameterValue);
break;

case Observation.SP_VALUE_CONCEPT:
addReferenceSearchByUuid(observationQuery, "valueCoded", parameterValue);
break;

case Observation.SP_VALUE_DATE:
addSimpleDateSearch(observationQuery, "valueDatetime", parameterValue);
break;

case Observation.SP_HAS_MEMBER:
DetachedCriteria memberQuery = DetachedCriteria.forClass(Obs.class);
memberQuery.setProjection(Projections.property("obsGroup"));

if (parameterValue != null) {
memberQuery.add(Restrictions.eq("uuid", parameterValue));
}

observationQuery.add(propertyIn("id", memberQuery));
break;

case Observation.SP_VALUE_STRING:
addSimpleSearch(observationQuery, "valueText", parameterValue);
break;

case Observation.SP_IDENTIFIER:
addSimpleSearch(observationQuery, "uuid", parameterValue);
break;

case Observation.SP_ENCOUNTER:
addReferenceSearchByUuid(observationQuery, "encounter", parameterValue);
break;

case Observation.SP_CATEGORY:
if (parameterValue == null) {
observationQuery.add(Restrictions.isNotNull("concept"));
break;
}

// TODO: where are the possible values defined? Are all values in ConceptClass relevant?
switch (parameterValue) {
case "laboratory":
addReferenceSearchByUuid(observationQuery, "concept", ConceptClass.LABSET_UUID);
break;

default:
log.warn(
"Failed to add has constraint for observation category with unknown concept " + parameterValue);
// ensure no entries are found
addNoResultsCriteria(observationQuery);
}
break;

case Observation.SP_STATUS:
addEnumSearch(observationQuery, "status", Obs.Status.class, parameterValue);
break;

case Observation.SP_CODE:
addReferenceSearchByUuid(observationQuery, "concept", parameterValue);
break;

case Observation.SP_VALUE_QUANTITY:
addSimpleDoubleSearch(observationQuery, "valueNumeric", parameterValue);
break;

case Observation.SP_PERFORMER:
DetachedCriteria encounterProviderQuery = DetachedCriteria.forClass(EncounterProvider.class);
encounterProviderQuery.setProjection(Projections.property("encounter"));

if (parameterValue != null) {
encounterProviderQuery.createAlias("provider", "provider");
encounterProviderQuery.add(Restrictions.eq("provider.uuid", parameterValue));
}

observationQuery.add(propertyIn("encounter", encounterProviderQuery));
break;

case Observation.SP_CODE_VALUE_STRING:
case Observation.SP_PART_OF:
case Observation.SP_CODE_VALUE_DATE:
case Observation.SP_CODE_VALUE_QUANTITY:
case Observation.SP_CODE_VALUE_CONCEPT:
case Observation.SP_SPECIMEN:
case Observation.SP_FOCUS:
case Observation.SP_DERIVED_FROM:
case Observation.SP_METHOD:
case Observation.SP_DATA_ABSENT_REASON:
case Observation.SP_DEVICE:
case Observation.SP_COMPONENT_DATA_ABSENT_REASON:
case Observation.SP_COMPONENT_CODE_VALUE_QUANTITY:
case Observation.SP_COMPONENT_VALUE_QUANTITY:
case Observation.SP_COMPONENT_CODE_VALUE_CONCEPT:
case Observation.SP_COMPONENT_VALUE_CONCEPT:
case Observation.SP_COMPONENT_CODE:
case Observation.SP_COMBO_DATA_ABSENT_REASON:
case Observation.SP_COMBO_CODE:
case Observation.SP_COMBO_CODE_VALUE_QUANTITY:
case Observation.SP_COMBO_CODE_VALUE_CONCEPT:
case Observation.SP_COMBO_VALUE_QUANTITY:
case Observation.SP_COMBO_VALUE_CONCEPT:
log.warn("Failed to add has constraint for observation search parameter " + parameterValue
+ ": Not Implemented.");
// ensure no entries are found
addNoResultsCriteria(observationQuery);
break;

default:
log.warn("Failed to add has constraint for observation search parameter " + parameterValue
+ ": Invalid search parameter.");
// ensure no entries are found
addNoResultsCriteria(observationQuery);
break;
}

return observationQuery;
}

private <T extends Enum<T>> void addEnumSearch(DetachedCriteria observationQuery, String parameterField,
Class<T> enumClass, String parameterValue) {
if (parameterValue == null) {
addSimpleSearch(observationQuery, parameterField, parameterValue);
return;
}

try {
T enumValue = Enum.valueOf(enumClass, parameterValue);
addSimpleSearch(observationQuery, parameterField, enumValue);
}
catch (IllegalArgumentException e) {
log.warn("Failed to parse Enum " + parameterValue);
// ensure no entries are found
addNoResultsCriteria(observationQuery);
}
}

private void addReferenceSearchByUuid(DetachedCriteria observationQuery, String parameterField, String parameterValue) {
if (parameterValue == null) {
observationQuery.add(Restrictions.isNotNull(parameterField));
return;
}

observationQuery.createAlias(parameterField, parameterField);
observationQuery.add(Restrictions.eq(parameterField + ".uuid", parameterValue));
}

private void addSimpleDateSearch(DetachedCriteria observationQuery, String parameterField, String parameterValue) {
if (parameterValue == null) {
addSimpleSearch(observationQuery, parameterField, parameterValue);
return;
}

try {
Date valueDate = DateFormat.getDateTimeInstance().parse(parameterValue);
addSimpleSearch(observationQuery, parameterField, valueDate);
}
catch (ParseException e) {
log.warn("Failed to parse Date " + parameterValue);
// ensure no entries are found
addNoResultsCriteria(observationQuery);
}
}

private void addSimpleDoubleSearch(DetachedCriteria observationQuery, String parameterField, String parameterValue) {
if (parameterValue == null) {
addSimpleSearch(observationQuery, parameterField, parameterValue);
return;
}

try {
Double valueInteger = Double.parseDouble(parameterValue);
addSimpleSearch(observationQuery, parameterField, valueInteger);
}
catch (NumberFormatException e) {
log.warn("Failed to parse Double " + parameterValue);
// ensure no entries are found
addNoResultsCriteria(observationQuery);
}
}

private void addSimpleSearch(DetachedCriteria observationQuery, String parameterField, Object parameterValue) {
if (parameterValue == null) {
observationQuery.add(Restrictions.isNotNull(parameterField));
return;
}

observationQuery.add(Restrictions.eq(parameterField, parameterValue));
}

private void addNoResultsCriteria(DetachedCriteria observationQuery) {
observationQuery.add(Restrictions.isNull("id"));
}
}
Loading
Loading