IGL Course

xtend - Code Generation

Go back

Do you remember that in the setup, we generated code from the model? Well, if you are checking, the code isn't complete nor working. We need to complete the generator.

In org.eclipse.papyrus.designer.languages.java.codegen.sm, edit the file JavaStateMachine.xtend in src/.../codegen/sm/xtend (the last one).

Question šŸ“š: How do I know that my generator is working?
Answer šŸ“–: Your generated code should be the same as the code in the folder expected-src in SMModel.


Let's be efficient

You need to know that

  • you will write a sort of Java code
  • if you are inside a template block ''', you need to write code in Ā«some_codeĀ», while you can only put one instruction per Ā«Ā»
  • more syntax will be given when needed

To generate Ā«Ā» or to trigger autocompletion, press CTRL+SPACE.


TODO 1

public void transit(String sourceState, String targetState) {
	if (!states.contains(sourceState)) {
		System.err.println("Invalid source state: " + sourceState);
		return;
	}
	
	if (!states.contains(targetState)) {
		System.err.println("Invalid target state: " + targetState);
		return;
	}

	String transition = sourceState + ";" + targetState;
	if (transitions.contains(transition)) {
		Ā«Ā«Ā«TODO Call entry, exit, doActivity, effect behaviors (if written in Java) in the correct order according to the context (i.e. switch/cases) of this transition, the target state, and source state

		currentState = targetState;
	} else {
		System.err.println("Invalid transition: current state " + currentState + " cannot transit to " + targetState);
	}
}

We are in a state. We are checking if we got a transition. If we do, we need to exit the current state, go through the transition so we are doing the effect, then we are entering the new state, and doing its activity. This is the order: exit, effect, enter, doActivity (you don't believe me? check expected-src).

Fortunately, doing exit/... is something easy, as this is simply a call to a method, at the bottom of the file (the method is empty with some todo inside, but don't mind it for now).

The syntax to call getEffectBehaviors(StateMachine stateMachine) is stateMachine.effectBehaviors.

Todo: call the methods in the right order.

The solution
// ...
if (transitions.contains(transition)) {
	Ā«Ā«Ā«DONE Call entry, exit, ...

	Ā«stateMachine.exitBehaviorsĀ»

	Ā«stateMachine.effectBehaviorsĀ»

	Ā«stateMachine.entryBehaviorsĀ»

	Ā«stateMachine.doActivityBehaviorsĀ»

	currentState = targetState;
} else {
// ...

Why do you add spaces like this? Later, you will compare your generated code with the one in expected-src and see that you need to add spaces.


TODO 2

static def getStates(StateMachine stateMachine) {
	var states = new ArrayList<State>

	// TODO fill states with all ownedElements which are State

	states
}

This method is supposed to return every state in our stateMachine. Note that states (the last line) is the same as return states;.

Todo: if stateMachine is not null, then iterate allOwnedElements and add every State (instanceof) in states.

Tip: this is almost of copy-paste of the method above (getInitialState). Use it for the syntax of the for (each).

The solution
// DONE fill states with all ownedElements which are State
if (stateMachine !== null) {
	for (ownedElement : stateMachine.allOwnedElements) {
		if (ownedElement instanceof State) {
			states.add(ownedElement)
		}
	}
}

TODO 3

static def getStateNamesList(StateMachine stateMachine) {
	var stateNames = new ArrayList<String>
	val states = stateMachine.states // uses getStates defined above
	
	// TODO fill stateNames
	
	stateNames
}

We know from the documentation that we got a function getName so we can call name on a state.

Todo: Loop through states and add their names in states.

Tip: this is as easy as you could guess.

The solution
// DONE fill stateNames
for (state : states) {
	stateNames.add(state.name)
}

TODO 4

/**
* Interpolated expression to get all states' names separated by ", "
*/
static def getStateNames(StateMachine stateMachine) '''
	Ā«Ā«Ā«TODO get all states names and build expression S1, S2, S3Ā»
'''

Notice that we are in a template block '''. The syntax of a for loop is

Ā«FOR element: set BEFORE "" SEPARATOR "" AFTER ""Ā»
	Ā«Ā«Ā«Some code
Ā«ENDFORĀ»

Todo: we need to print every stateName, with a separator ",". You may add an if checking if stateMachine isn't null (but that's useless, we already checked in getStates).

The solution
Ā«Ā«Ā«DONE get all states names and build expression S1, S2, S3Ā»
Ā«FOR stateName : stateMachine.stateNamesList BEFORE "" SEPARATOR ", " AFTER ""Ā»
	"Ā«stateNameĀ»"
Ā«ENDFORĀ»

Why do we need "", could I use '' or nothing at all? My teacher told me that "" is used when you want something to be "printed" in the code, so we have to.


TODO 5

static def getTransitions(StateMachine stateMachine) {
	var transitions = new ArrayList<Transition>
	 	
	// TODO fill transitions with all ownedElements which are Transition
	 	
	 transitions
}

Todo: some as TODO2 but with Transition

The solution
// DONE fill transitions with all ownedElements which are Transition
if (stateMachine !== null) {
	for (ownedElement : stateMachine.allOwnedElements) {
		if (ownedElement instanceof Transition) {
			transitions.add(ownedElement)
		}
	}
}

TODO 6

/**
* Utility method to get all transition names of the state machine
* @param stateMachine the state machine to investigate
* @return a list of transitions names in the state machine relating source & targets separated by ";"
*/
static def getTransitionNamesList(StateMachine stateMachine) {
	var transitionNames = new ArrayList<String>
	val transitions = stateMachine.transitions	// uses getTransitions defined above

	// TODO fill tansitionsNames if source & target of transition are not null & source and target names are not empty

	transitionNames
}

You need to check that transition.source and transition.target are not null, and both of them must have a non-empty name. As you can read in the doc, you can use name (such as transition.source.name).

  • Note: you can call empty (something.empty) to check if something is empty.
  • Note: if you didn't pay attention, we are checking the difference with !==.

Todo: if the check if passed, then add in transitionNames, transition.source.name + ";" + transition.target.name.

The solution
// DONE fill tansitionsNames iff source & target of transition are not null & source and target names are not empty
for (transition : transitions) {
	if (
		transition.source !== null && 
		transition.target !== null && 
		!transition.source.name.empty &&
		!transition.target.name.empty
		) {
			transitionNames.add(transition.source.name + ";" + transition.target.name)
	}
}

TODO 7

static def getTransitionNames(StateMachine stateMachine) '''
	Ā«Ā«Ā«TODO get all transition names and build expression T1, T2, ...Ā»
'''

Todo: Same as we did in TODO4.

The solution
Ā«Ā«Ā«DONE get all transition names and build expression T1, T2, ...Ā»
Ā«FOR transitionName : stateMachine.transitionNamesList BEFORE "" SEPARATOR ", " AFTER ""Ā»
	"Ā«transitionNameĀ»"
Ā«ENDFORĀ»

Mid-way explanation

Until now, you were coding functions without understanding why you did that. These functions are utility functions that we need for the 4 main functions that we called in transit a while back.

For each function, you are expecting something like that. Of course, sourceState will change, the values in the case will change, the content of the comments will change, and finally, you were not supposed to know System.out.println(???). The user, defined in his/her diagram some code that will be executed when exiting. This is System.out.println(???). Some states may not have such code, so you will have to check that they do (we don't want empty cases).

// Exit behaviors
switch (sourceState) {
case "S1":
	// Call S1 exit behavior
	System.out.println("Exiting S1");
	break;
case "S2":
	// Call S2 exit behavior
	System.out.println("Exiting S2");
	break;
case "S3":
	// Call S3 exit behavior
	System.out.println("Exiting S3");
	break;
}

Pro tip: go read the code in expected-src, if you want to know from where the answers are coming from (i.g.: what you have to put in your switch).
Question: hey, why are we using xxx in this switch, will this variable do not exist in the function. Answer: you called these functions in a template block. So they do not exist, their code will be printed directly in transit and we got sourceState/targetState/transition. Check expected-src if you don't believe it.


TODO 8

static def getExitBehaviors(StateMachine stateMachine) {
	var states = stateMachine.states
	var exitBehaviorCode = ""
	
	// TODO builds the cases code before surrounding it with switch (sourceState) {}
	
	// TODO if exitBehaviorCode is not empty then surround it with switch (sourceState) { exitBehaviorCode }
}

Todo * for each state, add a case in exitBehaviorCode, only if the state got an exit behavior (state.exit.javaBehavior not empty) * if the exitBehaviorCode is not empty, wrap it in a switch

Tips: you will use template blocks for the cases and the switch. Do not forget the comments.

The solution
// DONE builds the cases code before surrounding it with switch (sourceState) {}
for (state: states) {
	if (!state.name.empty && !state.exit.javaBehavior.empty) {
		exitBehaviorCode += 
		'''
		case "Ā«state.nameĀ»":
			// Call Ā«state.nameĀ» exit behavior
			Ā«state.exit.javaBehaviorĀ»
			break;
		'''
	}
}

// DONE if exitBehaviorCode is not empty then surround it with switch (sourceState) { exitBehaviorCode }
if (!exitBehaviorCode.empty) {
	exitBehaviorCode =
	'''
	// Exit behaviors
	switch (sourceState) {
	'''
	+ exitBehaviorCode +
	'''
	}
	'''
}

TODO 9

static def getEntryBehaviors(StateMachine stateMachine) {
	var states = stateMachine.states
	var entryBehaviorCode = ""

	// TODO builds the cases code before surrounding it with switch (sourceState) {}

	// TODO if entryBehaviorCode is not empty then surround it with switch (sourceState) { entryBehaviorCode }
}

Todo: Same as TODO 8.

The solution
// DONE builds the cases code before surrounding it with switch (sourceState) {}
for (state: states) {
	if (!state.name.empty && !state.entry.javaBehavior.empty) {
		entryBehaviorCode +=
		'''
		case "Ā«state.nameĀ»":
			// Call Ā«state.nameĀ» entry behavior
			Ā«state.entry.javaBehaviorĀ»
			break;
		'''
	}
}

// DONE if entryBehaviorCode is not empty then surround it with switch (sourceState) { entryBehaviorCode }
if (!entryBehaviorCode.empty) {
	entryBehaviorCode = '''
	// Entry behaviors
	switch (targetState) {
	'''
	+ entryBehaviorCode +
	'''
	}
	'''
}

TODO 10

static def getDoActivityBehaviors(StateMachine stateMachine) {
	var states = stateMachine.states
	var doActivityBehaviorCode = ""
	// TODO builds the cases code before surrounding it with switch (sourceState) {}

	// TODO if doActivityBehaviorCode is not empty then surround it with switch (sourceState) { doActivityBehaviorCode }
}

Todo: Same as TODO 8 or TODO 9.

The solution
// DONE builds the cases code before surrounding it with switch (sourceState) {}
for (state: states) {
	if (!state.name.empty && !state.doActivity.javaBehavior.empty) {
		doActivityBehaviorCode += 
			'''
			case "Ā«state.nameĀ»":
				// Call Ā«state.nameĀ» doActivity behavior
				Ā«state.doActivity.javaBehaviorĀ»
				break;
			'''
	}
}

// DONE if doActivityBehaviorCode is not empty then surround it with switch (sourceState) { doActivityBehaviorCode }
if (!doActivityBehaviorCode.empty) {
	doActivityBehaviorCode = '''
	// DoActivity behaviors
	switch (targetState) {
	'''
	+ doActivityBehaviorCode +
	'''
	}
	'''
}

TODO 11

static def getEffectBehaviors(StateMachine stateMachine) {
	var transitions = stateMachine.transitions
	var effectBehaviorCode = ""
	// TODO builds the transition name as "transition source name;transition target name" 
	// TODO for each case the source AND target of the transition must be non null
	// TODO for each case the source AND target names of the transition must be non empty

	// TODO if transition name is not empty AND there is an effect behavior there is a case for this transition so builds the cases code before surrounding it with switch (sourceState) {}
		
	// TODO if effectBehaviorCode is not empty then surround it with switch (transition) { effectBehaviorCode }
}

It's hard šŸ˜­, because you got 5 TODO. But it's easy, as this is almost like what you did before, but we are adding more clauses in the if.

Todo: Same as TODO 8 or TODO 9, with some additional checks.

The solution
for (transition: transitions) {
	// DONE for each case the source AND target of the transition must be non null
	// DONE for each case the source AND target names of the transition must be non empty
	if (
		transition.source !== null && 
		transition.target !== null && 
		!transition.source.name.empty &&
		!transition.target.name.empty &&
		!transition.effect.javaBehavior.empty
	) {
		// DONE builds the transition name as "transition source name;transition target name"
		var transitionName = transition.source.name + ";" + transition.target.name
		// DONE if transition name is not empty AND there is an effect behavior there is a case for this transition so builds the cases code
		effectBehaviorCode += '''
		case "Ā«transitionNameĀ»":
			// Call Ā«transitionNameĀ» effect behavior
			Ā«transition.effect.javaBehaviorĀ»
			break;
		'''
	}
}

// DONE if effectBehaviorCode is not empty then surround it with switch (transition) { effectBehaviorCode }
if (!effectBehaviorCode.empty) {
	effectBehaviorCode = '''
	// Transition effect behaviors
	switch (transition) {
	'''
	+ effectBehaviorCode +
	'''
	}
	'''
}

Generating the code

You should already know how you can do that, if not, check the setup section.

  • Run "runtime_eclipse" > Proceed
  • Double-click on SMModel
  • Click on "Project SMModel" > Designer > Generate Java Code ...

Then, let's run our code.

  • Right-click on Main (the JAVA file in the generated model) > Run as > Java Application.
  • You can't execute the Main in expected-src (at least I didn't manage to, in eclipse that is). It will execute again the previous main, so you might be fooled by eclipse.

Pro tip: to run expected-src, you need to right-click in the package explorer (somewhere), then new > others > Java > Java Project. I used the name org.eclipse.papyrus.expectedgen.SMModel, Java 1.8, and "no" when prompted to open Java perspective. You can now copy and paste the "com" in "expected-src", inside the "src" of your new project, then run it.


Checking that you got what we expected

I will use IntelliJ to check. You may use any IDE you want, simply enjoying this small adventure outside eclipse. To find a folder, don't forget to use Right-click > Show In > System Explorer.

  • Run the main in expected-src. Compare with yours
Exiting S1
Transiting from S1 to S2
Entering S2
Do activity of S2
========
Exiting S2
Transiting from S2 to S3
Entering S3
Do activity of S3
========
Invalid transition: current state S3 cannot transit to S2
========
Exiting S3
Transiting from S3 to S1
Entering S1
Do activity of S1
========
  • Compare AnotherSMClass with yours
  • Compare SMClass with yours

Note: In IntelliJ you got a tool to compare two or three files in a great interface, that's why I moved to IntelliJ to compare two files. You can use online tools such as this one if you want.