Creating a generic client simply requires you to create an instance of FhirContext
and use that to instantiate a client.
The following example shows how to create a client, and a few operations which can be performed.
// We're connecting to a DSTU1 compliant server in this example
FhirContext ctx = FhirContext.forDstu2();
String serverBase = "http://fhirtest.uhn.ca/baseDstu2";
IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Perform a search
Bundle results = client.search()
.forResource(Patient.class)
.where(Patient.FAMILY.matches().value("duck"))
.returnBundle(Bundle.class)
.execute();
System.out.println("Found " + results.getEntry().size() + " patients named 'duck'");
Performance Tip: Note that FhirContext is an expensive object to create, so you should try to keep an instance around for the lifetime of your application. It is thread-safe so it can be passed as needed. Client instances, on the other hand, are very inexpensive to create so you can create a new one for each request if needed (although there is no requirement to do so, clients are reusable and thread-safe as well).
The generic client supports queries using a fluent interface which is originally inspired by the excellent .NET FHIR API.
The fluent interface allows you to construct powerful queries by chaining method calls together, leading to highly readable code. It also allows you to take advantage of intellisense/code completion in your favourite IDE.
Note that most fluent operations end with an execute()
statement which actually performs the invocation. You may also invoke several configuration operations just prior to the execute() statement, such as encodedJson()
or encodedXml()
.
Searching is a very powerful part of the FHIR API specification itself, and HAPI FHIR aims to provide a complete implementation of the FHIR API search specification via the generic client API.
Searching for resources is probably the most common initial scenario for client applications, so we'll start the demonstration there. The FHIR search operation generally uses a URL with a set of predefined search parameters, and returns a Bundle containing zero-or-more resources which matched the given search criteria.
Search is a very powerful mechanism, with advanced features such as paging, including linked resources, etc. See the FHIR search specification for more information.
The following example shows how to query using the generic client:
Bundle response = client.search()
.forResource(Patient.class)
.where(Patient.BIRTHDATE.beforeOrEquals().day("2011-01-01"))
.and(Patient.GENERAL_PRACTITIONER.hasChainedProperty(
Organization.NAME.matches().value("Smith")))
.returnBundle(Bundle.class)
.execute();
To search for a set of possible values where ANY should be matched, you can provide multiple values to a parameter, as shown in the example below.
This leads to a URL resembling http://base/Patient?family=Smith,Smyth
.
response = client.search()
.forResource(Patient.class)
.where(Patient.FAMILY.matches().values("Smith", "Smyth"))
.returnBundle(Bundle.class)
.execute();
To search for a set of possible values where ALL should be matched, you can provide multiple instances of a parameter, as shown in the example below.
This leads to a URL resembling http://base/Patient?address=Toronto&address=Ontario&address=Canada
.
response = client.search()
.forResource(Patient.class)
.where(Patient.ADDRESS.matches().values("Toronto"))
.and(Patient.ADDRESS.matches().values("Ontario"))
.and(Patient.ADDRESS.matches().values("Canada"))
.returnBundle(Bundle.class)
.execute();
If the server supports paging results, the client has a page method which can be used to load subsequent pages.
FhirContext ctx = FhirContext.forDstu2();
IGenericClient client = ctx.newRestfulGenericClient("http://fhirtest.uhn.ca/baseDstu2");
// Perform a search
Bundle resultBundle = client.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("Smith"))
.returnBundle(Bundle.class)
.execute();
if (resultBundle.getLink(Bundle.LINK_NEXT) != null) {
// load next page
Bundle nextPage = client.loadPage().next(resultBundle).execute();
}
If a composite parameter is being searched on, the parameter takes a "left" and "right" operand, each of which is a parameter from the resource being searched. The following example shows the syntax.
response = client.search()
.forResource("Observation")
.where(Observation.CODE_VALUE_DATE
.withLeft(Observation.CODE.exactly().code("FOO$BAR"))
.withRight(Observation.VALUE_DATE.exactly().day("2001-01-01")))
.returnBundle(Bundle.class)
.execute();
You can also perform a search using a String URL, instead of using the fluent method calls to build the URL.
This can be useful if you have a URL you retrieved from somewhere else that you want to use as a search. This can also be useful if you need to make a search that isn't presently supported by one of the existing fluent methods (e.g. reverse chaining with _has
).
String searchUrl = "http://example.com/base/Patient?identifier=foo";
// Search URL can also be a relative URL in which case the client's base
// URL will be added to it
searchUrl = "Patient?identifier=foo";
response =
client.search().byUrl(searchUrl).returnBundle(Bundle.class).execute();
The fluent search also has methods for sorting, limiting, specifying JSON encoding, _include, _revinclude, _lastUpdated, _tag, etc.
response = client.search()
.forResource(Patient.class)
.encodedJson()
.where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22"))
.and(Patient.BIRTHDATE.after().day("2011-01-01"))
.withTag("http://acme.org/codes", "needs-review")
.include(Patient.INCLUDE_ORGANIZATION.asRecursive())
.include(Patient.INCLUDE_GENERAL_PRACTITIONER.asNonRecursive())
.revInclude(Provenance.INCLUDE_TARGET)
.lastUpdated(new DateRangeParam("2011-01-01", null))
.sort()
.ascending(Patient.BIRTHDATE)
.sort()
.descending(Patient.NAME)
.count(123)
.returnBundle(Bundle.class)
.execute();
The FHIR specification allows the use of an HTTP POST to transmit a search to a server instead of using an HTTP GET. With this style of search, the search parameters are included in the request body instead of the request URL, which can be useful if you need to transmit a search with a large number of parameters.
The usingStyle(SearchStyleEnum)
method controls which style to use. By default, GET style is used unless the client detects that the request would result in a very long URL (over 8000 chars) in which case the client automatically switches to POST.
If you wish to force the use of HTTP POST, you can do that as well.
response = client.search()
.forResource("Patient")
.where(Patient.NAME.matches().value("Tester"))
.usingStyle(SearchStyleEnum.POST)
.returnBundle(Bundle.class)
.execute();
To search a resource compartment, simply use the withIdAndCompartment()
method in your search.
response = client.search()
.forResource(Patient.class)
.withIdAndCompartment("123", "condition")
.where(Patient.ADDRESS.matches().values("Toronto"))
.returnBundle(Bundle.class)
.execute();
Sometimes you may want to only ask the server to include some parts of returned resources (instead of the whole resource). Typically this is for performance or optimization reasons, but there may also be privacy reasons for doing this.
To request that the server return only "summary" elements (those elements defined in the specification with the "Σ" flag), you can use the summaryMode(SummaryEnum)
qualifier:
response = client.search()
.forResource(Patient.class)
.where(Patient.ADDRESS.matches().values("Toronto"))
.returnBundle(Bundle.class)
.summaryMode(SummaryEnum.TRUE)
.execute();
To request that the server return only elements from a custom list provided by the client, you can use the elementsSubset(String...)
qualifier:
response = client.search()
.forResource(Patient.class)
.where(Patient.ADDRESS.matches().values("Toronto"))
.returnBundle(Bundle.class)
.elementsSubset("identifier", "name") // only include the identifier and name
.execute();
The following example shows how to perform a create operation using the generic client:
Patient patient = new Patient();
// ..populate the patient object..
patient.addIdentifier().setSystem("urn:system").setValue("12345");
patient.addName().setFamily("Smith").addGiven("John");
// Invoke the server create method (and send pretty-printed JSON
// encoding to the server
// instead of the default which is non-pretty printed XML)
MethodOutcome outcome = client.create()
.resource(patient)
.prettyPrint()
.encodedJson()
.execute();
// The MethodOutcome object will contain information about the
// response from the server, including the ID of the created
// resource, the OperationOutcome response, etc. (assuming that
// any of these things were provided by the server! They may not
// always be)
IIdType id = outcome.getId();
System.out.println("Got ID: " + id.getValue());
FHIR also specifies a type of update called "conditional create", where a set of search parameters are provided and a new resource is only created if no existing resource matches those parameters. See the FHIR specification for more information on conditional creation.
// One form
MethodOutcome outcome = client.create()
.resource(patient)
.conditionalByUrl("Patient?identifier=system%7C00001")
.execute();
// Another form
MethodOutcome outcome2 = client.create()
.resource(patient)
.conditional()
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001"))
.execute();
// This will return Boolean.TRUE if the server responded with an HTTP 201 created,
// otherwise it will return null.
Boolean created = outcome.getCreated();
// The ID of the created, or the pre-existing resource
IIdType id = outcome.getId();
Given a resource name and ID, it is simple to retrieve the latest version of that resource (a 'read').
// search for patient 123
Patient patient =
client.read().resource(Patient.class).withId("123").execute();
By adding a version string, it is also possible to retrieve a specific version (a 'vread').
// search for patient 123 (specific version 888)
Patient patient = client.read()
.resource(Patient.class)
.withIdAndVersion("123", "888")
.execute();
It is also possible to retrieve a resource given its absolute URL (this will override the base URL set on the client).
// search for patient 123 on example.com
String url = "http://example.com/fhir/Patient/123";
Patient patient = client.read().resource(Patient.class).withUrl(url).execute();
See Also: See the description of Read/VRead ETags below for information on specifying a matching version in the client request.
The following example shows how to perform a delete operation using the generic client:
MethodOutcome response =
client.delete().resourceById(new IdType("Patient", "1234")).execute();
// outcome may be null if the server didn't return one
OperationOutcome outcome = (OperationOutcome) response.getOperationOutcome();
if (outcome != null) {
System.out.println(outcome.getIssueFirstRep()
.getDetails()
.getCodingFirstRep()
.getCode());
}
Conditional deletions are also possible, which is a form where instead of deleting a resource using its logical ID, you specify a set of search criteria and a single resource is deleted if it matches that criteria. Note that this is not a mechanism for bulk deletion; see the FHIR specification for information on conditional deletes and how they are used.
client.delete()
.resourceConditionalByUrl("Patient?identifier=system%7C00001")
.execute();
client.delete()
.resourceConditionalByType("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001"))
.execute();
The following snippet shows now to request a cascading delete. Note that this is a HAPI FHIR specific feature and is not supported on all servers.
client.delete()
.resourceById(new IdType("Patient/123"))
.cascade(DeleteCascadeModeEnum.DELETE)
.execute();
client.delete()
.resourceConditionalByType("Patient")
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001"))
.execute();
Updating a resource is similar to creating one, except that an ID must be supplied since you are updating a previously existing resource instance.
The following example shows how to perform an update operation using the generic client:
Patient patient = new Patient();
// ..populate the patient object..
patient.addIdentifier().setSystem("urn:system").setValue("12345");
patient.addName().setFamily("Smith").addGiven("John");
// To update a resource, it should have an ID set (if the resource
// object
// comes from the results of a previous read or search, it will already
// have one though)
patient.setId("Patient/123");
// Invoke the server update method
MethodOutcome outcome = client.update().resource(patient).execute();
// The MethodOutcome object will contain information about the
// response from the server, including the ID of the created
// resource, the OperationOutcome response, etc. (assuming that
// any of these things were provided by the server! They may not
// always be)
IdType id = (IdType) outcome.getId();
System.out.println("Got ID: " + id.getValue());
FHIR also specifies a type of update called "conditional updates", where instead of using the logical ID of a resource to update, a set of search parameters is provided. If a single resource matches that set of parameters, that resource is updated. See the FHIR specification for information on how conditional updates work.
client.update()
.resource(patient)
.conditionalByUrl("Patient?identifier=system%7C00001")
.execute();
client.update()
.resource(patient)
.conditional()
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001"))
.execute();
See Also: See the description of Update ETags below for information on specifying a matching version in the client request.
The PATCH operation can be used to modify a resource in place by supplying a delta
The following example shows how to perform a patch using a FHIR Patch
// Create a client
String serverBase = "http://hapi.fhir.org/baseR4";
IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Create a patch object
Parameters patch = new Parameters();
Parameters.ParametersParameterComponent operation = patch.addParameter();
operation.setName("operation");
operation.addPart().setName("type").setValue(new CodeType("delete"));
operation.addPart().setName("path").setValue(new StringType("Patient.identifier[0]"));
// Invoke the patch
MethodOutcome outcome =
client.patch().withFhirPatch(patch).withId("Patient/123").execute();
// The server may provide the updated contents in the response
Patient resultingResource = (Patient) outcome.getResource();
The following example shows how to perform a patch using a JSON Patch
// Create a client
String serverBase = "http://hapi.fhir.org/baseR4";
IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Create a JSON patch object
String patch = "[ " + " { "
+ " \"op\":\"replace\", "
+ " \"path\":\"/active\", "
+ " \"value\":false "
+ " } "
+ "]";
// Invoke the patch
MethodOutcome outcome =
client.patch().withBody(patch).withId("Patient/123").execute();
// The server may provide the updated contents in the response
Patient resultingResource = (Patient) outcome.getResource();
To retrieve the version history of all resources, or all resources of a given type, or of a specific instance of a resource, you call the history()
method.
Bundle response =
client.history().onServer().returnBundle(Bundle.class).execute();
You can also optionally request that only resource versions later than a given date, and/or only up to a given count (number) of resource versions be returned.
Bundle response = client.history()
.onServer()
.returnBundle(Bundle.class)
.since(new InstantType("2012-01-01T12:22:32.038Z"))
.count(100)
.execute();
The following example shows how to execute a transaction using the generic client:
List<IResource> resources = new ArrayList<IResource>();
// .. populate this list - note that you can also pass in a populated
// Bundle if you want to create one manually ..
List<IBaseResource> response =
client.transaction().withResources(resources).execute();
To retrieve the server's capability statement, simply call the capabilities()
method as shown below.
// Retrieve the server's conformance statement and print its
// description
CapabilityStatement conf =
client.capabilities().ofType(CapabilityStatement.class).execute();
System.out.println(conf.getDescriptionElement().getValue());
FHIR also supports a set of extended operations, which are operations beyond the basic CRUD operations defined in the specification. These operations are an RPC style of invocation, with a set of named input parameters passed to the server and a set of named output parameters returned back.
To invoke an operation using the client, you simply need to create the input Parameters resource, then pass that to the operation()
fluent method.
The example below shows a simple operation call.
HttpGet
// Create a client to talk to the HeathIntersections server
FhirContext ctx = FhirContext.forDstu2();
IGenericClient client = ctx.newRestfulGenericClient("http://fhir-dev.healthintersections.com.au/open");
client.registerInterceptor(new LoggingInterceptor(true));
// Create the input parameters to pass to the server
Parameters inParams = new Parameters();
inParams.addParameter().setName("start").setValue(new DateType("2001-01-01"));
inParams.addParameter().setName("end").setValue(new DateType("2015-03-01"));
// Invoke $everything on "Patient/1"
Parameters outParams = client.operation()
.onInstance(new IdType("Patient", "1"))
.named("$everything")
.withParameters(inParams)
.useHttpGet() // Use HTTP GET instead of POST
.execute();
Note that if the operation does not require any input parameters, you may also invoke the operation using the following form. Note that the withNoParameters
form still requires you to provide the type of the Parameters resource so that it can return the correct type in the response.
// Create a client to talk to the HeathIntersections server
FhirContext ctx = FhirContext.forDstu2();
IGenericClient client = ctx.newRestfulGenericClient("http://fhir-dev.healthintersections.com.au/open");
client.registerInterceptor(new LoggingInterceptor(true));
// Invoke $everything on "Patient/1"
Parameters outParams = client.operation()
.onInstance(new IdType("Patient", "1"))
.named("$everything")
.withNoParameters(Parameters.class) // No input parameters
.execute();
By default, the client will invoke operations using the HTTP POST form. The FHIR specification also allows requests to use the HTTP GET verb if the operation does not affect state and has no composite/resource parameters. Use the following form to invoke operation with HTTP GET.
// Create a client to talk to the HeathIntersections server
FhirContext ctx = FhirContext.forDstu2();
IGenericClient client = ctx.newRestfulGenericClient("http://fhir-dev.healthintersections.com.au/open");
client.registerInterceptor(new LoggingInterceptor(true));
// Create the input parameters to pass to the server
Parameters inParams = new Parameters();
inParams.addParameter().setName("start").setValue(new DateType("2001-01-01"));
inParams.addParameter().setName("end").setValue(new DateType("2015-03-01"));
// Invoke $everything on "Patient/1"
Parameters outParams = client.operation()
.onInstance(new IdType("Patient", "1"))
.named("$everything")
.withParameters(inParams)
.useHttpGet() // Use HTTP GET instead of POST
.execute();
The $validate
operation asks the server to test a given resource to see if it would be acceptable as a create/update on that server. The client has built-in support for this operation.
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://hospital.com").setValue("123445");
patient.addName().setFamily("Smith").addGiven("John");
// Validate the resource
MethodOutcome outcome = client.validate().resource(patient).execute();
// The returned object will contain an operation outcome resource
OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome();
// If the OperationOutcome has any issues with a severity of ERROR or SEVERE,
// the validation failed.
for (OperationOutcome.OperationOutcomeIssueComponent nextIssue : oo.getIssue()) {
if (nextIssue.getSeverity().ordinal() >= OperationOutcome.IssueSeverity.ERROR.ordinal()) {
System.out.println("We failed validation!");
}
}
The $process-message
operation asks the server to accept a FHIR message bundle for processing.
FhirContext ctx = FhirContext.forDstu3();
// Create the client
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:9999/fhir");
Bundle bundle = new Bundle();
// ..populate the bundle..
Bundle response = client.operation()
.processMessage() // New operation for sending messages
.setMessageBundle(bundle)
.asynchronous(Bundle.class)
.execute();
This section contains ways of customizing the request sent by the client.
The Cache-Control
header can be used by the client in a request to signal to the server (or any cache in front of it) that the client wants specific behaviour from the cache, or wants the cache to not act on the request altogether. Naturally, not all servers will honour this header.
To add a cache control directive in a request:
Bundle response = client.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.cacheControl(new CacheControlDirective().setNoCache(true)) // <-- add a directive
.execute();
ETag features are added simply by adding fluent method calls to the client method chain, as shown in the following examples.
To notify the server that it should return an HTTP 304 Not Modified
if the content has not changed, add an ifVersionMatches()
invocation.
// search for patient 123
Patient patient = client.read()
.resource(Patient.class)
.withId("123")
.ifVersionMatches("001")
.returnNull()
.execute();
if (patient == null) {
// resource has not changed
}
This method will add the following header to the request:
If-None-Match: "W/001"
To implement version aware updates, specify a version in the request. This will notify the server that it should only update the resource on the server if the version matches the given version. This is useful to prevent two clients from attempting to modify the resource at the same time, and having one client's updates overwrite the other's.
// First, let's retrieve the latest version of a resource
// from the server
Patient patient =
client.read().resource(Patient.class).withId("123").execute();
// If the server is a version aware server, we should now know the latest version
// of the resource
System.out.println("Version ID: " + patient.getIdElement().getVersionIdPart());
// Now let's make a change to the resource
patient.setGender(Enumerations.AdministrativeGender.FEMALE);
// Invoke the server update method - Because the resource has
// a version, it will be included in the request sent to
// the server
try {
MethodOutcome outcome = client.update().resource(patient).execute();
} catch (PreconditionFailedException e) {
// If we get here, the latest version has changed
// on the server so our update failed.
}
The following header will be added to the request as a part of this interaction:
If-Match: "W/001"