Dans l'étude précédente, nous avons découvert le service Web SOAP. Ce protocole extrêmement sophistiqué utilise beaucoup de ressources puisqu'il est nécessaire de générer et de traduire des documents XML qui transitent au travers du protocole HTTP. Vous devez construire, à la fois côté client et aussi côté serveur un certain nombre de classes pour réaliser ces différents décodages pour retrouver les informations encapsulées dans le document XML. Durant cette étude, nous allons abordé une autre alternative. Malgré qu'elle ne soit pas standard, elle consiste à utiliser directement le protocole HTTP sans couche supplémentaire, à l'aide cette fois-ci d'un web service dénommé, service web REST. Cette approche est plus simple à implémenter. Par ailleurs, les services web, dans ce cas là, sont plutôt associés à gérer des ressources distantes avec toutes les phases classiques, de création, de récupération, de modification et de suppression (CRUD).
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.
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.
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.
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.
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.
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.
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.
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 :
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.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.
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.
idempotenteenvoyer 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.
elle est idempotente: même si nous envoyons plusieurs fois la même requête PUT, l'état de la ressource finale restera inchangé.
est idempotente, mais elle modifie évidemment l'état de la ressource.
requête/réponseidentifié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.
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.
Accept-Language
, le contenu de ce document CSV
pourrait être traduit par le serveur dans la langue correspondante. 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 :
Un code HTTP est associé à chaque réponse. La spécification définit environ 60 codes d'états ; l'élément Status-Code
est un entier de trois chiffres qui décrit le contexte d'une réponse et qui est intégéré dans l'enveloppe de celle-ci. Le premier
chiffre indique l'une des cinq classes de réponses possibles :
Voici quelques codes d'état que vous avez sûrement déjà dû rencontrer :
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.
If-Match
d'une requête. 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.
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.
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 :
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.
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 :
http://www.site.com/livres/
. Le livre créé, la réponse renvoie l'URI de la nouvelle ressource http://www.site.com/livres/123456
.http://www.apress.com/books/123456
.http://www.site.com/livres/123456
.http://www.site.com/livres/123456
.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.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.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.
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
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.
<?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>
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.
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
.
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.
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 :
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.
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; }
}
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());
}
}
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/
.
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}")
. 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));
}
}
/personnels/1234
. Cette
recherche sera effectuée au travers de la méthode unPersonnel()./personnels
, seule la méthode sans annotation @Path sera
automatiquement sélectionnée : ici toutLePersonnel(). /personnels/premiers
, c'est la méthode dixPremiers() qui sera invoquée./personnels/suivants/3
.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).
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();
}
}
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();
}
...
}
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();
}
...
}
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;
}
}
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();
}
...
}
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) {
...
}
}
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 classeMediaType
qui possède toutes les constantes prédéfinies de chacun des types
MIME connus, comme par exemple MediaType.TEXT_PLAIN. 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;
}
}
Accept
de la requête HTTP du client. Accept : text/plain
et que l'URI est /, c'est la méthode
bienvenue() qui sera invoquée.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 :
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"]
}
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.
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. |
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");
}
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;
}
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));
}
}
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>";
}
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);
}
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.
@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) {
...
}
}
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 :
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.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.@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()
}
}
@Path("/bienvenue")
public class Bienvenue {
@GET
public Response getBooks() {
return Response.serverError().build(); // Finalisation en appelant la méthode build()
}
}
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.
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
.
@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());
}
}
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.@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();
}
}
@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();
}
}
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 :@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();
}
}
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.@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";
}
}
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.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.
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.
<?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.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. 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.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.
<?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.
@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();
}
...
}
@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);
}
...
}
@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);
}
...
}
@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);
}
}
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.
<?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>
<?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>
<?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>
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();
}
}
<?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>
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; }
}
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);
}
}
<?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>
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();
}
}
Le deuxième projet sera plus simple puisque, au travers d'une simple petite application web, nous allons archiver et visualiser un ensemble de photos. Là aussi, l'application web utilise les compétences de composants de haut niveau proposés par PrimeFaces. Il sera donc de nouveau nécessaire de déployer, en même temps que le projet principal, les archives primefaces-3.2.jar pour l'ensemble des composants JSF ainsi que sunny.jar pour le thème.
Je vous donne l'ensembles des codes nécessaires à cette application web suivi des différents tests pour évaluer le bon fonctionnement du service web REST.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<context-param>
<param-name>primefaces.THEME</param-name>
<param-value>sunny</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>ServletAdaptor</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ServletAdaptor</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Nous remarquons ici la présence de deux servlets, la première relative à JSF qui sert de contrôleur pour les
requêtes classiques de l'application web, la deuxième s'occupant plus particulièrement des fonctionnalités du service web
REST.package service;
import java.io.*;
import javax.annotation.PostConstruct;
import javax.faces.bean.*;
import javax.ws.rs.*;
@Path("/")
@ManagedBean
public class Archivage {
private final String répertoire = "ArchivagePhotos/";
@PostConstruct
private void init() {
File rep = new File(répertoire);
if (!rep.exists()) rep.mkdir();
}
@GET
@Path("liste")
@Produces("application/json")
public String[] getPhotos() { return new File(répertoire).list(); }
@GET
@Path("{nomFichier}")
@Produces("image/jpeg")
public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
return new FileInputStream(répertoire+nom);
}
@POST
@Path("{nomFichier}")
@Consumes("image/jpeg")
public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {
byte[] octets = lireOctets(flux);
FileOutputStream fichier = new FileOutputStream(répertoire+nom);
fichier.write(octets);
fichier.close();
}
@DELETE
@Path("{nomFichier}")
public void supprimer(@PathParam("nomFichier") String nom) {
new File(répertoire+nom).delete();
}
private byte[] lireOctets(InputStream stream) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; int octetsLus = 0;
do {
octetsLus = stream.read(buffer);
if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
}
while (octetsLus > -1);
return baos.toByteArray();
}
}
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Archivage de photos</title>
</h:head>
<h:body>
<h:form>
<p:panel header="Archivage de photos">
<p:galleria value="#{archivage.photos}" var="photo">
<p:graphicImage value="rest/#{photo}" />
</p:galleria>
</p:panel>
</h:form>
</h:body>
</html>
La vue de index.xhtml est extrêmement simple. La particularité, comme je viens de l'évoquer, est d'utiliser le bean
gérer comme service web REST en ce qui concerne la visualisation de chaque photo, au travers de la méthode restituer()
qui renvoie le flux de la photo concernée.
@Path("/")
@Produces("image/jpeg")
@Consumes("image/jpeg")
@ManagedBean
public class Archivage {
private final String répertoire = "ArchivagePhotos/";
...
@POST
@Path("{nomFichier}")
public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {
byte[] octets = lireOctets(flux);
FileOutputStream fichier = new FileOutputStream(répertoire+nom);
fichier.write(octets);
fichier.close();
}
private byte[] lireOctets(InputStream stream) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; int octetsLus = 0;
do {
octetsLus = stream.read(buffer);
if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
}
while (octetsLus > -1);
return baos.toByteArray();
}
}
Voici la procédure à suivre pour intégrer une nouvelle photo dans votre service. Il suffit de préciser le nom de fichier dans
l'URI et sélectionner le fichier image qui est le contenu de la requête :
Après avoir structurer notre service web REST, je vous propose de fabriquer un client sous Android qui permet d'envoyer des photos prises avec l'APN intégré.
<?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>
<?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>
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();
}
}
<?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>
package fr.btsiris.photos;
import android.app.Activity;
import android.content.Intent;
import android.graphics.*;
import android.net.Uri;
import android.os.*;
import android.provider.MediaStore;
import android.view.View;
import android.widget.*;
import java.io.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.DefaultHttpClient;
public class ArchivagePhotos extends Activity {
private EditText description;
private ImageView image;
private Uri fichierUri;
private String adresse;
private final int RECHERCHE_ADRESSE = 1;
private final int PHOTOGRAPHIER = 2;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
description = (EditText) findViewById(R.id.description);
image = (ImageView) findViewById(R.id.image);
startActivityForResult(new Intent(this, ChoixServeur.class), RECHERCHE_ADRESSE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intention) {
switch (requestCode) {
case RECHERCHE_ADRESSE :
adresse = intention.getStringExtra("adresse");
break;
case PHOTOGRAPHIER :
File fichier = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
Bitmap photo = BitmapFactory.decodeFile(fichier.getPath());
image.setImageBitmap(photo);
break;
}
}
public void photographier(View vue) {
Intent intention = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File fichier = new File(Environment.getExternalStorageDirectory(), "photo.jpg");
fichierUri = Uri.fromFile(fichier);
intention.putExtra(MediaStore.EXTRA_OUTPUT, fichierUri);
startActivityForResult(intention, PHOTOGRAPHIER);
}
public void envoyer(View vue) throws IOException {
HttpClient client = new DefaultHttpClient();
HttpPost requete = new HttpPost("http://"+adresse+":8080/Photos/rest/"+description.getText().toString()+".jpg");
requete.setEntity(new FileEntity(new File(Environment.getExternalStorageDirectory(), "photo.jpg"), "image/jpeg"));
client.execute(requete);
}
}
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.
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.
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.
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.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 :
#-------------------------------------------------
#
# 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.
#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 :
#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;
}
#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
#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;
}
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.
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/jpegdepuis 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.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.
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();
}
}
#-------------------------------------------------
#
# 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. #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;
};
#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");
}
#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
#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);
}
Pour conclure sur les web services de type REST, je vous propose de reprendre l'idée d'archivage de photos en intégrant un certain nombre de fonctionnalités, comme la géolocalisation, l'édition des photos, l'affichage sous forme de gallerie d'images, etc. Comme vous pouvez le constater dans les figures ci-dessous, l'application cliente est une application Web. Dans ce projet ne sera pas intégré un client de type Android.
Je vous donne l'ensembles des codes nécessaires à ce service. Pour que le projet fonctionne correstement, vous avez un certain nombre d'archives à intégrer :
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<context-param>
<param-name>primefaces.THEME</param-name>
<param-value>afterdark</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>ServletAdaptor</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<filter>
<filter-name>PrimeFaces FileUpload Filter</filter-name>
<filter-class>org.primefaces.webapp.filter.FileUploadFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>PrimeFaces FileUpload Filter</filter-name>
<servlet-name>Faces Servlet</servlet-name>
</filter-mapping>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ServletAdaptor</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Archivage de photos</title>
</h:head>
<script src="http://maps.google.com/maps/api/js?sensor=true" type="text/javascript" />
<h:body style="background-color: darkseagreen">
<h:form>
<p:growl autoUpdate="true" showDetail="true" />
<p:panel header="Archivage de photos">
<p:outputPanel autoUpdate="true">
<p:selectOneMenu value="#{archivage.titre}">
<f:selectItems value="#{archivage.listeDesLots}" />
<p:ajax listener="#{archivage.changeLot()}"/>
</p:selectOneMenu>
</p:outputPanel>
<p:tabView>
<p:tab title="Gallerie des photos">
<p:outputPanel autoUpdate="true">
<p:galleria value="#{archivage.lot.photos}" var="photo" effect="slide" filmstripPosition="right" showOverlays="true" frameWidth="90" frameHeight="60" >
<p:graphicImage value="rest/#{photo.id}" width="600" height="400" styleClass="ui-corner-all" />
<p:galleriaOverlay title="#{photo}">
<h:outputText value="#{photo.jour}" rendered="#{photo.exif}">
<f:convertDateTime dateStyle="full" />
</h:outputText>
<br />
<h:outputText value="(Lat. = #{photo.latitude}, Lon.= #{photo.longitude})" rendered="#{photo.exif}" />
<h:outputText value="Aucunes données EXIF" rendered="#{not photo.exif}" />
</p:galleriaOverlay>
</p:galleria>
</p:outputPanel>
</p:tab>
<p:tab title="Google Map">
<p:outputPanel autoUpdate="#{archivage.miseAJour}">
<p:gmap zoom="15" type="HYBRID" center="#{archivage.centre}" style="width:600px;height:400px" model="#{archivage.cartographie}">
<p:ajax event="overlaySelect" listener="#{archivage.choixMarqueur}" />
<p:gmapInfoWindow>
<p:graphicImage value="rest/#{archivage.marqueur.data}" width="200" /><br />
<h:outputText value="#{archivage.marqueur.title}" />
</p:gmapInfoWindow>
</p:gmap>
</p:outputPanel>
</p:tab>
<p:tab title="Edition des photos">
<p:outputPanel autoUpdate="true">
Titre : <p:inputText value="#{archivage.lot.titre}" />
<p:commandButton icon="ui-icon-disk" value="Changer le titre" action="#{archivage.changeTitre()}" />
<p:commandButton icon="ui-icon-trash" value="Supprimer totalement ce lot" action="#{archivage.supprimeLot()}" />
<p:dataTable value="#{archivage.lot.photos}" var="photo" paginator="true" rows="4">
<p:column>
<f:facet name="header">Photo</f:facet>
<a target="_blank" href="rest/#{photo.id}">
<p:graphicImage value="rest/#{photo.id}" width="200" styleClass="ui-corner-all" id="image"/>
</a>
<p:tooltip for="image" value="Cliquez pour avoir un Zoom sur cette photo" showEffect="slide" />
</p:column>
<p:column>
<f:facet name="header">Traitements annexes</f:facet>
<p:inputText value="#{photo.description}" />
<p:commandButton icon="ui-icon-disk" value="Changer la description" action="#{archivage.changeDescription(photo)}" />
<p:commandButton icon="ui-icon-trash" value="Supprimer la photo" action="#{archivage.supprimePhoto(photo)}" />
</p:column>
</p:dataTable>
</p:outputPanel>
</p:tab>
<p:tab title="Ajouter une nouvelle photo">
Description obligatoire de la photo :
<p:inputText value="#{archivage.description}">
<p:ajax event="keyup" />
</p:inputText>
<p:fileUpload fileUploadListener="#{archivage.stockerPhoto}" mode="advanced" label="Choisissez votre photo" uploadLabel="Archiver" cancelLabel="Annuler" />
</p:tab>
</p:tabView>
<f:facet name="footer">
Titre : <p:inputText value="#{archivage.nouveauTitre}" />
<p:commandButton icon="ui-icon-document" value="Création d'un nouveau lot de photos" action="#{archivage.nouveauLot()}" />
</f:facet>
</p:panel>
</h:form>
</h:body>
</html>
package service;
import entité.*;
import java.io.*;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.application.FacesMessage;
import javax.faces.bean.*;
import javax.faces.context.FacesContext;
import org.primefaces.event.FileUploadEvent;
import org.primefaces.event.map.OverlaySelectEvent;
import org.primefaces.model.*;
import org.primefaces.model.map.*;
@ManagedBean
@SessionScoped
public class Archivage {
@EJB
private ServiceREST service;
private LotDePhotos lot;
private List<LotDePhotos> listeDesLots;
private String titre;
private String nouveauTitre;
private MapModel cartographie = new DefaultMapModel();
private String centre;
private Marker marqueur;
private boolean miseAJour;
private String description;
@PostConstruct
private void init() {
File rep = new File(service.getRépertoire());
if (!rep.exists()) rep.mkdir();
listeDesLots = service.getListeDesLots();
if (!listeDesLots.isEmpty()) lot = listeDesLots.get(0);
marqueurs();
}
public boolean isMiseAJour() { return miseAJour; }
public String getTitre() { return titre; }
public void setTitre(String titre) { this.titre = titre; }
public String getNouveauTitre() { return nouveauTitre; }
public void setNouveauTitre(String nouveauTitre) { this.nouveauTitre = nouveauTitre; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public List<LotDePhotos> getListeDesLots() { return listeDesLots; }
public LotDePhotos getLot() { return lot; }
public Photo getPhoto(String id) { return service.recherchePhoto(id); }
public String getCentre() { return centre; }
public MapModel getCartographie() { return cartographie; }
public void changeLot() {
lot = service.rechercheLot(titre);
marqueurs();
miseAJour = true;
}
public void nouveauLot() {
service.créationLotDePhotos(nouveauTitre);
listeDesLots = service.getListeDesLots();
}
public void changeTitre() {
service.modifierTitreLotDePhoto(lot);
listeDesLots = service.getListeDesLots();
}
public void supprimeLot() {
service.supprimeLot(lot.getId());
init();
}
public void changeDescription(Photo photo) { service.modifierDescriptionPhoto(photo); }
public void supprimePhoto(Photo photo) { service.supprimePhoto(lot, photo); }
public void stockerPhoto(FileUploadEvent evt) {
try {
UploadedFile fichier = evt.getFile();
service.stocker(lot.getId(), description, fichier.getContents());
message(description, "Votre photo vient d'être stockée dans le service");
lot = service.rechercheLot(titre);
}
catch (Exception ex) { message("ATTENTION", "Impossible de stocker votre photo dans le service"); }
}
public void choixMarqueur(OverlaySelectEvent event) {
marqueur = (Marker) event.getOverlay();
miseAJour = false;
}
public Marker getMarqueur() { return marqueur; }
private void marqueurs() {
for (Photo photo : lot.getPhotos()) {
if (photo.isExif()) {
LatLng coordonnées = new LatLng(photo.getLatitude(), photo.getLongitude());
marqueur = new Marker(coordonnées, photo.getDescription(), photo.getId());
cartographie.addOverlay(marqueur);
centre = photo.getLatitude()+", "+photo.getLongitude();
}
}
}
private void message(String titre, String détail) {
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(titre, détail));
}
}
package service;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.*;
import com.drew.metadata.exif.GpsDirectory;
import entité.*;
import java.io.*;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
@Path("/")
@Stateless
@LocalBean
public class ServiceREST {
private final String répertoire = "ArchivagePhotos/";
@PersistenceContext
private EntityManager bd;
@POST
@Path("{titre}")
public Response créationLotDePhotos(@PathParam("titre") String titre) {
LotDePhotos lot = new LotDePhotos(titre);
bd.persist(lot);
Query requête = bd.createNamedQuery("recherche");
requête.setParameter("titre", titre);
lot = (LotDePhotos) requête.getSingleResult();
return Response.ok().header("id", lot.getId()).build();
}
@POST
@Path("{id}/description={description}")
@Consumes("image/jpeg")
public void stocker(@PathParam("id") long id, @PathParam("description") String description, InputStream flux) throws Exception {
byte[] octets = lireOctets(flux);
stocker(id, description, octets);
}
@PUT
@Path("{id}/titre={titre}")
public void changeTitre(@PathParam("id") long id, @PathParam("titre") String titre) {
LotDePhotos lot = bd.find(LotDePhotos.class, id);
lot.setTitre(titre);
bd.merge(lot);
}
@GET
@Path("{nomFichier}")
@Produces("image/jpeg")
public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
return new FileInputStream(répertoire+nom);
}
@GET
@Produces("application/json")
public List<LotDePhotos> getListeDesLots() {
return bd.createNamedQuery("toutLesLots").getResultList();
}
@DELETE
@Path("{id}")
public void supprimeLot(@PathParam("id") long id) {
LotDePhotos lot = bd.find(LotDePhotos.class, id);
for (Photo photo : lot.getPhotos()) new File(répertoire+photo.getId()).delete();
bd.remove(lot);
}
public LotDePhotos rechercheLot(String titre) {
Query requête = bd.createNamedQuery("recherche");
requête.setParameter("titre", titre);
return (LotDePhotos) requête.getSingleResult();
}
public Photo recherchePhoto(String id) { return (Photo) bd.find(Photo.class, id); }
public void modifierTitreLotDePhoto(LotDePhotos lot) { bd.merge(lot); }
public void modifierDescriptionPhoto(Photo photo) { bd.merge(photo); }
public void supprimePhoto(LotDePhotos lot, Photo photo) {
lot.getPhotos().remove(photo);
bd.merge(lot);
bd.remove(recherchePhoto(photo.getId()));
new File(répertoire+photo.getId()).delete();
}
public String getRépertoire() { return répertoire; }
public void stocker(long id, String description, byte[] octets) throws Exception {
Photo photo = new Photo(description);
FileOutputStream fichier = new FileOutputStream(répertoire+photo.getId());
fichier.write(octets);
fichier.close();
exif(répertoire, photo);
LotDePhotos lot = bd.find(LotDePhotos.class, id);
lot.ajoutPhoto(photo);
bd.merge(lot);
}
private byte[] lireOctets(InputStream stream) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; int octetsLus = 0;
do {
octetsLus = stream.read(buffer);
if (octetsLus > 0) { baos.write(buffer, 0, octetsLus); }
}
while (octetsLus > -1);
return baos.toByteArray();
}
private void exif(String répertoire, Photo photo) throws Exception {
Metadata metadata = ImageMetadataReader.readMetadata(new File(répertoire+photo.getId()));
Directory exif = metadata.getDirectory(GpsDirectory.class);
boolean EXIF;
try {
exif.getDescription(GpsDirectory.TAG_GPS_LATITUDE);
EXIF = true;
}
catch (Exception ex) { EXIF = false; }
photo.setExif(EXIF);
if (photo.isExif()) {
photo.setLatitude(toDouble(exif.getDescription(GpsDirectory.TAG_GPS_LATITUDE)));
photo.setLongitude(toDouble(exif.getDescription(GpsDirectory.TAG_GPS_LONGITUDE)));
photo.setJour(date(exif.getDescription(GpsDirectory.TAG_GPS_DATE_STAMP)));
}
}
private double toDouble(String localisation) {
String[] degrés = localisation.split("°");
double valeur = Integer.parseInt(degrés[0]);
String[] minutes = degrés[1].split("'");
valeur += Integer.parseInt(minutes[0]) / (double)60;
String[] secondes = minutes[1].split("\"");
valeur += Double.parseDouble(secondes[0]) / 3600;
return valeur;
}
private Date date(String date) {
String[] séparation = date.split(":");
int année = Integer.parseInt(séparation[0]);
int mois = Integer.parseInt(séparation[1]);
int jour = Integer.parseInt(séparation[2]);
Calendar calendrier = new GregorianCalendar(année, mois-1, jour);
return calendrier.getTime();
}
}
package entité;
import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
@Entity
@NamedQueries({
@NamedQuery(name="recherche", query="SELECT lot FROM LotDePhotos lot WHERE lot.titre = :titre"),
@NamedQuery(name="toutLesLots", query="SELECT lot FROM LotDePhotos lot")
})
public class LotDePhotos implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String titre;
@JoinColumn
@OneToMany(fetch= FetchType.EAGER, cascade= CascadeType.ALL)
private List<Photo> photos = new ArrayList<Photo>();
public LotDePhotos() { }
public LotDePhotos(String titre) { this.titre = titre; }
public long getId() { return id; }
public String getTitre() { return titre; }
public void setTitre(String titre) { this.titre = titre; }
public List<Photo> getPhotos() { return photos; }
public void ajoutPhoto(Photo photo) { photos.add(photo); }
@Override
public String toString() { return titre; }
}
package entité;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
@Entity
public class Photo implements Serializable {
@Id
private String id;
private String description;
private boolean exif;
private double longitude;
private double latitude;
@Temporal(javax.persistence.TemporalType.DATE)
private Date jour;
public Photo() { id = System.currentTimeMillis()+".jpg"; }
public Photo(String description) {
id = System.currentTimeMillis()+".jpg";
this.description = description;
}
public String getId() { return id; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Date getJour() { return jour; }
public void setJour(Date jour) { this.jour = jour; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public boolean isExif() { return exif; }
public void setExif(boolean localisation) { this.exif = localisation; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
@Override
public String toString() { return description; }
}
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.
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.
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.
La sécurisation d'une application implique deux fonctions : l'authentification et l'habilitation.
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.
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.
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 :
security policyou sécurité de domaine, permet de définir la politique de sécurité pour l'accès aux ressources.
$ asadmin list-file-users
Java Key Storeet 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 Storepeuvent être gérés en utilisant l'outil Java SE
keytool
. Basic Authorization. Ce mécanisme consiste à utiliser un nom utilisateur/identifiant et un mot de passe en clair.
Digest Authet utilise l'algorithme de hachage unilatéral SHA ou MD5.
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 :
identifiant / mot de passe. Les protocoles supportés sont HTTP et HTTPS.
identifiant / mot de passepersonnalisable et développé en XHTML, supportant les protocoles HTTP et HTTPS.
un certificat à clé publique. La communication est effectuée au travers des protocoles HTTP et HTTPS.
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.
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.
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.
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();
}
}
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 :
<realm-name />
du fichier de configuration WEB-INF/web.xml permet de
spécifier quel realm doit être utilisé pour authentifier l'utilisateur. <role-name />
dans le
descripteur de déploiement (ou déclarés dans le code avec l'annotation @DeclareRoles que nous exploiterons ultérieurement).Secure Socket LayerSSL et son successeur
Transport Level SecurityTSL.
<security-role-mapping>
.
<?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>
<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>
.<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>
. <?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>
<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.<security-role>
à
l'intérieur de laquelle va se trouver l'ensemble des balises <role-name>
.<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.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);
}
<?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>
<?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>
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();
}
}
<?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>
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);
}
}
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.