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 4 of 4 of this article. Click here to view the first page.

Four Level Review Workflow

The tutorial team is growing! With team members from around the world, the management team decided to improve the content’s quality by introducing Editors into 3LRW.

After technical editing, the article goes to the editor. Editors ensure the writing is clean, concise and grammatically correct and follows guidelines before they send it to the final pass editor. This successor to 3LRW is called the Four Level Review Workflow (4LRW):

Four Level Review Workflow state diagram

This is a breaking change for non-hypermedia clients. But can this be implemented without making any changes to the client?

Building the 4LRW State Machine

Before starting with the code, ensure the React app is running. Go to localhost:3000 in your browser and ensure 3LRW is working.

First, you need to define the additional states and events the 4LRW requires. Go to ArticleState.kt and add EDITOR_DONE state to the enum class. And in the ArticleEvent.kt, add EDITOR_APPROVE("editorApprove").

In the ArticleStateMachineBeanConfig, define a new ArticleStateMachineFactory for 4LRW by adding the following method:

@Primary
@Bean(FOUR_LEVEL_REVIEW_STATE_MACHINE)
fun providesFourLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
  val configuration = StateMachineConfigurer.StateBuilder<ArticleState, ArticleEvent>()
    .withStartState(DRAFT)
    .withEndState(PUBLISHED)
    .withStates(
      DRAFT, 
      AUTHOR_SUBMITTED,
      TE_APPROVED,
      EDITOR_DONE,
      PUBLISHED
    )
    .and()
    .withTransitions {

      // Author
      defineTransition(start = DRAFT, trigger = AUTHOR_SUBMIT, end = AUTHOR_SUBMITTED)

      // TE
      defineTransition(start = AUTHOR_SUBMITTED, trigger = TE_APPROVE, end = TE_APPROVED)
      defineTransition(start = AUTHOR_SUBMITTED, trigger = TE_REJECT, end = DRAFT)

      // Editor
      defineTransition(start = TE_APPROVED, trigger = EDITOR_APPROVE, end = EDITOR_DONE)

      // FPE
      defineTransition(start = EDITOR_DONE, trigger = FPE_APPROVE, end = PUBLISHED)
      defineTransition(start = EDITOR_DONE, trigger = FPE_REJECT, end = DRAFT)
    }
  return StateMachineFactory(ReviewType.FOUR_LEVEL_WORKFLOW, configuration)
}

Outside the class, add the following line:

const val FOUR_LEVEL_REVIEW_STATE_MACHINE = "FourLevelReviewStateMachineFactory"

And finally, add FOUR_LEVEL_WORKFLOW in the ReviewType:

FOUR_LEVEL_WORKFLOW {
  override val key = this.name
}

The most important part is annotating the providesFourLevelReviewStateMachineFactory() with @Primary and removing it from the providesThreeLevelReviewStateMachineFactory(). After this, the outline of the ArticleStateMachineBeanConfig becomes:

const val THREE_LEVEL_REVIEW_STATE_MACHINE = "ThreeLevelReviewStateMachineFactory"
const val FOUR_LEVEL_REVIEW_STATE_MACHINE = "FourLevelReviewStateMachineFactory"

@Configuration
class ArticleStateMachineBeanConfig {
  @Bean(THREE_LEVEL_REVIEW_STATE_MACHINE)
  fun providesThreeLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
    // ...
  }

  @Primary 
  @Bean(FOUR_LEVEL_REVIEW_STATE_MACHINE)
  fun providesFourLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
    // ...
  }
}

The StateMachineFactoryProvider‘s getDefaultStateMachineFactory() will now return the 4LRW StateMachineFactory because you annotated it with @Primary.

Check ArticleService‘s save():

fun save(title: String, body: String): ArticleEntity {
  val stateMachineFactory = stateMachineFactoryProvider.getDefaultStateMachineFactory()
  return ArticleEntity
    .create(
      title = title,
      body = body,
      reviewType = stateMachineFactory.identifier as ReviewType
    )
    .let(repository::save)
}

Every newly created article will use 4LRW, because getDefaultStateMachineFactory() will return the 4LRW factory.

Also, look at the handleEvent():

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

As you might have guessed, this code won’t work for existing 3LRW articles because the default factory now follows 4LRW.

Because there is more than one StateMachineFactory now — one for 3LRW and one for 4LRW — you need to choose which StateMachineFactory to use. Do this by passing the review type to StateMachineFactoryProvider‘s getStateMachineFactory().

Replace the code in handleEvent(), where it gets the default factory, to the following:

fun handleEvent(articleId: Long, event: ArticleEvent) {
  // ...
  val stateMachineFactory = stateMachineFactoryProvider
    .getStateMachineFactory<ArticleState, ArticleEvent>(article.reviewType)
  // ...

Also, open ArticleAssembler.kt and replace the code in getAvailableActions() where it gets the default factory to the following:

private fun getAvailableActions(entity: ArticleEntity): List<ArticleEvent> {
  // ...
  val stateMachineFactory = stateMachineFactoryProvider
    .getStateMachineFactory<ArticleState, ArticleEvent>(entity.reviewType)
  // ...

This means the code now is retro-compatible, because getStateMachineFactory<ArticleState, ArticleEvent>(article.reviewType) will return the 3LRW factory for those articles previously saved with the THREE_LEVEL_WORKFLOW review type.

Now, restart the server and go to localhost:3000. Use the form at the bottom of the page to create a new article. Go to its detail page and use the workflow buttons to approve or reject the article. You’ll find that the newly created article uses 4LRW without you having to change any client-side code. Also, try out the 3LRW retro-compatibility! Choose an article created with 3LRW and go through the workflow. It should now follow the 4LRW process.

The client “adjusts” itself to changes in the workflow. This is possible because the hypermedia-based server offers the necessary metadata.

Where to Go From Here?

Click Download Materials at the top or bottom of the tutorial to download the final project.

Good job making it all the way through! You learned to create a hypermedia-based server using Spring-HATEOAS and how it helps lower the coupling between the client and the server. You also saw how the hypermedia-based client adjusted to the changes in the review workflow.

To complete the review workflow, try introducing the illustrator into the workflow. When the editor is done, the illustrator picks that article up and designs the perfect illustration for it before handing it over to the final pass editor. Its state diagram is:

Five Level Review Workflow state diagram

Find its solution in the challenge folder of the materials.

Here are some links for further reading:

If you have questions or comments, please join the forum discussion below!