I. Versions utilisées

GWT a connu pas mal d'évolution depuis sa création, et continue d'évoluer. À partir de la version 2.6, GWT utilise par défaut Java 7. Il reste possible de choisir la version 6 de Java, mais il n'y a aucune raison pour ceci dans notre tutoriel. Voici donc les versions des outils utilisées dans cette série :

  • Java 1.7
  • Maven 3.1.0
  • Eclipse Kepler 4.3
  • GWT 2.6

Je ne détaillerai pas l'installation des 3 premiers, qui font normalement partie des indispensables de tout développeur Java normalement constitué, surtout le premier... Je me concentrerai exclusivement sur GWT.

II. Installation du plugin Google pour Eclipse

Ceci est extrêmement simple. Dans Eclipse, ouvrez l'Eclipse Marketplace, et cherchez google :

Eclipse Marketplace

Cliquez sur le bouton Install, et suivez les instructions. Ce plugin concerne tous les outils et frameworks de Google, pas seulement GWT. Vous pouvez vous contenter de ceux qui vous intéressent, ou les installer tous si vous préférez. Pour GWT, je vous conseille d'installer au moins :

  • Google Plugin for Eclipse 4.3 : un plugin pour travailler avec l'ensemble des produits Google dans Eclipse.
  • Google Web Toolkit SDK 2.6.0 : Le framework GWT proprement dit.
  • GWT Designer Core
  • GWT Designer Editor
  • GWT Designer GPE

Les trois derniers sont des éditeurs graphiques, avec lesquels il sera possible de dessiner notre interface.

Et voilà, nous pouvons commencer à travailler.

III. Premier projet GWT

III-A. Création dans Eclipse

Avant d'utiliser Maven, nous allons créer un projet depuis Eclipse, ce qui nous permettra d'inspecter l'organisation d'un projet GWT, qui diffère sensiblement de la structure classique des projets Maven.

La création d'un projet GWT se fait avec CTRL + N, et en choisissant Web Application Project dans la fenêtre ci dessous :

Image non disponible

L'étape suivante consiste à entrer le nom du projet et le package à utiliser.

Image non disponible

Nous allons créer un projet de gestion de bibliothèque, nous l'appellerons donc atabib et le package sera fr.atatorus.atabib. Mais libre à vous de renommer tout cela si vous le souhaitez.

Décochez la case « Use Google App Engine », et laissez cochée la case « Generate project sample code ». Cliquez ensuite sur « Finish » pour obtenir le squelette d'une application GWT pleinement fonctionnelle. Exécutons la donc.

III-B. Exécution de l'application

Cliquons sur l'icône exécution dans la barre d'outils d'Eclipse, et choisissons atabib :

Image non disponible

L'application se lance en mode développement. Dans Eclipse, la vue correspondante s'est ouverte :

Image non disponible

Dans ce mode, l'application tourne sur un serveur Jetty compris dans le SDK, et nécessite un plugin pour fonctionner. En effet, à ce moment, notre application n'est pas compilée. Il s'agit toujours de Java qui s'exécute, ce qui permet de débogguer l'application comme n'importe quel programme Java. Pour s'exécuter dans le navigateur, nous aurons besoin d'un plugin, qui simule le fonctionnement de l'application dans le navigateur en javascript. On pourrait dire qu'il agit comme un compilateur JIT (Just In Time). Ce plugin s'installera automatiquement quand vous vous connecterez à votre application.

Comme demandé, double-cliquez sur l'URL. Pour la première utilisation du mode développement de GWT, un message vous demande donc d'installer le plugin dans votre navigateur :

Image non disponible

Au moment où j'écris cet article, le plugin n'est plus compatible avec Firefox 27 sur Linux, mais tourne très bien dans Chromium. Une fois le plugin installé, l'application se contente de nous demander notre nom et affiche une boite de dialogue avec diverses informations sur notre système :

Image non disponible

Voilà, nous avons notre première application GWT. Elle n'est pas très évoluée, mais ne nous a pas demandé beaucoup d'efforts non plus. Elle est cependant suffisante pour explorer la structure d'une application GWT.

III-C. Structure d'une application GWT

Voici la structure du projet atabib à la création :

Image non disponible

On remarque 3 packages dans le répertoire src :

  • client : c'est dans ce package qu'on place les classes Java qui seront compilées en Javascript. Ces classes sont soumises à certaines restrictions, toutes les classes du JDK ne sont pas traduisibles en Javascript. Le plugin a déjà généré pour nous une classe et deux interfaces :
    • Atabib : il s'agit du point d'entrée de notre application. Elle doit implémenter l'interface com.google.gwt.core.client.EntryPoint.
    • GreetingService : c'est l'interface du service de l'application.
    • GreetingServiceAsync : c'est l'interface asynchrone correspondant à notre service. Elle dépend directement de l'interface du service. Nous verrons quand nous attaquerons les communications avec le serveur comment tout ceci fonctionne.
  • server : ce package contient les classes s'exécutant sur le serveur, qui ne seront pas traduites en Javascript. La classe GreetingServiceImpl est l'implémentation du service coté serveur.
  • shared : un autre package à traduire en Javascript. Dans notre squelette d'application, il ne contient qu'un simple validateur de champ qui s'assure que le nom n'est pas vide.

Cette organisation est celle par défaut, mais il est possible de la changer, en modifiant le fichier Atabib.gwt.xml. C'est dans ce fichier qu'on trouve, entre autre, la définition du point d'entrée de l'application, ainsi que la liste des packages à traduire en Javascript, client et shared dans notre cas.

Le répertoire war est le répertoire qui contiendra le site web lui-même. On y trouve la page de notre site et sa feuille de style. Je dis bien la page de notre site, car GWT fonctionne selon le principe de la page unique. Le fichier web.xml contient la description de notre application et des servlets comme toute application J2EE classique.

Vous pouvez jeter un coup d'œil à ces fichiers, je ne vais pas m'étendre davantage dessus pour l'instant. Mais rassurez-vous, nous y reviendrons bientôt plus en détail.

Vous avez déjà sans doute remarqué que l'organisation du projet ne correspond pas du tout au schéma type d'une application Maven, pourtant un outil des plus utiles pour le développement Java. Mais il est bien entendu possible d'utiliser Maven avec GWT grâce au plugin approprié.

IV. GWT et Maven

IV-A. Création du projet

Le plugin Maven d'Eclipse ne propose pas d'artefact pour créer un projet GWT, nous allons donc devoir passer par la ligne de commande :

 
Sélectionnez
mvn archetype:generate -DarchetypeGroupId=org.codehaus.mojo -DarchetypeArtifactId=gwt-maven-plugin -DarchetypeVersion=2.6.0

Maven va vous poser quelques questions, concernant le groupId, l'artifactID, … Mettez ce que vous voulez, il ne s'agit que d'identifier votre projet, pas de configurer quoi que ce soit pour l'instant. Voici mes réponses :

  • groupId : fr.atatorus,
  • artifactId : atabib,
  • version : 1.0-SNAPSHOT,
  • package : fr.atatorus.atabib,
  • module : Atabib.

Notre projet créé, importons le dans Eclipse. Sélectionnez le menu « File > Import », et choisissez l'assistant « Existing Maven Project » :

Image non disponible

Cliquez sur « Next> », puis chercher le répertoire du projet créé :

Image non disponible

Recliquez sur « Next > » :

Image non disponible

L'assistant nous signale une erreur. En réalité, je pense qu'il s'agit d'un bug du plugin Maven d'Eclipse, d'autant plus qu'il me semble l'avoir lu quelque part, mais c'est à confirmer. Ceci ne nous empêche en rien de continuer. Cliquez sur « Finish », et notre projet apparait dans l'espace de travail. Nous allons pouvoir l'examiner.

IV-B. Structure du projet

Déplions l'arborescence, et examinons là plus en détails pour voir les différences par rapport à notre projet précédent :

Image non disponible

Nous retrouvons la structure type des projets Maven, avec src/main/java, src/main/ressources, etc. Nous remarquons également que la classe GreetingServiceAsync a migré : elle se retrouve dans target/generated-sources/gwt. Nous avons dit précédemment que son code dépend de l'interface du service. Le plugin est donc tout à fait capable de la générer à notre place.

Il y a cependant une erreur de syntaxe dans la classe Atabib. Ouvrez là, et vous verrez pourquoi Eclipse se plaint : il ne connait pas la classe Messages. Normal, elle n'existe pas encore.

Comme pour la classe GreetingServiceAsync, il s'agit d'une classe générée, dont le rôle consiste à gérer les messages de notre application. On retrouve les messages dans les fichiers Messages.properties et Messages_fr.properties. Nous y reviendrons quand on parlera de l'internationalisation de notre application. Pour l'instant, nous devons corriger cet oubli du plugin.Dans la console, tapez

 
Sélectionnez
mvn generate-sources

Cette commande va générer toutes les classes nécessaires à notre projet, c'est à dire GreetingServiceAsync et Messages. Rafraichissez le projet Eclipse, et tout rentrera dans l'ordre : la classe Messages apparait à coté de sa consœur. Parmi les autres différences avec le projet précédent, nous possédons maintenant une classe de test.

IV-C. Exécution du projet

Avant de lancer notre projet, nous devons intervenir dans le pom, pour dire à Maven d'utiliser Java 7 :

 
Sélectionnez
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
        <source>1.7</source>
        <target>1.7</target>
    </configuration>
</plugin>

Maintenant que c'est fait, nous pouvons exécuter notre projet. Ça peur se faire à la ligne de commande avec Maven :

 
Sélectionnez
mvn gwt:run

Nous voyons surgir une fenêtre du mode développement, comme au bon vieux temps de GWT 1.x :

Image non disponible

Cliquez sur « Launch Default Browser », et notre application s'exécutera à l'identique de la précédente.

Bien entendu, il nous est possible de lancer l'application directement depuis Eclipse, avec le menu contextuel « Run As > Web Application ». Cette fois, la fenêtre du mode développement est remplacée par un onglet dans Eclipse.

Note : si vous avez un message d'erreur dans la console à propos du module GWTJUnit qu'Eclipse n'arrive pas à charger, c'est sans doute une réminiscence de notre ancien projet. Aller dans « Run Configurations... », et effacer toutes les anciennes configurations. Après, tout devrait rentrer dans l'ordre.

V. Anatomie d'un projet GWT

Maintenant que nous savons créer et exécuter une application GWT, nous pouvons inspecter le mécanisme un peu plus en détail. Nous allons partir de notre projet Maven pour ceci.

GWT produisant des applications Web, commençons donc naturellement pas examiner à quoi ressemble notre page.

V-A. La page HTML

Je l'ai déjà dit, un projet GWT ne contient qu'une seule page HTML, même s'il peut être utile de découper de très gros projet en plusieurs modules, chacun ayant sa propre page. Mais en tout état de cause, une seule page nous suffit. Examinons donc le code de la page de notre application, débarrassé des commentaires superflus :

 
Sélectionnez
<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <link type="text/css" rel="stylesheet" href="Atabib.css">
    <title>Web Application Starter Project</title>
    <script type="text/javascript" language="javascript" src="Atabib/Atabib.nocache.js"></script>
  </head>
  <body>
    <!-- OPTIONAL: include this if you want history support -->
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
    <!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
    <noscript>
      <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
        Your web browser must have JavaScript enabled
        in order for this application to display correctly.
      </div>
    </noscript>

    <h1>Web Application Starter Project</h1>
    <table align="center">
      <tr>
        <td colspan="2" style="font-weight:bold;">Please enter your name:</td>
      </tr>
      <tr>
        <td id="nameFieldContainer"></td>
        <td id="sendButtonContainer"></td>
      </tr>
      <tr>
        <td colspan="2" style="color:red;" id="errorLabelContainer"></td>
      </tr>
    </table>
  </body>
</html>

Dans l'en-tête, on retrouve le lien vers un script Atabib.nocache.js : Il s'agit du script qui produit notre interface utilisateur. Il crée et détruit des objets, qu'il attache et détache de l'arbre DOM de la page. Nous ne voyons donc pas notre champ de texte ou notre bouton. Pour les voir, nous avons besoin d'inspecter le DOM de la page. Voici celui de notre table :

Image non disponible

On y voit bien apparaitre notre champ texte et notre bouton, ainsi qu'un label pour les erreurs. Nous les retrouvons tous dans notre code Java :

 
Sélectionnez
RootPanel.get("nameFieldContainer").add(nameField);
RootPanel.get("sendButtonContainer").add(sendButton);
RootPanel.get("errorLabelContainer").add(errorLabel);

Nous obtenons nos éléments containers, en l'occurrence les cellules de la table, via le RootPanel, et nous ajoutons simplement nos éléments à l'intérieur. Il est tout à fait possible de se passer de tout élément dans notre page. On peut très bien créer une interface à l'intérieur d'une page vide.

Notez aussi une petite iframe pour gérer l'historique : puisqu'on est dans une application mono-page, cliquer sur le bouton retour en arrière nous la fait quitter. Il est donc primordial de mettre en place un mécanisme qui s'en charge. Ça aussi nous le verrons.

Et maintenant, au tour de notre code Java d'être examiné.

V-B. Code Java

Notre code Java se répartit en 2 classes et une interface :

  • Atabib : il s'agit de la principale classe de l'application.
  • GreetingService : interface de notre service, elle est annotée ce qui permet au compilateur GWT de mettre en place toute la plomberie nécessaire à la communication avec le serveur en Ajax.
  • FieldVerifier : Pour contrôler la validité de nos données.

Nous n'examinerons que la première. L'interface du service sera l'objet d'un article prochain à elle toute seule. Quant à la dernière, son code n'appelle aucun commentaire tellement il est basique.

Examinons donc notre classe Atabib. Il s'agit du point d'entrée de notre application, ainsi que le signale l'interface EntryPoint :

 
Sélectionnez
public class Atabib implements EntryPoint {
    ...
    public void onModuleLoad() {
        ...
    }
}

Une fois la page chargée, le code qui s'exécute est celui de la méthode onModuleLoad(). Le code de notre méthode peut se décomposer en deux parties.

On commence par créer nos widgets (lignes 2 à 5) et les mettre en place (lignes 12 à 14). On crée aussi une boite de dialogue qui affichera la réponse du serveur :

 
Sélectionnez
  1. public void onModuleLoad() { 
  2.     final Button sendButton = new Button(messages.sendButton()); 
  3.     final TextBox nameField = new TextBox(); 
  4.     nameField.setText(messages.nameField()); 
  5.     final Label errorLabel = new Label(); 
  6.  
  7.     // We can add style names to widgets 
  8.     sendButton.addStyleName("sendButton"); 
  9.  
  10.     // Add the nameField and sendButton to the RootPanel 
  11.     // Use RootPanel.get() to get the entire body element 
  12.     RootPanel.get("nameFieldContainer").add(nameField); 
  13.     RootPanel.get("sendButtonContainer").add(sendButton); 
  14.     RootPanel.get("errorLabelContainer").add(errorLabel); 
  15.  
  16.     // Focus the cursor on the name field when the app loads 
  17.     nameField.setFocus(true); 
  18.     nameField.selectAll(); 
  19.  
  20.     // Create the popup dialog box 
  21.     final DialogBox dialogBox = new DialogBox(); 
  22.     dialogBox.setText("Remote Procedure Call"); 
  23.     dialogBox.setAnimationEnabled(true); 
  24.     final Button closeButton = new Button("Close"); 
  25.     // We can set the id of a widget by accessing its Element 
  26.     closeButton.getElement().setId("closeButton"); 
  27.     final Label textToServerLabel = new Label(); 
  28.     final HTML serverResponseLabel = new HTML(); 
  29.     VerticalPanel dialogVPanel = new VerticalPanel(); 
  30.     dialogVPanel.addStyleName("dialogVPanel"); 
  31.     dialogVPanel.add(new HTML("<b>Sending name to the server:</b>")); 
  32.     dialogVPanel.add(textToServerLabel); 
  33.     dialogVPanel.add(new HTML("<br><b>Server replies:</b>")); 
  34.     dialogVPanel.add(serverResponseLabel); 
  35.     dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT); 
  36.     dialogVPanel.add(closeButton); 
  37.     dialogBox.setWidget(dialogVPanel); 
  38.  
  39.     ... 
  40. } 

Une fois ceci fait, la seconde partie sert à « câbler » la gestion des évènements sur nos widgets. Il s'agit simplement d'handler instanciés sous forme de classes anonymes :

 
Sélectionnez
  1. public void onModuleLoad() { 
  2.     ... 
  3.  
  4.     // Add a handler to close the DialogBox 
  5.     closeButton.addClickHandler(new ClickHandler() { 
  6.         public void onClick(ClickEvent event) { 
  7.             dialogBox.hide(); 
  8.             sendButton.setEnabled(true); 
  9.             sendButton.setFocus(true); 
  10.         } 
  11.     }); 
  12.  
  13.     // Create a handler for the sendButton and nameField 
  14.     class MyHandler implements ClickHandler, KeyUpHandler { 
  15.         /** 
  16.          * Fired when the user clicks on the sendButton. 
  17.          */ 
  18.         public void onClick(ClickEvent event) { 
  19.             sendNameToServer(); 
  20.         } 
  21.  
  22.         /** 
  23.          * Fired when the user types in the nameField. 
  24.          */ 
  25.         public void onKeyUp(KeyUpEvent event) { 
  26.             if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { 
  27.                 sendNameToServer(); 
  28.             } 
  29.         } 
  30.  
  31.         /** 
  32.          * Send the name from the nameField to the server and wait for a 
  33.          * response. 
  34.          */ 
  35.         private void sendNameToServer() { 
  36.             // First, we validate the input. 
  37.             errorLabel.setText(""); 
  38.             String textToServer = nameField.getText(); 
  39.             if (!FieldVerifier.isValidName(textToServer)) { 
  40.                 errorLabel.setText("Please enter at least four characters"); 
  41.                 return; 
  42.             } 
  43.  
  44.             // Then, we send the input to the server. 
  45.             sendButton.setEnabled(false); 
  46.             textToServerLabel.setText(textToServer); 
  47.             serverResponseLabel.setText(""); 
  48.             greetingService.greetServer(textToServer, 
  49.                     new AsyncCallback<String>() { 
  50.                         public void onFailure(Throwable caught) { 
  51.                             // Show the RPC error message to the user 
  52.                             dialogBox 
  53.                                     .setText("Remote Procedure Call - Failure"); 
  54.                             serverResponseLabel 
  55.                                     .addStyleName("serverResponseLabelError"); 
  56.                             serverResponseLabel.setHTML(SERVER_ERROR); 
  57.                             dialogBox.center(); 
  58.                             closeButton.setFocus(true); 
  59.                         } 
  60.  
  61.                         public void onSuccess(String result) { 
  62.                             dialogBox.setText("Remote Procedure Call"); 
  63.                             serverResponseLabel 
  64.                                     .removeStyleName("serverResponseLabelError"); 
  65.                             serverResponseLabel.setHTML(result); 
  66.                             dialogBox.center(); 
  67.                             closeButton.setFocus(true); 
  68.                         } 
  69.                     }); 
  70.         } 
  71.     } 
  72.  
  73.     // Add a handler to send the name to the server 
  74.     MyHandler handler = new MyHandler(); 
  75.     sendButton.addClickHandler(handler); 
  76.     nameField.addKeyUpHandler(handler); 
  77. } 

L'appel à notre service se fait à la ligne 48. Dans notre code coté client, nous utiliserons toujours l'interface asynchrone. L'interface du service ne nous sert qu'à l'instantiation de l'interface asynchrone, grâce à la classe GWT :

 
Sélectionnez
private final GreetingServiceAsync greetingService = GWT.create(GreetingService.class);

L'interface GreetingServiceAsync reprend les mêmes méthodes que GreetingService, en y rajoutant un paramètre AsyncCallback. C'est cet objet qui contient le code à exécuter lors de la réponse du serveur, dans ses méthodes onSuccess() et onFailure().

Restons en là pour l'instant, nous y reviendrons dans un prochain article qui expliquera plus en détail la mise en œuvre des communications asynchrones avec le serveur.

Jetons maintenant un dernier coup d'œil au fichier principal d'une application GWT.

V-C. Le fichier Atabib.gwt.xml

C'est lui qui va définir notre point d'entrée dans l'application, ainsi que les packages à compiler en Javascript. En voici un extrait sans les commentaires :

 
Sélectionnez
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='Atabib'>
  <inherits name='com.google.gwt.user.User' />
  <inherits name='com.google.gwt.user.theme.standard.Standard' />

  <entry-point class='fr.atatorus.atabib.client.Atabib' />

  <source path='client' />
  <source path='shared' />

</module>

Comme toute application GWT, la nôtre est un module, qui hérite (utilise pourrait-on dire) deux autres modules : User, qui contient les définitions des widgets, et Standard, qui concerne le style de nos widgets. Il existe d'autres modules, pour gérer les formats JSON et XML, ou pour les tests avec Junit.

Nous définissons ensuite le point d'entrée de notre application, puis les packages sources qui doivent être compilés en Javascript. Java et Javascript sont des langages quelques peu différents, aussi tout ce qui existe en Java ne peut pas se compiler en Javascript. C'est notamment le cas de tout ce qui concerne la gestion des threads (Javascript est mono-thread), et des nombres (Javascript ne connait que les nombres flottants, avec une étendue moindre que le type long de Java). Pour la liste complète des restrictions, vous pouvez aller sur la page qui y est consacrée dans la documentation de GWT.

VI. Conclusion

Voilà pour ce premier article à propos de GWT. J'espère vous avoir suffisamment intéressé pour avoir envie de poursuivre. Nous n'avons en effet fait que gratter la surface d'un projet GWT, il va nous falloir aller un peu plus en profondeur pour apprécier toute la puissance de ce framework. Nous nous y attèlerons dans la seconde partie de cette série d'articles, en commençant par voir comment créer et utiliser une interface utilisateur.

VII. Liens