Smile CDR v2022.08.PRE
On this page:

11.0Interceptors

 

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

11.0.1HAPI 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");
}

11.0.2Deploying 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
FHIR Endpoint
  • SERVER_xxx (HAPI FHIR)
  • STORAGE_xxx (HAPI FHIR)
  • JPA_PERFTRACE_xxx (HAPI FHIR)
  • SERVER_CONFIGURATION_KEYSTORE (Smile CDR)
Examples
Hybrid Providers
  • SERVER_xxx (HAPI FHIR)
  • SERVER_CONFIGURATION_KEYSTORE (Smile CDR)
Examples
FHIR Gateway
  • SERVER_xxx (HAPI FHIR)
  • FHIRGW_xxx (Smile CDR)
  • SERVER_CONFIGURATION_KEYSTORE (Smile CDR)
Examples
FHIR Storage
  • INTERCEPTOR_xxx (HAPI FHIR)
  • STORAGE_xxx (HAPI FHIR)
  • JPA_PERFTRACE_xxx (HAPI FHIR)
Examples
Subscription
  • SUBSCRIPTION_xxx (HAPI FHIR)

Registering 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.

Registering 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 parts 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.