Logging tomcat et hibernate

Introduction

Dans une application d'un client que je reprends pour de la maintenance, j'ai un problème. Cette application est faite pour Oracle et, sur ma machine de test, je n'ai aucune envie d'installer cette base de données plutôt gourmande en ressources.

Cette application utilise l'ORM hibernate pour abstraire tous les accès à la base de données. C'est donc, normalement, un jeu d'enfant d'utiliser un au SGBD. J'ai donc décidé d'utiliser le MySQL 5.1.36 qui est installé sur ma machine. Ensuite, j'ai modifié le fichier "persistence.xml" pour qu'hibernate se connecte à une nouvelle base de données sous MySQL.

Problème

Alors que tout devrait fonctionner sans problème, en lançant l'application, je n'ai aucune table qui est créée.

Dans le logging de cette application, j'obtiens l'erreur suivante :

ERROR org.hibernate.tool.hbm2ddl.SchemaUpdate - Unsuccessful: alter table nom_de_la_table add index FKCD10932052C35B33 (clé), add constraint FKCD10932052C35B33 foreign key (clé) references preview (id)
ERROR org.hibernate.tool.hbm2ddl.SchemaUpdate - Can't create table 'nom_de_la_table' (errno: 150)

Logging

Bon, le fait d'avoir un message d'erreur ne me pose pas plus de problèmes que ça. Par contre, ce qui m'embête c'est le fait que ce message est plutôt vague. Il me faut donc avoir un logging plus détaillé.

persistence.xml

Pour ce faire, j'ai activé les logging de requêtes dans persistence.xml

 

C'est un bon début, mais je ne vois toujours pas les requêtes de création de tables qui m'intéressent...

log4j.properties

La première chose à faire est d'ajouter un fichier "log4j.properties" au bon endroit dans ce projet qui utilise "Maven" : Voici le contenu du fichier. J'y suis allé franchement en mettant les niveaux de logging à "debug" de manière à récupérer un maximum d'informations.

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
 
log4j.rootLogger=warn, stdout
log4j.logger.org.hibernate=debug, stdout
log4j.logger.org.hibernate.SQL=debug, stdout
log4j.logger.org.hibernate.type=debug, stdout
log4j.logger.org.hibernate.engine.QueryParameters=debug, stdout

Solution

En fouillant dans ce logging très détaillé, je trouve le problème suivant :

09:12:59,704 DEBUG SchemaExport:415 -
    create table NOM_DE_LA_TABLE (
        CLE integer not null,
        ...
        primary key (CLE)
    ) type=InnoDB
Cette requête n'est pas correcte. "type=InnoDB" ne fonctionne pas avec ma version de MySQL, il faudrait "Engine=InnoDB". Je dois donc, simplement, changer le dialecte SQL :
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>

Conclusion

En fait, j'avais créé moi-même le problème en utilisant le mauvais dialecte MySQL. Mais, sans un bon système de logging, je n'avais aucun moyen de m'en apercevoir. Je me dis que ça peut arriver à n'importe qui...

Essai d’utilisation de la bibliothèque « KineticJS » dans un projet GWT (Google Web Toolkit)

Introduction

Le but de cet article est de tester la bibliothèque "KineticJS". Celle-ci bénéficie d'une meilleure intégration à GWT que "D3.js" car un wrapper "KineticGWT" existe.

Du coup elle devrait la remplacer avantageusement dans un projet de ce type.

Projet de test

Pour générer le projet de test je vais utiliser un archetype Maven pour générer le squelette du projet.

mvn archetype:generate \
   -DarchetypeGroupId=org.codehaus.mojo \
   -DarchetypeArtifactId=gwt-maven-plugin \
   -DarchetypeVersion=2.5.0
  • groupId: conceptforge.kinetics
  • artifactId: TestKinetics
  • version: 1.0-SNAPSHOT
  • package: conceptforge.kinetics
  • module: TestKinetics

A ce stade, le projet est fonctionnel, mais c'est le projet par défaut généré par l'archetype Maven.

Après une bonne cure d'amaigrissement, le projet est devenu un magnifique "Hello World" à partir duquel je vais pouvoir commencer à intégrer "KineticJS".

Voici un aperçu des fichiers qui restent dans le projet :

Le code source complet de ce Hello World est disponible ici : TestKinetics1.tar.gz

Intégration de la librairie KineticsJS

Le portage de cette librairie sous GWT porte (naturellement) le nom de "KineticGWT". Le lien "github" met à disposition de la documentation pour l'intégration de cette librairie dans une application GWT/Maven. Ça tombe bien, c'est exactement le cas de ce programme de test.

Il faut donc :

  • Ajouter les nouvelles dépendances dans "pom.xml"
  • Ajouter le module dans "TestKinetics.gwt.xml"
  • Modifier l'implémentation de "onModuleLoad()"
package conceptforge.kinetics.client;
 
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;
import net.edzard.kinetic.*;
 
/**
 * Entry point classes define
 * <code>onModuleLoad()</code>.
 */
public class TestKinetics implements EntryPoint {
 
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        // Kinetic needs a special div in the DOM
        Element div = DOM.createDiv();
        RootPanel.getBodyElement().appendChild(div);
 
        // Setup stage
        Stage stage = Kinetic.createStage(div, 400, 400);
        Layer layer = Kinetic.createLayer();
        stage.add(layer);
 
        Rectangle c = Kinetic.createRectangle(new Box2d(10, 10, 200, 200));
        layer.add(c);
        stage.draw();
    }
}

Résultat

Site du zéro

Par chance le site du zéro propose un tutoriel sur KineticsJS.

Formes prédéfinies

Voici un exemple librement inspiré de la première partie du tutoriel (formes prédéfinies)

package conceptforge.kinetics.client;
 
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;
import net.edzard.kinetic.*;
 
/**
 * Entry point classes define
 * <code>onModuleLoad()</code>.
 */
public class TestKinetics implements EntryPoint {
 
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        Element div = DOM.createDiv();
        RootPanel.getBodyElement().appendChild(div);
 
        Stage scene = Kinetic.createStage(div, 400, 400);
        Layer calque = Kinetic.createLayer();
        scene.add(calque);
 
        Rectangle r = Kinetic.createRectangle(new Box2d(50, 75, 350, 300));
        r.setFill(Colour.blue);
        r.setStroke(Colour.blue);
        r.setStrokeWidth(0);
 
        Circle c = Kinetic.createCircle(new Vector2d(50, 50), 50);
        c.setFill(Colour.green);
        c.setStroke(Colour.darkgray);        
 
        RegularPolygon h = Kinetic.createRegularPolygon(new Vector2d(200,150), 50, 6);
        h.setRotation(30);
        h.setFill(Colour.red);
 
        calque.add(r);
        calque.add(c);
        calque.add(h);
 
        scene.draw();
    }
}

Résultat

Ombres

En m'inspirant du tutoriel "Opacité et ombre", j'ai ajouté une ombre aux formes de l'exemple précédent :

Pour arriver à ce résultat il suffit de créer l'instance suivante de "Shadow":

Shadow shadow = new Shadow(Colour.black, 12, new Vector2d(8, 8), 0.7);

Ensuite, il faut utiliser la méthode "setShadow" pour passer cette valeur à toutes les formes.

Gestion des événements

En m'inspirant du tutoriel "Interactions", j'ai écrit le code suivant pour observer le contenu de la variable "evt" passée en paramètre dans la gestion d'événements :

r.addEventListener(Event.Type.CLICK, new Node.EventListener() {
 
            public boolean handle(Event evt) {
                try {
                    Window.alert(evt.toString());
                } catch (Exception e) {
                    Window.alert(e.getMessage());
                }
                return true;
            }
        });

En testant, j'ai eu la désagréable surprise de recevoir le message suivant :

Grosse déception

Alors que tous mes tests jusqu'à maintenant me montraient une bonne librairie pour manipuler la balise Canvas et un bon wrapper pour GWT. Je viens de toucher aux limites de cette implémentation.
Lors de la gestion d'événements, la méthode "handle" d'un EventListener contient une variable evt mais celle-ci contient toujours "null".
Après une petite recherche dans le code source, j'ai trouvé (dans l'implémentation de la classe "Node") la méthode suivante qui explique mon problème :

	/**
	 * Add a multi-event listener to the node.
	 * @param eventTypes A number of events to listen to
	 * @param handler The handler.
	 */
	// Seems to be buggy in kineticjs. Always getting mousemove events
	// TODO Maik: It's not buggy, it just fails and then creates too many events due to the failure.
	//             the evt variable is directly from the browser, not from kineticjs, that's why there's
	//             a mismatch between event types
	//             Touch also needs to be handled differently if evt should be used.
	//       quick fix: ignore evt object
	public final native void addEventListener(List eventTypes, EventListener handler) /*-{
		this.on(@net.edzard.kinetic.Node::createEventTypeString(Ljava/util/List;)(eventTypes), function(evt) {
//			if (evt != null) {
//				console.log(evt.type);
//				var javaEvt = @net.edzard.kinetic.Event::new(Lnet/edzard/kinetic/Event$Type;Lnet/edzard/kinetic/Event$Button;II)(
//					@net.edzard.kinetic.Event.Type::valueOf(Ljava/lang/String;)(evt.type.toUpperCase()),
//					@net.edzard.kinetic.Event.Button::fromInteger(I)(evt.button),
//					evt.offsetX,
//					evt.offsetY
//				);
//				javaEvt.@net.edzard.kinetic.Event::setShape(Lnet/edzard/kinetic/Shape;)(evt.shape);
//				var bubble = handler.@net.edzard.kinetic.Node.EventListener::handle(Lnet/edzard/kinetic/Event;)(javaEvt);
//				evt.cancelBubble = !bubble;
//			} else {
				handler.@net.edzard.kinetic.Node.EventListener::handle(Lnet/edzard/kinetic/Event;)(null);
//			}
		});
	}-*/;

En attendant une correction de cette méthode, je propose une solution de contournement (pas très élégante, forcément) :

r.addEventListener(Event.Type.CLICK, new Node.EventListener() {
 
            public boolean handle(Event evt) {
                if (r.getFill().toString().equals(Colour.blue.toString())) {
                    r.setFill(Colour.white);
                } else {
                    r.setFill(Colour.blue);
                }
 
                scene.draw();
                return true;
            }
        });

Je fais un accès direct à l'objet. Ce n'est pas élégant et c'est beaucoup moins souple, mais, au moins, c'est fonctionnel.
En écrivant cette petite routine, j'ai eu une deuxième mauvaise surprise, mais moins grave que la précédente. La comparaison de la couleur ne fonctionne ni avec "==" ni avec "equals".

Conclusion

Tout s'est très bien passé jusqu'à ce que j'essaye de gérer les événements. Mon soupçon est que la librairie "KineticJS" est très fonctionnelle. Ce soupçon n'est pas fondé sur des arguments techniques, mais plutôt construit à partir des différents commentaires que j'ai pu lire à ce sujet sur le Web.

Par contre, le wrapper "KineticGWT" m'a permis de réaliser très facilement les exemples du tutoriel que j'ai utilisé tant que je ne faisais pas de gestion d'événements. Pour l'instant, il n'est qu'en version "0.9.1" c'est donc certainement un bug de jeunesse, mais à ce stade je ne sais pas si j'ose prendre le risque d'intégrer ce wrapper dans un projet réel et contourner tous les petits problèmes résiduels...

Représentation de données arborescentes à l’aide de D3.js

Introduction

Le but de cet article est de mettre en oeuvre la librairie D3.js pour représenter des données sous forme d’arborescence.

Données

Les données que je souhaite exploiter dans cet exemple sont stockées dans une base de données. La table "MenuItem" contient les éléments suivants :

Le champ "PARENT_ITEM" contient le "ID_MENU_ITEM" d'un élément de menu d'un ordre supérieur.

Cette simple table permet de stocker des menus sous forme arborescente.

Extraction

Etant donné que ces données sont déjà largement utilisées dans des projets Java, je vais écrire un petit programme de test en réutilisant au maximum les routines existantes. Mon but est d'obtenir un fichier JSON pour que D3.js puisse le représenter .

Maven

Dans les projets de production, Maven a été massivement utilisé pour lier les différents librairies et sous-projets. Je vais donc aussi utiliser Maven pour mon projet de test et ainsi importer facilement les librairies dont j'ai besoin et surtout l'accès aux données via hibernate qui va largement me simplifier la vie.

pom.xml

Ce fichier me permet de lier les librairies dont j'ai besoin :

  • JUnit : Pour les tests unitaires.
  • NavigationEntity : Librairie "maison" contenant les entités hibernate en rapport avec les données qui permettent à l'utilisateur de "naviguer" dans le programme.
  • Guice : Outil d'injection de dépendances.
  • Mysql-Connector : Pour accéder à mes données de test
  • XStream : Outils de sérialisation d'objets
  • Jettison : Librairie qui permet à XStream de sérialiser les données au format JSON
 
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>ch.conceptforge.group</groupId>
    <artifactId>json_d3</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>json_d3</name>
    <url>http://maven.apache.org</url>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>NavigationEntity</artifactId>
            <version>2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.inject.extensions</groupId>
            <artifactId>guice-persist</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.14</version>
        </dependency> 
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jettison</groupId>
            <artifactId>jettison</artifactId>
            <version>1.3.2</version>
        </dependency>        
    </dependencies>
</project>

Hibernate

Je ne vais pas entrer trop dans les détails, car c'est trop spécifique à la librairie maison "NavigationEntity". Celle-ci utilise Hibernate et Guice pour se connecter à la base de données et extraire les entités sous forme d'objets. Ceci nécessite de créer un fichier "persistence.xml" et de le configurer pour pouvoir utiliser la classe "MenuItem" qui encapsule les données présentées précédemment dans des objets.

Guice

De même que pour hibernate, cette partie est propre à la librairie maison "NavigationEntity", donc je ne vais pas entrer trop dans les détails. Je vais simplement préciser qu'il est nécessaire de construire une classe qui implémente l'interface "Module" de guice. Pour plus d'informations à ce propos, je vous invite à consulter les documents suivants :

Programme principal

package ch.conceptforge.paquet;
 
import ch.conceptforge.paquet.MenuItemTree;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
import com.thoughtworks.xstream.io.json.JsonWriter;
import java.io.Writer;
import javax.persistence.EntityManager;
 
public class App
{
 
    private static EntityManager createEntityManager() {
        Injector injector = Guice.createInjector(new GuiceModule());
        return injector.getInstance(EntityManager.class);
    }
 
    private static XStream createXStream() {
        XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {
 
            @Override
            public HierarchicalStreamWriter createWriter(Writer writer) {
                return new JsonWriter(writer, JsonWriter.DROP_ROOT_MODE);
            }
        });
 
        return xstream;
    }
 
    public static void main( String[] args )
    {
        // Liaison de l'entityManager à la classe MenuItem
        MenuItemTree.setEm(createEntityManager());
 
        // Construction de l'arborescence
        MenuItemTree mit = new MenuItemTree();
 
        // Exportation de l'arbre en JSON sur la console
        System.out.println(createXStream().toXML(mit));
    }
}

La partie la plus importante de ce petit programme de test est la construction de l'instance de "MenuItemTree". Le code qui précède cette instanciation permet d’accéder aux entités hibernate. Le passage de l'entityManager par une méthode statique n'est pas élégant, mais, s'agissant d'un petit programme de test, je ne vais pas y remédier. Le code qui suit l'instanciation de "MenuItemTree" permet de sérialiser cet objet. Il suffira ensuite de mettre cette chaine de caractères dans un fichier texte et de la lier à D3.js.

MenuItemTree

Cette classe va s'occuper d'interroger la base de données et de construire l’arborescence d'objets. Dans un esprit de réutilisabilité, j'ai découpé cette classe en deux niveaux d'abstraction.

Tree

Cette classe contient la structure de base d'un arbre qui pourra être utilisé par D3.js

package ch.conceptforge.paquet.tree;
 
import java.util.ArrayList;
import java.util.List;
 
public class Tree {
 
    private String name;
    private List children = null;
 
    public Tree() {
 
    }        
 
    public Tree(String name) {
        this.name = name;
    }
 
    protected void addChild(Tree tree) {
        if (children == null) {
            children = new ArrayList();
        }
        children.add(tree);
    }
 
    public List getChildren() {
        return children;
    }
}

MenuItemTree

Cette classe contient l'implémentation concrète de la structure arborescente pour mes MenuItems.

package ch.ultrasoft.cortex.tree;
 
import com.ultrasoft.server.core.entity.navigation.MenuItem;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
 
/**
 *
 * @author silenus
 */
public class MenuItemTree extends Tree {
 
    private static EntityManager em;        
 
    public static EntityManager getEm() {
        return em;
    }
 
    public static void setEm(EntityManager em) {
        MenuItemTree.em = em;
    }           
 
    public MenuItemTree() {
        super();
        parseItems(em.createQuery("from MenuItem where parentItem = null"));
    }
 
    public MenuItemTree(int id, String name) {
        super(name);
        parseItems(em.createQuery("from MenuItem where parentItem = " + id));
    }
 
    private void parseItems(Query q) {
        for (MenuItem mi : (List) q.getResultList()) {
            addChild(new MenuItemTree(mi.getId(),mi.getText()));
        }
    }
}

JSON

Comme expliqué précédemment, ce petit programme affiche sur la console les données au format json. Voici un extrait :

{
  "children": [
    {
      "name": "Stammdaten",
      "children": [
        {
          "name": "Adressetyp"
        },
        {
          "name": "Bauart"
        },
        {
          "name": "Betriebstyp"
...
        }
      ]
    }
  ]
}

La totalité des données est stockée dans le fichier test2.json

D3.js

A partir des nombreux exemples d'utilisation de cette librairie, voici quelques propositions d'affichage pour mon arborescence.

Le code html n'est pas particulièrement soigné, je souhaitais dans cet article me concentrer sur le rendu visuel des différentes variantes. Au final, une seule sera choisie pour être implémentée dans une application. C'est à ce moment-là que le code sera repris en profondeur.

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
------------------------------------------------------------------------
 
&gt;&gt;&gt; maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom &gt;&gt;&gt;
 
&lt;&lt;&lt; maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom &lt;&lt;&lt;
 
[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 – 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.

Java, machine à états et SCXML

Le problème

Il y a quelque temps, je devais développer un logiciel pour un client qui contenait beaucoup de boutons de commande. Pour la plupart des boutons, il y a des conditions du genre :

  • Si on clique sur "Ajouter" et qu'une édition est en cours ...
  • Si on clique sur "Enregistrer" et que c'était un ajout ...
  • Si on est en train éditer ou d'ajouter une donnée ...

Étant donné la quantité de boutons et de conditions, je me retrouvais rapidement dans un situation pénible pour gérer tous les cas de figure. De plus, je n'étais pas convaincu que le mandant arrivait à m'aider à définir toutes les conditions avant l'écriture du code. Donc, je m'attendais à une phase de tâtonnement avant de livrer la première version.

La proposition de solution

Il se trouve que, par hasard, à ce moment-là je suivais un cours sur la modélisation avec Monsieur Medard Rieder qui est très versé dans le développement d'applications embarquées. Nous avons notamment parlé de logiciels de commande d’ascenseurs. La problématique est assez comparable à la mienne. Beaucoup de boutons de commande et des conditions du genre :

  • Si l'utilisateur choisi le deuxième alors qu'il est au rez ...
  • Si un utilisateur appelle l'ascenseur ...
  • Si quelqu'un appelle ascenseur alors qu'il est au même étage ...

Pour résoudre ce problème, cela fait longtemps que les électroniciens ont résolu ce problème. Pour ce faire ils utilisent les machines à états (Finite State Machine).

Rational Rose

Pour ce cours de modélisation, nous avons utilisé Rational Rose. Concernant les machines à états, ce logiciel permet de les modéliser en UML grâce au diagramme d'états-transitions. Ensuite, le code source peut être généré dans différents langages de programmation.

Le but est atteint, mais à quel prix ? Rational Rose est un excellent produit, ce que je viens de présenter n'est qu'une infime partie de la partie émergée de l'iceberg. Le revers de la médaille  c'est que les licences sont coûteuses et donc largement disproportionnées pour résoudre mon problème de base.

Produit plus réaliste pour mes besoins

Ce que j'aimerais c'est rational rose, mais en moins cher. Je m'attends donc à devoir faire des compromis, mais voici les points importants

  • Représentation graphique de la machine à états (idéalement UML)
  • Passage automatique de la version graphique au code source (Java dans mon cas)
  • Bon marché

Editeurs UML

D'abord, je me suis concentré sur les produits concurrents à Rose, mais open source. Tout ce que j'ai vu était nettement inférieur à ma référence, mais également beaucoup moins onéreux.

Il y a d'abord  les logiciels de schématique UML que j'utilise régulièrement : Violet et UMLet Ils sont très simples et permettent de modéliser rapidement. Par contre le passage du modèle au code se faire manuellement.

ArgoUML permet de générer du code, mais d'après mes souvenirs, seulement pour le diagramme de classe.

Je n'avais pas tellement confiance en BOUML car l'auteur venait de se fâcher avec un administrateur de Wikipedia et qu'il voulait laisser tombé le projet.

SCXML

Ensuite, j'ai cherché un éditeur de machine à états et je suis tombé rapidement sur SCXML.

  • Fondation Apache
  • Normalisation W3C
  • Editeur graphique scxmlgui.

Par contre la symbolique n'est pas l'UML, mais bon le rapport qualité/prix m'a eu l'air suffisant pour l'étudier attentivement...

SCC

SCC est l'abréviation de SubCortex Client et SubCortex est le dépôt SubVersion qui contient le code source du projet Cortex.

Pour la gestion des versions, j'ai dû mettre en oeuvre un serveur SubVersion pour la société UltraSoft AG. Le choix de SubVersion était dicté par le mandant, car il possède déjà plusieurs serveurs de ce type pour d'autres projets.

Le projet Cortex est assez complexe. Il s'agit en réalité d'une vingtaine de projets et sous-projets qui constituent un framework permettant de construire rapidement des applications Web.

Pour gérer les dépendances entre les différentes versions des sous-projets, l'outil Maven a été utilisé. La combinaison Maven/SubVersion donne de très bons résultats. Le seul problème est qu'avec autant de projets, il est difficile pour les développeurs de gérer les bonnes versions. Il faut systématiquement ouvrir le pom.xml de tous les projets et gérer les dépendances à la ligne de commande. Mon client m'a donc demandé de réaliser un outil pour simplifier ces opérations...

Le programme SCC est développé en Java/Swing. Il utilise la librairie "snvkit" pour dialoguer avec le serveur SubVersion et une poignée de routines récursives pour les lectures sur le disque et l'affichage sous forme d'arbre.

La partie gauche de l'application présente tous les projets du dépôt et la partie droite affiche les différents outils.

SCC : page principale

Par exemple, un code de couleur montre les dépendances entre les différentes versions de sous-projets
SCC : Dépendances

Ou alors, la partie droite permet de visualiser le fichier "pom.xml"
SCC : pom.xml

Une fois le projet choisi dans l'arbre. Un simple click permet de télécharger l'ensemble des sources du projet et de tous les sous-projets avec lesquels il y a des dépendances.

Ecole des Arches

L'école des Arches prépare, entre autres, des jeunes à la maturité suisse, au baccalauréat international et au CFC d'informatique.

Depuis 2008, je donne des cours sur mandat dans cette école. Tous ces cours sont liés au développement logiciel.

  • Introduction au développement
  • Langages de programmation
    • Java
    • PHP
    • C
  • Modélisation

Cette activité représentait 20% à 25% de mon temps de travail, mais, depuis la rentrée 2012 ce pourcentage va augmenter pour dépasser les 50% et donc, devenir mon activité principale.