Intégration simple et élégante d'AJAX avec DWR

Baptiste Meurant

Ce tutoriel a pour objectif de présenter l'intégration du framework AJAX DWR à une application Web existante reposant sur Spring et Hibernate.

- Titre : Intégration simple et élégante d'AJAX avec DWR

- Auteur : Baptiste Meurant

- Parution : 23 octobre 2007

- Mise à jour : 19 février 2020

- Licence : Copyright ® 2007 Baptiste Meurant. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Architecture logicielle

- III. Mise en place

- IV. Création de la couche applicative dédiée

- V. Installation de DWR

- VI. Test via le debugger DWR

- VII. Intégration à Tapestry5

- VIII. Conclusion

- IX. Remerciements

- X. Dans la même série…

I - Introduction

Ce tutoriel n'a pas pour objectif de présenter DWR dans son intégralité, mais plutôt comment intégrer facilement ce framework AJAX à une application Web existante reposant sur Spring et Hibernate. Il fait suite au Tutoriel tapestry5, Spring et Hibernate.

DWR est un framework AJAX pour Java qui permet de « publier » des méthodes Java en JavaScript. Ces méthodes seront ensuite manipulables dans la page HTML. L'utilisation de DWR autorise ainsi la création d'applications Web 2.0 complètes tout en minimisant le code JavaScript nécessaire et en apportant la puissance, la souplesse et la robustesse de Java.

L'ensemble des échanges avec le serveur par XMLHttpRequest sont gérés de manière interne à DWR ce qui dispense le développeur de la résolution de ces problématiques. En outre, DWR repose sur l'excellent et très populaire framework JavaScript Prototype et offre certaines fonctionnalités et API JavaScript bien utiles et permettant la résolution de la majorité des problématiques de compatibilité entre navigateurs.

DWR fonctionne sur n'importe quel projet Web Java. Cependant, dans ce tutoriel, nous ne nous intéresserons qu'à son interconnexion avec un projet Spring, Hibernate. Pour cela nous prendrons comme base le premier tutoriel de cette série présentant l'application blanche Tapestry5, Spring, Hibernate. On note que la présence de Tapestry5 n'aura aucun impact sur DWR ; ces deux framework cohabitant sans interactions entre eux.

En effet seul la dernière partie fait intervenir Tapestry5 de manière assez légère. Il sera ainsi très facile de l'intégrer à n'importe quel autre framework front.

Ces deux tutoriels sont accessibles ici.

Les sources de ce tutoriel sont téléchargeables ici [miroir http]

II - Architecture logicielle

Comme cela avait été expliqué dans le tutoriel consacré à Spring et Hibernate, la séparation en couches d'une application favorise, entre autres, sa robustesse et son évolutivité. Dans cette optique, nous allons créer une nouvelle couche applicative située dans la couche front, au-dessus de la couche service.

Cette couche a pour tâche de faire l'interface entre le client, la page html et la couche service. Dans le cadre de ce tutoriel, cette couche peut paraître inutile, car elle est uniquement constituée de wrappers vers la couche service. Cependant, dans le cadre d'une vraie application, cette couche à une réelle utilité. En effet, cette couche - appelée dwr - accède, contrairement à la couche service à la requête http. Cela permet de récupérer des informations telles que la locale de l'utilisateur, son identité, de mettre en forme les formulaires postés, etc. Cette couche est réservée à une utilisation dans une application web alors que la couche service peut être publiée à travers des webServices, accédée depuis un client lourd, etc.

Comme on le voit sur la figure ci-dessous, cette couche est partie intégrante de la couche front, parallèlement à la couche Tapestry. Les aspects Tapestry et DWR, s'ils se côtoient dans la page html, n'interagissent jamais entre eux.

Le principe de DWR est relativement simple. La couche DWR, gérée également par Spring, publie un certain nombre de méthodes Java en JavaScript. En effet, toute classe définie dans la couche DWR et correctement configurée publiera les méthodes qui la composent. Le moteur DWR se chargera alors de créer l'interface JavaScript permettant d'appeler ces méthodes en AJAX, via XMLHttpRequest. Ces aspects sont totalement masqués par le framework DWR. De la même façon que DWR publie des méthodes Java en JavaScript, il transforme des objets Java en JavaScript. En effet, les méthodes publiées retournent des objets Java que DWR convertit en JavaScript s’il y est autorisé explicitement via des déclarations de Converters. De cette manière ces objets pourront être récupérés en JavaScript, après les appels de méthodes et affichés dans la page html.

Figure1 : Architecture logicielle

III - Mise en place

Si vous avez suivi le tutoriel précédent traitant de Tapestry, Spring et Hibernate :

- copiez ce projet depuis votre workspace Eclipse et copiez-le dans ce même workspace en changeant le nom.

Figure2 : Copie du projet

- allez ensuite dans les propriétés du projet, Web Project Settings et changez le nom du contexte.

Figure3 : Changement du contexte

Éditez ensuite le fichier org.eclipse.wst.common.component dans le répertoire .settings du nouveau projet. Modifiez l'élément wb-module deploy-name en changeant le nom du projet dupliqué par le nom du nouveau projet. Cela évite toute confusion dans la vue Server d'Eclipse (sans cela, c'est toujours l'ancien nom qui s'affiche, mais cela n'affecte pas le fonctionnement). Relancez ensuite Eclipse.

Si vous n'avez pas suivi le tutoriel précédent :

- récupérez les sources du tutoriel Tapestry5, Spring Hibernate ici. Le dézipper sur votre machine ;

- créez un nouveau projet en suivant la procédure décrite dans les chapitres II et III du premier tutoriel Tapestry5, Spring, Hibernate disponible ici ;

- importez l'ensemble du tutoriel (sources, lib, etc.) précédent dans ce nouveau projet ;

- redéfinissez le répertoire config comme source folder du projet.

À l'issue de ces étapes, le projet doit ressembler à cela :

Figure4 : Projet vide

Dans les deux cas :

- associez ce projet avec le serveur Tomcat.

IV - Création de la couche applicative dédiée

- Créez les packages tuto.webssh.web.dwr et tuto.webssh.web.dwr.impl.

- Créer respectivement dans ces packages l'interface UserDWR et l'implémentation UserDWRImpl. Ces deux objets seront associés par la création d'un bean Spring tel que défini dans le tutoriel Spring-Hibernate, de manière à accéder au UserManager de la couche service.

UserDWR

package tuto.webssh.web.dwr;

import javax.servlet.http.HttpServletRequest;

import org.directwebremoting.annotations.Param;
import org.directwebremoting.annotations.RemoteMethod;
import org.directwebremoting.annotations.RemoteProxy;
import org.directwebremoting.spring.SpringCreator;

import tuto.webssh.domain.model.User;

/**
* This interface publishes business features to handle user to web interfaces
* @author bmeurant
*/
public interface UserDWR {

/**
* Return a User object from a given login.
* @param login : user login
* @param request : the web client request used to get some specific data from client.
* @return the corresponding user object.
*/
public User getUser(String login, HttpServletRequest request);

/**
* This method checks the actual login and password and, if this verification
* is successful, change the password to 'password' for the given login.
* @param request : the web client request used to get some specific data from client.
* @param login : user login
* @param oldPass : user old password
* @param newPass : user new password
* @return the new User object
*/
public User changePassword(HttpServletRequest request, String login, String oldPass, String newPass);

}

UserDWRImpl

package tuto.webssh.web.dwr.impl;

import javax.servlet.http.HttpServletRequest;

import tuto.webssh.domain.model.User;
import tuto.webssh.service.UserManager;
import tuto.webssh.web.dwr.UserDWR;

/**
* Implements business features to handle user to web interfaces
* @author bmeurant
*/
public class UserDWRImpl implements UserDWR {

private UserManager userManager;

/**
* setter to allows spring to inject userManager implementation
* @param userManager : object (implementation of UserManager interface) to inject.
*/
public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}

/**
* {@inheritDoc}
*/
public User getUser(String login, HttpServletRequest request) {
return userManager.getUser(login);
}

/**
* {@inheritDoc}
*/
public User changePassword(HttpServletRequest request, String login,
String oldPass, String newPass) {
if (userManager.checkLogin(login, oldPass)) {
return this.userManager.changePassword(login, newPass);
}
else {
return null;
}
}
}

On note :

outre le paramètre métier 'login', on remarque un paramètre supplémentaire 'request'. Il s'agit d'un paramètre qui est automatiquement transféré par DWR du client vers le serveur afin de récupérer l'ensemble de la request du client. Au niveau de l'interface JavaScript, le seul paramètre nécessaire sera le login ; la request étant manipulée automatiquement par le moteur DWR.

- Déclarez le bean Spring associé pour intégrer DWR à Spring et donc au reste de l'application.

- Création d'un fichier applicationContextDWR.xml sous WEB-INF

applicationContextDWR.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
<bean id="userDWR" class="tuto.webssh.web.dwr.impl.UserDWRImpl">
<property name="userManager">
<ref bean="userManager" />
</property>
</bean>
</beans>

- intégration de ce fichier dans le web.xml :

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml /WEB-INF/applicationContextDao.xml
/WEB-INF/applicationContextDWR.xml</param-value>
</context-param>

La nouvelle couche applicative est ainsi créée et intégrée à l'architecture Spring existante.

On remarquera que, par rapport aux sources récupérées du tutoriel Spring-hibernate, on a développé la gestion des exceptions au niveau de la couche métier afin de gérer proprement les erreurs. L'étape suivante serait de développer une véritable solution de gestion des exceptions avec des exceptions spécialisées, mais cela sort du cadre de ce tutoriel. L'ensemble des classes et interfaces modifiées est reporté ici :

UserManager

package tuto.webssh.service;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import tuto.webssh.domain.model.User;

/**
* This interface publishes business features to handler users
* @author bmeurant
*/
@Transactional (readOnly=true, propagation=Propagation.REQUIRED)
public interface UserManager {

/**
* Check if the login exists and if the password is correct.
* @param login : user login
* @param password : user password
* @return true if the login exists and if the password is correct.
* Otherwise, return false.
*/
public boolean checkLogin (String login, String password);

/**
* Return a User object from a given login.
* @param login : user login
* @return the corresponding user object.
*/
public User getUser(String login);

/**
* Change the password to 'password' for the given login
* @param login : user login
* @param password : user new password
* @return the new User object
*/
@Transactional (readOnly=false)
public User changePassword (String login, String password);

}

UserManagerImpl

package tuto.webssh.service.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import tuto.webssh.domain.dao.UserDao;
import tuto.webssh.domain.model.User;
import tuto.webssh.service.UserManager;

/**
* Implements business features to handler users
* @author bmeurant
*/
public class UserManagerImpl implements UserManager {

private final Log logger = LogFactory.getLog(UserManagerImpl.class);

private UserDao userDao = null;

/**
* setter to allows spring to inject userDao implementation
* @param userDao : object (implementation of UserDao interface) to inject.
*/
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

/**
* {@inheritDoc}
*/
public boolean checkLogin (String login, String password) {
return userDao.checkLogin(login, password);
}

/**
* {@inheritDoc}
*/
public User changePassword(String login, String password) {
User user = userDao.getUser(login);
if (user != null) {
user.setPasswordUser(password);
}
return user;
}

/**
* {@inheritDoc}
*/
@SuppressWarnings("finally")
public User getUser(String login) {
return userDao.getUser(login);
}
}

UserDao

package tuto.webssh.domain.dao;

import tuto.webssh.domain.model.User;

/**
* Allows performing complex actions on persistent data
* @author bmeurant
*/
public interface UserDao {

/**
* Check if the login exists and if the password is correct in datasource.
* @param login : user login
* @param password : user password
* @return true if the login exists and if the password is correct.
* Otherwise, return false.
* @throws DataAccessException in case of Data access errors
* (database unreachable, etc.)
*/
public boolean checkLogin (String login, String password);

/**
* Return a User object from a given login.
* @param login : user login
* @return the corresponding user object.
* @throws DataAccessException in case of Data access errors
* (database unreachable, etc.)
*/
public User getUser(String login);

}

UserDaoImpl

package tuto.webssh.domain.dao.hibernate3;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Expression;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import tuto.webssh.domain.dao.UserDao;
import tuto.webssh.domain.model.User;

/**
* Implements a strategy to perform complex actions on persistent data.
* @author bmeurant
*/
public class UserDaoImpl extends HibernateDaoSupport implements UserDao {

/**
* {@inheritDoc}
*/
public boolean checkLogin(String login, String password) {
if (null == login || null == password) {
throw new IllegalArgumentException("Login and password are mandatory. Null values are forbidden.");
}
try {
logger.info("Check user with login: "+login+" and password : [PROTECTED]");
Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
// create a new criteria
Criteria crit = session.createCriteria(User.class);
crit.add(Expression.ilike("loginUser", login));
crit.add(Expression.eq("passwordUser", password));

User user = (User)crit.uniqueResult();
return (user != null);
}
catch(DataAccessException e) {
// Critical errors : database unreachable, etc.
logger.error("Exception - DataAccessException occurs : "+e.getMessage()
+" on complete checkLogin().");
return false;
}
}

/**
* {@inheritDoc}
*/
public User getUser(String login) {
if (null == login) {
throw new IllegalArgumentException("Login is mandatory. Null value is forbidden.");
}
try {
logger.info("get User with login: "+login);
Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
// create a new criteria
Criteria crit = session.createCriteria(User.class);
crit.add(Expression.eq("loginUser", login));

User user = (User)crit.uniqueResult();
return user;
}
catch(DataAccessException e) {
// Critical errors : database unreachable, etc.
logger.error("Exception - DataAccessException occurs : "+e.getMessage()
+" on complete getUser().");
return null;
}
}
}

À l'issue de cette étape, le projet devrait avoir l'allure suivante :

Figure5 : Projet avec couche dédiée

V - Installation de DWR

- Téléchargez la dernière distribution DWR 2.x ici.

- Copiez le dwr.jar télécharger dans le projet eclipse sous WEB-INF/lib.

Figure6 : Installation de DWR

- Modifiez le web.xml pour configurer la servlet DWR comme ceci :

web.xml

<servlet>
<description>DWR controller servlet</description>
<servlet-name>DWR controller servlet</servlet-name>
<servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
<init-param>
<param-name>classes</param-name>
<param-value>
tuto.webssh.web.dwr.UserDWR,
tuto.webssh.domain.model.User,
tuto.webssh.domain.model.Rights
</param-value>
</init-param>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>DWR controller servlet</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>

On note :

- le paramètre debug=true qui permet de disposer du debugger web de DWR que l'on présentera plus tard et qui s'avère extrêmement pratique en phase de développement. Ne pas oublier cependant de passer ce paramètre à false lors de la mise en production, car cela représente une faille de sécurité gigantesque ;

- l'url pattern est configuré à /dwr/. Cela signifie que toute url commençant par ce pattern (relativement à la racine de la webapp) interrogera la servlet dwr ;

- le paramètre classes : ce paramètre définit l'ensemble des objets - en l'occurrence des interfaces et des classes - qui seront gérées par DWR.

Le service que nous désirons publier est donc le service défini par l'interface userDWR. Il faut donc éditer cette interface comme ceci :

modification UserDWR

package tuto.webssh.web.dwr;

import javax.servlet.http.HttpServletRequest;

import org.directwebremoting.annotations.Param;
import org.directwebremoting.annotations.RemoteMethod;
import org.directwebremoting.annotations.RemoteProxy;
import org.directwebremoting.spring.SpringCreator;

import tuto.webssh.domain.model.User;

/**
* This interface publishes business features to handle user to web interfaces
* @author bmeurant
*/
@RemoteProxy(creator = SpringCreator.class,
creatorParams = @Param(name = "beanName", value = "userDWR"))
public interface UserDWR {

/**
* Return a User object from a given login.
* @param login : user login
* @param request : the web client request used to get some specific data from client.
* @return the corresponding user object.
*/
@RemoteMethod
public User getUser(String login, HttpServletRequest request);

/**
* This method checks the actual login and password and, if this verification
* is successful, change the password to 'password' for the given login.
* @param request : the web client request used to get some specific data from client.
* @param login : user login
* @param oldPass : user old password
* @param newPass : user new password
* @return the new User object
*/
@RemoteMethod
public User changePassword(HttpServletRequest request, String login, String oldPass, String newPass);

}

On note :

- l'annotation @RemoteProxy qui définit le fait que cette interface sera publiée par DWR ; c'est-à-dire que le moteur de DWR créera, au lancement de l'application, un fichier JavaScript permettant d'appeler, via une fonction JavaScript sur le client, une méthode Java sur le serveur par XMLHttpRequest. Cette annotation prend en paramètre une classe qui définit le fait que cet objet est géré par Spring (cela permettra à DWR de déléguer l'instanciation de l'objet à l'implémentation définie au niveau du bean Spring). On donne ensuite au creator le nom du bean en question ;

- l'annotation @RemoteMethod permet de définir les méthodes que l'on souhaite publier sur DWR. Toutes les autres méthodes - et notamment les méthodes héritées - seront masquées par DWR et non accessibles. Cela se révèle très important pour des questions évidentes de sécurité.

Éditez ensuite la classe User qui doit être déclarée à DWR puisqu'elle est retournée par la méthode getUser publiée. DWR nécessite cette déclaration pour être capable de convertir l'objet retourné (en l'occurrence l'objet User) de Java en JavaScript.

class User

package tuto.webssh.domain.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.directwebremoting.annotations.DataTransferObject;
import org.directwebremoting.annotations.RemoteProperty;

@DataTransferObject
@Entity
@Table(name = "user", catalog = "experiments")
public class User implements java.io.Serializable {

private static final long serialVersionUID = 1073256708139002061L;

private int idUser;
private String loginUser;
private String passwordUser;
private Set<Rights> rights = new HashSet<Rights>(0);

public User() {
}

public User(int idUser, String loginUser, String passwordUser) {
this.idUser = idUser;
this.loginUser = loginUser;
this.passwordUser = passwordUser;
}

public User(int idUser, String loginUser, String passwordUser,
Set<Rights> rights) {
this.idUser = idUser;
this.loginUser = loginUser;
this.passwordUser = passwordUser;
this.rights = rights;
}

@RemoteProperty
@Id
@Column(name = "id_user", unique = true, nullable = false)
public int getIdUser() {
return this.idUser;
}

public void setIdUser(int idUser) {
this.idUser = idUser;
}

@RemoteProperty
@Column(name = "login_user", nullable = false, length = 25)
public String getLoginUser() {
return this.loginUser;
}

public void setLoginUser(String loginUser) {
this.loginUser = loginUser;
}

@RemoteProperty
@Column(name = "password_user", nullable = false, length = 25)
public String getPasswordUser() {
return this.passwordUser;
}

public void setPasswordUser(String passwordUser) {
this.passwordUser = passwordUser;
}

@RemoteProperty
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user")
public Set<Rights> getRights() {
return this.rights;
}

public void setRights(Set<Rights> rights) {
this.rights = rights;
}

@Override
public String toString() {
return "User: [id: "+idUser+",login: "+loginUser+", rights: "+rights+"]";
}
}

class Rights

package tuto.webssh.domain.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.directwebremoting.annotations.DataTransferObject;
import org.directwebremoting.annotations.RemoteProperty;

@DataTransferObject
@Entity
@Table(name = "rights", catalog = "experiments")
public class Rights implements java.io.Serializable {

private static final long serialVersionUID = -8905167784828935704L;

private int id;
private User user;
private String label;

public Rights() {
}

public Rights(int id, User user, String label) {
this.id = id;
this.user = user;
this.label = label;
}

@RemoteProperty
@Id
@Column(name = "id", unique = true, nullable = false)
public int getId() {
return this.id;
}

public void setId(int id) {
this.id = id;
}

@RemoteProperty
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_user", nullable = false)
public User getUser() {
return this.user;
}

public void setUser(User user) {
this.user = user;
}

@RemoteProperty
@Column(name = "label", nullable = false, length = 45)
public String getLabel() {
return this.label;
}

public void setLabel(String label) {
this.label = label;
}

@Override
public String toString() {
return "Rights: [ id: "+id+", label: "+label+"]";
}
}

On note :

- l'annotation @DataTransfertObject qui définit le fait que cet objet doit pouvoir être converti du Java vers JavaScript par DWR ;

- l'annotation @RemoteProperty permet de définir précisément si telle ou telle propriété doit pouvoir être sérialisée en JavaScript par DWR. Les autres méthodes ne seront pas accessibles en JavaScript.

VI - Test via le debugger DWR

Lancez le serveur. Spring doit correctement charger le nouveau bean DWR défini :

logs au lancement du serveur

INFO  org.springframework.beans.factory.support.DefaultListableBeanFactory  -
Pre-instantiating singletons in org.springframework.beans.factory.support
.DefaultListableBeanFactory@18c56d: defining beans [userManagerTarget,userManager,
userDao,sessionFactory,transactionManager,transactionProxy,userDWR]; root of factory hierarchy

Entrer l'url : http://localhost:8080/BlankApplicationTapestryDWR/dwr/index.html. On obtient l'interface du debugger DWR. Sur cette page se trouve la liste de tous les services publiés par DWR. On retrouve notre UserDWR :

Figure7 : Interface du debugger DWR

On remarque que l'objet physique associé est bien l'implémentation et non l'interface. L'injection de dépendances Spring a bien fonctionné

Cliquez sur UserDWR. On obtient le détail de la classe UserDWR et de ses méthodes :

Figure8 : Debugger DWR pour le service UserManager

On remarque que toutes les méthodes héritées et donc non déclarées comme publiées sont associées à des messages d'avertissement : DWR nous informe de l'existence de ces méthodes, mais nous prévient que l'accès sera interdit. D'ailleurs, les clics sur les boutons Execute sont inopérants. On remarque un autre avertissement : No Converter. Cela signifie que l'objet retourné par cette méthode n'a pas été déclaré comme un DataTransfertObject : DWR ne sait donc pas convertir cet objet en JavaScript et l'objet n'est donc pas manipulable. En l'occurrence il s'agit de méthodes non publiées donc l'avertissement est à ignorer.

On remarque également le second paramètre 'AUTO' passé à la méthode getUser. Comme vu plus haut, il s'agit de la requête http, manipulée automatiquement et de manière transparente par DWR.

Au niveau de la méthode getUser, entrez 'test' comme paramètre et cliquez sur Execute. L'objet User défini en base est retourné correctement et converti par DWR en JavaScript :

Figure9 : Test de la méthode getUser()

On remarque au passage que le lazy loading configuré dans le tutoriel Tapestry5, Spring, Hibernate fonctionne puisque l'objet rights est bien inclus dans l'objet user.

Maintenant que DWR fonctionne de manière autonome, nous allons l'intégrer à l'architecture Tapestry5 du tutoriel Tapestry5-Spring-Hibernate.

VII - Intégration à Tapestry5

L'objectif est de permettre à DWR de récupérer le login de l'utilisateur connecté. L'objet login doit donc être mis en session lors de l'authentification pour pouvoir ensuite être récupéré par DWR à partir de son objet request. L'authentification de l'utilisateur étant gérée par Tapestry, la mise en session lui est dévolue.

- Enregistrement du login en session

On modifie donc la classe Login.java :

class Login.java

package tuto.webssh.pages;

import javax.servlet.http.HttpSession;

import org.apache.tapestry.annotations.ApplicationState;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.annotations.Service;
import org.apache.tapestry.beaneditor.Validate;
import org.apache.tapestry.services.RequestGlobals;

import tuto.webssh.service.UserManager;

public class Login {

private static final String BAD_CREDENTIALS = "Bad login and/or password. Please retry.";

@Persist
private boolean error = false;

@ApplicationState
private String login;

@Inject
private RequestGlobals requestGlobals;

@Inject
@Service("userManager")
private UserManager userManager;

private String password;

public String getLogin() {
return login;
}

@Validate("required")
public void setLogin(String login) {
this.login = login;
}

public String getPassword() {
return password;
}

public String getErrorMessage() {
String ret = null;
if (error) {
ret = BAD_CREDENTIALS;
}
return ret;
}

@Validate("required")
public void setPassword(String password) {
this.password = password;
}

String onSuccess() {
String ret = "Login";
error=true;
if (userManager.checkLogin(login, password)) {
error= false;
HttpSession session =
requestGlobals.getHTTPServletRequest().getSession();
session.setAttribute("loginUser", login);
ret = "Home";
}
return ret;
}
}

Les éléments ajoutés sont :

- Injection (via l'annotation @Inject - cf.tutoriel Tapestry) de la request standard J2EE (objet requestGlobals) ;

- dans la méthode onSuccess, au post du formulaire et si l'utilisateur s'est authentifié avec succès, son login est mis en session - la session étant accédée via l'objet requestGlobals récupéré plus haut ;

- récupération du login dans DWR.

Nous allons maintenant créer une nouvelle méthode publiée dans DWR : la méthode getUserFromSession qui récupèrera le login dans la session et ira ensuite chercher l'objet User à partir de ce login.

Attention : la méthode doit être définie avec un nom strictement différent des méthodes existantes (en l'occurrence la méthode getUser). En effet, en Java pur, nous aurions simplement défini une méthode getUser sans paramètres. Le compilateur Java aurait fait sans problème la différence entre les deux méthodes : l'une avec un paramètre de type String et l'autre sans paramètre. Ici, le problème est différent : si le compilateur s'en sort sans souci, il n'en est pas de même du code JavaScript généré par DWR pour accéder aux API Java publiées. En effet, le JavaScript n'est pas un langage compilé, mais interprété et une signature de méthode se résume à son nom. Si l'on appelle deux méthodes de la même manière en faisant varier uniquement leurs paramètres, l'interpréteur JavaScript sera incapable, au moment de l'exécution, de faire la différence entre les deux méthodes. Dans ce cas le comportement est indéterminé (à priori c'est la dernière fonction connue de l'interpréteur qui est utilisée).

- On ajoute donc la méthode dans l'interface DWR publiée :

Interface UserDWR

package tuto.webssh.web.dwr;

import javax.servlet.http.HttpServletRequest;

import org.directwebremoting.annotations.Param;
import org.directwebremoting.annotations.RemoteMethod;
import org.directwebremoting.annotations.RemoteProxy;
import org.directwebremoting.spring.SpringCreator;

import tuto.webssh.domain.model.User;

/**
* This interface publishes business features to handle user to web interfaces
* @author bmeurant
*/
@RemoteProxy(creator = SpringCreator.class,
creatorParams = @Param(name = "beanName", value = "userDWR"))
public interface UserDWR {

/**
* Return a User object from a given login.
* @param login : user login
* @param request : the web client request used to get some specific data from client.
* @return the corresponding user object.
*/
@RemoteMethod
public User getUser(String login, HttpServletRequest request);

/**
* Get the connected user object from session
* @param request : the web client request used to get some specific data from client.
* @return the corresponding user object.
*/
@RemoteMethod
public User getUserFromSession(HttpServletRequest request);

/**
* This method checks the actual login and password and, if this verification
* is successful, change the password to 'password' for the given login.
* @param request : the web client request used to get some specific data from client.
* @param login : user login
* @param oldPass : user old password
* @param newPass : user new password
* @return the new User object
*/
@RemoteMethod
public User changePassword(HttpServletRequest request, String login, String oldPass, String newPass);

}

- Puis on implémente cette méthode dans la classe associée (par un bean Spring) :

Implémentation UserDWRImpl

package tuto.webssh.web.dwr.impl;

import javax.servlet.http.HttpServletRequest;

import tuto.webssh.domain.model.User;
import tuto.webssh.service.UserManager;
import tuto.webssh.web.dwr.UserDWR;

/**
* Implements business features to handle user to web interfaces
* @author bmeurant
*/
public class UserDWRImpl implements UserDWR {

private UserManager userManager;

/**
* setter to allows spring to inject userManager implementation
* @param userManager : object (implementation of UserManager interface) to inject.
*/
public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}

/**
* {@inheritDoc}
*/
public User getUser(String login, HttpServletRequest request) {
return userManager.getUser(login);
}

/**
* {@inheritDoc}
*/
public User getUserFromSession(HttpServletRequest request) {
String login = (String)request.getSession().getAttribute("loginUser");
if (null != login) {
return this.getUser(login, request);
}
else {
return null;
}
}

/**
* {@inheritDoc}
*/
public User changePassword(HttpServletRequest request, String login,
String oldPass, String newPass) {
if (userManager.checkLogin(login, oldPass)) {
return this.userManager.changePassword(login, newPass);
}
else {
return null;
}
}
}

- Relancez ensuite le serveur et retournez sur http://localhost:8080/BlankApplicationTapestryDWR/dwr/test/UserDWR. On obtient la page suivante. Notez que l'on a bien la nouvelle méthode publiée - sans Warning.

Figure10 : méthode getUserFromSession dans le Debugger DWR

Le clic sur Exécuter de getUserFromSession renvoie null. En effet, aucun utilisateur n'est loggué et la méthode renvoie donc null.

Figure11 : exécution de la méthode getUserFromSession

La solution est donc d'utiliser cette méthode après l'authentification - en revenant ensuite sur le debugger DWR ou d'intégrer l'appel à la fonction JavaScript dans le code de la page Home, affichée après la connexion - c'est ce que nous allons faire.

- Intégration à la page Home.html

Modifiez la page Home.html de manière à intégrer un bouton 'show Details'. Lorsque l'utilisateur cliquera sur ce bouton, la fonction DWR publiée en JavaScript getUserFromSession sera appelée et l'objet User récupéré depuis la BDD. Les détails du user connecté seront alors affichés dans une partie du DOM (Document Object Model) affiché à la volée :

page Home.html

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Congratulation</title>
</head>
<script type='text/JavaScript' src='/BlankApplicationTapestryDWR/dwr/interface/UserDWR.js'></script>
<script type='text/JavaScript' src='/BlankApplicationTapestryDWR/dwr/engine.js'></script>
<script type='text/JavaScript' src='/BlankApplicationTapestryDWR/dwr/util.js'></script>
<script>

function detailsClicked() {
if ($('detailsButton').value=='Show Details') {
// Show the 'loading' message
DWRUtil.useLoadingMessage();
UserDWR.getUserFromSession (getUserFromSessionCallback);
}
else {
$('detailsButton').value = 'Show Details';
$('tableContainer').style.display='none';
}
}

function getUserFromSessionCallback (result) {
if (typeof result != 'undefined') {
$('idContainer').innerHTML = result.idUser;
$('loginContainer').innerHTML = result.loginUser;
$('passwordContainer').innerHTML = result.passwordUser;
$('detailsButton').value='Hide Details';
$('tableContainer').style.display='block';
}
}
</script>
<body>
Congratulations, you are logged with login: ${login} !!<br/><br/>
<input type='button' id='detailsButton' value='Show Details' onclick='detailsClicked();'/>
<br/><br/>
<table id='tableContainer' style="display:none;">
<tr>
<td style="font-weight:bold;">Id: </td>
<td id="idContainer"/>
</tr>
<tr>
<td style="font-weight:bold;">Login: </td>
<td id="loginContainer"/>
</tr>
<tr>
<td style="font-weight:bold;">Password: </td>
<td id="passwordContainer"/>
</tr>
</table>
<br/>
<span id="LogoutLink"><span class="moduleTitle"><t:actionlink t:id="logout">Deconnexion</t:actionlink></span></span>
</body>
</html>

On a donc créé une fonction JavaScript detailsClicked appelée au clic sur le bouton show details et qui se charge d'appeler la méthode DWR getUserFromSession. Cette méthode prend un seul paramètre : le callback. Il s'agit d'une sorte de pointeur sur la fonction getUserFromSessionCallback définie plus bas. En effet, AJAX étant par définition à priori asynchrone, le traitement est appelé, mais l'appel n'est pas bloquant. On sort alors de la méthode appelante. Une fois le traitement fini, le callback sera automatiquement appelé, en l'occurrence la méthode getUserFromSessionCallback. On remarque au passage que la fonction getUserFromSession ne prend pas d'autres paramètres que son callback : l'objet request est automatiquement passé par DWR (paramètre AUTO dans la signature de la méthode).

On note les inclusions de scripts en début de fichier. Toutes ces inclusions sont données par DWR sur la page http://localhost:8080/BlankApplicationTapestryDWR/dwr/test/UserDWR.

Figure12 : inclusion des scripts dans DWR

Le premier script permet d'utiliser l'ensemble des méthodes publiées par l'interface DWR UserDWR, le second comporte le cœur de la partie JavaScript de DWR et le dernier une série de fonctions utilitaires dont la plupart sont héritées du framework Prototype.

Ainsi l'instruction $('uneValeur') permet de récupérer un élément HTML du DOM à partir de son identifiant. Cette fonction prend en charge les spécificités des différents navigateurs et remplace les fonctions telles que getElementById() dont les implémentations peuvent varier.

De la même manière, l'instruction DWRUtil.useLoadingMessage() permet d'afficher, pendant l'exécution de la fonction AJAX DWR, l'indication 'loading' en haut à droite de la page. Cependant, dans notre cas, compte tenu de la vitesse d'exécution, il est fort probable que cette indication ne soit pas visible.

Ainsi lorsque l'utilisateur clique sur 'Show Details', la table contenant le détail du user est remplie avec les valeurs de l'objet User - récupéré sous la forme d'un objet JavaScript grâce à la conversion DWR. On accède aux propriétés de l'objet récupéré de manière standard par l'opérateur '.' Suivi du nom de la propriété. La table est ensuite affichée et la valeur du bouton est changée à 'Hide details'.

Lorsque l'utilisateur clique sur 'Hide details', la fonction DWR n'est pas appelée, les informations sont masquées et la valeur du bouton changée.

Pour tester tout cela :

- chargez la page de login : http://localhost:8080/BlankApplicationTapestryDWR/login ;

- logguez-vous avec le login 'test' et le password 'test'. Ceci est important, si le log n'est pas fait la fonction DWR retournera null et rien ne se passera ;

- la redirection vers la page Home est faite. L'affichage est le suivant :

Figure13 : fonction show details (1)

- Cliquer sur 'Show Details' donne l'affichage suivant :

Figure14 : fonction show details (2)

Nous avons bien traversé via DWR toutes les couches de l'application pour aller récupérer l'utilisateur en BDD et afficher le détail de son profil à l'écran.

VIII - Conclusion

Nous venons de le voir, la configuration d'une application utilisant DWR n'est ni très complexe ni très lourde à mettre en place. Les concepts d'AJAX sont d'ailleurs relativement simples. Attention cependant à bien prendre en compte le caractère asynchrone des appels (les appels à des fonctions DWR ne sont pas bloquants) ainsi qu'aux converters (tout objet que l'on souhaite manipuler en JavaScript doit être déclaré dans DWR impérativement).

DWR permet de développer des applications Web 2.0 AJAX complexes en déportant une grande partie de la complexité vers Java, bien plus structuré, robuste et maintenable que JavaScript. Il prend également en charge certains problèmes de compatibilité entre navigateurs.

En conclusion, je pense que l'utilisation de DWR peut simplifier grandement le développement, la maintenance et l'évolution d'une application. Je ne saurais trop que le conseiller à tous les développeurs désirant faire de l'AJAX en Java.

À noter que DWR peut parfaitement s'intégrer avec des bibliothèques de composants JavaScript graphiques évolués telles que dojo ou scriptaculous.

IX - Remerciements

Merci à Ricky81 et denisC pour leur aide, leurs conseils et leur relecture technique.
Merci à UNi[FR] pour la relecture orthographique.

Ce document a été publié avec l'autorisation de la société qui m'emploie : Atos Worldline.

X - Dans la même série…

Ce tutoriel est le deuxième d'une série de trois. Accédez aux autres tutoriels de cette série :

- Premier volet : Premier projet avec Tapestry5, Spring et Hibernate ;

- Deuxième volet : Intégration simple et élégante d'AJAX avec DWR ;

- Troisième volet : Sécuriser vos applications Web avec Spring acegi security -> En Relecture.