Restful Web Services avec les JSP

Nicolas Zozol

Cet article explique comment utiliser les pages JSP pour décortiquer des requêtes HTTP afin de construire simplement un Service Web RESTful sans passer par des frameworks ou une auto-génération par un IDE. L'article nécessite d'avoir un peu d'expérience en JSP et en Ajax, ainsi que de légères connaissances sur le protocole Http.

- Titre : Restful Web Services avec les JSP

- Auteur : Nicolas Zozol

- Parution : 28 mai 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Nicolas Zozol. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- I. Architecture Globale

- II. Récupérer les données de la requête

- II-1. Récupérer les paramètres

- II-2. Récupérer l'URI de la requête

- II-C. Récupérer le PostBody de la requête

- III. Traiter les différentes méthodes Http

- IV. Analyser les URI

- IV-1. Objectifs

- IV-2. Servlet-Mapping

- IV-3. Analyse de l'URI

- V. Plus d'élégance avec un Tag JSP

- VI. Identifier l'utilisateur avec un Cookie ou des Credentials

- VI-1. Pourquoi un Cookie ?

- VI-2. Pourquoi les Credentials ?

- VI-3. Architecture multi-tiers

- VI-4. Le Tag JSP récupérant les credentials

- VIII. Le Status Code de Réponse

- Conclusion

- Annexes

- Une version de Prototype adaptée

- L'interface RessourceController

- La ToolBox Robusta

- Remerciements

Introduction

On appellera un Service Web RESTful si il englobe la grande majorité des bons conseils de Roy T. Fielding, l'inventeur de REST.

- HTTP

- Simplicité

- Adressabilité

- Connectivité

- Absence d'Etat ou de Session du serveur (Stateless)

- Les URI définissent des Ressources

Comme tout autre Service Web, la sécurité des données est importante. Les mauvaises langues diront que les Ressources d'un service SOAP sont hautement sécurisées puisque impossibles à atteindre.

I - Architecture Globale

II - Récupérer les données de la requête

II-1 - Récupérer les paramètres

C'est la partie classique et facile. Soit une requête PUT /wsxseditor/service/document/document.jsp?dataBaseId=24&title=ChangeTheTitle Il suffit d'intégrer dans la page jsp :

<jsp:useBean id="controller" class="controller.documentController" scope="request"/>
<jsp:setProperty name="controller" property="*"/>

dataBaseId et title sont des attributs de votre classe DocumentController. Le DocumentController doit implémenter les fonctions setDataBaseId() et setTitle() et le moteur des JSP fait automatiquement le mapping.

II-2 - Récupérer l'URI de la requête

Chaque JSP peut manipuler l'objet HttpServletRequest en utilisant ${pageContext.request}. Voici ce que l'on ajoute à la JSP :

<jsp:setProperty name="controller" property="URI" value="${pageContext.request.requestURI}"/>

DocumentController doit maintenant implementer l'interface RessourceController, ou au moins la fonction setURI().

II-C - Récupérer le PostBody de la requête

Voici une requête POST vue par le sniffer Wireshark. Notez qu'une requête POST peut très bien avoir une QueryString, ce qui peut être utile pour l'algorithme utilisant cete requête.

Dans cet exemple classique, je crée un nouvel élément dans mon document. Cet élément est représenté en XML dans le PostBody, et j'utilise dans la QueryString les paramètres fatherId et brotherId pour savoir où placer l'élément. Mais récupérer le PostBody est un peu plus délicat car il faut jongler avec les Streams de Java.

<%
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(request.getInputStream()));

String line,result="";
while ((line = br.readLine()) != null) {
result+=line;
}
br.close();
controller.setPostBody(result);
%>

Le DocumentController doit implémenter l'interface RessourceController, ou au moins la fonction setPostBody().

III - Traiter les différentes méthodes Http

L'utilisation des méthodes Http GET (Read), POST (Create), PUT (Update) et DELETE (Delete) est, à mon avis, l'aspect fondamental d'un Service Web RESTful. Pour connaitre la méthode utilisée, il faut encore faire appel à HttpServletRequest :

<c:if test="${pageContext.request.method=='POST' }">
<root>
<c:choose>
<c:when test="${controller.createOK}">
<id>controller.id</id><!-- creating a ressource add a row in the database-->
</c:when>
<c:otherwise>
<error>controller.message</error>
</c:otherwise>
</c:choose>
</c:if>

<c:if test="${ pageContext.request.method == 'PUT' }">
<c:choose>
<c:when test="${controller.updateOK}">
<ok/> <!-- a simple update -->
</c:when>
<c:otherwise>
<error>controller.message</error>
</c:otherwise>
</c:choose>
</c:if>
</root>

Le RessourceController implémentera selon les possibilités du Service Web les fonctions isReadOK(), isCreateOK(), is UpdateOK(), isDeleteOK().

IV - Analyser les URI

IV-1 - Objectifs

Nous avions démarré l'article avec cette requête : PUT /wsxseditor/service/document/document.jsp?dataBaseId=24&title=ChangeTheTitle Nous voudrions une requête plus sexy comme celle-ci : PUT /wsxseditor/document/24?title=ChangeTheTitle

Cette écriture d'URL est moins importante que l'utilisation correcte des méthode HTTP, mais favorisera l'échange d'URL dans les différentes pages Web, et donc la Connectivité qui est bien sûr un concept essentiel du Web. L'écriture d'URL sexy permettra également, contrairement à SOAP, de comprendre au premier coup d'oeil ce que signifie la requête ce qui est très utile dans la maintenance du service.

IV-2 - Servlet-Mapping

C'est très facile avec Netbeans de transférer une requête /wsxseditor/document/24 vers la JSP /wsxseditor/service/document/document.jsp. Double-cliquez sur le fichier web.xml, et remplissez ce formulaire :

web.xml

Le fichier web.xml ressemblera à cela :

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<servlet>
<servlet-name>UriAdaptor</servlet-name>
<jsp-file>/service/document/document.jsp</jsp-file>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>UriAdaptor</servlet-name>
<url-pattern>/document/*</url-pattern>
</servlet-mapping>
</web-app>

Par contre, puisque le paramètre dataBaseId a disparu, il faut faire comprendre à notre RessourceController que le nombre 24 correspond au paramètre dataBaseId.

IV-3 - Analyse de l'URI

Le ResourceController implemente la fonction setURI (). Quand nous transmettrons l'URI au Controller, on en profitera pour la décoder :

public void setUri(String uri) throws NotFoundException {
this.uri = uri;
robusta.rest.j2ee.UrlDecoder decoder=new robusta.rest.j2ee.UrlDecoder();
this.dataBaseId=decoder.firstNumber(uri);
}

UrlDecoder.firstNumber() est une fonction de ma Robusta ToolBox qui utilise des expressions régulières pour détecter le premier nombre. Vous trouverez le code source à cette adresse ou dans le zip en annexe.

V - Plus d'élégance avec un Tag JSP

Les scripts JSP ( <% java code %> ) ne sont pas très lisibles et, pire, favorisent le copier/coller du code source. On peut cependant utiliser un tag JSP simple et non-intrusif pour résoudre ces problèmes.
Voici le fichier /WEB-INF/tags/request.tag :

<%@tag description="Conciliate RESTful Web Services and JSPs - public domain - By Nicolas Zozol" pageEncoding="UTF-8"%>

<%@attribute name="method"%>
<%@attribute name="ressource" rtexprvalue="true" type="base.RessourceController" required="true"%>
<%@attribute name="request" rtexprvalue="true" type="javax.servlet.http.HttpServletRequest" required="true"%>

<%
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(request.getInputStream()));

String line,result="";
while ((line = br.readLine()) != null) {
result+=line;
}
br.close();

ressource.setPostRequest(result);
ressource.setURI(request.getRequestURI());
this.method=request.getMethod();
%>

Le Tag nécessite le RessourceController et la HttpServletRequest, puis crée la variable method. La page JSP ressemblera finalement à :

<%@ taglib tagdir="/WEB-INF/tags/" prefix="robusta" %>
<jsp:useBean id="controller" class="controller.documentController" scope="request"/>
<jsp:setProperty name="controller" property="*"/>

<robusta:request ressource="${controller}" request="${pageContext.request}"/>

<c:if test="${method == 'POST' }">
... some xml ...
</c:if>

Votre controller devra impérativement implémenter l'interface RessourceController afin de recevoir l'URI et le PostBody.

VI - Identifier l'utilisateur avec un Cookie ou des Credentials

Il s'agit maintenant de protéger les Ressources afin d'éviter que le tout venant n'éxecute une requête DELETE /users/58. Il y a évidemment plusieurs méthodes pour identifier l'individu. Mais la plupart d'entre elles utilisent soit un cookie, soit le header Authorization:mode credential.

VI-1 - Pourquoi un Cookie ?

On utilisera en général le cookie pour lire des données qui ne sont pas dangereuses ni ultra-confidentielles. En principe l'utilisateur est responsable de son PC, ou doit cliquer sur le bouton deconnexion sur un PC public. Si il ne le fait pas, un intrus pourra lire le cookie. Par contre Wikipédia utilise par exemple le cookie pour afficher discrètement en haut à gauche des informations sur l'utilisateur en cours. Pour cela, Wikipédia envoie le cookie :

Cookie: frwikiUserName=Nicorama; frwiki_session=4f64774a3fkskfea0b2cbc4b6779fc94; frwikiUserID=52490; frwikiToken=abdf769sfcafhbb3c4c719cbdb8950a\r\n

En cliquant d'un lien à un autre sur Wikipédia, le DIV correspondant au user reste. Mais l'URL affichée dans le browser va changer, et vous pouvez envoyer par mail cette adresse URL à un ami si vous souhaitez entamer un debat. Le cookie est un moyen simple de garder l'Adressabilité de la page Web, ce qui facilite aussi le référencement dans les moteurs de recherches. Notez que la page est lisible en http et non https, ce qui est une raison de plus pour n'accéder qu'à des données non confidentielles.

Pour en savoir plus, voici un lien vers un blog en anglais traitant des "best practices" dans l'utilisation d'un cookie.

VI-2 - Pourquoi les Credentials ?

On appelle les Credentials la valeur du header http Authorization : BASIC fdhqjkdkjq==

Supposons maintenant que vous vouliez effacer un utilisateur avec une requête DELETE /users/58. En cliquant sur un lien <a> classique ou en rentrant une adresse URL dans votre browser, vous n'obtiendrez que des GET. Il faut donc programmer cette requête, en utilisant en général Ajax (1). Lors de cette requête, insérer des credentials permet d'identifier l'utilisateur en se passant de cookies (que l'on a vu peu fiables).

Il est cependant nécessaire d'avoir une méthode de génération différente entre le cookie et le credential (sinon un intrus sur votre PC pourrait (2) lire le cookie puis accéder aux données confidentielles), et il faudra également passer par https afin de crypter les credentials.

Voici un exemple d'implémentation d'une telle requête avec Prototype :

new Ajax.Request("/users/58", {

method : "DELETE",
requestHeaders:["Authorization", "BASIC fjqkks678dd=="],
onCreate:function(){},
onSuccess:function(){/*OK*/},
onComplete:function(transport){
return transport;
},
onException:function(request, exception){
alert("xhr -- Exception !:"+exception.message);
},
onFailure:function(){/*FAIL*/}

});

Pour exécuter cette requête, il faut que Javascript connaisse les credentials. En passant d'une page web à une autre, Javascript va perdre cette donnée puisqu'il est hors de question de passer par un cookie.

En utilisant les credentials dans une application Ajax, on perd donc en général l'Adressabilité.

VI-3 - Architecture multi-tiers

Dans ce chapitre, je suppose que c'est le navigateur qui envoit des requêtes vers le service web. Il est cependant possible et même fréquent d'utiliser un serveur pour la couche présentation (JSF, PHP ou autre) envoyant des requêtes vers le service web afin de nourrir cette présentation. C'est ce que l'on a toujours fait avant Ajax.

Dans ce cas là, le serveur de présentation n'est pas prédestiné à créer et envoyer des cookies vers le service web. Si il reçoit un cookie du navigateur, il doit le convertir en credential, ou éventuellement rediriger tel quel le cookie.

VI-4 - Le Tag JSP récupérant les credentials

Notre RessourceController va maintenant implémenter les fonctions setCredentials() et getCredentials(). Le tag va automatiquement récupérer ces données à la volée.

<%
/* Suite du tag robusta:request */

/* Credentials read in the header : Authorization:Basic xxxyyy or in the cookie : Authorization:Basic-xxxyyy
* In your cookies, you will use sometimes myToken instead of Authorization associated to Basic. The implementation below will change
*/
boolean credentialsFound = false;
//priority is to the header over the cookie : if there are credentials in the header "Authorization", we don't care of the cookie
if (request.getHeader("Authorization") != null && !request.getHeader("Authorization").equals("")) {
ressource.setCredentials(request.getHeader("Authorization"));
credentialsFound = true;
} else {//This is my implementation to read into the cookie.
javax.servlet.http.Cookie[] cookieList = request.getCookies();
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().toLowerCase().contains("authorization")) {
ressource.setCredentials(cookieList[i].getValue());
credentialsFound = true;
}
break;
}
}
}

%>

Avec cette partie de tag, vous pourrez récupérer le "code d'authentification" en utilisant soit le cookie, soit le header Authorization. Cependant votre code métier doit gérer ces différents modes puisque, pour un même utilisateur, le code d'authentification doit impérativement être différent dans les deux cas.

VIII - Le Status Code de Réponse

Les adeptes de REST aiment utiliser un large éventail des codes retournés par le serveur, bien au-delà de 200, 404 ou 500. Ce petit tag permet de le faire aisemment.
Dans le fichier /WEB-INF/tags/response.tag :

<%@tag description="Set datas to the response" pageEncoding="UTF-8"%>

<%@attribute name="statusCode" rtexprvalue="true" type="Integer"  required="false"%>
<%@attribute name="response" rtexprvalue="true" type="javax.servlet.http.HttpServletResponse"  required="true"%>
<%@attribute name="ressource" rtexprvalue="true" type="robusta.rest.RessourceController"  required="true"%>
<%-- any content can be specified here e.g.: --%>

<%
if (statusCode > 0) {
response.setStatus(statusCode);
} else {
if (ressource!=null && ressource.getStatusCode() > 0) {
response.setStatus(ressource.getStatusCode());
}
}

%>

On peut alors insérer dans la JSP :

<%@ taglib tagdir="/WEB-INF/tags/" prefix="robusta" %>
<jsp:useBean id="controller" class="controller.documentController" scope="request"/>
<jsp:setProperty name="controller" property="*"/>

<robusta:request ressource="${controller}" request="${pageContext.request}"/>

<c:if test="${method == 'POST' }">
... some xml ...
</c:if>

<robusta:response statusCode="212" ressource="${controller}" response="${pageContext.response}" />

Affichez la ligne <robusta:response> à la fin de la JSP, sinon votre serveur prendra le dessus. Vous pouvez utiliser l'attribut optionnel statusCode afin de noter directement le status, ou utiliser l'attribut également optionnel ressource - dans ce cas, vous devez coder dans votre Controller les différentes possibilités de statusCode car le tag appellera la fonction getStatusCode().

Conclusion

L'utilisation de cette méthode a un avantage immense vis-à-vis des Servlets : elle évite les affreux out.println("\"Me voilà\" dît Baudelaire"); mais intègre les URI, PostBody et méthodes Http de façon bien plus simple que ne le faisaient traditionnellement les Servlets. La page JSP sera parfaite pour échanger des données XML avec une application cliente Ajax ou avec la couche graphique de votre application.

JSF peut bien sûr faire le même travail, mais est plus lent et surtout plus compliqué à apprendre. Comme JSF est surtout dédié à la couche graphique du serveur cela ne vous aidera pas beaucoup pour créer un Service Web. Bien que les applications Ajax délèguent maintenant une bonne partie de la couche graphique au client Javascript, vous pouvez faire une application JSF qui lancera des requêtes vers votre service web JSP afin d'afficher ces données dans un composant JSF.

Le code des tags est sous domaine public afin d'être utilisé dans tout type de projets - à condition de spécifier mon nom dans le code. Si vous apportez des astuces ou améliorations non liées à votre code métier, n'hésitez pas à me les transmettre :). La bibliothèque Robusta est sous licence GPL 2.

Annexes

Une version de Prototype adaptée

Prototype transforme les requêtes DELETE et PUT en méthode GET, mais en rajoutant le paramètre _method. Voici le lien vers une version de Prototype effectuant réellement les méthodes PUT et DELETE - ce qui ne sera pas possible dans les navigateurs tels que Konquerors ou quelques navigateurs anciens ou exotiques.

L'interface RessourceController

Voici la version que j'utilise. On peut rajouter des fonctions permettant par exemple de s'assurer qu'un utilisateur ne fasse pas plusieurs POST en une minute.

package robusta.rest;

/**
* A RessourceController is implemented by Controllers - you can then design nice customs JSP tags
* @author Nicolas Zozol - Edupassion.com - Robusta Web - nzozol@edupassion.com
*/
public interface RessourceController {

public void setURI(String uri);
public void setPostBody(String postBody);

/* Functions for differents http methods */
public boolean isReadOK();
public boolean isCreateOK();
public boolean isUpdateOK();
public boolean isDeleteOK();

/* StatusCode and optional message sent */
public int getStatusCode();
public void setStatusCode(int statusCode);
public String getMessage();

/* security : you can extend the interface or use Utility Classes */
public void setCredentials(String credentials);
public String getCredentials();

}

La ToolBox Robusta

La bibliothèque Robusta présente plusieurs packages qui sont tous indépendants (si ce n'est qu'ils utilisent parfois le package exception). Pour l'instant, elle est en travail intensif, et ne doit être utilisée que pour l'inspiration (sous Licence GPL 2.0).

Remerciements

Merci à Florian Casabianca pour la critique technique de l'article et Diogène pour sa relecture.

(1) Il est également possible de capturer l'événement sur un lien <a> avec Javascript, ou d'utiliser <a href="javascript:myFunction()">xxxyyy</a>

(2) Il existe cependant des protocoles non cryptés et fiables, comme Kerberos, mais plus compliqués à mettre en oeuvre.