5.6.1REST Operations: Extended Operations

 

The FHIR specification defines a special kind of operations that have an RPC-like functionality. These are called "Execute Operations", or simply "Operations" throughout the FHIR specification.

A good introduction to this capability can be found on the Operations Page of the FHIR Specification.

FHIR extended operations are a special type of RPC-style invocation you can perform against a FHIR server, type, or resource instance. These invocations are named using the convention $name (i.e. the name is prefixed with $) and will generally take a Parameters resource as input and output. There are some cases where the input and/or output will be a different resource type however.

5.6.1.1Providers

To define an operation, a method should be placed in a Resource Provider class if the operation works against a resource type/instance (e.g. Patient/$everything), or on a Plain Provider class if the operation works against the server (i.e. it is global and not resource specific).

5.6.2Type-Level Operations

 

To implement a type-specific operation, the method should be annotated with the @Operation tag, and should have an @OperationParam tag for each named parameter that the input Parameters resource may be populated with. The following example shows how to implement the Patient/$everything method, defined in the FHIR specification.

@Operation(name = "$everything", idempotent = true)
public Bundle patientTypeOperation(
      @OperationParam(name = "start") DateDt theStart, @OperationParam(name = "end") DateDt theEnd) {

   Bundle retVal = new Bundle();
   // Populate bundle with matching resources
   return retVal;
}

Example URL to invoke this operation: http://fhir.example.com/Patient/$everything

5.6.3Instance-Level Operations

 

To create an instance-specific operation (an operation which takes the ID of a specific resource instance as a part of its request URL), you can add a parameter annotated with the @IdParam annotation, of type IdType. The following example shows how to implement the Patient/[id]/$everything operation.

@Operation(name = "$everything", idempotent = true)
public Bundle patientInstanceOperation(
      @IdParam IdType thePatientId,
      @OperationParam(name = "start") DateDt theStart,
      @OperationParam(name = "end") DateDt theEnd) {

   Bundle retVal = new Bundle();
   // Populate bundle with matching resources
   return retVal;
}

Example URL to invoke this operation: http://fhir.example.com/Patient/123/$everything

5.6.4Server-Level Operations

 

Server-level operations do not operate on a specific resource type or instance, but rather operate globally on the server itself. The following example shows how to implement a server-level operation. Note that the concept parameter in the example has a cardinality of 0..*, so a ``List` is used as the parameter type.

@Operation(name = "$closure")
public ConceptMap closureOperation(
      @OperationParam(name = "name") StringDt theStart,
      @OperationParam(name = "concept") List<Coding> theEnd,
      @OperationParam(name = "version") IdType theVersion) {

   ConceptMap retVal = new ConceptMap();
   // Populate bundle with matching resources
   return retVal;
}

Example URL to invoke this operation (HTTP request body is Parameters resource): http://fhir.example.com/$closure

5.6.5Using Search Parameter Types

 

FHIR allows operation parameters to be of a Search parameter type (e.g. token) instead of a FHIR datatype (e.g. Coding).

To use a search parameter type, any of the search parameter types listed in Rest Operations: Search may be used. For example, the following is a simple operation method declaration using search parameters:

@Operation(name = "$find-matches", idempotent = true)
public Parameters findMatchesBasic(
      @OperationParam(name = "date") DateParam theDate, @OperationParam(name = "code") TokenParam theCode) {

   Parameters retVal = new Parameters();
   // Populate bundle with matching resources
   return retVal;
}

Example URL to invoke this operation (HTTP request body is Parameters resource): http://fhir.example.com/$find-matches?date=2011-01-02&code=http://system%7Cvalue

It is also fine to use collection types for search parameter types if you want to be able to accept multiple values. For example, a List<TokenParam> could be used if you want to allow multiple repetitions of a given token parameter (this is analogous to the "AND" semantics in a search).

A TokenOrListParam could be used if you want to allow multiple values within a single repetition, separated by comma (this is analogous to "OR" semantics in a search).

For example:

@Operation(name = "$find-matches", idempotent = true)
public Parameters findMatchesAdvanced(
      @OperationParam(name = "dateRange") DateRangeParam theDate,
      @OperationParam(name = "name") List<StringParam> theName,
      @OperationParam(name = "code") TokenAndListParam theEnd) {

   Parameters retVal = new Parameters();
   // Populate bundle with matching resources
   return retVal;
}

If the string value to be searched contains a space character, you should encode it with a + sign or with %20, as in the following examples with an Organization named "Acme Corporation": http://fhir.example.com/Organization?name=Acme+Corporation
http://fhir.example.com/Organization?name=Acme%20Corporation

If the string value to be searched contains a literal + character, you should escape it with %2B, as in the following example with an Organization named "H+K": http://fhir.example.com/Organization?name=H%2BK

Certain strings are automatically escaped when the FHIR server parses URLs: "|"   -> "%7C"
"=>=" -> "=%3E%3D"
"=<=" -> "=%3C%3D"
"=>"  -> "=%3E"
"=<"  -> "=%3C"

5.6.6Returning Multiple OUT Parameters

 

In all of the Operation examples above, the return type specified for the operation is a single Resource instance. This is a common pattern in FHIR defined operations. However, it is also possible for an extended operation to be defined with multiple and/or repeating OUT parameters. In this case, you can return a Parameters resource directly.

5.6.7Accepting HTTP GET

 

The FHIR specification allows for operations to be invoked using an HTTP GET instead of an HTTP POST only if the following two conditions are met:

  • All parameters have primitive datatype values

  • The operation is marked as "affectsState = false". Note that early releases of the FHIR specification referred to an operation that did not affect state as "idempotent = true". It was subsequently determined that idempotency was the wrong term for the concept being expressed, but the term does persist in some HAPI FHIR documentation and code.

If you are implementing an operation which should allow HTTP GET, you should mark your operation with idempotent=true in the @Operation. The default value for this flag is false, meaning that operations will not support HTTP GET by default.

Note that the HTTP GET form is only supported if the operation has only primitive parameters (no complex parameters or resource parameters). If a client makes a request containing a complex parameter, the server will respond with an HTTP 405 Method Not Supported.

5.6.8Manually handing Request/Response

 

For some operations you may wish to bypass the HAPI FHIR standard request parsing and/or response generation. In this case you may use the manualRequest = true and/or manualResponse = true attributes on the @Operation annotation.

The following example shows an operation that parses the request and generates a response (by echoing back the request).

@Operation(name = "$manualInputAndOutput", manualResponse = true, manualRequest = true)
public void manualInputAndOutput(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
      throws IOException {
   String contentType = theServletRequest.getContentType();
   byte[] bytes = IOUtils.toByteArray(theServletRequest.getInputStream());

   ourLog.info("Received call with content type {} and {} bytes", contentType, bytes.length);

   theServletResponse.setContentType("text/plain");
   theServletResponse.getWriter().write("hello");
   theServletResponse.getWriter().close();
}