Faire du RIA avec des composants ajax et des services REST
Dans cet article, nous
allons construire une page contenant un composant ajax
- un simple tableau dynamique - qui ira chercher des données sur un serveur
web Tomcat. La communication utilisera la nouvelle JSR dédiée à l'implémentation de
services webs de type Rest - la JSR 311 (JAX-RS : "JAX-RS: The JavaTM API for
RESTful Web Services").
Nous poursuivons plusieurs buts.
- Expérimenter une architecture où, du côté serveur, on installe une solution assez légère et simple, à base de Rest ; et côté client une solution riche, éprouvée et ergonomique à base d'ajax. Et voir si cette architecture, qui reprend de nombreuses techniques récentes, demande beaucoup de travail.
- Faire une interface RIA avec des solutions "standards". Pas de Flex, ni Silverlight, ni de GWT cette fois-ci. Simplement des composants Ajax pris sur le web. Pourquoi cette démarche? C'est parce que ces composants sont légion. Il existe beaucoup de frameworks javascript de qualité permettant d'avoir des composants graphiques intéressants. Pourquoi ne pas les réutiliser? Et ainsi profiter de l'expérience et du savoir-faire de milliers de graphistes et de développeurs web. Nous prendrons ici JQGrid, qui est un plugin JQuery, mais on peut très facilement imaginer le même type d'architecture avec Ext-JS, ou Dojo ou Scriptaculous.
- Utiliser un composant riche Ajax et une série de services sans passer par un framework de présentation du type Struts, Tapestry, etc.
Architecture
L'architecture est ici un peu différente de celle que l'on connaît sur les applications webs classiques. D'habitude, la récupération des données et leur présentation se décide côté serveur. C'est la page struts/tapestry/struts2 etc. qui décide de mettre telles ou telles données dans une page html envoyée au client léger. Ici, la responsabilité est inversée. Une page web, chargée sur le navigateur du client, a la responsabilité de récupérer des données sur un serveur et de les afficher. Cette architecture fait suite aux articles sur la démarche SOFEA décrite dans cet article, qui intéresse également un spécialiste de la partie présentation comme Matt Raible et dont nous avons déjà parlé sur le blog d'Oxiane.
Une différence avec l'article initial de Ganesh Prasad est que nous utiliserons Rest, qui nous paraît mieux adapté que Soap, dans le cadre d'un composant javascript. Plus simple à mettre en place, Rest met l'accent sur les URL, ce qui est l'univers naturel des pages html/javascript et devrait nous permettre une intégration aisée. Nous développerons ce "back-end" en java mais rien n'empêche d'envisager de faire des services Rest en php ou en ruby ou en Squeak. Cette architecture a la particularité d'être très souple. Le langage côté client est autonome du langage côté serveur.
La conséquence est que tout repose sur le protocole de communication entre
client ajax et serveur. Deux méthodes existent : JSon et XML. Nous
ferons ici appel à du xml, mais la solution JSon est tout aussi simple
- voire plus simple - à implémenter.
L'architecture de notre application test se présente ainsi:
- Une page html/javascript est mise à disposition sur un serveur web. Elle est chargée par le navigateur.
- Cette page contient un composant qui va aller récupérée les données qu'il doit afficher, par une requête HTTP.
- Le serveur fait tourner en son sein une petite application chargée de renvoyer les données sous la forme voulue.
Pour cela nous avons besoin des outils usuels pour développer une application web avec java : tomcat et Eclipse. Afin de recevoir les requêtes venues de pages web, nous utiliserons Jersey (1.0.2) qui est l'implémentation de référence de la JSR-311 (qui définit les spécifications pour une implémentation de services Rest en java).
Côté client nous utiliserons la librairie ajax JQuery avec son plugin JQGrid qui permet d'afficher des datatables dynamiques. Ce composant gère la pagination, l'affichage, etc...
Outils requis
- Eclipse pour développeur JEE
- Tomcat 6
- Jquery
- jqGrid-3.4.1
- Jersey. Le packaging de Jersey est basé sur maven, mais On peut récupérer le zip de jersey et des dépendance ici.
Installation
Créer un serveur dans eclipse.
Créer un projet dynamic web.
Créer un répertoire /static. C’est là où nous mettrons toutes nos données statiques : pages html, frameworks javascript
Installer
les bibliothèques javascript. Nous les placerons ici dans un répertoire
js/jquery situé dans static. C’est là que nous copions nos
bibliothèques javascript, Jquery et JQGrid, suivant leurs
recommandations. Il faut paramétrer jquery.jqGrid.js la variable fixant
le répertoire où sont les javascript. Dans notre cas:
var pathtojsfiles = “js/jquery/js/”;
Développement ajax côté client
Nous allons maintenant créer une page html qui va afficher une liste de données. Nous afficherons ici un tableau de régions viticoles, avec un petit descriptif pour chacune de ces régions.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>jqGrid Demo</title>
<link rel="stylesheet" type="text/css" media="screen"
href="js/jquery/themes/basic/grid.css" />
<link rel="stylesheet" type="text/css" media="screen"
href="js/jquery/themes/jqModal.css" />
<script src="js/jquery/jquery-1.3.1.js" type="text/javascript"></script>
<script src="js/jquery/jquery.jqGrid.js" type="text/javascript"></script>
<script src="js/jquery/js/jqModal.js" type="text/javascript"></script>
<script src="js/jquery/js/jqDnR.js" type="text/javascript"></script>
<script type="text/javascript">
jQuery(document).ready(function(){
jQuery("#regions").jqGrid({
url:'http://localhost:8080/poc-sofea/static/sample_regions.xml',
datatype: 'xml',
mtype: 'GET',
colNames:['id', 'Nom de la région','Description'],
colModel :[ {name:'id', index:'id', width:40},
{name:'libelle', index:'libelle', width:120},
{name:'description',
index:'description',
width:300,
align:'right'} ],
pager: jQuery('#pager'),
rowNum:10,
rowList:[10,20,30],
sortname: 'libelle',
sortorder: "asc",
viewrecords: true,
imgpath: 'js/jquery/themes/basic/images',
caption: 'Tableau des régions viticoles' });
});
</script>
</head>
<body>
<table id="regions" class="scroll"></table>
<div id="pager" class="scroll" style="text-align: center;"></div>
</body>
</html>
Cette page contient un composant défini dans les base <script>. Ce composant est chargé d’afficher un tableau de données, qu’il va récupérer par l’url définie dans url:’…’
Afin de vérifier le fonctionnement de cette page test, créons sur le serveur un fichier « sample_regions.xml » qui correspond aux données sous la forme attendue par le composant.. C’est un bouchon. Il contient en dur les données formatées de manière attendues et il permet de tester ce composant.
sample_regions.xml (dans le répertoire "/static")
<?xml version ="1.0" encoding="utf-8"?>
<!-- Extrait de la documentation
<The tags used in this example are explained in the following table.
* rows : the root tag for the grid
* page : the number of the requested page
* total : the total pages of the query
* records: the total records from the query
* row : a particular row in the grid
* cell : the actual data. Note that CDATA can be used.
This way we can add images, links and check boxes.
The number of cell tags in each row must equal the number of cells defined in the colModel.
In our example, we defined six columns, so the number of cell tags in each row tag
should be six.
Note the id attribute in the <row> tags. While this attribute can be omitted,
it is a good practice to have a unique id for every row.
-->
<!--
<rows>
<page> </page>
<total> </total>
<records> </records>
<row id = “unique_rowid”>
<cell> cellcontent </cell>
<cell> <![CDATA[<font color=”red”>cell</font> content]]> </cell>
</row>
<row id = “unique_rowid”>
<cell> cellcontent </cell>
<cell> <![CDATA[<font color=”red”>cell</font> content]]> </cell>
</row>
(...)
</rows>
-->
<rows>
<page>1</page>
<total>1</total>
<records>2</records>
<row id = "1">
<cell>1</cell>
<cell>Bourgogne</cell>
<cell> <![CDATA[
Le vignoble de Bourgogne est un vignoble français situé
exclusivement en Bourgogne sur les départements de l'Yonne,
de la Côte-d'Or et de la Saône-et-Loire.
Il s’étend sur 250 km de longueur du nord de Chablis au sud du Mâconnais.
]]> </cell>
...
</row>
<row id = "2">
<cell>2</cell>
<cell> Val de Loire </cell>
<cell> <![CDATA[
Le vignoble du Val de Loire, région de production du vin de Loire,
regroupe en fait plusieurs régions viticoles.
Le Val de Loire produit des vins blancs secs, demi-secs, moelleux voire liquoreux,
des vins rouges le plus souvent légers et des vins rosés.
On trouve également de nombreux vins effervescents.
Toutes ces régions sont situées au bord de la Loire et de ses affluents.
]]> </cell>
</row>
</rows>
La forme de ce fichier est décrite dans la documentation de JQGrid. Comme c’est du xml, on peut imaginer utiliser un XML Schema pour valider ce fichier. JQGrid accepte également des données en JSon.

Une fois lancé le serveur de test, on obtient une page contenant un
composant qui affiche les données du fichier sample_regions.xml.

Développement Rest côté serveur
Il ne reste plus qu’à écrire un service Rest qui remplace l’appel à ce fichier en dur
Pour cela:
Importer dans le projet web les librairies jersey. Afin de ne pas compliquer la démonstration, nous pouvons prendre tous les jars qui sont dans jersey-archive.zip mais les jars réellement nécessaires sont : jersey-core.jar, jersey-server.jar, asm.jar et jsr-311.jar. Les autres jars peuvent être utiles lorsque l’on formatte ses réponses en JSon.
Déclarer dans web.xml l’utilisation du ServletContainer Rest avec ses paramètres d’initialisation. Parmi les paramètres de la servlet (init-param) renseigner le package racine où nous déposerons nos services rest.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>poc-sofea</display-name>
<servlet>
<servlet-name>ServletAdaptor</servlet-name>
<servlet-class>
com.sun.jersey.spi.container.servlet.ServletContainer
</servlet-class>
<init-param>
<param-name>
com.sun.jersey.config.property.resourceConfigClass
</param-name>
<param-value>
com.sun.jersey.api.core.PackagesResourceConfig
</param-value>
</init-param>
<init-param>
<param-name>
com.sun.jersey.config.property.packages
</param-name>
<param-value>
com.oxiane.caveavins.rest
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ServletAdaptor</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
Ecrire un service Rest renvoyant sous la forme voulue par le composant les données à afficher. Nous reviendrons plus en détail dans un prochain article sur l’écriture de services Rest. Mais vous trouverez ici le code de la classe qui renvoit une réponse xml. La méthode qui nous intéresse ici est celle qui renvoit la liste des régions. Elle est constituée d'un appel à un service métier (getCaveAVinsService().getRegions()) dont le retour - une liste d'objets métiers Region - est passé à un service chargé de mettre en forme la réponse pour que cette réponse soit utilisable par le composant graphique.
package com.oxiane.caveavins.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import com.oxiane.caveavins.services.ServiceLocator;
@Path("/region")
public class RegionRestService {
@GET
@Produces("text/html")
@Path("hello")
public String hello() {
return "Hello !";
}
@GET
@Produces("text/xml")
public String getRegions() {
return ServiceLocator.getJQGridSerializer().creerTableauObjetMetier(
ServiceLocator.getCaveAVinsService().getRegions()
);
}
}
Grâce à l’annotation @GET, le service Rest répond à une requête de type GET envoyée à l’URI qui se termine par /region”. Nous sommes avec un serveur en localhost sur le port 8080 et l’application web qui contient ce service a pour nom “poc-sofea”. Dans web.xml , nous avons déterminé que les requêtes Rest devaient être sous la forme /rest/*
L’adresse de ce service REST est donc : http://localhost:8080/poc-sofea/rest/region
C’est cette adresse qu’il faudra donner au composant ajax pour qu’il récupère les données “régions” sous forme de liste.
La méthode getRegions utilise deux services.
Un premier CaveAVinsService renvoit une liste de régions. Nous utilisons ici une liste “bouchon” (en dur). Il est très facile de substituer à ce bouchon un appel à une base de données, ou une requête à Wikipedia.
La définition de la classe Region : elle est pour notre exemple minimale.
public class Region {
private Integer id;
private String libelle;
private String description;
// Suivent les getters et setters
}
Second Service utilisé par le service Rest : le formattage des données métier (List<Region>) en données XML utilisables par le composant Ajax.
Jusqu’ici, nous étions dans une application quasiment sans programmation. Le composant ajax a été utilisé “brut de décoffrage”, les services Rest sont de simples facades, l'appel au service métier qui renvoit la liste des régions devrait être très facile à écrite en JPA. Reste à faire un petit effort pour formatter les données sous la forme voulue.
La structure du XML attendu peut se trouver dans la documentation.
Pour cela, j’ai créé une petite classe chargée de créer un XML .
Elle porte une méthode creerTableauObjetMetier(List<Region> regions)
qui renvoit sous forme de String le fichier xml voulu. Peu importe l’implémentation ici de cette sérialisation xml (elle se fait
ici “à la main” et sans souci de la performance ou de la mémoire)
public class JQGridSerializer {
public String creerTableauObjetMetier(List<Region> regions) {
TableauRegions tableau = new TableauRegions(regions);
return tableau.serialisation();
}
class TableauRegions extends TableauObjetMetierJQSerlialise<Region>{
public TableauRegions(List<Region> regions) {
super(regions);
}
@Override
protected void prepareLignesTableau(List<Region> regions) {
for (Region region : regions) {
addLigneDonnees(
new ObjetMetierJQSerialise(region.getId().toString())
.addDonnee(region.getId().toString())
.addDonnee(region.getLibelle())
.addDonnee(region.getDescription()));
}
}
}
abstract class TableauObjetMetierJQSerlialise<T> {
private String page;
private String total;
private String records;
StringBuilder sb = new StringBuilder();
private final List<ObjetMetierJQSerialise> donnees;
/**
* méthode à implémenter pour chaque objet métier à afficher en tableau
*
* @param donneesAAfficher
*/
protected abstract void prepareLignesTableau(List<T> donneesAAfficher) ;
public TableauObjetMetierJQSerlialise() {
throw new UnsupportedOperationException();
}
public TableauObjetMetierJQSerlialise(List<T> donneesAAfficher) {
donnees=new ArrayList<ObjetMetierJQSerialise>(donneesAAfficher.size());
prepareLignesTableau(donneesAAfficher);
}
protected void addLigneDonnees(ObjetMetierJQSerialise nouvelleLignesDonnees) {
donnees.add(nouvelleLignesDonnees);
}
String serialisation() {
initTableau(donnees.size());
nouvelleReponseXML();
ajoutTagOuvrant("rows");
addLineSeparator();
ajoutCellule("page", page);
ajoutCellule("total", total);
ajoutCellule("records", records);
for (ObjetMetierJQSerialise objetMetierJQSerlialise : donnees) {
ajoutTagOuvrantAvecId("row", objetMetierJQSerlialise.getRowId());
ajoutCellules(objetMetierJQSerlialise.getDonnees());
ajoutTagFermant("row");
}
ajoutTagFermant("rows");
return sb.toString();
}
private void initTableau(int size) {
// TODO ici : intégrer la pagination
page = "1";
total = "1";
records = Integer.toString(size);
}
private void nouvelleReponseXML() {
sb.append("<?xml version =\"1.0\" encoding=\"utf-8\"?>");
addLineSeparator();
}
private void ajoutCellules(List<String> cellules) {
for (String contenuCellule : cellules) {
ajoutCellule("cell", contenuCellule);
}
}
// Ouverture du tag
private void ajoutTagOuvrant(String nomTag) {
sb.append("<").append(nomTag).append(">");
}
// Ouverture du tag avec un identifiant
private void ajoutTagOuvrantAvecId(String nomTag, String id) {
sb.append("<").append(nomTag).append(" id=\"").append(id).append("\">");
}
// Fermeture du tag
private void ajoutTagFermant(String nomTag) {
sb.append("</").append(nomTag).append(">");
}
private void ajoutCellule(String nomTag, String contenuCellule) {
// Ouverture du tag
ajoutTagOuvrant(nomTag);
// Contenu
sb.append(contenuCellule);
// fermeture de tag
ajoutTagFermant(nomTag);
addLineSeparator();
}
private void addLineSeparator() {
sb.append(System.getProperty("line.separator"));
}
/**
* Classe portant données pour chacun des objets métier à afficher.
* Contient un identifiant obligatoire et une liste de données
*
* @author gabriel
*
*/
class ObjetMetierJQSerialise {
private final String rowId;
private final List<String> donnees = new ArrayList<String>();
ObjetMetierJQSerialise() {
throw new UnsupportedOperationException();
}
public ObjetMetierJQSerialise(String rowId) {
this.rowId = rowId;
}
public ObjetMetierJQSerialise addDonnee(String donnee) {
donnees.add(donnee);
return this;
}
public List<String> getDonnees() {
return Collections.unmodifiableList(donnees);
}
public String getRowId() {
return rowId;
}
}
}
}
(A noter que dans une version précédente de cet article, le formattage se faisait en JSon et que c’était au moins aussi facile - voire plus facile - que ce formattage en xml.)
Lorsque maintenant on appelle le serveur à l’adresse:
http://localhost:8080/poc-sofea/rest/region
on obtient les données métier, formattées pour JQGRid. La réponse a été renvoyée grâce aux annotations

On renseigne cette URL dans la page statique test.html, en remplaçant l’url par celle du service rest.
jQuery(document).ready(function(){
jQuery("#regions").jqGrid({
url:'http://127.0.0.1:8080/poc-sofea/rest/region',
// url:'http://127.0.0.1:8080/poc-sofea/static/sample_regions.xml',
datatype: 'xml',
mtype: 'GET',
colNames:['id', 'Nom de la région','Description'],
colModel :[ {name:'id', index:'id', width:40},
{name:'libelle', index:'libelle', width:120},
{name:'description',
index:'description',
width:300,
align:'right'} ],
pager: jQuery('#pager'),
rowNum:10,
rowList:[10,20,30],
sortname: 'libelle',
sortorder: "asc",
viewrecords: true,
imgpath: 'js/jquery/themes/basic/images',
caption: 'Tableau des régions viticoles' });
});
Et on peut maintenant tester de nouveau.
http://localhost:8080/poc-sofea/static/test.html

Et voilà
Conclusions
- Une page web entièrement développée en html vient s'interfacer à un service de type Rest au travers d'une URI. Les deux parties de l'application sont développées dans leur langage propre. Côté client, pas de tags jsp (>c:forEach <... ) dans le html, ni de propriétés comme le wicket:id. Et côté java, Un simple adapter à développer, pour transformer un résultat en java en un résultat en XML.
- Spécialisation des compétences plus encore que séparation des responsabilités. En revanche il faut connaître javascript et maîtriser un framework : protptype, ExtJS etc… Mais on retrouve ce souci avec toutes les autres solutions RIA.
- Le couplage lâche permet d'envisager une grande liberté de choix côté serveur : développement en php, grails, autre... Côté client on attend les données formattées d'une certaine manière, pour un certain type de composant. Mais il est aisément envisageable paramétrer l'adapter pour qu'il envoie sa réponse formattée selon la nature du client : un autre composant ajax, un client téléphonique...
- Testabilité : Les côté html et Rest veulent aisément être développés et testés en parallèle, grâce à des bouchons.
- Souci : Il doit y avoir coordination dans le format des données, entre la partie client et la partie serveur. Si le client veut rajouter une nouvelle colonne au tableau, il faut que le serveur intègre cette donnée dans le xml…
- Les possibilités d'amélioration : de part le couplage lâche, on peut envisager un certains nombre d'améliorations de cette architecture.
On peut par exemple :
* envisager l'encapsulation du javascript dans une taglig (ou une autre solution équivalente)
* générer du code html/javascript qui encapsulent le javascript ;
* On peut également imposer le format des données envoyées au client - à ce dernier de de récuperer les données de la manière qu’il veut. Mais c’est à mon sens retrouver la difficulté de développer en javascript, et non plus réutiliser des composants tout faits.
* On peut aussi mettre à disposition des metadata décrivant les ressources.
Ressources
- Jquery : http://www.jquery.com
- Jqgrid : http://www.secondpersonplural.ca/jqgriddocs/index.htm
-
Jersey : http://download.java.net/maven/2/com/sun/jersey/jersey-archive/1.0.2/jersey-archive-1.0.2.zip
- SOFEA : http://wisdomofganesh.blogspot.com/2007/10/life-above-service-tier.html
- Le consultant Matt Raible : il aide LinkedIn et d'autres clients dans des expérimentations web et lire aussi cette discussion sur Comet qui évoque des architectures basées sur les services Resfull et non sur des frameworks usuels.
- Présentation de la JSR 311 par le Touilleur : http://www.touilleur-express.fr/2008/04/25/jsr-311-jax-rs-rest-une-histoire-de-restaurant/
- Sur le blog d'Oxiane, un article sur le ria
- Le zip de ce POC sous la forme d'un projet Eclipse: Télécharger ici.
