State machines, as they are used today in software projects, are an effective way of developing software for a lot of different applications. They have been used in the embedded systems domain to model reactive systems for decades. Highly-sophisticated tools, like Mathworks Stateflow and YAKINDU Statechart Tools, help embedded systems engineers to model and simulate the behavior of their systems and generate high-quality code out of the statechart models.
In recent times, there has been a trend towards state machines for web applications. Frameworks, like XState and Spring Statemachine, support developers in using finite-state machines for front-end and back-end web development. Wouldn’t it be nice to reuse existing tools with all their modeling and simulation capabilities for modern web frameworks?
This article is a step-by-step guide on how to develop custom code generators for YAKINDU Statechart Tools. All parts of YAKINDU Statecharts that we use throughout this article are open source. We will use Spring Statemachine as the target framework to explain the development of our custom code generator. And we will use Xtend — a flexible and expressive dialect of Java with a lot of features that are helpful for code generator development.
Grab your copy of YAKINDU Statechart Tools here, and let’s start coding!
Let’s have a look at a simple example and how it is written in Spring Statemachine. The turnstile example was taken from the Spring project example repository. It consists of two states, Locked and Unlocked. If the coin event is raised, the turnstile enters the Unlocked state, if the push event is raised, the turnstile goes back to the Locked state.
As you can see in the following code listing, only 29 lines of Java code are required to have a running example of the turnstile in Spring Statemachine.
@Configuration
@EnableStateMachine
public class TurnstileConfig
extends EnumStateMachineConfigurerAdapter<States, Events> {
public static enum States {
LOCKED, UNLOCKED
}
public static enum Events {
COIN, PUSH
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states.withStates().initial(States.LOCKED)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(
StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions.withExternal().source(States.LOCKED).target(States.UNLOCKED)
.event(Events.COIN).and().withExternal().source(States.UNLOCKED)
.target(States.LOCKED).event(Events.PUSH);
}
}
The
TurnstileConfig
class starts with two annotations —@Configuration
and @EnableStatemachine
— that are required for the Spring runtime environment to set up things properly. For more details on how the instantiation with spring works, check out the Spring Statemachine online documentation.The
TurnstileConfig
class extends the EnumStateMachineConfigurerAdapter
framework class. This is a generic type that takes two classes as type arguments — the States
a nd Events
enumerations. These enumerations contain literals for every State and Event within the state machine. There are two configure
methods, which are derived from the base class. They are used to define the States and connect them to each other via Transitions. All States are added to the states’ configuration, and the Locked state is marked to be the initial state of the state machine. The
configure
method adds the Transitions with source and target States and Events, respectively, to the configuration.In the next part, we will set up a new generator project and see how we can generate that code from our example model.
Creating custom code generators is a built-in concept in YAKINDU Statechart Tools. You can develop custom code generators directly within your project workspace. You can choose between Xtend or Java for implementing the code generator templates. Throughout this example, we will use Xtend as the programming language.
Xtend compiles into optimized, readable and pretty-printed Java code. Java and Xtend have a lot in common syntactically and semantically. Xtend is aimed to improve several aspects of Java and is especially useful when developing code generators.
Since the output is plain Java code, it is easily possible to integrate Xtend code with any existing Java library and vice versa.
Xtend is — like Java — a statically-typed language with the ability to infer variable and expression types in most cases. This slims down the code and increases its readability. Features that are useful for the development of code generators are extension methods, lambda expressions, dispatch functions and especially template expressions. Don’t worry — if you are familiar with Java you will learn Xtend in no time.
To set up a new generator project in YAKINDU Statechart Tools, we will use the generator template from our example wizard. So start up YAKINDU Statechart Tools and do the following:
A background task is scheduled that will install some additional components into YAKINDU Statechart Tools. They are required for custom code generator development. Confirm the appearing dialog with Next. Accept the terms and conditions and click Install.
Once the installation is finished, a restart is required. That’s all you have to do to get started! 🎉 🎉
Let us have a look at the generated artifacts and their purpose.
Since Xtend code compiles to Java code, there is a source folder called xtend-gen. It contains only generated resources and should therefore not be added to a version control systems like Git. The src folder contains the source code of our code generator.
YAKINDU Statechart Tools uses a dependency injection framework, Google Guice, to manage dependencies. All references to implementation types are handled in the
CustomGeneratorModule
.
CustomGenerator
is the class containing the actual code generator implementation. In the model folder, you will find the example state machine turnstile.sct and the code generator model turnstile.sgen. The code generator model is used to test our new code generator. When you right-click the turnstile.sgen file and select Generate Statechart Artifacts from the context menu, the generator is executed and creates a new file src-gen/turnstile.txt with the following contents:
The name of the state machine is 'turnstile'
I am a region main region
I am an entry of kind INITIAL
I am a state Locked
I am a state Unlocked
This is the default output of the initial generator. Congratulations, everything is set-up properly! 💪
As previously stated, the code that is responsible for the generated output is located in the
CustomGenerator
class. It is just a simple template that prints out the names of the states contained in the example model into a text file — not very useful right now 🙈. But it is a good start to dive deeper into the details of YAKINDU’s code generator API:class CustomGenerator implements ISGraphGenerator {
override generate(Statechart sm, GeneratorEntry entry, IFileSystemAccess access) {
access.generateFile(sm.name + '.txt', sm.generate.toString());
}
def dispatch String generate(Statechart it) {
'''
The name of the state machine is '«name»'
«FOR Region region : regions»
«region.generate»
«ENDFOR»
'''.toString
}
def dispatch String generate(Region it) {
'''
I am a region «name»
«FOR state : vertices»
«state.generate»
«ENDFOR»
'''
}
def dispatch String generate(State it) {
'''
I am a state «name»
«FOR region : regions»
«region.generate»
«ENDFOR»
'''
}
def dispatch String generate(Entry it) {
'''
I am an entry of kind «it.kind.toString»
'''
}
}
The
CustomGenerator
class implements the ISGraphGenerator
interface, which is the basic interface for all custom code generators. It defines only a single method generate
with three parameters. The first parameter sm is of type Statechart and represents the actual model, turnstile.sct in our case. The following figure gives an overview of the structure of the statechart model.A Statechart contains zero or more Regions. A Region contains zero or more Vertices. A Vertex can be either of type State or of type Pseudostate. A Pseudostate is a state that can not be active — an Entry, Exit, Choice or Synchronization. If a State contains one or more Regions, it is called a Composite State.
The second parameter entry is of type GeneratorEntry and allows access to the code generator features of the turnstile.sgen generator model.
The last parameter, access, is of tye IFileSystemAccess and can be used to write the result to the file system. It is a file system abstraction that is preconfigured with the target folders specified in the generator model. To create a new source file, the
generateFile
method can be used. It takes two arguments, the first one is the name of the file and the second one is the content. As the latter, we pass in the result of the call sm.generate.toString(). We use Xtends extension methods here. The method
generate
creates the content that is written to the file. We use Xtends template expressions for readable string concatenation. Such a template expression is surrounded by triple single quotes(‘ ‘ ‘). Each character that is placed in between a pair of triple single quotes is taken as plain text, except for Xtend expressions like «something». Xtend expressions are surrounded by so-called guillemets. They are evaluated at runtime. In our example, the result of the «name» expression will be Turnstile. Template expressions even allow control structures like FOR loops or IF statements.
We iterate over every Region and — again — call the generate method. Xtends dispatch keyword selects the suitable generate method depending on the runtime type of the argument.
Now that you are familiar with the basic language features lets have a look at how we can adopt this generic template to produce actual code for Spring Statemachine. Here is the example code that can handle basic features like States, Regions, Events, and Entries.
/**
* (c) 2019 by Andreas Muelder
*/
package org.yakindu.sct.generator.spring
import org.eclipse.xtext.generator.IFileSystemAccess
import org.yakindu.sct.generator.core.ISGraphGenerator
import org.yakindu.sct.model.sgen.GeneratorEntry
import org.yakindu.sct.model.sgraph.Entry
import org.yakindu.sct.model.sgraph.State
import org.yakindu.sct.model.sgraph.Statechart
class SpringGenerator implements ISGraphGenerator {
override generate(Statechart statechart, GeneratorEntry entry, IFileSystemAccess access) {
access.generateFile(statechart.name + "/" + statechart.name.toFirstUpper + 'Config.java', statechart.code);
}
def code(Statechart statechart) {
'''
package «statechart.name»;
«statechart.imports»
@Configuration
@EnableStateMachine
public class «statechart.name.toFirstUpper»Config
extends EnumStateMachineConfigurerAdapter<States, Events> {
public static enum States {
«FOR state : statechart.regions.map[vertices].flatten.filter(State) SEPARATOR ','»
«state.name.toUpperCase»
«ENDFOR»
}
public static enum Events {
«FOR event : statechart.scopes.map[events].flatten SEPARATOR ','»
«event.name.toUpperCase»
«ENDFOR»
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.«statechart.initialState»)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
«FOR transition : statechart.regions.head.vertices.filter(State).map[outgoingTransitions].flatten BEFORE 'transitions' SEPARATOR '.and()' AFTER ';'»
.withExternal()
.source(States.«transition.source.name.toUpperCase»)
.target(States.«transition.target.name.toUpperCase»)
.event(Events.«transition.specification.toUpperCase»)
«ENDFOR»
}
}
'''
}
def protected initialState(Statechart it) {
regions.head.vertices.filter(Entry).head.outgoingTransitions.head.target.name.toUpperCase
}
def protected imports(Statechart it) '''
import java.util.EnumSet;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import «name».«name.toFirstUpper»Config.*;
'''
}
The
code
method takes a Statechart as an argument and returns a template expression. Please note: Everything that is not written between « and » is taken as a plain string. The For loop iterates over all States that are contained in the Statechart and generate an Enumeration Literal for each State. The same is done for all Events.The first
configure
method sets the initial state of the Statechart and adds all the States from the generated Enumeration above. The second configure
method uses a For-expression to iterate over all Transitions. Every Transition is connected to its source and target State and the triggering Event.This is everything you need to generate Spring Statemachine code for simple state machines. The full source code is available at GitHub. Do not forget to open a pull request if you’d like to provide additional features! 😃