18.3.1Custom Operation Providers

 

You can create custom providers for systemwide operations or specific to a Resource type or instance by supplying your own handling classes.

A custom operation handling class is a simple POJO class with one or more method annotated with @Operation and implementing IResourceProvider when the operation applies to a Resource type or a Resource instance.

18.3.2Sample Custom System Operation Provider

 

Class CustomSystemProviderOperation listed below defines systemwide operation $customSystemOperation that is invoked with a POST {base_url_endpoint/$customSystemOperation}. The logic creates a $meta operationRequest to be issued to all targets capable of handling the operation and delegates invocation to injected service IOperationOrchestrator. Target responses are extracted and collected to create the operation response object which is returned to the caller.

package ca.cdr.endpoint.fhirgw.module;

import ca.cdr.api.fhirgw.model.OperationRequest;
import ca.cdr.api.fhirgw.model.OperationResponse;
import ca.cdr.api.fhirgw.svc.IOperationOrchestrator;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 * This class serves as an entry point to provide a custom system operation named 'customSystemOperation'.
 * The custom operation consists on issuing a META operation on all targets defined for the route and
 * returns a Parameters resource with the target responses.
 */
public class CustomSystemProviderOperation {

	public static final String ourCustomSystemOperationName = "customSystemOperation";

	@Autowired
	private IOperationOrchestrator myIOperationOrchestrator;

	@Operation(
		name = ourCustomSystemOperationName,
		idempotent = true) //idempotent = true allows the operation to be called via GET as well as POST
	public Parameters customSystemOperation(ServletRequestDetails theRequestDetails){
		Parameters retVal = new Parameters();

		OperationRequest operationRequest = new OperationRequest(ProviderConstants.OPERATION_META);
		operationRequest.setServletRequest(theRequestDetails.getServletRequest());

		// delegate invocations to member IOperationOrchestrator which provides a list of OperationResponse(s)
		// returned by the invoked target(s).
		List<OperationResponse> operationResponseList = myIOperationOrchestrator.invokeOperationResponseReturning(operationRequest, theRequestDetails);

		// The IBaseResponse is extracted from each OperationResponse and processed as required.  Any issues encountered during
		// target invocation is wrapped in a BaseServerRequestException and made available for custom processing.
		for( OperationResponse operationResponse : operationResponseList){
			Parameters.ParametersParameterComponent parameterComponent = retVal.addParameter();
			parameterComponent.setName(ProviderConstants.OPERATION_META);

			if(operationResponse.hasResponse()){
				parameterComponent.setResource((Parameters) operationResponse.getResponse());
			}

			if(operationResponse.hasException()){
				BaseServerResponseException exception = operationResponse.getException();
				int statusCode = exception.getStatusCode();
				parameterComponent.setValue(new IntegerType(statusCode));
			}

		}

		return retVal;
	}
	
}

Sample response:

{
  "resourceType": "Parameters",
  "parameter": [
  {	  
    "name": "$meta",
    "resource": {
      "resourceType": "Parameters",
      "parameter": [ {
        "name": "META_PARAM",
        "valueString": "META_PARAM_VALUE1"
      } ]
    }
  }, {
    "name": "$meta",
    "resource": {
      "resourceType": "Parameters",
      "parameter": [ {
        "name": "META_PARAM",
        "valueString": "META_PARAM_VALUE2"
      } ]
    }
  } ]
}

18.3.3Processing Target Invocation Responses

 

To process target invocation responses, a custom provider needs to wire in Bean of type IOperationOrchestrator to gain access to API method invokeOperationResponseReturning. As per the documentation, the method returns a list of OperationResponse that wraps target invocation responses. Each OperationResponse will provide either:

  • an IBaseResource which is the Resource returned by the target endpoint as the result of the operation execution or
  • an BaseServerResponseException if the invocation was not successful. BaseServerResponseException will only be provided as OperationResponses if the invocation on the target is allowed to fail.

18.3.4The Spring Context Config Class

 

To make the custom operations callable on an existing REST endpoint, handling classes need to be defined as Spring Beans provided through a mandatory Spring Context Configuration class. The class is a Spring Framework Annotation-based Application Context Config class where the class name is supplied to the module by setting configuration property key definitions.spring_context_config.class. It is characterized by having the @Configuration annotation on the class and declaring one or more non-static factory methods annotated with @Bean that return lists of your providers instances(as well as creating any other utility classes you might need, such as database pools, HTTP clients, etc.).

More specifically, the context configuration class may:

  • Define a method named resourceProviders annotated with @Bean that returns a List<IResourceProvider> which is a list of Beans implementing interface IResourceProvider and supplying logic for custom operation(s) on Resource(s).
  • Define a method named systemProviders annotated with @Bean that returns a List<Object> which is a list of Beans supplying logic for custom system operation(s). Note that for autowiring to work, you should not construct objects with new here, but use a method reference to the provider bean.
  • Define methods for each of your custom providers, annotated with @Bean, that returns the class name of the provider.
  • Any other Beans required for wiring instances of custom operation provider Beans.

The following example shows a Spring Context Config class that creates two resource providers and one system provider.


import org.springframework.context.annotation.Bean;

@Configuration
public class TestServerAppCtx {

	/**
	 * This bean is a list of Resource Provider classes, each one
	 * of which implements FHIR operations for a specific resource
	 * type.
	 */
	@Bean(name = "resourceProviders")
	public List<IResourceProvider> resourceProviders() {
		List<IResourceProvider> retVal = new ArrayList<>();
		retVal.add(new PatientResourceProvider());
		retVal.add(new ObservationResourceProvider());
		return retVal;
	}

	/**
	 * This bean is a list of system Provider classes, each one
	 * of which implements FHIR operations for system-level
	 * FHIR operations.
	 */
	@Bean(name = "systemProviders")
	public List<Object> systemProviders() {
		List<Object> retVal = new ArrayList<>();
		retVal.add(customOperationProvider());
		return retVal;
	}

	/**
	 * This is the bean definition of the provider we wrote above. It needs to exist here so Spring behaviour such as 
	 * {@link org.springframework.beans.factory.annotation.Autowire} can work. 
	 */
	@Bean
	public CustomSystemProviderOperation customOperationProvider() {
		return new CustomSystemProviderOperation();
	}
}

18.3.5Packaging Your Providers

 

The Spring Context Config class and your Provider classes must all be packaged up in a normal Java JAR file. No third party libraries should be packaged with your code.

If you are using Apache Maven as your build system, this just means you should use a normal project with a packaging of jar.

18.3.6Deploying Your Custom Providers

 

Once you have created a JAR with your custom Providers in it, this JAR should be placed in the customerlib/ directory of the Smile CDR installation. The next step is about giving visibility to the custom providers:

  1. Log into the Web Admin Console.
  2. Select or create the FHIR Gateway module that will host the custom providers.
  3. Under FHIR Gateway Configuration category, fill out the Spring Context Config Class property with the fully qualified class name for your Spring Context Config Class.