vendredi 27 mai 2011

Intégration de GWT dans un projet Spring - Partie 2

Nous avons vu comment intégrer GWT dans un projet Maven, nous allons maintenant nous intéresser à la communication GWT/Spring.

Définition des services

Pour communiquer avec le serveur, GWT utilise une interface de service utilisée coté client et coté serveur. Cette interface va définir les méthodes pouvant être appelées coté client par GWT, l'implémentation étant réalisée coté serveur.
Dans notre exemple, nous allons réaliser une interface UserService qui contiendra une méthode login prenant en paramètres un nom d'utilisateur et un mot de passe. En retour, elle renverra un objet de type User.

Pour commencer, nous allons créer un package service dans le package com.myjavaland.spring.gwt.
Dans ce package, nous allons ajouter notre interface service.


package com.myjavaland.spring.gwt.service;

import org.apache.catalina.User;

import com.google.gwt.user.client.rpc.RemoteService;


@RemoteServiceRelativePath("userService.rpc")
public interface UserService extends RemoteService{
    public User login(String login, String password);
}

L'interface étend RemoteService, cette interface ne définit aucune méthode mais va permettre à GWT de traiter cette interface comme une interface de service distant. Nous avons également utilisé l'annotation @RemoteServiceRelativePath qui prend une chaîne de caractère en argument. Cette URL va nous permettre de faire le mapping entre notre client et notre serveur. Concrètement, lorsque GWT va faire son appel à la procédure login, il va en réalité envoyer une requête HTTP en mode POST à l'URL http://server/project/userService.rpc en lui passant ses paramètres dans le corps du message HTTP. Le serveur devra alors intercepter la requête et l'envoyer au bon service.

Nous allons également définir un bean User. Cet objet sera partagé entre la partie cliente et serveur, nous allons donc placer la classe dans un package nommé com.myjavaland.spring.gwt.shared.

package com.myjavaland.spring.gwt.shared;

public class User implements Serializable {
   
    private static final long serialVersionUID = 1L;
   
    private long id;
    private String firstName;
    private String lastName;
    private String login;
    private String password;
   
    public User(){   
    }
   
    public User(long id, String firstName, String lastName, String login,
            String password) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.login = login;
        this.password = password;
    }


    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public String getLogin() {
        return login;
    }
    public void setLogin(String login) {
        this.login = login;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", firstName=" + firstName + ", lastName="
                + lastName + "]";
    }
}

Ce bean classique définit 5 attributs et leurs accesseurs associés.

Nous voyons que la classe UserService comporte une erreur : Missing asynchronous interface UserServiceAsync. Nous devons en effet créer une interface, dite asynchrone, qui reprend les méthodes de l'interface UserService, mais dont le type de retour est void et qui possède un paramètre supplémentaire de type AsyncCallBack<User>. C'est cette interface qui va nous permettre d'appeler les procédures distantes depuis la partie cliente.

GWT nous propose un outil pour créer automatiquement notre interface asynchrone, il suffit de cliquer sur l'erreur pour voir apparaître la popup suivante :


Une fois la fenêtre de création validée, nous obtenons le code suivant :


package com.myjavaland.spring.gwt.service;

import org.apache.catalina.User;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface UserServiceAsync {
    void login(String login, String password, AsyncCallback<User> callback);
}

Nous voyons notre méthode login, les 2 paramètres d'origine puis un 3ème permettant la gestion des retours d'appel distant, avec pour type générique, le type de retour de notre méthode login d'origine.

Nous allons maintenant voir comment appeler notre méthode depuis notre application cliente.

Pour cela, nous allons ouvrir le fichier Application.java présent dans le package com.myjavaland.spring.gwt.client. Dans l'article précédent, nous avions défini 2 TextBox et un Button au sein d'un DecoratorPanel. Nous allons modifier la classe comme suit :

 package com.myjavaland.spring.gwt.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DecoratorPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;

public class Application implements EntryPoint {

    public void onModuleLoad() {
                // Nommage de nos composants
        final TextBox txtLogin;
        final TextBox txtPassword;
        final Button btConnexion;
       
        // (1) Création d'un layout en tableau
        FlexTable layout = new FlexTable();
        layout.setCellSpacing(6);
        FlexCellFormatter cellFormatter = layout.getFlexCellFormatter();

        // (2) Ajout du titre de la page
        layout.setHTML(0, 0, "Connexion");
        cellFormatter.setColSpan(0, 0, 2);
        cellFormatter.setHorizontalAlignment(0, 0,
                HasHorizontalAlignment.ALIGN_CENTER);

        // (3) Création de 2 TextBoxes pour le login et le mot de passe
        layout.setHTML(1, 0, "Login");
        txtLogin = new TextBox();
        layout.setWidget(1, 1, txtLogin);
        layout.setHTML(2, 0, "Password");
        txtPassword = new TextBox();
        layout.setWidget(2, 1, txtPassword);

        // (4) Ajout du bouton de connexion
        btConnexion = new Button("Connexion");
        layout.setWidget(3, 0, btConnexion);
        cellFormatter.setColSpan(3, 0, 2);
        cellFormatter.setHorizontalAlignment(3, 0,
                HasHorizontalAlignment.ALIGN_CENTER);

        // (4b)
        btConnexion.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
               //TODO Appel de procédure distante
            }
        });

        // (5) Création du Panneau d'affichage et définition du layout
        DecoratorPanel decPanel = new DecoratorPanel();
        decPanel.setWidget(layout);

        // (6) Ajout du Panneau dans la page courrante
        RootPanel.get("content").add(decPanel);

    }
}

Par rapport au code précédent, nous avons externalisé les 3 composants TextBox et Button, nous les utiliserons plus tard. Nous avons par ailleurs ajouté au repère 4b, un Handler sur le bouton. Lorsque l'utilisateur clique sur le bouton Connexion, la méthode onClick est appelée.

Il ne nous reste plus maintenant qu'à appeler notre méthode distante.

Pour comprendre le fonctionnement d'un appel à une procédure distante, nous allons étudier l'un des mécanismes fondamentaux de GWT : La liaison différée.

La liaison différée

La liaison différée (ou deferred binding) est un mécanisme puissant permettant l'instanciation d'une classe générée automatiquement à partir d'une interface ou d'une classe abstraite. Comment cela fonctionne-t-il ?

GWT propose une méthode : GWT.create. Cette méthode prend en paramètre une interface ou une classe abstraite et en fonction d'un certain nombre de règles définies dans les fichiers de configuration de modules de GWT, une instance d'une classe dérivée de l'interface ou de la classe abstraite va être créée. Dans le cas d'une procédure distante, nous allons créer un attribut userService dans la classe Application comme suit :

private  UserServiceAsync userService = GWT.create(UserService.class);

Lors de la compilation (et uniquement lors de la compilation, GWT n'étant pas capable de réaliser de la génération de code JavaScript à l’exécution), GWT va regarder les règles associées à l'interface UserService et remarquer que UserService étend RemoteService. Pour celà, il se base sur le fichier /com/google/gwt/rpc/RPC.gwt.xml qui définit la règle.

<generate-with class="com.google.gwt.rpc.rebind.RpcServiceGenerator">
  ...
  <when-type-assignable class="com.google.gwt.user.client.rpc.RemoteService" />
  ...
</generate-with>


Nous voyons donc que lorsqu'une interface héritant de RemoteService est appelée par la méthode GWT.Create, GWT va lui attribuer un Generator RpcServiceGenerator.

Ce generator va créer le code correspondant à une implémentation de la classe. Dans le cas d'un service distance, cela consiste à créer non pas une implémentation de UserService, mais de UserServiceAsync. La classe, pour chacune des méthodes, servira en fait à construire la requête RPC, à la transmettre au client et une fois la réponse renvoyée par le serveur, à décoder les flux de retour.

Notre attribut userService est donc une instance d'une classe générée à la compilation par GWT.

Maintenant que nous avons défini notre service distant, nous allons voir comment l'appeler.

Appel de procédures distantes

Au repère 4b, nous avons laissé une place libre pour réaliser notre appel, à la place du TODO, nous allons insérer le code suivant :


               userService.login(txtLogin.getValue(), txtPassword.getValue(), new AsyncCallback<User>() {
                    public void onSuccess(User user) {
                        if(user != null){
                            Window.alert("Authentication Ok : "  + user);
                        } else {
                            Window.alert("Wrong login or password");
                        }
                    }

                    public void onFailure(Throwable caught) {
                        Window.alert("Authentication failed - Error : " + caught.getMessage());               
                    }
                }); 

Dans le code ci-dessus, nous voyons que nous avons appelé la méthode login de la classe UserServiceAsync. Nous lui passons en paramètre les textes présents dans les TextBox txtLogin et txtPassword. Le 3ème paramètre est une classe anonyme pour laquelle nous allons définir 2 méthodes imposées par l'interface AsyncCallBack onSuccess et OnFailure. Pourquoi n'avons nous pas pu, plus simplement appeler directement login de l'interface UserService comme suit :

User user = userService.login(login, password);

Simplement parce que les appels distants se font de manière asynchrone. En effet, imaginons que notre serveur soit chargé et qu'il mette plusieurs secondes à nous renvoyer un résultat. Notre navigateur sera bloqué en attendant sa réponse. L'intérêt d'utiliser des appels distants de manière asynchrone est que notre navigateur peut réaliser d'autres tâches (réaliser un calcul, mettre à jour une IHM, etc..) en attendant le retour du serveur.

Lorsque le serveur va nous retourner une réponse, 2 cas peuvent se présenter :
  • Le serveur n'a pas rencontré de problème et nous renvoie la réponse, la méthode onSuccess est appelée.
  • Le serveur a rencontré un problème (Exception, Erreur 404, etc...), la méthode onFailure est appelée.
Dans notre cas, la méthode onSuccess vérifie que l'utilisateur renvoyé par le serveur est différent de null pour autoriser la connexion. 

Pour terminer le code de la partie cliente, il ne nous reste plus qu'à ajouter les packages service et shared comme source dans le fichier de configuration de module de notre application  : Application.gwt.xml

 <source path="service" />
 <source path="shared" />

Grâce à ces lignes, GWT saura qu'il doit également aller chercher les classes dans les packages service et shared.

Nous venons de terminer le code de la partie client de l'application. Nous devons maintenant nous intéresser à la partie serveur.

Développement coté serveur

La première chose que nous allons faire est de créer un package com.myjavaland.spring.gwt.server.
Ce package contiendra toutes nos classes relatives aux serveurs. Dans un projet plus conséquent, il peut être utile de séparer le code client et serveur dans 2 projets distincts.

Dans un package com.myjavaland.spring.gwt.server.service, nous allons coder l'implémentation de notre service UserService.

package com.myjavaland.spring.gwt.server.service;

import com.myjavaland.spring.gwt.service.UserService;

import com.myjavaland.spring.gwt.shared.User;

public class UserServiceImpl implements UserService {

    public User login(String login, String password) {
        if("foo".equals(login) && "123".equals(password)){
            return new User(1, "Eric", "PELIEU", "foo", "123");
        }
        return null;
    }
}



Le code ici est trivial, nous vérifions simplement que l'utilisateur se logue avec foo et 123. Sinon, nous renvoyons un utilisateur null.

La prochaine étape consiste à définir notre service comme un bean Spring. Pour cela, nous allons renommer notre fichier sample-servlet.xml en applicationContext.xml et y définir notre bean.

 <?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="userService" class="com.myjavaland.spring.gwt.server.service.UserServiceImpl" />

</beans>

Il est important ici, de nommer notre bean de la même manière que l'URL défini dans l'interface UserService via l'annotation @RemoteServiceRelativePath. C'est en effet grâce à cette similitude que nous serons capable de faire le mapping entre un service GWT et un bean Spring.

Nous devons maintenant indiquer à notre serveur comment traiter la requête userService.rpc. Nous avons pour ça 2 possibilités :
  • Définir pour chaque service dans le web.xml une servlet correspondante
  • Définir une servlet unique pour chaque service et réaliser le dispatching sur cette servlet.
Nous allons mettre en oeuvre la 2nd solution.
La servlet va réaliser plusieurs actions :
  • Récupérer la requête GWTRPC,
  • Transformer les paramètres de la requête en objets java,
  • Trouver la classe et la méthode associées à la requête parmi nos bean Spring,
  • Appeler la méthode de la classe en lui passant les paramètres,
  • Récupérer et encoder le résultat de l'appel,
  • Envoyer le résultat encodé au client.
Nous voyons qu'il y a beaucoup de choses à faire avant de pouvoir appeler notre service. Par chance, GWT propose déjà tous les mécanismes pour décoder, appeler, encoder et transmettre une requête. Nous allons donc massivement réutiliser le code de GWT en y apportant de petites modifications afin de l'adapter à notre projet.
Pour ce faire, nous allons créer une classe dans un package nommé com.myjavaland.spring.gwt.server.adapter.gwt

package com.myjavaland.spring.gwt.server.adapter.gwt;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RpcTokenException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class GenericRPCServlet extends RemoteServiceServlet {

    private static final long serialVersionUID = 1L;

    public String processCall(String payload) throws SerializationException {
        // First, check for possible XSRF situation
        checkPermutationStrongName();

        try {
            // Récupération du bean associé à l'URL
            String beanName = extractBeanName(getThreadLocalRequest()
                    .getServletPath());

            // Chargement de l'applicationContext
            WebApplicationContext applicationContext = WebApplicationContextUtils
                    .getWebApplicationContext(getServletContext());

            // Récupération du bean
            Object bean = applicationContext.getBean(beanName);


            // Décodage de la requête
            RPCRequest rpcRequest = RPC.decodeRequest(payload, bean.getClass(),
                    this);
            onAfterRequestDeserialized(rpcRequest);
            // Appel de la méthode et encodage de la réponse
            return RPC.invokeAndEncodeResponse(bean, rpcRequest.getMethod(),
                    rpcRequest.getParameters(),
                    rpcRequest.getSerializationPolicy(), rpcRequest.getFlags());
        } catch (IncompatibleRemoteServiceException ex) {
            log("An IncompatibleRemoteServiceException was thrown while processing this call.",
                    ex);
            return RPC.encodeResponseForFailure(null, ex);
        } catch (RpcTokenException tokenException) {
            log("An RpcTokenException was thrown while processing this call.",
                    tokenException);
            return RPC.encodeResponseForFailure(null, tokenException);
        }
    }

    private String extractBeanName(String servletPath)
            throws IncompatibleRemoteServiceException {
        if (servletPath == null) {
            throw new IncompatibleRemoteServiceException(
                    "servletPath can not be null");
        }

        String[] tServletPath = servletPath.split("/");
        if (tServletPath.length < 2) {
            throw new IncompatibleRemoteServiceException(
                    "The servletPath has failes. ServletPath : " + servletPath);
        }
        String servletName = tServletPath[tServletPath.length - 1];
        return servletName.substring(0, servletName.length() - 4);
    }

}

La classe RemoteServiceServlet est une classe GWT qui va nous fournir les mécanismes nécessaires à l’exécution de notre requête. La méthode principale de cette classe est la méthode processCall : C'est cette méthode qui, à partir des données brutes du POST (String Payload), va appeler la bonne méthode et retourner le résultat correctement encodé. Nous allons la recopier et la modifier légèrement.

RemoteServiceServlet est, à l'origine, prévue pour être héritée par les implémentations de services. Ainsi, RemoteServiceServlet se contente d'appeler les méthodes définies dans la classe qui l'hérite. Ici, nous allons récupérer la classe grâce au nom de la requête : La méthode extractBeanName va nous permettre d'obtenir le nom du bean associé à la requête. Dans notre exemple, si nous appelons http://localhost:8080/MavenWebApplication/userService.rpc, la méthode va nous renvoyer "userService". Nous n'avons alors plus qu'à rechercher dans notre contexte Spring le bean nommé userService. Ceci explique pourquoi il est impératif de nommer de manière identique nos beans et nos url de service.
Une fois notre bean récupéré, nous pouvons le passer en paramètre de la méthode invokeAndEncodeResponse et laisser le soin RemoteServiceServlet d'instancier, encoder et renvoyer le resultat.

Pour finir, nous devons modifier notre fichier web.xml pour lui faire router les appels RPC vers notre servlet.

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>com.myjavaland.spring.gwt.server.adapter.gwt.GenericRPCServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.rpc</url-pattern>
    </servlet-mapping>
</web-app>



La dernière étape consiste à ajouter gwt dans le pom.xml de notre projet. En effet, en ajoutant le plugin, nous avons ajouté les librairies nécessaires à GWT dans le classpath  d'eclipse, mais une fois notre application déployée, ces librairies ne sont pas recopiées. Il suffit d'ajouter les dépendances suivante dans le pom.xml.

<dependency>
            <groupId>com.google.gwt</groupId>
            <artifactId>gwt-servlet</artifactId>
            <version>2.3.0</version>
            <type>jar</type>
            <scope>runtime</scope>
            <optional>false</optional>
        </dependency>
        <dependency>
            <groupId>com.google.gwt</groupId>
            <artifactId>gwt-user</artifactId>
            <version>2.3.0</version>
            <type>jar</type>
            <scope>provided</scope>
            <optional>false</optional>
 </dependency>


Conclusion

Il nous suffit maintenant de lancer notre serveur tomcat et notre client GWT pour s'assurer que tout fonctionne correctement.



Et voilà, nous sommes maintenant en mesure d'utiliser Spring et GWT simultanément. Cette architecture présente un certain nombre d'avantages : Nous avons désormais accès à tous les modules de Spring, nous pouvons facilement intégrer un système de persistence comme hibernate et surtout, nous disposons d'une application complètement découplée. Il est ainsi facile de modifier le programme pour utiliser notre bean userService au sein d'un webService par exemple.

Lors d'un prochain article, nous verrons comment ajouter une couche DAO à l'aide d'Hibernate à notre application. Nous verrons également comment intégrer les tests unitaires à notre application.

mardi 19 avril 2011

Intégration de GWT dans un projet Spring - Partie 1

Nous avons vu précédemment comment intégrer Spring et Maven au sein d'un projet Web. Nous allons maintenant nous intéresser à un framework également très intéressant : GWT.

GWT (pour Google Web Toolkit) est un ensemble d'outils permettant de créer des interfaces graphiques web évoluées.

Le principe est simple :

Nous codons notre partie cliente en java et GWT transforme ce code java en code javascript. L'ensemble de l'application est ensuite exécutée sur le navigateur du client, le serveur d'application n'a plus qu'à répondre à des requête AJAX, l'aspect graphique étant entièrement géré par le navigateur. GWT propose également des outils de debug et d'analyse de performance.


Nous allons maintenant voir comment intégrer GWT dans un projet Maven et Spring. Dans cette première partie, nous n'aborderons pas la communication client/serveur. Cela fera l'objet d'un prochain article.

Installation du plugin GWT

Google met à disposition des développeurs un plugin eclipse qui intègre le SDK GWT. Très classiquement, celui-ci s'installe depuis le menu Help -> Install New Software...


La fenêtre suivante nous permet de spécifier le plugin que nous souhaitons installer. Sur la page d'installation de GWT, nous trouvons l'URL à spécifier : http://dl.google.com/eclipse/plugin/3.6/


Nous avons besoin du plugin et du SDK de Google Web Toolkit, je vous invite cependant à installer également le SDK de Google App Engine, cela pourra toujours nous servir.

Une fois, le plugin installé et Eclipse redémarré, nous pouvons commencer.

 Intégration de GWT dans un projet Maven

Reprenons le projet Spring MVC que nous avons réalisé lors du précédent article. La première chose à faire est de renommer le package principal com.myjavaland.mvc.spring par com.myjavaland.gwt.spring et de supprimer le fichier LoginController.java . Nous allons également créer un nouveau package nommé client.

Nous allons maintenant intégrer GWT à notre projet maven. Pour ce faire, il suffit de se rendre dans les propriétés de GWT en réalisant un clic droit sur le projet :

 

et de valider la case à cocher Use Google Web ToolKit.


Comme nous allons utiliser l'arborescence Maven, nous devons également spécifier l'endroit où GWT doit générer ses fichiers. Pour cela, il faut sélectionner le menu Web Application juste au dessus et cocher la case This project has a WAR directory.


En cliquant sur Ok, nous venons d'ajouter GWT à notre projet et de définir le chemin de compilation dans src/main/webapp.

Nous pouvons vérifier que notre projet est correctement configuré en vérifiant les librairies du projet. Normalement, les 2 librairies GWT doivent être ajoutées comme suit :


Développement du client

Développement Java

Nous devons maintenant réaliser nos classes Java qui seront ensuite traduite en Javascript.

Nous allons spécifier à GWT que le code java présent dans le package  com.myjavaland.gwt.spring.client doit être compilé en javascript. Pour cela, nous allons créer dans le package principal com.myjavaland.gwt.spring un fichier nommé Application.gwt.xml :


<!DOCTYPE module PUBLIC "//gwt-module/" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.2/distro-source/core/src/gwt-module.dtd">
<module>
    <!-- Inherit the core Web Toolkit stuff. -->
    <inherits name='com.google.gwt.user.User' />
    <!-- inherit css based theme -->
    <inherits name='com.google.gwt.user.theme.standard.Standard' />
    <!-- Specify the application specific style sheet. -->
    <stylesheet src='Application.css' />
    <!-- Application entry point -->
    <entry-point class='com.myjavaland.spring.gwt.client.Application' />
    <!-- source package -->
    <source path="client" />
</module>

Dans ce fichier, nous spécifions les modules nécessaires à l'exécution de l'application, le fichier css de l'application que nous allons créer plus tard. Les deux dernières lignes sont importantes :
  • L'entry-point est la classe principale de notre application. Elle contient la méthode onModuleLoad qui s'apparente à la fonction main d'une application java classique.
  • La balise source nous permet de spécifier le (ou les) package(s) contenant nos fichiers sources. Pour spécifier plusieurs packages, il suffit d'ajouter plusieurs balises source.
 Nous allons maintenant définir cette classe principale dans le package com.myjavaland.mvc.spring.client

package com.myjavaland.spring.gwt.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DecoratorPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;

public class Application implements EntryPoint {

    public void onModuleLoad() {
        // (1) Création d'un layout en tableau
        FlexTable layout = new FlexTable();
        layout.setCellSpacing(6);
        FlexCellFormatter cellFormatter = layout.getFlexCellFormatter();

        // (2) Ajout du titre de la page
        layout.setHTML(0, 0, "Connexion");
        cellFormatter.setColSpan(0, 0, 2);
        cellFormatter.setHorizontalAlignment(
            0, 0, HasHorizontalAlignment.ALIGN_CENTER);

        // (3) Création de 2 TextBoxes pour le login et le mot de passe
        layout.setHTML(1, 0, "Login");
        layout.setWidget(1, 1, new TextBox());
        layout.setHTML(2, 0, "Password");
        layout.setWidget(2, 1, new TextBox());
      
        // (4) Ajout du bouton de connexion
        layout.setWidget(3, 0, new Button("Connexion"));
        cellFormatter.setColSpan(3, 0, 2);
        cellFormatter.setHorizontalAlignment(
            0, 0, HasHorizontalAlignment.ALIGN_CENTER);

        // (5) Création du Panneau d'affichage et définition du layout
        DecoratorPanel decPanel = new DecoratorPanel();
        decPanel.setWidget(layout);
      
        // (6) Ajout du Panneau dans la page courrante
        RootPanel.get("content").add(decPanel);

    }
}

 Lors de la compilation ou de l’exécution (en mode devMode) du programme, GWT va chercher la classe Application et appeler la méthode onModuleLoad().


Nous réalisons plusieurs choses dans cette méthode :
  1. Nous définissons un layout. Un layout est une stratégie de positionnement des composants. Dans notre cas, FlexTable n'est pas à proprement parlé un layout, il s'agit plutôt d'un composant contenant d'autres composants de manière ordonnée.
  2. Nous ajoutons notre premier composant à notre layout. Comme pour du HTML classique, nous spécifions que la cellule de notre première ligne utilisera 2 colonnes avec la propriété colSpan.
  3. Nous ajoutons ensuite 2 champs de saisies login et password. Nous spécifions les indices des lignes et colonnes pour ordonner notre affichage.
  4. Nous terminons notre formulaire par un bouton de connexion sur le même principe que le titre de la fenêtre.
  5. Nous créons notre panneau principal et lui ajoutons le flexTable que nous avons défini.
  6. Nous récupérons le conteneur principal pour lui ajouter notre panneau principal. La String "content" va nous permettre de faire référence à une div dans notre page HTML de démarrage.
Nous venons de définir les fichiers présents dans le code java, il ne nous reste maintenant plus qu'à modifier le contenu du répertoire webapp.

Développement Web

Dans l'arborescence src/main/webapp/WEB-INF, nous allons dans un premier temps pouvoir supprimer le répertoire views s'il est encore présent.
Nous pouvons également effacer les 3 beans du fichier  sample-servlet.xml, afin d'avoir un fichier beans vide que nous modifierons lors de l'ajout des services.
Nous supprimons également les balises servlet et servlet-mapping du fichier web.xml.

A ce niveau, nous n'avons plus aucune attache à Spring et la librairie pourrait très bien être retirée du projet. Patience, nous l'utiliserons à nouveau très bientôt.

Il ne nous reste plus qu'à modifier le fichier index.html pour le rendre compatible avec GWT. 



<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>GWT Spring Application</title>

<link type="text/css" rel="stylesheet" href="Application.css">
<!-- Chargement du fichier javascript de notre application -->
<script type="text/javascript" language="javascript"
    src="com.myjavaland.spring.gwt.Application/com.myjavaland.spring.gwt.Application.nocache.js"></script>
</head>
<body>
    <noscript>Erreur : Javascript Non Supporté</noscript>
<!-- Définition du div contenant notre application -->
    <div id="content" align="center"></div>
</body>
</html>

Ce fichier est classique, il contient un lien vers le script javascript généré par GWT, et une div nommée content qui va nous permettre de positionner notre composant principal (pour nous un DecoratorPanel) dans notre page HTML.

Nous voyons que le code fait référence à un fichier CSS : Application.css. Nous allons le créer dans le répertoire webapp et le laisser vide pour le moment.

Une lecture attentive du code nous fait remarquer que la page HTML pointe vers un fichier javascript qui n'existe pas : com.myjavaland.spring.gwt.Application/com.myjavaland.spring.gwt.Application.nocache.js

Logique ! Car ce fichier est normalement généré par GWT lors de la compilation et nous n'avons pas encore lancé de compilation sur notre projet. Pour ce faire, il suffit de faire un clic droit sur le projet et de sélectionner Google > GWT Compile.



La fenêtre qui apparait permet de spécifier les options de compilation :
Nous retrouvons notre Entry Point défini dans le fichier Application.gwt.xml.

Une fenêtre peut éventuellement s'ouvrir, nous demandant de renseigner le chemin du répertoire WAR :


Il suffit de lui indiquer le chemin du répertoire src/main/webapp.

La compilation dure plusieurs secondes. A la fin de celle-ci, nous remarquons la création d'un répertoire com.myjavaland.spring.gwt.Application. Ce répertoire contient toute notre application convertie en javascript.


L'arborescence finale de notre application doit ressembler à ça : 




Il ne nous reste plus qu'à lancer notre serveur d'application Tomcat et d'aller à l'adresse http://localhost:8080/MavenWebApplication/ pour observer le résultat :






  Debug d'application GWT

Bien qu'intéressante, cette application souffre d'un gros handicap: nous devons recompiler l'application GWT  à chaque modification de l'IHM. Sachant que sur une grosse application, cette compilation peut durer plusieurs minutes, il n'est pas envisageable d'utiliser cette technique pour le développement d'application.

Heureusement, le plugin de GWT vient à notre rescousse en proposant un outil de compilation à la volée (permettant également le debug) appelé devMode. Pour l'utiliser, il nous suffit de faire un clic droit sur le projet et de sélectionner Run As -> Web Application (running as external server).



Le plugin nous demande de renseigner des informations sur le serveur :



Une fois lancé, une vue s'active sous Eclipse pour nous indiquer l'URL de notre page :

 En copiant cette URL et en la saisissant sous un navigateur (Firefox par exemple), nous obtenons un message indiquant la nécessité de télécharger un plugin. Ce plugin permettra la communication entre le programme Java lancé sous Eclipse et le navigateur.


Une fois le plugin installé, le page apparait.
Lorsque nous modifions le code source de l'application, celui-ci est directement mis à jour sur le navigateur après rafraichissement.
Nous pouvons également utiliser la commande "Debug as..." pour débugger le code Java d'une application s'exécutant sur le navigateur, comme nous le ferions pour n'importe quelle autre application.

Conclusion

Nous venons de voir dans cette article comment intégrer GWT à un projet Maven. Nous avons abordé les phases de compilation et de débuggage d'un projet GWT sur un serveur autonome.
Cependant, nous n'avons pas du tout abordé la notion de communication client/serveur. Cette notion fera l'objet d'un article prochainement.

Code source de l'article 

lundi 7 mars 2011

Utilisation de Spring MVC dans un projet WTP/Maven

Après avoir installé Eclipse et configuré notre premier projet web à l'aide de maven, nous allons aujourd'hui nous intéresser à un Framework très en vogue : Spring

Dans cet article, nous allons mettre en œuvre un petit site d'exemple sous forme d'un login en utilisant les possibilités MVC de Spring.

Installation de Spring

Grâce à Maven, nous allons pouvoir ajouter facilement les librairies nécessaires à Spring pour notre application.
Il suffit d'ajouter l'artifact de Spring dans le fichier pom.xml. Pour celà, nous avons 2 possibilités :

Ajouter directement le code dans le fichier

<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>3.0.5.RELEASE</version>
        <optional>false</optional>
 </dependency>

Nous allons demander directement le téléchargement du module MVC de Spring, Maven va alors se charger pour nous de télécharger toutes les dépendances nécessaires à l'utilisation de spring

Utiliser le plugin IAM

Le 1er onglet de configuration propose 4 sous-onglets, dont  un nommé Dependencies :

En cliquant sur New, nous pouvons saisir directement les informations concernant la librairie


Le bouton [...] en haut à droite offre une option intéressante, il suffit de saisir une partie du nom de la librairie pour que maven télécharge les artifacts disponibles correspondant aux patterns saisis.


Sur cet écran, nous pouvons télécharger spring-webmvc en version 3.0.5.RELEASE. Une fois enregistré, le fichier pom.xml est modifié de la même manière que manuellement.

Si nous regardons les librairies, nous voyons que Maven a téléchargé toutes les librairies nécessaires à l'utilisation de Spring en mode MVC


Nous voyons par ailleurs que l'ensemble des librairies sont téléchargées dans le repository local de maven (Ici C:\Users\Eric\.m2\). Ce repository sera commun à tous nos projets maven.

Nous allons également avoir besoin de 2 autres librairies que nous ajouterons directement dans notre fichier pom.xml :


   <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <optional>false</optional>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
        <optional>false</optional>
    </dependency>

Configuration du projet Web

Maintenant que nous disposons de toutes les libraires nécessaires à la réalisation de notre projet, il nous faut créer et configurer les fichiers.

Le premier fichier à configurer est le fichier \src\main\webapp\WEB-INF\web.xml. Nous allons demander à spring de s'occuper du mapping de notre application. Pour cela, il nous faut définir une servlet spécifique.

<servlet>
   <servlet-name>sample</servlet-name>
   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
   <load-on-startup>1</load-on-startup>
</servlet>

 Nous allons également définir un mapping pour renvoyer toutes les requêtes finissant par .form dans
 la servlet que nous venons de créer.
 
<servlet-mapping>
    <servlet-name>sample</servlet-name>
    <url-pattern>*.form</url-pattern>
</servlet-mapping>

Le fichier web.xml est maintenant configuré pour transmettre toutes les requêtes se terminant par form à la classe DispatcherServlet, il ne nous reste plus qu'à configurer Spring pour lui indiquer vers quelle classe transmettre les requêtes en fonction de leur URL.

Notre servlet s'appelant sample, le fichier de configuration Spring doit, par convention, se nommer sample-servlet.xml et se trouver dans le répertoire WEB-INF de notre projet.

Ce fichier va contenir la définition de nos beans.

Le bean est le concept à la base de spring : Il s'agit d'une description XML faisant référence à une classe java. Ce formalisme permet de mettre en œuvre les 2 concepts fondamentaux de spring : L'inversion de contrôle et l'injection de dépendance. Spring et sa syntaxe feront l'objet d'un article à part entière. L'important, ici, est de comprendre que nous allons définir un ensemble d'objets (appelés bean) au format XML que Spring va utiliser pour réaliser son mapping.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">




     <!-- Spécification du mapping -->
     <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/login.form">loginController</prop>
            </props>
        </property>
    </bean>

    <!-- les contrôleurs de l'application -->
    <bean id="loginController" class="com.myjavaland.spring.mvc.LoginController" />
 
    <!-- le résolveur de vues -->
    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/WEB-INF/views/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>


</beans>

Nous définissons 3 beans dans ce fichier :
  • Le premier bean va indiquer à Spring quelle méthodologie de mapping, nous allons utiliser. Le bean s'appelle SimpleUrlHandlerMapping, ce qui signifie que nous allons utiliser le nom de l'URL pour lui associer un contrôleur. La propruniété mappings va nous permettre de spécifier que l'url /login.form va être traité par le contrôleur loginController
  • Le 2ème bean est le contrôleur proprement dit. Nous lui donnons un Id afin qu'il puisse être identifié par les autres beans (notamment le bean de mapping défini auparavant)  ainsi que le nom de la classe associée. Nous verrons plus tard comment cette classe est codée.
  • Le 3ème est dernier bean va nous permettre de spécifier comment vont être trouvées les vues de notre modèle MVC. Nous voyons ici que nous allons utiliser JsltView et que nos vues vont avoir le pattern /WEB-INF/views/*.jsp.
Développement des vues et du contrôleur.

 Notre application est maintenant configurée, il ne nous reste plus qu'à développer nos différents composants : les vues et le contrôleur.

La page d'index :


La première chose à faire est de coder notre page de démarrage. C'est elle qui affichera le formulaire de login. Nous créerons cette page dans le répertoire webapp de notre projet et nous la nommerons index.html

<html>
<body>
<form method="POST" action="login.form">
    <table align="center">
        <tr>
            <td colspan="2" align="center">Connexion</td>
        </tr>
        <tr>
            <td>Login</td>
            <td><input type="text" name="login" /></td>
        </tr>
        <tr>
            <td>Password</td>
            <td><input type="text" name="password" /></td>
        </tr>
        <tr>
        <tr>
            <td colspan="2" align="center"><input type="submit" /></td>
        </tr>
    </table>
</form>
</body>
</html>

Nous voyons dans ce code que nous avons définit 2 champs : login et password et que l'appui sur le bouton de soumission appelle la page login.form

Le contrôleur :

Dans le fichier Spring que nous venons d'écrire, nous avons défini le nom de notre contrôleur :  com.myjavaland.spring.mvc.LoginController. Nous allons donc créer cette classe dans le répertoire src/main/java de notre application. Pour fonctionner, cette classe doit implémenter l'interface org.springframework.web.servlet.mvc.Controller. Cette interface impose l'implémentation d'une méthode : handleRequest.


Cette méthode prend 2 paramètres très connus des développeurs de servlets : HttpServletRequest et HttpServletResponse. Ces 2 paramètres vont nous permettre de travailler avec les données de la requêtes (paramètres, attributs, locales, etc...) et celles de la réponse.


La type de retour de cette méthode est ModelAndView, nous allons donc devoir renvoyer ce type d'objet pour que Spring puisse afficher correctement notre page.


Voyons maintenant le code de notre programme.


package com.myjavaland.spring.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class LoginController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        String login = request.getParameter("login");
        String password = request.getParameter("password");
        request.setAttribute("login", login);
        if("foo".equals(login) && "123".equals(password)){   
            return new ModelAndView("main");
        } else {
            return new ModelAndView("loginError");
        }
    }
}


L'implémentation de la méthode est très basique, nous allons dans un premier temps récupérer les valeurs de login et password et les vérifier. Puis, en fonction de ces valeurs, nous allons appeler l'une ou l'autre des vues. 

Souvenons-nous de notre fichier de configuration Spring dans lequel nous spécifions le pattern des vues. La ligne return new ModelAndView("main") aura pour conséquence l'appel de la jsp /WEB-INF/views/main.jsp alors que return new ModelAndView("loginError") appellera /WEB-INF/views/loginError.jsp

Passons tout de suite à la création des vues


Les vues :



Dans le répertoire WEB-INF, nous allons créer un répertoire views dans lequel nous allons mettre 2 jsp : main.jsp et loginError.jsp servant respectivement à valider une connexion ou afficher un message d'erreur.


Les 2 JSP sont on ne peut plus simple :


main.jsp


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ page isELIgnored="false" %>
<html>
    <body>
        <h1>Bienvenue ${login}</h1>
    </body>
</html>

loginError.jsp 



<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ page isELIgnored="false" %>
<html>
    <body>
        <p>
        Impossible de connecter en tant que ${login}
        </p>
    </body>
</html>


Voilà, notre application est prête à être testée, il ne nous reste plus qu'à ajouter le projet sur tomcat si ça n'est pas déjà fait et à lancer le serveur pour pouvoir tester que tout fonctionne bien !




Sources du programme