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
En tapant l'URL d'un site, l'internaute envoie via le navigateur
une requête au serveur.
Une connexion s'établit entre le client et le serveur sur le port 80 port par défaut d'un serveur Web .
Le navigateur envoie une requête demandant l'affichage d'un document. La requête contient entre autres la méthode GET ,
POST , etc. qui précise comment l'information est envoyée.
Le serveur répond à la requête en envoyant une réponse HTTP composée de
plusieurs parties, dont :
l'état de la réponse, à savoir une ligne de texte qui décrit le résultat du serveur (code 200 pour un accord, 400
pour une erreur due au client, 500 pour un erreur due au serveur) ;
les données à afficher.
Une fois la réponse reçue par le client, la connexion est fermée. Pour afficher une nouvelle page du site, une nouvelle connexion
doit être établie.
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 .
Pour en savoir plus sur le protocole HTTP et les en-têtes
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.
Pour en savoir plus sur les types MIME
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 :
1xx : Information : La requête a été reçue et le traitement se poursuit.
2xx : Succès : L'action a bien été reçue, comprise et acceptée.
3xx : Redirection : Une autre action est requise pour que la requête s'effectue.
4xx : Erreur du client : La requête contient des erreurs de syntaxe ou ne peut pas être exécutée.
5xx : Erreur du serveur : Le serveur n'a pas réussi à exécuter une requête pourtant apparemment valide.
Voici quelques codes d'état que vous avez sûrement déjà dû rencontrer :
200 OK : La requête a réussi. Le corps de l'entité, si elle en possède un, contient la représentation de la ressource.
301 Moved Permanently : La ressource demandée a été affectée à une autre URI permanente et toute référence
future à cette ressource devrait utiliser l'une des URI renvoyées.
404 Not Found : Le serveur n'a rien trouvé qui corresponde à l'URL demandée.
500 Internal Server Error : Le serveur s'est trouvé dans une situation inattendue qui l'a empêché de répondre à la
requête.
Pour en savoir plus sur le protocole HTTP et les codes d'erreur
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 :
Le support de beans session sans état comme ressources racine.
Il est désormais possible d'injecter des ressources externes gestionnaire de persistance, sources de données, EJB, etc.
dans une ressource REST .
Les annotations JAX-RS peuvent s'appliquer à l'interface locale d'un bean ou directement à un bean sans interface.
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 :
Utiliser POST sur des données au format XML, JSON ou texte afin de créer une ressource livre avec
l'URI http://www.site.com/livres/
. Le livre créé, la réponse renvoie l'URI de la nouvelle ressource http://www.site.com/livres/123456
.
Utiliser GET pour lire la ressource et les éventuels liens vers d'autres ressources à partir du corps de
l'entité à l'URI http://www.apress.com/books/123456
.
Utiliser PUT pour modifier la ressource à l'URI http://www.site.com/livres/123456
.
Utiliser DELETE pour supprimer la ressource à l'URI http://www.site.com/livres/123456
.
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 :
Retour de l'information globale, ce que nous nommons l'entité : getEntity() .
Status de la réponse avec son code de succès ou d'erreur : getStatus() .
Données de l'en-tête sous forme de couple clé/valeur : getMetaData() .
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 :
UriBuilder getBaseUriBuilder() : relatif au chemin de l’application.
UriBuilder getAbsolutePathBuilder() : relatif au chemin absolu base+ chemins .
UriBuilder getRequestUriBuilder() : relatif au chemin absolu incluant les paramètres.
Le principe d’utilisation de la classe utilitaire UriBuilder est identique à ResponseBuilder . Les principales
méthodes sont les suivantes :
URI build(Object... values) : construit une URI à partir d’une liste de valeurs pour les Template Parameters.
UriBuilder queryParam(String name, Object...values) : ajoute des paramètres de requête.
UriBuilder path(String path) : ajout un chemin de requête.
UriBuilder fromUri(String uri) : nouvelle instance à partir d’une URI .
UriBuilder host(String host) : modifie l’URI de l’hôte.
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();
}
}
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 .
GET : cette méthode permet de récupérer une ressource depuis le serveur pour l'avoir sur le poste local.
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.
PUT : cette méthode permet de modifier une ressource distante.
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.
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 :
http://localhost:8080/franc?euro=15.24
: retourne une valeur en Franc à partir d'une valeur donnée en €uro.
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 :
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 .
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 .
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 :
http://localhost:8080/Archivage/
- GET : donne la liste des noms des photos stockées dans le
serveur, sous forme de chaîne de caractères.
http://localhost:8080/Archivage/nom-photo
- GET : renvoie l'image sous forme de flux d'octets, avec le
type MIME image/jpeg depuis le serveur, correspondant au nom proposée par l'URL .
http://localhost:8080/Archivage/nom-photo
- POST : envoie l'image depuis le poste local vers le serveur,
le nom du fichier image correspond au nom proposée par l'URL . Le contenu à envoyer est l'ensemble des octets
constituant la photo. Nous devons également préciser le type MIME, ici également image/jpeg .
http://localhost:8080/Archivage/change?ancien=ancien-nom&nouveau=nouveau-nom
- PUT : permet de
changer le nom du fichier image du serveur distant.
http://localhost:8080/Archivage/nom-photo
- DELETE : supprime définitivement la photo stockée sur le
serveur distant, dont le nom est proposée à la fin de l'URL .
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 .
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 .
La première consiste à vérifier l'identité de l'utilisateur son identifiant et son mot de passe, son empreinte
biométrique, etc. en utilisant son système d'authentification et en affectant un principal à cet utilisateur.
L'habilitation consiste à déterminer si un principal un utilisateur authentifié a accès à une
ressource particulière une photo, par exemple ou à une fonction donnée supprimer une photo, par exemple .
Selon son rôle, l'utilisateur peut avoir accès à toutes les ressources, à aucune ou à certaines d'entre elles.
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 :
Tout utilisateur peut être membre d'aucun ou plusieurs rôles.
Tous les rôles peuvent contenir aucun ou plusieurs utilisateurs.
Les Realms permettent de définir les utilisateurs qui peuvent accéder à des ressources en fournissant la liste d'un ou
plusieurs rôles disposant d'un droit d'accès.
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 :
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.
Le contrôle d'accès est basé sur les rôles déclarés sous la forme de balises <role-name />
dans le
descripteur de déploiement (ou déclarés dans le code avec l'annotation @DeclareRoles que nous exploiterons ultérieurement).
Le descripteur de déploiement spécifique au serveur WEB-INF/glassfish-web.xml doit ensuite fournir le mapping entre les
utilisateurs et groupes du realm ainsi que les rôles définis dans l'application.
Les informations confidentielles peuvent être échangées avec le protocole Secure Socket Layer SSL et son
successeur Transport Level Security TSL .
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);
}
La classe org.apache.http.auth.AuthScope permet de définir le serveur à atteindre avec son numéro de service. Vous pouvez
spécifier le serveur par son nom DNS ou par son adresse IP.
La classe org.apache.http.auth.UsernamePasswordCredentials vous vous en doutez, permet de donner l'authentification
proprement dit avec le nom d'utilisateur associé à son mot de passe.
Vous pouvez ainsi appeler la méthode setCredentials() à chaque fois que vous aurez besoin de communiquer avec un
service sécurisé.
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 .