Skip to content

Commit

Permalink
Merge pull request #11 from openmrs/HIE-2-pixm
Browse files Browse the repository at this point in the history
[HIE-2] Skeleton code for client registry search flow
  • Loading branch information
samuelmale authored Mar 21, 2024
2 parents e693659 + 7a17001 commit cdd2597
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -58,4 +73,8 @@ public String getClientRegistryPassword() {
public String getClientRegistryIdentifierRoot() {
return identifierRoot;
}

public String getClientRegistryTransactionMethod() {
return administrationService.getGlobalProperty(ClientRegistryConstants.GP_CLIENT_REGISTRY_TRANSACTION_METHOD);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

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";

public static final String GP_CLIENT_REGISTRY_PASSWORD = "clientregistry.password";

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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.openmrs.module.clientregistry;

public enum ClientRegistryTransactionType {
FHIR,
HL7
}
Original file line number Diff line number Diff line change
@@ -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<Patient> getCRPatients(String sourceIdentifier, String sourceIdentifierSystem, List<String> extraTargetSystems);

List<Patient> searchCRForPatients(PatientSearchParams patientSearchParams);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Patient> getCRPatients(String sourceIdentifier, String sourceIdentifierSystem, List<String> targetSystems) {
// construct request to external FHIR $ihe-pix endpoint
IOperationUntypedWithInputAndPartialOutput<Parameters> 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<String> 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<Patient> searchCRForPatients(PatientSearchParams patientSearchParams) {
return null;
}

/**
* Filter and parse out fhir patients from Client Registry Patient Search results
*/
private List<Patient> parseCRPatientSearchResults(Bundle patientBundle) {
return patientBundle
.getEntry()
.stream()
.filter(entry -> entry.hasType(FhirConstants.PATIENT))
.map(entry -> (Patient) entry.getResource())
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
@@ -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<? extends IBaseResource> 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<Patient> 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<String> 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<Patient> patients = clientRegistryManager.getPatientService().getCRPatients(
sourceIdentifierParam.getValue(), sourceIdentifierSystem, targetSystems
);

if (patients.isEmpty()) {
throw new ResourceNotFoundException("No Client Registry patients found.");
}

return patients;
}

@Search
public List<Patient> searchClientRegistryPatients() {
throw new NotImplementedOperationException("search client registry is not yet implemented");
}
}
Loading

0 comments on commit cdd2597

Please sign in to comment.