Search code examples
springkotlinspring-hateoas

Is it possible to use Spring HATEOAS WebFluxLinkBuilders with Kotlin Coroutines?


I am trying to translate the following reactive code into kotlin coroutines:

  @GetMapping
  fun getAllTodosMono(): Mono<CollectionModel<TodoItem>> =
      repository
        .findAll()
        .collectList()
        .flatMap { mkSelfLinkMono(it) }

  private fun mkSelfLinkMono(list: List<TodoItem>): Mono<CollectionModel<TodoItem>> {
    val method = methodOn(Controller::class.java).getAllTodosMono()
    val selfLink = linkTo(method).withSelfRel().toMono()
    return selfLink.map { CollectionModel.of(list, it) }
  }

Coroutine Version:

  @GetMapping
  suspend fun getAllTodosCoroutine(): CollectionModel<TodoItem> =
      repository
        .findAll()
        .collectList()
        .awaitSingle()
        .let { mkSelfLinkCoroutine(it) }

  private suspend fun mkSelfLinkCoroutine(list: List<TodoItem>): CollectionModel<TodoItem> {
    val method = methodOn(Controller::class.java).getAllTodosCoroutine()
    val selfLink = linkTo(method).withSelfRel().toMono().awaitSingle()
    return CollectionModel.of(list, selfLink)
  }

However, I get a runtime error when trying to run the code.

java.lang.ClassCastException: class org.springframework.hateoas.server.core.LastInvocationAware$$EnhancerBySpringCGLIB$$d8fd0e7e cannot be cast to class org.springframework.hateoas.CollectionModel (org.springframework.hateoas.server.core.LastInvocationAware$$EnhancerBySpringCGLIB$$d8fd0e7e is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @62b177e9; org.springframework.hateoas.CollectionModel is in unnamed module of loader 'app')

I suspect methodOn(...) does not support suspend functions. The only solution that actually works is to build the link by hand instead of using the linkTo(...) function:

  private fun mkSelfLink(list: List<TodoItem>): CollectionModel<TodoItem> {
    return Link
      .of("/api/v1/todos")
      .withSelfRel()
      .let { CollectionModel.of(list, it) }
  }

However, I lose the ability to link to existing endpoints in my REST controller and also the host that is automagically added to the link uri.

Am I missing something?

EDIT: Here is the link to my github repo: https://github.com/enolive/kotlin-coroutines/tree/master/todos-coroutini

If you paste the following code sample into the TodoController replacing the original getTodo(...) method, you can see the failure I described above.

private suspend fun Todo.withSelfLinkByBuilder(): EntityModel<Todo> {
    val method = methodOn(Controller::class.java).getTodo(id!!)
    val selfLink = linkTo(method).withSelfRel().toMono().awaitSingle()
    return EntityModel.of(this, selfLink)
  }
  @GetMapping("{id}")
  suspend fun getTodo(@PathVariable id: ObjectId) =
    repository.findById(id)?.withSelfLinkByBuilder()
      ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

Solution

  • Well, I found a solution, I don't know if is it a satisfactory one, but it works, none of the less.

    By simple chaining the function calls together the runtime appears to work as intended:

    private suspend fun mkSelfLinkCoroutine(list: List<TodoItem>): CollectionModel<TodoItem> {
        val selfLink = linkTo(methodOn(Controller::class.java)
                       .getAllTodosCoroutine())
                       .withSelfRel()
                       .toMono()
                       .awaitSingle()
        return CollectionModel.of(list, selfLink)
      }
    

    This is really strange, but it is what it is.