Smile CDR v2024.05.PRE
On this page:

12.8.1MDM Examples

 

This page contains example interceptors that can be registered with MDM.

12.8.2Example: Operate on resources analyzed by MDM, before and after analysis

 

The following interceptor is intended to be used with MDM module, and can be used to operate on resources analyzed by MDM.

The sample interceptor has two goals, when MDM module is called to analyze input Patient resources:

  • The first goal is to make sure Patient resources have US SSN identifier, before MDM analysis
  • The second goal is to create/update a Person resource on an external FHIR repository, after MDM analysis

In the sample interceptor, before passing the input Patient resource to MDM module, using Pointcut.MDM_BEFORE_PERSISTED_RESOURCE_CHECKED, it creates a Task resource for input Patient resource that is missing a US SSN value in its identifiers, making it easier to update these Patient resources later with an appropriate and trackable workflow.

Once a Patient resource has been processed by the MDM module, the interceptor will be invoked using Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED method. The interceptor then verifies if the input Patient resource was newly created, verifies the MDM links to create or update a Person resource based on the linked Patient golden resource information, and finally sends the Person resource to an external FHIR repository.

The sample interceptor also demonstrates how to use Spring Configuration class and DaoRegistry to access data.

/*-
 * #%L
 * Smile CDR - CDR
 * %%
 * Copyright (C) 2016 - 2024 Smile CDR, Inc.
 * %%
 * All rights reserved.
 * #L%
 */
package com.smilecdr.demo.server.r4;

import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkEvent;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage;
import ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage;
import ca.uhn.fhir.util.StopWatch;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * This sample interceptor is intended to be used with MDM module.
 * MDM module configuration 'Interceptor Bean Types' should be set to 'com.smilecdr.demo.server.r4.TestServerAppCtx'
 * to test it in Smile CDR, so appropriate dependencies could be passed to its constructor.
 * <p>
 * The sample interceptor has two goals, when operating on Patient resources.
 * </p><p>
 * The first goal is to make sure Patient resources have US SSN identifier,
 * which is treated as an important value in our sample scenario,
 * before the MDM module searches for candidate Patient resources and tries to create links.
 * If the operated Patient resource is missing a US SSN identifier,
 * the interceptor will then create a Task resource in the internal FHIR repository to facilitate tracking
 * these Patient resources that need to be updated.
 * </p><p>
 * The second goal is to create a Person resource on an external FHIR repository,
 * based on the golden Patient resource, for newly created Patient resources,
 * once the MDM module linked the created Patient resources to a golden Patient resource.
 * </p>
 */
@Interceptor
public class PatientMDMInterceptor {

	// let's add some entries in smile.log!
	private static final Logger ourLog = LoggerFactory.getLogger(PatientMDMInterceptor.class);

	private final static String IDENTIFIER_US_SSN_SYSTEM = "http://hl7.org/fhir/sid/us-ssn";

	// used to access internal / local FHIR repository
	private final IGenericClient myInternalFhirClient;

	// used to access a potential external / remote FHIR repository
	private final IGenericClient myExternalFhirClient;

	// used to access local FHIR repository directly with Dao, see important note about it in TestServerAppCtx
	private final DaoRegistry myDaoRegistry;

	private IFhirResourceDao<Patient> myPatientDao;

	private IFhirResourceDao<Task> myTaskDao;

	public PatientMDMInterceptor(
		IGenericClient theInternalFhirClient,
		IGenericClient theExternalFhirClient,
		DaoRegistry theDaoRegistry) {

		myInternalFhirClient = theInternalFhirClient;
		myExternalFhirClient = theExternalFhirClient;
		myDaoRegistry = theDaoRegistry;
	}

	@PostConstruct
	public void init() {
		// we don't need to retrieve the DAO from the registry at each usage, so do it once in a PostConstruct method
		ourLog.info("Initialize Patient MDM Interceptor");
		myPatientDao = myDaoRegistry.getResourceDao(Patient.class);
		myTaskDao = myDaoRegistry.getResourceDao(Task.class);
	}

	/**
	 * Interception method, called before MDM module searches for candidate resources and tries to create links.
	 * @param theResource Operated resource, which MDM will analyze after interception
	 */
	@Hook(value = Pointcut.MDM_BEFORE_PERSISTED_RESOURCE_CHECKED)
	public void mdmBeforePersistedResourceChecked(IBaseResource theResource) {

		if (!ResourceType.Patient.toString().equals(theResource.fhirType())) {
			// safeguard, quickly exit if we intercepted MDM call on something other than Patient resource
			// which shouldn't happen if MDM is properly configured
			return;
		}

		ourLog.info("Interceptor MDM_BEFORE_PERSISTED_RESOURCE_CHECKED - started");
		StopWatch stopWatch = new StopWatch();
		try {
			Patient patient = (Patient) theResource;
			Optional<Identifier> ssnIdentifier = patient.getIdentifier().stream()
				.filter(identifier -> IDENTIFIER_US_SSN_SYSTEM.equals(identifier.getSystem()))
				.findFirst();
			if (ssnIdentifier.isEmpty()) {
				Task task = buildTaskForPatientWithoutSsn(patient);

				ourLog.info("Saving\n{}\n",
					myInternalFhirClient.getFhirContext().newJsonParser().setPrettyPrint(true)
						.encodeResourceToString(task));

				DaoMethodOutcome daoMethodOutcome = myTaskDao.create(task);
				// we could have used the internal FhirClient here instead of calling the Dao
				// if we had wanted more standard/robust code
				// myInternalFhirClient
				//	.create()
				//	.resource(task)
				//	.execute();
				if (daoMethodOutcome.getCreated()) {
					ourLog.info("Task successfully created\n{}\n",
						myInternalFhirClient.getFhirContext().newJsonParser().setPrettyPrint(true)
							.encodeResourceToString(daoMethodOutcome.getResource()));
				}
			}
		}
		finally {
			ourLog.info("Interceptor MDM_BEFORE_PERSISTED_RESOURCE_CHECKED - ended, execution took {}", stopWatch);
		}
	}

	/**
	 * Interception method, called after MDM module created links for the operated resource
	 * according to the MDM rule definition provided in MDM module.
	 * @param theResourceOperationMessage The information about the operated resource
	 * @param theTransactionLogMessages The Transaction log messages about the operated resource
	 * @param theMdmLinkEvent The MDM links information, after MDM module analyzed the operated resource
	 */
	@Hook(value = Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED)
	public void mdmAfterPersistedResourceChecked(
		ResourceOperationMessage theResourceOperationMessage,
		TransactionLogMessages theTransactionLogMessages,
		MdmLinkEvent theMdmLinkEvent) {

		IBaseResource resource = theResourceOperationMessage.getPayload(myInternalFhirClient.getFhirContext());
		if (!ResourceType.Patient.toString().equals(resource.fhirType())) {
			// safeguard, quickly exit if we intercepted MDM call on something other than Patient resource
			// which shouldn't happen if MDM is properly configured
			return;
		}

		ourLog.info("Interceptor MDM_AFTER_PERSISTED_RESOURCE_CHECKED - started");
		StopWatch stopWatch = new StopWatch();
		try {
			// verify the operation done on the operated resource,
			// as in this demo, we want to check MDM links for newly created Patient resource only
			if (BaseResourceMessage.OperationTypeEnum.CREATE.equals(theResourceOperationMessage.getOperationType())) {

				for (MdmLinkJson mdmLinkJson : theMdmLinkEvent.getMdmLinks()) {
					String goldenResourceId = mdmLinkJson.getGoldenResourceId();

					if (MdmMatchResultEnum.MATCH.equals(mdmLinkJson.getMatchResult())) {
						// it means the newly created Patient resource is linked to a golden resource
						// as a matching resource, according to the MDM rule definition provided in MDM module
						Patient goldenResource = getGoldenResource(goldenResourceId);
						if (goldenResource != null) {
							// build a unique Person resource based on golden resource information
							Person person = buildPersonFromGoldenResource(goldenResource);

							ourLog.info("Sending to external FHIR repository\n{}\n",
								myExternalFhirClient.getFhirContext().newJsonParser().setPrettyPrint(true)
									.encodeResourceToString(person));

							// and send that Person resource to our external FHIR repository
							// using PUT {external-base-url}/Person/{person-fhir-id}
							// so this will either update or create the Person resource if not present
							myExternalFhirClient
								.update()
								.resource(person)
								.withId(person.getIdElement().getIdPart())
								.execute();
						}
					}
					else if (MdmMatchResultEnum.POSSIBLE_MATCH.equals(mdmLinkJson.getMatchResult())) {
						// in this case, we could create a task for someone to settle the possible match
						// using $mdm-update-link operation!
						ourLog.info("MDM Possible match link for golden resource {}", goldenResourceId);
					}
				}
			}
		}
		finally {
			ourLog.info("Interceptor MDM_AFTER_PERSISTED_RESOURCE_CHECKED - ended, execution took {}", stopWatch);
		}
	}

	private Task buildTaskForPatientWithoutSsn(Patient thePatient) {

		Reference patientReference = new Reference(thePatient);

		return new Task()
			.setStatus(Task.TaskStatus.DRAFT)
			.setIntent(Task.TaskIntent.PLAN)
			.setCode(new CodeableConcept().setText("Missing required information"))
			.setFocus(patientReference)
			.setFor(patientReference)
			.setDescription("Patient without Social Security Number; patient should be contacted to get that important information");
	}

	private Patient getGoldenResource(String theGoldenResourceId) {

		Patient goldenResource = null;
		try {
			// golden resources are like any other Patient resources
			// but with meta.tag entries and an additional identifier
			// (having 'http://hapifhir.io/fhir/NamingSystem/mdm-golden-resource-enterprise-id' system)
			// MDM module keeps the golden record updated according to MDM Survivorship Script
			// (usually, with the latest Patient information, but it is configurable)
			goldenResource = myPatientDao.read(new IdType(theGoldenResourceId));

			// we could have used the internal FhirClient here instead of calling the Dao
			// if we had wanted more standard/robust code
			// goldenResource = myInternalFhirClient
			//	.read()
			//	.resource(Patient.class)
			//	.withId(goldenResourceId)
			//	.execute();

		} catch (BaseServerResponseException theBaseServerResponseException) {
			ourLog.error("Unable to get golden resource " + theGoldenResourceId,
				theBaseServerResponseException);
		}
		return goldenResource;
	}

	private Person buildPersonFromGoldenResource(
		Patient theGoldenResource) {

		// build Person resource from Patient golden record
		// here, for demonstration purposes, we don't want to forward any internal FHIR id
		// to the external FHIR repository
		// so create the same Person FHIR id for the same Patient golden record FHIR id
		// to make sure we don't create multiple Person resources for the same Patient golden record
		// as it is a purposed requirement on the external FHIR repository
		UUID personFhirIdUUID = UUID.nameUUIDFromBytes(
			theGoldenResource.getIdElement().getIdPart().getBytes(StandardCharsets.UTF_8)
		);

		Person person = new Person();
		person.setId(personFhirIdUUID.toString());
		// filter out the additional internal identifier
		// (having 'http://hapifhir.io/fhir/NamingSystem/mdm-golden-resource-enterprise-id' system)
		// from golden record
		person.setIdentifier(theGoldenResource.getIdentifier().stream()
			.filter(identifier -> MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM.equals(identifier.getSystem()))
			.collect(Collectors.toList()));
		if (theGoldenResource.hasActive()) {
			person.setActive(theGoldenResource.getActive());
		}
		person.setBirthDate(theGoldenResource.getBirthDate());
		person.setGender(theGoldenResource.getGender());
		person.setName(theGoldenResource.getName());
		person.setAddress(theGoldenResource.getAddress());
		person.addLink().setTarget(
			new Reference()
				.setType(ResourceType.Patient.toString())
				// dao_config.allow_external_references.enabled must be enabled
				// 'Allow External References Enabled' in 'Persistence' module
				.setReference(myExternalFhirClient.getServerBase()
					+ (!myExternalFhirClient.getServerBase().endsWith("/") ? "/" : "")
					+ theGoldenResource.fhirType() + "/" + theGoldenResource.getIdPart())
				.setDisplay("Golden resource reference in source FHIR repository")
			)
			// According to the MDM rule definition, this is the same person
			.setAssurance(Person.IdentityAssuranceLevel.LEVEL3);
		return person;
	}
}

MDM module configuration Interceptor Bean Types should be set to com.smilecdr.demo.server.r4.TestServerAppCtx, to test PatientMDMInterceptor.

/*-
 * #%L
 * Smile CDR - CDR
 * %%
 * Copyright (C) 2016 - 2024 Smile CDR, Inc.
 * %%
 * All rights reserved.
 * #L%
 */
package com.smilecdr.demo.server.r4;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * This sample interceptor configuration is intended to be used with MDM module.
 * MDM module configuration 'Interceptor Bean Types' should be set to 'com.smilecdr.demo.server.r4.TestServerAppCtx'
 * to test PatientMDMInterceptor in Smile CDR.
 * <p>
 * Sample MDM module configuration
 * </p><p>
 * A sample 'MDM Rule Definition Script' is provided in resources/patient.mdm.interceptor/sample.mdm.rule.definition.json
 * and 'MDM Survivorship Script' is provided in resources/patient.mdm.interceptor/sample.mdm.survivorship.script.js
 * that could be used to test PatientMDMInterceptor.
 * See <a href="https://smilecdr.com/docs/mdm/quickstart.html">MDM Quickstart</a>
 * and <a href="https://smilecdr.com/docs/mdm/mdm_survivorship.html">MDM Survivorship rules</a>
 * for more information about MDM configuration.
 * </p>
 */
@Configuration
public class TestServerAppCtx {

	// base url to our local/internal FHIR repository
	private static final String INTERNAL_FHIR_BASE_URL = "http://localhost:8000/";

	// base url to an external FHIR repository
	// it could be another Smile CDR FHIR Endpoint hosted in the cloud, for example,
	// or a second FHIR endpoint configured in the same Smile CDR instance.
	// For the purposes of this demo, and to make it simpler, we will use the same endpoint as our internal one
	// and name it external in the code.
	// The important part is that we will create two distinct FHIR clients.
	private static final String EXTERNAL_FHIR_BASE_URL = "http://localhost:8000/";

	/**
	 * Important note about DaoRegistry
	 * <p>
	 * It is not the best practice to use Dao to access data, as they are highly tied to Hapi-FHIR / Smile CDR version.
	 * The interfaces and/or behaviour can change quite a lot between versions,
	 * so there should be no expectation on compatibility between versions used.
	 * It is much safer to use a FHIR client for long term use to retrieve/send data from/to a FHIR repository,
	 * (IGenericClient created from FhirContext.newRestfulGenericClient())
	 * as the interfaces and/or behaviour are more guaranteed by the FHIR specification.
	 * But in some specific cases, a Dao can be used. This example demonstrates how to use the Dao.
	 * Also, using Dao could allow users to access data that might not be permitted normally,
	 * depending on the user permissions. So it needs to be planned carefully.
	 * </p>
	 * Implementation note
	 * <p>
	 * Smile CDR will provide an instance of DaoRegistry via dependency injection,
	 * so it is not required to manually configure it / add it in Spring Context
	 * with @Bean annotation.
	 * </p>
	 */
	@Autowired
	private DaoRegistry myDaoRegistry;

	@Primary
	@Bean(name = "interceptorFhirContext")
	public FhirContext getFhirContext() {
		return FhirContext.forR4();
	}

	@Bean(name = "internalFhirClient")
	public IGenericClient getInternalFhirClient(
		@Qualifier("interceptorFhirContext") FhirContext theFhirContext) {
		return theFhirContext.newRestfulGenericClient(INTERNAL_FHIR_BASE_URL);
	}

	@Bean(name = "externalFhirClient")
	public IGenericClient getExternalFhirClient(
		@Qualifier("interceptorFhirContext") FhirContext theFhirContext) {
		return theFhirContext.newRestfulGenericClient(EXTERNAL_FHIR_BASE_URL);
	}

	@Bean(name = "patientMDMInterceptor")
	public PatientMDMInterceptor getPatientMDMInterceptor(
		@Qualifier("internalFhirClient") IGenericClient theInternalFhirClient,
		@Qualifier("externalFhirClient") IGenericClient theExternalFhirClient) {

		return new PatientMDMInterceptor(theInternalFhirClient, theExternalFhirClient, myDaoRegistry);
	}
}

Here is a sample MDM Rule Definition Script that could be used to test PatientMDMInterceptor.

{
  "version" : "1",
  "mdmTypes" : [ "Patient" ],
  "candidateSearchParams" : [ {
    "resourceType" : "Patient",
    "searchParams" : [ "identifier" ]
  }, {
    "resourceType" : "Patient",
    "searchParams" : [ "birthdate" ]
  } ],
  "candidateFilterSearchParams" : [ ],
  "matchFields" : [ {
    "name" : "patient-birthdate-match",
    "resourceType" : "Patient",
    "resourcePath" : "birthDate",
    "matcher" : {
      "algorithm" : "STRING",
      "exact" : "true"
    }
  }, {
    "name" : "patient-last-name-match",
    "resourceType" : "Patient",
    "resourcePath" : "name.family",
    "matcher" : {
      "algorithm" : "STRING",
      "exact" : "false"
    }
  }, {
    "name" : "patient-first-name-match",
    "resourceType" : "Patient",
    "resourcePath" : "name.given",
    "matcher" : {
      "algorithm" : "STRING",
      "exact" : "false"
    }
  }, {
    "name" : "patient-id-ssn-match",
    "resourceType" : "Patient",
    "resourcePath" : "identifier",
    "matcher" : {
      "algorithm" : "IDENTIFIER",
      "identifierSystem" : "http://hl7.org/fhir/sid/us-ssn"
    }
  } ],
  "matchResultMap" : {
    "patient-birthdate-match,patient-last-name-match,patient-first-name-match" : "MATCH",
    "patient-id-ssn-match" : "MATCH"
  }
}

Here is a sample MDM Survivorship Script that could be used to test PatientMDMInterceptor.

// Global handler
// This function will be called by MDM module to determine which attributes are replaced
// and which are preserved in the golden record.
// It modifies the current golden record based on the target record found.
function mdmApplySurvivorshipRules(targetRec, goldenRec, txContext) {
    // Replaces all values in the golden resource with the values in the target resource.
    // But IDs, identifiers and metadata are not replaced.
    var helper = new MdmHelper(Fhir.getContext(), targetRec, goldenRec, txContext);
    helper.replaceAll();
    // Now copy new identifiers and update current ones per system in the golden record.
    for (let i = 0; i < targetRec.getIdentifier().size(); i++) {
        let targetId = targetRec.getIdentifier().get(i);
        let goldenId = getIdentifier(goldenRec, targetId.getSystem());
        if (goldenId === null) {
            goldenRec.addIdentifier(targetId.copy());
        } else {
            goldenId.setValue(targetId.getValue());
        }
    }
}

function getIdentifier(resource, system) {
    // get first identifier using the system from resource identifiers
    for (let i = 0; i < resource.getIdentifier().size(); i++) {
        var id = resource.getIdentifier().get(i);
        if (id.getSystem().equals(system)) {
            return id;
        }
    }
    return null;
}

// This function will be called by MDM module to merge two golden records, when needed.
function mdmApplySurvivorshipRulesOnMergeGoldenResources(targetRec, goldenRec, transactionContext) {
   // merge all fields by default
   var helper = new MdmHelper(Fhir.getContext(), targetRec, goldenRec, transactionContext);
   helper.mergeAll();
}

12.8.3Example: Starter MDM interceptor for all MDM_xxx pointcuts

 

The following example shows an interceptor that can be used as a starter MDM interceptor, implementing a hook method for each available pointcut.

/*-
 * #%L
 * Smile CDR - CDR
 * %%
 * Copyright (C) 2016 - 2024 Smile CDR, Inc.
 * %%
 * All rights reserved.
 * #L%
 */
package com.smilecdr.demo.mdm;

import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkEvent;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage;
import ca.uhn.fhir.util.StopWatch;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Sample MDM Interceptor implementing all MDM_XXX pointcuts.
 * It is intended to be used in MDM module 'Interceptor Bean Types'.
 * Can be used as a starting point for your mdm interceptor.
 */
@SuppressWarnings({"unused", "EmptyTryBlock"})
@Interceptor
public class MDMInterceptorTemplate {

	private static final Logger ourLog = LoggerFactory.getLogger(MDMInterceptorTemplate.class);

	@Hook(Pointcut.MDM_BEFORE_PERSISTED_RESOURCE_CHECKED)
	public void mdmBeforePersistedResourceChecked(
		IBaseResource theResource) {

		ourLog.info("Interceptor MDM_BEFORE_PERSISTED_RESOURCE_CHECKED - started");
		StopWatch stopWatch = new StopWatch();
		try {
			// your implementation goes here
		} finally {
			ourLog.info("Interceptor MDM_BEFORE_PERSISTED_RESOURCE_CHECKED - ended, execution took {}", stopWatch);
		}
	}

	@Hook(value = Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED)
	public void mdmAfterPersistedResourceChecked(
		ResourceOperationMessage theResourceOperationMessage,
		TransactionLogMessages theTransactionLogMessages,
		MdmLinkEvent theMdmLinkEvent) {

		ourLog.info("Interceptor MDM_AFTER_PERSISTED_RESOURCE_CHECKED - started");
		StopWatch stopWatch = new StopWatch();
		try {
			// your implementation goes here
		} finally {
			ourLog.info("Interceptor MDM_AFTER_PERSISTED_RESOURCE_CHECKED - ended, execution took {}", stopWatch);
		}
	}
}