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:
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");
}
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 |
|
|
Examples |
HL7v2 Endpoint |
|
|
Examples |
Hybrid Providers |
|
Currently no beans are available for autowiring in the hybrid provider context | Examples |
FHIR Gateway |
|
|
Examples |
FHIR Storage |
|
|
Examples |
Subscription |
|
|
Examples |
MDM |
|
|
Examples |
Channel Import |
|
|
Examples |
Cluster Manager |
|
|
Examples |
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.
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.
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
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 |
|
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
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:
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
.