From e14b7ae2b926428c3c9989f4acc24cf02f088d21 Mon Sep 17 00:00:00 2001 From: Pawel Mankowski Date: Mon, 3 Oct 2022 18:06:25 -0400 Subject: [PATCH] [HIE-2] client registry Patient search flow implementation [HIE-2] define ihe-pix operation and start implementation of getCRPatient [HIE-2] adding config options for default system and get patient endpoint [HIE-2] drop format param and change targetSystems to OR [HIE-2] move targetSystem param logic to controller [HIE-2] change to OperationParam in getCRPatient provider method cleanup, adding comments, refactoring Patient parsing --- .../clientregistry/ClientRegistryConfig.java | 19 ++++ .../ClientRegistryConstants.java | 8 +- .../ClientRegistryTransactionType.java | 6 ++ .../clientregistry/api/CRPatientService.java | 14 +++ .../api/ClientRegistryManager.java | 35 ++++++ .../api/impl/FhirCRPatientServiceImpl.java | 67 ++++++++++++ .../providers/FhirCRConstants.java | 26 +++++ .../r4/FhirCRPatientResourceProvider.java | 102 ++++++++++++++++++ omod/src/main/resources/config.xml | 28 ++++- pom.xml | 5 +- 10 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryTransactionType.java create mode 100644 api/src/main/java/org/openmrs/module/clientregistry/api/CRPatientService.java create mode 100644 api/src/main/java/org/openmrs/module/clientregistry/api/impl/FhirCRPatientServiceImpl.java create mode 100644 api/src/main/java/org/openmrs/module/clientregistry/providers/FhirCRConstants.java create mode 100644 api/src/main/java/org/openmrs/module/clientregistry/providers/r4/FhirCRPatientResourceProvider.java diff --git a/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConfig.java b/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConfig.java index 257d172..f7dd537 100644 --- a/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConfig.java +++ b/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConfig.java @@ -10,6 +10,7 @@ import org.apache.commons.lang.StringUtils; import org.openmrs.api.AdministrationService; +import org.openmrs.module.clientregistry.providers.FhirCRConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @@ -34,6 +35,20 @@ public String getClientRegistryServerUrl() { return administrationService.getGlobalProperty(ClientRegistryConstants.GP_CLIENT_REGISTRY_SERVER_URL); } + public String getClientRegistryGetPatientEndpoint() { + String globalPropPatientEndpoint = administrationService + .getGlobalProperty(ClientRegistryConstants.GP_FHIR_CLIENT_REGISTRY_GET_PATIENT_ENDPOINT); + + // default to Patient/$ihe-pix if patient endpoint is not defined in config + return (globalPropPatientEndpoint == null || globalPropPatientEndpoint.isEmpty()) ? String.format("Patient/%s", + FhirCRConstants.IHE_PIX_OPERATION) : globalPropPatientEndpoint; + } + + public String getClientRegistryDefaultPatientIdentifierSystem() { + return administrationService + .getGlobalProperty(ClientRegistryConstants.GP_CLIENT_REGISTRY_DEFAULT_PATIENT_IDENTIFIER_SYSTEM); + } + public String getClientRegistryUserName() { return administrationService.getGlobalProperty(ClientRegistryConstants.GP_CLIENT_REGISTRY_USER_NAME); } @@ -45,4 +60,8 @@ public String getClientRegistryPassword() { public String getClientRegistryIdentifierRoot() { return administrationService.getGlobalProperty(ClientRegistryConstants.GP_CLIENT_REGISTRY_IDENTIFIER_ROOT); } + + public String getClientRegistryTransactionMethod() { + return administrationService.getGlobalProperty(ClientRegistryConstants.GP_CLIENT_REGISTRY_TRANSACTION_METHOD); + } } diff --git a/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConstants.java b/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConstants.java index f10b6b0..bde5225 100644 --- a/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConstants.java +++ b/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryConstants.java @@ -2,7 +2,11 @@ public class ClientRegistryConstants { - public static final String GP_CLIENT_REGISTRY_SERVER_URL = "clientregistry.serverUrl"; + public static final String GP_CLIENT_REGISTRY_SERVER_URL = "clientregistry.clientRegistryServerUrl"; + + public static final String GP_FHIR_CLIENT_REGISTRY_GET_PATIENT_ENDPOINT = "clientregistry.fhirGetPatientEndpoint"; + + public static final String GP_CLIENT_REGISTRY_DEFAULT_PATIENT_IDENTIFIER_SYSTEM = "clientregistry.defaultPatientIdentifierSystem"; public static final String GP_CLIENT_REGISTRY_USER_NAME = "clientregistry.username"; @@ -10,6 +14,8 @@ public class ClientRegistryConstants { public static final String GP_CLIENT_REGISTRY_IDENTIFIER_ROOT = "clientregistry.identifierRoot"; + public static final String GP_CLIENT_REGISTRY_TRANSACTION_METHOD = "clientregistry.transactionMethod"; + public static final String UPDATE_MESSAGE_DESTINATION = "topic://UPDATED:org.openmrs.Patient"; public static final String CLIENT_REGISTRY_INTERNAL_ID_SYSTEM = "http://clientregistry.org/openmrs"; diff --git a/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryTransactionType.java b/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryTransactionType.java new file mode 100644 index 0000000..57d83f1 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/ClientRegistryTransactionType.java @@ -0,0 +1,6 @@ +package org.openmrs.module.clientregistry; + +public enum ClientRegistryTransactionType { + FHIR, + HL7 +} diff --git a/api/src/main/java/org/openmrs/module/clientregistry/api/CRPatientService.java b/api/src/main/java/org/openmrs/module/clientregistry/api/CRPatientService.java new file mode 100644 index 0000000..93a2369 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/api/CRPatientService.java @@ -0,0 +1,14 @@ +package org.openmrs.module.clientregistry.api; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.r4.model.Patient; +import org.openmrs.module.fhir2.api.search.param.PatientSearchParams; + +import java.util.List; + +public interface CRPatientService { + + List getCRPatient(String sourceIdentifier, String sourceIdentifierSystem, List extraTargetSystems); + + List searchCRForPatients(PatientSearchParams patientSearchParams); +} diff --git a/api/src/main/java/org/openmrs/module/clientregistry/api/ClientRegistryManager.java b/api/src/main/java/org/openmrs/module/clientregistry/api/ClientRegistryManager.java index 3b82308..512921f 100644 --- a/api/src/main/java/org/openmrs/module/clientregistry/api/ClientRegistryManager.java +++ b/api/src/main/java/org/openmrs/module/clientregistry/api/ClientRegistryManager.java @@ -10,8 +10,11 @@ import org.openmrs.api.GlobalPropertyListener; import org.openmrs.event.Event; import org.openmrs.module.DaemonToken; +import org.openmrs.module.clientregistry.ClientRegistryConfig; import org.openmrs.module.clientregistry.ClientRegistryConstants; +import org.openmrs.module.clientregistry.ClientRegistryTransactionType; import org.openmrs.module.clientregistry.api.event.PatientCreateUpdateListener; +import org.openmrs.module.clientregistry.api.impl.FhirCRPatientServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -27,6 +30,12 @@ public class ClientRegistryManager implements GlobalPropertyListener { @Autowired private PatientCreateUpdateListener patientListener; + @Autowired + private FhirCRPatientServiceImpl fhirPatientService; + + @Autowired + private ClientRegistryConfig clientRegistryConfig; + public void setDaemonToken(DaemonToken daemonToken) { this.daemonToken = daemonToken; } @@ -82,4 +91,30 @@ public void disableClientRegistry() { isRunning.set(false); } + + /** + * Determine the appropriate PatientService class based off of the client registry transaction + * type configuration + * + * @return PatientService class corresponding to the appropriate transaction type supported by + * the client registry + * @throws IllegalArgumentException if defined transaction type is unsupported + */ + public CRPatientService getPatientService() throws IllegalArgumentException { + try { + String transactionMethodGlobalProperty = clientRegistryConfig.getClientRegistryTransactionMethod().toUpperCase(); + + switch (ClientRegistryTransactionType.valueOf(transactionMethodGlobalProperty)) { + case FHIR: + return fhirPatientService; + case HL7: + throw new IllegalArgumentException("HL7 transaction type is currently unsupported"); + } + } + catch (Exception ignored) { + + } + + throw new IllegalArgumentException("Unsupported transaction type"); + } } diff --git a/api/src/main/java/org/openmrs/module/clientregistry/api/impl/FhirCRPatientServiceImpl.java b/api/src/main/java/org/openmrs/module/clientregistry/api/impl/FhirCRPatientServiceImpl.java new file mode 100644 index 0000000..ee338c7 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/api/impl/FhirCRPatientServiceImpl.java @@ -0,0 +1,67 @@ +package org.openmrs.module.clientregistry.api.impl; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.IQuery; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Patient; +import org.openmrs.module.clientregistry.ClientRegistryConfig; +import org.openmrs.module.clientregistry.api.CRPatientService; +import org.openmrs.module.clientregistry.providers.FhirCRConstants; +import org.openmrs.module.fhir2.FhirConstants; +import org.openmrs.module.fhir2.api.search.param.PatientSearchParams; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class FhirCRPatientServiceImpl implements CRPatientService { + + @Autowired + private IGenericClient fhirClient; + + @Autowired + private ClientRegistryConfig config; + + /** + * Constructs a $ihe-pix fhir client call to an external Client Registry returning any patients that match the given + * identifier and target systems. + */ + @Override + public List getCRPatient(String sourceIdentifier, String sourceIdentifierSystem, List extraTargetSystems) { + // construct and send request to external client registry + IQuery crRequest = fhirClient + .search() + .byUrl( + String.format("%s/%s", config.getClientRegistryServerUrl(), config.getClientRegistryGetPatientEndpoint())) + .where( + FhirCRConstants.SOURCE_IDENTIFIER_PARAM.exactly().systemAndIdentifier(sourceIdentifierSystem, + sourceIdentifier)); + + if (!extraTargetSystems.isEmpty()) { + crRequest.and(FhirCRConstants.TARGET_SYSTEM_PARAM.matches().values(extraTargetSystems)); + } + + Bundle patientBundle = crRequest.returnBundle(Bundle.class).execute(); + return parseCRPatientSearchResults(patientBundle); + } + + @Override + public List searchCRForPatients(PatientSearchParams patientSearchParams) { + return null; + } + + /** + * Filter and parse out fhir patients from Client Registry Patient Search results + */ + private List parseCRPatientSearchResults(Bundle patientBundle) { + return patientBundle + .getEntry() + .stream() + .filter(entry -> entry.hasType(FhirConstants.PATIENT)) + .map(entry -> (Patient) entry.getResource()) + .collect(Collectors.toList()); + } +} diff --git a/api/src/main/java/org/openmrs/module/clientregistry/providers/FhirCRConstants.java b/api/src/main/java/org/openmrs/module/clientregistry/providers/FhirCRConstants.java new file mode 100644 index 0000000..3ec4eea --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/providers/FhirCRConstants.java @@ -0,0 +1,26 @@ +package org.openmrs.module.clientregistry.providers; + +import ca.uhn.fhir.model.api.annotation.SearchParamDefinition; +import ca.uhn.fhir.rest.gclient.StringClientParam; +import ca.uhn.fhir.rest.gclient.TokenClientParam; +import ca.uhn.fhir.rest.gclient.UriClientParam; + +public class FhirCRConstants { + + public static final String IHE_PIX_OPERATION = "$ihe-pix"; + + @SearchParamDefinition(name = "sourceIdentifier", path = "Patient.sourceIdentifier", description = "A patient identifier used to find cross-matching identifiers in client registry", type = "token") + public static final String SOURCE_IDENTIFIER = "sourceIdentifier"; + + public static final TokenClientParam SOURCE_IDENTIFIER_PARAM = new TokenClientParam("sourceIdentifier"); + + @SearchParamDefinition(name = "targetSystem", path = "Patient.targetSystem", description = "Assigning Authorities for the Patient Identifier Domains from which the returned identifiers shall be selected", type = "token") + public static final String TARGET_SYSTEM = "targetSystem"; + + public static final UriClientParam TARGET_SYSTEM_PARAM = new UriClientParam("targetSystem"); + + @SearchParamDefinition(name = "_format", path = "Patient.targetSystem", description = "Assigning Authorities for the Patient Identifier Domains from which the returned identifiers shall be selected", type = "token") + public static final String _FORMAT = "_FORMAT"; + + public static final StringClientParam _FORMAT_PARAM = new StringClientParam("_format"); +} diff --git a/api/src/main/java/org/openmrs/module/clientregistry/providers/r4/FhirCRPatientResourceProvider.java b/api/src/main/java/org/openmrs/module/clientregistry/providers/r4/FhirCRPatientResourceProvider.java new file mode 100644 index 0000000..83638cd --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/providers/r4/FhirCRPatientResourceProvider.java @@ -0,0 +1,102 @@ +package org.openmrs.module.clientregistry.providers.r4; + +import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import lombok.Setter; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Patient; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import org.openmrs.module.clientregistry.ClientRegistryConfig; +import org.openmrs.module.clientregistry.api.ClientRegistryManager; +import org.openmrs.module.clientregistry.providers.FhirCRConstants; +import org.openmrs.module.fhir2.api.annotations.R4Provider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static lombok.AccessLevel.PACKAGE; + +@Component("crPatientFhirR4ResourceProvider") +@R4Provider +@Setter(PACKAGE) +public class FhirCRPatientResourceProvider implements IResourceProvider { + + @Autowired + private ClientRegistryManager clientRegistryManager; + + @Autowired + private ClientRegistryConfig config; + + @Override + public Class getResourceType() { + return Patient.class; + } + + /** + * FHIR endpoint to get Patient references from external client registry Example request: GET + * [fhirbase]/Patient/$ihe-pix?sourceIdentifier=1234[&targetSystem=system1,system2] + * + * @param sourceIdentifierParam patient identifier + * @param targetSystemsParam (optional) Patient assigning authorities (ie systems) from which + * the returned identifiers shall be selected. Use module defined default if not + * provided. + * @return OpenMRS Patient corresponding to identifier (TODO this might change to a list of + * identifier references returned by CR) + */ + @Operation(name = FhirCRConstants.IHE_PIX_OPERATION, idempotent=true, type = Patient.class, bundleType = BundleTypeEnum.SEARCHSET) + public List getCRPatientById( + @OperationParam(name = FhirCRConstants.SOURCE_IDENTIFIER) StringParam sourceIdentifierParam, + @OperationParam(name = FhirCRConstants.TARGET_SYSTEM) StringOrListParam targetSystemsParam + ) { + + if (sourceIdentifierParam == null || sourceIdentifierParam.getValue() == null) { + throw new InvalidRequestException("sourceIdentifier must be specified"); + } + + List targetSystems = targetSystemsParam == null + ? Collections.emptyList() + : targetSystemsParam.getValuesAsQueryTokens().stream().filter(Objects::nonNull).map(StringParam::getValue).collect(Collectors.toList()); + + // If no targetSystem provided, use config defined default. Otherwise, take first targetSystem provided and + // include in sourceIdentifier token. Remaining targetSystems included in targetSystem param passed to CR + String sourceIdentifierSystem; + if (targetSystems.isEmpty()) { + sourceIdentifierSystem = config.getClientRegistryDefaultPatientIdentifierSystem(); + } else { + sourceIdentifierSystem = targetSystems.get(0); + targetSystems.remove(0); + } + + if (sourceIdentifierSystem == null || sourceIdentifierSystem.isEmpty()) { + throw new InvalidRequestException("ClientRegistry module does not have a default target system assigned " + + "via the defaultPatientIdentifierSystem property. At least one targetSystem must be provided in " + + "the request"); + } + + List patients = clientRegistryManager.getPatientService().getCRPatient( + sourceIdentifierParam.getValue(), sourceIdentifierSystem, targetSystems + ); + + if (patients.isEmpty()) { + throw new ResourceNotFoundException("No Client Registry patients found."); + } + + return patients; + } + + @Search + public List searchClientRegistryPatients() { + throw new NotImplementedOperationException("search client registry is not yet implemented"); + } +} diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index dbb643f..6d2e745 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -35,13 +35,37 @@ - @MODULE_ID@.serverUrl - http://localhost:5001/CR/fhir/ + @MODULE_ID@.clientRegistryServerUrl + http://localhost:5001/CR/fhir Base URL for the Client Registry Server + + @MODULE_ID@.fhirGetPatientEndpoint + Patient/$ihe-pix + + Client registry endpoint implementing the Patient identifier cross-reference transaction (ITI-83) + + + + + @MODULE_ID@.defaultPatientIdentifierSystem + + + Default system from which the Patient identifiers will be returned, if no system provided in requests + + + + + @MODULE_ID@.transactionMethod + fhir + + Transaction method supported by the Client Registry. Currently supporting fhir or hl7. + + + @MODULE_ID@.username openmrs diff --git a/pom.xml b/pom.xml index cdccace..207a5a1 100644 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ org.openmrs.tools openmrs-tools - ${openmrsPlatformVersion} + ${openmrsPlatformToolsVersion} @@ -217,7 +217,8 @@ 1.1.5 - 1.5.1 + 1.8.0 1.11.6 + 2.4.0