Skip to main content

DHIS2 Java SDK

DHIS2 Java SDK is a lightweight library that hides the nuts and bolts of DHIS2 Web API interactions behind a fluent Java API and type-safe resource models. The SDK code is open-source and hosted on GitHub. Contributions from the DHIS2 community are more than welcome.

Basics

Getting started

important

DHIS2 Java SDK is compatible with Java 8 and later versions.

The DHIS2 Java SDK can be built from source, however, binaries of the SDK releases are distributed on the Sonatype Maven repository and can be added to your project's Java class path with Maven. The following is a template for declaring the SDK as dependencies in your Maven project's POM:

<project>
...
<dependencies>
<dependency>
<groupId>org.hisp.dhis.integration.sdk</groupId>
<artifactId>dhis2-java-sdk</artifactId>
<version>x.y.z</version>
</dependency>

<!-- exclude for Android -->
<dependency>
<groupId>org.hisp.dhis.integration.sdk</groupId>
<artifactId>jackson-resource-model</artifactId>
<classifier>[v40.2.2|v40.0|v2.39.1|v2.38.1|v2.37.7|v2.36.11|v2.35.13]</classifier>
<version>x.y.z</version>
</dependency>

<!-- include for Android -->
<dependency>
<groupId>org.hisp.dhis.integration.sdk</groupId>
<artifactId>android-jackson-resource-model</artifactId>
<classifier>[v40.2.2|v40.0|v2.39.1|v2.38.1|v2.37.7|v2.36.11|v2.35.13]</classifier>
<version>x.y.z</version>
</dependency>
...
</dependencies>
</project>

The dhis2-java-sdk artifact contains the DHIS2 client and helper classes while the jackson-resource-model artifact contains the Jackson-annotated classes representing the DHIS2 API resources. The android-jackson-resource-model artifact is similar to jackson-resource-model with the exception that its resource classes are compatible with Android. jackson-resource-model should be omitted from your POM if you are building for Android.

The x.y.z version in the above template is to be replaced with the latest version of the SDK. The jackson-resource-model or android-jackson-resource-model artifact classifier should bet set to one of the shown DHIS2 versions (i.e., v40.0 or v2.39.1 or v2.38.1 or v2.37.7 or v2.36.11 or v2.35.13). The classifier ought to closely match the DHIS2 version you are integrating with. You can have multiple jackson-resource-model or android-jackson-resource-model dependencies with different classifiers: one for each DHIS2 version you are integrating with. For instance:

<project>
...
<dependencies>
<dependency>
<groupId>org.hisp.dhis.integration.sdk</groupId>
<artifactId>dhis2-java-sdk</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.hisp.dhis.integration.sdk</groupId>
<artifactId>jackson-resource-model</artifactId>
<classifier>v40.2.2</classifier>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.hisp.dhis.integration.sdk</groupId>
<artifactId>jackson-resource-model</artifactId>
<classifier>v2.35.13</classifier>
<version>3.0.0</version>
</dependency>
</dependencies>
</project>

Having multiple jackson-resource-model dependencies will not create a conflict as long as each dependency has a distinct classifier. The classes within jackson-resource-model v40.2.2 are located in the Java package org.hisp.dhis.api.model.v40_2_2, the ones within jackson-resource-model v40.0 are located in the package org.hisp.dhis.api.model.v40_0, and so on.

Creating a DHIS2 client

The SDK client for exchanging data with a DHIS2 server is constructed programmatically with Dhis2ClientBuilder:

org.hisp.dhis.integration.sdk.api.Dhis2Client dhis2Client = org.hisp.dhis.integration.sdk.Dhis2ClientBuilder.newClient(...).build();

Dhis2ClientBuilder has a static method called newClient that expects as parameters the DHIS2 server base API URL together with the DHIS2 user account's username and password or PAT. In the following example, a Dhis2Client is built that will transmit requests to https://play.dhis2.org/40.2.0/api/ using the PAT d2pat_apheulkR1x7ac8vr9vcxrFkXlgeRiFc94200032556 for authentication:

Dhis2Client dhis2Client = Dhis2ClientBuilder.newClient( "https://play.dhis2.org/40.2.0/api/", "d2pat_apheulkR1x7ac8vr9vcxrFkXlgeRiFc94200032556" ).build();
note

It is not necessary to terminate the base API URL with a slash.

Apart from the mandatory parameters, Dhis2Client can have its connection parameters tweaked with Dhis2ClientBuilder. What follows is a table describing the connection parameters.

Parameter NameMethod NameDescriptionDefaultJava type
Max idle connectionswithMaxIdleConnectionsThe maximum number of idle connections in the HTTP connection pool.5int
Keep alive durationwithKeepAliveDurationTime to keep the connection alive in the pool before closing it.30 secondslong
Call timeoutwithCallTimeoutThe call timeout spans the entire call: resolving DNS, connecting, writing the request body, server processing, and reading the response body. If the call requires redirects or retries all must complete within one timeout period.0 seconds (no timeout)long
Read timeoutwithReadTimeoutThe read timeout is applied to both the TCP socket and for individual read IO operations including on Source of the Response.10 secondslong
Write timeoutwithWriteTimeoutThe write timeout is applied for individual write IO operations.10 secondslong
Connect timeoutwithConnectTimeoutThe connect timeout is applied when connecting a TCP socket to the target host.10 secondslong

Dhis2Client

A Dhis2Client method chain assembles a request for retrieving or sending a DHIS2 resource. The first method of a Dhis2Client method chain identifies the HTTP verb of the request. This can be one of the following:

  • get
  • post
  • put
  • delete

Each of the above methods expects at least one parameter that denotes the relative URL path the request is for. For example:

dhis2Client.get( "organisationUnits/fdc6uOvgoji" );

Assuming the base API URL is https://play.dhis2.org/40.2.0/api, the absolute URL path for the GET request in the example is https://play.dhis2.org/40.2.0/api/organisationUnits/fdc6uOvgoji.

Alternatively, instead of having a hard-coded URL path, you can name the variable parts of the path. The names can be then bound to values in the parameter list of the method:

dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" );

Dhis2Client replaces {id} with fdc6uOvgoji in the above snippet. Query parameters can be added to the request using various methods depending on the HTTP verb but withParameter is a method that is applicable across all HTTP verbs:

dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).withParameter("fields", "name").withParameter("filter", "level:eq:3");

withParameters is similar to withParameter with the notable difference that it accepts a map of parameters:

dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).withParameters(Map.of("fields", "name", "filter", "level:eq:3"));

A method chain should always have a transfer() method in order to send the assembled request over the wire and wait for a response:

dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer();

If the response is successful, you can either choose to ignore or read the JSON result. Ignoring the result without closing the call could lead to a memory leak so it is always good practice to terminate the method chain with close() after transferring a request:

dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().close();

However, typically you would want to read the resource after fetching it. There are a number of different ways on how to accomplish this. The recommended way is to deserialise the reply body into a POJO with returnAs(...):

org.hisp.dhis.api.model.v2_37_7.OrganisationUnit organisationUnit = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().returnAs( org.hisp.dhis.api.model.v2_37_7.OrganisationUnit.class );
note

returnAs(...) implicitly closes the response.

OrganisationUnit is one of the many POJO classes provided by the jackson-resource-model JAR (the jackson-resource-model artifact classifier used for this example is v2.37.7). Most of the DHIS2 resources are available as classes from this JAR. Having said this, you are free to desrialise the body into a java.util.Map:

Map<String, Object> organisationUnitAsMap = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().returnAs( Map.class );

You could also obtain the raw JSON content like so:

String organisationUnitAsJson = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().returnAs( String.class );

For large payloads, you might want to stream the result rather than reading it all into memory. In such cases, it is better to use the read() method at the end of the chain in order to incrementally read the JSON from an input stream:

InputStream organisationUnitAsStream = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().read();

Remote Operations

Fetching

A DHIS2 resource is fetched with Dhis2Client#get(...) as shown in the next example:

OrganisationUnit orgUnit = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().returnAs( OrganisationUnit.class );

withField

The fields of a resource can be selected using the withField(...) method:

OrganisationUnit orgUnit = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).withField( "*" ).transfer().returnAs( OrganisationUnit.class );
note

More than one withField(...) method can be included in a method chain.

withFields

Multiple withField(...) methods can be combined into a single withFields(...) method like so:

OrganisationUnit orgUnit = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).withFields( "id", "name" ).transfer().returnAs( OrganisationUnit.class );

Collections

Fetching a non-paginated resource collection is accomplished by including withoutPaging() in the method chain:

Iterable<OrganisationUnit> orgUnits = dhis2Client.get( "organisationUnits" ).withoutPaging().transfer().returnAs( OrganisationUnit.class, "organisationUnits" );

for ( OrganisationUnit orgUnit : orgUnits )
{
// do stuff
}

Observe that returnAs(...) has an extra parameter specifying the array field name holding the collection in the reply. In this example, the array name is organisationUnits.

Fetching a resource collection with pagination is accomplished by including withPaging() instead of withoutPaging() in the method chain:

Iterable<OrganisationUnit> orgUnits = dhis2Client.get( "organisationUnits" ).withPaging().transfer().returnAs( OrganisationUnit.class, "organisationUnits" );

for ( OrganisationUnit orgUnit : orgUnits )
{
// do stuff without having to worry about fetching the next page
}

withPaging() leads to a lazy Iterable object being from returnAs. When the iterator reaches the last item in the current page, the client transparently fetches the next page, if there is one: the developer does not need to worry about fetching the next page of results.

withFilter

Filtering collections with DHIS2's object filter is possible using withFilter(...):

Iterable<OrganisationUnit> organisationUnits = dhis2Client.get( "organisationUnits" ).withFilter( "level:eq:3" ).withPaging().transfer().returnAs( OrganisationUnit.class, "organisationUnits" );

The above snippet fetches organisation units situated at the third level of the organisation hierarchy.

note

More than one withFilter(...) method can be included in a method chain.

withAndRootJunction

The AND logical operator is applied to multiple filters using the withAndRootJunction() method:

Iterable<DataElement> dataElements = dhis2Client.get( "dataElements" ).withFilter( "id:in:[id1,id2]" ).withFilter( "code:eq:code1" ).withAndRootJunction().withPaging().transfer().returnAs( DataElement.class, "dataElements" );
withOrRootJunction

The OR logical operator is applied to multiple filters using the withOrRootJunction() method:

Iterable<DataElement> dataElements = dhis2Client.get( "dataElements" ).withFilter( "id:in:[id1,id2]" ).withFilter( "code:eq:code1" ).withOrRootJunction().withPaging().transfer().returnAs( DataElement.class, "dataElements" );

Saving

A DHIS2 resource is saved with Dhis2Client#post(...) as shown in the next example:

WebMessage webMessage = dhis2Client.post( "organisationUnits" ).withResource( new OrganisationUnit().withName( "Acme" ).withCode( "ACME" ).withShortName( "Acme" ).withOpeningDate( new Date( 964866178L ) ) ).transfer().returnAs( WebMessage.class );

A post(...) is usually followed by a withResource(...) method to set the body of the request. The request body in the above snippet is set to an organisation unit POJO. Apart from POJOs, withResource(...) accepts JSON strings and objects of type java.util.Map.

Updating

A DHIS2 resource is updated with Dhis2Client#put(...) as shown in the next example:

WebMessage webMessage = dhis2Client.put( "organisationUnits" ).withResource( new OrganisationUnit().withName( "Acme" ).withCode( "ACME" ).withShortName( "Acme" ).withOpeningDate( new Date( 964866178L ) ) ).transfer().returnAs( WebMessage.class );

A put(...) is usually followed by a withResource(...) method to set the body of the request. The request body in the above snippet is set to an organisation unit POJO. Apart from POJOs, withResource(...) accepts JSON strings and objects of type java.util.Map.

Deleting

A DHIS2 resource is deleted with Dhis2Client#delete(...) as shown in the next example:

dhis2Client.delete( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().close();

A delete(...) can be followed by a withResource(...) method to set the body of the request. The request body in the above snippet is set to an organisation unit POJO. Apart from POJOs, withResource(...) accepts JSON strings and objects of type java.util.Map.

Runtime errors

An application should be prepared to handle Dhis2ClientException and RemoteDhis2ClientException unchecked exceptions when invoking DHIS2 Java SDK methods.

Dhis2ClientException

Dhis2ClientException exception represents any error that is not originating from the server. This includes network timeouts. You can extract the error message and the cause from the caught exception:

try {
org.hisp.dhis.api.model.v2_37_7.OrganisationUnit organisationUnit = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().returnAs( org.hisp.dhis.api.model.v2_37_7.OrganisationUnit.class );
} catch (org.hisp.dhis.integration.sdk.api.Dhis2ClientException e) {
System.out.println(e.getMessage());
}

RemoteDhis2ClientException

RemoteDhis2ClientException is a special type of Dhis2ClientException that occurs when the server returns a reply with an HTTP status code that is unsuccessful (i.e., not 2xx). While Dhis2ClientException contains the error message and the cause, RemoteDhis2ClientException adds the HTTP status code and the HTTP response body:

try {
org.hisp.dhis.api.model.v2_37_7.OrganisationUnit organisationUnit = dhis2Client.get( "organisationUnits/{id}", "fdc6uOvgoji" ).transfer().returnAs( org.hisp.dhis.api.model.v2_37_7.OrganisationUnit.class );
} catch (org.hisp.dhis.integration.sdk.api.RemoteDhis2ClientException e) {
System.out.println(e.getHttpStatusCode() + " => " + e.getHttpStatusCode().getBody());
}