12.6.1FHIR Gateway Examples

 

This page contains example interceptors that can be registered with the FHIR Gateway module.

12.6.2Pointcuts

 

Here is the state of currently available pointcuts on the FHIR Gateway module.

FHIR Gateway Pointcuts

12.6.3Example: Direct Search based on Parameter Value

 

The following example shows an interceptor that looks for a search parameter for gateway searches and directs them to the appropriate target based on the parameter value. This could be used in cases where the same type of resource is served by multiple targets but additional logic is needed in order to determine which target to actually query.

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

import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.fhirgw.json.GatewayTargetJson;
import ca.cdr.api.fhirgw.model.SearchRequest;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;

/**
 * This interceptor expects Observation searches to contain a mandatory
 * parameter called <code>Category</code>. This parameter may have the
 * values <code>lab</code> or <code>vitals</code> and will direct the
 * search to one target or the other.
 */
@Interceptor
public class ChooseGatewaySearchTargetBasedOnParameterInterceptor {

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_PREINVOKE)
	public void hook(SearchRequest theRequest, GatewayTargetJson theTarget, ServletRequestDetails theRequestDetails) {

		// This interceptor only applies to Observation searches
		if (!theRequest.getResourceType().equals("Observation")) {
			return;
		}

		// Look for a parameter on the search request called: category
		String category = theRequest.getParameter("category");

		// Remove the parameter from the request so that we don't pass
		// it to the target servers
		theRequest.removeParameters("category");

		// Depending on the value, only query specific targets
		if ("lab".equals(category)) {
			if (!theTarget.getId().equals("lab_target")) {
				theRequest.setSkip(true);
			}
			return;
		}
		if ("vitals".equals(category)) {
			if (!theTarget.getId().equals("vital_target")) {
				theRequest.setSkip(true);
			}
			return;
		}

		// If the category parameter value was missing or invalid, default to
		// an HTTP 400 Invalid Request response from the gateway
		throw new InvalidRequestException("Missing or invalid required parameter: category");
	}

}

Using this interceptor, the following search URL would be directed only to the Gateway target server with the ID lab_target.

http://fhir.example.com/Observation?category=lab

12.6.4Example: Modify Target Search based on Previous Target Search

 

This example shows how to modify a search being performed on multiple targets so that the actual query used for the later targets is changed based on the results from the search on the earlier targets.

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

import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.fhirgw.json.GatewayTargetJson;
import ca.cdr.api.fhirgw.model.ISearchResultsAccumulator;
import ca.cdr.api.fhirgw.model.SearchRequest;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient;

import java.util.List;
import java.util.Optional;

/**
 * This interceptor is intended to be used in a FHIR Gateway that is configured with multiple
 * search targets. It uses the results of the first target's search results to inform what the
 * query should look like for the second target.
 *
 * While the exact pattern being used here is obviously a bit contrived, it is a simple
 * demonstration of a pattern that can be useful when you have complicated gateway setups.
 *
 * This interceptor would only be effective if the Gateway Route is configured in non-parallel
 * mode, since the results from the first target will not always be available when the second
 * target is being invoked if the searches are performed in parallel.
 */
@Interceptor
public class GatewayEnrichSearchUsingPreviousTargetResults {

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_PREINVOKE)
	public void hook(SearchRequest theRequest, GatewayTargetJson theTarget, ISearchResultsAccumulator theISearchResultsAccumulator, ServletRequestDetails theRequestDetails) {

		// We assume there are two targets, one called "target1" and one called "target2". This
		// logic only applies to the second target.
		if (!theTarget.getId().equals("target2")) {
			return;
		}

		/*
		 * For this example, we look for the first patient in the search results, and
		 * then modify the query for the second target based on those results. This
		 * is just an example of the type of logic you might use.
		 */
		List<IBaseResource> target1Results = theISearchResultsAccumulator.getResults("target1");
		Optional<Patient> patient = target1Results
			.stream()
			.filter(t -> t instanceof Patient)
			.map(t -> (Patient) t)
			.findFirst();

		if (!patient.isPresent()) {
			return;
		}

		// Add a search parameter to the second target search request
		String searchForId = patient.get().getId().replaceAll("^1", "2");
		theRequest.addParameter("_id", searchForId);
	}

}

12.6.5Example: Modify Target Search Results

 

This example shows an interceptor that modifies search results returned by a FHIR Gateway target. Any changes made by this interceptor will be reflected in the eventual client Gateway response.

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

import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.fhirgw.json.GatewayTargetJson;
import ca.cdr.api.fhirgw.model.ISearchResultsAccumulator;
import ca.cdr.api.fhirgw.model.SearchResponse;
import ca.uhn.fhir.interceptor.api.Interceptor;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType;

/**
 * This interceptor is intended to be used in a FHIR Gateway that is configured with multiple
 * search targets. It modifies the results of searches from a specific target, changing the
 * way that they will be presented to Gateway clients.
 *
 * In this example, we are simply making a basic change to the ID of the
 * resources being returned. You could make other changes however, such
 * as adding or removing elements, or even removing result resources
 * entirely.
 *
 * If you need to see reults from previous searches in order to inform changes being
 * made here, you can retrieve these from {@literal theSearchResultsAccumulator}.
 */
@Interceptor
public class GatewayEnrichGatewayTargetResponses {

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_POSTINVOKE)
	public void hook(GatewayTargetJson theTarget, ISearchResultsAccumulator theISearchResultsAccumulator, SearchResponse theResponse) {

		// We assume there are two targets, one called "target1" and one called "target2". This
		// logic only applies to the second target.
		if (!theTarget.getId().equals("target1")) {
			return;
		}

		// Loop through the results of the search and modify them
		for (IBaseResource next : theResponse.getSearchResults()) {
			IIdType existingId = next.getIdElement();
			IdType newId = new IdType("1000" + existingId.getIdPart());
			next.setId(newId);
		}
	}

}

12.6.6Example: Modify Target Search Uri

 

This example shows an interceptor that modifies search uri before calling a FHIR Gateway target, using http POST method. Useful when a targeted FHIR server doesn't strictly follow endpoint definitions, for some reason.

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

import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.fhirgw.json.GatewayTargetJson;
import ca.cdr.api.fhirgw.model.ISearchResultsAccumulator;
import ca.cdr.api.fhirgw.model.SearchRequest;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;

import java.io.IOException;

/**
 * This interceptor is intended to be used in a FHIR Gateway that is configured with multiple
 * search targets.
 *
 * It modifies the uri used for searches for a specific target.
 * For example, the first target doesn't support usual FHIR endpoint '/_search' when using http POST method,
 * but instead offer the same functionality under '/myCustomSearch', in its FHIR API, for some reason.
 * Note: yep, some FHIR servers use creative endpoints!
 *
 * So in this example, we are simply removing '/_search' from the uri of the target when searching using POST method,
 * and replace it with the expected server endpoint '/myCustomSearch'.
 */

@Interceptor
public class GatewayUpdateTargetUriUsingClientInterceptor {

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_PREINVOKE)
	public void hook(SearchRequest theRequest, GatewayTargetJson theTarget, ISearchResultsAccumulator theISearchResultsAccumulator, ServletRequestDetails theRequestDetails) {

		// We assume there are multiple targets, and the first one is called "target1".
		// This logic only applies to the first target, that also uses http POST method for searching,
		// as configured in Gateway target json configuration
		if (theTarget.getId().equals("target1")
			&& theTarget.getUseHttpPostForAllSearches()) {

			// in that case, we want to modify the uri used to call the target server
			// using a client interceptor
			theRequest.setClientInterceptor(new IClientInterceptor() {

				@Override
				public void interceptRequest(IHttpRequest theHttpRequest) {

					if (theHttpRequest.getUri().endsWith("/_search")) {
						// assuming there is no other '/_search' string in the uri,
						// this will make the Gateway FHIR client do this call:
						// POST [baseUrl]/myCustomSearch
						String correctedUri = theHttpRequest.getUri().replace("/_search", "/myCustomSearch");
						theHttpRequest.setUri(correctedUri);
					}
				}

				@Override
				public void interceptResponse(IHttpResponse theIHttpResponse) throws IOException {
					// nothing to do here!
				}
			});
 		}

	}

}

12.6.7Example: Search Using Alternate Patient ID

 

In this example, we are using an interceptor to entirely replace the patient reference ID with a different ID in queries made to one of the targets, and then replacing the references in the response from the target. This allows the client to make requests using a different patient ID than the one an individual target knows the patient as.

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

import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.fhirgw.json.GatewayTargetJson;
import ca.cdr.api.fhirgw.model.ISearchResultsAccumulator;
import ca.cdr.api.fhirgw.model.SearchRequest;
import ca.cdr.api.fhirgw.model.SearchResponse;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * This interceptor demonstrates a gateway search where the target uses different
 * resource IDs than the one(s) the client understands.
 */
@Interceptor
public class GatewaySearchUsingAlternateTargetIds {

	private final Map<String, String> myClientIdToTargetId;

	/**
	 * Constructor
	 */
	public GatewaySearchUsingAlternateTargetIds() {

		// For this example, we'll just use a hardcoded map to translate the
		// IDs we receive from the client into the IDs we'll use to communicate
		// with the target. Obviously in a real scenario this would probably
		// be a lookup to something external.
		myClientIdToTargetId = new HashMap<>();
		myClientIdToTargetId.put("100", "AAA");
		myClientIdToTargetId.put("200", "BBB");

	}

	/**
	 * This hook is called before the search is executed against the target server
	 */
	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_PREINVOKE)
	public void preInvoke(SearchRequest theRequest, GatewayTargetJson theTarget, ISearchResultsAccumulator theISearchResultsAccumulator, ServletRequestDetails theRequestDetails) {

		// We assume there are two targets, one called "target1" and one called "target2". This
		// logic only applies to the second target.
		if (!theTarget.getId().equals("target2")) {
			return;
		}

		// This logic only applies to the Observation resource type
		if (!theRequest.getResourceType().equals("Observation")) {
			return;
		}

		// Look for the subject ID in either the "patient" or "subject"
		// URL parameters.
		String patientId = theRequest.getParameter("patient");
		if (isBlank(patientId)) {
			patientId = theRequest.getParameter("subject");
		}
		if (isBlank(patientId)) {
			throw new InvalidRequestException("No patient ID supplied in request");
		}

		// The ID supplied by the client can be in forms like "Patient/100" or just "100"
		// so we'll use a HAPI FHIR IdType structure to extract just the ID Part (100).
		patientId = new IdType(patientId).getIdPart();

		// Lookup the mapping for the patient ID in our map
		String newPatientId = myClientIdToTargetId.get(patientId);
		if (newPatientId == null) {
			throw new InvalidRequestException("No mapping found for patient " + patientId);
		}

		// Replace the existing search parameter with the mapped version
		theRequest.removeParameters("patient");
		theRequest.removeParameters("subject");
		theRequest.addParameter("patient", "Patient/" + newPatientId);

		// Store the original patient ID an an attribute so we can look it up later. Note that
		// we use the ID of the target in the attribute name just to avoid any conflicts if
		// multiple interceptors like this one are registered
		theRequestDetails.setAttribute("target2-observation-patientId", patientId);

	}

	/**
	 * This hook is called after the target server has responded, and can be used to manipulate
	 * the results
	 */
	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_POSTINVOKE)
	public void postInvoke(GatewayTargetJson theTarget, SearchResponse theSearchResponse, ServletRequestDetails theRequestDetails) {

		// We assume there are two targets, one called "target1" and one called "target2". This
		// logic only applies to the second target.
		if (!theTarget.getId().equals("target2")) {
			return;
		}

		String patientId = (String) theRequestDetails.getAttribute("target2-observation-patientId");
		if (isBlank(patientId)) {
			return;
		}

		// Replace the patient ID in the results we received from "target2" with the
		// original ID the user had searched for. This is important because the
		// client won't be expecting to see references using the internal ID, and also
		// because the authorization service will only allow the client to access
		// patients in compartments it has access to.
		List<IBaseResource> results = theSearchResponse.getSearchResults();
		for (IBaseResource nextResult : results) {
			if (nextResult instanceof Observation) {
				Observation nextObservation = (Observation) nextResult;
				nextObservation.getSubject().setReference("Patient/" + patientId);
			}
		}

	}


}

12.6.8Example: Starter Gateway interceptor for all FHIRGW_xxx pointcuts

 

The following example shows an interceptor that can be used as a starter Gateway 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.fhirgateway;

import ca.cdr.api.broker.IPublicBrokerSender;
import ca.cdr.api.fhir.interceptor.CdrHook;
import ca.cdr.api.fhir.interceptor.CdrPointcut;
import ca.cdr.api.fhirgw.json.AvailableRoutesJson;
import ca.cdr.api.fhirgw.json.GatewayTargetJson;
import ca.cdr.api.fhirgw.json.MatchedRoutesJson;
import ca.cdr.api.fhirgw.model.ISearchResultsAccumulator;
import ca.cdr.api.fhirgw.model.OperationRequest;
import ca.cdr.api.fhirgw.model.OperationResponse;
import ca.cdr.api.fhirgw.model.ReadRequest;
import ca.cdr.api.fhirgw.model.SearchPageRequest;
import ca.cdr.api.fhirgw.model.SearchRequest;
import ca.cdr.api.fhirgw.model.SearchResponse;
import ca.cdr.api.fhirgw.model.UpdateRequest;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Sample Gateway Interceptor implementing all Smile CDR FHIRGW_XXX pointcuts.
 * It is intended to be used in FHIR Gateway REST Endpoint module 'Interceptor Bean Types'.
 * Can be used as a starting point for your Gateway interceptor.
 */
@SuppressWarnings({"unused", "EmptyTryBlock"})
@Interceptor
public class GatewayInterceptorTemplate {


	@Autowired
	private IPublicBrokerSender myBrokerSender;

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

	@CdrHook(CdrPointcut.FHIRGW_READ_TARGET_PREINVOKE)
	public void fhirgwReadTargetPreInvoke(
		ReadRequest theReadRequest,
		GatewayTargetJson theGatewayTargetJson,
		ServletRequestDetails theServletRequestDetails) {

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

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_PREINVOKE)
	public void fhirgwSearchTargetPreInvoke(
		SearchRequest theSearchRequest,
		GatewayTargetJson theGatewayTargetJson,
		ISearchResultsAccumulator theISearchResultsAccumulator,
		ServletRequestDetails theServletRequestDetails) {

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

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_PAGE_TARGET_PREINVOKE)
	public void fhirgwSearchPageTargetPreInvoke(
		SearchPageRequest theSearchPageRequest,
		GatewayTargetJson theGatewayTargetJson,
		ISearchResultsAccumulator theISearchResultsAccumulator,
		ServletRequestDetails theServletRequestDetails) {

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

	@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_POSTINVOKE)
	public void fhirgwSearchTargetPostInvoke(
		GatewayTargetJson theGatewayTargetJson,
		ISearchResultsAccumulator theISearchResultsAccumulator,
		SearchResponse theSearchResponse,
		ServletRequestDetails theServletRequestDetails) {

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

	@CdrHook(CdrPointcut.FHIRGW_OPERATION_TARGET_PREINVOKE)
	public void fhirgwOperationTargetPreInvoke(
		OperationRequest theOperationRequest,
		GatewayTargetJson theGatewayTargetJson,
		ServletRequestDetails theServletRequestDetails) {

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

	@CdrHook(CdrPointcut.FHIRGW_OPERATION_TARGET_POSTINVOKE)
	public void fhirgwOperationTargetPostinvoke(
		OperationRequest theOperationRequest,
		ISearchResultsAccumulator theISearchResultsAccumulator,
		OperationResponse theOperationResponse,
		GatewayTargetJson theGatewayTargetJson,
		ServletRequestDetails theServletRequestDetails) {

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

	@CdrHook(CdrPointcut.FHIRGW_UPDATE_POST_SELECT_ROUTE)
	public void fhirgwUpdatePostSelectRoute(
		UpdateRequest theUpdateRequest,
		ServletRequestDetails theRequestDetails,
		MatchedRoutesJson theMatchedRoutes,
		AvailableRoutesJson theAvailableRoutes) {
		ourLog.info("interceptor FHIRGW_UPDATE_POST_SELECT_ROUTE - started");
		StopWatch stopWatch = new StopWatch();
		try {
			// your implementation goes here
		} finally {
			ourLog.info("Interceptor FHIRGW_UPDATE_POST_SELECT_ROUTE - ended, execution took {}", stopWatch);
		}
	}

}