Skip to content

Latest commit

 

History

History
338 lines (268 loc) · 11.2 KB

README.md

File metadata and controls

338 lines (268 loc) · 11.2 KB

A "json to statemachine" library

This library can take a json-encoded state machine and create a runnable state machine from it. The supported features and the used terminology are modeled along the UML state chart diagram (which itself is based on finite automatas).

It allows not only to track a state but also to execute custom commands while the transitions are traversed.

Features

The describable state machines consist of

  • a set of states identified by names
  • one of them being the initialState
  • a set of finalStates ("accepting states"); the state machine will report if one of them is reached; processing can nevertheless continue
  • transitions defining when to change from state to state
  • a set of triggers; they are matched against incoming events (the "input alphabet") and might trigger traversal of a transition
  • a state can have one or more onEntry behaviors attached. These are commands (code given by the user of the library) that are executed when the state is entered via a transition.
  • a state can also have one or more onExit behaviors attached which are executed when leaving the state
  • a transition can have one or more effects behaviors attached which are executed when traversing the transition
  • the given input is heavily checked and validated. Exceptions contain "useful" error messages (so the author of the json will know what to fix)
  • User provided code (the "behaviors") is wrapped at runtime. Thrown exceptions will be logged and don't affect the state machine or other behaviors.

Maven

    <dependency>
        <groupId>com.github.aytchell</groupId>
        <artifactId>jsonfsm</artifactId>
        <version>2.0.0</version>
    </dependency>

Example

The following state chart diagram has two states which are "connected" by one transmission. The state 'Start' is the initial state; the state machine will start "from within" this state. 'Stop' is a final state. If the transmission is traversed there are several behaviors executed.

The way this lib allows a user to define commands can be described as "execute this command string on the device with id 5". What exactly "device 5" is and how it handles a given command string is completely up to the user's code.

A small state chart with states 'Start' and 'Stop' and several behaviors involved

The json-encoding for this state chart is shown in the following block.

Triggers are identified in the json via an eventSourceId and an eventPayload.

Behaviors are described in a way so that the json part only contains a string representation of the command (commandString) and the ID of the device to execute it (deviceId). The user of this lib has to provide the java implementation.

{
  "initialState" : "Start",
  "triggers" : [
    {
      "name" : "move",
      "eventSourceId" : 3,
      "eventPayload" : "move ya"
    }
  ],
  "states" : [
    {
      "name" : "Start",
      "onExit" : [
        {
          "deviceId" : 1,
          "commandString" : "Exiting1"
        }, {
          "deviceId" : 5,
          "commandString" : "Exiting2"
        }
      ],
      "transitions" : [
        {
          "triggerName" : "move",
          "targetState" : "Stop",
          "effects" : [
            {
              "deviceId" : 1,
              "commandString" : "Moving1"
            },{
              "deviceId" : 5,
              "commandString" : "Moving2"
            } ]
        } ]
    },
    {
      "name" : "Stop",
      "onEntry" : [
        {
          "deviceId" : 1,
          "commandString" : "Entering1"
        }, {
          "deviceId" : 5,
          "commandString" : "Entering2"
        } ]
    } ],
  "finalStates" : [ "Stop" ]
}

Basic usage

The API of this lib is quite slim. Given a string with a json encoded state machine the caller first has to start the parser:

final String jsonStateMachine = "{ ... }";

final StateMachineCompiler compiler = StateMachineParser.parse(jsonStateMachine);

The parser will also validate the given json string. If something is wrong, it will throw a ValidationException which will contain detailed information what's wrong.

Parsing the json will give you a StateMachineCompiler. This compiler knows which eventSourceIds might send events, and it knows, the deviceIds of the devices where commandStrings will be executed.

Set<Integer> eventSourceIds = compiler.getAcceptedEventSources();

Set<Integer> deviceIds = compiler.getRequiredDevices();

The eventSourceIds will be important later during execution of the state machine. The deviceIds are important to actually "compile" the state machine. For each given deviceId the caller has to provide an implementation of the interface DeviceCommandCompiler.

public interface DeviceCommandCompiler {
    DeviceCommand compile(String commandString) throws Exception;
}

for a given commandString this DeviceCommandCompiler must be able to create an implementation of the interface DeviceCommand.

public interface DeviceCommand {
    void execute();
}

How this is done is completely up to the specific DeviceCommandCompiler. If DeviceCommandCompilers are available for all the requested deviceIds it's possible to compile the state machine:

Map<Integer, DeviceCommandCompiler> commandCompilers = Map.of(...);

final StateMachine machine = compiler.compileStateMachine(commandCompilers);

During compilation a CompilationException might be thrown (e.g. if compiling a command throws or if a command compiler is missing). This exception will contain information to figure out, what's wrong.

If compilation succeeds you'll have a StateMachine where you basically only have to inject events and the machine takes care of the rest. Only events originating from those eventSourceIds indicated by the parser will trigger an action. All others will be ignored (a log message will be emitted).

boolean isFinal = machine.injectEvent(3, "move ya");

The return value will tell you whether you've reached a final state. (Of course it's possible to leave a final state if you inject another event.) Beneath informing the caller about the final state it does not have any effects on the state machine.

State-machine json

The top-level structure of the accepted json format is shown in the block below. You can see four of the five elements of a finite automata. The transitions are placed at their respective start state.

{
  "initialState" : "...",
  "triggers" : [ ... ],
  "states" : [ ... ],
  "finalStates" : [ ... ]
}

initialState

The initialState is mandatory. Its value is a string denoting the name of the state where execution of the state machine starts. Note that the machine starts inside this state. If there are onEntry behaviors they will not get executed on startup.

triggers

The triggers entry is mandatory. Its value is an array of trigger descriptions which must contain at least one entry. Each description of a trigger has this format:

{
  "name" : "...",
  "eventSourceId" : int,
  "eventPayload" : "..."
}

A trigger describes an event coming from a specific source (identified via an ID) and having a known payload. Every time the user's code calls injectEvent(int eventSourceId, String eventPayload) the state machine tries to find a matching trigger. If one is found, and the current state has a transition with states[].transitions[].triggerName being equal to the trigger's name then this transition will be traversed.

The name of the trigger is a string; there must not be two triggers with the same name.

The eventSourceId (an integer value) defines from which input source this event is expected to arrive. The eventPayload (a string) describes the exact event coming from that source. It is not allowed to have two triggers with identical content (eventSourceId plus eventPayload).

Of course eventPayload might be a self-chosen description in case your event is not a string. In this case your event source needs to have a "translation layer" which emits events with string payloads.

states

Probably the biggest and most important part of the state machine description. The states entry is mandatory. Its value is an array of state descriptions:

{
  "name" : "...",
  "onEntry" : [ ... ],
  "onExit" : [ ... ],
  "transitions" : [ ... ]
}

The name entry (a string) is mandatory. It denotes the name of this state. Each state must have a unique name.

entry/exit behaviors

The entries onEntry and onExit are optional. If present, they contain arrays of behaviors to be executed when the state is entered or left (via transition). This happens even if the target state of a transition is the state itself ("self-transition").

A behavior is defined like this

{
  "deviceId": int,
  "commandString": "..."
}

where deviceId identifies "the device" where a command is executed and commandString describes the actual command. Note that it is called "device" even though it doesn't need to be a piece of hardware. A "device", as used by this library, might also be a REST-call, a logger, a connection to an MQTT broker, a command line, ... whatever you have an adapter for.

transitions

The transitions entry is optional (so you might have states with no way out). If present, it contains an array of transition descriptions.

{
  "triggerName": "...",
  "ignore": boolean,
  "targetState": "...",
  "effects": [ ... ]
}

The triggerName (a string) is mandatory. It will be matched with incoming events and if they equal the transition will be traversed.

The entry ignore says, whether events matching this trigger should be ignored. If ignore is not given its value is treated as false. If this entry is given and its value is true then the entries targetState and effects are ignored, too (so they become optional). This flag is mostly useful for expressing intent: as a documentation for others as well as for the state machine during execution. If an event arrives in a state with no matching trigger the lib will log a warning message. Ignored transitions can be seen as (internal) self-transitions without effects and disabled entry/exit behaviors.

The targetState entry is mandatory (except if ignore is true). It contains the name of the state where the machine ends up if the transition is traversed.

The effects entry is optional. If given, it contains an array of behaviors which will be executed during traversal of the transition. See above at the entry/exit behaviors for the structure.

finalStates

The finalStates entry is optional. If present its value is an array of strings where each of them is the name of a state (if no state with the given name exist the parser will throw an exception).

If during execution the state machine reaches one of these states, the method StateMachine.injectEvent() will return true. For non-final states it will return false.

(The name of the current state can be queried via StateMachine.getCurrentState(). There's also a method StateMachine.isCurrentStateFinal().)

License

Apache 2.0 License

Created and maintained by Hannes Lerchl

Feel free to send in pull requests. Please also add unit tests and adapt the README if appropriate.