Java code generator

This chapter describes the required steps for generating Java code with YAKINDU Statechart Tools. Furthermore, all components of the generated code will be described in detail and each configurable generator feature will be explained.


Table of content:

Statechart example model

We will use the following example to explain the code generation, the generated code and the integration with client code. The example model describes a light switch with two states and the following behavior:

  • The on_button event turns the light on and, upon repeated pushing, turns the brightness higher until the latter’s maximum is reached.
  • The off_button event turns the light off.
  • When the light is on, it automatically turns off after 30 seconds.
  • Whenever a state is entered, an outgoing event is raised.

The ‚Java Code Generation’ example can be found in the example wizard:
File -> New -> Example... -> YAKINDU Statechart Examples -> Getting Started – Code Generation -> Java Code Generation

Statechart example model

Statechart example model

Generating Java code


Generating Java code from a statechart requires a generator file (.sgen). It must at least specify the yakindu::java generator, reference a statechart and define the targetProject and targetFolder in the Outlet feature. By specifying these attributes, Java state machine code can be generated.

Example:

GeneratorModel for yakindu::java {

	statechart LightSwitch {

		feature Outlet {
			targetProject = "org.yakindu.sct.examples.codegen.java"
			targetFolder = "src-gen"
		}
	}
}

You can create a generator model with the YAKINDU Statechart generator model wizard by selecting File → New → Code generator model.

The code generation is performed automatically whenever the statechart or the generator file is modified. See also chapter Running a generator for more information.

Generated code files

Generated code files can be categorized into base and library files.

The base source file is the implementation of the state machine model as a Java class. It is generated into the folder defined by the targetFolder parameter. Its package is composed of the basePackage parameter and the statechart’s namespace. The file name is derived from the statechart name, but can be overridden by the typeName parameter.

  • LightSwitch.java: Contains the implementation class of the state machine. It always implements the IStatemachine interface, and based on the used features also ICycleBased and/or ITimed.

Library files are independent of the concrete state machine model. They are generated into the libraryTargetFolder , or if that one is not defined, into the targetFolder . Their package is com.yakindu.core if not otherwise defined by the libraryPackage parameter.

  • IStatemachine.java: Contains the interface IStatemachine .
  • ICycleBased.java: Contains the interface ICycleBased . It is only generated if the statechart is cycle-based.
  • ITimed.java Contains the interface ITimed . It is only generated if the statechart uses timed event triggers, like every or after clauses.
  • ITimerService.h: Contains the interface ITimerService. It is only generated if the statechart uses timed event triggers, like every or after clauses.
  • TimerService.java: A default timer service implementation. It is only generated if enabled in the generator model.
  • ITracingListener.java: Contains the interface ITracingListener . It is only generated if tracing is enabled in the generator model.
  • Observable.java: Contains the Observable class used for the observer mechanism to react on outgoing events. It is generated only if it is required.
  • Observer.java: Contains the Observer class used for the observer mechanism to react on outgoing events. It is generated only if it is required.

The interface IStatemachine

Each generated state machine implements the IStatemachine interface. The IStatemachine interface describes the API to enter and exit a state machine, as well as to check if the machine is active or final:

package com.yakindu.core;

/**
 * Basic interface for state machines.
 */
public interface IStatemachine {

	/**
	 * Enters the state machine. Sets the state machine into a defined state.
	 */
	public void enter();

	/**
	 * Exits the state machine. Leaves the state machine with a defined state.
	 */
	public void exit();

	/**
	 * Checks whether the state machine is active. 
	 * A state machine is active if it has been entered. It is inactive if it has not been entered at all or if it has been exited.
	 */
	public boolean isActive();

	/**
	 * Checks whether all active states are final. 
	 * If there are no active states then the state machine is considered being incative. In this case this method returns <code>false</code>.
	 */
	public boolean isFinal();
}

The interface defines the following methods:

  • The enter() method must be called to enter the state machine. It brings the state machine to a well-defined state.
  • The exit() method is used to leave a state machine statefully. If for example a history state is used in one of the top regions, the last active state is stored and the state machine is left via exit(). Re-entering it via enter() continues to work with the saved state.
  • The isActive() method checks whether the state machine is active, i.e. at least one state of the machine is active. A state machine is active if it has been entered. It is inactive if it has not been entered at all or if it has been exited.
  • The isFinal() method checks whether the all active states are final. If there are no active states then the state machine is considered being inactive. In this case this method returns false.

The interface ICycleBased

In addition, state machines that use the cycle-based execution scheme implement the ICycleBased interface which adds the runCycle() method to the API.

package com.yakindu.core;

/**
 * Interface for cycle-based state machines.
 */
public interface ICycleBased {

	/**
	* Start a run-to-completion cycle.
	*/
	public void runCycle();
}

The runCycle() method is used to trigger a run-to-completion step in which the state machine evaluates arising events and computes possible state changes. For event-driven statecharts, this method is called automatically when an event is received. For cycle-based statecharts, this methods needs to be called explicitly in the client code. See also chapter Execution schemes. Somewhat simplified, a run-to-completion cycle consists of the following steps:

  • Clear the list of outgoing events.
  • Check whether any events have occurred which are leading to a state change.
  • If a state change has to be done:
    • Make the present state inactive.
    • Execute exit actions of the present state.
    • Save history state, if necessary.
    • Execute transition actions, if any.
    • Execute entry actions of the new state.
    • Make the new state active.
  • Clear the list of incoming events.

The interface ITimed

State machines that use timed event triggers also implement the ITimed interface. This interface adds two methods to the API: one for setting a timer service, and one for raising a time event. The timer service is used to start timers and informs the state machine with the raiseTimeEvent() method when such a timer is finished. Read more about how a state machine interacts with its timer service in section Time-controlled state machines.

package com.yakindu.core;

/**
* Interface for state machines which use timed event triggers.
*/
public interface ITimed {
	
	/**
	* Callback method if a time event occurred.
	* 
	* @param eventID	the id of the occurred event
	*/
	public void raiseTimeEvent(int eventID);
	
	/**
	* Set the {@link ITimerService} for the state machine. It must be set
	* externally on a timed state machine before <code>enter()</code> is called.
	* 
	* @param timerService	the timer service implementation to be set.
	*/
	public void setTimerService(ITimerService timerService);
}

The state machine class

The state machine class implements at least the IStatemachine interface and potentially also the ICycleBased and ITimed interfaces.

The state machine class defines a constructor in which all state machine variables are initialized to their respective default values or their initializations as defined in the statechart model.

Each named statechart interface is reflected by an inner class. For the example model above, the two statechart interfaces user and light are transformed into the two classes User and Light:

public static class User {
	private LightSwitch parent;
	
	public User(LightSwitch parent) {
		this.parent = parent;
	}
	private boolean on_button;
	
	public void raiseOn_button() {
		parent.inEventQueue.add(new Runnable() {
			@Override
			public void run() {
				on_button = true;
			}
		});
		parent.runCycle();
	}
	
	private boolean off_button;
	
	public void raiseOff_button() {
		parent.inEventQueue.add(new Runnable() {
			@Override
			public void run() {
				off_button = true;
			}
		});
		parent.runCycle();
	}
}

public static class Light {
	private boolean on;
	
	protected void raiseOn() {
		on = true;
		onObservable.next(null);
	}
	
	private Observable<Void> onObservable = new Observable<Void>();
	
	public Observable<Void> getOn() {
		return onObservable;
	}
	
	private boolean off;
	
	
	protected void raiseOff() {
		off = true;
		offObservable.next(null);
	}
	
	private Observable<Void> offObservable = new Observable<Void>();
	
	public Observable<Void> getOff() {
		return offObservable;
	}
	
	private long brightness;
	
	public long getBrightness() {
		return brightness;
	}
	
	public void setBrightness(long value) {
		this.brightness = value;
	}
}

In this example code you can see the following:

  • Statechart variables are transformed into class members with according getters and setters (see getBrightness() and setBrightness(long value)).
  • Incoming events are transformed into methods to raise such an event from the client code (see raiseOn_button() and raiseOff_button()).
  • Outgoing events are transformed into observable objects to which the client code can subscribe (see getOn() and getOff()).

A statechart can also define an unnamed interface. In this case, all the members and methods are defined directly in the state machine class.

The resulting class for the light switch example looks like the following. For documentation purposes we only show the public methods here:

public class LightSwitch implements IStatemachine, ITimed {
	
	public static class User {
	
		public void raiseOn_button() {
			...
		}
		
		public void raiseOff_button() {
			...
		}
	}
	
	public static class Light {
		
		public Observable<Void> getOn() {
			...
		}
		
		public Observable<Void> getOff() {
			...
		}
		
		public long getBrightness() {
			...
		}
		
		public void setBrightness(long value) {
			...
		}
	}
	
	public enum State {
		MAIN_REGION_OFF,
		MAIN_REGION_ON,
		$NULLSTATE$
	};

	public LightSwitch() {
		// constructor
	}
	
	public void enter() {
		...
	}
	
	public void exit() {
		...
	}
	
	public boolean isActive() {
		...
	}
	
	public boolean isFinal() {
		...
	}
	
	public boolean isStateActive(State state) {
		...
	}
	
	public void setTimerService(ITimerService timerService) {
		...
	}
	
	public ITimerService getTimerService() {
		...
	}
	
	public void raiseTimeEvent(int eventID) {
		// called by timer server
	}
	
	public User user() {
		// access to statechart interface "user"
	}
	
	public Light light() {
		// access to statechart interface "light"
	}
	
}

The following code snippet demonstrates how to use the state machine API:


// instantiate the state machine
LightSwitch sm = new LightSwitch();

// subscribe to out events to be called when they are raised
sm.light().getOn().subscribe(() -> System.out.println("Light is on.");
sm.light().getOff().subscribe(() -> System.out.println("Light is off.");

// set the timer servie if statechart is timed
sm.setTimerService(new TimerService());

// enter the state machine; from this point the machine reacts to events
sm.enter();

// raise input events, this will cause a run-to-completion step
sm.user().raiseOn_button();

// access variable via its getter/setter
int value = sm.light().getBrightness();

// exit the state machine
sm.exit();


Serving operation callbacks

YAKINDU Statechart Tools support operations that are executed by a state machine as actions, but are implemented by client-side code.

As a simple example a function myOp can be defined in the definition section of the LightSwitch example:

interface:
operation myOp()

For state machines that define operations in their interface(s), the code generator creates an operation callback interface as inner interface in the state machine class:

public interface OperationCallback {
	public void myOp();
}

When the operation is called from within the state machine, the operation call is delegated to the operation callback member:

this.operationCallback.myOp()

Hence, the client code needs to:

  • provide an implementation of this interface and
  • pass an instance of it to the state machine via the setOperationCallback(OperationCallback operationCallback) method.

At first, the OperationCallback interface must be implemented:

class MyOperationCallback implements LightSwitch.OperationCallback {
	@Override
	public void myOp() {
		System.out.println("Operation myOp has been called by the state machine");
	}
}

After this, the callback must be set before entering the state machine:

LightSwitch sm = new LightSwitch();
sm.setOperationCallback(new MyOperationCallback());
sm.enter();

Simulating operations with custom Java code

To simulate a model with operations it is possible to use custom Java code that mocks the desired behavior or even to simulate against an existing Java backend. For this purpose it is required to provide one or more custom Java classes having a method with a matching signature.

A statechart model with an operation

A statechart model with an operation

To simulate the statechart above, a new Java class must be created matching the method signature defined in the statechart. This class must be placed onto the classpath of the statecharts project.

YAKINDU Statechart Tools' default types are mapped to Java types as follows:

YSCT type Java type
integer long
real double
boolean boolean
string String
void void
package example;
public class Calculator {
   public long add(long param1, long param2) {
      return param1 + param2;
   }
}

This custom class can be passed to Eclipse’s run configuration as an Operation Class, see the figure below. It is possible to specify multiple Java classes, separated by comma.

When the simulation is executed, the variable result gets the value 2.

Configuring an operations class

Configuring an operations class

Time-controlled state machines

As already mentioned, time-controlled state machines implement the ITimed interface and require a timer service to work properly.

The light switch example model is such a time-controlled state machine as it uses an after clause at the transition from state On to state Off.

Using timed event triggers, like after or every clauses, makes the code generator generate the interface ITimed and ITimerService. Like IStatemachine, they are independent of any particular state machine and are generated into the libraryTargetFolder directory, if specified. See section "Outlet" for details.

The generated state machine class implements the ITimed interface and has a property timerService of type ITimerService. The client code must provide an ITimerService implementation to the state machine by calling the latter’s setTimerService() method before entering the state machine.

LightSwitch sm = new LightSwitch();
sm.setTimerService(new TimerService());
sm.enter();

Timer functions generally depend on the hardware target used, therefore the proper time handling has to be implemented by the developer. In principle, for each hardware target a dedicated timer service class implementing the ITimerService interface has to be developed.

Default timer service implementation

The Java code generator can create a default implementation of the ITimerService interface, and in many cases it will be sufficient. This implementation is based on java.util.Timer and java.util.TimerTask.

To generate the default timer service class, set the TimerService feature in the generator model to true. Example:

GeneratorModel for yakindu::java {

    statechart LightSwitch {

        /* … */
        
        feature GeneralFeatures {
            TimerService = true
        }
    }
}

The generated class is named TimerService and looks like this:

package com.yakindu.core;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Default timer service implementation.
 */
public class TimerService implements ITimerService {
	
	private final Timer timer = new Timer();
	
	private final List<TimeEventTask> timerTaskList = new ArrayList<TimeEventTask>();
	
	private final Lock lock = new ReentrantLock();
	
	/**
	 * Timer task that reflects a time event. It's internally used by
	 * {@link TimerService}.
	 */
	private class TimeEventTask extends TimerTask {
	
		private ITimed callback;
	
		int eventID;
	
		/**
		 * Constructor for a time event.
		 *
		 * @param callback	an ITimed object that is called when the timer expires
		 * @param eventID	the id of the state machine's time event
		 */
		public TimeEventTask(ITimed callback, int eventID) {
			this.callback = callback;
			this.eventID = eventID;
		}
	
		public void run() {
			callback.raiseTimeEvent(eventID);
		}
		
		@Override
		public boolean equals(Object obj) {
			if (obj instanceof TimeEventTask) {
				return ((TimeEventTask) obj).callback.equals(callback)
						&& ((TimeEventTask) obj).eventID == eventID;
			}
			return super.equals(obj);
		}
		
		@Override
		public int hashCode() {
			int prime = 37;
			int result = 1;
			
			int c = (int) this.eventID;
			result = prime * result + c;
			c = this.callback.hashCode();
			result = prime * result + c;
			return result;
		}
		
	}
	
	public void setTimer(final ITimed callback, final int eventID,
			long time, boolean isPeriodic) {
	
		// Create a new TimerTask for given event and store it.
		TimeEventTask timerTask = new TimeEventTask(callback, eventID);
		lock.lock();
		timerTaskList.add(timerTask);
	
		// start scheduling the timer
		if (isPeriodic) {
			timer.scheduleAtFixedRate(timerTask, time, time);
		} else {
			timer.schedule(timerTask, time);
		}
		lock.unlock();
	}
	
	public void unsetTimer(ITimed callback, int eventID) {
		lock.lock();
		int index = timerTaskList.indexOf(new TimeEventTask(callback, eventID));
		if (index != -1) {
			timerTaskList.get(index).cancel();
			timer.purge();
			timerTaskList.remove(index);
		}
		lock.unlock();
	}
	
	/**
	 * Cancel timer service. Use this to end possible timing threads and free
	 * memory resources.
	 */
	public void cancel() {
		lock.lock();
		timer.cancel();
		timer.purge();
		lock.unlock();
	}
}


Timer service

A timer service must implement the ITimerService interface and must be able to maintain a number of time events and the timers associated with them. A time event is identified by a numeric ID.

If suitable, an application can use the default timer service class TimerService, see section "Default timer implementation" for details.

The ITimerService interface looks like this:

package com.yakindu.core;

/**
 * Defines methods to set and unset timers for a state machine's timed event triggers.
 */
public interface ITimerService {

	/**
	 * Starts the timing for a given time event id.
	 * 
	 * @param callback		an ITimed object that is called when the timer expires
	 * @param eventID		the id of the state machine's time event
	 * @param time			time in milliseconds after which the time event should be triggered
	 * @param isPeriodic	set to true to trigger the time event periodically
	 */
	public void setTimer(ITimed callback, int eventID, long time, boolean isPeriodic);

	/**
	 * Unsets a time event.
	 * 
	 * @param callback	an ITimed object that is called when the timer expires
	 * @param eventID	the id of the state machine's time event
	 */
	public void unsetTimer(ITimed callback, int eventID);
}


Method setTimer

A state machine calls the setTimer(ITimed callback, int eventID, long time, boolean isPeriodic) method to tell the timer service that it has to start a timer for the given eventID. The time parameter specifies the number of milliseconds until the timer expires. When this period of time has elapsed, the timer service must raise the time event by calling the method public void raiseTimeEvent(int eventID) on the ITimed callback specified by the callback parameter, i.e., usually the state machine.

It is important to keep the execution of the setTimer() method short and use it only to start a timer thread, a hardware timer interrupt, or the like. Avoid any time-consuming operations like extensive computations, Thread.sleep(…), waiting, etc. Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.

If the parameter isPeriodic is false, the timer service raises the time event only once. If isPeriodic is true, the timer service raises the time event every time milliseconds.

Method unsetTimer

If the state machine calls the unsetTimer(ITimerCallback callback, int eventID) method the timer service must unset the timer for the given eventID, i.e., the time event will not be raised.

Raising time events on a state machine

The ITimed interface specifies a method to raise time events: public void raiseTimeEvent(int eventID).

It is the timer service’s responsibility to actually raise a time event on a state machine. To do so, the timer service calls the state machine’s raiseTimeEvent()_ method and supplies the time event’s eventID as a parameter. The state machine recognizes the time event and will process it during the next run cycle.

For event-driven state machines, raising a time event is treated equally to raising an input event. This means, that a run-to-completion step is automatically invoked (i.e. the internal runCycle() method is called).

For cycle-based state machines, the runCycle() methods needs to be called as frequently as needed to process time events without too much latency. Consider, for example, a time event which is raised by the timer service after 500 ms. However, if the runtime environment calls the state machine’s runCycle() method with a frequency of once per 1000 ms only, the event will quite likely not be processed at the correct points in time.

Runtime service

The RuntimeService class maintains all state machines that are expected to execute run-to-completion steps periodically. A client application can retrieve the RuntimeService singleton using RuntimeService.getInstance(). It can then pause, resume or cancel all state machines that are poised to run at a specified interval.

Please note: To enable this feature, set the RuntimeService parameter of GeneralFeatures to true, see Java: General Features.

Trace-observed state machines

By using the tracing feature the execution of the state machine can be observed. In detail, entered and exited states can be traced. For this, additional operation callbacks onStateEntered and onStateExited are generated in the ITracingListener interface:

/**
 * Tracing interface for state machines.
 * Must be implemented if tracing feature is used.
 * Multiple listeners can be added to and removed from the state machine.
 */
public interface ITracingListener<T> {
	
	/**
	 * OnStateEntered will be called if any state has been entered.
	 */
	void onStateEntered(T state);
	
	/**
	* OnStateExited will be called if any state has been exited.
	*/
	void onStateExited(T state);
}

The client code needs to:

  • provide an implementation of this interface and
  • pass an instance of it to the state machine via the addTraceObserver(ITracingListener<State> traceObserver) method.

A minimum implementation of this interface could look like this:

public class TracingImpl<T> implements ITracingListener<T>{
	@Override
	public void onStateEntered(T state) {
		System.out.println("State entered");
	}
	@Override
	public void onStateExited(T state) {
		System.out.println("State left");
	}
}


When the tracing feature is enabled, the code generator generates two additional methods addTraceObserver(ITracingListener<State> traceObserver) and removeTraceObserver(ITracingListener<State> traceObserver) which can be used to add and remove multiple trace observers:

LightSwitch sm = new LightSwitch();

// create multiple observer
TracingImpl<State> traceObserver1 = new TracingImpl<State>();
TracingImpl<State> traceObserver2 = new TracingImpl<State>();

// add observer to the statemachine
sm.addTraceObserver(traceObserver1);
sm.addTraceObserver(traceObserver2);

sm.enter();

// remove observer if it is not needed anymore
sm.removeTraceObserver(traceObserver2);


Java code generator features

Beside the general code generator features, there are language specific generator features, which are listed in the following chapter.

Naming feature

The Naming feature allows the configuration of package names for the generated classes (and interfaces) as well as a prefix and/or a suffix for their names.

It is an optional feature and has the following parameters:

  • basePackage (String, optional): Package name for the generated statechart class. In case the statechart defines a namespace, it will be appended to this package name. In case of multiple statecharts referencing each other from different base packages, these need to be defined in the same generator model.
  • libraryPackage (String, optional): Package name for model-independent classes and interfaces.
  • typeName (String, optional): Name to be used for the generated statechart class.

Example:

feature Naming {
    basePackage = "org.ourproject.sct.impl"
    libraryPackage = "org.ourproject.sct.lib"
    typeName = "MyStatemachine"
}

GeneralFeatures feature

The GeneralFeatures feature allows to configure additional services to be generated along with the state machine. Per default, all parameters are false, meaning to disable the corresponding features, respectively.

GeneralFeatures is an optional feature and has the following parameters:

  • RuntimeService (Boolean, optional): Enables/disables the generation of a runtime service that triggers the run cycle of a cycle-based state machine.
  • TimerService (Boolean, optional): Enables/disables the generation of a timer service implementation using java.util.Timer.
  • synchronized (Boolean, optional): Sets all methods where it is appropriate to synchronized.
  • runnable (Boolean, optional): Makes the statemachine implement java.lang.Runnable. Works only for event-driven statemachines. This implies synchronized.

Example:

feature GeneralFeatures {
    RuntimeService = true
    TimerService = true
    synchronized = true
    runnable = true
}

Tracing feature

The Tracing feature enables the generation of tracing callback functions:

  • enterState (boolean, optional): Specifies whether to generate a callback function that is used to notify about state-entering events.
  • exitState (boolean, optional): Specifies whether to generate a callback that is used to notify about state-exiting events.

Example:

feature Tracing {
    enterState = true
    exitState  = true
}