12.7.1FHIR Client interceptor Examples

 

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

12.7.2Example: Starter Client interceptor for all CLIENT_xxx pointcuts

 

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

import ca.cdr.api.i18n.ILocalizer;
import ca.cdr.api.transactionlog.ITransactionLogFetchingSvc;
import ca.cdr.api.transactionlog.ITransactionLogStoringSvc;
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.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.client.api.IRestfulClient;
import ca.uhn.fhir.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * This interceptor is intended to be used with a FHIR client (IRestfulClient, or its implementations)
 * and must be registered with it, before being called.
 * <p>
 * Example:
 * 		IGenericClient fhirClient = FhirContext.forR4().newRestfulGenericClient("http://localhost:8000/");
 * 		fhirClient.registerInterceptor(new ClientInterceptorTemplate());
 * Afterward, calls done using 'fhirClient' will be intercepted by 'ClientInterceptorTemplate'
 * and CLIENT_REQUEST and CLIENT_RESPONSE hook methods will be called.
 * </p><p>
 * Client interceptor can be useful to change the behaviour of a FHIR client before it send requests to a FHIR Endpoint,
 * by changing the url/headers/content for example,
 * or by doing some checks (on http status, headers, etc.) on the response received from a FHIR Endpoint.
 * Currently, the response received can be modified in a limited way by the Client interceptor `CLIENT_RESPONSE` hook.
 * More specifically, they can only override the HTTP response by invoking the setting on ClientResponseContext, the
 * fourth parameter of the Pointcut.
 * See ClientResponseInterceptorModificationTemplate for more details.
 * </p><p>
 * In fact, this is how the FHIR client BasicAuthInterceptor, BearerTokenAuthInterceptor, LoggingInterceptor,
 * and others are actually implemented.
 * </p>
 * Can be used as a starting point for your client interceptor.
 */
@SuppressWarnings({"unused", "EmptyTryBlock"})
@Interceptor
public class ClientInterceptorTemplate {

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

	@Hook(Pointcut.CLIENT_REQUEST)
	public void clientRequest(
		IHttpRequest theHttpRequest,
		IRestfulClient theRestfulClient) {

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

	@Hook(Pointcut.CLIENT_RESPONSE)
	public void clientResponse(
		IHttpRequest theHttpRequest,
		IHttpResponse theHttpResponse,
		IRestfulClient theRestfulClient) {

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

The following example shows an interceptor that can be used to modify an HTTP request, in this case, by wrapping an R4 Patient resource with a R4 Bundle.

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

import ca.uhn.fhir.context.FhirContext;
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.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.apache.ModifiedStringApacheHttpResponse;
import ca.uhn.fhir.rest.client.api.ClientResponseContext;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.io.Reader;

/**
 * This interceptor is intended to be used with a FHIR client (IRestfulClient, or its implementations)
 * and must be registered with it, before being called.
 * <p>
 * Example:
 * 		IGenericClient fhirClient = FhirContext.forR4().newRestfulGenericClient("http://localhost:8000/");
 * 		fhirClient.registerInterceptor(new ClientInterceptorTemplate());
 * Afterward, calls done using 'fhirClient' will be intercepted by 'ClientResponseInterceptorModificationTemplate'
 * and CLIENT_REQUEST and CLIENT_RESPONSE hook methods will be called.
 * </p>
 * This particular interceptor illustrates how to take an existing HTTP response containing a FHIR resource and return
 * a modified copy of that response that includes said Resource within a new Bundle.
 */
@Interceptor
public class ClientResponseInterceptorModificationTemplate {

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

	@Hook(Pointcut.CLIENT_RESPONSE)
	public void intercept(ClientResponseContext clientResponseContext) throws IOException {
		final IBaseResource baseResourceVersionAgnostic = parseBaseResource(clientResponseContext);

		if (baseResourceVersionAgnostic == null) {
			ourLog.info("Ignoring since we could not parse the HTTP response for a Resource");
			return;
		}

		if (!(baseResourceVersionAgnostic instanceof Resource)) {
			ourLog.info("Ignoring since we could not parse the HTTP response for a Resource");
			return;
		}

		final Class<? extends IBaseResource> returnType = clientResponseContext.getReturnType();

		// We only want to trigger further processing if the expected return type is a Bundle
		if (returnType != null &&  Bundle.class != returnType) {
			ourLog.info("Ignoring since the expected return type is not a Bundle");
			return;
		}

		// After this point, you must commit to a FHIR version, in this case: R4
		final Resource resourceR4 = (Resource) baseResourceVersionAgnostic;

		// If this is a Bundle do nothing
		if (resourceR4 instanceof Bundle) {
			ourLog.info("Ignoring since this resource is already a Bundle");
			return;
		}

		final Bundle bundleR4 = bundleFromResourceR4(resourceR4);
		final String bundleR4AsJson = jsonFromBundle(clientResponseContext.getFhirContext(), bundleR4);

		// Wrap the old response in such a way as the new bundle JSON will be rendered instead of the old resource
		final ModifiedStringApacheHttpResponse modifiedStringApacheHttpResponse =
			new ModifiedStringApacheHttpResponse(clientResponseContext.getHttpResponse(), bundleR4AsJson, new StopWatch());
		// Mutate the original response so that FHIR Gateway will now process the bundle in place of the patient
		clientResponseContext.setHttpResponse(modifiedStringApacheHttpResponse);
	}

	private IBaseResource parseBaseResource(ClientResponseContext clientResponseContext) throws IOException {
		final IHttpResponse httpResponse = clientResponseContext.getHttpResponse();
		if (Constants.STATUS_HTTP_204_NO_CONTENT == httpResponse.getStatus()) {
			ourLog.info("There is no content in this HTTP response so doing nothing");
			return null;
		}
		final FhirContext fhirContext = clientResponseContext.getFhirContext();
		final String mimeType = httpResponse.getMimeType();

		final String body = getResponseBody(httpResponse);
		if (Constants.CT_TEXT.equals(mimeType)) {
			ourLog.info("There is no FHIR resource in this HTTP response so doing nothing");
			return null;
		}

		final EncodingEnum enc = EncodingEnum.forContentType(mimeType);
		if (enc == null) {
			ourLog.info("Could not find a valid mime type in this HTTP response so doing nothing");
			return null;
		}

		final IParser p = enc.newParser(fhirContext);
		return p.parseResource(body);
	}

	@Nonnull
	private Bundle bundleFromResourceR4(Resource iBaseResource) {
		final Bundle bundle = new Bundle();
		bundle.setType(Bundle.BundleType.SEARCHSET);
		bundle.setTotal(1);
		final Bundle.BundleEntryComponent entry = bundle.addEntry();
		entry.setResource(iBaseResource);
		return bundle;
	}

	private String jsonFromBundle(FhirContext fhirContext, Bundle bundle) {
		return fhirContext.newJsonParser()
			.setPrettyPrint(true)
			.encodeResourceToString(bundle);
	}

	private String getResponseBody(IHttpResponse theHttpResponse) throws IOException {
		// It is CRUCIAL that you call the line below or downstream code in FHIR Gateway will be unable to close the InputStream
		theHttpResponse.bufferEntity();
		try (final Reader reader = theHttpResponse.createReader()) {
			return IOUtils.toString(reader);
		}
	}
}