Kotlin and WebFlux in Space
Introduction
Wednesday 9th December 2020 will see the official release of JetBrains Space. Space is an environment for teams that aims to provide a ‘one stop shop’ for version control, blogging, chats, code review etc…
At Instil we were early adopters of Space. Our training team was presenting at KotlinConf in Copenhagen and attended the launch event. Thereafter we’ve used it to great effect, initially to manage codebases for new courses internally and then as part of our conversion to virtual training during the coronavirus pandemic.
Space has its own HTTP API, which can be used to perform CRUD operations on the various resources you find within an instance. These include Profiles, Projects, Publications, Chats and Calendars. In order to use these endpoints you would typically create and register your own application, but to get you started a Space Admin can use the built-in HTTP API Playground.
Here’s an example of using the API Playground to retrieve the Profiles of all registered users within an instance.
The Task to be Accomplished
To celebrate the official launch of Space, let me show you how to create and register an application that can access the same endpoints externally. Keeping with the theme of modern technologies we’ll use the Kotlin language and Reactive Spring (via Project Reactor and WebFlux).
The completed code is available in this BitBucket Git Repository, but here I will walk you through the setup and implementation from scratch.
Making a Start
The first stage is to create a fresh project with the Spring Initializr. We select Kotlin as our language and Gradle as our build tool.
In the dependencies window we select Spring Reactive Web to acquire the WebClient. We will use this to access the Space API. But before we can do this we will need to log in via OAuth, so we also include the OAuth2 Client.
When we hit the Generate button the browser will download a ZIP file, containing a pre-configured build file and some sample code. We can unpack this archive and open the resulting folder in IntelliJ.
In the screenshot below I’ve modified the standard code produced by the Initializr to create a basic Console Application. The SpaceApp class contains a single bean provider method, which in turns builds and returns a CommandLineRunner. Spring will automatically detect and run this to produce our “Hello Spring Boot” output.
Because we have included the WebFlux framework, Spring would automatically start an instance of the Netty server. We have to explicitly disable this in the application.properties configuration file (as shown above).
Registering Our Application
We now have our basic application up and running. Before we can go any further we need to register it in Space.
In the administration menu of our Space instance we elect to create a new application and give it a name…
We then need to specify which OAuth Flow we wish to use to authenticate ourselves. As our application will not need to support login via third parties we can select the Client Credentials Flow.
Note that Space has assigned our application a Client ID and Client Secret. These are what we will need in our Kotlin / Spring code to access the HTTP API. Normally these would be secured, but for the purposes of the demo we will place them directly in the properties file.
Finally we need to explicitly grant the appropriate permissions to the application. The Space permissions model is very fine-grained, in order to enable you to integrate external systems without compromising security.
Adding Registration Details to the Application
Back in IntelliJ we need to add the settings we have just discovered to the application.properties file. In the listing below I have replaced the actual company name with megacorp and put in dummy values for the Client ID (1234)and Secret (5678).
space.base.url=https://megacorp.jetbrains.space
space.api.url=${space.base.url}/api/http
spring.security.oauth2.client.registration.HELLO.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.HELLO.client-id=1234
spring.security.oauth2.client.registration.HELLO.client-secret=5678
spring.security.oauth2.client.registration.HELLO.scope=**
spring.security.oauth2.client.provider.HELLO.token-uri=${space.base.url}/oauth/token
Note that the names of the properties are defined by the Spring OAuth2 module. But the penultimate part of each property name is an identifier which we can use in our code, in order to fetch a group of related settings. In this case the identifier is HELLO.
Finishing the Code
The final stage in the process is the one we all love. The coding :-)
We begin by creating Kotlin types to represent the data that will be returned by Space. Kotlin’s succinct syntax makes this much shorter than the equivalent code in older OO languages.
class Name(var firstName: String,
var lastName: String) {
override fun toString() = "$firstName $lastName"
}
class Profile(var id: String,
var username: String,
var name: Name) {
override fun toString() = "$id is $name with username $username"
}
class AllProfilesResponse(var next: String,
var totalCount: String,
var data: List<Profile>)
The next part is the trickiest. We need to create a Bean Provider method that will manufacture a WebClient object for us. Normally this would be trivial, but here we must use the OAuth2 support in Spring to ensure that the client will automatically log us in.
Here’s how it’s done. I’ve put the method in a separate Configuration class for clarity:
@Configuration
class SpaceAppConfig {
@Bean
fun oauthWebClient(
clientRegistrations: ReactiveClientRegistrationRepository,
@Value("\${space.api.url}") baseUrl: String
): WebClient {
val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations) val manager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService) val oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction(manager)
oauth.setDefaultClientRegistrationId("HELLO")
return WebClient.builder()
.filter(oauth)
.baseUrl(baseUrl)
.build()
}
}
Note that:
- We use the Value annotation to pass in the URL of the Space instance defined in the application.properties file. Thereby avoiding duplication.
- We use the HELLO identifier to specify which settings we want the OAuth2 module to use from the configuration file. In a real solution we might need to authenticate with many remote services, so we need a way to support arbitrary groups of properties.
Now that we have a WebClient object that can communicate with the Space instance, we can write a method that retrieves all the available Profiles:
fun retrieveSpaceProfiles(webClient: WebClient) = webClient
.get()
.uri("/team-directory/profiles")
.retrieve()
.bodyToFlux(AllProfilesResponse::class.java)
.flatMap { Flux.fromIterable(it.data) }
As you can see this code makes use of Springs reactive streams, better known as Flux’s. The WebClient type is geared towards a future (no pun intended) where all API’s are reactive. This has been widely anticipated, but is not yet realised. Specifically the WebClient assumes that the server will want to send us items incrementally, via a protocol such as SSE, WebSockets or RSocket.
As this is not the case here we need to:
- Send a standard GET request to the server
- Marshal the data returned to an AllProfilesResponse
- Convert the list of profiles inside this into a Flux
Finally we need to extend our console method like so:
@Bean
fun console(webClient: WebClient) = CommandLineRunner {
val header = Mono.just("Details of all the profiles:")
val profiles = retrieveSpaceProfiles(webClient).map { "\t $it" }
val footer = Mono.just("All done - hit return to exit")
val values = header
.concatWith(profiles)
.concatWith(footer)
values.subscribe(::println)
//Because this is a console app we need to keep the
// main thread alive. Otherwise Spring would exit.
readLine()
}
As you can see the bean provider method now takes the WebClient as a parameter. This will be created and passed in via Dependency Injection. We need to remember that the activities within the WebClient will be happening asynchronously to the main thread. So having entered the world of streams it’s best to keep going.
We create a single item stream (aka. a Mono) for the first and last messages we want to send and then concatenate these with the stream of profiles. The combined stream can then be subscribed to. We only need to specify the event handler for completed values. In the case of our simple demo that’s just println.
Because this is a console application, and the Spring ApplicationContext is being managed on the main thread, we need to keep that thread running till all the results are displayed. Obviously this would not be the case if this code was running in a WebFlux Controller executing within Netty.
Here’s the finished code, running against a sample Spring instance:
Conclusions
Hopefully this tutorial has shown you that it’s not overly complicated to log into, and remotely manage, a Space instance. This is especially true when we use Kotlin to remove unnecessary boilerplate and simplify reactive plumbing. In a future tutorial I’ll show how we use the API at Instil to configure custom Space instances for courses, workshops and hackathons.