Maven/Netbeans : Tutoriel simple

Introduction

Prochainement, j'ai envie de présenter quelques tutoriels Java. Un prérequis à ces futurs tutoriels sera l'utilisation de Maven, c'est pourquoi je commence par présenter ce formidable projet. Même si Maven est un sujet maintes fois abordé, je vais en présenter ma propre variante. Plus d'un point de vue pragmatique que théorique.

Documentation

Quitte à passer pour un dinosaure, quand je m'intéresse à un nouveau jouet technologique, j'aime bien lire le mode d'emploi. Dans le cas de Maven, le mode d'emploi que j'ai utilisé est le livre suivant :

Titre : Apache Maven

Auteurs : Nicolas De loof et Arnaud Héritier

Amazon : Apache Maven

Ce livre est très agréable à lire. Il est rédigé dans un style "détendu" tout en fournissant les informations nécessaires à la compréhension et à l'utilisation de Maven.

Il se trouve que les différents sujets et technologies abordés à fil des pages et très proche de plusieurs projets sur lesquels j'ai travaillé ces dernières années:

  • Systèmes de gestion des versions (notamment SubVersion et Git)
  • Testing (JUnit, TestNG et Selenium)
  • Google Web Toolkit (GWT)
  • Serveurs d'application Web Java (Tomcat, Jetty)

Mise en oeuvre de Maven

L'EDI que j'utilise le plus souvent pour du développement Java est Netbeans. Cet environnement de développement peut ouvrir un projet ayant la structure de fichiers Maven et intègre tous les outils nécessaires aux tâches courantes d'un programmeur.

Netbeans 7.1

Création, sous Netbeans, d'un nouveau projet Java en utilisant Maven. Ayant pour habitude de faire des "Hello World" à tout vent, en voici un de plus, le 5ème ces derniers temps ;-)

Création du projet helloworld5


Sorties à la création du projet

cd /Users/silenus/NetBeansProjects; JAVA_HOME=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home "/Applications/NetBeans/NetBeans 7.1.2.app/Contents/Resources/NetBeans/java/maven/bin/mvn" -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.1 -DarchetypeRepository=http://repo.maven.apache.org/maven2 -DgroupId=ch.conceptforge -DartifactId=helloworld5 -Dversion=1.0-SNAPSHOT -Dpackage=ch.conceptforge.helloworld5 -Dbasedir=/Users/silenus/NetBeansProjects -Darchetype.interactive=false --batch-mode archetype:generate
Scanning for projects...
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-deploy-plugin/2.7/maven-deploy-plugin-2.7.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-deploy-plugin/2.7/maven-deploy-plugin-2.7.pom (6 KB at 12.5 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-plugins/22/maven-plugins-22.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-plugins/22/maven-plugins-22.pom (13 KB at 108.8 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/maven-parent/21/maven-parent-21.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/maven-parent/21/maven-parent-21.pom (26 KB at 150.5 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/apache/10/apache-10.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/apache/10/apache-10.pom (15 KB at 112.9 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-deploy-plugin/2.7/maven-deploy-plugin-2.7.jar
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-deploy-plugin/2.7/maven-deploy-plugin-2.7.jar (27 KB at 149.7 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-site-plugin/3.0/maven-site-plugin-3.0.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-site-plugin/3.0/maven-site-plugin-3.0.pom (20 KB at 165.2 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-site-plugin/3.0/maven-site-plugin-3.0.jar
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-site-plugin/3.0/maven-site-plugin-3.0.jar (112 KB at 460.4 KB/sec)
 
------------------------------------------------------------------------
Building Maven Stub Project (No POM) 1
------------------------------------------------------------------------
 
>>> maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom >>>
 
<<< maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom <<<
 
[archetype:generate]
Generating project in Batch mode
Archetype defined by properties
----------------------------------------------------------------------------
Using following parameters for creating project from Old (1.x) Archetype: maven-archetype-quickstart:1.1
----------------------------------------------------------------------------
Parameter: groupId, Value: ch.conceptforge
Parameter: packageName, Value: ch.conceptforge.helloworld5
Parameter: package, Value: ch.conceptforge.helloworld5
Parameter: artifactId, Value: helloworld5
Parameter: basedir, Value: /Users/silenus/NetBeansProjects
Parameter: version, Value: 1.0-SNAPSHOT
********************* End of debug info from resources from generated POM ***********************
project created from Old (1.x) Archetype in dir: /Users/silenus/NetBeansProjects/helloworld5
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.814s
Finished at: Sun Aug 26 20:41:03 CEST 2012
Final Memory: 9M/81M
------------------------------------------------------------------------


Cool, ils ont pensé à moi chez Sun Oracle. Mon "Hello World" est déjà écrit.

Un petit run

Le "run project (F6)" lance le projet comme s'il s'agissait d'un projet Java standard :

cd /Users/silenus/NetBeansProjects/helloworld5; JAVA_HOME=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home "/Applications/NetBeans/NetBeans 7.1.2.app/Contents/Resources/NetBeans/java/maven/bin/mvn" "-Dexec.args=-classpath %classpath ch.conceptforge.helloworld5.App" -Dexec.executable=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -Dexec.classpathScope=runtime process-classes org.codehaus.mojo:exec-maven-plugin:1.2:exec
Scanning for projects...
 
------------------------------------------------------------------------
Building helloworld5 1.0-SNAPSHOT
------------------------------------------------------------------------
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.5/maven-resources-plugin-2.5.pom
 
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.5/maven-resources-plugin-2.5.pom (7 KB at 9.1 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.5/maven-resources-plugin-2.5.jar
 
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.5/maven-resources-plugin-2.5.jar (26 KB at 147.7 KB/sec)
 
[resources:resources]
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-filtering/1.0/maven-filtering-1.0.pom
 
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-filtering/1.0/maven-filtering-1.0.pom (6 KB at 46.9 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-filtering/1.0/maven-filtering-1.0.jar
 
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-filtering/1.0/maven-filtering-1.0.jar (42 KB at 94.2 KB/sec)
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
skip non existing resourceDirectory /Users/silenus/NetBeansProjects/helloworld5/src/main/resources
 
[compiler:compile]
Compiling 1 source file to /Users/silenus/NetBeansProjects/helloworld5/target/classes
 
[exec:exec]
Hello World!
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.142s
Finished at: Sun Aug 26 20:58:13 CEST 2012
Final Memory: 10M/81M
------------------------------------------------------------------------

Ajout d'une dépendance

Pour illustrer l'utilité de Maven, je vais ajouter une fonctionnalité à mon beau "helloworld5". Je vais le faire parler Xml. Et, pour ce faire, je souhaite utiliser la librairie XStream.

Habituellement, je modifie directement le pom.xml, mais il existe aussi une possibilité via l'interface utilisateur de Netbeans.


Une recherche de XStream me propose directement les librairies qui pourraient m'intéresser.

Et voilà. Un clic et la dépendance est créée et donc, la librairie utilisable.

Projet de test

Container

package ch.conceptforge.helloworld5;
 
public class Container {
 
    private String text;
 
    public Container() {
    }
 
    public Container(String text) {
        this.text = text;
    }
 
    public String getText() {
        return text;
    }
 
    public void setText(String text) {
        this.text = text;
    }    
 
}

App

package ch.conceptforge.helloworld5;
 
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
 
public class App
{
 
    private static Container c;
 
    private static void dispalyText() {
        System.out.println(c.getText());
    }
 
    private static void displayXml() {
        XStream xs = new XStream(new DomDriver());
        xs.alias("container", Container.class);
        System.out.println(xs.toXML(c));
    }
 
    public static void main( String[] args )
    {
        c = new Container("Hello World!");
        dispalyText();
        displayXml();
    }
}

Nouveau run

cd /Users/silenus/NetBeansProjects/helloworld5; JAVA_HOME=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home "/Applications/NetBeans/NetBeans 7.1.2.app/Contents/Resources/NetBeans/java/maven/bin/mvn" "-Dexec.args=-classpath %classpath ch.conceptforge.helloworld5.App" -Dexec.executable=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -Dexec.classpathScope=runtime process-classes org.codehaus.mojo:exec-maven-plugin:1.2:exec
Scanning for projects...
 
------------------------------------------------------------------------
Building helloworld5 1.0-SNAPSHOT
------------------------------------------------------------------------
 
[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 0 resource
 
[compiler:compile]
Compiling 2 source files to /Users/silenus/NetBeansProjects/helloworld5/target/classes
 
[exec:exec]
Hello World!
 
  Hello World!
 
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 1.585s
Finished at: Sun Aug 26 21:38:49 CEST 2012
Final Memory: 8M/81M
------------------------------------------------------------------------

Conclusion

Mon but, dans cet article, n'était pas de réécrire le livre sur Maven, que j'ai présenté au début de l'article. Je ne souhaite pas non plus essayer de parler de Maven dans sa globalité.
Maven est présent dans la plupart des projets Java sur lesquels je travaille. C'est un outil qui fait partie de ma vie de tous les jours et qui me rend de grands services. Dans l'immense majorité des projets Java, j'utilise Netbeans comme EDI. Il y a quelque temps, j'ai eu l'occasion d'utiliser Eclipse pour un projet. Je dois avouer que j'ai été très surpris de voir à quel point Maven à l'air d'une pièce rapportée sous Eclipse par rapport à ce que j'ai l'habitude d'utiliser avec le couple Netbeans/Maven.

Exemple de mise en oeuvre de SCXML-JS

Introduction

Voici quelques articles que je parle des machines à états SCXML. Il faut dire que j'ai passé pas mal de temps à les étudier. Le cas que j'ai présenté jusqu'à maintenant (Exemple de mise en oeuvre de SCXML - partie 1) simule une application Java/Swing. Il m'arrive souvent de travailler sur des applications réseau. Celles-ci sont basées sur les technologies du Web. J'utilise beaucoup GWT, Google Web Toolkit, (dont je parlerai prochainement) qui produit une application Web HTML/CSS/JavaScript.

Quand j'ai étudié SCXML, j'avais simulé une application Web. Il s'agissait d'une calculatrice quatre opérations. C'est sur la base de cette étude que j'ai écrit les articles précédents.

Calculatrice

Comme expliqué précédemment, ce tutoriel présente une calculatrice. Cette application d'étude a été écrite il y a quelque temps et j'ai repris cette base pour écrire les articles précédents.

La calculatrice est séparée en deux parties

  • HTML : IHM de l'application Web
  • JavaScript : Machine à états
Voici l'application elle-même : calculator.html

Machine à états

La machine à états de l'exemple de mise en oeuvre de SCXML - partie 1 a été construite à partir de celle-ci. Elle est donc à 90% identique. Les différences sont au niveau du langage. En Java, les listeners s'occupent de surveiller l'activité de la machine et de réagir en fonction des changements d'état. Cette machine-ci utilise JavaScript, il est donc possible d'inclure le code dans la balise <script> de SCXML.

Étant donné le peu de différence entre cette machine et celle du premier tutoriel, je ne vais pas revenir sur le fonctionnement général. Je vais juste reprendre les états et les transitions qui, selon moi, méritent une petite explication et mettre en évidence les morceaux de JavaScript.

Clear

  • La balise <log> produit une trace dans la console du browser
  • <assign> donne une valeur (new String('')) à la variable "result", donc l'initialise avec une chaine vide.
  • <script> contient le code JavaScript "$('#display').val(0)". C'est un appel à la librairie jquery qui aura pour effet de mettre la valeur "0" dans le input ayant l'id "display".

Reset

Remise à zéro des variables op1, op2 et oper.

operand1 / onDigit

Comme dans la première partie de cette série de tutoriels, la transition "onDigit" est déclenchée dès qu'un chiffre est saisi. Le code présenté ci-dessous permet d'ajouter (concaténer) ce chiffre à la variable "op1".

  • la balise <log> affiche dans la console l'objet "_event".

Par exemple après un click sur le bouton "1", voici ce que nous pouvons voir dans la console de Firebug :

Object { name="onDigit", data="1"}

operand1 / received_op1

Mise à jour de l'affichage

onOperation

Mise à jour de la variable "oper".

Compute

Cet état contient le coeur de la calculatrice, le calcul lui-même.

  • La balise <assign> contient le calcul proprement dit. l'expression "eval(op1 + oper + op2)" demande à JavaScript d'interpréter la concaténation de ces trois variables. Avec l'exemple habituel "eval('123' + '+' + '12')" => 135

IHM

L'interface utilisateur de cette application web est réalisée en HTML. Elle est fortement inspirée d'un article de daniweb : A four function calculator (HTML). Par rapport à daniweb, j'ai modifié les points suivants :

  • utilisation de jquery pour simplifier le code javascript
  • intégration de "calculator.js"
  • modification de tous les javascripts pour utiliser la machine à états

Liaison avec la machine à états

Il s'agit maintenant de lier l'interface en HTML avec la machine à états en XML. Pour ce faire, il faut utiliser SCXML-JS.

Télécharger SCXML-JS

Un dépôt SubVersion est mis à disposition et nous devons aller chercher les sources :

svn checkout http://svn.apache.org/repos/asf/commons/sandbox/gsoc/2010/scxml-js/trunk commons-scxml-js

"Compiler" la machine à états

./run.sh --backend state --beautify --ie /path/to/calculator.scxml &gt; /path/to/calculator.js

lier la machine à états à l'IHM

D'abord, il faut faire connaitre à l'IHM le scipt généré ci-dessus

   ...
   <!-- Machine à états (calculator.scxml compilée avec scxml-js) --><script type="text/javascript" src="calculator.js"></script>

Ensuite, il faut créer l'instance JavaScript de la machine à états

...
<!-- Initialisation de la machine à états finis --><script type="text/javascript">// <![CDATA[
    sec = new StatechartExecutionContext();
    sec.initialize();
// ]]></script>

Réaliser une transition

Il est maintenant possible d'implémenter l'événement "onClick" d'un bouton de l'IHM pour qu'il demande à la machine à états de réaliser une transition.

<input onclick="sec.onClear()" type="button" name="clear" value=" C " />

Conclusion

L'application est disponible ici : calculator.html
La machine à états est ici : calculator.scxml

Cet article montre comment utiliser SCXML dans un contexte différent d'une application Java/Swing. En rédigeant ces quelques lignes, je me suis aperçu que, malheureusement, le projet SCXML-JS ne sera pas mis à jour. C'est dommage, j'ai fait de très bonnes expériences avec ce projet. Au moins, le projet SCXML-JS est utilisable et le code source est publié, il nous reste à retrousser nos manches et à le faire évoluer nous-mêmes.

Exemple de mise en oeuvre de SCXML – Code source

Les deux articles précédents (partie 1 et partie 2) faisaient une description détaillée d'une calculatrice quatre opérations qui permettait d'illustrer le développement d'une machine à états "SCXML".

Voici le code source de cette application : code source de scxml_calc.zip

Etant à buts pédagogiques, cette application n'est pas codée selon les exigences d'une application destinée à la production. Ceci étant précisé, je suis ouvert à toute demande de clarification ou suggestion d'amélioration.

Ce code source n'est pas soumis à licence. Vous pouvez utiliser tout, ou partie de celles-ci, dans des projets commerciaux ou non et rien ne vous oblige à en préciser la provenance.

Exemple de mise en oeuvre de SCXML – partie 2

Introduction

La première partie de cet article présentait la machine à états d'une calculatrice quatre opérations.

Cet article va présenter la partie "Java" de l'application.

Diagramme de classe

Diagramme de classes de "scxml_calc"

  • La classe "App" constitue le point d'entrée de l'application
  • L'interface graphique tient dans la classe "Main" du package "gui"
  • Le package "common" contient des abstractions pour faciliter l'utilisation de machine à états SCXML
  • Le coeur du programme est situé dans le package "fsm"

Implémentation

La suite présente l'implémentation de quelques classes importantes. Ensuite, le paragraphe "Entrées / Sorties" reprend les classes décrites dans cette partie et explique les interactions entre le code java et le code SCMXL.

GUI

...
public class Main extends javax.swing.JFrame {
 
    private CalcFSM fsm;
 
    public Main() throws Exception {
        initComponents();
        fsm = new CalcFSM();
        fsm.addListener(new DebugToConsoleListener());
        fsm.addListener(new DisplayListener(fsm, tfDisplay));
        fsm.addListener(new ComputeListener(fsm));
    }
 
    ...                 
 
    private void digitActionPerformed(java.awt.event.ActionEvent evt) {
        JButton b = (JButton) evt.getSource();
        fsm.doOnDigit(b.getText());
    }                                     
 
    private void operationActionPerformed(java.awt.event.ActionEvent evt) {
        JButton b = (JButton) evt.getSource();
        fsm.doOnOperation(b.getText());
    }                                         
 
    private void btnEqualsActionPerformed(java.awt.event.ActionEvent evt) {
        fsm.doOnEquals();
    }                                         
 
    private void btnCActionPerformed(java.awt.event.ActionEvent evt) {
        fsm.doOnClear();
    }
 
    private javax.swing.JButton btn0;
    private javax.swing.JButton btn1;
    private javax.swing.JButton btn2;
    private javax.swing.JButton btn3;
    private javax.swing.JButton btn4;
    private javax.swing.JButton btn5;
    private javax.swing.JButton btn6;
    private javax.swing.JButton btn7;
    private javax.swing.JButton btn8;
    private javax.swing.JButton btn9;
    private javax.swing.JButton btnAddition;
    private javax.swing.JButton btnC;
    private javax.swing.JButton btnDivision;
    private javax.swing.JButton btnEquals;
    private javax.swing.JButton btnMultiplication;
    private javax.swing.JButton btnSubstraction;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JLabel tfDisplay;
}

La classe graphique contient un lien fort avec le moteur de machine à états (CalcFSM). Ensuite, selon les contrôles graphiques activés, les transitions de la machine à états sont déclenchées.

CalcFSM

...
public class CalcFSM extends FiniteStateMachine {
 
    public CalcFSM() throws Exception {
        startSCXML("file:///////path/to/calc.scxml");
    }
 
    public void doOnDigit(String digit) {
        doTriggerEvent(new TriggerEvent("onDigit", TriggerEvent.SIGNAL_EVENT, digit));
    }
 
    public void doOnClear() {
        doTriggerEvent(new TriggerEvent("onClear", TriggerEvent.SIGNAL_EVENT));
    }
 
    public void doOnOperation(String operator) {
        doTriggerEvent(new TriggerEvent("onOperation", TriggerEvent.SIGNAL_EVENT, operator));
    }
 
    public void doOnEquals() {
        doTriggerEvent(new TriggerEvent("onEqual", TriggerEvent.SIGNAL_EVENT));
    }
}

Cette classe est la fille de la classe suivante.

Dans son constructeur, cette classe ouvre et lance la machine à états. De plus, elle publie les méthodes qui permettent de déclencher les transitions.

FiniteStateMachine

...
public abstract class FiniteStateMachine {
 
    private SCXMLExecutor exec = null;
    private SCXML scxml = null;
 
    public FiniteStateMachine() throws Exception {
        // Il faut réimplementer ce constructeur avec un appel à startSCXML
    }
 
    protected void startSCXML(String filename) throws Exception {
        ErrorHandler errHandler = null;
 
        System.out.println("SCXML file : " + filename);
 
        scxml = SCXMLParser.parse(new URL(filename), errHandler);
 
        JexlEvaluator expEvaluator = new JexlEvaluator();
        SimpleDispatcher evtDisp = new org.apache.commons.scxml.env.SimpleDispatcher();
        SimpleErrorReporter errRep = new org.apache.commons.scxml.env.SimpleErrorReporter();
 
        exec = new SCXMLExecutor(expEvaluator, evtDisp, errRep);
        exec.setStateMachine(scxml);
 
        exec.go();
    }
 
    public void addListener(SCXMLListener listener) throws ModelException {
        exec.addListener(scxml, listener);
        exec.reset();
    }
 
    protected void doTriggerEvent(TriggerEvent evt) {
        try {
            exec.triggerEvent(evt);
        } catch (ModelException ex) {
            Logger.getLogger(FiniteStateMachine.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
 
    public void reset() throws ModelException {
        exec.reset();
    }
 
    public Object evaluate(String expression) throws SCXMLExpressionException {
        return exec.getEvaluator().eval(exec.getRootContext(), expression);
    }
 
    public void set(String variable, Object value) throws SCXMLExpressionException {
        exec.getRootContext().set(variable, value);
    }
}

Cette classe fait le lien entre les classes SCXML d'Apache et la classe spécifique pour la machine à états de cette calculatrice.

Elle contient la méthode startSCXML qui permet de charger et de démarrer la machine à états. Cette méthode est largement inspirée de la documentation officielle.

Les méthodes "addListener" et "doTriggerEvent" sont des petites aides à l'implémentation.

"evaluate" et "set" sont deux méthodes qui permettent de lire et de modifier une variable déclarée dans le datamodel de SCXML.

Remarque : Cette classe mérite d'être améliorée pour s'abstraire beaucoup mieux de la technologie de la machine à états elle même, mais ce n'est pas le sujet de ce tutoriel.

Entrées / Sorties

Les classes présentées jusqu'à maintenant permettent d'ouvrir et de lancer la machine à états présentée dans le premier article sur ce sujet.

Elles permettent également d'interagir avec la machine en lui envoyant des ordres de transition.

Par exemple, le bouton "1" sur l'interface graphique contient le gestionnaire d'événements suivant :

    private void digitActionPerformed(java.awt.event.ActionEvent evt) {
        JButton b = (JButton) evt.getSource();
        fsm.doOnDigit(b.getText());
    }

En cas de clique, l'événement "doOnDigit" de la machine à états est appelé avec le texte "1" (le texte du bouton).

Etat : waiting_op1

Donc, si la machine est dans l'état "waiting_op1", la transition "onDigit" va avoir lieu et, du coup le mécanisme expliqué dans le premier article va faire que la variable "op1" dans le SCXML va se remplir avec cette valeur.

Ainsi, à mesure que l'utilisateur clique sur les différents boutons de l'interface graphique, la machine passe d'un état à l'autre.

Listeners

Comme le dit wikipedia : "Le listener, en français écouteur, est un terme anglais utilisé de façon générale en informatique pour qualifier un élément logiciel qui est à l'écoute d'évènements afin d'effectuer des traitements."

DebugToConsoleListener

public class DebugToConsoleListener implements SCXMLListener, Serializable {
 
    @Override
    public void onEntry(TransitionTarget tt) {
        System.out.println(tt.getId() + ".onEntry");
    }
 
    @Override
    public void onExit(TransitionTarget tt) {
        System.out.println(tt.getId() + ".onExit");
    }
 
    @Override
    public void onTransition(TransitionTarget tt, TransitionTarget tt1, Transition trnstn) {
        System.out.println(tt.getId() + ".onTransition =&gt; " + tt1.getId());
    }
}

Comme son nom l'indique, ce listener est surtout utile pour le développeur. Il affiche une trace sur la console dès qu'une transition à lieu.

Voici, ce qu'il se passe avec la séquence : "1 2 + 1 2 3 =" (avec un petit commentaire)
Trace Commentaire
start.onEntry initialisation
start.onExit  
start.onTransition => clear  
clear.onEntry  
clear.onExit  
clear.onTransition => reset  
reset.onEntry  
reset.onExit  
reset.onTransition => operand1  
operand1.onEntry operand1
waiting_op1.onEntry  
start.onEntry  
start.onExit  
start.onTransition => clear  
clear.onEntry  
clear.onExit  
clear.onTransition => reset  
reset.onEntry  
reset.onExit  
reset.onTransition => operand1  
operand1.onEntry  
waiting_op1.onEntry  
waiting_op1.onExit  
waiting_op1.onTransition => display_op1 OnDigit(1)
display_op1.onEntry  
display_op1.onExit  
display_op1.onTransition => waiting_op1  
waiting_op1.onEntry  
waiting_op1.onExit  
waiting_op1.onTransition => display_op1 OnDigit(2)
display_op1.onEntry  
display_op1.onExit  
display_op1.onTransition => waiting_op1  
waiting_op1.onEntry  
waiting_op1.onExit  
operand1.onExit  
operand1.onTransition => operand2 onOperation(+)
operand2.onEntry operand2
waiting_op2.onEntry  
waiting_op2.onExit  
waiting_op2.onTransition => display_op2 OnDigit(1)
display_op2.onEntry  
display_op2.onExit  
display_op2.onTransition => waiting_op2  
waiting_op2.onEntry  
waiting_op2.onExit  
waiting_op2.onTransition => display_op2 OnDigit(2)
display_op2.onEntry  
display_op2.onExit  
display_op2.onTransition => waiting_op2  
waiting_op2.onEntry  
waiting_op2.onExit  
waiting_op2.onTransition => display_op2 OnDigit(3)
display_op2.onEntry  
display_op2.onExit  
display_op2.onTransition => waiting_op2  
waiting_op2.onEntry  
waiting_op2.onExit  
operand2.onExit  
operand2.onTransition => compute onEqual
compute.onEntry calcul
compute.onExit  
compute.onTransition => display_result  
display_result.onEntry  
display_result.onExit  
display_result.onTransition => reset  
reset.onEntry retour à operand1
reset.onExit  
reset.onTransition => operand1  
operand1.onEntry  
waiting_op1.onEntry  

DisplayListener

public class DisplayListener implements SCXMLListener, Serializable {
 
    private FiniteStateMachine fsm;
    private JLabel display;
 
    public DisplayListener(FiniteStateMachine fsm, JLabel display) {
        this.fsm = fsm;
        this.display = display;
    }
 
    public void onEntry(TransitionTarget tt) {
        if (tt.getId().equalsIgnoreCase("clear")) {
            display.setText("0");
        }
 
        try {
            if (tt.getId().equalsIgnoreCase("display_op1")) {
                display.setText(fsm.evaluate("op1").toString());
            }
            if (tt.getId().equalsIgnoreCase("display_op2")) {
                display.setText(fsm.evaluate("op2").toString());
            }
            if (tt.getId().equalsIgnoreCase("display_result")) {
                display.setText(fsm.evaluate("result").toString());
            }
 
        } catch (SCXMLExpressionException ex) {
            Logger.getLogger(DisplayListener.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
 
    public void onExit(TransitionTarget tt) {
        //
    }
 
    public void onTransition(TransitionTarget tt, TransitionTarget tt1, Transition trnstn) {
        //
    }

Cette classe reçoit au constructeur le label qui servira à l'affichage. Le coeur de cette classe est assez trivial. Selon l'état de la machine, le contenu d'une des variables (op1, op2 ou result) est affiché sur le label.

ComputeListener

public class ComputeListener implements SCXMLListener, Serializable {
 
    private FiniteStateMachine fsm;
    private String result;
 
    public ComputeListener(FiniteStateMachine fsm) {
        this.fsm = fsm;
    }
 
    public void onEntry(TransitionTarget tt) {
       try {
            if (tt.getId().equalsIgnoreCase("compute")) {
                String op1 = fsm.evaluate("op1").toString();
                String oper = fsm.evaluate("oper").toString();
                String op2 = fsm.evaluate("op2").toString();
                result = fsm.evaluate(op1 + oper + op2).toString();
            }
        } catch (SCXMLExpressionException ex) {
            Logger.getLogger(ComputeListener.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
 
    public void onExit(TransitionTarget tt) {
        try {
            if (tt.getId().equalsIgnoreCase("compute")) {
                fsm.set("result",result);
            }
        } catch (SCXMLExpressionException ex) {
            Logger.getLogger(ComputeListener.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
 
    public void onTransition(TransitionTarget tt, TransitionTarget tt1, Transition trnstn) {
        //
    }
 
}

Cette classe effectue le calcul lui-même. Quand la machine atteint l'état "compute", les valeurs de "op1", "op2" et "oper" sont lues grâce à la méthode "evaluate" de "FiniteStateMachine".

Ensuite, le calcul est réalisé en appelant à nouveau "evaluate" mais cette fois avec l'expression "op1 + oper + op2". L'expression est évaluée puis le résultat est renvoyé dans la variable "result".

Remarque : Cette manière d'évaluer cette expression n'est pas très orthodoxe. Cette application étant écrite à des fins pédagogiques, c'est un raccourci que je m'autorise...

Conclusion

Ce n'est pas un cas très courant, mais il m'est, de temps en temps, arrivé d'intervenir sur des applications qui contenaient un grand nombre de commandes. La plupart n'avaient pas été codées en utilisant une méthodologie de développement appropriée. Les commandes et les états n'étaient pas toujours gérés absolument systématiquement. Du coup, les premiers 80% de fonctionnement étaient faciles à atteindre, mais pour les 20% derniers, c'était vraiment la  beaucoup cycles de tests et de modifications jusqu'à avoir identifié et gérer tous les cas particuliers.

Comme je l'ai expliqué au début du premier article, un de mes clients a passé commande pour une application répondant à ces critères. De plus, je voyais bien que mon interlocuteur n'était pas certain à 100% de toutes les fonctionnalités.

La solution de la machine à états me plaisait, car je la connaissais déjà et que je pensais qu'elle me permettrait de résoudre ce problème. Ce qui me dérangeait dans cette solution était le fait de devoir résonner sur le diagramme (p.ex le diagramme d'états-transitions UML), puis de coder le tout.

Je me suis donc mis à la recherche d'une solution plus souple et je l'ai trouvée. Les outils que j'ai présentés tout au long de cet article permettent de réaliser le diagramme et de l'utiliser en temps que "produit fini" dans l'application. La séparation des couches est très bien faite et, du coup, les modifications de la machine à états seront faciles à réaliser.

Exemple de mise en oeuvre de SCXML – partie 1

Introduction

Cet article fait, plus ou moins, suite à ma réfection sur les machines à états. J'ai décidé de présenter un exemple d'utilisation de SCXML. Etant donné que le programme réel dans lequel j'ai mis en oeuvre cette technologie est un logiciel commercial, je vais plutôt reprendre la documentation que j'ai écrite lors de ma phase de découverte de SCXML et l'adapter au blog.

Pour ce faire, je vais présenter une calculatrice quatre opérations.

IHM

Simple et classique :

IHM de la calculatrice SCXML

Machine à états

Voici un aperçu global de la machine à états.
SCXML : Machine à états complète
Au coeur de cette machine, il y a deux éléments similaires : operand1 et operand2. Ces éléments s'occupent de "construire" les deux opérandes pour les calculs. Entre les deux opérandes, il y a la gestion de l'opération. Avant tout cela, il y a une phase d'initialisation et après, un traitement des opérandes et de l'opérateur.

Implémentation

Cette machine a été réalisée à l'aide de la technologie SCXML. Le programme scxmlgui m'a permis de construire la totalité de la machine. Les captures d'écran sont issues de ce produit.

Déclaration des variables

Dans SCXML, il est possible de définir et de manipuler des variables dans la machine à états.

DataModel dans la machine à états

Comme on peut le voir, quatre variables sont définies :

"op1" et "op2" pour les opérandes, "oper" pour l'opérateur et "result" pour stocker le résultat. Toutes ces variables sont initialisées comme chaîne de caractères vides (&#039 pour l'apostrophe).

Exécution

Cette partie montre les principales étapes de l'exécution de la machine à états. Seuls les états ou les transitions réalisant des modifications des variables sont présentés.

Clear

Remise à zéro de la variable "result"
Etat : clear

Reset

Remise à zéro des variables op1, op2 et oper.
Etat : reset

operand1 / onDigit

La transition "onDigit" est déclenchée dès qu'un chiffre est saisi. Le code présenté ci-dessous permet d'ajouter (concaténer) ce chiffre à la variable "op1".
Transition : operand1 / onDigit

onOperation

La transition "onOperation" stocke l'opération choisie dans la variable "oper". La condition qui suit sert à reprendre le dernier résultat comme premier opérande. Ce cas particulier sera détaillé plus tard...
Transition : onOperation

operand2 / onDigit

Sans surprise, cette transition fonctionne de la même manière que pour "operand1".
Transition : operand2 / onDigit

Fonctionnement

Voici un exemple de séquence que peut traiter cette machine à états : "1 2 + 1 2 3 =". Les valeurs numériques provoquent des transitions "onDigit", les opérations (+, -, *, /) "onOperation" et le = "onEqual". Juste avant le "onEqual", voici le contenu des variables : op1=12; oper=+; op2=123; result = ''.
La suite de cette partie présente chaque étape de l'exécution en détail.

Initialisation

La machine démarre à l'état "start" puis passe à "clear" et à "reset". Une fois ces trois étapes terminées, toutes les variables sont initialisées et vides.

Premier opérande

Ensuite, la machine entre dans l'étape "operand1". Pour sortir de l'état "opérand1", il est possible de demander la transition "onClear", qui va réinitialiser les variables et se remettre à l'état "operand1", ou "onOperation" qui permet de continuer l'exécution. A l'intérieur de l'état "opérand1", il y a une "sous-machine". Celle-ci ne contient que deux états "waiting_op1" et "display_op1", la transition de l'une à l'autre s'effectue grâce à la transition "onDigit" qui s'occupe de remplir la variable "op1".

Opération

En utilisation normale, la sortie de "operand1" s'effectue grâce à la transition "onOperation" qui va donner une valeur à la variable "oper".

Deuxième opérande

Après cela, la machine se retrouve dans l'état "operand2". Cet état fonctionne exactement de la même manière que "operand1" mais en remplissant la variable "op2". Les possibilités de sorties sont "onClear" ou "onEqual".

Résultat

la transition "onEqual" va faire passer la machine par les états "compute", qui demandera à java de faire le calcul, "display_result", qui demandera l'affichage, puis retour à "reset" et donc à "operand1".

Cas particulier

Précédemment, j'ai parlé d'un cas particulier pour "onOperation". C'est à ce moment que ce cas peut se produire. La machine est à l'état "operand1" et les variables op1, op2 et oper sont vides. Si dans ce cas, la transition "onOperation" est appelée, le cas particulier fait que le contenu de result est copié dans "op1". Ainsi, il est possible de réaliser une séquence du genre "1 + 2 = + 3 =". Le "1 + 2 =" nous amène à "operand1" et le "+ " force ce cas particulier, du coup le deuxième calcul est : "3 + 3 =" ou avec les variables (result = 3 => op1=3; oper=+; op2=3).

Conclusion

Dans ce premier article sur ce sujet, j'ai présenté une machine à états permettant d'implémenter une calculatrice quatre opérations. Cette machine à été réalisée à l'aide de la technologie SCXML. Dans l'article suivant, je vais présenter la partie "Java" de cette application.