Best-Practices to use the Microservice SDK for Java for custom API queries

Introduction

The Cumulocity IoT API provides a wide range of API query capabilities, but only a subset is easily useable directly by the Microservice SDK for Java.

Mostly the inventory API provides the most capable (and complex) query language, which is heavily used in many projects. Also other APIs like alarm, event, measurement, audit and identity also can be used with the same approach!

Due to the wide variety of features, query language and URL parameters, the SDK does not support this in all detail.
Implementing a microservice usually requires to interact for example with the inventory API to search for devices by specific criteria. Once the required API call is identified, e.g. via Postman, you think of easily taking that into a microservice to automate the task.
This article is about to show some best practices including code examples on how to simplify calling the Cumulocity IoT API with the full set of its features.

Pre-requisite

Any microservice based on the Cumulocity IoT Microservice SDK for Java is applicable.

How does it work?

Problem statement

You want to query devices by their creation time. Hence via API call you know to use a GET API call like below and you want to be able to do the same call within a microservice, using the microservice SDK.

GET /inventory/managedObjects?query=$filter=creationTime.date gt '2023-06-12T12:00:00.000Z' and creationTime.date le '2023-06-12T12:59:59.999Z'

Limitations of standard SDK classes

Normally you can use InventoryFilter to query the inventory API, but it is limited and does not (easily) support the powerful query language (e.g. filter by creation time from the problem statement above) or URL parameters like withParents. Hence I want to share a best practice to use the SDK in a code-friendly, reusable, and readable way.

Using the QueryParam class directly from the SDK - not optimal

The QueryParam class allows us to define custom URL parameters when doing API calls - especially those not directly supported by the InventoryFilter class which is used by the InventoryApi class.

The getManagedObjects() method of the inventoryApi returns an ManagedObjectCollection object, for which the interesting and simple method get is available.

This get method takes the pageSize as the first parameter and one or more QueryParams as following parameters.

Note: This applies not only to the inventory API but also to other APIs mentioned in the introduction.

In the below simple example the withParents URL parameter is used with the value true.

However, this code gets quite huge when using more parameters. The next section shows a convenience class that reduces the code and ensures reusability.

import com.cumulocity.sdk.client.Filter;
import com.cumulocity.sdk.client.QueryParam;

QueryParam queryParam = new QueryParam(new Param() {
		@Override
		public String getName() {
			return "withParents";
		}
	}, "true");

Iterable<ManagedObjectRepresentation> managedObjects = inventoryApi
	.getManagedObjects()
	.get(50, queryParam)
	.allPages();

Create a convenience class for code simplification

The below class CustomQueryParam implements the Param interface and allows easy, repetitive use QueryParam objects to define any URL parameter of the inventory API.

Any URL parameter can be simply provided as an enum with its actual parameter name - feel free to use and extend this enum.
See below for how to use this enum and how it simplifies the code.

public enum CustomQueryParam implements Param {
	WITH_TOTAL_PAGES("withTotalPages"),
	PAGE_SIZE("pageSize"),
	QUERY("query"),
	DEVICE_QUERY("q"),
	DATE_FROM("dateFrom"),
	STATUS("status"),
	FRAGMENT_TYPE("fragmentType"),
	DEVICE_ID("deviceId"),
	REVERT("revert"),
	;

	private String name;
	
	private String value;
	
	public String getValue() {
		return value;
	}

	private CustomQueryParam(final String name) {
		this.name = name;
	}

	@Override
	public String getName() {
		return name;
	}

	public CustomQueryParam setValue(final String value) {
		this.value = value;
		return this;
	}

	public QueryParam toQueryParam() {
		return new QueryParam(this, Filter.encode(value));
	}

}

Making use of the CustomQueryParam class

Now we come back to our initial example and see how we can simplify this.
Additionally we can solve our initial problem to also query by creation time easily.

We can easily create 2 QueryParam objects for the withParents URL parameter and for the creation time query - both as a (long) one-liner.

The get method can then be called with the page size and both query parameters in one go.

Now allPages nicely returns an Iterable with the result of our query.

int pageSize = 50;
QueryParam withParents = CustomQueryParam.WITH_PARENTS.setValue("true").toQueryParam();
QueryParam creationTimeQuery = CustomQueryParam.QUERY.setValue("creationTime.date ge " + "'" + CustomUtil.getDateWithTime() + "'").toQueryParam();
		
Iterable<ManagedObjectRepresentation> managedObjects = inventoryApi
	.getManagedObjects()
	.get(pageSize, withParents, creationTimeQuery)
	.allPages();

The above code would result in the following API call:

GET /inventory/managedObjects?pageSize=50&withParents=true&query=$filter=creationTime.date gt '2023-06-12T12:00:00.000Z' and creationTime.date le '2023-06-12T12:59:59.999Z'

Conclusion

The above best practice shows that even any complex API calls using multiple URL parameters can be implemented much easier with the given convenience class and methods to allow a fast transfer from the raw API call into the microservice implementation.

Useful links | Relevant resources

2 Likes