CSC 3101 – Algorithmique et langage de programmation

Portail informatique

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
  • 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)
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.

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 une classe FilmDB représentant cette base de données. Cette classe ne possède qu'un unique champ de type table d'association ( Map). Dans le constructeur de FilmDB, allouez une instance de HashMap pour initialiser ce champ.

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.

En Java, on utilise des objets appelés "sockets" pour communiquer en TCP/IP. Un serveur qui souhaite attendre des connexions TCP/IP commence par créer un socket dit de type connexion : une instance de la classe java.net.ServerSocket. Le constructeur de cette classe prend en argument un numéro de port pour savoir sur quel port le serveur attend les connexions. Le client crée de son côté un socket dit de communication (une instance de la classe java.net.Socket).

Ensuite, comme illustré sur la figure ci-dessous, après que le serveur s'est mis à attendre des connexions, le client utilise son socket de communication pour envoyer une demande de connexion au serveur (étape 1). Pour cela, le client spécifie l'adresse de la machine sur laquelle le serveur s'exécute et le numéro de port auquel est attaché le socket de connexion du serveur. À la réception d'une demande de connexion, le serveur crée un socket de communication pour communiquer avec le client (étape 2), ce qui permet de laisser libre le socket de connexion du serveur et donc de pouvoir recevoir de nouvelles connexions d'autres clients en parallèle. Finalement, le socket de communication du serveur établit un canal de communication avec le socket de communication du client (étape 3). Une fois ce canal de communication établi, le client (resp. le serveur) peut écrire des données dans le canal de communication et ces données pourront alors être lues par le serveur (resp. le client).

Connexion avec un socket.

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.

Ensuite, effectuez le test suivant. Commencez par lancer votre serveur. Après, dans votre navigateur préféré, connectez-vous à votre serveur en saisissant l'URL localhost:4123. Si vous voyez un message s'afficher sur le terminal, c'est que votre code est correct. Le navigateur devrait vous expliquer que le serveur se comporte de façon étrange : c'est normal, notre serveur ne répond pas encore.

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 :
HTTP/1.1 200 OK contenu de la ressource res ce contenu peut bien sûr faire plusieurs lignes

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.

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.

Pour tester votre code, dans handle, avant de fermer le socket, affichez sur le terminal le résultat d'un appel à reader.readLine(). Lorsque vous saisissez l'URL localhost:4123 dans votre navigateur préféré, votre serveur devrait afficher GET / HTTP/1.1, ce qui correspond à une demande de lecture de la ressource "/" dans le protocole HTTP.

À cette étape, le code de handle() devrait donc être similaire à celui-ci :
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 :
  • 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())

Pour tester l'envoi de données sur le socket, dans handle, ajoutez, entre la lecture de la requête et la fermeture du socket, le code suivant (attention la ligne vide est importante pour le protocole HTTP) :
HTTP/1.1 200 OK coucou

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 :
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>

Notre serveur Web va gérer la demande de deux types de ressources HTTP.
  • 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 :
<a href='/titre-film/année'>titre (année)</a>

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 :
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 }

Testez votre serveur en :
  • 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 :
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 }

Vérifiez que :
  • 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 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.

Avant de traiter une requête, il faut donc que le serveur :
  • 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("/").

Dans handle(), après avoir retrouvé la variable location, on vous demande de transformer les « %20 » en espace, puis de stocker dans une variable String[] tokens le résultat de l'appel à location.split("/"). Au lieu d'afficher location, affichez la chaîne de caractères Request LOCATION => N tokens, où LOCATION est la variable location et N le nombre d'éléments dans le tableau tokens. À cette étape, le code de handle() devrait donc être similaire à celui-ci :
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 }

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 :
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 :
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 }

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 { 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 !