-
Notifications
You must be signed in to change notification settings - Fork 272
REST Client
Wiki ▸ Documentation ▸ 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.
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"
}
}
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)
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
.
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.
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.")
}
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.
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 concreteengine
implementation that is given the currentRest
instance as argument.
You can override the configured engine
in a Rest
instance at any time.
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))
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.
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