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

[HIE-2] Skeleton code for client registry search flow #11

Merged
merged 6 commits into from
Mar 21, 2024
Merged
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 @@ -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");
Comment on lines +107 to +111
Copy link
Member Author

Choose a reason for hiding this comment

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

Do we plan to support HL7 or other formats in the future? If not, can we just simply support FHIR? cc: @pmanko @smallgod @ebambo

Copy link

Choose a reason for hiding this comment

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

I would vote for FHIR only as any other transformation will reside outside of this module

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, gonna file a ticket to address this.

}
}
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
Loading