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
Figure1 : Architecture logicielle

III. Mise en place


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

  • Copier ce projet depuis votre workspace Eclipse et le copier dans ce même workspace en changeant le nom.
Figure2 : Copie du projet
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
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
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
Sélectionnez

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
Sélectionnez

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é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
Sélectionnez

<?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 :
 
Sélectionnez

<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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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;
		}
	}
}


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

Figure5 : Projet avec couche dédiée
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
Figure6 : Installation de DWR
  • Modifier le web.xml pour configurer la servlet DWR comme ceci :
web.xml
Sélectionnez

<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
Sélectionnez

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é.

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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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
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
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()
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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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;
		}
	}
}
Figure10 : methode getUserFromSession dans le Debugger DWR
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
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
Sélectionnez

<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 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).

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
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)
Figure13 : fonction show details (1)
  • Cliquer sur 'Show Details' donne l'affichage suivant :
Figure14 : fonction show details (2)
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 :