Kotlin and Spring Boot: Hypermedia Driven Web Service

Learn about HATEOAS, build a state machine to model an article review workflow, use Spring-HATEOAS and see how hypermedia clients adapt. By Prashant Barahi.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Handling Events

To handle the events, open ArticleService.kt and look at the following method:

fun handleEvent(articleId: Long, event: ArticleEvent) {
  val article = repository.findById(articleId).orElseThrow()
  // 1
  val stateMachineFactory = stateMachineFactoryProvider
    .getDefaultStateMachineFactory<ArticleState, ArticleEvent>()
  // 2
  val stateMachine = stateMachineFactory
    .buildFromHistory(article.getPastEvents())

  // 3
  stateMachine.setOnTransitionListener(object :
    OnStateTransitionListener<ArticleState, ArticleEvent> {
    override fun onTransition(
      prevState: ArticleState,
      event: ArticleEvent,
      nextState: ArticleState
    ) {
      article.state = nextState
      article.consumeEvent(event)
    }
  })
  // 4
  val eventResult = stateMachine.sendEvent(event)
  if (!eventResult) {
    throw StaleStateException("Event ${event.alias} could not be accepted.")
  }
  repository.save(article)
}

Here’s what’s going on:

  1. You get the default StateMachineFactory, which is the one whose transitions you reconfigured before.
  2. On this factory, you could call create() to get a fresh instance of the 3LRW StateMachine. But in this case, you need to restore a StateMachine to the article’s current state by calling buildFromHistory() and passing in the list of events it must consume to reach that state. This is called Event Sourcing.
  3. Whenever a transition happens, you update the state of the article and store the corresponding event.
  4. You call sendEvent() to initiate a state transition. All the events that have been consumed successfully get persisted to the database (and are used for Event Sourcing).

Take a few moments to explore this setup. In the next section, you’ll learn about HATEOAS.

Understanding Hypermedia Responses

Consider a hypothetical endpoint that serves articles. Sending a GET request to /articles/1 should return an article with ID of 1.

{
  "id": 1,
  "title": "Heading 1",
  "body": "Lorem ipsum.",
  "status": "DRAFT"
}

It’s difficult to know what actions are allowed on a resource, just from the response above. However, a hypermedia response includes actions that are currently available as well. The server crafts them using the current snapshot of the resource.

So what does this “hypermedia response” look like? A GET /articles/1 but on a hypermedia-based server returns this:

{
  "id": 1,
  "title": "Heading 1",
  "body": "Lorem ipsum.",
  "status": "DRAFT",
  "_links": {
    "self": {
      "href": "http://localhost:8080/articles/1"
    },
    "update": {
      "href": "http://localhost:8080/articles/1"
    }
  }
}

Through this response, a client can determine what operations are allowed on a resource. The presence of _links.update implies it’s possible to update that resource and use that information to, for example, display an Update button to the users.

Next, you’ll learn to build these hypermedia links.

Supporting Hypermedia in Spring Boot

The sample project uses the Spring-HATEOAS library to construct the hypermedia response. You’ll notice the following dependency in the build.gradle.kts:

implementation("org.springframework.boot:spring-boot-starter-hateoas")

This library provides static methods under WebMvcLinkBuilder to introspect methods. It also has classes like RepresentationModel, CollectionModel and so on to model the hypermedia response.

You enable the support for hypermedia by annotating the Application using @EnableHypermediaSupport (the project is already annotated with it):

import org.springframework.hateoas.config.EnableHypermediaSupport

@SpringBootApplication
@EnableHypermediaSupport(type = [
  EnableHypermediaSupport.HypermediaType.HAL,
  EnableHypermediaSupport.HypermediaType.HAL_FORMS,
])
class Application

Later, you’ll learn what these HypermediaType.HAL and HypermediaType.HAL_FORMS are. For now, go to ArticleResource.kt, where you’ll find ArticleResource inheriting from RepresentationModel. ArticleController exposes the /articles endpoint that returns an ArticleResource or a variation of it.

Finally, go to the most important class: ArticleAssembler.kt. Here, you craft the hypermedia response the client receives. Its outline looks like this:


@Component
class ArticleAssembler 
@Autowired constructor(
  private val stateMachineFactoryProvider: StateMachineFactoryProvider
) : RepresentationModelAssembler<ArticleEntity, ArticleResource> {
  // ...
}

The ArticleAssembler translates the database entity ArticleEntity to the ArticleResource.

Build and run the server and execute this curl command:

curl http://localhost:8080/articles -H "Accept:application/hal+json" 

And fetch the article with ID 1 using:

curl http://localhost:8080/articles/1 -H "Accept:application/hal+json" 

The responses you get aren’t any different than the non-hypermedia-based responses. Next, you’ll learn what this application/hal+json is!

Media Types

In the curl commands you executed before, you sent a strange looking Accept header: application/hal+json. This is the registered media type identifier for JSON HAL (JSON Hypertext Application Language). HAL is the simplest and most widely adopted hypermedia media type.

Another media type called HAL-FORMS provides information on HTTP methods, message content-type and request parameters to use when making a request to the server. Its associated media type identifier is application/prs.hal-forms+json.

Other media types are available for hypermedia as well, and the “On choosing a hypermedia type for your API” blog does a great job of comparing them.

The React client is built with support for a HAL-FORMS-based response. Thus, it sends application/prs.hal-forms+json as an Accept header on every request.

Note: This project uses HAL-FORMS because of its structure that can be used to specify actions on a resource, and not for its runtime forms capability. Another alternative for this would be Siren.

Now that you’re familiar with media types and identifiers, you’ll learn to enrich the hypermedia response in the upcoming sections.

Building the “Self” Link

The _links field is a reserved property. It contains one or more link objects. One of them is the self link, with which a client can get the full representation of the resource.

Adding a self link is straightforward. Go to the ArticleAssembler.kt. There, you’ll find buildSelfLink():

fun buildSelfLink(entity: ArticleEntity): Link {
  return linkTo(
    methodOn(ArticleController::class.java)
      .getById(entity.id!!)
  )
  .withSelfRel()
}

Both linkTo() and methodOn() are static methods from org.springframework.hateoas.server.mvc.WebMvcLinkBuilder. The getById() is a method in ArticleController, which is used in building a self link.

Finally, inside toModel(), call this method by replacing the // TODO: Add Link here with the snippet below:

val resourceLink = buildSelfLink(entity)
resource.add(resourceLink)

Again, build and run the project and execute the following command:

curl http://localhost:8080/articles/1 -H "Accept:application/hal+json" 

It returns:

{
  "id": 1,
  "state": "DRAFT",
  "title": "Getting Started with Cucumber",
  "body": "[...]",
  "updatedDate": "2022-05-22T23:43:33.990617",
  "createdDate": "2022-05-22T23:43:33.990576",
  "reviewType": "FOUR_LEVEL_WORKFLOW",
  "_links": {
    "self": {
      "href": "http://localhost:8080/articles/1"
    }
  }
}

As you can see, it now includes the _links.self field.

Before you continue enriching the response, you’ll first learn about affordances. To the next section!

Affordance

Hypermedias are resources and affordances. An article is a resource, and the actions you can perform on that resource are its affordances.

As mentioned in the “hypermedia affordances” blog, affordances are what the resource “offers” and how they offer it.

So updating an article by sending a PUT request to /articles/1 can be one of its affordances. Moreover, they also need to be context-sensitive. If you aren’t allowed to edit or delete an article that’s already published, the affordances that allow for those two actions shouldn’t be available in the response.

Affordances also convey what “actions” are available, and clients can use that information to drive the application state forward. Hence, unlike resources, affordances are represented as verbs, not nouns.

To see these “actions” in action, open ArticleController.kt and check the handleAction():

@PostMapping("/{articleId}/{action}")
fun handleAction(
  @PathVariable articleId: Long,
  @PathVariable action: String
): RepresentationModel<ArticleResource> {
  val event =
    eventMapper.getArticleEvent(action) ?: throw IllegalArgumentException("$action is invalid")
  service.handleEvent(articleId, event)
  return articleAssembler.toModel(service.findById(articleId))
}

ArticleEventMapper translates the “verbified” alias to the corresponding ArticleEvent that the state machine consumes in the handleEvent(). If that event is supported, it transitions into the next state while also updating the article’s state.