août
2009
A l’étape du billet précédant [step9] nous avons représenté graphiquement les instances EMF ActionType par des carrées et StateType par des ellipses. Les actions ActionType sont liées à des states StateType via leurs propriétés fromState et toState. Ces 2 propriétés ne sont pas encore représentées graphiquement. Dans ce billet et le suivant nous allons représenter graphiquement les 2 propriétés fromState et toState des actions par des connections. Le contenu XML du workflow :
<workflow xmlns="http://www.example.org/workflow">
<state name="s1"/>
<state name="s2"/>
<action name="a2" fromState="s1" toState="s2"/>
</workflow>
sera représenté graphiquement comme ceci :
Pour les raisons expliquées dans la section Model & Connection GEF, les modèles EMF StateType et ActionType doivent être modifiés pour gérer la notion de Connection. Une Connection est le modèle (EMF) qui représente :
- le lien entre un StateType et une ActionType : cette connection se mettra à jour en fonction de la propriété fromState de l’action.
- ou le lien entre un ActionType et un StateType : cette connection se mettra à jour en fonction de la propriété toState de l’action
C’est ce que nous allons effectuer dans ce billet. Dans le billet suivant nous représenterons graphiquement le modèle Connection qui sera géré lui aussi par un EditPart (capable de gérer des connections) pour représenter graphiquement les propriétés fromState et toState des actions.
Vous pouvez télécharger le projet org.example.workflow_step10.zip présenté dans ce billet.
Model & Connection GEF
Dans tous les exemples GEF que j’ai pu trouver, les modèles (EMF ou non) représentés graphiquement à l’aide de GEF ont une notion de Connection. Plus exactement chacun des noeuds modèles qui doivent être représentés graphiquement, gérent une liste de connections cibles (target) et source. L’article A Shape Diagram Editor qui affiche des figures carrés et ellipses que l’on peut lier entre elles est un exemple de modèle (non EMF) qui à cette notion de connection dans son modèle.
Cependant dans notre cas, la notion de Connection n’existe pas. En effet l’information de connections entre un state et une action provient de la propriété fromState de l’action et celle entre une action et un state provient de la propriété toState de l’action.
Pour rester dans le « standard » des exemples GEF trouvés, nous allons modifier les modèles EMF StateType et ActionType pour ajouter la notion de Connection. Cependant, l’ajout du modèle Connection dans les modèle EMF existant ActionType et StateType, « polluera » le modèle « métier » du workflow avec des données graphiques, car ce modèle Connection ne sera utilisé que dans le cas de GEF. J’ai passé pas mal de temps à explorer l’API de GEF pour voir si il était possible de se passer de ces listes de Connections dans le modèle, mais sans succès. Je pense que l »idéal serait de gérer un modèle EMF (visuel) qui contiendrait les informations de Connections et qui serait synchronisé avec les instances EMF StateType et ActionType du WorkflowType. Ce modèle EMF (visuel) serait ensuite utilisé pour afficher les connections dans les EditPart qui affichent les connections.
J’ai fait le choix de « polluer » le modèle EMF WorkflowType (données métiers) avec des informations de Connections (données visuelles), pour me retrouver dans le cas « standard » des examples GEF et pouvoir ensuite expliquer plus facilement le code GEF (expliqué dans le billet suivant) qui gère les connections.
L’article Using GEF with EMF reprend l’article A Shape Diagram Editor avec un modèle EMF Shape. Je me suis basé sur cet article pour avoir le même modèle EMF Connection.
Voici un schéma du modèle EMF WorkflowType que nous allons obtenir à la fin de ce billet :
REMARQUE : ce schéma a été généré après avoir sélectionné le modèle workflow.ecore puis à l’aide du menu contextuel Initialize ecore_diagram diagram file (avec la version Eclipse Modeling).
Ce schéma montre que les StateType et ActionType hériteront de ConnectableNode qui représente un noeud qui est constitué :
- d’une liste de connections source : la propriété sourceConnections (qui est une liste EMF EList
) contient toutes les connections sources du noeud ConnectableNode. - d’une liste de connections cibles (target) : la propriété targetConnections (qui est une liste EMF EList
) contient toutes les connections cibles du noeud ConnectableNode.
Le modèle EMF Connection est constitué :
- d’une propriété source qui pointe sur le noeud ConnectableNode source.
- d’une propriété target qui pointe sur le noeud ConnectableNode target.
Les Connection EMF se mettront à jour en fonction des propriétés fromState et toState des instances EMF ActionType.
Si on reprend le contenu XML suivant :
<workflow xmlns="http://www.example.org/workflow">
<state name="s1"/>
<state name="s2"/>
<action name="a2" fromState="s1" toState="s2"/>
</workflow>
Voici un schéma représentant les connections EMF :
Ce schéma met en évidence les 2 instances EMF Connection :
- Connection c1, lien entre le state s1 et l’action a2. Cette connection EMF est dù à la propriété fromState= »s1″ de l’action. Cette connection aurra les propriétés :
- source renseigné avec l’instance EMF StateType s1.
- target renseigné avec l’instance EMF ActionType a2.
- Connection c2, lien entre l’action a2 et le state s2. Cette connection EMF est dù à la propriété toState= »s2″ de l’action. Cette connection aurra les propriétés :
- source renseigné avec l’instance EMF ActionType a2.
- target renseigné avec l’instance EMF StateType s2.
l’instance EMF WorkflowType sera constitué de :
- une instance EMF ActionType a2 :
- la liste EMF sourceConnections sera constitué d’une Connection EMF, la connection c2.
- la liste EMF targetConnections sera constitué d’une Connection EMF, la connection c1.
- 2 instances EMF StateType :
- l’instance EMF StateType s1 :
- la liste EMF sourceConnections sera constitué d’une Connection EMF, la connection c1.
- la liste EMF targetConnections n’aurra aucune connections.
- l’instance EMF StateType s2 :
- la liste EMF sourceConnections n’aurra aucune connections.
- la liste EMF targetConnections sera constitué d’une Connection EMF, la connection c2.
- l’instance EMF StateType s1 :
Pour obtenir ce modèle de Connection nous devons :
- modifier le ecore workflow.ecore pour créer le modèle EMF Connection et ConnectableNode et faire hériter StateType et ActionType de ConnectableNode.
- mettre à jour les connections en fonction des propriétés fromState et toState des instances EMF ActionType du workflow. Pour cela nous devons observer tous les changements du workflow (ajout/suppression d’une action, modification d’une propriété fromState d’une action….) qui s’effectuera via les Adapter EMF.
Modification workflow.ecore
La section suivante décrira pas à pas comment modifier le ecore workflow.ecore pour avoir obtenir le modèle EMF avec les Connection décrit ci-dessus.
EMF Connection/ConnectableNode
Créez un EClasss Connection :
Créee un EClasss ConnectableNode :
Créez un ERefrence sourceConnections dans l’EClass ConnectableNode avec les propriétés suivantes :
- Etype: Connection
- Upper Bound: -1
Creer un ERefrence targetConnections dans l’EClass ConnectableNode avec les propriétés suivantes :
- Etype: Connection
- Upper Bound: -1
Ajoutez l’EAttribute Source a Connection :
Ajoutez l’EAttribute target a Connection :
StateType doit étendre ConnectableNode. Pour cela sélectionnez StateType puis cliquez sur le bouton … de la propriété ESUper Types :
Cette action ouvre la fenêtre de dialogue suivante. Sélectionnez à gauche « ConnectableNode » puis cliquez sur le bouton « Add » :
Effectuez la même action pour ActionType :
Regénerer le code Java du modèle EMF à partir de workflow.genmodel (à l’aide du menu contextuel Generate Model Code) :
Après avoir géneré le code vous devez avoir :
- la classe org.example.workflow.model.Connection.
- la classe org.example.workflow.model.ConnectableNode.
- les classes org.example.workflow.model.StateType et org.example.workflow.model.ActionType qui doivent hériter de org.example.workflow.model.ConnectableNode.
Mise à jour EMF Connections
A cette étape le modèle EMF du workflow a la notion de connection, mais les connections EMF ne se mettent pas à jour en fonction des propriétés toState et fromState des instances EMF ActionType. Dans cette section nous allons effectuer cette tâche. Pour cela nous devons observer tous les changements susceptibles des noeuds StateType et ActionType constituant le workflow. Pour observer tous ces changements nous allons utiliser les Adapter EMF qui permettent d’observer les changements d’une instance EMF, autrement dit nous allons ajouter pour chacune des instances EMF StateType et ActionType des Adapter EMF pour écouter leur changement et mettre à jour en conséquence les Connections EMF :
- Créer/Supprimer une Connection EMF.
- Mettre à jour les listes ConnectableNode#sourceConnections et ConnectableNode#targetConnections des noeuds StateType et ActionType avec la connection créée/suppriméée.
Pour information SSE qui permet de synchroniser le DOM (provenant du XML saisi) avec les instances EMF utilise aussi cette technique en ajoutant un Adpapter EMF EMFDOMSSEAdapter sur chacune des instances EMF pour synchroniser le DOM-SSE (contenu XML) avec les instances EMF.
Nous allons donc créer plusieurs Adapter EMF dans le projet org.example.workflow puis nous verrons comment les utiliser.
ConnectionUtils
J’ai passé pas mal de temps a developper les Adapter EMF qui mettent à jour les Connection et j’ai pu remarquer qu’il y avait du code en commun entre les differents Adapter EMF dont on a besoin. Pour mettre en commun ce code, j’ai décidé de creer une classe utilitaire ConnectionUtils. Créer la classe org.example.workflow.model.util.ConnectionUtils comme suit :
import org.eclipse.emf.common.util.EList;
import org.example.workflow.model.ConnectableNode;
import org.example.workflow.model.Connection;
import org.example.workflow.model.WorkflowFactory;
public class ConnectionUtils {
public static void connect(ConnectableNode source, ConnectableNode target) {
Connection connection = WorkflowFactory.eINSTANCE.createConnection();
connection.setSource(source);
connection.setTarget(target);
source.getSourceConnections().add(connection);
target.getTargetConnections().add(connection);
}
public static void clearTargetConnections(ConnectableNode node) {
EList<Connection> targetConnections = node.getTargetConnections();
for (Connection targetConnection : targetConnections) {
ConnectableNode source = targetConnection.getSource();
source.getSourceConnections().remove(targetConnection);
}
targetConnections.clear();
}
public static void clearSourceConnections(ConnectableNode node) {
EList<Connection> sourceConnections = node.getSourceConnections();
for (Connection sourceConnection : sourceConnections) {
ConnectableNode target = sourceConnection.getTarget();
target.getTargetConnections().remove(sourceConnection);
}
sourceConnections.clear();
}
}
Cette classe utilitaire permet de connecter des noeuds et permet de supprimer les noeuds source ou cible.
Adapter ActionType
Le premier Adapter EMF qui coule de source est celui qui est capable d’observer les modification d’une instance EMF ActionType :
- lorsque la propriété fromState change :
- on doit supprimer l’ancienne connection cible (target) sur lequel l’action pointait.
- créer la nouvelle connection avec le nouveau state en mettant à jour la liste targetConnections de l’action et la liste sourceConnections du state avec la connection créée.
- lorsque la propriété toState change :
- on doit supprimer l’ancienne connection source sur le state qui pointait sur l’action.
- créer la nouvelle connection avec le nouveau state en mettant à jour la liste sourceConnections de l’action et la liste targetConnections du state avec la connection créée.
Créer la classe org.example.workflow.model.util.ActionTypeAdapter comme suit :
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.example.workflow.model.ActionType;
import org.example.workflow.model.StateType;
import org.example.workflow.model.WorkflowPackage;
public class ActionTypeAdapter implements Adapter {
private Notifier target;
public boolean isAdapterForType(Object type) {
return type.getClass() == ActionTypeAdapter.class;
}
public void notifyChanged(Notification notification) {
int type = notification.getEventType();
int featureId = notification.getFeatureID(WorkflowPackage.class);
ActionType action = (ActionType) notification.getNotifier();
StateType oldState = null;
switch (type) {
case Notification.SET:
switch (featureId) {
case WorkflowPackage.ACTION_TYPE__FROM_STATE:
// fromState attribute from action has changed
// If old fromState is not null, remove connection
oldState = (StateType) notification.getOldValue();
if (oldState != null) {
ConnectionUtils.clearTargetConnections(action);
}
// target
// Create connection between Action and from State
StateType fromState = action.getFromState();
if (fromState != null) {
ConnectionUtils.connect(fromState, action);
}
break;
case WorkflowPackage.ACTION_TYPE__TO_STATE:
// toState attribute from action has changed
// If old toState is not null, remove connection
oldState = (StateType) notification.getOldValue();
if (oldState != null) {
ConnectionUtils.clearSourceConnections(action);
}
// source
// Create connection between Action and to State
StateType toState = action.getToState();
if (toState != null) {
ConnectionUtils.connect(action, toState);
}
break;
}
}
}
public Notifier getTarget() {
return target;
}
public void setTarget(Notifier target) {
this.target = target;
}
}
Adapter WorkflowType
A ce stade nous avons créé l’Adapter EMF qui permet d’observer les instances EMF ActionType, mais celui-ci n’est pas encore opérationnel car nous n’avons pas encore ajouter ces Adapter à ces instances. Pour effectuer cela nous devons observer les ajouts des ActionType à l’instance EMF WorkflowType. Pour cela nous allons crééer l’Adapter EMF WorkflowTypeAdapter. Cet adapter à pour rôle :
- d’ajouter un Adapter EMF ActionTypeAdapter lorsqu’une instance EMF ActionType est ajoutée au workflow (quand on saisit par exemple l’élement XML
- supprimer les connections cible & source associées à une instance EMF ActionType lorsque celle-ci est supprimée du workflow (quand on enlève par exemple un élement XML
- supprimer les connections cible & source associées à une instance EMF StateType lorsque celle-ci est supprimée du workflow (quand on enlève par exemple un élement XML
- supprimer les connections cible & source associées à une instance EMF ActionType lorsque celle-ci est supprimée du workflow (quand on enlève par exemple un élement XML
REMARQUE : le cas ou l’on modifie le nom d’un state doit aussi influer sur la mise à jour des connections, autrement dit si on modifie le nom d’un state cela doit mettre à jour les actions liés avec un state null. Ceci n’est pas effectué dans ce billet. Nous développerons cette fonctionnalité dans le billet suivant en montrant par des exemples le problème que cela posera.
Créer la classe org.example.workflow.model.util.WorkflowTypeAdapter comme suit :
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.example.workflow.model.ActionType;
import org.example.workflow.model.ConnectableNode;
import org.example.workflow.model.Connection;
import org.example.workflow.model.StateType;
import org.example.workflow.model.WorkflowPackage;
public class WorkflowTypeAdapter implements Adapter {
private Notifier target;
public boolean isAdapterForType(Object type) {
return type.getClass() == WorkflowTypeAdapter.class;
}
public void notifyChanged(Notification notification) {
int type = notification.getEventType();
int featureId = notification.getFeatureID(WorkflowPackage.class);
switch (type) {
case Notification.REMOVE:
switch (featureId) {
case WorkflowPackage.WORKFLOW_TYPE__STATE:
case WorkflowPackage.WORKFLOW_TYPE__ACTION:
// Suppression state/action => suppression des
// connections
ConnectableNode node = (ConnectableNode) notification
.getOldValue();
if (node != null) {
// Target
ConnectionUtils.clearTargetConnections(node);
// Source
ConnectionUtils.clearSourceConnections(node);
}
break;
}
break;
case Notification.ADD:
switch (featureId) {
case WorkflowPackage.WORKFLOW_TYPE__ACTION:
ActionType action = (ActionType) notification.getNewValue();
addActionAdpaterIfNeed(action);
break;
}
}
}
private void addActionAdpaterIfNeed(ActionType action) {
if (EcoreUtil.getExistingAdapter(action, ActionTypeAdapter.class) != null)
return;
action.eAdapters().add(new ActionTypeAdapter());
}
public Notifier getTarget() {
return target;
}
public void setTarget(Notifier target) {
this.target = target;
}
}
On peut remarquer que l’ajout d’un Adapter EMF ActionTypeAdapter s’effectue que si il n’y en n’a pas déja un :
if (EcoreUtil.getExistingAdapter(action, ActionTypeAdapter.class) != null)
return;
action.eAdapters().add(new ActionTypeAdapter());
}
Ceci fonctionne car la méthode ActionTypeAdapter#isAdapterForType(Object type) est implementée comme ceci
return type.getClass() == ActionTypeAdapter.class;
}
Modification WorkflowTranslator – Ajout Adapter WorkflowType
A cette étape, nous avons créé l’Adpater EMF WorkflowTypeAdapter qui permet de mettre à jour les Connections EMF en fonction des propriétés fromState et toState des actions. Il faut maintenant ajouter cet Adpater à l’instance EMF WorkflowType. Le meilleur endroit pour effectuer ceci est de l’ajouter à la création de cette instance. La création de cette instance s’effectue via la méthode WorkflowTranslator#createEMFObject(String nodeName, String readAheadName) qui est appelée par SSE. Nous allons surcharger cette méthode pour y ajouter notre Adapter EMF WorkflowTypeAdapter.
Ajouter le code suivant dans la classe org.example.workflow.model.util.WorkflowTranslator :
public EObject createEMFObject(String nodeName, String readAheadName) {
EObject eObject = super.createEMFObject(nodeName, readAheadName);
if (EcoreUtil.getExistingAdapter(eObject, WorkflowTypeAdapter.class) == null) {
eObject.eAdapters().add(new WorkflowTypeAdapter());
}
return eObject;
}
Pour éviter d’ajouter plusieurs fois l’adpater EMF WorkflowTypeAdapter à l’instance EMF WorkflowType, le test suivant :
if (EcoreUtil.getExistingAdapter(eObject, WorkflowTypeAdapter.class) == null) ...
permet de vérifier qu’il n’a pas déja été ajouté.
Voici la classe WorkflowTranslator en entière :
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.wst.common.internal.emf.resource.GenericTranslator;
import org.eclipse.wst.common.internal.emf.resource.RootTranslator;
import org.eclipse.wst.common.internal.emf.resource.Translator;
import org.example.workflow.model.WorkflowPackage;
public class WorkflowTranslator extends RootTranslator {
public static RootTranslator INSTANCE = new WorkflowTranslator();
public WorkflowTranslator() {
super("workflow", WorkflowPackage.eINSTANCE.getWorkflowType());
}
protected Translator[] getChildren() {
return new Translator[] { createStateTranslator(),
createActionTranslator() };
}
private Translator createStateTranslator() {
GenericTranslator translator = new GenericTranslator("state",
WorkflowPackage.eINSTANCE.getWorkflowType_State());
translator.setChildren(new Translator[] { new MyIDTranslator("name",
WorkflowPackage.eINSTANCE.getStateType_Name(),
Translator.DOM_ATTRIBUTE) });
return translator;
}
private Translator createActionTranslator() {
GenericTranslator translator = new GenericTranslator("action",
WorkflowPackage.eINSTANCE.getWorkflowType_Action());
translator.setChildren(new Translator[] {
new Translator("name", WorkflowPackage.eINSTANCE
.getActionType_Name(), Translator.DOM_ATTRIBUTE),
new HRefTranslator("fromState", WorkflowPackage.eINSTANCE
.getActionType_FromState(), Translator.DOM_ATTRIBUTE),
new HRefTranslator("toState", WorkflowPackage.eINSTANCE
.getActionType_ToState(), Translator.DOM_ATTRIBUTE), });
return translator;
}
@Override
public EObject createEMFObject(String nodeName, String readAheadName) {
EObject eObject = super.createEMFObject(nodeName, readAheadName);
if (EcoreUtil.getExistingAdapter(eObject, WorkflowTypeAdapter.class) == null) {
eObject.eAdapters().add(new WorkflowTypeAdapter());
}
return eObject;
}
}
Conclusion
Dans ce billet nous avons mis à jour les Connections entre les state et les actions. Il est difficile de tester notre travail, mais dans le billet suivant la représentation graphique des connections permettront de valider le code que nous avons fait dans ce billet.
Articles récents
- Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step5]
- Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step4]
- Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step3]
- Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step2]
- Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step1]
Commentaires récents
- Conception d’un Editeur Eclipse de workflow XML [step 0] dans
- Conception d’un Editeur Eclipse de workflow XML [step 19] dans
- Conception d’un Editeur Eclipse de workflow XML [step 7] dans
- Conception d’un Editeur Eclipse de workflow XML [step 7] dans
- Conception d’un Editeur Eclipse de workflow XML [step 7] dans