CI8 : Mini-Allocine
L'objectif de cet exercice est de vous faire manipuler la bibliothèque java.util
et les exceptions.
La partie entraînement de cet exercice a aussi pour but de vous montrer comment fonctionne un petit serveur Web,
ce qui vous sera utile pour le module CSC4101.
Durée des exercices obligatoires : 2h en présentiel
Dans cet exercice, nous concevons un petit serveur Web fournissant des informations
cinématographiques.
Réaliser un site complet est bien entendu irréaliste dans le cadre d'un TP.
Pour cette raison, nous ne nous occupons que de deux fonctionnalités :
celle permettant d'accéder à la liste des films gérés par le site et celle
permettant de lire les avis que des utilisateurs ont déposé sur un film.
De façon à minimiser le travail à réaliser, nous ne nous préoccuperons pas non
plus de l'aspect esthétique ou ergonomique de notre site Web.
Nous ne nous focalisons que sur les structures de données des films et
sur les mécanismes permettant à un serveur Web de générer des pages Web dynamiquement.
La gestion des films est obligatoire et la construction du serveur Web
est optionnelle. Cette partie optionnelle est longue (2h de travail), mais
très instructive. Nous invitons les étudiants qui souhaitent se diriger vers
des approfondissements en informatique à la faire.
- Exercice 1 : obligatoire (facile, ∼30mn)
- Exercice 2 : obligatoire (facile, ∼40mn)
- Exercice 3 : obligatoire (moyen, ∼50mn)
- Exercice 4 : entraînement (moyen, ∼30mn)
- Exercice 5 : entraînement (moyen, ∼20mn)
- Exercice 6 : entraînement (moyen, ∼70mn)
Les recommandations (∼ 30mn – facile – obligatoire)
Dans ce premier exercice, nous concevons la classe Recommendations.
Une instance de cette classe représente les recommandations associées à un film.
Une recommandation étant une simple chaîne de caractères donnant l'avis d'un utilisateur sur
un film, la classe Recommendations ne possède qu'un unique champ : une collection
de chaînes de caractères.
Commencez par créer un projet nommé webserver.
Dans le package tsp.webserver, créez une classe Main contenant la méthode
main.
Ajoutez une classe Recommendations au package tsp.webserver.
Cette classe ne doit contenir qu'un unique champ privé de type Collection<String> nommé
recommendations.
Dans un constructeur sans argument,
choisissez la classe mettant en œuvre une collection qui vous semble
la plus adéquate et allouez-en une instance que vous référencerez à partir de
recommendations.
Ajoutez une méthode addRecommendation(String recommendation) à la classe Recommendations
permettant d'ajouter une recommandation à la collection de recommandations.
public void addRecommendation(String recommendation) {
recommendations.add(recommendation);
}
Ajoutez une méthode toString() à Recommendations
renvoyant une chaîne de caractères contenant l'ensemble des recommandations.
Pensez que vous pouvez ajouter un caractère de passage à la ligne de façon à mettre une recommandation
par ligne avec la méthode statique
System.lineSeparator().
public String toString() {
String res = "";
for(String recommendation: recommendations)
res += recommendation + System.lineSeparator();
return res;
}
Dans votre méthode main, créez une instance de la classe Recommendations,
ajoutez-y trois recommandations quelconques et affichez-les sur le terminal en utilisant
Recommendations.toString().
Les films (∼40mn – facile – obligatoire)
Dans cette question, nous écrivons une classe représentant les films.
Nous ne stockons pas directement les recommandations dans les films, nous utiliserons
une table de hachage dans le prochain exercice pour cela.
Un film possède uniquement deux champs : un titre (une chaîne de caractères) et
une année de production (un entier) de façon à traiter le problème des remakes et des collisions
de noms.
Créez une classe Film possédant les deux champs title et year.
Ajoutez un constructeur permettant d'initialiser ces paramètres et
une méthode toString() renvoyant la chaîne de caractères
titre (année), dans laquelle titre est le titre du film et
année son année de production.
Vérifiez que votre code est correct en créant le film Black Sheep produit
en 2008 dans votre méthode main et en l'affichant sur le terminal en utilisant
la méthode toString().
Nos films vont être utilisés dans la suite comme clé de hachage. Il faut écrire
des méthodes boolean equals(Object o) et int hashCode()
car ces méthodes sont utilisées par la classe
java.util.HashMap pour respectivement savoir si deux clés sont
égales et pour connaître le code de hachage associé à un film.
Pour commencer, écrivez la méthode equals.
Vérifiez :
- que les films Black Sheep produit en 2008 et Black Sheep produit en 2008 sont bien égaux,
- que les films Evil Dead produit en 1981 et Black Sheep produit en 2008 sont bien différents,
- que les films Evil Dead produit en 1981 et Evil Dead produit en 2013 sont bien différents,
- et que le film Black Sheep produit en 2008 est bien différent de la chaîne de caractères Black Sheep.
Dans la méthode equals, vous
devez :
- vérifier que le paramètre est bien de type Film en utilisant instanceof,
- sous-typer le paramètre en Film,
- vérifier que le titre du film paramètre est bien égal au titre de this en utilisant la méthode java.lang.String.equals(Object o),
- et enfin vérifier que les années de production sont bien les mêmes.
public boolean equals(Object o) {
if(!(o instanceof Film))
return false;
Film film = (Film)o;
return title.equals(film.title) && year == film.year;
}
Nous pouvons maintenant mettre en œuvre la méthode hashCode.
Comme hashCode d'un film, nous vous suggérons de simplement
renvoyer le résultat de la somme de l'année de production et
du résultat de l'appel à hashCode() sur le titre du film, car la classe
String fournit déjà une méthode
String.hashCode() qui minimise les collisions de code de hachage.
public int hashCode() {
return title.hashCode() + year;
}
La base de données des films (∼50mn – moyen – obligatoire)
Nous créons maintenant la base de données associant des films et des recommandations.
Ajoutez à la classe FilmDB une méthode void create(String title, int year)
permettant de créer un film dans la base de données.
Pour cela, créez un film à partir des paramètres, créez une instance de Recommendations,
et associez le film à cette instance.
public void create(String title, int year) {
Film film = new Film(title, year);
db.put(film, new Recommendations());
}
Lors d'un appel à create,
on souhaite lever une exception de type FilmAlreadyExistsException si
un film existe déjà.
À cette question, on vous demande donc de créer la nouvelle classe
d'exception FilmAlreadyExistsException
(on vous rappelle qu'une exception est simplement une classe héritant de Exception).
Ajoutez à FilmAlreadyExistsException un constructeur prenant en argument une chaîne de caractères.
Utilisez ensuite super pour transmettre ce paramètre au constructeur de Exception.
Lors de la création de la classe sur Eclipse, décochez les cases Constructor from super class et
inherited abstract methods si ce n'est pas déjà fait.
Modifiez la méthode create de façon à lever (mot clé throw) une exception de type
FilmAlreadyExistsException si le film existe déjà.
On vous demande de ne pas attraper cette exception dans votre programme, y compris dans
la méthode main : il faut donc ajouter cette exception à la liste
des exceptions levées par create et main en utilisant
le mot throws.
Vérifiez que votre application termine bien sur cette exception si vous essayez
d'ajouter deux fois le film Black Sheep produit en 2008 à partir de la
méthode main.
public void create(String title, int year) throws FilmAlreadyExistsException {
Film film = new Film(title, year);
if(!db.containsKey(film))
throw new FilmAlreadyExistsException(film + " already exists");
else
db.put(film, new Recommendations());
}
Commencez par supprimer tous les tests de votre méthode main. À la place,
copiez-collez le code suivant pour créer quelques films dans votre base de données :
FilmDB db = new FilmDB();
db.create("Evil Dead", 1981);
db.create("Evil Dead", 2013);
db.create("Fanfan la Tulipe", 1952);
db.create("Fanfan la Tulipe", 2003);
db.create("Mary a tout prix", 1998);
db.create("Black Sheep", 2008);
Dans la classe FilmDB, ajoutez une méthode toString() renvoyant
la concaténation de tous les films se trouvant dans la base de données (sans les recommandations).
Pensez à séparer les films par un retour à la ligne en utilisant
System.lineSeparator().
Vous pouvez itérer sur les clés après avoir appelé
Map.keySet() qui vous renvoie une collection contenant l'ensemble
des clés.
public String toString() {
String res = "";
for(Film film: db.keySet())
res += film + System.lineSeparator();
return res;
}
De façon à ajouter une recommandation à un film, il faut d'abord le retrouver.
Ajoutez une méthode Recommendations lookup(String title, int year)
renvoyant l'instance de Recommendations associée au film
ayant pour titre title et pour année de production year
dans la base de données.
Si le film n'existe pas, votre méthode doit lever une exception de type
FilmDoesNotExistsException qu'on vous demande de ne pas attraper
dans votre application.
Il faut bien sûr aussi créer la classe FilmDoesNotExistsException à cette question.
Comme pour FilmAlreadyExistsException, on vous demande d'ajouter un constructeur
prenant en paramètre une chaîne de caractères et transmettant cette chaîne au constructeur
de Exception.
Vérifiez que votre code est correct en essayant de retrouver les recommandations associées
(i) au film Black Sheep produit en 2008 (que vous devriez donc pouvoir afficher) et (ii) au film Star Wars
produit en 1977 (ce qui devrait lever une exception).
public Recommendations lookup(String title, int year) throws FilmDoesNotExistsException {
Film film = new Film(title, year);
if(!db.containsKey(film))
throw new FilmDoesNotExistsException(film + " does not exists");
else
return res;
}
Supprimez le test précédent et ajoutez les recommandations suivantes :
db.lookup("Evil Dead", 1981).addRecommendation("Ouh ! Mais ca fait peur !");
db.lookup("Evil Dead", 2013).addRecommendation("Meme pas peur !");
db.lookup("Evil Dead", 2013).addRecommendation("Insipide et sans saveur");
db.lookup("Fanfan la Tulipe", 1952).addRecommendation("Manque de couleurs");
db.lookup("Fanfan la Tulipe", 1952).addRecommendation("Supers scenes de combat");
db.lookup("Fanfan la Tulipe", 2003).addRecommendation("Mais pourquoi ???");
db.lookup("Mary a tout prix", 1998).addRecommendation("Le meilleur film de tous les temps");
db.lookup("Black Sheep", 2008).addRecommendation("Un scenario de genie");
db.lookup("Black Sheep", 2008).addRecommendation("Une realisation parfaite");
db.lookup("Black Sheep", 2008).addRecommendation("A quand Black Goat ?");
Félicitations ! Vous êtes maintenant capable de construire rapidement des applications
complexes en Java !
La connexion au serveur Web (∼30mn – moyen – entraînement)
Maintenant que notre base de données est en place, nous pouvons la transformer en serveur Web.
Nous réalisons ce serveur en plusieurs étapes. Dans cet exercice, nous ne nous occupons
que d'apprendre à recevoir des connexions à partir de notre serveur Web.
Pour cela, nous utilisons le protocole TCP/IP. Sans trop rentrer dans les détails,
le protocole TCP/IP commence par définir une notion d'adresse IP permettant de joindre
une machine sur internet.
Cette adresse est une sorte de numéro de téléphone de machine.
De façon à pouvoir exposer plusieurs services à partir de la même machine, le protocole
TCP/IP définit aussi une notion de port : à chaque numéro de port est attaché un
service (un processus) d'une machine.
Typiquement, un serveur Web est joignable sur le port numéro 80, alors
qu'un serveur ssh est joignable sur le port 22.
Pour illustrer, la machine www.telecom-sudparis.eu possède l'adresse IP
157.159.11.8. Sur cette machine, un serveur Web (le processus apache) attend des connexions
sur le port Web par défaut, donc 80. Pour joindre le serveur Web, il suffit donc
d'envoyer des messages sur le port 80 de la machine 157.159.11.8.
Le protocole TCP/IP est un protocole dit connecté, c'est-à-dire qu'une communication
est séparée en deux phases.
- Une phase de connexion pendant laquelle un client se connecte à un serveur. Une fois cette connexion établie, on a créé un canal de communication entre le client et le serveur. Ce canal de communication est une sorte de tube généralisé, un peu comme ceux que vous avez manipulés dans le module CSC3102, mais permettant à deux processus de communiquer via un réseau (au lieu de communiquer localement via un système de fichier comme avec les tubes nommés UNIX).
- Une phase de communication pendant laquelle le client et le serveur peuvent lire ou écrire des données dans le canal de communication.
Après avoir créé la base de données dans la méthode main,
créez une instance de
java.net.ServerSocket associée au port 4123 dans votre main.
Ensuite, dans une boucle infinie (while(true) { ... }) :
- appelez accept() sur votre socket serveur (cette méthode met le socket de connexion en attente de connexions provenant de clients et renvoie le socket de communication créé lors de la connexion d'un client),
- stockez la référence vers le socket de communication renvoyée par accept dans une variable nommée com,
- affichez un message sur le terminal,
- puis appelez com.close()pour fermer le socket.
Pensez à interrompre votre serveur après avoir effectué vos tests (en appuyant sur le carré rouge
d'Eclipse au-dessus de la fenêtre terminal) !
Si par hasard vous oubliez votre serveur et en relancez un, le port qu'utilise votre vieux serveur reste
utilisé et vous ne pouvez donc plus y attacher votre nouveau serveur.
Lorsque vous relancez votre serveur, vous verrez
alors une exception indiquant que le port est déjà utilisé.
Si un tel problème vous arrive à cette question
ou dans la suite de l'exercice, il vous suffit de changer le numéro de port du serveur
(utilisez ainsi 4124, puis 4125, etc.).
Pensez aussi à adapter l'énoncé pour que les tests utilisent votre nouveau port.
int port = 4123;
ServerSocket server = new ServerSocket(port);
System.out.println("Start with port: " + port);
while(true) {
Socket com = server.accept();
System.out.println("Connexion accepted");
com.close()
}
De façon à traiter une requête, nous allouons une instance d'une classe Server
que nous concevons à cette question.
Créez une classe Server avec un constructeur prenant en paramètre
la base de données des films (FilmDB) et le socket de communication
renvoyée par accept().
Ajoutez deux champs à la classe Server pour stocker ces paramètres
et un constructeur prenant deux paramètres et permettant d'initialiser ces champs.
Ensuite, ajoutez une méthode void handle() à la classe
Server. Cette méthode doit afficher un message sur le terminal
et fermer le socket de communication avec close().
Enfin, supprimez le code se trouvant dans la boucle infinie de votre
main.
À la place, allouez une instance de Server en lui transmettant le résultat
de accept() comme paramètre puis appelez handle().
Vérifiez que votre programme se comporte de façon similaire à la question précédente
en utilisant le navigateur.
La méthode close de Socket peut lever une exception de type
java.io.IOException.
Pour cette raison, ajoutez throws IOException aux définitions
de Server.handle et Main.main.
L'exception java.io.Exception est levée lorsqu'il y a un problème de communication
avec le client, typiquement s'il se déconnecte intempestivement.
De façon à éviter de quitter le serveur lorsqu'un client a un comportement inattendu,
nous attrapons les exceptions de type IOException à l'intérieur de la boucle
principale du serveur (le but étant que cette boucle ne soit pas interrompue).
Lorsque vous attrapez l'exception de type IOException, affichez le message
associé à l'exception accompagné de la pile d'exécution ayant mené à l'exception
sur le terminal.
Modifiez votre programme en conséquence.
while(true) {
try {
new Server(db, server.accept()).handle();
} catch(IOException e) {
System.out.println(e.getMessage());
e.printStackTrace();
server.close();
}
}
La communication (∼20mn – moyen – entrainement)
Maintenant que nous sommes capables de créer un canal de communication entre
un navigateur Web et notre serveur, nous pouvons utiliser le socket pour
communiquer.
Un navigateur Web communique en utilisant le protocole HTTP au-dessus de TCP/IP.
Le navigateur envoie, via le canal de communication entre les sockets de communication,
une requête (une chaîne de caractères)
GET res HTTP/1.1 pour accéder à la ressource res.
Cette ressource est typiquement le chemin vers une page Web.
Le serveur, lorsqu'il reçoit une requête GET res HTTP/1.1 cherche la ressource
res puis renvoie les données suivantes :
La ligne HTTP/1.1 200 OK indique que la requête a pu être satisfaite.
Il faut faire suivre cette ligne d'une ligne vide puis du contenu de la ressource demandée
(la page Web).
Avant de pouvoir envoyer les chaînes de caractères du protocole HTTP, il faut
être capable de lire et écrire des données dans un socket.
En Java un socket ne permet pas de communiquer directement, mais un socket
fournit deux méthodes renvoyant des flux associés aux sockets. Ces flux peuvent être vus
comme les flux que vous avez manipulés en CSC3102 (par exemple, souvenez vous que exec 3<mon-tube
créé un nouveau flux et lui associe le numéro 3).
En Java, un flux en lecture est représenté par une instance de java.net.InputStream
et un flux en écriture par une instance de java.net.OutputStream.
On peut récupérer ces flux avec socket.getInputStream()
et socket.getOutputStream().
Ces flux permettent d'envoyer et recevoir des octets bruts,
ce qui n'est pas très confortable pour envoyer ou recevoir des chaînes de caractères.
Pour cette raison, nous utilisons aussi des classes annexes
offertes par la bibliothèque Java permettant d'envoyer et recevoir des chaînes de caractères.
HTTP/1.1 200 OK
contenu de la ressource res
ce contenu peut bien sûr faire plusieurs lignes
Pour pouvoir lire des données à partir du flux associé à notre socket de communication,
nous utilisons la classe
java.io.BufferedReader.
Cette classe offre principalement une méthode
readLine() permettant de lire une ligne à partir d'un flux.
La méthode readLine se comporte donc un peu comme
la commande read que vous avez vu en CSC3102
(souvenez vous que, en bash, read x <&3 permet de lire
une ligne à partir du flux 3 préalablement
ouvert avec exec 3<pipe-name).
Pour utiliser le
BufferedReader, il faut :
- Ajouter un champ reader de type java.io.BufferedReader à la classe Server.
- Initialiser ce champ à la valeur new BufferedReader(new InputStreamReader(socket.getInputStream())) dans le constructeur de Server, où socket est le socket de communication passée en paramètre du constructeur.
public void handle() throws IOException {
Lit une ligne à partir du socket et affiche la *** nouveau code à cette question
Ferme le socket de communication
}
Pour les utilisateurs de chrome (et peut-être d'autres navigateurs), vous devriez remarquer que vous recevez
aussi une requête pour la ressource /favicon.ico.
Cette demande est tout à fait normale, vous pouvez l'ignorer dans la suite de l'exercice.
Pour pouvoir écrire des données dans le flux associé à notre socket de communication,
nous utilisons la classe
java.io.PrintStream.
Cette classe offre principalement une méthode println permettant
d'envoyer une chaîne de caractères sur le flux.
System.out est d'ailleurs un PrintStream, mais
qui a été connecté au flux associé au terminal d'attache de la machine virtuelle Java.
Pour utiliser le
java.io.PrintStream, il faut :
Cette réponse signifie que la requête a été correctement exécutée et que
le contenu de la ressource demandée est "coucou".
C'est ce que devrait afficher votre navigateur Web si votre code est correct.
À cette étape, le code de handle() devrait donc être similaire à celui-ci :
- ajouter un champ out de type java.io.PrintStream à la classe Server.
- initialiser dans le constructeur de Server ce champ à la valeur new PrintStream(socket.getOutputStream())
HTTP/1.1 200 OK
coucou
public void handle() throws IOException {
Lit une ligne à partir du socket et affiche la
Envoi HTTP/1.1 200 OK au navigateur *** nouveau code à cette question
Envoi une ligne blanche au navigateur *** nouveau code à cette question
Envoi coucou au navigateur *** nouveau code à cette question
Ferme le socket de communication
}
Félicitations, vous êtes maintenant capables d'écrire des applications réparties en Java !
Le serveur de films (∼1h10 – moyen – entraînement)
Maintenant que l'infrastructure de communication entre
notre serveur et un navigateur Web est en place, nous pouvons
construire notre serveur de films.
Pour cela, nous envoyons des pages Web au navigateur.
Nous avons donc besoin de générer du code HTML puisqu'une page Web est un fichier formaté
avec le langage HTML. Apprendre HTML en quelques heures est bien sûr irréaliste.
Pour réaliser le TP, vous avez besoin de savoir que dans un code HTML :
- le code HTML se trouve entre les balises <html> et </html>,
- le contenu de la page HTML se trouve entre les balises <body> et </body>,
- le texte <a href='url'>TXT</a> permet de générer un lien hypertexte ayant pour texte TXT et référençant l'URL url,
- on peut afficher une liste avec le code suivant :
<ul> <li>premier élément de la liste</li> <li>second élément de la liste</li> ... </ul>
- Si la ressource demandée est /, notre serveur envoie la liste des films au navigateur (c'est la requête générée par défaut par un navigateur lorsque vous essayez de vous connecter à l'URL localhost:4123).
- Si la ressource demandée est /titre-film/année, notre serveur cherche les recommandations associées au film ayant pour titre titre-film et année de production année, puis renvoie cette liste de recommandations au navigateur.
Avant d'envoyer des réponses plus précises, nous nous focalisons sur l'envoi
d'un lien hypertexte forgé à partir d'un film.
Un lien hypertexte est un texte qui, lorsqu'il est cliqué par l'utilisateur,
génère une nouvelle demande de ressource au serveur.
Par exemple, si un texte HTML contient le lien <a href='/bip'>coucou<a>,
le navigateur affiche coucou et, si l'utilisateur clique sur ce texte,
le navigateur envoie une requête GET /bip HTTP/1.1 au serveur.
De façon à gérer une requête de demandes d'affichage des recommandations d'un film,
nous devons générer des liens hypertextes de ce type :
Dans cette chaîne de caractères titre-film est une version
sans espace d'un titre de film. En effet, HTML ne permet pas de gérer les espaces dans
les liens hypertextes.
Techniquement, le protocole HTTP spécifie que les espaces doivent être remplacés
par la chaîne de caractères « %20 ».
Pour effectuer ce remplacement, vous pouvez appeler
title.replace(" ", "%20"), où title est le titre du film.
Ajoutez une méthode String asHTML() dans la classe Film.
Cette méthode doit renvoyer un lien hypertexte associé au film, en utilisant
title.replace pour remplacer les espaces par des %20.
Après avoir écrit cette méthode, modifiez le code de handle
de façon à envoyer une page Web ne contenant que le lien
généré par un appel à (new Film("Black Sheep", 2008)).asHTML().
Pour cela, remplacez l'envoi de "coucou" par l'envoi de la page Web.
Techniquement, le code de handle() devrait être similaire à celui-ci :
Testez votre serveur en :
<a href='/titre-film/année'>titre (année)</a>
public void handle() throws IOException {
Lit une ligne à partir du socket et affiche la
Envoi HTTP/1.1 200 OK au navigateur
Envoi une ligne blanche au navigateur
Envoi <html><body> au navigateur **** nouveau code
Envoi le lien hypertexte associé à Black Sheep/2008 **** nouveau code
Envoi </body><html> **** nouveau code
Ferme le socket de communication
}
- saisissant l'URL localhost:4123 dans votre navigateur,
- vérifiant que le texte affiché dans le navigateur est bien Black Sheep (2008),
- vérifiant que le serveur affiche bien GET /Black%20Sheep/2008 HTTP/1.1 lorsque vous cliquez sur le lien hypertexte Black Sheep (2008)
Félicitations, vous venez de réaliser la partie la plus difficile !
public String asHTML() {
return "<a href=/" + title.replace(" ", "%20") + "/" + year + ">"
+ title + " " + year
+ "</a>";
}
public void handle() throws IOException {
System.out.println(reader.readLine());
out.println("HTTP/1.1 200 OK");
out.println("");
out.println("<html><body>");
out.println((new Film("Black Sheep", 2008)).asHTML());
out.println("</body></html>");
socket.close();
}
Avant d'aller plus loin, il faut maintenant être capable de savoir quelle
est la requête générée par le navigateur Web : une requête pour
consulter la liste des films ou une requête pour consulter les recommandations
associées à un film. Nous devons donc décomposer une requête HTTP.
Au début de handle, au lieu d'afficher le résultat
de l'appel à reader.readLine() dans le terminal,
stockez ce résultat dans une variable nommée method.
Ensuite, il faut décomposer la requête, c'est-à-dire isoler les mots dans method.
Pour cela, vous pouvez appeler
String.split(" ") de façon à séparer les mots de la requête.
Cette méthode renvoie un tableau de chaîne de caractères, dans lequel chaque entrée
référence l'un des mots de la requête.
Stockez la chaîne de caractères se trouvant à l'indice 1 de votre tableau
dans une variable que vous appellerez location et
affichez cette variable dans le terminal.
À cette étape, votre code devrait donc être similaire à celui-ci :
Vérifiez que :
public void handle() throws IOException {
Retrouve location *** nouveau code
Affiche location *** nouveau code
Envoi HTTP/1.1 200 OK au navigateur
Envoi une ligne blanche au navigateur
Envoi <html><body> au navigateur
Envoi le lien hypertexte associé à Black Sheep/2008
Envoi </body><html>
Ferme le socket de communication
}
- Lorsque vous saisissez l'URL localhost:4123, location vaut bien /,
- Lorsque vous cliquez sur Black Sheep (2008), location vaut bien /Black%20Sheep/2008.
Avec chrome, vous recevez probablement aussi une demande pour la ressource /favicon.ico.
public void handle() throws IOException {
String method = reader.readLine();
String[] cmd = method.split(" ");
String location = cmd[1];
System.out.println(location);
out.println("HTTP/1.1 200 OK");
out.println("");
out.println("<html><body>");
out.println((new Film("Black Sheep", 2008)).asHTML());
out.println("</body></html>");
socket.close();
}
Nous pouvons maintenant commencer à traiter les différentes requêtes.
Notre serveur doit gérer trois types de requête :
Si vous saisissez l'URL localhost:4123 dans votre navigateur et cliquez
ensuite sur le film Black Sheep, vous devriez alors avoir cet affichage dans le terminal :
- si location est égal à la chaîne de caractères « / », le serveur doit envoyer la liste des films au navigateur,
- si le motif de location est /titre-film/année, le serveur doit chercher le film. Si le film existe, il doit renvoyer les recommandations associées au film, sinon, il doit renvoyer un message expliquant l'erreur au navigateur.
- dans tous les autres cas, le serveur doit renvoyer un message d'erreur adéquat au navigateur. C'est en particulier le cas pour la ressource /favicon.ico que chrome demande afin de pouvoir associer une icône personnalisée à la page.
- transforme les « %20 » en espace dans location en utilisant String.replace,
- décompose la chaîne de caractères location en utilisant le séparateur « / » de façon à pouvoir interpréter la requête. Pour cela, il faut de nouveau utiliser String.split("/").
public void handle() throws IOException {
Retrouve location
Remplace %20 par espace dans location **** nouveau code
Met dans tokens le résultat de l'appel à location.split("/"); **** nouveau code
Nouvel affichage de location **** nouveau code
Envoi HTTP/1.1 200 OK au navigateur
Envoi une ligne blanche au navigateur
Envoi <html><body> au navigateur
Envoi le lien hypertexte associé à Black Sheep/2008
Envoi </body><html>
Ferme le socket de communication
}
Request / => 0 tokens
Request /favicon.ico => 2 tokens
Request /Black Sheep/2008 => 3 tokens
Request /favicon.ico => 2 tokens
public void handle() throws IOException {
String method = reader.readLine();
String[] cmd = method.split(" ");
String location = cmd[1];
location = location.replace("%20", " ");
String[] tokens = location.split("/", 0);
System.out.println("Request " + location + " => " + tokens.length + " tokens");
//...
}
Nous gérons maintenant les erreurs. Supprimez l'envoi du lien hypertexte associé
à Black Sheep car ce test ne nous servira plus.
À la place, si le nombre d'éléments dans tokens est différent de 0
(demande de la liste des films) et de 3 (demande les recommandations associées à un film),
on vous demande de lever une exception BadRequestException que vous devez créer à cette question.
Ensuite, entourez ce test d'un bloc try/catch et attrapez cette exception.
Lorsque vous attrapez cette exception, envoyez le résultat de l'appel à getMessage
au navigateur.
À cette étape, le code de handle devrait donc ressembler à celui-ci :
Dans votre navigateur, saisissez l'URL localhost:4123/coucou et vérifiez
que le navigateur vous affiche bien un message d'erreur.
public void handle() throws IOException {
Retrouve location
Remplace %20 par espace dans location
Met dans tokens le résultat de l'appel à location.split("/");
Nouvel affichage de location
Envoi HTTP/1.1 200 OK au navigateur
Envoi une ligne blanche au navigateur
Envoi <html><body> au navigateur
try { **** nouveau code
Si tokens.length != 0 et tokens.length != 3 **** nouveau code
lève une exception BadRequestException **** nouveau code
} catch(BadRequestException e) { **** nouveau code
Envoi e.getMessage() au navigateur **** nouveau code
} **** nouveau code
Envoi </body><html>
Ferme le socket de communication
}
public void handle() throws IOException {
String method = reader.readLine();
String[] cmd = method.split(" ");
String location = cmd[1];
location = location.replace("%20", " ");
String[] tokens = location.split("/", 0);
System.out.println("Request " + location + " => " + tokens.length + " tokens");
try {
if (tokens.length != 0 && tokens.length != 3) {
throw new BadRequestException("bad request: " + location);
}
} catch(BadRequestException e) {
out.println(e.getMessage());
}
out.println("</body></html>");
socket.close();
}
Nous traitons maintenant le premier type de requête, à savoir l'envoi
de la liste des films lorsque le tokens.length est égal
à 0.
Modifiez le code se trouvant dans le bloc try/catch
de façon à renvoyer cette liste si tokens.length est égal à 0.
Pour cela, ajoutez une méthode String asHTML() à FilmDB
renvoyant la liste des liens hypertextes des films.
Comme pour toString(), la méthode asHTML() doit itérer sur les clés
de façon à produire la chaîne de caractères :
Liste des films :
<ul>
<li>lien-film-1</li>
<li>lien-film-2</li>
...
</ul>
Les espaces et retours à la ligne ne sont pas significatifs.
Pour tester que votre code est correct, vous pouvez :
- lancer le serveur
- connecter votre navigateur au serveur avec l'URL localhost:4123,
- cliquer sur un des films dans le navigateur,
- vérifier que l'URL que vous voyez dans votre navigateur correspond bien à un lien vers le film.
public String asHTML() {
String res = "Liste des films :<ul>";
for(Film film: db.keySet()) {
res += "<li>" + film.asHTML() + "</li>";
}
return res + "</ul>";
}
public void handle() throws IOException {
//...
try {
if (tokens.length == 0) {
out.println(db.asHTML());
} else if (tokens.length != 3) {
throw new BadRequestException("bad request: " + location);
}
} catch(BadRequestException e) {
out.println(e.getMessage());
}
//...
}
Nous pouvons maintenant traiter le dernier type de requêtes dans le serveur,
à savoir l'envoi des recommandations d'un film.
En utilisant les méthodes
String.split,
String.replace et
Integer.parseInt, extrayez le nom et l'année de production du film
à partir de la variable location lorsque tokens.length est égal à 3.
Ensuite, utilisez la méthode FilmDB.lookup pour retrouver les recommandations
associées au film.
Cet appel pouvant générer une exception de type FilmDoesNotExistsException,
il faut l'attraper dans le bloc catch de handle()
de façon à renvoyer un message adéquat au navigateur.
Enfin, renvoyez les recommandations au navigateur.
Pour vous guider, le texte HTML que vous devriez renvoyer est le suivant :
Recommandations pour le film LIEN-FILM :
<ul>
<li>recommendation-1</li>
<li>recommendation-2</li>
</ul>
<a href='/'>Back</a>
public void handle() throws IOException {
//...
try {
if (tokens.length == 0) {
out.println(db.asHTML());
} else if (tokens.length == 3){
String name = tokens[1];
int year = Integer.parseInt(tokens[2]);
Recommendations recommendations = db.lookup(name, year);
out.println("Recommendations pour le film " + name + " (" + year + ") :");
out.println(recommendations.asHTML());
out.println("<p/><a href='/'>Back</a>");
} else {
throw new BadRequestException("bad request: " + location);
}
} catch(BadRequestException|FilmDoesNotExistsException e) {
out.println(e.getMessage());
out.println("<p/><a href='/'>Back</a>");
}
//...
}
Félicitations, vous venez de mettre en œuvre votre premier serveur Web dynamique en Java !