Skip to content

REST Client

Edvin Syse edited this page Nov 14, 2016 · 18 revisions

WikiDocumentationREST Client

REST Client

Tornado FX comes with a REST Client that makes it easy to perform JSON based REST calls. The underlying HTTP engine interface has two implementations. The default uses HttpURLConnection and there is also an implementation based on Apache HttpClient. It is easy to extend the Rest.Engine to support other http client libraries if needed.

To use the Apache HttpClient implementation, simply call Rest.useApacheHttpClient() in the init method of your App class and include the org.apache.httpcomponents:httpclient dependency in your project descriptor.

Configuration

If you mostly access the same api on every call, you can set a base uri so subsequent calls only need to include relative urls. You can configure the base url anywhere you like, but the init function of your App class is a good place to do it.

class MyApp : App() {
    val api: Rest by inject()

    init {
        api.baseURI = "http://contoso.com/api"
    }
}

Basic operations

There are convenience functions to perform GET, PUT, POST and DELETE operations.

class CustomerController : Controller() {
    val api = Rest by inject()

    fun loadCustomers(): ObservableList<Customer> = 
        api.get("customers").list().toModel()
}

CustomerController with loadCustomers call

So, what exactly is going on in the loadCustomers function? First we call api.get("customers") which will perform the call and return a Response object. We then call Response.list() which will consume the response and convert it to a javax.json.JsonArray. Lastly, we call the extension function JsonArray.toModel() which creates one Customer object per JsonObject in the array and calls JsonModel.updateModel on it. In this example, the type argument is taken from the function return type, but you could also write the above method like this if you prefer:

fun loadCustomers() = api.get("customers").list().toModel(Customer::class)

How you provide the type argument to the toModel function is a matter of taste, so choose the syntax you are most comfortable with.

These functions take an optional parameter with either a JsonObject or a JsonModel that will be the payload of your request, converted to a JSON string.

See the JsonModel page for more information on the JSON support in TornadoFX.

The following example updates a customer object.

fun updateCustomer(customer: Customer) = api.put("customers/${customer.id}", customer)

If the api endpoint returns the customer object to us after save, we would simply append a toModel call at the end of the function.

fun updateCustomer(customer: Customer) = api.put("customers/${customer.id}", customer)
    .toModel(Customer::class)

Error handling

If an I/O error occurs during the processing of the request, the default Error Handler will report the error to the user. You can ofcourse catch any errors yourself instead. To handle HTTP return codes, you might want to inspect the Response before you convert the result to JSON. Make sure you always call consume() on the response if you don't extract data from it using any of the methods list(), one(), text() or bytes().

fun getCustomer(id: Int): Customer {
    val response = api.get("some/action")

    try {
        if (response.ok()) 
            return response.one().toModel() 
        else if (response.statusCode == 404)
            throw CustomerNotFound()
        else
            throw MyException("getCustomer returned ${response.statusCode} ${response.reason}")
    } finally {
        response.consume()
    }
}

Extract status code and reason from HttpResponse

response.ok() is shorthand for response.statusCode == 200.

Authentication

Tornado FX makes it very easy to add basic authentication to your api requests:

api.setBasicAuth("username", "password")

To configure authentication manually, configure the authInterceptor of the engine to add custom headers etc to the request. For example, this is how the basic authentication is implemented for the HttpUrlEngine:

authInterceptor = { engine ->
    val b64 = Base64.getEncoder().encodeToString("$username:$password".toByteArray(UTF_8))
    engine.addHeader("Authorization", "Basic $b64")
}

For a more advanced example of configuring the underlying client, take a look at how basic authentication is implemented in the HttpClientEngine.setBasicAuth function in Rest.kt.

Intercepting calls

You can for example show a login screen if an HTTP call fails with statusCode 401:

api.engine.responseInterceptor = { response ->
    if (response.statusCode == 401)
        showLoginScreen("Invalid credentials, please log in again.")
}

Connect to multiple API's

You can create multiple instances of the Rest class by subclassing it and configuring each subclass as you wish. Injection of subclasses work seamlessly. Override the engine property if you want to use another engine than the default.

Default engine for new Rest instances

The engine used by a new Rest client is configured with the engineProvider of the Rest class. This is what happens when you call Rest.useApacheHttpClient:

Rest.engineProvider = { rest -> HttpClientEngine(rest) }

The engineProvider returns a concrete engine implementation that is given the current Rest instance as argument.

You can override the configured engine in a Rest instance at any time.

Proxy

A proxy can be configured either by implementing an interceptor that augments each call, or, preferably once per Rest client instance:

rest.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("127.0.0.1", 8080))

Sequence numbers

If you do multiple http calls they will not be pooled and returned in the order you executed the calls. Any http request will return as soon as it is available. If you want to handle them in sequence, or even discard older results, you can use the Response.seq value which will contain a Long sequence number.

Progress indicator

Tornado FX comes with a HTTP ProgressIndicator View. This view can be embedded in your application and will show you information about ongoing REST calls. Embed the RestProgressBar into a ToolBar or any other parent container:

toolbar.add(RestProgressBar::class)

Next: Layout Debugger