12.0.1Interceptors

 

The HAPI FHIR Server Interceptor Framework is a mechanism for modifying the behavior of the server in a variety of ways.

As described in the documentation, special classes called Interceptors can be used to:

  • Pre-process or mask data that is being returned to the client as a result of a FHIR search, read, etc.
  • Add additional business checks before processing a write operation
  • Modify data that was submitted by a client prior to saving it

12.0.2HAPI FHIR and Smile CDR Pointcuts

 

In the interceptor framework, a Pointcut is a specific spot in the processing pipeline where custom logic can be added via an interceptor. The custom logic (i.e. code) itself is called a Hook. See the HAPI FHIR Interceptor Documentation for more information on the terminology used here.

Most HAPI FHIR interceptors are supported in the appropriate Smile CDR modules for the given Pointcut type. In addition to supporting HAPI FHIR interceptor Pointcuts, a set of Smile CDR specific pointcuts are available as well.

HAPI FHIR interceptors use the @Hook annotation and Pointcut enum. For example:

@Hook(Pointcut.SERVER_OUTGOING_WRITER_CREATED)
public Writer capture(RequestDetails theRequestDetails,Writer theWriter){
	CountingWriter retVal=new CountingWriter(theWriter);
	theRequestDetails.getUserData().put(COUNTING_WRITER_KEY,retVal);
	return retVal;
	}

Smile CDR interceptors use an equivalent pair of classes: The @CdrHook annotation and the CdrPointcut enum. For example:

@CdrHook(CdrPointcut.FHIRGW_SEARCH_TARGET_PREINVOKE)
public void preInvoke(SearchRequest theRequest){
	theRequest.removeParameters("_id");
	theRequest.addParameter("_id","101");
	}

12.0.3Deploying Interceptors to Smile CDR Modules

 

The following table outlines the various module types, and the various types of Pointcuts that may be intercepted by an interceptor registered against that module.

Module Types Allowable Pointcuts Autowireable Dependencies (Source Module)
FHIR Endpoint
  • SERVER_xxx (HAPI FHIR)
  • STORAGE_xxx (HAPI FHIR)
  • JPA_PERFTRACE_xxx (HAPI FHIR)
  • SERVER_CONFIGURATION_KEYSTORE (Smile CDR)
  • Supplier<ICdrEndpointRequestDetailsJson> (FHIR Endpoint)
  • FhirContext (Persistence)
  • DaoRegistry (Persistence)
  • JpaStorageSettings (Persistence)
  • PlatformTransactionManager (Persistence)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
Examples
HL7v2 Endpoint
  • HL7V2IN_PRE_HL7V2_TO_FHIR_MAPPING_PROCESSING (Smile CDR)
  • FhirContext (Persistence)
  • DaoRegistry (Persistence)
  • IFhirResourceDaoConceptMap (Persistence)
  • JpaStorageSettings (Persistence)
  • PlatformTransactionManager (Persistence)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
Examples
Hybrid Providers
  • SERVER_xxx (HAPI FHIR)
  • SERVER_CONFIGURATION_KEYSTORE (Smile CDR)
Currently no beans are available for autowiring in the hybrid provider context Examples
FHIR Gateway
  • SERVER_xxx (HAPI FHIR)
  • FHIRGW_xxx (Smile CDR)
  • SERVER_CONFIGURATION_KEYSTORE (Smile CDR)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
  • IOperationOrchestrator (FHIR Gateway)
Examples
FHIR Storage
  • INTERCEPTOR_xxx (HAPI FHIR)
  • STORAGE_xxx (HAPI FHIR)
  • JPA_PERFTRACE_xxx (HAPI FHIR)
  • STORAGE_xxx (Smile CDR)
  • FhirContext (Persistence)
  • DaoRegistry (Persistence)
  • IFhirResourceDaoConceptMap (Persistence)
  • JpaStorageSettings (Persistence)
  • PlatformTransactionManager (Persistence)
  • RepositoryValidatingRuleBuilder (Persistence)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
Examples
Subscription
  • SUBSCRIPTION_xxx (HAPI FHIR)
  • FhirContext (Persistence)
  • DaoRegistry (Persistence)
  • IFhirResourceDaoConceptMap (Persistence)
  • StorageSettings (Subscription)
  • PlatformTransactionManager (Persistence)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
  • IModuleRegistry (Cluster Manager)
Examples
MDM
  • MDM_xxx (HAPI FHIR)
  • FhirContext (Persistence)
  • DaoRegistry (Persistence)
  • IFhirResourceDaoConceptMap (Persistence)
  • JpaStorageSettings (Persistence)
  • PlatformTransactionManager (Persistence)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
  • IModuleRegistry (Cluster Manager)
Examples
Channel Import
  • CHANNEL_IMPORT_MESSAGE_PRE_PROCESSED (Smile CDR)
  • FhirContext (Persistence)
  • DaoRegistry (Persistence)
  • IFhirResourceDaoConceptMap (Persistence)
  • JpaStorageSettings (Persistence)
  • PlatformTransactionManager (Persistence)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
Examples
Cluster Manager
  • SMART_OIDC_CLIENT_xxx (Smile CDR)
  • AG_APPLICATION_xxx (Smile CDR)
  • ILocalizer (Cluster Manager)
  • ITransactionLogFetchingSvc (Cluster Manager)
  • ITransactionLogStoringSvc (Cluster Manager)
  • IModuleConfigSvc (Cluster Manager)
  • IModuleRegistry (Cluster Manager)
  • IPublicBroker (Cluster Manager)
Examples

12.0.3.1Smile CDR Dependencies in Interceptors

Occasionally, it is useful for a custom interceptor to have access to certain services that Smile CDR publishes. For example, you may want to create a custom transaction log entry inside of an interceptor. By default, Smile CDR allows you to autowire any beans from the parent application context, using the @Autowire annotation. Eventually, this functionality will be removed in favour of what is called a Secure Application Context, which will provide a static subset of beans which are safe to use in an interceptor.

You can enable the secure mode now by toggling the Secure Application Context property in the module where you are loading your interceptors. While running in secure mode, only the beans listed in the table above will be available for autowiring.

12.0.3.2Registering An Interceptor

Interceptor classes should be packaged as a simple JAR file containing only the classes defined by the interceptor (i.e. library dependencies such as Spring or Commons-Lang should not be included in your JAR). The JAR file should be placed in the smilecdr/customerlib folder.

The interceptor class (or classes) that you have defined should then be set on the interceptor_bean_types property within the appropriate module configuration. Smile CDR must be restarted for changes to take effect.

For example, the following property sets a custom interceptor from within the configuration property file:

module.persistence.config.interceptor_bean_types=com.example.fhir.ExampleAttributeEnhancingInterceptor

Interceptor classes have access to any of the standard libraries available within Smile CDR. This includes Spring Framework, Apache HTTPClient, Gson, Jackson, Woodstox, and others.

12.0.3.3Registering Complex interceptors with internal dependencies

Sometimes an interceptor you are writing requires more context, or the ability to interact with other beans. When registering Interceptors via class name, you are limited to the beans provided by Smile to all interceptors as dependencies. However, this does not allow you to write your own beans which you can inject in your interceptors. If you would like to setup a more complex interceptor with such dependencies, this can be achieved by providing the Configuration class name to the interceptor_bean_types property. The following example illustrates this in action.

Consider this interceptor:

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.server.servlet.ServletRequestDetails;
import org.springframework.beans.factory.annotation.Autowired;

@Interceptor
class SpecialInterceptor {
	@Autowired
	private AuditService auditService;

	@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
	public void interceptRequest(ServletRequestDetails srd) {
		auditService.auditServerConnection(srd);
	}
}

This interceptor uses another bean, AuditService, shown below:


import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;

public class AuditService {
	public void auditServerConnection(ServletRequestDetails srd) {
		//Reach out to some auditing DB/log to file/log to kafka/whatever.
	}
}

These two beans can be setup together in a Configuration class, (annotated with @Configuration) as follows:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class MySpecialConfiguration {
	@Bean
	public SpecialInterceptor specialInterceptor() {
		return new SpecialInterceptor();
	}

	@Bean
	public AuditService auditService() {
		return new AuditService();
	}
}

If you choose to list the Configuration file in the interceptor_bean_types field, then Smile CDR will create a child context, and automatically extract any beans defined with the @Interceptor annotation. The important part here is that your configuration file, and your interceptors, are both correctly annotated.

For example, the following property sets up the entire application context defined above, and registers the SpecialInterceptor to the interceptor service.

module.persistence.config.interceptor_bean_types=com.example.fhir.MySpecialConfiguration
Note that if you have multiple classes annotated with `@Interceptor`, they will _all_ be registered.

12.0.4Registering a Client Interceptor

 

The following table outlines the client types, and the various types of Pointcuts that may be intercepted by an interceptor registered against that client.

Types Allowable Pointcuts
FHIR Client
  • CLIENT_xxx (HAPI FHIR)
Examples

Client interceptor classes also need to be packaged in a JAR file, and placed in the smilecdr/customerlib folder.

But, they don't require to be set in any module configuration property, as they must be registered with a FHIR Client, before being called.

IGenericClient fhirClient=FhirContext.forR4().newRestfulGenericClient("http://localhost:8000/");

	fhirClient.registerInterceptor(new SampleClientInterceptor());

Afterward, SampleClientInterceptor hook methods will be called when a request is executed with the FHIR Client.

Client interceptors 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,
  • 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. Specifically, it's possible to invoke ClientResponseContext.setHttpResponse() using an HTTP request modified with the help of ModifiedStringApacheHttpResponse.

One example use case is if FHIR Gateway is linked to a non-standard FHIR endpoint that returns a raw Resource (ex: Patient) instead of wrapping that Resource in a Bundle and your FHIR Gateway needs to override the response to wrap that Resource in a Bundle.

There are two important things to note:

  1. It's crucial to call IHttpResponse.bufferEntity(), otherwise, you will experience this error: "HAPI-1861: Failed to parse JSON encoded FHIR content: Attempted read from closed stream." (see example for a more detailed explanation) 1. DO NOT chain this type CLIENT_RESPONSE hook with another CLIENT_RESPONSE, as this could cause erratic and unexpected results given the fact that this solution calls for modifying the HTTP response.

Also, HAPI FHIR provides numerous built-in Client Interceptors that can be extended, including a detailed example using CLIENT_RESPONSE and ModifiedStringApacheHttpResponse.