CentraleSupélec LMF, UMR CNRS 9021
Département informatique Laboratoire Méthodes Formelles
Bât Breguet, 3 rue Joliot-Curie Bât 650 Ada Lovelace, Université Paris Sud
91190 Gif-sur-Yvette, France Rue Noetzlin, 91190 Gif-sur-Yvette, France
Métamodèles, modèles et transformations de modèles

Résultats du 4 décembre 2019

Les résultats obtenus lors de la séance du 4 décembre 2019 sont disponibles.

Introduction

Ces cours se déroulent sur machine, dans l'environnement Eclipse (Modeling tools) et abordent :

  • la création d'un métamodèle,
  • la création de modèles (instances d'un métamodèle),
  • les transformations M2M (Model to model), avec QVT operational,
  • les transformations M2T (Model to text), avec Acceleo,
  • la création d'une syntaxe textuelle concrète pour un métamodèle (avec Xtext).

Contexte

On souhaite créer un DSL pour représenter des automates. On crée pour cela un projet eCore et un métamodèle comportant les concepts suivants :

  • StateMachine (une machine à états) possédant un nom, une liste d'entrées (Input), une liste de sorties (Output) et une liste d'états (State) ;
  • Input, représente une entrée, caractérisée par son nom ;
  • Output, représente une sortie, caractérisée par son nom ;
  • State, représente un état, caractérisé par son nom. Un état peut être initial ou non et possède des transitions (Transition) ;
  • Transition, représente une transition entre deux états. Elle possède une garde (Input), une action (Output) et un état cible (State).

Une fois ce métamodèle créé, instanciez une StateMachine ayant pour entrées a et b, pour sorties x et y et deux états A et B, A étant initial. Il existe une transition de A vers B de garde a et d'action x, et une transition de B vers A de garde b et d'action y. Pour cela, choisissez "Create Dynamic Instance..." dans le menu contextuel de la classe StateMachine dans l'arborescence du .ecore.

Modèle objet

Afin de pouvoir générer du code, on définit (dans un autre projet) un autre métamodèle des automates, plus proche de l'implémentation :

  • FSMClass, la classe des automates, ayant pour attributs un nom, une liste d'états (StateClass) et un état initial ;
  • StateClass, la classe des états, ayant pour attributs un nom et une liste de transitions (TransitionClass) ;
  • TransitionClass, la classe des transitions, ayant pour attributs une garde et une action (chaînes de caractères) et un état cible (StateClass).

Ce modèle permettra de générer du code Java ou C++ par exemple.

Transformation M2M

Pour modéliser la transformation d'un automate vers un modèle d'un programme, nous utiliserons QVT operational. Ce langage permet de décrire des transformations M2M (Model to model). Vous trouverez en ligne une documentation de QVT operational ainsi qu'un tutoriel. Il y a également une présentation faite à la conférence EclipseCon en 2009.

Pour créer la transformation, créez un nouveau projet Model to Model Transformation > Operational QVT Project, et choisissez Create a plug-in project. Cochez Create artifacts in new QVT Operational Project et sélectionnez Operational QVT Transformation.

Dans le fichier source de la transformation, déclarez les métamodèles source et destination à l'aide du mot clef modeltype.

Définissez les mapping des différents concepts du métamodèle source vers ceux du métamodèle cible, puis complétez le main pour convertir l'élément racine du modèle source (StateMachine), ce qui entraînera la conversion des autres éléments (à condition que vous ayez bien géré les propriétés de Containment dans le métamodèle source).

Votre transformation devrait avoir une structure semblable à ceci :

modeltype FSM "strict" uses fSMModel('http://si.centralesupelec.fr/fSMModel');
modeltype OBJ "strict" uses objectModel('http://si.centralesupelec.fr/objectModel');

transformation FSM2Object(in fsm: FSM, out obj: OBJ);

main() {
	fsm.rootObjects()[StateMachine]->map toObject();
}

mapping StateMachine::toObject() : FSMClass
{
	name := self.name;
	states := self.states->map toObject();
	// [...]
}

mapping State::toObject() : StateClass {
	name := self.name;
	// [...]
}

mapping Transition::toObject() : TransitionClass {
	// [...]
}

Exécutez cette transformation sur une instance du métamodèle source et inspectez le résultat.

Pour exécuter une transformation QVTop, il faut créer une Run configuration de type QVT Operational Transformation et renseigner les paramètres Transformation module (sélectionnez votre transformation en cliquant sur le bouton Browse), le modèle source (IN) et le modèle cible (OUT). Pour créer un nouveau modèle cible, sélectionnez tout d'abord le même fichier que pour le modèle source, puis changez son nom dans le champ de texte.

Transformation M2T (Model to text)

On souhaite maintenant générer le code Java correspondant à un modèle objet d'un automate. Nous utiliserons Acceleo : créez un projet Acceleo. Sur la deuxième page du dialogue de création du projet, ajoutez le métamodèle source de la transformation (celui qui correspond à la version objet des automates, et que vous trouverez en sélectionnant Runtime version) et cochez la case Main template.

Ouvrez le fichier generate.mtl qui se trouve dans le package main, et complétez le comme suit :

[module generate('http://si.centralesupelec.fr/objectModel')]

[template public generateElement(aFSMClass : FSMClass)]
[comment @main/]
[let className:String = aFSMClass.name.toUpperFirst()]
[file (className.concat('.java'), false, 'UTF-8')]

public class [className/] {
	public static void main(String args['[]'/]) {
		System.out.println("Hello [className/]");
	}
}
[/file]
[/let]
[/template]

Comme il n'est pas commode d'éditer du code Java dans l'éditeur Acceleo, nous allons exécuter la transformation sur un modèle, éditer le code Java généré jusqu'à obtenir le comportement voulu, puis reporter ce code dans le fichier .mtl en remplaçant les parties qui dépendent du modèle source par des instructions de génération Acceleo.

Créez un projet Java qui contiendra les fichiers nécessaires à l'exécution du code généré.

Pour exécuter la transformation, utilisez Run As, choisissez Launch Acceleo Application, et renseignez le modèle source et le dossier src du projet Java comme dossier de destination.

Complétez le code généré (vous pouvez créer d'autres classes dans le projet Java si nécessaire), et une fois qu'il fonctionne, recopiez ce code dans la transformation Acceleo, et éditez-le pour générer du code similaire pour tout modèle.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

public class Machine {
	private class State {
		private class Transition {
			private String guard;
			private String action;
			private State target;

			public Transition(String guard, String action, State target) {
				this.guard = guard;
				this.action = action;
				this.target = target;
			}

			public String fire(String input) {
				if (input.equals(guard)) {
					current = this.target;
					return this.action;
				}
				return null;
			}

			public String toString() {
				return guard + "/" + action + "->" + target.name;
			}
		}

		private String name;
		private List<Transition> transitions;

		public State(String name) {
			this.name = name;
			this.transitions = new LinkedList<Transition>();
		}

		public void addTransition(String guard, String action, State target) {
			this.transitions.add(new Transition(guard, action, target));
		}

		public String react(String input) {
			for (Transition t : transitions) {
				String out = t.fire(input);
				if (out != null) {
					return out;
				}
			}
			return null;
		}

		public String toString() {
			String res = "state " + this.name + ": ";
			for (Transition t: transitions) {
				res += System.getProperty("line.separator");
				res += "  ";
				res += t.toString();
			}
			return res;
		}
	}

	private State current;
	private HashMap<String, State> states = new HashMap<String, State>();

	public Machine() {
		State s;
		s = new State("A");
		states.put("A", s);
		current = s;
		s = new State("B");
		states.put("B", s);

		s = states.get("A");
		s.addTransition("a", "x", states.get("B"));
		s = states.get("B");
		s.addTransition("b", "y", states.get("A"));
	}

	public String react(String input) {
		return current.react(input);
	}

	public static void main(String[] args) {
		Machine fsm = new Machine();
		BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
		while (true) {
			System.out.println("In state " + fsm.current);
			String in = null;
			try {
				in = input.readLine();
			} catch (IOException ioe) {
				ioe.printStackTrace();
				System.exit(1);
			}
			if (in == null) {
				break;
			} else {
				String out = fsm.react(in);
				if (out != null) {
					System.out.println("output = " + out);
				}
			}
		}
	}
}
[comment encoding = UTF-8 /]
[module generate('http://www.example.org/objectModel')]


[template public generateElement(aFSMClass : FSMClass)]
[comment @main/]
[let className:String = aFSMClass.name.toUpperFirst()]
[file (className.concat('.java'), false, 'UTF-8')]
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

public class [className/] {
	private class State {
		private class Transition {
			private String guard;
			private String action;
			private State target;

			public Transition(String guard, String action, State target) {
				this.guard = guard;
				this.action = action;
				this.target = target;
			}

			public String fire(String input) {
				if (input.equals(guard)) {
					current = this.target;
					return this.action;
				}
				return null;
			}

			public String toString() {
				return guard + "/" + action + "->" + target.name;
			}
		}

		private String name;
		private List<Transition> transitions;

		public State(String name) {
			this.name = name;
			this.transitions = new LinkedList<Transition>();
		}

		public void addTransition(String guard, String action, State target) {
			this.transitions.add(new Transition(guard, action, target));
		}

		public String react(String input) {
			for (Transition t : transitions) {
				String out = t.fire(input);
				if (out != null) {
					return out;
				}
			}
			return null;
		}

		public String toString() {
			String res = "state " + this.name + ": ";
			for (Transition t: transitions) {
				res += System.getProperty("line.separator");
				res += "  ";
				res += t.toString();
			}
			return res;
		}
	}

	private State current;
	private HashMap<String, State> states = new HashMap<String, State>();

	public [className/]() {
		State s;
        [for (state:StateClass | aFSMClass.states)]
		s = new State("[state.name/]");
		states.put("[state.name/]", s);
        [if (aFSMClass.initialState = state)]
		current = s;
        [/if]
        [/for]

        [for (state:StateClass | aFSMClass.states)]
		s = states.get("[state.name/]");
        [for (trans:TransitionClass | state.transitions)]
		s.addTransition("[trans.guard/]", "[trans.action/]", states.get("[trans.target.name/]"));
        [/for]
        [/for]
	}

	public String react(String input) {
		return current.react(input);
	}

	public static void main(String['[]'/] args) {
		[className/] fsm = new [className/]();
		BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
		while (true) {
			System.out.println("In state " + fsm.current);
			String in = null;
			try {
				in = input.readLine();
			} catch (IOException ioe) {
				ioe.printStackTrace();
				System.exit(1);
			}
			if (in == null) {
				break;
			} else {
				String out = fsm.react(in);
				if (out != null) {
					System.out.println("output = " + out);
				}
			}
		}
	}
}
[/file]
[/let]
[/template]