Intégration simple et élégante d'AJAX avec DWR
Date de publication : 23/10/2007 , Date de mise à jour : 25/10/2007
Par
Baptiste Meurant (baptiste-meurant.developpez.com)
Ce tutoriel a pour objectif de présenter l'intégration du framework AJAX DWR à une application Web existante reposant sur Spring et Hibernate..
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. Integration à 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és 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 [mirroir
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, parrallèlement à la couche Tapestry. Les aspects Tapestry et DWR, si ils se côtoient dans la page html, n'intégragissent 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 si 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
- Copier ce projet depuis votre workspace Eclipse et le copier 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 changer le nom du context.

Figure3 : Changement du contexte
Editer ensuite le fichier org.eclipse.wst.common.component dans le répertoire .settings du nouveau projet. Modifier 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). Relancer ensuite Eclipse.
Si vous n'avez pas suivi le tutoriel précédent :
- Récupérer les sources du tutoriel Tapestry5, Spring Hibernate ici. Le dézipper sur votre machine.
- Créer un nouveau projet en suivant la procédure décrite dans les chapitres II et III du premier tutoriel Tapestry5, Spring, Hibernate disponible ici.
- Importer l'ensemble du tutoriel (sources, lib, etc) précédent dans ce nouveau projet.
- Redéfinir le répertoire config comme source folder du projet.
A l'issue de ces étapes, le projet doit ressembler à cela :

Figure4 : Projet vide
Dans les deux cas :
- Associer ce projet avec le serveur Tomcat.
IV. Création de la couche applicative dédiée
- Créer les packages tuto.webssh.web.dwr et tuto.webssh.web.dwr.impl.
- Créer respectivement dans ces package 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éfinit 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;
@author
public interface UserDWR {
@param
@param
@return
public User getUser(String login, HttpServletRequest request);
@param
@param
@param
@param
@return
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;
@author
public class UserDWRImpl implements UserDWR {
private UserManager userManager;
@param
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éclarer 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;
@author
@Transactional (readOnly=true, propagation=Propagation.REQUIRED)
public interface UserManager {
@param
@param
@return
public boolean checkLogin (String login, String password);
@param
@return
public User getUser(String login);
@param
@param
@return
@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;
@author
public class UserManagerImpl implements UserManager {
private final Log logger = LogFactory.getLog(UserManagerImpl.class);
private UserDao userDao = null;
@param
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;
@author
public interface UserDao {
@param
@param
@return
@throws
public boolean checkLogin (String login, String password);
@param
@return
@throws
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;
@author
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();
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) {
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();
Criteria crit = session.createCriteria(User.class);
crit.add(Expression.eq("loginUser", login));
User user = (User)crit.uniqueResult();
return user;
}
catch(DataAccessException e) {
logger.error("Exception - DataAccessException occurs : "+e.getMessage()
+" on complete getUser().");
return null;
}
}
}
|
A 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écharger la dernière distribution DWR 2.x ici.
- Copier le dwr.jar télécharger dans le projet eclipse sous WEB-INF/lib.

Figure6 : Installation de DWR
- Modifier 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éfinit 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;
@author
@RemoteProxy(creator = SpringCreator.class,
creatorParams = @Param(name = "beanName", value = "userDWR"))
public interface UserDWR {
@param
@param
@return
@RemoteMethod
public User getUser(String login, HttpServletRequest request);
@param
@param
@param
@param
@return
@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é.
Editer 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 convertit 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
Lancer 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é
Cliquer 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 clicks 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, entrer 'test' comme paramètre et cliquer sur Execute. L'objet User définit en base est retourné correctement et convertit par DWR en Javascript :

Figure9 : Test de la méthode getUser()
On remarque au passage que le lazy loading configuré dans le tutorial 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. Integration à 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éfinit une méthode getUser sans paramètres. Le compilateur java aurait fait sans problème la différences 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 soucis, il n'en n'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 language 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;
@author
@RemoteProxy(creator = SpringCreator.class,
creatorParams = @Param(name = "beanName", value = "userDWR"))
public interface UserDWR {
@param
@param
@return
@RemoteMethod
public User getUser(String login, HttpServletRequest request);
@param
@return
@RemoteMethod
public User getUserFromSession(HttpServletRequest request);
@param
@param
@param
@param
@return
@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;
@author
public class UserDWRImpl implements UserDWR {
private UserManager userManager;
@param
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;
}
}
}
|

Figure10 : methode getUserFromSession dans le Debugger DWR
Le click 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
Modifier 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') {
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 click 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 finit, le callback sera automatiquement appelé, en l'occurrence la méthode getUserFromSessionCallback. On remarque au passage que la fonction getUserFromSession ne prend pas d'autre paramètres que son callback : l'objet request est automatiquement passé par DWR (paramètre AUTO dans la signature de la méthode).

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 Coeur 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 :
- charger la page de login : http://localhost:8080/BlankApplicationTapestryDWR/login
- se logguer 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.
A 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 3. Accéder aux autres tutoriels de cette série :


Copyright © 2007 . 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'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.