Custom Operations
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.
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"
} ]
}
} ]
}
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:
IBaseResource
which is the Resource returned by the target endpoint as the result of the operation execution orBaseServerResponseException
if the invocation was not successful. BaseServerResponseException
will only be provided as OperationResponses if the invocation on the target is allowed to fail.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:
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).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.@Bean
, that returns the class name of the provider.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();
}
}
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
.
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: