19.1.1REST Custom Operations

 

You can create your own custom operations on either Resource Provider or Plain Provider, by using @Operation annotation on a method.

Custom operations are usually called with POST method and return a FHIR resource, but they can also be made to accept and return non-FHIR data type.

19.1.1.1Spring Context Configuration

To make the custom operations callable on an existing REST endpoint, their classes need to be added in Resource or Plain Providers list beans, by providing the required Spring Context Configuration class name in Resource Provider Bean Types configuration property. This property is available in these REST endpoint modules' configuration:

  • Hybrid Providers REST Endpoint
  • FHIR REST Endpoint
  • FHIR Gateway REST Endpoint

19.1.1.2Sample REST Custom Operations

The sample custom $obfuscateName operation below handles requests done on POST {base_url_endpoint}/Practitioner/$obfuscateName. It requires Content-Type header application/fhir+json and a Parameters resource in the request body. It outputs a Parameters resource in the response body.

public class PractitionerProvider implements IResourceProvider {
    // ...
    
    /**
     * This is a custom operation, that can be called on POST 'Practitioner/$obfuscateName' endpoint,
     * with a Parameters resource body.
     *
     * @param theOldName Input Parameters resource named oldName with string value
     * @return Output Parameters resource, one named oldName with input string value
     *         and one named newName, with obfuscated oldName string value using name based UUID
     */
    @Operation(name = "$obfuscateName")
    public Parameters obfuscateName(@OperationParam(name = "oldName") StringType theOldName) {
        
        if (!theOldName.hasValue()) {
            throw new IllegalArgumentException(
                    "Parameter named 'oldName' must be present and have a non-empty string value");
        }
        
        Parameters retVal = new Parameters();
        retVal.addParameter().setName("oldName").setValue(theOldName);
        
        // Name based UUID (type 3), will return same UUID for the same input bytes
        String obfuscated = UUID.nameUUIDFromBytes(theOldName.getValue().getBytes(StandardCharsets.UTF_8)).toString();
        
        retVal.addParameter().setName("newName").setValue(new StringType(obfuscated));
        return retVal;
    }

    @Override
    public Class<Practitioner> getResourceType() {
        return Practitioner.class;
    }
}

Sample request, with Parameters resource body:

POST {base_url_endpoint}/Practitioner/$obfuscateName
Content-Type: application/fhir+json

{
    "resourceType": "Parameters", 
    "parameter": [
        {
            "name": "oldName", 
            "valueString": "John Smith"
        }
    ]
}

Sample response:

{
    "resourceType": "Parameters", 
    "parameter": [
        {
            "name": "oldName",
            "valueString": "John Smith"
        },
        {
            "name": "newName",
            "valueString": "6117323d-2cab-3c17-944c-2b44587f682c"
        }
    ]
}

19.1.1.3Operation Annotation Options

Custom operations can also:

  • be called with GET method by using idempotent = true option in @Operation annotation,
  • read other types of data than a FHIR Resource from the request body by using manualRequest = true option in @Operation annotation,
  • return other types of data than a FHIR Resource by using manualResponse = true option in the @Operation annotation.

For example, the following sample code handles both requests:

  • POST {base_url_endpoint}/Practitioner/$exportToCSV with no request body
  • GET {base_url_endpoint}/Practitioner/$exportToCSV

since it is using idempotent = true in @Operation. It returns a Binary resource with Base64 encoded data containing the CSV lines, if Content-Type header with application/fhir+json, application/json, application/fhir+xml or text/xml value is sent in the request. Otherwise, if no 'Content-Type' header is sent, the CSV lines are returned as text/plain directly.

public class PractitionerProvider implements IResourceProvider {
    // ...

    /**
     * Another custom operation, which goal is to return CSV lines about Practitioners.
     * That can be called either on these endpoints:
     * POST 'Practitioner/$exportToCSV'
     * 	  without 'Content-Type' header 'application/fhir+json', 'application/json', 'application/fhir+xml' or 'text/xml'
     * or
     * POST 'Practitioner/$exportToCSV'
     * 	  with 'Content-Type' header 'application/fhir+json', 'application/json', 'application/fhir+xml' or 'text/xml'
     * or, since idempotent = true,
     * GET 'Practitioner/$exportToCSV'
     * 	  without 'Content-Type' header 'application/fhir+json', 'application/json', 'application/fhir+xml' or 'text/xml'
     * or
     * GET 'Practitioner/$exportToCSV'
     * 	  with 'Content-Type' header 'application/fhir+json', 'application/json', 'application/fhir+xml' or 'text/xml'
     * <p>
     * Important note: if 'Content-Type' header has 'application/fhir+json', 'application/json', 'application/fhir+xml' 
     * or 'text/xml' value in the request, then the servlet request input stream will always be empty, 
     * since it was already read previously in the stack.
     * The manualRequest option is not intended to be used with FHIR json data in the request body,
     * as a proper FHIR type should be used in the method parameter instead.
     * </p>
     *
     * @param theServletRequest The servlet request, since we set manualRequest = true,
     *                          no attempt to parse the request body was done,
     *                          and was delegated to this method instead, if needed
     * @return If 'Content-Type' header 'application/fhir+json' or 'application/json' is present in the request,
     * 	 the Binary resource json will be returned, with Base64 encoded data containing the CSV lines
     * 	 If 'Content-Type' header 'text/xml' is present in the request,
     * 	 the Binary resource xml will be returned, with Base64 encoded data containing the CSV lines
     * 	 Otherwise, only the CSV lines will be returned in plain text
     */
    @Operation(name = "exportToCSV", idempotent = true, manualRequest = true)
    public Binary exportPractitionersToCSV(
        HttpServletRequest theServletRequest) {
        // ...
        // load your Practitioner resources here
        // ...
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try (CSVWriter csvWriter = new CSVWriter(byteArrayOutputStream)) {
           // ...
           // write the appropriate data in CSV format
            csvWriter.line(/*...*/);
        }
        // ...
        Binary retVal = new Binary();
        retVal.setId(UUID.randomUUID().toString());
        retVal.setContentType("text/plain;charset=utf-8");
        retVal.getDataElement().setValue(byteArrayOutputStream.toByteArray());
        return retVal;
    }

    @Override
    public Class<Practitioner> getResourceType() {
        return Practitioner.class;
    }
}

19.1.1.4Returning Other Data Types than FHIR Resource

In some cases, it can be useful to provide additional endpoints and data type to support some FHIR resources. The DocumentReference resource content.attachment property that is based on Attachment datatype is a common use case. The attachment.url property could target a custom operation returning images or other binary data types for example.

The Manually handing Request/Response section of the HAPI FHIR documentation demonstrates how to return other types of data than a FHIR Resource.

The sample code provided in that section uses @Operation(..., manualResponse=true, ...) to return data with the HttpServletResponse writer, using the specified Content Type. Depending on the data written, the content type could be a valid MimeType like text/plain, text/xml, application/fhir+json, or binary data like application/pdf, image/png, application/octet-stream, or else.

19.1.1.5Additional REST Custom Operation Code Examples

We also provided code examples in the sample Hybrid Providers project, to demonstrate how to implement Custom Operations. Look for PractitionerProvider classes.