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 #5

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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.stereotype.Component;
Expand All @@ -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);
}
Expand All @@ -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);
}
}
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 {
Copy link
Collaborator Author

@pmanko2 pmanko2 Jan 24, 2023

Choose a reason for hiding this comment

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

this only needs to be determined once on module startup (and on property changed/deleted), maybe we move the logic into ClientRegistryActivator ?

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
@@ -0,0 +1,77 @@
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.Parameters;
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 source identifier/system and target systems.
*/
@Override
public List<Patient> getCRPatients(String sourceIdentifier, String sourceIdentifierSystem, List<String> targetSystems) {


Parameters betterCrRequest = fhirClient
.operation()
.onInstance(String.format("%s|%s", sourceIdentifierSystem, sourceIdentifier))
.named(FhirCRConstants.IHE_PIX_OPERATION)
.withNoParameters(Parameters.class)
.execute();
samuelmale marked this conversation as resolved.
Show resolved Hide resolved

// construct and send request to external client registry
IQuery<IBaseBundle> crRequest = fhirClient
.search()
.byUrl(
String.format("%s/%s", config.getClientRegistryServerUrl(), config.getClientRegistryGetPatientEndpoint()))
.where(
FhirCRConstants.SOURCE_IDENTIFIER_PARAM.exactly().systemAndIdentifier(sourceIdentifierSystem,
sourceIdentifier));

if (!targetSystems.isEmpty()) {
crRequest.and(FhirCRConstants.TARGET_SYSTEM_PARAM.matches().values(targetSystems));
}

Bundle patientBundle = crRequest.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,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");
}
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");
}
}
28 changes: 26 additions & 2 deletions omod/src/main/resources/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,37 @@

<!-- Required Global Properties -->
<globalProperty>
<property>@MODULE_ID@.serverUrl</property>
<defaultValue>http://localhost:5001/CR/fhir/</defaultValue>
<property>@MODULE_ID@.clientRegistryServerUrl</property>
<defaultValue>http://localhost:5001/CR/fhir</defaultValue>
<description>
Base URL for the Client Registry Server
</description>
</globalProperty>

<globalProperty>
<property>@[email protected]</property>
<defaultValue>Patient/$ihe-pix</defaultValue>
<description>
Client registry endpoint implementing the Patient identifier cross-reference transaction (ITI-83)
</description>
</globalProperty>

<globalProperty>
<property>@[email protected]</property>
<defaultValue></defaultValue>
<description>
Default system from which the Patient identifiers will be returned, if no system provided in requests
</description>
</globalProperty>

<globalProperty>
<property>@[email protected]</property>
<defaultValue>fhir</defaultValue>
<description>
Transaction method supported by the Client Registry. Currently supporting fhir or hl7.
</description>
</globalProperty>

<globalProperty>
<property>@[email protected]</property>
<defaultValue>openmrs</defaultValue>
Expand Down
Loading