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 b7bb866..574e7f7 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.beans.factory.annotation.Value; @@ -46,7 +47,21 @@ public boolean clientRegistryConnectionEnabled() { public String getClientRegistryServerUrl() { return serverUrl; } - + + 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 username; } @@ -58,4 +73,8 @@ public String getClientRegistryPassword() { public String getClientRegistryIdentifierRoot() { return identifierRoot; } + + 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..f1cc9da --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/api/CRPatientService.java @@ -0,0 +1,13 @@ +package org.openmrs.module.clientregistry.api; + +import org.hl7.fhir.r4.model.Patient; +import org.openmrs.module.fhir2.api.search.param.PatientSearchParams; + +import java.util.List; + +public interface CRPatientService { + + List getCRPatients(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/event/PatientCreateUpdateListener.java b/api/src/main/java/org/openmrs/module/clientregistry/api/event/PatientCreateUpdateListener.java index 27973fd..bab5bce 100644 --- a/api/src/main/java/org/openmrs/module/clientregistry/api/event/PatientCreateUpdateListener.java +++ b/api/src/main/java/org/openmrs/module/clientregistry/api/event/PatientCreateUpdateListener.java @@ -5,6 +5,7 @@ import javax.jms.Message; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -20,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; +import ca.uhn.fhir.parser.DataFormatException; @Component public class PatientCreateUpdateListener implements EventListener { @@ -98,7 +100,18 @@ private void processMessage(Message message) throws JMSException { if (mapMessage.getJMSDestination().toString().equals(ClientRegistryConstants.UPDATE_MESSAGE_DESTINATION)) { client.update().resource(patient).execute(); } else { - client.create().resource(patient).execute(); + try { + client.create().resource(patient).execute(); + } + catch (FhirClientConnectionException e) { + Throwable cause = e.getCause(); + if (cause instanceof DataFormatException) { + // just warn if the CR responds with unsupported data format + log.warn(e.getMessage()); + } else { + throw e; + } + } } } } 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..6f91773 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/api/impl/FhirCRPatientServiceImpl.java @@ -0,0 +1,89 @@ +package org.openmrs.module.clientregistry.api.impl; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput; +import ca.uhn.fhir.rest.gclient.IQuery; +import ca.uhn.fhir.rest.gclient.StringClientParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.UriOrListParam; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.r4.model.*; +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.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Component +public class FhirCRPatientServiceImpl implements CRPatientService { + + @Autowired + private IGenericClient fhirClient; + + @Autowired + private ClientRegistryConfig config; + + /** + * Get patient identifiers from an external client registry's $ihe-pix implementation. Use the + * returned identifiers to then request a matching Patient bundle from the client registry. + */ + @Override + public List getCRPatients(String sourceIdentifier, String sourceIdentifierSystem, List targetSystems) { + // construct request to external FHIR $ihe-pix endpoint + IOperationUntypedWithInputAndPartialOutput identifiersRequest = fhirClient + .operation() + .onType(FhirConstants.PATIENT) + .named(FhirCRConstants.IHE_PIX_OPERATION) + .withSearchParameter(Parameters.class, FhirCRConstants.SOURCE_IDENTIFIER, new TokenParam(sourceIdentifierSystem, sourceIdentifier)); + + if (!targetSystems.isEmpty()) { + identifiersRequest.andSearchParameter(FhirCRConstants.TARGET_SYSTEM, new StringParam(String.join(",", targetSystems))); + } + + Parameters crMatchingParams = identifiersRequest.useHttpGet().execute(); + List crIdentifiers = crMatchingParams.getParameter().stream() + .filter(param -> Objects.equals(param.getName(), "targetId")) + .map(param -> param.getValue().toString()) + .collect(Collectors.toList()); + + if (crIdentifiers.isEmpty()) { + return Collections.emptyList(); + } + + // construct and send request to external client registry + Bundle patientBundle = fhirClient + .search() + .forResource(Patient.class) + .where(new StringClientParam(Patient.SP_RES_ID).matches().values(crIdentifiers)) + .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..66c7286 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/providers/FhirCRConstants.java @@ -0,0 +1,27 @@ +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; +import ca.uhn.fhir.rest.param.StringOrListParam; + +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..c88cad7 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/clientregistry/providers/r4/FhirCRPatientResourceProvider.java @@ -0,0 +1,99 @@ +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.param.TokenParam; +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={sourceSystem|}1234[&targetSystem=system1,system2] + * + * @param sourceIdentifierParam patient identifier token. If source system is included in token, + * we will use it to override the module defined source system. + * @param targetSystemsParam (optional) Patient assigning authorities (ie systems) from which + * the returned identifiers shall be selected + * @return List of matching FHIR patients returned by the client registry + */ + @Operation(name = FhirCRConstants.IHE_PIX_OPERATION, idempotent=true, type = Patient.class, bundleType = BundleTypeEnum.SEARCHSET) + public List getCRPatientById( + @OperationParam(name = FhirCRConstants.SOURCE_IDENTIFIER) TokenParam 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 sourceSystem provided, use config defined default + boolean userDefinedSourceSystem = sourceIdentifierParam.getSystem() != null && !sourceIdentifierParam.getSystem().isEmpty(); + String sourceIdentifierSystem = userDefinedSourceSystem + ? sourceIdentifierParam.getSystem() + : config.getClientRegistryDefaultPatientIdentifierSystem(); + + if (sourceIdentifierSystem == null || sourceIdentifierSystem.isEmpty()) { + throw new InvalidRequestException("ClientRegistry module does not have a default source system assigned " + + "via the defaultPatientIdentifierSystem property. Source system must be provided as a token in " + + "the sourceIdentifier request param"); + } + + List patients = clientRegistryManager.getPatientService().getCRPatients( + 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 57915de..5eafd0d 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -32,13 +32,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 eb74137..0d474cd 100644 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ org.openmrs.tools openmrs-tools - ${openmrsPlatformVersion} + ${openmrsPlatformToolsVersion} @@ -216,7 +216,8 @@ - 1.5.1 + 1.8.0 1.11.6 + 2.4.0