This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
A simple application to demonstrate implementing KFSM with the classic Turnstile FSM.
./gradlew bootRun
When designing a FSM careful consideration of how the state is externalised is important. It is advisable that the type used to represent the state in the definition also be the type persisted by the domain entity or at least providing a consistent bi-directional conversion.
In the Turnstile example we are only concerned with 2 states so a Boolean
is an acceptible standin.
The FSM definition uses TurnstileContext
to represent the operations and determine the current state.
The domain class TurnstileData
is a representation of the state of the Turnstile for external use and is return from the event.
data class TurnstileData(
val id: Long,
val locked: Boolean,
val message: String
) {
val currentState: TurnstileState
get() = if (locked) LOCKED else UNLOCKED
}
interface TurnstileContext {
val currentState: TurnstileState
fun alarm(): TurnstileData?
fun lock(): TurnstileData?
fun unlock(): TurnstileData?
fun returnCoin(): TurnstileData?
}
We extend the TurnstileContext
to create a persistent context using
Spring Data JDBC
@ResponseStatus(CONFLICT)
class TurnstileAlarmException(message: String) : Exception(message)
@ResponseStatus(NOT_FOUND)
class TurnstileInfoNotFound(message: String) : Exception(message)
@Table("TURNSTILE_INFO")
@Entity
data class TurnstileEntity(
@Id var id: Long? = null,
@Column("LOCKED_B")
val locked: Boolean = true,
@Column("MESSAGE_S")
val message: String = ""
) {
fun update(locked: Boolean? = null, message: String? = null) =
copy(locked = locked ?: this.locked, message = message ?: "")
fun toInfo() = TurnstileData(id!!, locked, message)
}
interface TurnstileRepository : PagingAndSortingRepository<TurnstileEntity, Long>
class TurnstilePersistentContext(private val turnstileRepository: TurnstileRepository, id: Long) : TurnstileContext {
val turnstileInfo: TurnstileEntity
init {
turnstileInfo =
turnstileRepository.findById(id).orElseThrow { TurnstileInfoNotFound("TurnstileEntity $id not found") }
}
override val currentState: TurnstileState
get() = if (turnstileInfo.locked) LOCKED else UNLOCKED
override fun alarm(): TurnstileData? {
throw TurnstileAlarmException("Alarm")
}
override fun lock(): TurnstileData? {
return turnstileRepository.save(turnstileInfo.update(locked = true)).toInfo()
}
override fun unlock(): TurnstileData? {
return turnstileRepository.save(turnstileInfo.update(locked = false)).toInfo()
}
override fun returnCoin(): TurnstileData? {
return turnstileRepository.save(turnstileInfo.update(message = "Coin returned")).toInfo()
}
}
A Web UI is available at kfsm-hateoas-client
This is embedded in src/main/resources/static
This section describes the various operations.
We can obtain a list of top level API operations.
http http://localhost:8080/api
Should return:
{
"_links": {
"create": {
"href": "http://localhost:8080/api/turnstile/"
},
"list": {
"href": "http://localhost:8080/api/turnstile/"
},
"self": {
"href": "http://localhost:8080/api?requestedVersion=1"
}
},
"apiName": "Turnstile",
"apiVersion": 1
}
Only list
and create
are available at the top-level.
All other operations apply to specific turnstile resources, and those resources
contain the embedded links for performing the options.
We start by creating some turnstile entities:
http POST http://localhost:8080/api/turnstile/
Should return:
{
"_links": {
"coin": {
"href": "http://localhost:8080/api/turnstile/1/coin"
},
"self": {
"href": "http://localhost:8080/api/turnstile/1"
}
},
"currentState": "LOCKED",
"id": 1,
"locked": true,
"message": ""
}
After calling is 5 times we can list the turnstiles
http http://localhost:8080/api/turnstile/
Should return:
{
"_embedded": {
"turnstiles": [
{
"id": 1,
"locked": true,
"message": "",
"currentState": "LOCKED",
"_links": {
"self": {
"href": "http://localhost:8080/api/turnstile/1"
},
"delete": {
"href": "http://localhost:8080/api/turnstile/1"
},
"coin": {
"href": "http://localhost:8080/api/turnstile/1/coin"
}
}
},
{
"id": 2,
"locked": true,
"message": "",
"currentState": "LOCKED",
"_links": {
"self": {
"href": "http://localhost:8080/api/turnstile/2"
},
"delete": {
"href": "http://localhost:8080/api/turnstile/2"
},
"coin": {
"href": "http://localhost:8080/api/turnstile/2/coin"
}
}
},
{
"id": 3,
"locked": true,
"message": "",
"currentState": "LOCKED",
"_links": {
"self": {
"href": "http://localhost:8080/api/turnstile/3"
},
"delete": {
"href": "http://localhost:8080/api/turnstile/3"
},
"coin": {
"href": "http://localhost:8080/api/turnstile/3/coin"
}
}
},
{
"id": 4,
"locked": true,
"message": "",
"currentState": "LOCKED",
"_links": {
"self": {
"href": "http://localhost:8080/api/turnstile/4"
},
"delete": {
"href": "http://localhost:8080/api/turnstile/4"
},
"coin": {
"href": "http://localhost:8080/api/turnstile/4/coin"
}
}
},
{
"id": 5,
"locked": true,
"message": "",
"currentState": "LOCKED",
"_links": {
"self": {
"href": "http://localhost:8080/api/turnstile/5"
},
"delete": {
"href": "http://localhost:8080/api/turnstile/5"
},
"coin": {
"href": "http://localhost:8080/api/turnstile/5/coin"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/turnstile/?page=0&size=10"
}
},
"page": {
"size": 10,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
This uses the self
link from the resource.
http http://localhost:8080/api/turnstile/1
Returns:
{
"_links": {
"coin": {
"href": "http://localhost:8080/api/turnstile/1/coin"
},
"self": {
"href": "http://localhost:8080/api/turnstile/1"
}
},
"id": 1,
"locked": true,
"message": ""
}
http POST http://localhost:8080/api/turnstile/1/coin
Should return:
{
"_links": {
"coin": {
"href": "http://localhost:8080/api/turnstile/1/coin"
},
"pass": {
"href": "http://localhost:8080/api/turnstile/1/pass"
},
"self": {
"href": "http://localhost:8080/api/turnstile/1"
}
},
"id": 1,
"locked": false,
"message": ""
}
http POST http://localhost:8080/api/turnstile/1/pass
Should return:
{
"_links": {
"coin": {
"href": "http://localhost:8080/api/turnstile/1/coin"
},
"self": {
"href": "http://localhost:8080/api/turnstile/1"
}
},
"id": 1,
"locked": true,
"message": ""
}
http POST http://localhost:8080/api/turnstile/1/pass
The system throws TurnstileAlarmException
which results in 409 - Conflict
{
"error": "Conflict",
"message": "Alarm",
"path": "/1/pass",
"status": 409,
"timestamp": "2020-01-30T21:06:05.491+0000"
}
http POST http://localhost:8080/api/turnstile/1/coin
Should return:
{
"_links": {
"coin": {
"href": "http://localhost:8080/api/turnstile/1/coin"
},
"pass": {
"href": "http://localhost:8080/api/turnstile/1/pass"
},
"self": {
"href": "http://localhost:8080/api/turnstile/1"
}
},
"id": 1,
"locked": false,
"message": "Coin returned"
}
To learn more about visualization visit kfsm-viz and kfsm-viz-plugin