Service Web REST

Dans l'étude précédente, nous avons découvert le service Web SOAP. Ce protocole extrêmement sophistiqué utilise beaucoup de ressources puisqu'il est nécessaire de générer et de traduire des documents XML qui transitent au travers du protocole HTTP. Vous devez construire, à la fois côté client et aussi côté serveur un certain nombre de classes pour réaliser ces différents décodages pour retrouver les informations encapsulées dans le document XML. Durant cette étude, nous allons abordé une autre alternative. Malgré qu'elle ne soit pas standard, elle consiste à utiliser directement le protocole HTTP sans couche supplémentaire, à l'aide cette fois-ci d'un web service dénommé, service web REST. Cette approche est plus simple à implémenter. Par ailleurs, les services web, dans ce cas là, sont plutôt associés à gérer des ressources distantes avec toutes les phases classiques, de création, de récupération, de modification et de suppression (CRUD).

Sommaire de l'étude

Présentation des services web

Avec la plateforme Java EE, nous avons découvert de nombreux services différents, qui sont très faciles à développer codage extrêmement simplifié, grâce à la technique des objets distants. Dans un premier temps, nous avons utiliser les beans Session qui s'intéressent à la logique métier. Ce sont des objets distants facile à manipuler. Effectivement, avec une application fenêtrée classique, il suffit d'appeler les méthodes de ces objets comme s'ils étaient présent sur le poste local. Par contre, pour que ce fonctionnement simple puisse s'établir, vous devez rester dans le réseau local de l'entreprise le numéro de port 3307 de ce service va être bloqué par le pare-feu.

Travailler avec des applications clientes riches

Si vous désirez utiliser ce service depuis Internet, nous avons découvert que nous devions passer par une application Web qui elle-même communique, en local, aux différents beans Session. Cela fonctionne très bien, mais nous devons passer systématiquement par un navigateur Web, ce qui limite beaucoup la présentation et l'ergonomie il s'agit d'un client léger.

Le top du top serait de pouvoir faire comme en réseau local, c'est-à-dire de pouvoir utiliser une application fenêtrée, qui fait appel aux différents services, tout en étant sur Internet, et donc sans passer par un navigateur. Il existe une solution pour cela, il s'agit de la technique des services Web. En réalité, ces services Web communiquent comme une application Web, c'est-à-dire au travers du protocole HTTP ce qui permet la communication par Internet.

Service Web REST

La pile des services web SOAP, WSDL, WS-* décrite au chapitre précédent fournit à la fois une interopérabilité pour l'injection des messages et le style RPC. Avec l'avènement du Web 2.0, un nouveau type de services web est devenu à la mode : les services web REST. De nombreux acteurs essentiels du Web, comme Amazon, Google et Yahoo!, ont abandonné leurs services SOAP en faveur de services REST orientés ressources. Lorsqu'il s'agit de choisir entre des services SOAP et REST, de nombreux facteurs entrent en ligne de compte. REST est intégré à Java EE 7 via JAX-RS, que nous utiliserons au cours de ce chapitre.

Architecture d'un service Web REST

REST est un type d'architecture reposant sur le fonctionnement même du web, qu'il applique aux services web. Pour concevoir un service REST, il faut bien connaître tout simplement le protocole HTTP, le principe des URI et respecter quelques règles. Il faut raisonner en terme de ressources.

Dans l'architecture REST, toute information est une ressource et chacune d'elles est désignée par une URI - généralement un lien sur le Web. Les ressources sont manipulées par un ensemble d'opérations simples et bien définies. L'architecture client-serveur de REST est conçue pour utiliser un protocole de communication sans état - le plus souvent HTTP. Ces principes encouragent la simplicité, la légèreté et l'efficacité des applications.

Ressources
Les ressources jouent un rôle central dans les architectures REST. Une ressource est tout ce que peut désigner ou manipuler un client, tout information pouvant être référencée dans un lien hypertexte. Elle peut être stockée dans une base de données, un fichier, etc. Il faut éviter autant que possible d'exposer des concepts abstraits sous forme de ressources et privilégier plutôt les objets simples.
URI
Une ressource web est identifiée par une URI, qui est un identifiant unique formé d'un nom et d'une adresse indiquant où trouver la ressource. Il existe différents types d'URI : les adresses web, les UDI, les URI, et enfin les combinaisons d'URL et d'URN.
Quelques exemples d'URI
http://www.movies.fr/categories/aventure
http://www.movies.fr/catalog/titles/movies/123456
http://www.weather.com/weather/2012?location=Aurillac,France
http://www.flickr.com/explore/intersting/2012/01/01
http://www.flickr.com/explore/intersting/24hours

Les URI devraient être aussi descriptives que possible et ne désigner qu'une seule ressource, bien que des URI différentes qui identifient des ressources différentes puissent mener aux mêmes données : à un instant donné, la liste des photos intéressantes publiées sur Flickr le 01/01/2012 était la même que la liste des photos données au cours des 24 dernières heures, bien que l'information envoyée par les deux URI ne fût pas la même.

Représentation
Nous pouvons obtenir la représentation d'un objet sous forme de texte, de XML de PDF ou tout autre format. Un client traite toujours une ressource au travers de sa représentation ; la ressource elle-même reste sur le serveur. La représentation contient toutes les informations utiles à propos de l'état d'une ressource :
http://www.apress.fr/book/catalog/java
http://www.apress.fr/book/catalog/java.csv
http://www.apress.fr/book/catalog/java.xml

La première URI est la représentation par défaut de la ressource, les représentations supplémentaires lui ajoutent simplement l'extension de leur format : .csv, .xml, .pdf, etc. L'autre solution consiste à n'exposer qu'une seule URI pour toutes les représentation, comme la première par exemple, et à utiliser un mécanisme appelé négociation du contenu, que nous présenterons un peu plus loin.

Le protocole HTTP

La requête la plus simple du protocole HTTP est formé de GET suivi d'une URL qui pointe sur des données fichier statiques, traitement dynamique.... Elle est envoyée par un navigateur quand nous saisissons directement une URL dans le champ d'adresse du navigateur. Le serveur HTTP répond en renvoyant les données demandées.

La méthode GET du protocole HTTP

Requêtes et réponses

Un client envoie une requête à un serveur afin d'obtenir une réponse. Les messages utilisés pour ces échanges sont formés d'une enveloppe et d'un corps également appelé document ou représentation. Voici, par exemple, un type de requête envoyée à un serveur :

Cette requête contient plusieurs informations envoyées par le client :

  • La méthode HTTP GET ;
  • Le chemin, ici la racine / ;
  • Plusieurs autre en-têtes de requête.
Vous remarquez que la requête n'a pas de corps un GET n'a jamais de corps. En réponse, le serveur renvoie sa réponse et elle est formée des parties suivantes :
  • Un code de réponse : ici le code est 200 OK.
  • Plusieurs en-têtes de réponse, notamment Date, Server, Content-Type. Ici, le type de contenu est text/html, mais il pourrait s'agir de n'importe quel format comme du XML (application/xml) ou une image (image/jpeg), etc.
  • Un corps ou représentation. Ici, il s'agit du contenu de la page web renvoyée qui n'est pas visible sur la figure proposée ci-dessus.
Rappel sur les méthodes du HTTP

Le web est formé de ressources bien identifiées, reliées ensemble et auxquelles accéder au moyen de requêtes HTTP simples. Les requêtes principales de HTTP sont de type GET, POST, PUT et DELETE. Ces types sont appelés verbes ou méthodes. HTTP définit quatre autres verbes plus rarement utilisés, HEAD, TRACE, OPTIONS et CONNECT.

GET
GET est une méthode de lecture demandant une représentation d'une ressource. Elle doit être implémentée de sorte à ne pas modifier l'état de la ressource. En outre, GET doit être idempotente, ce qui signifie qu'elle doit laisser la ressource dans le même état, quel que soit le nombre de fois où elle est appelée. Ces deux caractéristiques garantissent une plus grande stabilité : si un client n'obtient pas de réponse à cause d'un problème réseau, par exemple, il peut renouveler sa requête et s'attendre à la même réponse que celle qu'il aurait obtenue initialement, sans corrompre l'état de la ressource sur le serveur.
POST
A partir d'une représentation texte, XML, etc., POST crée une nouvelle ressource subordonnée à une ressource principale identifiée par l'URI demandée. Des exemples d'utilisation de POST sont l'ajout d'un message à un fichier journal, d'un livre à une liste d'ouvrages, etc. POST modifie donc l'état de la ressource et n'est pas idempotente envoyer deux fois la même requête produit deux nouvelles ressources subordonnées. Si une ressource a été créée sur le serveur d'origine, le code de la réponse devrait être 201 Created. La plupart des navigateurs modernes ne produisent que des requêtes GET et POST.
PUT
Une requête PUT est conçue pour modifier l'état de la ressource stockée à une certaine URI. Si l'URI de la requête fait référence à une ressource inexistante, celle-ci sera créée avec cette URI. PUT modifie donc l'état de la ressource, mais elle est idempotente : même si nous envoyons plusieurs fois la même requête PUT, l'état de la ressource finale restera inchangé.
DELETE
Une requête DELETE supprime une ressource. La réponse à DELETE peut être un message d'état dans le corps de la réponse ou aucun code du tout. DELETE est idempotente, mais elle modifie évidemment l'état de la ressource.
HEAD
HEAD ressemble à GET sauf que le serveur ne renvoie pas de corps dans sa réponse. HEAD permet par exemple de vérifier la validité d'un client ou la taille d'une entité sans avoir besoin de la transférer.
TRACE
TRACE retrace la requête reçue.
OPTION
OPTION est une demande d'information sur les options de communication disponibles pour la chaîne requête/réponse identifiée par l'URI. Cette méthode permet au client de connaître les options et/ou les exigences associées à une ressource, ou les possibilités d'un serveur sans demander d'action sur une ressource et sans récupérer aucune ressource.
CONNECT
CONNECT est utilisé avec un proxy pouvant se transformer dynamiquement en tunnel une technique grâce à laquelle le protocole HTTP sert d'enveloppe à différents protocoles réseau.
Négociation du contenu

La négociation de contenu est définie comme le fait de choisir la meilleure représentation pour une réponse donnée lorsque plusieurs représentations sont disponibles. Les besoins, les souhaits et les capacités des clients varient : la meilleure représentation pour l'utilisateur d'un téléphone portable au Japon peut, en effet, ne pas être la plus adaptée à un lecteur flux RSS en France.

La négociation du contenu utilise entre autres les en-têtes HTTP : Accept, Accept-Charset, Accept-Encoding, Accept-Language et User-Agent. Pour obtenir, par exemple, la représentation CSV de la liste des livres sur Java publiés par Apress, l'application cliente l'agent utilisateur demandera http://www.apress.com/books/catalog/java avec un en-tête Accept initialisé à text/csv.

Vous pouvez aussi imaginer que, selon la valeur de l'en-tête Accept-Language, le contenu de ce document CSV pourrait être traduit par le serveur dans la langue correspondante.
Types de contenu

HTTP utilise des types de supports Intenet initialement appelés types MIME dans les en-têtes Content-Type et Accept afin de permettre un typage des données et une négociation de contenu ouverts et extensibles. Les types de support Internet sont divisés en cinq catégories : text, image, audio, video et application. Ces types sont à leur tour divisés en sous-types text/plain, text/html, text/xhtml, etc.. Voici quelques-uns des plus utilisés :

text/html
HTML est utilisé par l'infrastructure d'information du World Wide Web depuis 1990 et sa spécification a été décrite dans plusieurs documents informels. Le type de support text/html a été initialement défini en 1995 par le groupe le groupe de traveil IETF HTML. Il permet d'envoyer et d'interpréter les pages web classiques.
text/plain
Il s'agit du type de contenu par défaut car il est utilisé pour les messages textuels simples.
imagegif, image/jpeg, image/png
Le type de support image exige la présence d'un dispositif d'affichage un écran ou une imprimante graphique, par exemple permettant de visualiser l'information.
text/xml, application/xml
Envoi et réception de document XML.
application/json
JSON est un format textuel léger pour l'échange de données. Il est indépendant des langages de programmation.
Code d'état

Un code HTTP est associé à chaque réponse. La spécification définit environ 60 codes d'états ; l'élément Status-Code est un entier de trois chiffres qui décrit le contexte d'une réponse et qui est intégéré dans l'enveloppe de celle-ci. Le premier chiffre indique l'une des cinq classes de réponses possibles :

Voici quelques codes d'état que vous avez sûrement déjà dû rencontrer :

Mise en cache et requêtes conditionnelles

La mise en cache est un élément crucial pour la plupart des systèmes distribués. Elle a pour but d'améliorer les performances en évaluant les requêtes inutiles et en réduisant le volume de données des réponses. HTTP dispose de mécanisme permettant la mise en cache et la vérification de l'exactitude des données du cache. Si le client décide de ne pas utiliser ce cache, il devra toujours demander les données, même si elles n'ont pas été modifiées depuis la dernière requête.

La réponde à une requête de type GET peut contenir un en-tête Last-Modified indiquant la date de dernière modification de la ressource. La prochaine fois que l'agent utilisateur demandera cette ressource, il passera cette date dans l'en-tête If-Modified-Since : le serveur web ou le proxy la comparera alors à la date de dernière modification. Si celle envoyée par l'agent utilisateur est égale ou plus récente, le serveur renverra une réponse sans corps, avec un code d'état 304 Not Modified. Sinon l'opération demandée sera réalisée ou transférée.

Les dates peuvent être difficiles à manipuler et impliquent que les agents concernés soient, et restent, synchronisés : c'est le but de l'en-tête de réponse ETag, qui peut être considéré comme un hachage MD5 ou SHA1 de tous les octets d'une représentation - si un seul octet est modifié, la valeur d'ETag sera différente. La valeur ETag reçue dans une réponse à une requête GET peut, ensuite, être affectée à un en-tête If-Match d'une requête.

Spécification des services web REST

Contrairement à SOAP et à la pile WS-* qui reposent sur les standards du W3C ; REST n'est pas un standard : c'est uniquement un style d'architecture respectant certains critères de conception. Les applications REST, cependant, dépendent fortement d'autres standards comme : HTTP, XML, JSON, JPEG, etc. Sa prise en compte par Java a été spécifiée par JAX-RS, mais REST est comme un patron de conception : c'est une solution réutilisable d'un problème courant, qui peut être implémentée en différents langages.

JAX-RS 1.1

Pour écrire des services web REST, il suffit d'un client et d'un serveur reconnaissant le protocole HTTP. N'importe quel navigateur et un conteneur de servlet HTTP pourraient donc faire l'affaire, au prix d'un peu de configuration XML et d'ajustement du code. Au final, ce code pourrait devenir peu lisible et difficile à maintenir : c'est là que JAX-RS vole à notre secours.

Sa première version, finalisée en octobre 2008, définit un ensemble d'API mettant en avant une architecture REST. Au moyen d'annotations, il simplifie l'implémentation de ces services et améliore la productivité. La spécification ne couvre que la partie serveur du REST.

Nouveautés de JAX-RS 1.1 : Il s'agit d'une version de maintenance axée sur l'intégration avec Java EE 6 et ses nouvelles fonctionnalités. Les nouveautés principales de JAX-RS 1.1 sont les suivantes :

L'approche REST

Comme nous l'avons déjà mentionné, REST est un ensemble de contraintes de conceptions générales reposant sur HTTP. Ce chapitre s'intéressant aux services web et REST dérivant du web, nous commencerons par une navigation réelle passant en revue les principes du Web. Ce dernier est devenu une source essentielle d'informations et fait désormais partie de nos outils quotidiens : vous le connaissez donc sûrement très bien et cette familiarité vous aidera donc à comprendre les concepts et les propriétés de REST.

Du Web aux services web

Nous savons comment fonctionne le Web : pourquoi les services web devraient-ils se comporter différemment ? Après tout, ils échangent souvent uniquement des ressources bien identifiées, liées à d'autres au moyen de liens hypertextes. L'architecture du Web ayant prouvé sa tenue en charge au cours du temps, pourquoi réinventer la roue ?

Pour créer, modifier et supprimer une ressource livre, pourquoi ne pas utiliser les verbes classiques de HTTP ? Par exemple :

En se servant ainsi des verbes HTTP, nous pouvons donc effectuer toutes les actions CRUD sur une ressource à l'image des bases de données.
Pratique de la navigation sur le web

Comment obtenir la liste des livres sur Java publiés par ce site ? En faisant pointer son navigateur sur le site web : http://www.livre.com. Même si cette page ne contiendra sûrement pas les informations exactes que nous recherchons, nous nous attendons à ce qu'elle donne accès, par un moyen ou un autre, à la liste des livres consacrés à Java.

La page d'accueil offre un moteur de recherche de tous les livres, mais également un répertoire de livres classés par technologies. Si nous cliquons sur le noeud Java, la magie de l'hypertexte opère et nous obtenons la liste complète des livres sur Java publiés.

Supposons que nous ayons sauvegardé le lien dans notre gestionnaire de favoris et qu'au cours du parcours de la liste, un ouvrage particulier attire votre attention : le lien hypertexte sur le titre en question nous mènera à la page contenant le résumé, la biographie des auteurs, etc.

Nous vondrons comparer ce livre avec un au ouvrage. Les pages des livres nous donnent accès à une représentation plus concrète sous la forme de prévisualisations : nous pouvons alors ouvrir une prévisualisation, lire la table des matières et faire notre choix.

Voici ce nous faisons quotidiennement avec nos navigateurs. REST applique les mêmes principes à vos services où les livres, les résultats des recherches, une table des matières ou la couverture d'un livre peuvent être définis comme des ressources.
Sans état

La dernière fonctionnalité de REST est l'absence d'état, ce qui signifie que toute requête HTTP est totalement indépendante puisque le serveur ne mémorisera jamais les requêtes qui ont été effectuées.

Pour plus de clarté, l'état de la ressource et celui de l'application sont généralement différenciés : l'état de la ressource doit se trouver sur le serveur et être partagé par tous, tandis que celui de l'application doit rester chez le client et être sa seule propriété.

Si nous revenons à l'exemple des livres, l'état de l'application est que le client a récupéré par exemple une représentation du livre désiré, mais le serveur ne mémorisera pas cette information. L'état de la ressource, quant à lui, est l'information sur l'ouvrage : le serveur doit évidemment la mémoriser et le client peut le modifier. Si le panier virtuel est une ressource dont l'accès est réservé à un seul client, l'application doit stocker l'identifiant de ce panier dans la session du client.

L'absence d'état possède de nombreux avantages, notamment une meilleure adaptation à la charge : aucune information de session à gérer, pas besoin de router les requêtes suivantes vers le même serveur, gestion des erreurs, etc. Si vous devez mémoriser l'état, le client devra faire un travail supplémentaire pour le stocker.

JAX-RS : Java API for RESTful Web Services

Vous pouvez vous demander à quoi ressemblera le code qui s'appuie sur des concepts d'aussi bas niveau que le protocole HTTP. En réalité, vous n'avez pas besoin d'écrire des requêtes HTTP ni de créer manuellement des réponses car JAX-RS est une API très élégante qui permet d'écrire une ressource à l'aide de quelques annotations seulement.

Mise en oeuvre d'une application Web

Je vous propose toute de simple de prendre un exemple extrêmement simple de service web REST qui utilise seulement la méthode GET. Ce service nous renvoie un texte non interprété qui nous souhaite la bienvenue. J'utilise Glassfish 4 et Netbeans pour ce développement. Glassfish dans sa version Java EE 7 intégre en natif Jersey

Structure de l'application Web

La première démarche consiste à utiliser une application Web au travers de laquelle nous allons annoter une simple classe Java POJO et renseigner le descripteur de déploiement web.xml qui va activer la servlet qui s'occupe d'implémenter la technologie REST.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
   
    <servlet>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    
</web-app>
rest.Bienvenue.java
package rest;

import javax.ws.rs.*;

@Path("/bienvenue")
public class Bienvenue {    
   @GET   
   @Produces("text/plain")   
   public String getMessage() {      
      return "Bienvenue... (premier exemple REST)";   
   }
}
Bienvenue étant une classe Java annotée par @Path, la ressource sera hébergée à l'URI /bienvenue. La méthode getMessage() est elle-même annotée par @GET afin d'indiquer qu'elle traitera les requêtes HTTP GET et qu'elle produit du texte le contenu est identifié par le type MIME text/plain.
Utilisation du service web

Pour accéder à la ressource, il suffit d'un client HTTP, simple navigateur par exemple, pouvant envoyer une requête GET vers l'URL http://localhost:8080/AWRest/bienvenue.

Plugin Poster

Il est également possible d'utiliser un client HTTP qui permet de tester les services web de type REST en nous montrant les différentes en-têtes. Vous pouvez installer par exemple le pluggin Poster du navigateur Firefox ou autre.

Conclusion

Le code précédent montre que le service REST n'implémente aucune interface et n'étend aucune classe : @Path est la seule annotation obligatoire pour transformer un POJO en service REST. JAX-RS utilisant la configuration par exception, un ensemble d'annotations permettent de modifier son comportement par défaut. Les exigences que doit satisfaire une classe pour devenir REST sont les suivantes :

  • Elle doit être annoté par @javax.ws.rs.Path.
  • Pour ajouter les fonctionnalités des EJB au service REST, la classe doit être annotée @javax.ejb.Stateless.
  • Elle doit être publique et ne pas être finale ni abstraite.
  • Les classes ressources racine celles ayant une annotation @Path doivent avoir un constructeur par défaut public ou aucun. Les classes ressources non racine n'exigent pas ce constructeur.
  • La classe ne doit pas définir la méthode finalyze().
Par nature, JAX-RS repose sur HTTP et dispose d'un ensemble de classes et d'annotations clairement définies pour gérer HTTP et les URI. Une ressource pouvant avoir plusieurs représentations, l'API permet de gérer un certain nombre de types de contenu et utilise JAXB pour sérialiser et désérialiser les représentations XML et JSON en objets. JAX-RS étant indépendant du conteneur, les ressources peuvent être évidemment déployées dans Glassfish, mais également dans un grand nombre d'autres conteneurs de servlets.
A l'aide d'un bean session

Les services REST peuvent également être des beans session sans état, ce qui permet d'utiliser des transactions pour accéder à une couche de persistance à l'aide des entités et des gestionnaires d'entités.

entité.Personne.java
package entité;
import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;

@Entity
@XmlRootElement
@NamedQueries({   
   @NamedQuery(name="toutLePersonnel", query="SELECT p FROM Personne p ORDER BY p.nom, p.prénom"),   
   @NamedQuery(name="recherchePersonnel", query="SELECT p FROM Personne p WHERE p.nom = :nom AND p.prénom = :prénom")
})
public class Personne implements Serializable {
   @Id   
   @GeneratedValue(strategy = GenerationType.IDENTITY)   
   private long id;   private String nom;   
   private String prénom;   
   @Temporal(javax.persistence.TemporalType.DATE)
   private Date naissance;   
   private String téléphone;   

   public long getId() { return id;  }   

   public Date getNaissance() { return naissance;  }   
   public void setNaissance(Date naissance) {  this.naissance = naissance;  }   

   public String getNom() { return nom;  }   
   public void setNom(String nom) { this.nom = nom.toUpperCase(Locale.FRENCH); }

   public String getPrénom() { return prénom; }   
   public void setPrénom(String prénom) {  
       StringBuilder chaine = new StringBuilder(prénom.toLowerCase());
       chaine.setCharAt(0, Character.toUpperCase(chaine.charAt(0)));       
       this.prénom = chaine.toString();   
   }   

   public String getTéléphone() { return téléphone; }   
   public void setTéléphone(String téléphone) { this.téléphone = téléphone;  }   

   @Override   
   public String toString() { return nom +" "+ prénom;  } 
}
session.GestionPersonnel.java
package session;
import entité.Personne;
import java.util.List;
import javax.ejb.*;
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.xml.bind.JAXBElement;

@Path("/personnels")
@Stateless
@Produces({"application/xml", "application/json"})
@Consumes({"application/xml", "application/json"})
public class GestionPersonnel {
   @PersistenceContext   
   private EntityManager bd;   
   @Context   
   private UriInfo uri;
   
   @GET   
   public List<Personne> toutLePersonnel() {      
     Query requête = bd.createNamedQuery("toutLePersonnel");      
     return requête.getResultList();   
   }
   
   @POST   
   public void nouveauPersonnel(JAXBElement<Personne> personne) {      
     bd.persist(personne.getValue());    
   }
}
Mise en place du Service
Le code du bean session représente un service REST pouvant consommer et produire les représentations XML et JSON d'un personnel.
Récupérer une ressource
La méthode toutLePersonnel() récupère la liste de tout le personnel à partir d'une base de données et renvoie sa représentation XML ou JSON en utilisant la négociation de contenu ; elle est appelée par une requête GET.
Création d'une nouvelle ressource
La méthode nouveauPersonnel() prend une représentation XML ou JSON d'une personne et la stocke dans la base de données. Cette méthode est invoquée par une requête POST.
Définition des URI

La valeur de l'annotation @Path est relative à un chemin URI. Lorsqu'elle est utilisée sur des classes, celles-ci sont considérées comme des ressources racine, car elles fournissent la racine de l'arborescence des ressources et l'accès aux sous-ressources.

Il est ainsi possible de régler très facilement le chemin de base de l'URI de telle sorte que cela corresponde à la racine de l'application web du premier exemple de bienvenue. l'URL à introduire au niveau du navigateur devient alors http://localhost:8080/AWRest/.

rest.Bienvenue.java
package rest;

import javax.ws.rs.*;

@Path("/")  // Changement du chemin de l'URI pour que cela corresponde à la racine de l'application Web
public class Bienvenue {
   
   @GET
   @Produces("text/plain")
   public String getMessage() {
      return "Bienvenue... (premier exemple REST)";
   }
}
Vous pouvez également intégrer dans la syntaxe de l'URI des modèles de chemins d'URI au moyen d'un nom de variable entouré d'accolades : ces variables seront évaluées à l'exécution. @Path("/personnels/{nom}").
@Path peut également s'appliquer aux méthodes des ressources racine, ce qui permet de regrouper les fonctionnalités à plusieurs ressources.
session.GestionPersonnel.java
package session;

import entité.Personne;
import java.util.List;
import javax.ejb.*;
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.xml.bind.JAXBElement;

@Path("/personnels")
@Stateless
@Produces({"application/xml", "application/json"})
@Consumes({"application/xml", "application/json"})
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;
   @Context
   private UriInfo uri;
   
   @GET
   public List<Personne> toutLePersonnel() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
   
   @GET
   @Path("{idPersonnel}")
   public Personne unPersonnel(@PathParam("idPersonnel") long idPersonnel) {
      return bd.find(Personne.class, idPersonnel);
   }        
   
   @GET
   @Path("requete")
   public Personne unPersonnel(@QueryParam("nom") String nom, @QueryParam("prenom") String prénom) {
      Query requête = bd.createNamedQuery("recherchePersonnel");
      requête.setParameter("nom", nom);
      requête.setParameter("prénom", prénom);
      return (Personne) requête.getSingleResult();
   }           
   
   @GET
   @Path("/premiers/")
   public List<Personne> dixPremiers() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      requête.setMaxResults(10);
      return requête.getResultList();
   }        
   
   @GET
   @Path("/suivants/{page}")
   public List<Personne> dixSuivants(@PathParam("page") int page) {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      requête.setFirstResult(page);
      requête.setMaxResults(10*page);
      return requête.getResultList();
   }          
   
   @POST
   public void nouveauPersonnel(JAXBElement<Personne> personne) {
      bd.persist(personne.getValue());    
   }
 
   @DELETE
   @Path("{idPersonnel}")
   public void supprimePersonnel(@PathParam("idPersonnel") long idPersonnel) {
      bd.remove(unPersonnel(idPersonnel));
   }  

   @PUT
   @Path("{idPersonnel}")
   public void modifiePersonnel(@PathParam("idPersonnel") long idPersonnel) {
      bd.merge(unPersonnel(idPersonnel));
   }     
}
  • Si @Path est appliquée à la fois sur la classe et une méthode, le chemin relatif de la ressource produite par cette méthode est la concaténation de la classe et de la méthode.
  • Ainsi, pour obtenir un personnel par son identifiant, par exemple, le chemin sera /personnels/1234. Cette recherche sera effectuée au travers de la méthode unPersonnel().
  • Si nous demandons la ressource racine /personnels, seule la méthode sans annotation @Path sera automatiquement sélectionnée : ici toutLePersonnel().
  • Si nous demandons /personnels/premiers, c'est la méthode dixPremiers() qui sera invoquée.
  • Si nous désirons avoir les dix personnels de la troisème page, c'est la méthode dixSuivants() qui sera invoquée en proposant l'URI /personnels/suivants/3.
  • Si @Path("/personnels") n'annotait que la classe et aucune méthode, le chemin d'accès de toutes les méthodes serait le même et il faudrait alors utiliser le verbe HTTP (GET, PUT) et la négociation du contenu texte, XML, etc. pour les différencier.
Extraction des paramètres

Nous avons besoin d'extraire des informations sur les URI et les requêtes lorsque nous les manipulons. Le code précédent nous a déjà montré comment extraire un paramètre du chemin à l'aide de l'annotation @javax.ws.rs.PathParam. JAX-RS un ensemble d'annotation supplémentaires pour extraire les différents paramètres qu'une requête peut envoyer (@QueryParam, @MatrixParam, @CookieParam et @FormParam).

@PathParam

Cette annotation permet d'extraire la valeur du paramètre d'une requête. Le code suivant permet d'extraire l'identifiant du personnel 1234 de l'URI : http://localhost:8080/RESTPersonnel/personnels/1234 :

@Path("/personnels")
public class GestionPersonnel {

   @GET
   @Path("{idPersonnel}")
   public Personne unPersonnel(@PathParam("idPersonnel") long idPersonnel) {
      return bd.find(Personne.class, idPersonnel);
   }        
...
}

Dans l'annotation @Path associée à une méthode, il est possible de proposer plusieurs variables qui deviendront les différents paramètres de la méthode. Il est ainsi possible de répérer un personnel à partir de son nom et de son prénom grâce à l'URI suivante : http://localhost:8080/RESTPersonnel/personnels/nom-REMY-prenom-Emmanuel.

@Path("/personnels")
public class GestionPersonnel {

   @GET
   @Path("nom-{nom}-prenom-{prenom}")
   public Personne unPersonnel(@PathParam("nom") String nom, @PathParam("prenom") String prénom) {
      Query requête = bd.createNamedQuery("recherchePersonnel");
      requête.setParameter("nom", nom);
      requête.setParameter("prénom", prénom);
      return (Personne) requête.getSingleResult();
   }         
...
}

Une particularité, c'est qu'il est possible de proposer une liste variable de paramètres dans votre URI. Chaque paramètre sera ensuite récupéré séparément.

@Path("/bienvenue/{civilité}")
public class Bienvenue {
   
   @GET
   @Path("{identité : .+}/age/{age}")
   @Produces("text/plain")
   public String récupérer(@PathParam("civilité") String choix, @PathParam("identité") List<PathSegment> params, @PathParam("age") int age) {
      StringBuilder chaîne = new StringBuilder(choix+' ');
      for (PathSegment segment : params) chaîne.append(segment.getPath()+' ');
      chaîne.append(age+" ans");
      return chaîne.toString();
   }
}

@QueryParam

Cette annotation permet d'extraire la valeur d'un paramètre modèle d'une URI. Là aussi, le code suivant permet de retrouver un personnel à partir de son nom et de son prénom au moyen de l'URI suivante : http://localhost:8080/RESTPersonnel/personnels?nom=REMY&prenom=Emmanuel :

@Path("/personnels")
public class GestionPersonnel {

   @GET
   public Personne unPersonnel(@QueryParam("nom") String nom, @QueryParam("prenom") String prénom) {
      Query requête = bd.createNamedQuery("recherchePersonnel");
      requête.setParameter("nom", nom);
      requête.setParameter("prénom", prénom);
      return (Personne) requête.getSingleResult();
   }           
...
}
@MatrixParam

Cette annotation agit comme @QueryParam, sauf qu'elle extrait la valeur d'un paramètre matrice d'une URI le délimiteur est ; au lieu de ?. Le code suivant permet également, sous une autre forme, de retrouver un personnel à partir de son nom et de son prénom avec l'URI suivante : http://localhost:8080/RESTPersonnel/personnels;nom=REMY;prenom=Emmanuel :

@Path("/personnels")
public class GestionPersonnel {

   @GET
   public Personne unPersonnel(@MatrixParam("nom") String nom, @MatrixParam("prenom") String prénom) {
      Query requête = bd.createNamedQuery("recherchePersonnel");
      requête.setParameter("nom", nom);
      requête.setParameter("prénom", prénom);
      return (Personne) requête.getSingleResult();
   }           
...
}
@CookieParam et @HeaderParam

Deux autres méthodes sont liées aux détails internes de HTTP, ce que nous ne voyons pas directement dans les URI : les cookies et les en-têtes. @CookieParam extrait la valeur d'un cookie, tandis que @HeaderParam permet d'obtenir la valeur d'un en-tête :

@Path("/")
@Produces("text/plain")
public class Bienvenue {
   
   @GET  
   public String bienvenue() {
      return "Bienvenue... (premier exemple REST)";
   }
   
   @GET
   @Path("paramètres")
   public String paramètres(@HeaderParam("Accept") String typeContenu, @HeaderParam("User-Agent") String agent) {
      return typeContenu+'\n'+agent;
   }
}

@FormParam

Cette annotation précise que la valeur d'un paramètre doit être extraite d'un formulaire situé dans le corps de la requête :

@Path("/personnels")
public class GestionPersonnel {

   @GET
   @Path("formulaire")
   @Consumes("application/x-www-form-urlencoded")
   public Personne recherche(@FormParam("nom") String nom, @FormParam("prenom") String prénom) {
      Query requête = bd.createNamedQuery("recherchePersonnel");
      requête.setParameter("nom", nom);
      requête.setParameter("prénom", prénom);
      return (Personne) requête.getSingleResult();
   }          
...
}
@DefaultValue

Nous pouvons ajouter @DefaultValue à toutes ces annotations pour définir une valeur par défaut pour le paramètre que nous attendons. Cette valeur sera utilisée si les métadonnées correspondantes sont absentes de la requête. Si le paramètre age ne se trouve pas dans la requête, par exemple, le code suivant utilisera la valeur 50 par défaut :

@Path("/personnels")
public class GestionPersonnel {

   @GET
   public Personne unPersonnel(@DefaultValue("50") @QueryParam("age") int âge) {
      ...
   }           
}
Consommation et production des types de contenu

Avec REST, une ressource peut avoir plusieurs représentations : un personnel peut être représenté comme une page web, un document XML ou une image affichant sa photo d'identité. Les annotations @javax.ws.rs.Consumes et @javax.ws.rs.Produces peuvent s'appliquer à une ressource qui propose plusieurs représentations : elle définit les types des médias échangés entre le client et le serveur.

L'utilisation de l'une de ces annotations sur une méthode redéfinit celle qui s'appliquait sur la classe de la ressource pour un paramètre d'une méthode ou une valeur de retour. En leur absence, on suppose que la ressource supporte tous les types de média (*/*). L'expression du type de contenu du type MIME se fait au moyen d'une simple chaine de caractères, comme text/plain par exemple, ou au travers de la classe MediaType qui possède toutes les constantes prédéfinies de chacun des types MIME connus, comme par exemple MediaType.TEXT_PLAIN.
Format brut ou HTML

Dans le code qui suit, Bienvenue produit par défaut une représentation en texte brut, sauf pour la première méthode qui propose un résultat sous forme de page HTML.

package rest;

import javax.ws.rs.*;

@Path("/")
@Produces("text/plain")
public class Bienvenue {
   
   @GET  
   @Produces("text/html")
   public String bienvenue() {
      return "<html><h2 align='center'>Bienvenue... (premier exemple REST)</h2></html>";
   }
   
   @GET
   public String paramètres(@HeaderParam("Accept") String typeContenu, @HeaderParam("User-Agent") String agent) {
      return typeContenu+'\n'+agent;
   }
}

  • Si une ressource peut produire plusieurs types de média Internet, la méthode choisie correspondra au type qui convient le mieux à l'en-tête Accept de la requête HTTP du client.
  • Si, par exemple, cet en-tête est : Accept : text/plain et que l'URI est /, c'est la méthode bienvenue() qui sera invoquée.
  • Le client aurait pu également utiliser l'en-tête suivant : Accept : text/plain; q=0.8, text/html. Cet en-tête annonce que le client peut accepter les types text/plain et text/html mais qu'il préfère le dernier choix avec un facteur de qualité de 0.8 je préfère huit fois plus le text/html que le text/plain. En incluant cet en-tête à la même requête /, c'est la méthode paramètres() qui est cette fois-ci invoquée :

Format JSON

JSON est un format léger pour l'échange de données structurées complexes. Il est à l'image des documents XML en moins verbeux. Il est très utile lorsque vous devez transférer toutes les informations relative à une entité par exemple. Voici ci-dessous un exemple de document JSON représentant une entité Personne qui peut disposer de plusieurs numéros de téléphones :

{
   "id" : 51,
   "nom" : "REMY",
   "prénom" : "Emmanuel"
   "naissance" : 01/10/1959
   "téléphones" : ["04-55-88-99-77", "06-89-89-87-77"]
}
Fournisseur d'entités

Lorsque les entités sont reçues dans des requêtes ou envoyées dans des réponses, l'implémentation JAX-RS doit pouvoir convertir les représentations en Java et vice versa : c'est le rôle des fournisseurs d'entités. JAXB, par exemple, traduit un objet en représentation et réciproquement. Il existe deux variantes de fournisseur d'entités : MessageBodyReader et MessageBodyWriter.

Requêtes
Pour traduire le corps d'une requête en Java, une classe doit implémenter l'interface javax.ws.rs.MessageBodyReader et être annotée par @Provider. Par défaut, l'implémentation est censée consommer tous les types de médias */*, mais l'annotation @Consumes permet de restreindre les types supportés.
Réponses
De la même façon, un type Java peut être traduit en corps de réponse. Une classe Java voulant effectuer ce traitement doit implémenter l'interface javax.ws.rs.MessageBodyWriter et être annotée par l'interface @Provider. L'annotation @Produces indique les types de médias supportés.
Fournisseurs d'entités par défaut
L'implémentation de JAX-RS offre un ensemble de fournisseurs d'entités par défaut convenant aux situations courantes.
Type Description
byte[] Tous les types de média */*.
java.lang.String Tous les types de média */*.
java.io.InputStream Tous les types de média */*.
java.io.Reader Tous les types de média */*.
java.io.File Tous les types de média */*.
java.activation.DataSource Tous les types de média */*.
javax.xml.transform.Source Type XML text/xml, application/xml et application/-*+xml.
javax.xml.bind.JAXBElement Types de média XML de JAXB text/-xml, application/xml et application/*+xml.
MultivalueMap<String,String> Contenu de formulaire application/x-www-form-urlencoded.
javax.ws.rs.core.StreamingOutput Tous les types de médias */*, uniquement MessageBodyWriter.
Sérialisation automatique
JAX-RS peut automatiquement effectuer des opérations de sérialisation et dé-sérialisation vers un type Java spécifique
  • */* : byte[]
  • text/* : String
  • text/xml, application/xml, application/*+xml : JAXBElement
  • application/x-www-form-urlencoded : MultivalueMap<String,String>
Gestion du contenu : InputStream

Requête et réponse avec un flux d’entrée :

@POST
@Path("flux")
public void lireFlux(InputStream is) throws IOException {
   byte[] octets = lireOctets(is);
   String input = new String(octets);
   System.out.println(input);
}
   
private byte[] lireOctets(InputStream stream) throws IOException {
   ByteArrayOutputStream baos = new ByteArrayOutputStream();
   byte[] buffer = new byte[1000]; int octetsLus = 0;
   do {
       octetsLus = stream.read(buffer);
       if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
    } 
    while (octetsLus > -1);
    return baos.toByteArray();
}
   
@Path("flux")
@GET
@Produces(MediaType.TEXT_XML)
public InputStream envoyerDocumentXML() throws FileNotFoundException {
    return new FileInputStream("exemple.xml");
}
Gestion du contenu : File

Requête et réponse avec un fichier :

@Path("fichier")
@PUT
public void lireFichier(File file) throws IOException {
   byte[] octets = lireOctets(new FileInputStream(file));
   String input = new String(octets);
   System.out.println(input);
}

@Path("fichier")
@GET
@Produces(MediaType.TEXT_XML)
public File envoyerFichier() {
   File file = new File("exemple.xml");
   return file;
}
Gestion du contenu : byte[]

Requête et réponse avec un tableau d'octets :

@Path("/")
public class Bienvenue {
   
   @GET
   @Produces("text/plain")
   public byte[] get() {
      return "Bienvenue à tous".getBytes();
   }
   
   @POST
   @Consumes("text/plain")
   public void post(byte[] octets) {
      System.out.println(new String(octets));
   }
}
Gestion du contenu : String

Requête et réponse avec une chaîne de caractères

@Path("chaine")
@PUT
public void contenuChaine(String current) throws IOException {
   System.out.println(current);
}

@Path("chaine")
@GET
@Produces(MediaType.TEXT_XML)
public String envoiChaine() {
   return "<?xml version=\"1.0\"?>" + "<details>Bienvenue à tous" +"</details>";
}
Types personnalisés : format XML

Gestion d'un personnel de l'entreprise

@GET
@Produces(MediaType.APPLICATION_XML)
public List<Personne> toutLePersonnel() {
   Query requête = bd.createNamedQuery("toutLePersonnel");
   return requête.getResultList();
}

@GET
@Produces(MediaType.APPLICATION_XML) 
// Le type MIME retourné par le service ce qui permet au client de connaître le format à traiter
@Path("{idPersonnel}")
public Personne unPersonnel(@PathParam("idPersonnel") long idPersonnel) {
   return bd.find(Personne.class, idPersonnel);
}      

@POST
@Consumes(MediaType.APPLICATION_XML)  // Utilisation d’un objet JAXBElement pour envelopper le type Personne
public void nouveauPersonnel(JAXBElement<Personne> personne) {
   bd.persist(personne.getValue());    
}

@DELETE
@Path("{idPersonnel}")
public void supprimePersonnel(@PathParam("idPersonnel") long idPersonnel) {
   bd.remove(unPersonnel(idPersonnel));
}  

@PUT
@Consumes(MediaType.APPLICATION_XML)
public void modifiePersonnel(Personne personne) {
   bd.merge(personne);
}
Méthodes ou interface uniforme

Nous avons découvert comment le protocole HTTP gérait ses requêtes, ses réponses et ses méthodes d'actions (GET, POST, PUT, etc.). JAX-RS définit ces méthodes HTTP à l'aide des annotations respectives : @GET, @POST, @PUT, @DELETE, @HEAD et @OPTIONS. Seules les méthodes publiques peuvent être exposées comme des méthodes de ressources. Le code suivant montre une ressource personnels exposant les méthodes CRUD.

Méthodes CRUD
@Path("/personnels")
public class GestionPersonnel {
   
   @GET
   public List<Personne> toutLePersonnel() {
      ...
   }
   
   @POST
   @Consumes("application/xml")
   public Response nouveauPersonnel(InputStream is) {
      ...   
   }

   @PUT
   @Path("{idPersonnel}")
   @Consumes(MediaType.APPLICATION_XML)
   public Response modifiePersonnel(Personne personne, InputStream is) {
      ...
   }     
   
   @DELETE
   @Path("{idPersonnel}")
   public void supprimePersonnel(@PathParam("idPersonnel") long idPersonnel) {
      ...
   }  
}
  • Lorsqu'une méthode ressource est invoquée, les paramètres annotés par l'une des méthodes d'extraction vues précédemment sont initialisés. La valeur d'un paramètre non annoté appelé paramètre entité est obtenue à partir du corps de la requête et convertie par un des fournisseurs d'entités vus précédemment.
  • Les méthodes peuvent retourner void, Response ou un autre type Java. Response indique qu'il faudra fournir d'autres métadonnées. Lorsque nous créons un nouvel agent de l'entreprise, par exemple, il serait judicieux de renvoyer son URI personnelle.
La classe Response

Actuellement, tous les services développés retournaient soit un type void soit un type Java défini par le développeur. JAX-RS facilite la construction de réponses en permettant de choisir un code de retour, de fournir des paramètres dans l’en-tête, de retourner une URI, etc. Les réponses complexes sont définies par la classe Response disposant de méthodes abstraites non utilisables directement :

Les informations de ces méthodes sont obtenues par des méthodes statiques retournant des ResponseBuilder.
Principales méthodes de la classe Response
  • ResponseBuilder created(URI locale) : Modifie la valeur de Location dans l’en-tête, à utiliser pour une nouvelle ressource créée.
  • ResponseBuilder notModified() : Statut à Not Modified.
  • ResponseBuilder ok() : Statut à Ok.
  • ResponseBuilder serverError() : Statut à Server Error.
  • ResponseBuilder status(Response.Status) : défini un statut particulier défini dans Response.Status.
Principales méthodes de la classe ReponseBuilder
  • Response build() : crée une instance.
  • ResponseBuilder entity(Object value) : modifie le contenu du corps.
  • ResponseBuilder header(String, Object) : modifie un paramètre de l’en-tête.
Exemple : Code de retour OK et ajout d'informations dans l’en-tête de la réponse
@Path("/bienvenue")
public class Bienvenue {

   @GET
   @Produces("text/plain")
   public Response getBooks() {
       return Response.status(Response.Status.OK)
                                 .header("message1", "Bonjour")
                                 .header("message2", "Salut")
                                 .entity("Bienvenue... (premier exemple REST)")
                                 .build();      // Finalisation en appelant la méthode build()
   }
}

Exemple : Code de retour avec erreur dans la réponse
@Path("/bienvenue")
public class Bienvenue {

   @GET
   public Response getBooks() {
       return Response.serverError().build();      // Finalisation en appelant la méthode build()
   }
}

Informations contextuelles

Le fournisseur de ressources a besoin d'informations contextuelles pour traiter correctement une requête. L'annotation @javax.ws.rs.Context sert à injecter les classes suivantes dans un attribut ou dans la paramètre d'une méthode : HttpHeaders informations liées à l’en-tête, UriInfo informations liées aux URIs, Request informations liées au traitement de la requête, SecurityContext informations liées à la sécurité et Providers. Ainsi, l’annotation @Context permet d’injecter des objets liés au contexte de l’application. Certains de ces objets permettent d’obtenir les mêmes informations que les précédentes annotations liées aux paramètres.

UriInfo

Un objet de type UriInfo permet d’extraire les informations brutes d’une requête HTTP. Les principales méthodes sont les suivantes :

  • String getPath() : Chemin relatif de la requête.
  • MultivaluedMap<String, String> getPathParameters() : Valeurs des paramètres de la requête contenues dans Template Parameters.
  • MultivaluedMap<String, String> getQueryParameters() : Valeurs des paramètres de la requête.
  • URI getBaseUri() : Chemin de l’application.
  • URI getAbsolutePath() : Chemin absolu base + chemins.
  • URI getRequestUri() : Chemin absolu incluant les paramètres.
@Path("/bienvenue")
public class Bienvenue {

   @GET
   @Path("{param}")
   public void getInformationUriInfo(@Context UriInfo uriInfo, @PathParam("param") String param, @QueryParam("nom") String nom) {
      System.out.println("getPath() : " + uriInfo.getPath());
      System.out.println("getAbsolutePath() : " + uriInfo.getAbsolutePath());
      System.out.println("getBaseUri() : " + uriInfo.getBaseUri());
      System.out.println("getRequestUri() : " + uriInfo.getRequestUri());
      System.out.println("paramètre : "+ param);
      System.out.println("nom : "+ nom);
      System.out.println("getPathParameters() : "+uriInfo.getPathParameters());
      System.out.println("getQueryParameters() : "+uriInfo.getQueryParameters());
   }
}
Voici les résultats obtenus sur le serveur d'application à l'issue de cette requête http://localhost:8080/AWRest/bienvenue/test?nom=REMY.
  • Infos: getPath() : bienvenue/test.
  • Infos: getAbsolutePath() : http://localhost:8080/AWRest/bienvenue/test.
  • Infos: getBaseUri() : http://localhost:8080/AWRest/.
  • Infos: getRequestUri() : http://localhost:8080/AWRest/bienvenue/test?nom=REMY.
  • Infos: paramètre : test.
  • Infos: nom : REMY.
  • Infos: getPathParameters() : {param=[test]}.
  • Infos: getQueryParameters() : {nom=[REMY]}.
Plutôt que d'associer une information d'URI proposée à chacune des méthodes de la classe en spécifiant systématiquement un paramètre supplémentaire, il peut être judicieux de factoriser cette information en tant qu'attribut de la classe du  Service Web REST. Ainsi, nous pourrons éventuellement exploiter ces informations pour chacune des méthodes de la classe. Voici le changement effectué de l'exemple précédent :
@Path("/bienvenue")
public class Bienvenue {
   
   @Context 
   UriInfo uriInfo;
   
   @GET
   @Path("{param}")
   public void getInformationUriInfo(@PathParam("param") String param, @QueryParam("nom") String nom) {
      System.out.println("getPath() : " + uriInfo.getPath());
      System.out.println("getAbsolutePath() : " + uriInfo.getAbsolutePath());
      System.out.println("getBaseUri() : " + uriInfo.getBaseUri());
      System.out.println("getRequestUri() : " + uriInfo.getRequestUri());
      System.out.println("paramètre : "+ param);
      System.out.println("nom : "+ nom);
      System.out.println("getPathParameters() : "+uriInfo.getPathParameters());
      System.out.println("getQueryParameters() : "+uriInfo.getQueryParameters());
   }
}
Les en-têtes : HttpHeaders

Comme nous l'avons vu précédemment, les informations transportées entre le client et le serveur sont formées non pas uniquement du corps d'une entité, mais également d'en-têtes Date, Content-type, etc.. Les en-têtes HTTP font partie de l'interface uniforme et les services web REST les utilisent. La classe javax.ws.rs.HttpHeaders peut être injectée dans un attribut ou dans un paramètre de méthode au moyen de l'annotation @Context afin de permettre d'accéder aux valeurs des en-têtes sans tenir compte de leur casse. Un objet de type HttpHeader permet d’extraire les informations contenues dans l’en-tête d’une requête. Les principales méthodes sont les suivantes :

  • Map<String, Cookie> getCookies() : les cookies de la requête.
  • Locale getLanguage() : la langue de la requête.
  • MultivaluedMap<String, String> getRequestHeaders() : valeurs des paramètres de l’en-tête de la requête.
  • MediaType getMediaType() : le type MIME de la requête.
A noter que ces méthodes permettent d’obtenir le même résultat que les annotations @HeaderParam et @CookieParam.
@Path("/bienvenue")
public class Bienvenue {

   @GET
   @Produces("text/plain")
   public Response get(@Context HttpHeaders enTêtes) {
      StringBuilder chaîne = new StringBuilder(enTêtes.getAcceptableLanguages().toString());
      chaîne.append('\n');
      chaîne.append(enTêtes.getRequestHeader("accept-language"));
      return Response.ok().entity(chaîne.toString()).build();
   }
}

Là aussi, nous pouvons prendre un attribut de type HttpHeaders plutôt de la prévoir pour chacune des méthodes de la classe du Service Web REST :
@Path("/bienvenue")
public class Bienvenue {

   @Context 
   HttpHeaders enTêtes;
   
   @GET
   @Produces("text/plain")
   public Response get() {
      StringBuilder chaîne = new StringBuilder(enTêtes.getAcceptableLanguages().toString());
      chaîne.append('\n');
      chaîne.append(enTêtes.getRequestHeader("accept-language"));
      return Response.ok().entity(chaîne.toString()).build();
   }
}
Construction d'URI

Les liens hypertextes sont un élément central des applications REST. Afin d'évoluer à travers les états de l'application, les services web REST doivent gérer les transitions et la construction des URIs. Pour ce faire, JAX-RS fournit une classe javax.ws.rs.core.UriBuilder destinée à remplacer java.net.URI et à faciliter la construction d'URI. UriBuilder dispose d'un ensemble de méthodes permettant de construire de nouvelles URIs ou d'en fabriquer à partir d'URIs existantes.

La classe utilitaire UriBuilder permet de construire des URIs complexes. Il est possible de construire des URIs avec UriBuilder via UriInfo au moyen de l'annotation @Context comme précédemment où toutes URIs seront relatives au chemin de la requête. Voici les méthodes pour obtenir un UriBuilder : Le principe d’utilisation de la classe utilitaire UriBuilder est identique à ResponseBuilder. Les principales méthodes sont les suivantes :
Création d'une URI pour la réponse
@Path("/bienvenue")
public class Bienvenue {

   @Context UriInfo info;
   
   @GET
   public Response get() {
      UriBuilder fabrique = info.getAbsolutePathBuilder();
      URI uri = fabrique.path("{nom}/{prenom}").queryParam("age", "{age}").build("REMY", "Emmanuel", 59);
      return Response.created(uri).build();
   }
}

Gestion des exceptions

Les codes que nous avons présentés jusqu'à maintenant s'exéctaient dans un monde parfait, où tout se passe bien et où il n'y a pas besoin de traiter les exceptions. Malheureusement, ce monde n'existe pas et, tôt ou tard, une ressource nous exploser en plein visage parce que les données reçues ne sont pas valides ou que des parties du réseau ne sont pas fiable. Comme le montre le code suivant, nous pouvons lever à tout instant une exception WebApplicationException ou l'une de ses sous-classes dans un fournisseur de ressources. Cette exception sera capturée par l'implémentation de JAX-RS et convertie en réponse HTTP.

L'erreur par défaut est un code 500 avec un message vide, mais la classe javax.ws.rs.WebApplicationException offre différents constructeurs permettant de choisir un code d'état spécifique défini dans l'énumération javax.ws.rs.core.Response.Status ou une entité. Les exceptions non controlées et les erreurs qui n'entrent pas dans les deux cas précédents seront relancées comme toute exception Java non contrôlée.
rest.Bienvenue.java
@Path("/bienvenue")
public class Bienvenue {
   
   @GET
   @Path("age/{age}")
   @Produces("text/html")
   public String getAge(@PathParam("age") int age) {
      if (age<0) throw new WebApplicationException(Response.status(400).entity("Vous devez donner une valeur positive...").build());
      return "Age = "+age+ " ans";
   }
}

Cycle de vie

Lorsqu'une requête arrive, la ressource cible est résolue et une nouvelle instance de la classe ressource racine correspondante est créée. Le cycle de vie d'une classe ressource racine dure donc le temps d'une requête, ce qui implique que la classe ressource n'a pas à s'occuper des problèmes de concurrence et qu'elle peut donc utiliser les variables d'instance en toute sécurité.

S'ils sont employés dans un conteneur Java EE servlet ou EJB, les classes ressources et les fournisseurs JAX-RS peuvent également utiliser les annotations de gestion de cycle de vie et de la sécurité : @PostConstruct, @Predestroy, @RunAs, @RolesAllowed, @PermitAll, @DenyAll et @DeclareRoles. Le cycle de vie d'une ressource peut ainsi se servir de @PostConstruct et de @PreDestroy pour ajouter de la logique métier respectivement après la création d'une ressource et avant sa suppression.

Mise en oeuvre d'un projet de gestion du personnel

Je vous propose de mettre en oeuvre un projet qui va nous permettre de valider toutes ces nouvelles compétences. Ce projet sera placé dans une application Web qui sera accessible de façon classique au travers d'un simple navigateur. Il sera aussi possible de gérer le personnel au travers d'un service web REST à l'aide de méthodes adaptées à pas mal de situations différentes. Dans ce dernier cas, nous pourrons gérer le personnel à l'aide d'une application cliente Android par exemple.

L'application web utilise les compétences de composants de haut niveau proposés par PrimeFaces. Il sera donc nécessaire de déployer, en même temps que le projet principal, les archives primefaces-3.2.jar pour l'ensemble des composants JSF ainsi que sunny.jar pour le thème.

Développement du projet

Je vous donne l'ensembles des codes nécessaire à cette application web suivi des différents tests pour évaluer le fonctionnement correct du service web REST.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" ...
    <context-param>
        <param-name>primefaces.THEME</param-name>
        <param-value>sunny</param-value>
    </context-param>
    
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>ServletAdaptor</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    
    <servlet-mapping>
        <servlet-name>ServletAdaptor</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>
Nous remarquons ici la présence de deux servlets, la première relative à JSF qui sert de contrôleur pour les requêtes classiques de l'application web, la deuxième s'occupant plus particulièrement des fonctionnalités du service web REST.
entité.Personne.java
package entité;

import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@Entity
@NamedQuery(name="toutLePersonnel", query="SELECT p FROM Personne p ORDER BY p.nom, p.prénom")
public class Personne implements Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private long id;
   private String nom;
   private String prénom;
   @Temporal(javax.persistence.TemporalType.DATE)
   private Date naissance;
   private String téléphone;

   public Personne() {}

   public Personne(String nom, String prénom, Date naissance, String téléphone) {
      setNom(nom);
      setPrénom(prénom);
      this.naissance = naissance;
      this.téléphone = téléphone;
   }   

   public long getId() { return id;  }

   public Date getNaissance() { return naissance;  }
   public void setNaissance(Date naissance) {  this.naissance = naissance;   }

   public String getNom() { return nom;  }
   public void setNom(String nom) {  this.nom = nom.toUpperCase(Locale.FRANCE);   }

   public String getTéléphone() { return téléphone;  }
   public void setTéléphone(String téléphone) { this.téléphone = téléphone; }

   public String getPrénom() { return prénom;   }

   public void setPrénom(String prénom) { 
      StringBuilder chaîne = new StringBuilder(prénom.toLowerCase(Locale.FRANCE));
      chaîne.setCharAt(0, Character.toUpperCase(chaîne.charAt(0)));
      this.prénom = chaîne.toString();   
   }

   @Override
   public String toString() { return nom + " " + prénom;  }   
}
Il s'agit ici de l'entité Personne. Remarquez au passage que cette entité possède l'annotation indispensable @XmlRootElement si vous souhaitez soumettre cette entité au format JSON.
service.GestionPersonnel.java
package service;

import entité.Personne;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;

@Path("/")
@Stateless
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

   @GET
   @Produces("application/json")
   @Path("tous")
   public List<Personne> listePersonnels() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
   
   @GET
   @Produces("application/json")
   @Path("json/{id}")
   public Personne rechercheJSON(@PathParam("id") long id) {
      return bd.find(Personne.class, id);
   }
   
   @GET
   @Path("{id}")
   public Response rechercheDétaillée(@PathParam("id") long id) {
      Personne personne = bd.find(Personne.class, id);
      return Response.ok().header("nom", personne.getNom())
                                        .header("prenom", personne.getPrénom())
                                        .header("date", personne.getNaissance().getTime())
                                        .header("telephone", personne.getTéléphone()).build();
   }   
   
   @POST
   public Response nouveau(@HeaderParam("nom") String nom, 
                           @HeaderParam("prenom") String prénom, 
                           @HeaderParam("date") long date, 
                           @HeaderParam("telephone") String téléphone) {
      Personne personne = new Personne(nom, prénom, new Date(date), téléphone);
      bd.persist(personne);
      return Response.ok().header("id", personne.getId()).build();
   }   
   
   @POST
   @Path("{nom}/{prénom}/{téléphone}")
   public Response nouveau(@PathParam("nom") String nom, 
                           @PathParam("prénom") String prénom, 
                           @QueryParam("annee") int année, 
                           @QueryParam("mois") int mois,
                           @QueryParam("jour") int jour,
                           @PathParam("téléphone") String téléphone) {
      Personne personne = new Personne(nom, prénom, new GregorianCalendar(année, mois-1, jour).getTime(), téléphone);
      bd.persist(personne);
      return Response.ok().header("id", personne.getId()).build();
   }   

   @POST
   @Path("json")
   @Consumes("application/json")
   public void nouveau(Personne personne) {
      bd.persist(personne);
   }

   @PUT
   @Path("enTête/{id}")
   public void modifier(@PathParam("id") long id, 
                        @HeaderParam("date") long date, 
                        @HeaderParam("telephone") String téléphone) {     
      Personne personne = bd.find(Personne.class, id);
      personne.setNaissance(new Date(date));
      personne.setTéléphone(téléphone);
      bd.merge(personne);
   }  
   
   @PUT
   @Path("{id}/{téléphone}")
   public void modifier(@PathParam("id") long id, 
                        @QueryParam("annee") int année, 
                        @QueryParam("mois") int mois,
                        @QueryParam("jour") int jour,
                        @PathParam("téléphone") String téléphone) {
      Personne personne = bd.find(Personne.class, id);
      personne.setNaissance(new GregorianCalendar(année, mois-1, jour).getTime());
      personne.setTéléphone(téléphone);
      bd.merge(personne);
   }     

   @PUT
   @Path("{id}/telephone({téléphone})")
   public void modifier(@PathParam("id") long id, @PathParam("téléphone") String téléphone) {
      Personne personne = bd.find(Personne.class, id);
      personne.setTéléphone(téléphone);
      bd.merge(personne);
   }     

   @PUT
   @Path("{id}/jour={jour}-mois={mois}-annee={annee}")
   public void modifier(@PathParam("id") long id, 
                        @PathParam("annee") int année, 
                        @PathParam("mois") int mois,
                        @PathParam("jour") int jour) {
      Personne personne = bd.find(Personne.class, id);
      personne.setNaissance(new GregorianCalendar(année, mois-1, jour).getTime());
      bd.merge(personne);
   }        

   @PUT
   @Path("json")
   @Consumes("application/json")
   public void modifier(Personne personne) {
     bd.merge(personne);
   }
   
   @DELETE
   @Path("{id}")
   public void supprimer(@PathParam("id") long id) {
      Personne personne = bd.find(Personne.class, id);
      bd.remove(personne);
   }   

   @DELETE
   @Path("json")
   @Consumes("application/json")
   public void supprimer(Personne personne) {
     Personne recherche = bd.find(Personne.class, personne.getId());
     bd.remove(recherche);
   }   
}
Le bean session GestionPersonnel sert à la fois pour l'application web classique avec des méthodes qui seront utilisées en local par le bean représentant le modèle de la structure JSF, mais rend aussi service à l'extérieur directement, à la fois par les mêmes méthodes, mais également par des méthodes plus spécifiques pour le service web REST.
bean.Personnel.java
package bean;

import entité.Personne;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.application.FacesMessage;
import javax.faces.bean.*;
import javax.faces.context.FacesContext;
import service.GestionPersonnel;

@ManagedBean
@ViewScoped
public class Personnel {
   private Personne personne;
   @EJB
   private GestionPersonnel gestion;
   private List<Personne> tous;
   private int indice;
   
   @PostConstruct
   private void init() {
      tous = gestion.listePersonnels();
      if (tous.isEmpty()) personne = new Personne();
      else {
         indice = tous.size()-1;
         personne = tous.get(indice);
      }
   }

   public Personne getPersonne() { return personne;  }
   public void setPersonne(Personne personne) {   this.personne = personne;  }

   public List<Personne> getTous() { return tous;   }
   
   public boolean isNouveau() { return personne.getId() == 0; }
   
   public void nouveau()     { 
      personne = new Personne(); 
      message("Nouvel agent", "Saisissez l'ensemble des coordonnées");
   }

   public void enregistrer()   { 
      if (isNouveau()) gestion.nouveau(personne); 
      else gestion.modifier(personne); 
      init(); 
      message("Enregistrement", personne+" est bien enregistrée");      
   }

   public void supprimer()   { 
      message("Suppression", personne+" ne fait plus partie du personnel");
      gestion.supprimer(personne); 
      init(); 
   }
   
   public void précédent() {
      if (indice>0) indice--;
      personne = tous.get(indice);
   }
   
   public void suivant() {
      if (indice<tous.size()-1) indice++;
      personne = tous.get(indice);
   }
   
   private void message(String titre, String détail) {
      FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(titre, détail));
   }
}

Ce code correspond au bean managé Personnel qui utilise les compétences du bean session GestionPersonnel vu précédemment. Il sert de modèle dans la structure JSF et il est en étroite relation avec la vue représentée par la page web index.xhtml.

index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui"
      xmlns:f="http://java.sun.com/jsf/core">
   <h:head>
      <title>Gestion du personnel</title>
   </h:head>
   
   <h:body>
      <h:form>
         <p:growl autoUpdate="true" showDetail="true" />
         <p:panel header="Gestion du personnel">
            <p:accordionPanel activeIndex="-1">
               <p:tab title="Liste du personnel">
                  <p:outputPanel autoUpdate="true">
                     <p:dataTable var="personne" value="#{personnel.tous}" paginator="true" rows="7">
                        <p:column>
                           <f:facet name="header"><h:outputText value="Nom" /></f:facet>
                           <h:outputText value="#{personne.nom}" />
                        </p:column>
                        <p:column>
                           <f:facet name="header"><h:outputText value="Prénom" /></f:facet>
                           <h:outputText value="#{personne.prénom}" />
                        </p:column>
                        <p:column>
                           <f:facet name="header"><h:outputText value="Date de naissance" /></f:facet>
                           <p:calendar locale="fr" value="#{personne.naissance}" pattern="EEEE dd MMMM yyyy" disabled="true"/>
                        </p:column>
                        <p:column>
                           <f:facet name="header"><h:outputText value="Téléphones" /></f:facet>                          
                           <h:outputText var="téléphone" value="#{personne.téléphone}" />
                        </p:column>
                     </p:dataTable> 
                  </p:outputPanel>
               </p:tab>
               <p:tab title="Edition de chaque agent">
                  <p:outputPanel autoUpdate="true">
                     <p:panelGrid columns="2">
                        <h:outputText value="Nom :" />
                        <p:inputText value="#{personnel.personne.nom}" disabled="#{not personnel.nouveau}" />
                        <h:outputText value="Prénom :" />
                        <p:inputText value="#{personnel.personne.prénom}" disabled="#{not personnel.nouveau}"  />
                        <h:outputText value="Date de naissance :" />
                        <p:calendar locale="fr" value="#{personnel.personne.naissance}" beforeShowDay="true" pattern="dd MMMM yyyy" />
                        <h:outputText value="Téléphone :" />
                        <p:inputMask value="#{personnel.personne.téléphone}" mask="99-99-99-99-99" />                          
                        <f:facet name="footer">
                           <p:commandButton value="Nouveau" action="#{personnel.nouveau()}" icon="ui-icon-document"/>
                           <p:commandButton value="Enregistrer" action="#{personnel.enregistrer()}" icon="ui-icon-disk"/>
                           <p:commandButton value="Supprimer" action="#{personnel.supprimer()}" disabled="#{personnel.nouveau}" icon="ui-icon-trash" />
                           <p:commandButton value="Précedent" action="#{personnel.précédent()}" />
                           <p:commandButton value="Suivant" action="#{personnel.suivant()}" />
                        </f:facet>
                     </p:panelGrid>
                  </p:outputPanel>
               </p:tab>
            </p:accordionPanel>
         </p:panel>
      </h:form>
   </h:body>
</html>
Il s'agit ici de la vue présentée au navigateur lorsque nous lançons l'application web, la page web index.xhtml.

Les méthodes CRUD du service web REST
Récupération d'informations - Utilisation de la méthode GET
@Path("/")
@Stateless
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

   @GET
   @Produces("application/json")
   @Path("tous")
   public List<Personne> listePersonnels() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
   
   @GET
   @Produces("application/json")
   @Path("json/{id}")
   public Personne rechercheJSON(@PathParam("id") long id) {
      return bd.find(Personne.class, id);
   }
   
   @GET
   @Path("{id}")
   public Response rechercheDétaillée(@PathParam("id") long id) {
      Personne personne = bd.find(Personne.class, id);
      return Response.ok().header("nom", personne.getNom())
                                        .header("prenom", personne.getPrénom())
                                        .header("date", personne.getNaissance().getTime())
                                        .header("telephone", personne.getTéléphone()).build();
   }   
...
}
  • Ce service propose trois méthodes avec l'annotation @GET. Le première permet de restituer l'ensemble du personnel au format JSON. Voici ce que nous obtenons respectivement au travers d'un navigateur et du pluggin Poster. Dans ce dernier cas, les accents sont pris en compte. Remarquez bien au passage l'URI à soumettre pour que ce service soit opérationnel.

  • La deuxième méthode permet de retourner un personnel également au format JSON à partir de son identifiant :

  • Enfin, la dernière méthode retourne également un seul personnel, mais les informations retournées se situent dans l'en-tête de la réponse :

Enregistrement de nouvelles informations - Utilisation de la méthode POST
@Path("/")
@Stateless
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

   @POST
   public Response nouveau(@HeaderParam("nom") String nom, 
                           @HeaderParam("prenom") String prénom, 
                           @HeaderParam("date") long date, 
                           @HeaderParam("telephone") String téléphone) {
      Personne personne = new Personne(nom, prénom, new Date(date), téléphone);
      bd.persist(personne);
      return Response.ok().header("id", personne.getId()).build();
   }   
   
   @POST
   @Path("{nom}/{prénom}/{téléphone}")
   public Response nouveau(@PathParam("nom") String nom, 
                           @PathParam("prénom") String prénom, 
                           @QueryParam("annee") int année, 
                           @QueryParam("mois") int mois,
                           @QueryParam("jour") int jour,
                           @PathParam("téléphone") String téléphone) {
      Personne personne = new Personne(nom, prénom, new GregorianCalendar(année, mois-1, jour).getTime(), téléphone);
      bd.persist(personne);
      return Response.ok().header("id", personne.getId()).build();
   }   

   @POST
   @Path("json")
   @Consumes("application/json")
   public void nouveau(Personne personne) {
      bd.persist(personne);
   }
  ...
}
  • Ce service propose deux méthodes avec l'annotation @POST. Le première permet de créer un nouveau personnel à partir de l'en-tête de la requête :

  • La deuxième méthode permet également de créer un nouveau personnel directement au travers de l'URI. L'avantage d'utiliser des paramètres de type @QueryParam, c'est que lorsque nous élaborons notre URI, l'ordre des paramètres n'a pas d'importance du moment que nous les évoquons tous.

  • Enfin, la troisième méthode, qui sert également pour le bean géré, permet au travers du service web REST de générer un nouveau personnel directement à partir d'un document JSON. Comme l'entité possède l'annotation @XmlRootElement, le mapping entre le document JSON et la classe Personne se fait automatiquement.

Modification des informations déjà enregistrées - Utilisation de la méthode PUT
@Path("/")
@Stateless
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;
   
   @PUT
   @Path("enTête/{id}")
   public void modifier(@PathParam("id") long id, 
                        @HeaderParam("date") long date, 
                        @HeaderParam("telephone") String téléphone) {     
      Personne personne = bd.find(Personne.class, id);
      personne.setNaissance(new Date(date));
      personne.setTéléphone(téléphone);
      bd.merge(personne);
   }  
   
   @PUT
   @Path("{id}/{téléphone}")
   public void modifier(@PathParam("id") long id, 
                        @QueryParam("annee") int année, 
                        @QueryParam("mois") int mois,
                        @QueryParam("jour") int jour,
                        @PathParam("téléphone") String téléphone) {
      Personne personne = bd.find(Personne.class, id);
      personne.setNaissance(new GregorianCalendar(année, mois-1, jour).getTime());
      personne.setTéléphone(téléphone);
      bd.merge(personne);
   }     

   @PUT
   @Path("{id}/telephone({téléphone})")
   public void modifier(@PathParam("id") long id, @PathParam("téléphone") String téléphone) {
      Personne personne = bd.find(Personne.class, id);
      personne.setTéléphone(téléphone);
      bd.merge(personne);
   }     

   @PUT
   @Path("{id}/jour={jour}-mois={mois}-annee={annee}")
   public void modifier(@PathParam("id") long id, 
                        @PathParam("annee") int année, 
                        @PathParam("mois") int mois,
                        @PathParam("jour") int jour) {
      Personne personne = bd.find(Personne.class, id);
      personne.setNaissance(new GregorianCalendar(année, mois-1, jour).getTime());
      bd.merge(personne);
   }    

   @PUT
   @Path("json")
   @Consumes("application/json")
   public void modifier(Personne personne) {
     bd.merge(personne);
   }
...    
}
  • Ce service propose cette fois-ci quatre méthodes avec l'annotation @PUT. Le première permet de modifier le numéro de téléphone ainsi que la date de naissance en plaçant les nouvelles valeurs dans l'en-tête de la requête, en spécifiant l'identifiant du personnel à modifier directement dans l'URI.

  • La deuxième méthode modifie également le numéro de téléphone et la date de naissance du personnel identifié, mais cette fois-ci tout doit être spécifié directement dans l'URI et au travers de paramètres de requête :

  • La troisième méthode ne modifie que le numéro de téléphone du personnel identifié :

  • La quatrième méthode ne modifie quant à elle que la date de naissance :

  • La dernière version permet de modifier la totalité du personnel à l'aide d'un document JSON.
Suppression d'informations - Utilisation de la méthode DELETE
@Path("/")
@Stateless
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

...
   @DELETE
   @Path("{id}")
   public void supprimer(@PathParam("id") long id) {
      Personne personne = bd.find(Personne.class, id);
      bd.remove(personne);
   }   

   @DELETE
   @Path("json")
   @Consumes("application/json")
   public void supprimer(Personne personne) {
     Personne recherche = bd.find(Personne.class, personne.getId());
     bd.remove(recherche);
   }   
}
  • Il n'existe ici deux méthodes de suppression, reconnaissables par l'annotation @DELETE. Dans le premier cas, il suffit de préciser l'identifiant du personnel à supprimer :

  • Dans le deuxième cas, nous envoyons la totalité des informations du personnel au travers d'un document JSON.
Client du service web REST

Après avoir structurer notre service web REST, je vous propose de fabriquer un client sous Android qui gère l'ensemble du personnel, avec tous les modes d'édition possibles.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.rest"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Client REST Personnel" >
         <activity android:name="ListePersonnel" android:label="Liste du Personnel">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>       
        <activity android:name="ChoixServeur" android:label="Choix du serveur" android:theme="@android:style/Theme.Dialog" />        
        <activity android:name="Personnel" android:label="Edition du Personnel" android:theme="@android:style/Theme.Dialog"/>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
res/drawable/fond.xml
<?xml version="1.0" encoding="UTF-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
   <gradient 
      android:startColor="#FF0000"
      android:endColor="#FFFF00"
      android:type="radial"
      android:gradientRadius="300" />
</shape>
res/layout/adresse.xml
<?xml version="1.0" encoding="UTF-8"?>
   <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="2dp">
      <Button
         android:id="@+id/ok"
         android:layout_alignParentRight="true"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:text="OK" 
         android:onClick="ok"/>              
      <EditText
         android:layout_toLeftOf="@id/ok"
         android:id="@+id/adresse"
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content" 
         android:hint="Adresse IP" />                   
</RelativeLayout>
fr.btsiris.rest.ChoixServeur.java

package fr.btsiris.rest;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.*;
import android.widget.EditText;

public class ChoixServeur extends Activity {
   private EditText adresse;

   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  
      setContentView(R.layout.adresse);
      adresse = (EditText) findViewById(R.id.adresse);
   }
   
   public void ok(View vue) {
      Intent intent = new Intent();
      intent.putExtra("adresse", adresse.getText().toString());
      setResult(RESULT_OK, intent);
      finish();      
   }
}
res/layout/liste.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/fond"
    android:padding="3dp">  
   <Button
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Nouvelle personne"
      android:onClick="edition" />
   <ListView  
      android:id="@android:id/list"
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" />
</LinearLayout>
fr.btsiris.rest.Personne.java
package fr.btsiris.rest;

import java.io.Serializable;

public class Personne implements Serializable {
   private long id;
   private String nom;
   private String prenom;
   private long naissance;
   private String telephone;

   public long getId() { return id;  }
   public void setId(long id) {  this.id = id;  } 

   public long getNaissance() { return naissance;  }
   public void setNaissance(long naissance) {  this.naissance = naissance;   }

   public String getNom() { return nom;  }
   public void setNom(String nom) {  this.nom = nom; }

   public String getTelephone() { return telephone;  }
   public void setTelephone(String telephone) { this.telephone = telephone; }

   public String getPrenom() { return prenom;   }
   public void setPrenom(String prenom) { this.prenom = prenom; }

   @Override
   public String toString() { return nom + " " + prenom;  }   
} 
fr.btsiris.rest.ListePersonnel.java

package fr.btsiris.rest;

import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.IOException;
import java.util.*;
import org.apache.http.HttpResponse;
import org.apache.http.client.*;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.*;

public class ListePersonnel extends ListActivity {
    private ArrayList<Personne> personnes = new ArrayList<Personne>();
    private String adresse;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.liste);
       startActivityForResult(new Intent(this, ChoixServeur.class), 1);
    }
    
    protected void onActivityResult(int requestCode, int resultCode, Intent intention) {
       if (resultCode == RESULT_OK) {
          adresse = intention.getStringExtra("adresse");
          try {
             miseAJour();
          } 
          catch (Exception ex) {  Toast.makeText(this, "Réponse incorrecte", Toast.LENGTH_SHORT).show();  }
       }
    }

   @Override
   protected void onResume() {
      super.onResume();
      if (adresse!=null) try {
         miseAJour();
      } 
      catch (Exception ex) { }
   }

   @Override
   protected void onStop() {
      super.onStop();
      finish();
   }
   
    public void miseAJour() throws IOException, JSONException {
      HttpClient client = new DefaultHttpClient();
      HttpGet requete = new HttpGet("http://"+adresse+":8080/Personnel/rest/tous/");
      HttpResponse reponse = client.execute(requete);
      if (reponse.getStatusLine().getStatusCode() == 200) {
         Scanner lecture = new Scanner(reponse.getEntity().getContent());
         StringBuilder contenu = new StringBuilder();
         while (lecture.hasNextLine()) contenu.append(lecture.nextLine()+'\n');             
         JSONArray tableauJSON = new JSONArray(contenu.toString());
         StringBuilder message = new StringBuilder();
         personnes.clear();
         for (int i=0; i<tableauJSON.length(); i++) {
            JSONObject json = tableauJSON.getJSONObject(i);
            Personne personne = new Personne();
            personne.setId(json.getLong("id"));
            personne.setNom(json.getString("nom"));
            personne.setPrenom(json.getString("prénom"));
            personne.setNaissance(json.getLong("naissance"));
            personne.setTelephone(json.getString("téléphone"));
            personnes.add(personne);
         }
         setListAdapter(new ArrayAdapter<Personne>(this, android.R.layout.simple_list_item_1, personnes));
      }
      else Toast.makeText(this, "Problème de communication", Toast.LENGTH_SHORT).show();       
    }
    
    public void edition(View vue) {
       Intent intention = new Intent(this, Personnel.class);
       intention.putExtra("id", 0);
       intention.putExtra("adresse", adresse);
       startActivity(intention);
    }

    @Override
    protected void onListItemClick(ListView liste, View vue, int position, long id) {
       Personne personne = personnes.get(position);
       Intent intention = new Intent(this, Personnel.class);
       intention.putExtra("id", personne.getId());
       intention.putExtra("adresse", adresse);
       intention.putExtra("nom", personne.getNom());
       intention.putExtra("prenom", personne.getPrenom());
       intention.putExtra("naissance", personne.getNaissance());
       intention.putExtra("telephone", personne.getTelephone());
       startActivity(intention);      
    }      
}
res/layout/personnel.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/fond"
    android:padding="3px">
    <EditText  
      android:id="@+id/nom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Nom" />   
    <EditText  
      android:id="@+id/prenom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Prénom" />   
    <EditText 
      android:id="@+id/naissance"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Date naissance"
      android:onClick="changeDate"/>   
    <EditText  
      android:id="@+id/telephone"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="n° téléphone"/>         
      <LinearLayout 
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:gravity="center">
         <Button
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Nouveau"
            android:onClick="nouveau"/>    
         <Button
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Enregistrer" 
            android:onClick="enregistrer"/>            
         <Button
            android:id="@+id/supprimer"
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Supprimer" 
            android:onClick="supprimer"/>              
      </LinearLayout>
</LinearLayout>
fr.btsiris.rest.Personnel.java

package fr.btsiris.rest;

import android.app.*;
import android.content.Intent;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.*;
import android.widget.*;
import java.io.IOException;
import java.util.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.impl.client.DefaultHttpClient;

public class Personnel extends Activity {  
   private EditText nom, prenom, naissance, telephone;
   private Button supprimer;
   private long id;
   private String adresse;
   private Calendar calendrier = Calendar.getInstance();
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
      setContentView(R.layout.personnel);
      nom = (EditText) findViewById(R.id.nom);
      prenom = (EditText) findViewById(R.id.prenom);
      telephone = (EditText) findViewById(R.id.telephone);
      naissance = (EditText) findViewById(R.id.naissance);
      supprimer = (Button) findViewById(R.id.supprimer);
   }

   @Override
   protected void onStart() {
      super.onStart();
      Intent intention = getIntent();
      Bundle donnees = intention.getExtras();
      id = donnees.getLong("id");
      adresse = donnees.getString("adresse");
      if (id==0) toutEffacer();
      else {
         nom.setEnabled(false);
         prenom.setEnabled(false);      
         supprimer.setEnabled(true);
         nom.setText(donnees.getString("nom"));
         prenom.setText(donnees.getString("prenom"));
         long date = donnees.getLong("naissance");
         calendrier.setTimeInMillis(date);
         naissance.setText(DateFormat.format("EEEE dd MMMM yyyy", date)); 
         telephone.setText(donnees.getString("telephone"));               
      }      
   }
   
   private void toutEffacer() {
      id = 0;
      nom.setEnabled(true);
      prenom.setEnabled(true);
      supprimer.setEnabled(false);  
      nom.setText("");
      prenom.setText("");
      naissance.setText("");
      telephone.setText("");        
      calendrier = Calendar.getInstance();
   }
   
   private DatePickerDialog.OnDateSetListener evt = new DatePickerDialog.OnDateSetListener() {
      public void onDateSet(DatePicker dialog, int annee, int mois, int jour) {
         calendrier.set(annee, mois, jour);
         naissance.setText(DateFormat.format("EEEE dd MMMM yyyy", calendrier));
      }
   };
   
   public void changeDate(View vue) {
       new DatePickerDialog(this, evt, 
               calendrier.get(Calendar.YEAR), 
               calendrier.get(Calendar.MONTH),
               calendrier.get(Calendar.DAY_OF_MONTH)).show();      
   }
   
   public void nouveau(View vue) {
      toutEffacer();
   }
   
   public void enregistrer(View vue) throws IOException {
      if (id==0) nouveauPersonnel();
      else modifierPersonne();
   }
   
   public void supprimer(View vue) throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpDelete requete = new HttpDelete("http://"+adresse+":8080/Personnel/rest/"+id);
      client.execute(requete);
      Toast.makeText(this, "Personnel "+id+" supprimé", Toast.LENGTH_SHORT).show();   
      finish();
   }

   private void nouveauPersonnel() throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpPost requete = new HttpPost("http://"+adresse+":8080/Personnel/rest/");  
      requete.addHeader("nom", nom.getText().toString());
      requete.addHeader("prenom", prenom.getText().toString());
      requete.addHeader("date", ""+calendrier.getTimeInMillis());
      requete.addHeader("telephone", telephone.getText().toString());
      client.execute(requete);      
      Toast.makeText(this, "Nouveau personnel enregistré", Toast.LENGTH_SHORT).show();    
      finish();
   }

   private void modifierPersonne() throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpPut requete = new HttpPut("http://"+adresse+":8080/Personnel/rest/enTête/"+id);
      requete.setHeader("date", ""+calendrier.getTimeInMillis());
      requete.setHeader("telephone", telephone.getText().toString());
      client.execute(requete);      
      Toast.makeText(this, "Personnel modifié", Toast.LENGTH_SHORT).show();         
   }
}

Mise en oeuvre d'un projet d'archivage de photos

Le deuxième projet sera plus simple puisque, au travers d'une simple petite application web, nous allons archiver et visualiser un ensemble de photos. Là aussi, l'application web utilise les compétences de composants de haut niveau proposés par PrimeFaces. Il sera donc de nouveau nécessaire de déployer, en même temps que le projet principal, les archives primefaces-3.2.jar pour l'ensemble des composants JSF ainsi que sunny.jar pour le thème.

Développement du service de l'application

Je vous donne l'ensembles des codes nécessaires à cette application web suivi des différents tests pour évaluer le bon fonctionnement du service web REST.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <context-param>
        <param-name>primefaces.THEME</param-name>
        <param-value>sunny</param-value>
    </context-param>
    
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>ServletAdaptor</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>ServletAdaptor</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>
Nous remarquons ici la présence de deux servlets, la première relative à JSF qui sert de contrôleur pour les requêtes classiques de l'application web, la deuxième s'occupant plus particulièrement des fonctionnalités du service web REST.
service.Archivage.java
package service;

import java.io.*;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;
import javax.ws.rs.*;

@Path("/")
@ManagedBean
public class Archivage {
   private final String répertoire = "ArchivagePhotos/";
   
   @PostConstruct
   private void init() {
      File rep = new File(répertoire);
      if (!rep.exists()) rep.mkdir();
   }
   
   @GET
   @Path("liste")
   @Produces("application/json")
   public String[] getPhotos() { return new File(répertoire).list();  }
   
   @GET
   @Path("{nomFichier}")
   @Produces("image/jpeg")
   public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
      return new FileInputStream(répertoire+nom);
   }  
   
   @POST
   @Path("{nomFichier}")
   @Consumes("image/jpeg")
   public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {     
      byte[] octets = lireOctets(flux);
      FileOutputStream fichier = new FileOutputStream(répertoire+nom);
      fichier.write(octets);
      fichier.close();
   }
   
   @DELETE
   @Path("{nomFichier}")
   public void supprimer(@PathParam("nomFichier") String nom) {
      new File(répertoire+nom).delete();
   }   
  
   private byte[] lireOctets(InputStream stream) throws IOException {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024]; int octetsLus = 0;
      do {
         octetsLus = stream.read(buffer);
         if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
      } 
      while (octetsLus > -1);
      return baos.toByteArray();
   }   
}
  • La grande particularité ici, c'est que notre service web REST est également un bean managé qui va être également utile pour la vue de la structure JSF.
  • Par contre, il ne s'agit pas d'un bean session puisque la persistance, au travers d'entité, n'est pas gérée.
  • Un répertoire de stockage est prévu directement dans le serveur d'application utilisé. Cela se produit la première fois que le service entre en action au moyen de la méthode de rappel init(), grâce à l'annotation @PostConstruct.
  • Nous retrouvons ensuite tous les méthodes classiques du protocole HTTP au moyen des annotation spécifiques @GET, @POST et @DELETE. Seule la méthode @PUT n'est pas utilisée ici puisqu'elle ne correspond à aucune fonctionnalité adaptée à ce genre de problème.
  • Une autre grande particularité, que nous découvrirons tout à l'heure, c'est que la méthode restituer() va également être utilisée par la vue afin de proposer le flux de chaque image à la galerie présentée.
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui"
      xmlns:f="http://java.sun.com/jsf/core">
   
   <h:head>
      <title>Archivage de photos</title>
   </h:head>
   
   <h:body>
      <h:form>
         <p:panel header="Archivage de photos">
            <p:galleria value="#{archivage.photos}" var="photo">  
               <p:graphicImage value="rest/#{photo}" />   
            </p:galleria>  
          </p:panel>        
      </h:form>
   </h:body>
</html>
La vue de index.xhtml est extrêmement simple. La particularité, comme je viens de l'évoquer, est d'utiliser le bean gérer comme service web REST en ce qui concerne la visualisation de chaque photo, au travers de la méthode restituer() qui renvoie le flux de la photo concernée.

Enregistrement d'une nouvelle photo - Utilisation de la méthode POST
@Path("/")
@Produces("image/jpeg")
@Consumes("image/jpeg")
@ManagedBean
public class Archivage {
   private final String répertoire = "ArchivagePhotos/";
...   

   @POST
   @Path("{nomFichier}")
   public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {     
      byte[] octets = lireOctets(flux);
      FileOutputStream fichier = new FileOutputStream(répertoire+nom);
      fichier.write(octets);
      fichier.close();
   }

   private byte[] lireOctets(InputStream stream) throws IOException {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024]; int octetsLus = 0;
      do {
         octetsLus = stream.read(buffer);
         if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
      } 
      while (octetsLus > -1);
      return baos.toByteArray();
   }   
}
Voici la procédure à suivre pour intégrer une nouvelle photo dans votre service. Il suffit de préciser le nom de fichier dans l'URI et sélectionner le fichier image qui est le contenu de la requête :

Client du service web REST

Après avoir structurer notre service web REST, je vous propose de fabriquer un client sous Android qui permet d'envoyer des photos prises avec l'APN intégré.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.photos"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Stockage photos" >
        <activity android:name="ArchivagePhotos" android:label="Stocker vos photos" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="ChoixServeur" android:label="Choix du serveur" android:theme="@android:style/Theme.Dialog" /> 
    </application>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
res/layout/adresse.xml
<?xml version="1.0" encoding="UTF-8"?>
   <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="2dp">
      <Button
         android:id="@+id/ok"
         android:layout_alignParentRight="true"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:text="OK" 
         android:onClick="ok"/>              
      <EditText
         android:layout_toLeftOf="@id/ok"
         android:id="@+id/adresse"
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content" 
         android:hint="Adresse IP" />                   
</RelativeLayout>
fr.btsiris.rest.ChoixServeur.java
package fr.btsiris.rest;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.*;
import android.widget.EditText;

public class ChoixServeur extends Activity {
   private EditText adresse;

   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  
      setContentView(R.layout.adresse);
      adresse = (EditText) findViewById(R.id.adresse);
   }
   
   public void ok(View vue) {
      Intent intent = new Intent();
      intent.putExtra("adresse", adresse.getText().toString());
      setResult(RESULT_OK, intent);
      finish();      
   }
}
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:background="#008800"
   android:padding="3dp">
    <EditText
       android:id="@+id/description"
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:hint="Description de la photo"
       android:layout_alignParentBottom="true"/> 
    <LinearLayout 
       android:id="@+id/boutons"
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:layout_above="@id/description">   
       <Button 
            android:layout_width="0dp" 
            android:layout_height="wrap_content" 
            android:layout_weight="1"
            android:text="Photographier" 
            android:onClick="photographier" /> 
       <Button 
            android:layout_width="0dp" 
            android:layout_weight="1"
            android:layout_height="wrap_content" 
            android:text="Envoyer" 
            android:onClick="envoyer" />             
    </LinearLayout>
    <ImageView
       android:id="@+id/image"
       android:layout_width="fill_parent" 
       android:layout_height="fill_parent"
       android:padding="10dp"
       android:layout_above="@id/boutons" />
</RelativeLayout>
fr.btsiris.rest.ArchivagePhotos.java
package fr.btsiris.photos;

import android.app.Activity;
import android.content.Intent;
import android.graphics.*;
import android.net.Uri;
import android.os.*;
import android.provider.MediaStore;
import android.view.View;
import android.widget.*;
import java.io.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.DefaultHttpClient;

public class ArchivagePhotos extends Activity {
   private EditText description;
   private ImageView image;
   private Uri fichierUri;
   private String adresse;
   private final int RECHERCHE_ADRESSE = 1;
   private final int PHOTOGRAPHIER = 2;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        description = (EditText) findViewById(R.id.description);
        image = (ImageView) findViewById(R.id.image);
        startActivityForResult(new Intent(this, ChoixServeur.class), RECHERCHE_ADRESSE);
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent intention) {
      switch (requestCode) {
         case RECHERCHE_ADRESSE :
            adresse = intention.getStringExtra("adresse");
            break;         
         case PHOTOGRAPHIER :
            File fichier = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
            Bitmap photo = BitmapFactory.decodeFile(fichier.getPath());
            image.setImageBitmap(photo);
            break;
      }
   }

   public void photographier(View vue) {
      Intent intention = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      File fichier = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
      fichierUri = Uri.fromFile(fichier);
      intention.putExtra(MediaStore.EXTRA_OUTPUT, fichierUri);
      startActivityForResult(intention, PHOTOGRAPHIER);
   }   
   
   public void envoyer(View vue) throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpPost requete = new HttpPost("http://"+adresse+":8080/Photos/rest/"+description.getText().toString()+".jpg");  
      requete.setEntity(new FileEntity(new File(Environment.getExternalStorageDirectory(), "photo.jpg"), "image/jpeg"));
      client.execute(requete);      
   }
}

Client Qt - Conversion monétaire

Lors de cette étude, nous allons voir comment communiquer avec des web services codés en Java. Il s 'agit ici des services web  de type REST, c'est-à-dire des services qui permettent de gérer des ressources à distance. Je rappelle que les services Web sont conçus comme d'autres services, avec toutefois la particularité de transmettre les requêtes et les réponses tout simplement à l'aide du protocole HTTP. Du coup, notre pare-feu n'a pas besoin d'une configuration particulière et nous protège en nous laissant passer le port 80.

Rappels préliminaires

Nous allons élaborer deux projets différents qui vont nous permettre de découvrir graduellement l'utilisation des méthodes offertes par le protocole HTTP, savoir les méthodes GET, POST, PUT et DELETE.

  1. GET : cette méthode permet de récupérer une ressource depuis le serveur pour l'avoir sur le poste local.
  2. POST : cette méthode est l'inverse de la précédente, cette fois-ci nous envoyons une ressource depuis le poste local. Elle permet ainsi de sauvegarder à distance une ressource que nous possédons sur notre ordinateur.
  3. PUT : cette méthode permet de modifier une ressource distante.
  4. DELETE : cette méthode permet, comme sont non l'indique de supprimer une ressource du serveur.
Premier projet : Conversion entre les €uros et les Francs

Le premier projet consiste à communiquer avec un Web Service qui permet de faire la conversion entre les €uros et les francs. Dans cet exemple, la communication est très simple puisque nous utilisons uniquement la méthode HTTP GET, qui renvoie les valeurs sous forme de chaînes de caractères.

formulaire

Creation du Service Web Rest

Le web service est réalisé en Java. Je vous donne juste le code nécessaire pour comprendre comment communiquer avec lui, sans explication particulière, parce que ce n'est pas le sujet ici. Toutefois, il est intéressant de connaître exactement quelles sont les formes des deux requêtes que nous pouvons proposer afin d'obtenir les résultats requis. Voici les deux types d'exemple ci-dessous :

  1. http://localhost:8080/franc?euro=15.24 : retourne une valeur en Franc à partir d'une valeur donnée en €uro.
  2. http://localhost:8080/euro?franc=100 : retourne une valeur en €uro à partir d'une valeur donnée en Franc.

Classes utilisées pour communiquer avec un Web service REST

La communication avec un  Service Web REST est extrêmement facile à réaliser avec une application cliente développée avec la librairie QT. En effet, QT propose uniquement trois classes pour résoudre toutes les situations possibles :

  1. QNetworkAccessManager : C'est au travers de cette classe que nous établissons la communication avec le service Web distant. Je rappelle que dans la philosophie de QT, la communication réseau se fait au travers d'une gestion événementielle. Ainsi, à chaque fois qu'une requête sera proposée, un événement sera automatiquement lancé dès que la réponse sera prête. Cette classe est également spécialisée dans la communication au travers du protocole HTTP. Ainsi, elle possède des méthodes toutes prêtes pour traduire tous les souhaits de l'utilisateur, grâce notamment aux méthodes : get(), post(), put() et deleteResource() qui font appel aux méthodes respectives du protocole HTTP : GET, POST, PUT et DELETE.
  2. QNetworkRequest : Comme sont nom l'indique, cette classe nous permet d'élaborer les différentes requêtes requises en proposant à chaque fois la bonne URL en adéquation avec ce que souhaite le web service REST. L'objet ainsi créé servira d'argument à l'une des méthodes précédentes - get(), post(), put() et deleteResource() - de la classe QNetworkAccessManager.
  3. QNetworkReply : Cette classe représente la réponse à la requête sollicité par QNetworkRequest. En réalité, l'objet de cette classe est un paramètre d'une méthode SLOT qui sera automatiquement appelée lorsque effectivement une réponse sera reçue du service Web REST gestion événementielle. Bien entendu, cette classe dispose de méthodes adaptées à la gestion du résultat, avec notamment : la méthode error() qui nous prévient si la réponse a été correctement envoyée, la méthode readAll() qui nous retourne la totalité du contenu espéré, la méthode readLine() qui récupère le texte reçu ligne par ligne, canReadLine() qui teste si il existe encore une ligne de texte à lire, etc.
Fichier de projet – Conversionrest.pro
#-------------------------------------------------
#
# Project created by QtCreator 2014-01-12T10:30:24
#
#-------------------------------------------------
QT       += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG   += c++11
TARGET    = ConversionREST
TEMPLATE  = app
SOURCES  += main.cpp principal.cpp  rest.cpp
HEADERS  += principal.h   rest.h
FORMS    += principal.ui
Dans ce fichier de projet, n'oubliez pas d'intégrer la librairie propre à la programmation réseau. Par ailleurs, si vous devez écrire une syntaxe récente, pensez également à configurer QT pour prendre en compte la dernière version du langage, savoir c++11.
Rest.h
#ifndef REST_H
#define REST_H

#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QnetworkReply>

class Rest : public QObject
{
    Q_OBJECT
    QNetworkAccessManager reseau;
public:
    Rest();
    void requete(const QString &adresse, const QString &demande);
signals:
    void alerte(const QString&);
    void reception(const QString&);
private slots:
    void reponse(QNetworkReply *resultat);
};

#endif // REST_H
Nous avons mis en œuvre une classe spécialisée pour toute la gestion du réseau et de la communication avec le Service Web REST. Nous retrouvons :
  • reseau : un objet de la classe QNetworkAccessManager qui permet d'établir la communication avec le service Web.
  • requete() : une méthode qui permet de formaliser la requête souhaitée.
  • reponse() : une méthode qui récupère la réponse à la requête.
  • reception() : signal envoyé avec le contenu de la réponse.
  • alerte() : signal envoyé au cas où un problème de communication apparaît.
Rest .cpp
#include "rest.h"

Rest::Rest()  { connect(&reseau, SIGNAL(finished(QNetworkReply *)), this, SLOT(reponse(QNetworkReply *))); }

void Rest::requete(const QString &adresse, const QString &demande)
{
    QString url = "HTTP://";
    url+=adresse;
    url+=":8080/Conversion/";
    url+=demande;
    reseau.get(QNetworkRequest(QUrl(url)));
}

void Rest::reponse(QNetworkReply *resultat)
{
    if (resultat->error() == QNetworkReply::NoError)
    {
        QByteArray donnees = resultat->readAll();
        reception(donnees.data());
    }
    else alerte("Problème de communication !");
    delete resultat;
}
  • Le constructeur met en œuvre la gestion événementielle qui permet de lancer automatiquement la méthode reponse() dès que le résultat a été obtenu à l'issue de chaque requête.
  • requete() : la méthode formalise correctement la bonne URL afin de se connecter au bon service Web d'une part et de faire la demande souhaitée par l'application cliente. Nous passons par la classe QUrl pour que la chaîne de caractère soit bien formater. Nous passons ensuite par la classe QNetworkRequest pour envoyer notre requête avec l'URL formatée et nous utilisons l'objet reseau de type QNetworkAccessManager pour finaliser le type de requête HTTP souhaitée, ici la méthode GET.
  • reponse() : la méthode est appelée automatiquement après chaque envoi d'une requête. Elle prend en paramètre un objet de type QNetworkReply qui représente le résultat reçu. Il convient de vérifier systématiquement si l'opération s'est bien déroulée en appelant la méthode error(). Si effectivement la communication a pue être établie, nous pouvons récupérer la totalité de la donnée envoyée par le web service à l'aide de la méthode readAll(). ATTENTION ! Une fois que vous avez bien récupéré votre valeur, vous devez également systématiquement la supprimer du buffer de réception à l'aide de l'opération delete.
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include <QMainWindow>
#include "ui_principal.h"
#include "rest.h"

class Principal : public QMainWindow, public Ui::Principal
{
    Q_OBJECT
Public:
    explicit Principal(QWidget *parent = 0);
private:
    Rest rest;
    enum {AUCUN, EURO, FRANC} commande = AUCUN;
private slots:
    void euroFranc();
    void francEuro();
    void resultat(QString monnaie);
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"
#include <QMessageBox>

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
    setupUi(this);
    connect(&rest, SIGNAL(alerte(QString)), barreEtat, SLOT(showMessage(QString)));
    connect(&rest, SIGNAL(reception(QString)), this, SLOT(resultat(QString)));
}

void Principal::euroFranc()
{
    commande = FRANC;
    rest.requete(adresseIP->text(), QString("franc?euro=%1").arg(euro->value()));    
}

void Principal::francEuro()
{
    commande = EURO;
    rest.requete(adresseIP->text(), QString("euro?franc=%1").arg(franc->value()));
}

void Principal::resultat(QString monnaie)
{
    switch (commande) {
       case FRANC : franc->setValue(monnaie.toDouble()); break;
       case EURO  : euro->setValue(monnaie.toDouble()); break;
       case AUCUN : barreEtat->showMessage("Choisissez votre monnaie"); break;
    }
    commande = AUCUN;
}

Client Qt - Archivage de photos à distance

Dans ce deuxième projet relatif à Qt, nous allons mettre en œuvre un système qui permet d'archiver un ensemble de photos à distance. À tout moment, il doit être possible de stocker une photo depuis le disque du poste local vers le serveur, ensuite à l'inverse de la récupérer, de modifier à distance le nom de la photo et pour finir de pouvoir la supprimer définitivement du serveur. Grâce à ce projet, nous voyons bien que nous allons utiliser l'ensemble des méthodes usuelles du protocole HTTP, je le rappelle, les méthodes GET, PUT, PUT et DELETE.

Requêtes possibles pour le service web d'archivage de photos

Nous allons le découvrir bientôt, le Service Web REST d'archivage de photos propose cinq fonctionnalités associées, bien entendu, à cinq méthodes de la classe Archivage qui représente ce service Web. Vous avez ci-dessous les URLs à proposer afin d'obtenir les requêtes souhaitées :

Mise en œuvre du service web REST d'archivage de photos

Vous avez ci-dessous le code Java correspondant au service même d'archivage dans la technologie REST. Malgré le fait que nous manipulions des données relativement conséquentes, le code lui-même demeure relativement simple au vue des fonctionnalités proposées.

service.Archivage.class
package service;

import java.io.*;
import java.util.*;
import javax.ws.rs.*;

@Path("/")
public class Archivage {
   private final String répertoire = "/home/manu/Applications/Archivage/";
   
   @GET
   @Produces("text/plain")
   public String listePhotos() { 
      String[] liste = new File(répertoire).list();  
      StringBuilder noms = new StringBuilder();
      for (String nom : liste) noms.append(nom.split(".jpg")[0]+'\n');
      return noms.toString();
   }
   
   @GET
   @Path("{nomFichier}")
   @Produces("image/jpeg")
   public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
      return new FileInputStream(répertoire+nom+".jpg");
   }  
   
   @POST
   @Path("{nomFichier}")
   @Consumes("image/jpeg")
   public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {     
      byte[] octets = lireOctets(flux);
      FileOutputStream fichier = new FileOutputStream(répertoire+nom+".jpg");
      fichier.write(octets);
      fichier.close();
   }
   
   @PUT
   @Path("change")
   public void  changerNom(@QueryParam("ancien") String ancien, @QueryParam("nouveau") String nouveau) {
      new File(répertoire+ancien+".jpg").renameTo(new File(répertoire+nouveau+".jpg"));
   }
   
   @DELETE
   @Path("{nomFichier}")
   public void supprimer(@PathParam("nomFichier") String nom) {
      new File(répertoire+nom+".jpg").delete();
   }   
  
   private byte[] lireOctets(InputStream stream) throws IOException {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024]; int octetsLus = 0;
      do {
         octetsLus = stream.read(buffer);
         if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
      } 
      while (octetsLus > -1);
      return baos.toByteArray();
   }   
}
Mise en œuvre de la partie cliente au web service REST d'archivage
Fichier de projet
#-------------------------------------------------
#
# Project created by QtCreator 2014-01-15T21:34:58
#
#-------------------------------------------------
QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG   += c++11
TARGET   = ClientPhotoREST
TEMPLATE = app
SOURCES  += main.cpp  client.cpp   image.cpp
HEADERS  += client.h  image.h
FORMS    += client.ui
Par rapport au premier projet, ce fichier de configuration de projet est relativement similaire, si ce n'est les noms des fichiers sources et des fichiers en-têtes utilisés.
Constitution de l'IHM et de la gestion événementielle

Image.h
#ifndef IMAGE_H
#define IMAGE_H

#include <QWidget>

class Image : public QWidget
{
    Q_OBJECT
public:
    explicit Image(QWidget *parent = 0) : QWidget(parent) {}
    void chargerPhoto(const QByteArray &octets);
    QByteArray getOctets() { return octets; }
signals:
    void envoyerMessage(const QString &message);
    void envoyerNom(const QString &nom);
public slots:
    void chargerPhoto();    
    void sauverPhoto();
protected:
    void paintEvent(QPaintEvent *) override;
private:
    QImage photo;
    QByteArray octets;
};
Image.cpp
#include "image.h"

#include <QFileDialog>
#include <QPainter>
#include <QRect>
#include <Qfile>

void Image::chargerPhoto()
{
    QString nom = QFileDialog::getOpenFileName(this, "Choisissez votre photo", "", "Images (*.jpeg *.jpg)");
    if (!nom.isEmpty())
    {     
        envoyerMessage(QString("Local : %1").arg(nom));
        QFile fichier(nom);
        fichier.open(QIODevice::ReadOnly);
        octets = fichier.readAll();
        photo.loadFromData(octets);
        update();
        QFileInfo infoFichier(nom);
        envoyerNom(infoFichier.baseName());
    }
}

void Image::chargerPhoto(const QByteArray &octets)
{
    photo.loadFromData(this->octets = octets);
    update();
}

void Image::sauverPhoto()
{
    if (!photo.isNull())
    {
        QString nom = QFileDialog::getSaveFileName(this, "Sauvegardez la photo");
        if (!nom.isEmpty())
        {
            QFile fichier(nom);
            fichier.open(QIODevice::WriteOnly);
            fichier.write(octets);
            envoyerMessage("La photo est sauvegardée sur le disque local");
        }
    }
}

void Image::paintEvent(QPaintEvent *)
{
    if (!photo.isNull())
    {
        QPainter dessin(this);
        double ratio = (double) photo.width() / photo.height();
        int largeur = width();
        int hauteur = width() / ratio;
        QRect cadrage(0, 0, largeur, hauteur);
//        QImage image = photo.scaledToWidth(width());
        dessin.drawImage(cadrage, photo, photo.rect());
    }
    else envoyerMessage("Choisissez votre photo");
}
Client.h
#ifndef CLIENT_H
#define CLIENT_H

#include <QMainWindow>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include "ui_client.h"

class Client : public QMainWindow, public Ui::Client
{
    Q_OBJECT
public:
    explicit Client(QWidget *parent = 0);
private:
    QNetworkAccessManager reseau;
    QString adresse;
    enum {Aucune, ListePhotos, Restituer, Stocker, ChangerNom, Supprimer} requete = Aucune;
private slots:
    void reponse(QNetworkReply *resultat);
    void changerAdresse(const QString &adresse);
    void listePhotos();
    void changerPhoto(const QString &nom);
    void stocker();
    void changerNom();
    void supprimer();
signals:
    void info(const QString &info);
private:   
    void restituer();    
};

#endif // CLIENT_H
Client.cpp
#include "client.h"

Client::Client(QWidget *parent) : QMainWindow(parent)
{
    setupUi(this);
    connect(&reseau, SIGNAL(finished(QNetworkReply*)), this, SLOT(reponse(QNetworkReply*)));
    adresse = "http://";
    adresse += adresseIP->text();
    adresse += ":8080/Archivage/";
}

void Client::reponse(QNetworkReply *resultat)
{
    if (resultat->error() == QNetworkReply::NoError)
    {
        switch (requete) {
        case ListePhotos :
            photos->clear();
            while(resultat->canReadLine()) {
                QString photo = resultat->readLine();
                photo.remove("\n");
                photos->addItem(photo);
            }
            nomPhotoServeur->setText(photos->currentText());
            break;
        case Restituer :
            photoServeur->chargerPhoto(resultat->readAll());
            break;
        case Stocker :
            info("Photo envoyée sur le serveur");
            listePhotos();
            break;
        case ChangerNom :
            info("Le nom de la photo est changée sur le serveur");
            listePhotos();
            break;
        case Supprimer :
            info("La photo a été supprimée sur le serveur");
            listePhotos();
            break;
        case Aucune :
            info("Aucune requête demandée");
            break;
        }
    }
    else info("Problème avec le service d'archivage !");
    delete resultat;
}

void Client::listePhotos()
{
    requete = ListePhotos;
    reseau.get(QNetworkRequest(QUrl(adresse)));
}

void Client::changerPhoto(const QString &nom)
{
    nomPhotoServeur->setText(nom);
    onglets->setCurrentIndex(1);
    restituer();
}

void Client::restituer()
{
    requete = Restituer;
    QString nomImage = QString("%1%2").arg(adresse).arg(nomPhotoServeur->text());
    info(nomImage);
    reseau.get(QNetworkRequest(QUrl(nomImage)));
}

void Client::stocker()
{
    QString nom = nomPhotoClient->text();
    if (!nom.isEmpty())
    {
        requete = Stocker;
        QString url = adresse;
        url+=nom;
        QNetworkRequest envoi;
        envoi.setUrl(QUrl(url));
        envoi.setRawHeader("Content-Type", "image/jpeg");
        reseau.post(envoi, photoLocal->getOctets());
    }
}

void Client::changerNom()
{
    QString nom = nomPhotoServeur->text();
    if (!nom.isEmpty())
    {
        requete = ChangerNom;
        QString url = QString("%1change?ancien=%2&nouveau=%3").arg(adresse).arg(photos->currentText()).arg(nom);
        QByteArray vide;
        reseau.put(QNetworkRequest(QUrl(url)), vide);
    }
}

void Client::supprimer()
{
    QString nom = nomPhotoServeur->text();
    if (!nom.isEmpty())
    {
        requete = Supprimer;
        QString url = adresse;
        url+=nom;
        reseau.deleteResource(QNetworkRequest(QUrl(url)));
    }
}

void Client::changerAdresse(const QString &adresse)
{
    this->adresse = "http://";
    this->adresse += adresse;
    this->adresse += ":8080/Archivage/";
    info(this->adresse);
}
  • Vous remarquez que cette fois-ci nous avons proposé l'ensemble des requête HTTP en utilisant les méthodes get(), post(), put() et deleteResource() de la classe QNetworkAccessManager.
  • Par ailleurs, lorsque nous devons spécifier le type mime lors de l'envoi d'un contenu supplémentaire, comme c'est le cas avec les méthodes post() et put(), nous devons donc au préalable utiliser la méthode setRawHeader() de la classe QNetworkRequest.

Projet complet d'archivage de photos

Pour conclure sur les web services de type REST, je vous propose de reprendre l'idée d'archivage de photos en intégrant un certain nombre de fonctionnalités, comme la géolocalisation, l'édition des photos, l'affichage sous forme de gallerie d'images, etc. Comme vous pouvez le constater dans les figures ci-dessous, l'application cliente est une application Web. Dans ce projet ne sera pas intégré un client de type Android.

Mise en place de l'application Web

Je vous donne l'ensembles des codes nécessaires à ce service. Pour que le projet fonctionne correstement, vous avez un certain nombre d'archives à intégrer :

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <context-param>
        <param-name>primefaces.THEME</param-name>
        <param-value>afterdark</param-value>
    </context-param>
     
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet>
        <servlet-name>ServletAdaptor</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
       
    <filter>
       <filter-name>PrimeFaces FileUpload Filter</filter-name>
       <filter-class>org.primefaces.webapp.filter.FileUploadFilter</filter-class>
    </filter>
    
    <filter-mapping>
       <filter-name>PrimeFaces FileUpload Filter</filter-name>
       <servlet-name>Faces Servlet</servlet-name>
    </filter-mapping>

    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    
    <servlet-mapping>
        <servlet-name>ServletAdaptor</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui"
      xmlns:f="http://java.sun.com/jsf/core">
   
   <h:head>
      <title>Archivage de photos</title>
   </h:head>
   
   <script src="http://maps.google.com/maps/api/js?sensor=true" type="text/javascript" />
   
   <h:body style="background-color: darkseagreen">
      <h:form>  
         <p:growl autoUpdate="true" showDetail="true" />
         <p:panel header="Archivage de photos">  
            <p:outputPanel autoUpdate="true">
               <p:selectOneMenu value="#{archivage.titre}">
                  <f:selectItems value="#{archivage.listeDesLots}" />
                  <p:ajax listener="#{archivage.changeLot()}"/>
               </p:selectOneMenu>      
            </p:outputPanel>
            <p:tabView>
               <p:tab title="Gallerie des photos">
                  <p:outputPanel autoUpdate="true">
                     <p:galleria value="#{archivage.lot.photos}" var="photo" effect="slide" filmstripPosition="right" showOverlays="true" frameWidth="90" frameHeight="60" >  
                        <p:graphicImage value="rest/#{photo.id}" width="600" height="400" styleClass="ui-corner-all" />
                        <p:galleriaOverlay title="#{photo}">  
                           <h:outputText value="#{photo.jour}" rendered="#{photo.exif}">
                              <f:convertDateTime dateStyle="full" />
                           </h:outputText> 
                           <br />
                           <h:outputText value="(Lat. = #{photo.latitude}, Lon.= #{photo.longitude})" rendered="#{photo.exif}" /> 
                           <h:outputText value="Aucunes données EXIF" rendered="#{not photo.exif}" />
                        </p:galleriaOverlay>                             
                     </p:galleria>                 
                  </p:outputPanel>                  
               </p:tab>
               <p:tab title="Google Map">
                  <p:outputPanel autoUpdate="#{archivage.miseAJour}">
                     <p:gmap zoom="15" type="HYBRID" center="#{archivage.centre}" style="width:600px;height:400px" model="#{archivage.cartographie}">
                        <p:ajax event="overlaySelect" listener="#{archivage.choixMarqueur}" />  
                        <p:gmapInfoWindow>  
                           <p:graphicImage value="rest/#{archivage.marqueur.data}" width="200" /><br />
                           <h:outputText value="#{archivage.marqueur.title}" />                                  
                        </p:gmapInfoWindow>  
                     </p:gmap> 
                  </p:outputPanel>
               </p:tab>        
               <p:tab title="Edition des photos">
                  <p:outputPanel autoUpdate="true">
                     Titre : <p:inputText value="#{archivage.lot.titre}" />
                     <p:commandButton icon="ui-icon-disk" value="Changer le titre" action="#{archivage.changeTitre()}" />
                     <p:commandButton icon="ui-icon-trash" value="Supprimer totalement ce lot" action="#{archivage.supprimeLot()}" />
                     <p:dataTable value="#{archivage.lot.photos}" var="photo" paginator="true" rows="4">
                        <p:column>
                           <f:facet name="header">Photo</f:facet>
                           <a target="_blank" href="rest/#{photo.id}">
                              <p:graphicImage value="rest/#{photo.id}" width="200" styleClass="ui-corner-all"  id="image"/>
                           </a>
                           <p:tooltip for="image" value="Cliquez pour avoir un Zoom sur cette photo" showEffect="slide" />
                        </p:column>
                        <p:column>
                           <f:facet name="header">Traitements annexes</f:facet>
                           <p:inputText value="#{photo.description}" />
                           <p:commandButton icon="ui-icon-disk" value="Changer la description" action="#{archivage.changeDescription(photo)}" />
                           <p:commandButton icon="ui-icon-trash" value="Supprimer la photo" action="#{archivage.supprimePhoto(photo)}" />
                        </p:column>                               
                     </p:dataTable>
                  </p:outputPanel>
               </p:tab>  
               <p:tab title="Ajouter une nouvelle photo">
                  Description obligatoire de la photo : 
                  <p:inputText value="#{archivage.description}">
                     <p:ajax event="keyup"  />
                  </p:inputText>
                  <p:fileUpload fileUploadListener="#{archivage.stockerPhoto}" mode="advanced" label="Choisissez votre photo" uploadLabel="Archiver" cancelLabel="Annuler" />
               </p:tab>
            </p:tabView> 
            <f:facet name="footer">
                Titre : <p:inputText value="#{archivage.nouveauTitre}" />
                <p:commandButton icon="ui-icon-document" value="Création d'un nouveau lot de photos" action="#{archivage.nouveauLot()}" />                                 
            </f:facet>
          </p:panel>             
      </h:form>
   </h:body>
</html>
service.Archivage.java
package service;

import entité.*;
import java.io.*;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.application.FacesMessage;
import javax.faces.bean.*;
import javax.faces.context.FacesContext;
import org.primefaces.event.FileUploadEvent;
import org.primefaces.event.map.OverlaySelectEvent;
import org.primefaces.model.*;
import org.primefaces.model.map.*;

@ManagedBean
@SessionScoped
public class Archivage {
   @EJB
   private ServiceREST service;
   private LotDePhotos lot;
   private List<LotDePhotos> listeDesLots;
   private String titre;
   private String nouveauTitre;
   private MapModel cartographie = new DefaultMapModel();
   private String centre;
   private Marker marqueur;
   private boolean miseAJour;
   private String description;
   
   @PostConstruct
   private void init() {
      File rep = new File(service.getRépertoire());
      if (!rep.exists()) rep.mkdir();
      listeDesLots = service.getListeDesLots();
      if (!listeDesLots.isEmpty()) lot = listeDesLots.get(0);
      marqueurs();
   }

   public boolean isMiseAJour() { return miseAJour;  }
   
   public String getTitre() { return titre;   }
   public void setTitre(String titre) {  this.titre = titre; }

   public String getNouveauTitre() { return nouveauTitre;  }
   public void setNouveauTitre(String nouveauTitre) { this.nouveauTitre = nouveauTitre;  }

   public String getDescription() { return description;  }
   public void setDescription(String description) { this.description = description;  }

   public List<LotDePhotos> getListeDesLots() { return listeDesLots;  }
   public LotDePhotos getLot() { return lot;  }
   public Photo getPhoto(String id) {  return service.recherchePhoto(id);  }
   public String getCentre() { return centre; }
   public MapModel getCartographie() { return cartographie;  }   

   public void changeLot() {  
      lot = service.rechercheLot(titre);
      marqueurs();
      miseAJour = true;
   }
   
   public void nouveauLot() { 
      service.créationLotDePhotos(nouveauTitre); 
      listeDesLots = service.getListeDesLots();
   }
    
   public void changeTitre() { 
      service.modifierTitreLotDePhoto(lot);  
      listeDesLots = service.getListeDesLots();
   }   

   public void supprimeLot() {
      service.supprimeLot(lot.getId());
      init();
   }
   
   public void changeDescription(Photo photo) { service.modifierDescriptionPhoto(photo);  }   
   public void supprimePhoto(Photo photo) {  service.supprimePhoto(lot, photo);  }
   
   public void stockerPhoto(FileUploadEvent evt) { 
      try {
         UploadedFile fichier = evt.getFile();
         service.stocker(lot.getId(), description, fichier.getContents());
         message(description, "Votre photo vient d'être stockée dans le service");
         lot = service.rechercheLot(titre);
      } 
      catch (Exception ex) { message("ATTENTION", "Impossible de stocker votre photo dans le service"); }
   }
 
   public void choixMarqueur(OverlaySelectEvent event) { 
      marqueur = (Marker) event.getOverlay(); 
      miseAJour = false;
   }

   public Marker getMarqueur() { return marqueur; }
   
   private void marqueurs() {
      for (Photo photo : lot.getPhotos()) {
         if (photo.isExif()) {
            LatLng coordonnées = new LatLng(photo.getLatitude(), photo.getLongitude());
            marqueur = new Marker(coordonnées, photo.getDescription(), photo.getId());
            cartographie.addOverlay(marqueur); 
            centre = photo.getLatitude()+", "+photo.getLongitude();
         }       
      }  
   }
   
   private void message(String titre, String détail) {
      FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(titre, détail));
   }
}
Mise en place du service
service.ServiceREST.java
package service;

import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.*;
import com.drew.metadata.exif.GpsDirectory;
import entité.*;
import java.io.*;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;

@Path("/")
@Stateless
@LocalBean
public class ServiceREST {
   private final String répertoire = "ArchivagePhotos/";
   @PersistenceContext
   private EntityManager bd;
   
   @POST
   @Path("{titre}")
   public Response créationLotDePhotos(@PathParam("titre") String titre) {
      LotDePhotos lot = new LotDePhotos(titre);
      bd.persist(lot);
      Query requête = bd.createNamedQuery("recherche");
      requête.setParameter("titre", titre);
      lot = (LotDePhotos) requête.getSingleResult();
      return Response.ok().header("id", lot.getId()).build();     
   }

   @POST
   @Path("{id}/description={description}")
   @Consumes("image/jpeg")
   public void stocker(@PathParam("id") long id, @PathParam("description") String description, InputStream flux) throws Exception {     
      byte[] octets = lireOctets(flux);
      stocker(id, description, octets);
   }     

   @PUT
   @Path("{id}/titre={titre}")
   public void changeTitre(@PathParam("id") long id, @PathParam("titre") String titre) {
      LotDePhotos lot = bd.find(LotDePhotos.class, id);
      lot.setTitre(titre);
      bd.merge(lot);
   } 
     
   @GET
   @Path("{nomFichier}")
   @Produces("image/jpeg")
   public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
      return new FileInputStream(répertoire+nom);
   }   
   
   @GET
   @Produces("application/json")
   public List<LotDePhotos> getListeDesLots() {
      return bd.createNamedQuery("toutLesLots").getResultList();
   }
   
   @DELETE
   @Path("{id}")
   public void supprimeLot(@PathParam("id") long id) {
      LotDePhotos lot = bd.find(LotDePhotos.class, id);
      for (Photo photo : lot.getPhotos()) new File(répertoire+photo.getId()).delete();
      bd.remove(lot);
   }
   
   public LotDePhotos rechercheLot(String titre) {
      Query requête = bd.createNamedQuery("recherche");
      requête.setParameter("titre", titre);
      return (LotDePhotos) requête.getSingleResult();      
   }
   
   public Photo recherchePhoto(String id) { return (Photo) bd.find(Photo.class, id); }
   
   public void modifierTitreLotDePhoto(LotDePhotos lot) { bd.merge(lot); }
   public void modifierDescriptionPhoto(Photo photo) { bd.merge(photo); }
   
   public void supprimePhoto(LotDePhotos lot, Photo photo) {
      lot.getPhotos().remove(photo);
      bd.merge(lot);
      bd.remove(recherchePhoto(photo.getId()));
      new File(répertoire+photo.getId()).delete();
   }
   
   public String getRépertoire() {  return répertoire;  }

   public void stocker(long id, String description, byte[] octets) throws Exception {     
      Photo photo = new Photo(description);
      FileOutputStream fichier = new FileOutputStream(répertoire+photo.getId());
      fichier.write(octets);
      fichier.close();
      exif(répertoire, photo);
      LotDePhotos lot = bd.find(LotDePhotos.class, id);
      lot.ajoutPhoto(photo);
      bd.merge(lot);
   }        

   private byte[] lireOctets(InputStream stream) throws IOException {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024]; int octetsLus = 0;
      do {
         octetsLus = stream.read(buffer);
         if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
      } 
      while (octetsLus > -1);
      return baos.toByteArray();
   }  
   
   private void exif(String répertoire, Photo photo) throws Exception {
      Metadata metadata = ImageMetadataReader.readMetadata(new File(répertoire+photo.getId()));
      Directory exif = metadata.getDirectory(GpsDirectory.class);
      boolean EXIF;
      try {
         exif.getDescription(GpsDirectory.TAG_GPS_LATITUDE);
         EXIF = true;
      }
      catch (Exception ex) { EXIF = false; }
      photo.setExif(EXIF);
      if (photo.isExif()) {
         photo.setLatitude(toDouble(exif.getDescription(GpsDirectory.TAG_GPS_LATITUDE)));
         photo.setLongitude(toDouble(exif.getDescription(GpsDirectory.TAG_GPS_LONGITUDE)));
         photo.setJour(date(exif.getDescription(GpsDirectory.TAG_GPS_DATE_STAMP)));
      }
   }
   
   private double toDouble(String localisation) {
      String[] degrés = localisation.split("°");
      double valeur = Integer.parseInt(degrés[0]);
      String[] minutes = degrés[1].split("'");
      valeur += Integer.parseInt(minutes[0]) / (double)60;
      String[] secondes = minutes[1].split("\"");
      valeur += Double.parseDouble(secondes[0]) / 3600;
      return valeur;
   }
   
   private Date date(String date) {
      String[] séparation = date.split(":");
      int année = Integer.parseInt(séparation[0]);
      int mois = Integer.parseInt(séparation[1]);
      int jour =  Integer.parseInt(séparation[2]);
      Calendar calendrier = new GregorianCalendar(année, mois-1, jour);
      return calendrier.getTime();
   }   
}
entité.LotDePhotos.java
package entité;

import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@Entity
@NamedQueries({
   @NamedQuery(name="recherche", query="SELECT lot FROM LotDePhotos lot WHERE lot.titre = :titre"),
   @NamedQuery(name="toutLesLots", query="SELECT lot FROM LotDePhotos lot")
})
public class LotDePhotos implements Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private long id;
   private String titre;
   @JoinColumn
   @OneToMany(fetch= FetchType.EAGER, cascade= CascadeType.ALL)
   private List<Photo> photos = new ArrayList<Photo>();

   public LotDePhotos() { }
   public LotDePhotos(String titre) {  this.titre = titre;  }

   public long getId() {  return id;   }

   public String getTitre() { return titre;  }
   public void setTitre(String titre) {  this.titre = titre;  }

   public List<Photo> getPhotos() { return photos;  }
   public void ajoutPhoto(Photo photo) { photos.add(photo); }

   @Override
   public String toString() { return titre; } 
}
entité.Photo.java
package entité;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@Entity
public class Photo implements Serializable {
   @Id
   private String id;
   private String description;
   private boolean exif;
   private double longitude;
   private double latitude;
   @Temporal(javax.persistence.TemporalType.DATE)
   private Date jour;

   public Photo() {  id = System.currentTimeMillis()+".jpg"; }

   public Photo(String description) {
      id = System.currentTimeMillis()+".jpg"; 
      this.description = description;
   }

   public String getId() { return id;  }

   public String getDescription() { return description;  }
   public void setDescription(String description) {  this.description = description;   }

   public Date getJour() {  return jour;  }
   public void setJour(Date jour) {  this.jour = jour;  }

   public double getLatitude() {  return latitude;  }
   public void setLatitude(double latitude) {  this.latitude = latitude;  }
   
   public boolean isExif() { return exif;  }
   public void setExif(boolean localisation) {  this.exif = localisation;  }

   public double getLongitude() { return longitude;  }
   public void setLongitude(double longitude) {  this.longitude = longitude;  }

   @Override
   public String toString() { return description;  }
}

Sécurisation des web services REST

La sécurisation des applications est ou devrait être un souci majeur pour les sociétés. Ceci peut aller de la sécurisation d'un réseau au chiffrement des transferts de données, en passant par l'octroi de certaines permissions aux utilisateurs d'un système. Au cours de notre navigation quotidienne sur Internet, nous rencontrons de nombreux sites où nous devons entrer un nom d'utilisateur et un mot de passe pour permettre l'accès à certaines parties d'une application. La sécurité est devenue une nécessité sur le Web et, en conséquence, Java EE a défini plusieurs mécanismes pour sécuriser les applications.

Autorisation
La sécurité utilise le mécanisme d'autorisation également connu sous le terme access control. Ce mécanisme permet d'associer des droits et permissions à chaque utilisateur. Après authentification, le rôle associé à l'utilisateur détermine les opérations qu'il est capable de faire.
Rôle
Le rôle définit les parties des applications auxquelles l'utilisateur peut accéder. Par exemple, tous les utilisateurs du groupe Enseignant peuvent accéder au système de gestion des notes et des cours alors que les utilisateurs associés au groupe Etudiant pouront seulement consulter la liste des notes. Un rôle est différent pour chaque groupe mais un utilisateur peut être associé à plusieurs groupes.
Descripteur de déploiment
Les rôles sont précisés dans le descripteur de déploiement de l'application /WEB-INF/web.xml. Les administrateurs de l'application affectent ensuite les rôles à chaque groupe à partir d'un fichier XML, d'une base de données ou d'un annuaire LDAP.
Principal et rôle

Les principaux et les rôles tiennent une place importante dans la sécurité logicielle. Un principal est un utilisateur qui a été authentifié par un nom et un mot de passe stockés dans une base de données ou dans un fichier spécifique dans le serveur d'application, par exemple. Les principaux peuvent être organisées en groupes, appelés rôles, qui leur permettent de partager un ensemble de permissions accès au système de facturation ou possibilité d'envoyer des messages dans un workflow, par exemple.

Comme vous pouvez le constater, un utilisateur authentifié est lié à un principal qui possède un identifiant unique et qui peut être associé à plusieurs rôles. Ainsi le principal de l'utilisateur Jeanne, par exemple, est lié aux rôles de consultation, de création et de modification.

Authentification et habilitation

La sécurisation d'une application implique deux fonctions : l'authentification et l'habilitation.

Dans un scénario de sécurité classique, l'utilisateur doit entrer son identifiant et son mot de passe via une interface client (web ou Swing). Ces informations sont vérifiées avec JAAS via un système d'authentification sous-jacent. Si l'authentification réussit, l'utilisateur est associé à un principal qui est ensuite lui-même associé à un ou plusieurs rôles. Lorsque l'utilisateur accède à un EJB sécurisé (ou un composant REST sécurisé), le principal est transmis de façon transparente à l'EJB, qui l'utilise pour savoir si le rôle de l'appelant l'autorise à accéder aux méthodes qu'il tente d'exécuter.

La sécurité de java EE repose largement sur l'API JAAS qui est automatiquement intégrée dans les serveurs d'applications certifiés Java EE. En fait, JAAS est l'API utilisée en interne par les couches web et EJB pour réaliser des opérations d'authentification et d'habilitation. Elle peut accéder également aux systèmes d'authentification sous-jacents comme LDAP, Active Directory, etc..
Cas d'utilisation où le client utilise un navigateur

Le protocole HTTP fonctionne sous la forme de requêtes/réponses permettant de demander les identités des utilisateurs et de fournir l'accès aux ressources en fonction de ces identités. Lorsqu'une requête arrive à destination d'une ressource protégée, le serveur Web vérifie si le navigateur a envoyé les informations d'authentification.

Si tel n'est pas le cas, le serveur envoie une page d'erreur avec le code 401 au navigateur en précisant le type d'information d'authentification qu'il attend. Le navigateur affiche alors une boîte de dialogue ou un formulaire d'authentification afin de demander les informations à l'utilisateur. Si l'utilisateur est précédemment authentifié, le navigateur utilise alors le cache local possédant les renseignements d'une page précédente.

Sécurisation avec Glassfish

Le système d'authentification est présent depuis le début du Web et a été défini en 1996 dans la spécification HTTP 1.0. Le mécanisme d'authentification a également introduit le concept de realm définit par la balise <realm-name /> dans le descripteur de déploiement WEB-INF/web.xml.

Un Realm est une chaîne interprétée par le seveur pour identifier une source de données afin de résoudre les accès par identifiant et mot de passe. L'authentification prévue par Glassfish est basée sur les Realms. Un Realm contient typiquement plusieurs utilisateurs et leurs informations de sécurité. Les comptes utilisateurs sont basés sur des identifiants et des mots de passe.

Le serveur Java EE Glassfish dispose d'un mécanisme intégré très souple pour gérer les Realms. Il est possible d'utiliser le fichier domain.xml, le fichier de configuration spécifique au domaine, une base de données ou encore les données d'un serveur LDAP. Les règles d'utilisation des Realms sont les suivantes :

Une authentification basée sur les Realms, également appelée security policy ou sécurité de domaine, permet de définir la politique de sécurité pour l'accès aux ressources.
Le serveur Glassfish est livré avec trois Realms pré-configurés file, certificate et admin-realm. Deux sont basés sur des fichiers et un autre sur un certificat. Si une application ne précise pas de Realm elle utilise le Realm file par défaut.

Les types de Realms suivants sont utilisables avec GlassFish
file
Le Realm par défaut est nommé file. Ce realm stocke les utilisateurs, mots de passe et le groupe dans le fichier domains/domain1/config/keyfile.

La commande suivante liste tous les utilisateurs contenus dans le Realm de type file :
$ asadmin list-file-users
admin-realm
Ce type de Realm est également un Realm de type file mais sauvegarde les comptes administrateurs dans le fichier domains/domain1/config/admin-keyfile.
certificat
L'authentification est alors basée sur les certificats clients. Le serveur utilise des comptes dans une base de données certifiée. Avec ce type de sécurité, le serveur utilise les certificats avec le protocole HTTPS pour l'authentification des clients. Glassfish utilise le format Java Key Store et stocke sa clé privée et ses certificats dans les fichiers domains/domain1/config/kestore.jsk et domains/domain1/config/cacerts.jsk. Les fichiers Java Key Store peuvent être gérés en utilisant l'outil Java SE keytool.
ldap
Ce type de Realm permet d'utiliser un annuaire LDAP pour les authentifications utilisateurs.
jdbc
Les Realms JDBC permettent de stocker les comptes utilisateurs dans une base de données relationnelle. Les Realms JDBC utilisent une ressource JDBC pour se connecter à la base de données contenant les comptes utilisateurs. La mise en place d'un Realm JBDC repose sur des paramètres JAAS. Ainsi, dans le paramètre JAAS context nous devons spécifier le nom de la classe pour le Realm JDBC jdbcRealm. La propriété Digest Algorithm est nécessaire pour la gestion du mot de passe. L'algorithme de cryptage MD5 est souvent utilisé pour spécifier le cryptage des mots de passe. Le paramètre Assign Group est utilisé pour spécifier un groupe pour tous les utilisateurs.
custom
Ce type très souple permet de définir notre propre implémentation du Realm.
Actuellement, trois types d'authentification sont proposés :
Authentification de base
Le schéma d'authentification HTTP le plus simple se nomme autorisation de base Basic Authorization. Ce mécanisme consiste à utiliser un nom utilisateur/identifiant et un mot de passe en clair.
Authentification par digest
Avec ce type d'autorisation, l'utilisateur saisie son mot de passe en clair, mais celui-ci est envoyé sous forme criptée à partir d'une chaîne de caractères sur le réseau. Cette authentification est appelée authentification par Digest Digest Auth et utilise l'algorithme de hachage unilatéral SHA ou MD5.
Authentification par certificat
Avec ce type d'authentification, l'utilisateur doit posséder un certificat client pour accéder au serveur. Cette approche est la plus sûre puisque les certificats sont gérés de façon centralisée par des autorités spécialisées.
Les types d'authentification Realm

Il existe plusieurs types d'authentification pouvant être utilisés pour les Realms. Dans une application, l'élément <realm-name /> du fichier de configuration WEB-INF/web.xml permet de spécifier quel Realm doit être utilisé pour authentifier l'utilisateur. Les applications Web peuvent utiliser les types :

BASIC
L'authentification est basée sur une boîte de saisie de type identifiant / mot de passe. Les protocoles supportés sont HTTP et HTTPS.

DIGEST
L'authentification est proche de la précédente mais le mot de passe client est crypté avant l'envoi par le navigateur.
FORM
L'authentification est basée sur un formulaire de saisie de type identifiant / mot de passe personnalisable et développé en XHTML, supportant les protocoles HTTP et HTTPS.
CLIENT-CERT
L'authentification est basée sur un certificat à clé publique. La communication est effectuée au travers des protocoles HTTP et HTTPS.
Première mise en oeuvre d'un système de sécurité avec Glassfish

Après tout ce préambule, je vous propose de valider toutes ces compétences nouvellement acquises en reprenant le projet du service web REST d'archivage de photos afin d'y associer un système de sécurité basé sur les realms.

Création de son propre realm

Ce type de sécurité est utilisée avec l'interface d'administration de GlassFish accessible à l'adresse URL suivante http://localhost:4848/. Je profite de l'occasion pour générer un nouveau realm qui sera utilisé uniquement par ce service Web. Les captures d'écran ci-dessous vous montre l'ensemble des enchaînements.

Retour sur le projet d'archivage de photos

Nous allons donc sécuriser l'application web qui contient le web service REST d'archivage de photos. Je vous rappelle ci-dessous le code concernant ce service avec quelques petites modifications mineures.

service.Archivage.java
package service;

import java.io.*;
import javax.annotation.PostConstruct;
import javax.ws.rs.*;

@Path("/")
public class Archivage {
   private final String répertoire = "ArchivagePhotos/";
   
   @PostConstruct
   private void init() {
      File rep = new File(répertoire);
      if (!rep.exists()) rep.mkdir();
   }
   
   @GET  
   @Path("consultation/liste")
   @Produces("application/json")
   public String[] getPhotos() { return new File(répertoire).list();  }
   
   @GET
   @Path("consultation/{nomFichier}")
   @Produces("image/jpeg")
   public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
      return new FileInputStream(répertoire+nom);
   }  
   
   @POST
   @Path("gestion/{nomFichier}")
   @Consumes("image/jpeg")
   public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {     
      byte[] octets = lireOctets(flux);
      FileOutputStream fichier = new FileOutputStream(répertoire+nom);
      fichier.write(octets);
      fichier.close();
   }
   
   @PUT
   @Path("gestion/ancien={ancien}/nouveau={nouveau}")
   public void changerNomPhoto(@PathParam("ancien") String ancien, @PathParam("nouveau") String nouveau) {
      File rep = new File(répertoire);
      File ancienFichier = new File(rep+"/"+ancien);
      File nouveauFichier = new File(rep+"/"+nouveau);
      ancienFichier.renameTo(nouveauFichier);
      System.out.println(ancienFichier.getPath());
      System.out.println(nouveauFichier.getPath());
   }
   
   @DELETE
   @Path("gestion/{nomFichier}")
   public void supprimer(@PathParam("nomFichier") String nom) {
      new File(répertoire+nom).delete();
   }   
  
   private byte[] lireOctets(InputStream stream) throws IOException {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024]; int octetsLus = 0;
      do {
         octetsLus = stream.read(buffer);
         if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
      } 
      while (octetsLus > -1);
      return baos.toByteArray();
   }   
}
Mise en application de photos-realm

Le modèle de sécurité Java EE propose des accès privilégiés à certaines ressources en association avec des rôles au travers du mécanisme des realms. Pour réaliser cette opération, les crières suivants sont à prendre en considération :

Comme mentionné précédemment, le principe d'autorisation Java EE est basée sur le mécanisme des rôles. L'accès à une ressource par un utilisateur dépend du mapping de cet utilisateur ou de son groupe vers un rôle associé. Ainsi, le descripteur de déploiement spécifique au serveur WEB-INF/glassfish-web.xml spécifie le mapping entre les utilisateurs (ou les groupes) dans le realm choisi et les rôles de l'application. Ceci est réalisée au travers de la balise <security-role-mapping>.
glassfish-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<sun-web-app error-url="">
   <context-root>/Photos</context-root>
   
   <security-role-mapping>
      <role-name>gestion</role-name>
      <group-name>creation</group-name>
      <group-name>modification</group-name>
   </security-role-mapping>
   
   <security-role-mapping>
      <role-name>recuperer</role-name>
      <group-name>consultation</group-name>
   </security-role-mapping>   
   
   <security-role-mapping>
      <role-name>supprimer</role-name>
      <principal-name>manu</principal-name>
   </security-role-mapping>   
</sun-web-app>
  • Quand nous avons créé photos-realm, nous avons défini un certain nombre d'utilisateurs associés à un ensemble de groupes, respectivement, pour notre exemple, les utilisateurs manu, jeanne et martin avec les groupes consultation, creation, modification et suppression.
  • Cette notion de groupe est souvent associée à la notion de rôle. Toutefois, il est possible de faire la distinction pour une application donnée. C'est d'ailleurs souvent le concept qui est retenu dans le système de sécurité des serveurs d'applications implémentant Java EE.
  • C'est dans cette démarche que le descripteur de déploiement spécifique au serveur glassfish-web.xml est conçu. Il définit les rôles attendus à cette application Web et les associent à des utilisateurs ou des groupes d'utilisateurs.
  • Ainsi, dans notre exemple, nous devons prendre en compte trois rôles spécifiques, gestion, recuperer et supprimer.
  • Puisque nous avons trois cas particulier, nous devons donc proposer trois balises <security-role-mapping> qui vont réaliser le mapping entre les rôles de l'application définis par les balises <role-name> et les utilisateurs ou les groupes au travers respectivement des balises <principal-name> et <group-name>.
Une fois que nous connaissons les différents rôles nécessaires, nous pouvons gérer de façon précise la sécurité associée à cette application web dans le descripteur de déploiement WEB-INF/web.xml. Trois parties sont nécessaires pour exploiter pleinement la sécurité, le mode d'authentification au travers de la balise <login-config>, tous les rôles utilisés par cette application avec la balise <security-role> et enfin les contraintes d'utilisation pour chaque rôle au travers de la balise <security-constraint>.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <servlet>
        <servlet-name>ServletAdaptor</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>ServletAdaptor</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    
    <security-constraint>
       <web-resource-collection>
          <web-resource-name>Gestion des photos</web-resource-name>
          <url-pattern>/gestion/*</url-pattern>
          <http-method>POST</http-method>
          <http-method>PUT</http-method>
       </web-resource-collection>
       <auth-constraint>
          <role-name>gestion</role-name>
       </auth-constraint>
    </security-constraint> 

    <security-constraint>
       <web-resource-collection>
          <web-resource-name>Suppression de photos</web-resource-name>
          <url-pattern>/gestion/*</url-pattern>
          <http-method>DELETE</http-method>
       </web-resource-collection>
       <auth-constraint>
          <role-name>supprimer</role-name>
       </auth-constraint>
    </security-constraint> 

    <security-constraint>
       <web-resource-collection>
          <web-resource-name>Récupération de photo</web-resource-name>
          <url-pattern>/consultation/*</url-pattern>
          <http-method>GET</http-method>
       </web-resource-collection>
       <auth-constraint>
          <role-name>recuperer</role-name>
       </auth-constraint>
    </security-constraint> 
    
    <security-role>
       <role-name>gestion</role-name>
       <role-name>recuperer</role-name>
       <role-name>supprimer</role-name>
    </security-role>    

    <login-config>
       <auth-method>BASIC</auth-method>
       <realm-name>photos-realm</realm-name>
    </login-config>
</web-app>
  • Ainsi, à l'aide de la balise <login-config>, nous pouvons spécifier le realm à prendre en compte avec la balise <realm-name> et de préciser le type d'authentification attendu à l'aide de la balise <auth-method>, ici le mode basique.
  • Nous définissons ensuite tous les rôles à prendre en compte au travers de la balise <security-role> à l'intérieur de laquelle va se trouver l'ensemble des balises <role-name>.
  • La grosse partie se situe dans la définissions des contraintes de sécurité associées à chaque rôle spécifié, au travers de la balise <security-constraint>. A l'intérieur de cette balise se trouvent un certain nombre d'autres balises qui vont nous permettre de régler précisément les filtres d'utilisation pour chacun de ces rôles.
  • <web-resource-collection> : cette balise permet de décrire les contarintes d'accès aux ressources spécifiées.
  • <web-resource-name> : cette balise permet de donner un nom au système d'authentification.
  • <url-pattern> : cette balise permet de définir, à partir d'une expression régulière, les ressources à protéger. Il doit y avoir zéro ou plusieurs balises <url-pattern> dans une balise <web-ressource-collection>. L'abscence de la balise <url-pattern> indique que la contrainte de sécurité doit s'appliquer à toutes les ressources.
  • <http-method> : il est possible de proposer un filtre sur le choix des méthodes HTTP. Là aussi, cette balise est optionnelle et nous pouvons en avoir plusieurs.
  • <auth-constraint> : cette balise contient zéro ou plusieurs balises <role-name> afin de préciser le ou les rôles ayant accès aux ressources.
  • <role-name> : cette balise permet de définir le nom du rôle ayant accès aux ressources sécurisées.
Utilisation de cette application web sécurisée

Authentification à l'aide d'un client Android

Lorsque nous avons un navigateur, nous sommes habitué à gérer les authentifications. Quand est-il d'un client quelconque qui interroge le service web REST ? et plus spécifiquement quand est-il d'un client Android ? Je vais m'intéressé plus particulièrement au client Android sachant que si une autre application cliente implémente la librairie HttpClient, la réponse sera totalement traitée de la même façon.

En effet HttpClient 4.x de la librairie Apache supporte les authentifications Basic, Digest et Certificat. Ces authentifications s'implémentent à l'aide de la méthode setCredentials() elle-même issue de la méthode getCreadentialsProvider() elle même contenue dans la classe DefaultHttpClient.
public void envoyer(View vue) throws IOException {
  DefaultHttpClient client = new DefaultHttpClient();
  client.getCredentialsProvider().setCredentials(
          new AuthScope(adresse, 8080),
          new UsernamePasswordCredentials(utilisateur, motdepasse));
  HttpPost requete = new HttpPost("http://"+adresse+":8080/Photos/gestion/"+description.getText().toString()+".jpg");  
  requete.setEntity(new FileEntity(new File(Environment.getExternalStorageDirectory(), "photo.jpg"), "image/jpeg"));
  client.execute(requete);      
}
Je vous redonne le code complet de l'application cliente Android qui permet de prendre une photo et de l'envoyer ensuite au service REST d'archivage, avec en plus les modifications qui tiennent compte de l'authentification.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.photos"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Stockage photos" >
        <activity android:name="ArchivagePhotos" android:label="Stocker vos photos" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="ChoixServeur" android:label="Choix du serveur" android:theme="@android:style/Theme.Dialog" /> 
    </application>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
res/layout/adresse.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   android:padding="3dp">
   <RelativeLayout
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="3dp">
      <Button
         android:id="@+id/ok"
         android:layout_alignParentRight="true"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:text="OK" 
         android:onClick="ok"/>              
      <EditText
         android:layout_toLeftOf="@id/ok"
         android:id="@+id/adresse"
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content" 
         android:hint="Adresse IP" />                   
   </RelativeLayout>
   <EditText
      android:id="@+id/utilisateur"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Utilisateur" />       
   <EditText
      android:id="@+id/motdepasse"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:password="true"
      android:hint="Mot de passe" />             
</LinearLayout>
fr.btsiris.rest.ChoixServeur.java
package fr.btsiris.rest;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.*;
import android.widget.EditText;

public class ChoixServeur extends Activity {
   private EditText adresse;

   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  
      setContentView(R.layout.adresse);
      adresse = (EditText) findViewById(R.id.adresse);
   }
   
   public void ok(View vue) {
      Intent intent = new Intent();
      intent.putExtra("adresse", adresse.getText().toString());
      setResult(RESULT_OK, intent);
      finish();      
   }
}
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:background="#008800"
   android:padding="3dp">
    <EditText
       android:id="@+id/description"
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:hint="Description de la photo"
       android:layout_alignParentBottom="true"/> 
    <LinearLayout 
       android:id="@+id/boutons"
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:layout_above="@id/description">   
       <Button 
            android:layout_width="0dp" 
            android:layout_height="wrap_content" 
            android:layout_weight="1"
            android:text="Photographier" 
            android:onClick="photographier" /> 
       <Button 
            android:layout_width="0dp" 
            android:layout_weight="1"
            android:layout_height="wrap_content" 
            android:text="Envoyer" 
            android:onClick="envoyer" />             
    </LinearLayout>
    <ImageView
       android:id="@+id/image"
       android:layout_width="fill_parent" 
       android:layout_height="fill_parent"
       android:padding="10dp"
       android:layout_above="@id/boutons" />
</RelativeLayout>
fr.btsiris.rest.ArchivagePhotos.java
package fr.btsiris.photos;

import android.app.Activity;
import android.content.Intent;
import android.graphics.*;
import android.net.Uri;
import android.os.*;
import android.provider.MediaStore;
import android.view.View;
import android.widget.*;
import java.io.*;
import org.apache.http.auth.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.DefaultHttpClient;

public class ArchivagePhotos extends Activity {
   private EditText description;
   private ImageView image;
   private Uri fichierUri;
   private String adresse, utilisateur, motdepasse;
   private final int RECHERCHE_ADRESSE = 1;
   private final int PHOTOGRAPHIER = 2;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        description = (EditText) findViewById(R.id.description);
        image = (ImageView) findViewById(R.id.image);
        startActivityForResult(new Intent(this, ChoixServeur.class), RECHERCHE_ADRESSE);
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent intention) {
      switch (requestCode) {
         case RECHERCHE_ADRESSE :
            adresse = intention.getStringExtra("adresse");
            utilisateur = intention.getStringExtra("utilisateur");
            motdepasse = intention.getStringExtra("motdepasse");
            break;         
         case PHOTOGRAPHIER :
            File fichier = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
            Bitmap photo = BitmapFactory.decodeFile(fichier.getPath());
            image.setImageBitmap(photo);
            break;
      }
   }

   public void photographier(View vue) {
      Intent intention = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      File fichier = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
      fichierUri = Uri.fromFile(fichier);
      intention.putExtra(MediaStore.EXTRA_OUTPUT, fichierUri);
      startActivityForResult(intention, PHOTOGRAPHIER);
   }   
   
   public void envoyer(View vue) throws IOException {
      DefaultHttpClient client = new DefaultHttpClient();
      client.getCredentialsProvider().setCredentials(
              new AuthScope(adresse, 8080),
              new UsernamePasswordCredentials(utilisateur, motdepasse));
      HttpPost requete = new HttpPost("http://"+adresse+":8080/Photos/gestion/"+description.getText().toString()+".jpg");  
      requete.setEntity(new FileEntity(new File(Environment.getExternalStorageDirectory(), "photo.jpg"), "image/jpeg"));
      client.execute(requete);      
   }
}
Optimiser la sécurisation avec HTTPS

L'exemple de sécurisation que nous venons de traiter peut convenir dans la majorité des cas. Toutefois, il existe un petit problème, c'est que le mot de passe est envoyé en clair et qu'il peut donc être vue au travers d' un analyseur de trame.

Procédure à suivre Le serveur GlassFish supporte les protocoles SSL et TLS pour les échanges sécurisés. Pour utiliser SSL, GlassFish doit avoir un certificat pour chaque interface externe ou adresse IP acceptant les connexions sécurisées. Par défaut, HTTPS est activé sur le port 8181 pour le trafic Web, et SSL est activé sur les ports 3820 et 3902 pour le trafic IIOP.