Développer une application avec le framework Play !

Romain Linsolas

Traduction française du tutoriel officiel de Play Framework visant à créer un moteur de blog.

Commentez

- Titre : Développer une application avec le framework Play !

- Auteur : Romain Linsolas

- Parution : 26 janvier 2010

- Licence : Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- I. Préambule

- I-A. A propos de Play

- I-B. A propos de ce tutoriel

- II. Démarrage du projet

- II-A. Introduction

- II-B. Le projet

- II-C. Pré-requis

- II-D. Installation

- II-E. Création d'un projet

- II-F. Lancer l'application

- II-G. Configurer la base de données

- II-H. Utiliser un gestionnaire de configuration

- III. Premiers pas pour le modèle de données

- III-A. Introduction à JPA

- III-B. Commençons avec la classe User

- III-C. Ecriture du premier test

- III-D. La classe Post

- III-E. Terminons avec Comment

- III-F. Utilisation de Fixtures pour l'écriture de tests plus complexes

- III-G. Sauvegardons notre travail

- IV. Construction du premier écran

- IV-A. Amorçage avec des données par défaut

- IV-B. La page d'accueil du blog

- IV-C. Améliorer la disposition

- IV-D. Ajoutons des styles

- IV-E. Commitons !

- V. Visualisation et écriture des commentaires

- V-A. Création de l'action show

- V-B. Ajout de liens dans la page des détails

- V-C. Spécification d'une plus belle URL

- V-D. Gérons la pagination

- V-E. Ajout du formulaire de nouveau commentaire

- V-F. Ajout de la validation

- VI. Définition d'un captcha

- VI-A. Génération de l'image captcha

- VI-B. Comment gérer l'état ?

- VI-C. Ajout du code du captcha dans le formulaire

- VI-D. Validation du captcha

- VII. Support des tags

- VII-A. L'objet modèle Tag

- VII-B. Tagguer les billets

- VII-C. Corsons un peu les choses !

- VII-D. Le nuage de tags

- VII-E. Ajoutons les tags à l'interface graphique

- VII-F. La nouvelle page "taggué avec"

- VIII. Création d'une interface d'administration en se basant sur du CRUD

- VIII-A. Activer le module CRUD

- VIII-B. Déclaration des contrôleurs CRUD

- VIII-C. Ajout de la validation

- VIII-D. Des libellés plus lisibles

- VIII-E. Paramétrer la liste des commentaires

- VIII-F. Paramétrer le formulaire de publication

- IX. Ajout de l'authentification

- IX-A. Activation du module Secure

- IX-B. Protection de l'interface d'administration

- IX-C. Configuration du processus d'authentification

- IX-D. Refactorisation de l'interface d'administration

- IX-E. Ajout des rôles

- IX-F. Configurer la mise en page du CRUD

- IX-G. Style de l'écran de connexion

- X. Création d'une zone de saisie personnalisée

- X-A. Lister les billets de l'auteur

- X-B. La page d'écriture d'un billet

- X-C. Réutilisation pour l'édition d'un billet

- XI. Terminer les tests de l'application

- XI-A. Test de la couche contrôleur

- XI-B. Ecriture des tests Selenium

- XI-C. Mesurer la couverture de test

- XII. Préparation pour la production

- XII-A. Définition du framework id

- XII-B. Définir l'application en mode PROD

- XII-C. Configurer une base MySQL

- XII-D. Mise en place d'un serveur HTTP frontal

- XII-E. Ce n'est que le début !

- XIII. Références

- XIII-A. Références

- XIII-B. Remerciements

I - Préambule

I-A - A propos de Play

Play est un framework destiné à la création d'applications web en Java. Contrairement à beaucoup d'autres frameworks, Play se focalise sur la productivité des développeurs, et cible les architectures RESTful.

Play est distribué selon la licence Apache 2.0.

I-B - A propos de ce tutoriel

Ce tutoriel est la traduction française d'un tutoriel en anglais proposé par le site de Play. Au cours de ce guide, nous allons développer un moteur de blog, yabe, ce qui nous permettra de nous familiariser avec les principaux concepts du framework Play, et d'en voir les atouts majeurs.

A noter que je me suis basé sur la version 1.1 de cette documentation et du framework Play. Cette version est à l'heure actuelle encore en développement...

II - Démarrage du projet

II-A - Introduction

Ce tutoriel a pour but de vous apprendre à utiliser le framework Play ! en codant une application "réelle", du début à la fin. Nous allons essayer d'utiliser la plupart la plupart des éléments que l'on rencontre dans un vrai projet, en introduisant également des bonnes pratiques pour un développement d'applications Play.

Ce tutoriel est divisé en plusiers parties indépendantes. Chacune d'elle introduira des fonctionnalités de plus en plus complexes, et fournira tout ce dont un projet a besoin : validation, gestion d'erreurs, couche de sécurité, tests automatisés, interface graphique agréable, interface d'administration, etc.

Le code source présenté dans ce tutoriel est utilisable sans aucune restriction pour vos propres projets.

II-B - Le projet

Nous faisons le choix de développer un moteur de blog. Il ne s'agit pas d'une idée très originale, mais cela a l'avantage de passer en revue les principales fonctionnalités attendues d'une application web moderne.

Pour rendre les choses plus intéressantes encore, nous décidons que les utilisateurs pourront avoir différents rôles (éditeur ou administrateur).

Ce projet s'appellera YABE.

L'ensemble du code de cette application est accessible dans le fichier téléchargé avec le framework Play, dans samples-and-tests/yabe.

II-C - Pré-requis

Tout d'abord, assurez-vous d'avoir une version Java 5 (ou plus récente) installée sur votre machine. Comme nous utiliserons essentiellement la console de commandes, il est préférable d'utiliser un OS de type Unix. Toutefois, Windows pourra très bien faire l'affaire (l'outil console2 peut se révéler être très utile dans ce cas).

Un minimum de connaissances sur le développement Java et web (essentiellement HTML, CSS et Javascript) est requis. Play est un framework complètement basé et orienté Java, et offre ou encapsule toutes les parties de l'API Java dont vous aurez besoin. A noter qu'il ne sera pas nécessaire non plus de savoir comment se configure le gestionnaire d'entités JPA (JPA entity manager) ou la façon dont on déploie des composants JavaEE.

Bien entendu, un éditeur de texte est nécessaire. Si vous avez l'habitude d'utiliser une IDE tel qu'Eclipse ou NetBeans, ne changez pas vos habitudes ! Toutefois, avec Play, vous pouvez travailler tout aussi efficacement avec un simple éditeur de texte. Tout cela grâce au fait que le framework Play gère lui-même la compilation du code Java et prend en charge le processus de déploiement. Nous verrons tout cela très rapidement...

Plus tard dans ce tutoriel, nous utiliserons lightppd et MySql afin de voir comment une application peut se déployer en production. Cependant, Play peut travailler sans avoir besoin de ces éléments.

II-D - Installation

L'installation de Play sur sa machine est extrêmement simple. Première étape, téléchargez la dernière version stable de Play, ici.

Si vous installez Play sur une machine Windows, il est préférable de l'installer dans un répertoire dont le nom ne contient aucun espace. Ainsi, au lieu de C:\Documents and Settings\user, privilégiez C:\Dev\Play par exemple.

Afin de travailler proprement, nous ajouterons le répertoire d'installation de Play au sein de la variable d'environnement PATH. Ainsi, il sera possible de taper la commande play n'importe où, en ligne de commande, pour accéder à l'environnement Play. Pour vérifier que tout s'est bien passé, nous lançons simplement la commande play, et devrions voir l'aide de l'outil Play :

> play
~        _            _
~  _ __ | | __ _ _  _| |
~ | '_ \| |/ _' | || |_|
~ |  __/|_|\____|\__ (_)
~ |_|            |__/
~
~ play! 1.0, http://www.playframework.org
~
~ Usage: play cmd [app_path] [--options]
~
~ with,  new      Create a new application
~        run      Run the application in the current shell
~        help     Show play help
~

II-E - Création d'un projet

Maintenant que l'installation de Play s'est correctement passée, il est temps de commencer l'application. Créer une application est très simple avec Play. Tout se déroule en lignes de commandes. L'exécution de la commande suivante va ainsi créer notre template d'application :

> play new yabe

Play nous demande alors le nom de l'application que l'on souhaite créer...

> play new yabe
~        _            _
~  _ __ | | __ _ _  _| |
~ | '_ \| |/ _' | || |_|
~ |  __/|_|\____|\__ (_)
~ |_|            |__/
~
~ play! 1.0, http://www.playframework.org
~
~ The new application will be created in d:\developpement\play-1.0\yabe
~ What is the application name? yabe
~
~ OK, the application is created.
~ Start it with : play run yabe
~ Have fun!
~

Play crée ainsi un répertoire yabe/ et y place un certain nombre de fichiers, dont les principaux sont les suivants :

- app/ contient le cœur de l'application, séparé entre les modèles, les contrôleurs, et les vues. Ce sont dans ces répertoires que sont stockés les classes Java ;

- conf/ stocke les fichiers de configuration, en particulier application.conf, les définitions des "routes" (redirection d'une URL vers une classe Java) ou encore les fichiers nécessaires à l'internationalisation ;

- lib/ contient les librairies Java optionnelles ;

- public/ héberge les ressources publiques, à savoir les images, les fichiers Javascript ou CSS ;

- test/ contient les fichiers de test. Il peut s'agir de tests Java (JUnit) ou encore Selenium.

Play utilise l'encodage UTF-8. Il est important que tous les fichiers stockés dans ces répertoires respectent cet encodage. Vérifiez la configuration de votre éditeur de texte pour respecter ceci.

On peut se demander où sont stockés les fichiers .class (les classes Java compilées). La réponse est : nulle part ! Play n'utilise en effet pas les classes compilées, mais lit directement les fichiers .java ! Pour information, Play utilise le compilateur Eclipse pour compiler les classes Java à la demande.

Deux choses importantes en découlent. Tout d'abord, Play détecte chaque modification de fichier Java, et recompile automatiquement ces sources. La seconde, c'est que lorsqu'une erreur arrive, Play est à même d'afficher une erreur détaillée avec le code source complet.

Play conserve en cache le bytecode de l'application dans le répertoire tmp/, mais dans le seul but d'accélérer le redémarrage des applications. En lançant la commande play clean, on désactive ce cache.

II-F - Lancer l'application

Bien que nous n'ayons pas encore touché à notre nouvelle application, nous pouvons d'ores et déjà la tester. Rendons-nous dans le répertoire créé par Play, yabe/. Maintenant, tapez la commande play run. Play va alors démarrer un serveur web en écoute sur le port 9000. Il suffit donc de se rendre à l'adresse suivante : http://localhost:9000. La page suivante nous accueille alors :

Regardons maintenant comment et pourquoi cette page s'affiche. Le point d'entrée de l'application est le fichier conf/routes. Ce fichier définit toutes les URL accessibles pour l'application. On peut y voir la "route" suivante :

GET        /            Application.index

Cette ligne indique que lorsque le serveur web reçoit une requête de type GET pour le chemin /, alors il doit appeler la méthode Java Application.index. Notons que l'on ne précise pas ici le package Java du contrôleur, car il est ici implicite.

Généralement, dans une application Java "classique" (de type client lourd), le point d'entrée est la méthode main :

public static void main(String[] args) {
...
}

Pour une application Play, chaque URL est considérée comme un point d'entrée spécifique. Play appelle les méthodes liées à ces points d'entrées des actions. Ces actions sont définies dans des classes Java appelées contrôleurs (controller).

Jettons maintenant un œil sur controllers.Application, qui se trouve dans le répertoire /yabe/app/controllers :

package controllers;

import play.mvc.*;

public class Application extends Controller {

public static void index() {
render();
}

}

Notons tout d'abord que cette classe étend play.mvc.Controller. Cette dernière propose de nombreuses méthodes utiles aux contrôleurs, telle que la méthode render().

Nous voyons également que l'action index est une méthode publique et statique. C'est de cette façon que sont définies les actions Play. L'aspect statique montre que les classes de contrôleur ne sont jamais instanciées. Enfin, ces méthodes ne retournent jamais rien (void).

Ici, l'action que l'on voit est assez simple. Elle ne fait ici qu'appeler la méthode render(), qui génère une page HTML selon un template. Se baser sur un template est la façon la plus commune - mais pas la seule - de générer une réponse à une requête HTTP.

Les templates sont de simples fichiers textes qui se trouvent dans le répertoire app/views/. Parce que dans notre exemple nous ne spécifions aucun template, celui par défaut est utilisé, à savoir apps/views/Application/index.html. Ce fichier contient le code suivant :

#{extends 'main.html' /}
#{set title:'Home' /}

#{welcome /}

Ce template est très léger. Il n'est d'ailleurs composé que de tags Play. Ces tags sont assez proches de ceux proposés par JSP. Ici, c'est le tag #{welcome /} qui génère le message de bienvenue que nous avons pu voir précédemment.

Le tag #{extends /} spécifie que le template hérite d'un autre template appelé main.html. Ce concept d'héritage permet la création de pages très complexes, en particulier en réutilisant des morceaux communs.

Voyons maintenant le contenu de /yabe/app/views/main.html, le template de base :

<!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" xml:lang="en" lang="en">
<head>
<title>#{get 'title' /}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" media="screen"
href="@{'/public/stylesheets/main.css'}" />
<link rel="shortcut icon" type="image/png"
href="@{'/public/images/favicon.png'}" />
</head>
<body>
#{doLayout /}
</body>
</html>

Le tag #{doLayout /} est utilisé afin d'insérer le contenu de la page Application/index.html.

Editons maintenant le fichier du contrôleur pour voir comment Play va charger automatiquement les modifications. Ouvrons ainsi le fichier /yabe/app/controllers/Application.java et supprimons par exemple le ";" à la fin de l'appel à la méthode render() :

public static void index() {
render()
}

Bien évidemment, comme il s'agit d'un code Java erroné, celui-ci ne doit même pas compiler. Rendons-nous toutefois sur notre page web. Comme on peut le voir, Play prend automatiquement en compte les modifications et essaie de recharger notre classe de contrôleur. Parce que nous y avons inséré du code non compilable, nous sommes redirigés vers une page d'erreur de compilation :

Corrigeons donc notre erreur et faisons une "vraie" modification :

public static void index() {
System.out.println("Yop");
render();
}

Cette fois, Play va recharger correctement le contrôleur et remplacer l'ancien code dans la JVM. Toutes les requêtes arrivant vers l'URL "/" affichera alors Yop dans la console Java.

Retirons cette ligne inutile, et éditons maintenant le template /yabe/app/views/Application/index.html afin de remplacer le message d'accueil :

#{extends 'main.html' /}
#{set title:'Home' /}

<h1>A blog will be there</h1>

A l'instar des changements apportés au code Java, nous pouvons voir immédiatement les effets de nos modifications sur le template en rechargeant simplement la page sur notre navigateur.

Nous allons maintenant commencer le développement de notre application. Il est tout à fait possible de continuer à éditer le code avec un simple éditeur de texte ou d'opter pour un IDE plus évolué. Dans ce cas, vous pouvez vous référez à cette page pour paramétrer le projet pour votre IDE.

II-G - Configurer la base de données

Une dernière chose avant de démarrer : notre moteur de blog a besoin d'une base de données. Pour faciliter le développement rapide, Play est livré avec une base de données intégrée, appelée HSQLDB. Cela peut être très utile au début d'un projet, avant de migrer vers une base de données plus robuste. Il est possible de choisir une base de données en mémoire (IMDB) ou une base de données plus classique, qui conservera nos données même après l'arrêt de notre serveur.

Durant les premières étapes de développement, nous allons faire beaucoup de tests et changerons souvent le modèle de données. C'est pour cette raison que nous utiliserons une base de données en mémoire.

Pour configurer notre base de données, nous éditons le fichier /yabe/conf/application.conf et décommentons la ligne suivante :

db=mem

Comme on peut le voir dans ce fichier, définir une base de données JDBC reste très facile, et nous pouvons même changer le pool de connexions. Maintenant, retournons sur notre navigateur et rafraichissons la page d'accueil. Play va alors démarrer automatiquement la base de données. Si on jette un œil sur les logs de l'application, on peut y voir la ligne suivante :

INFO  ~ Connected to jdbc:hsqldb:mem:playembed

II-H - Utiliser un gestionnaire de configuration

Lorsqu'on travaille sur un projet, en particulier en équipe, il est très fortement recommandé de stocker le code source dans un gestionnaire de configuration (GCL ou SCM). Cela offre l'avantage de pouvoir revenir en arrière dans les modifications en cas d'erreur, de travailler en équipe, d'offrir un accès à différentes versions de l'application, etc. Bien entendu, nous pouvons utiliser n'importe quel gestionnaire pour notre projet (CVS, Subversion, Git, etc.), mais ici, nous nous baserons sur Bazaar. Bazaar est un gestionnaire de configuration logicielle distribué.

Nous n'aborderons pas ici la procédure d'installation de Bazaar, mais cela reste très facile, quelque soit le système utilisé. Une fois Bazaar installé, allons dans le répertoire de yabe et initions le versioning de l'application en tapant :

> bzr init

Lorsqu'on stocke une application Play dans un SCM, il est important d'exclure les répertoires tmp/ et logs/ :

> bzr ignore tmp
> bzr ignore logs

Nous pouvons désormais commiter (ajouter les sources dans le SCM) notre première version de yabe :

> bzr add
> bzr commit -m "YABE inital version"

Notre version 1 est désormais disponible et nous pouvons passer à l'étape suivante.

III - Premiers pas pour le modèle de données

Dans ce chapitre, nous allons écrire le modèle pour notre moteur de blog.

III-A - Introduction à JPA

La couche modèle occupe une place centrale au sein des applications Play (c'est également le cas pour les applications correctement architecturées). Il s'agit d'une représentation des données en langage dédié (domain-specific language) à partir de laquelle l'application travaille. Notre but étant de créer un moteur de blog, la couche modèle contiendra des classes telles que User, Post ou Comment.

Parce qu'il est préférable que la plupart des objets du modèle persistent même après un redémarrage de l'application, nous devons les stocker dans une base de données persistante. Un choix assez courant est d'utiliser une base de données relationnelle. Java étant un langage orienté objet, nous utiliserons un mapping objet-relationnel (Object Relational Mapper) afin de limiter la différence d'impédance (voir Wikipédia).

JPA est une spécification Java qui définit l'API standard pour le mapping objet-relationnel. Play choisit le célèbre framework Hibernate pour l'implémentation de cette spécification. L'un des avantages à utiliser JPA plutôt que l'API standard d'Hibernate est que tout le mapping est déclaré directement dans les objets Java.

Si vous avez déjà utilisé Hibernate ou JPA auparavant, vous serez surpris de la simplicité proposée par Play concernant ce point. Il n'est plus nécessaire de configurer quoique ce soit. JPA fonctionne sans aucune configuration ou installation supplémentaire (out of the box).

Si vous souhaitez avoir plus d'informations sur JPA, vous pouvez consulter le site de Sun à ce propos.

III-B - Commençons avec la classe User

Nous allons commencer par coder notre moteur de blog en créant la classe User. Tout d'abord, créons le fichier /yabe/app/models/User.java, et écrivons une première implémentation de cette classe :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;

@Entity
public class User extends Model {

public String email;
public String password;
public String fullname;
public boolean isAdmin;

public User(String email, String password, String fullname) {
this.email = email;
this.password = password;
this.fullname = fullname;
}

}

L'annotation @Entity marque cette classe comme une entité JPA. La classe parente, Model, offre un ensemble d'assistants pour JPA que nous découvrirons plus tard. Tous les champs de cette classe seront automatiquement persistés en base.

Il n'est pas nécessaire que cette classe étende play.db.jpa.Model. Toutefois, cela peut être une bonne idée car cela offre un certain nombre d'outils JPA.

Si vous avez utilisé JPA auparavant, vous devez savoir que chaque entité JPA doit fournir une propriété marquée par l'annotation @Id. Ici, notre classe parente Model fournit automatiquement un ID numérique auto-généré, ce qui est suffisant dans la plupart des cas.

Il ne faut pas voir ce champ généré comme un identifiant fonctionnel, mais bien comme un identifiant technique. Il est généralement conseillé de conserver ces deux concepts séparés, et ainsi de conserver un ID numérique auto-généré comme identifiant technique.

Un point qui ne vous a sans doute pas échappé, surtout si vous êtes un développeur confirmé, c'est que toutes les propriétés de notre classe sont publiques ! En Java (ainsi que pour les autres langages orientés objet), une bonne pratique stipule de mettre les propriétés en accès privé, mais de fournir des méthodes d'accès (les getters et setters), cela dans le but de promouvoir le principe important d'encapsulation. En réalité, Play se charge pour nous de générer les getters et setters afin de préserver cette encapsulation ! Nous verrons plus tard comment cela fonctionne exactement.

Nous pouvons désormais rafraîchir la page d'accueil et vérifier le résultat. A moins d'avoir fait une erreur lors de l'écriture de la classe User, nous ne devrions voir aucun changement. Effectivement, Play a automatiquement compilé et chargé la classe User mais pour l'instant, cela n'apporte aucune fonctionnalité particulière à l'application...

III-C - Ecriture du premier test

La meilleure méthode pour vérifier la classe User est d'écrire un cas de test JUnit. De cette façon, nous pourrons compléter progressivement notre modèle de données, et tester que tout se passe bien.

Avec Play, le lancement d'un test unitaire se fait par le démarrage de l'application dans un mode spécial, le mode "test". Nous devons tout d'abord stopper notre application, puis la redémarrer avec la commande suivante :

> play test

La commande play test est très similaire à la commande play run à l'exception qu'il démarre également un module d'exécution de tests qui nous permet alors de lancer des tests depuis l'interface web !

Lorsque nous lançons une application Play en mode de test, Play utilise automatiquement l'ID du framework de test et charge le fichier application.conf automatiquement. Des informations supplémentaires sont disponibles ici.

Ouvrons maintenant notre navigateur à la page http://localhost:9000/@tests afin de voir l'exécuteur de tests. Sélectionnons tous les tests par défaut, et exécutons-les. Normalement, tous les indicateurs devraient virer au vert... Cependant, ces tests par défaut ne testent pas grand chose !

Pour tester la partie du modèle de données, nous allons utiliser des tests JUnit. Comme vous pourrez le constater, il existe déjà un fichier de test, /yabe/test/BasicTest.java :

import org.junit.*;
import play.test.*;
import models.*;

public class BasicTest extends UnitTest {

@Test
public void aVeryImportantThingToTest() {
assertEquals(2, 1 + 1);
}

}

Supprimons ce test (c'est-à-dire supprimons la méthode aVeryImportantThingToTest() et non la classe), puis écrivons un test qui vise à vérifier la création d'un nouvel utilisateur ainsi que sa récupération :

@Test
public void createAndRetrieveUser() {
// Create a new user and save it
new User("bob@gmail.com", "secret", "Bob").save();

// Retrieve the user with email address bob@gmail.com
User bob = User.find("byEmail", "bob@gmail.com").one();

// Test
assertNotNull(bob);
assertEquals("Bob", bob.fullname);
}

Nous constatons ici deux méthodes utiles proposées par la classe parent Model : save() et find().

Vous pouvez obtenir plus d'informations sur ces méthodes dans la documentation de Play sur JPA.

De retour sur la page web de l'exécuteur de test, choisissons le fichier BasicTests.java, démarrons-le, et vérifions que les indicateurs restent verts.

Nous souhaitons maintenant ajouter une méthode dans la classe User qui a pour but de vérifier l'existence d'un utilisateur et d'un mot de passe donnés. Ecrivons-la, puis testons-la. Pour ce faire, nous écrivons la méthode connect() à la classe User ainsi que son test JUnit correspondant :

public static User connect(String email, String password) {
return find("byEmailAndPassword", email, password).first();
}

@Test
public void tryConnectAsUser() {
// Create a new user and save it
new User("bob@gmail.com", "secret", "Bob").save();

// Test
assertNotNull(User.connect("bob@gmail.com", "secret"));
assertNull(User.connect("bob@gmail.com", "badpassword"));
assertNull(User.connect("tom@gmail.com", "secret"));
}

A chaque fois que nous modifions notre code, nous devrions lancer les tests afin de s'assurer que nous n'avons pas ajouté de régressions.

III-D - La classe Post

La classe Post représente un billet de notre blog. Voyons une première implémentation de cette classe :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;

@Entity
public class Post extends Model {

public String title;
public Date postedAt;

@Lob
public String content;

@ManyToOne
public User author;

public Post(User author, String title, String content) {
this.author = author;
this.title = title;
this.content = content;
this.postedAt = new Date();
}

}

Nous utilisons ici l'annotation @Lob qui indique à JPA que nous utilisons ici un type de données permettant le stockage d'une grande quantité de texte. Cette propriété correspond au contenu du billet. Nous déclarons ensuite la relation entre le User et notre classe, en utilisant le @ManyToOne, ce qui signifie qu'un utilisateur peut être à l'origine de plusieurs billets, mais qu'un billet n'a qu'un seul rédacteur.

Nous écrivons maintenant un nouveau cas de test permettant de nous assurer que cette classe fonctionne comme nous l'avions prévu. Mais avant cela, nous allons apporter une modification au niveau de notre classe JUnit. Dans l'état actuel du test, le contenu de la base de données n'est jamais supprimé, ce qui signifie qu'à chaque exécution, de nouveaux objets sont stockés dans la base de données. Cela devient problématique assez rapidement dès le moment où l'on disposera de tests qui auront besoin de compter le nombre d'éléments en base pour s'assurer que tout s'est bien passé.

Ecrivons donc une méthode setup() qui supprimera les données avant chaque test :

public class BasicTest extends UnitTest {

@Before
public void setup() {
Fixtures.deleteAll();
}

...

}

@Before est une annotation proposée par JUnit indiquant que notre méthode doit être appelée avant chaque méthode de test de notre classe.

Comme nous le voyons, la classe Fixtures est un assistant qui vient nous aider à gérer la base de données pendant les phases de tests. Nous pouvons à nouveau lancer les tests afin de nous assurer que nous n'avons rien cassé de l'existant, et commencer à écrire un nouveau test :

@Test
public void createPost() {
// Create a new user and save it
User bob = new User("bob@gmail.com", "secret", "Bob").save();

// Create a new post
new Post(bob, "My first post", "Hello world").save();

// Test that the post has been created
assertEquals(1, Post.count());

// Retrieve all post created by bob
List<Post> bobPosts = Post.find("byAuthor", bob).fetch();

// Tests
assertEquals(1, bobPosts.size());
Post firstPost = bobPosts.get(0);
assertNotNull(firstPost);
assertEquals(bob, firstPost.author);
assertEquals("My first post", firstPost.title);
assertEquals("Hello world", firstPost.content);
assertNotNull(firstPost.postedAt);
}

Comme nous n'utilisons pas forcément un IDE pour écrire notre code Java, il ne faut pas oublier ici d'inclure dans nos imports la classe java.util.List. Autrement, nous aurons une erreur de compilation.

III-E - Terminons avec Comment

Avant de considérer notre premier brouillon de modèle de données terminé, nous allons y ajouter la classe Comment et autoriser l'ajout de commentaires à un billet. L'écriture de cette classe ne révèle aucune difficulté particulière :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;

@Entity
public class Comment extends Model {

public String author;
public Date postedAt;

@Lob
public String content;

@ManyToOne
public Post post;

public Comment(Post post, String author, String content) {
this.post = post;
this.author = author;
this.content = content;
this.postedAt = new Date();
}

}

Comme auparavant, nous rédigeons notre test unitaire :

@Test
public void postComments() {
// Create a new user and save it
User bob = new User("bob@gmail.com", "secret", "Bob").save();

// Create a new post
Post bobPost = new Post(bob, "My first post", "Hello world").save();

// Post a first comment
new Comment(bobPost, "Jeff", "Nice post").save();
new Comment(bobPost, "Tom", "I knew that !").save();

// Retrieve all comments
List<Comment> bobPostComments = Comment.find("byPost", bobPost).fetch();

// Tests
assertEquals(2, bobPostComments.size());

Comment firstComment = bobPostComments.get(0);
assertNotNull(firstComment);
assertEquals("Jeff", firstComment.author);
assertEquals("Nice post", firstComment.content);
assertNotNull(firstComment.postedAt);

Comment secondComment = bobPostComments.get(1);
assertNotNull(secondComment);
assertEquals("Tom", secondComment.author);
assertEquals("I knew that !", secondComment.content);
assertNotNull(secondComment.postedAt);
}

Comme nous pouvons le constater, naviguer entre le Post et ses Comments n'est pas aisé. Nous devons ainsi passer par une requête pour retrouver l'ensemble des commentaires liés au billet. Nous pouvons cependant améliorer les choses en définissant, au niveau du Post le lien avec les commentaires. Cela se fait par l'ajout d'une propriété comments dans la classe Post :

...
@OneToMany(mappedBy="post", cascade=CascadeType.ALL)
public List<Comment> comments;

public Post(User author, String title, String content) {
this.comments = new ArrayList<Comment>();
this.author = author;
this.title = title;
this.content = content;
this.postedAt = new Date();
}
...

Notons l'utilisation de l'attribut mappedBy que nous avons faite pour dire à JPA que c'est la classe Post qui maintient la relation. Lorsqu'on définit une relation bidirectionnelle en JPA, il est très important de dire de quel côté la relation est maintenue. Dans notre cas, puisque les commentaires appartiennent à un billet, il est préférable que ce soit la classe Comment qui maintienne cette relation.

La propriété cascade indique à JPA que nous souhaitons que dans le cas où un billet est supprimé, alors tous les commentaires qui lui sont liés soient aussi supprimés.

Maintenant que nous avons établit cette relation, nous pouvons ajouter une méthode à la classe Post pour simplifier l'ajout de nouveaux commentaires :

public Post addComment(String author, String content) {
Comment newComment = new Comment(this, author, content).save();
this.comments.add(newComment);
return this;
}

A nouveau, écrivons un nouveau cas de test :

@Test
public void useTheCommentsRelation() {
// Create a new user and save it
User bob = new User("bob@gmail.com", "secret", "Bob").save();

// Create a new post
Post bobPost = new Post(bob, "My first post", "Hello world").save();

// Post a first comment
bobPost.addComment("Jeff", "Nice post");
bobPost.addComment("Tom", "I knew that !");

// Count things
assertEquals(1, User.count());
assertEquals(1, Post.count());
assertEquals(2, Comment.count());

// Retrieve Bob's post
bobPost = Post.find("byAuthor", bob).first();
assertNotNull(bobPost);

// Navigate to comments
assertEquals(2, bobPost.comments.size());
assertEquals("Jeff", bobPost.comments.get(0).author);

// Delete the post
bobPost.delete();

// Check that all comments have been deleted
assertEquals(1, User.count());
assertEquals(0, Post.count());
assertEquals(0, Comment.count());
}

Est-ce que nos indicateurs sont toujours dans le vert ?

III-F - Utilisation de Fixtures pour l'écriture de tests plus complexes

Lorsqu'on aborde l'écriture de tests plus complexes, la question d'avoir un vrai jeu de données se pose. Fixtures nous autorise à écrire notre jeu de données en se basant sur le format YAML. Ces données seront ainsi chargées en base avant chaque test.

Editons le fichier /yabe/test/data.yml et décrivons un utilisateur :

User(bob):
email: bob@gmail.com
password: secret
fullname: Bob

...

Pour la suite de notre tutoriel, vous pouvez télécharger le fichier Yaml final.

Nous rédigeons maintenant un nouveau cas de test qui utilise ces données, et réalise des vérifications sur celles-ci :

@Test
public void fullTest() {
Fixtures.load("data.yml");

// Count things
assertEquals(2, User.count());
assertEquals(3, Post.count());
assertEquals(3, Comment.count());

// Try to connect as users
assertNotNull(User.connect("bob@gmail.com", "secret"));
assertNotNull(User.connect("jeff@gmail.com", "secret"));
assertNull(User.connect("jeff@gmail.com", "badpassword"));
assertNull(User.connect("tom@gmail.com", "secret"));

// Find all bob's posts
List<Post> bobPosts = Post.find("author.email", "bob@gmail.com").fetch();
assertEquals(2, bobPosts.size());

// Find all comments related to bob's posts
List<Comment> bobComments = Comment.find("post.author.email", "bob@gmail.com").fetch();
assertEquals(3, bobComments.size());

// Find the most recent post
Post frontPost = Post.find("order by postedAt desc").first();
assertNotNull(frontPost);
assertEquals("About the model layer", frontPost.title);

// Check that this post has two comments
assertEquals(2, frontPost.comments.size());

// Post a new comment
frontPost.addComment("Jim", "Hello guys");
assertEquals(3, frontPost.comments.size());
assertEquals(4, Comment.count());
}

III-G - Sauvegardons notre travail

Nous avons maintenant terminé une partie importante de notre application de blog. Comme nous avons créé et testé toutes ces choses, nous allons à présent nous occuper de l'application web à proprement parler.

Mais avant cela, il est temps pour nous de sauvegarder notre travail dans notre gestionnaire de configuration, Bazaar. Exécutons la ligne de commande bzr st afin de voir les modifications apportées depuis le précédent commit :

> bzr st

Comme nous pouvions nous y attendre, certains nouveaux fichiers ne sont pas encore versionnés. Le répertoire test-result ne devant pas être versionné, nous indiquons à Bazaar de l'ignorer :

> bzr ignore test-result

Nous pouvons dès à présent ajouter les autres fichiers grâce à la commande bzr add :

> bzr add

Enfin, le commit final peut être réalisé, avec le petit commentaire qui va bien :

> bzr commit -m "The model layer is ready"

IV - Construction du premier écran

Nous avons désormais notre premier jet du modèle de données. Il est temps de nous occuper de la création de la première page de l'application. Celle-ci sera dédiée à l'affichage des billets les plus récents, mais proposera également une liste des billets plus anciens.

Voici un brouillon de ce que nous souhaitons obtenir :

IV-A - Amorçage avec des données par défaut

Avant de nous lancer corps et âme dans le codage de notre premier écran, nous devons réaliser une toute dernière chose. Travailler sur une application web sans données n'est pas vraiment fun. On ne peut même pas tester ce que l'on est en train de faire ! Hélas, nous n'avons pas encore développé les écrans d'écriture de billets, et de fait, nous ne pouvons alimenter la base nous-mêmes.

L'une des façons d'injecter des données par défaut est de charger un fichier Fixtures au démarrage de l'application. Pour cela, nous créons une tâche d'amorçage (bootstrap job). Une tâche Play est quelque chose qui s'exécute tout seul, sans aucun lien avec une quelconque requête HTTP, comme par exemple au démarrage de l'application, ou encore à intervalles donnés (via une planification CRON).

Créons la classe /yabe/app/Bootstrap.java qui va charger les données par défaut en utilisant Fixtures :

import play.*;
import play.jobs.*;
import play.test.*;

import models.*;

@OnApplicationStart
public class Bootstrap extends Job {

public void doJob() {
// Check if the database is empty
if(User.count() == 0) {
Fixtures.load("initial-data.yml");
}
}

}

Nous avons annoté notre classe avec @OnApplicationStart qui spécifie que nous voulons que Play exécute cette classe lors du démarrage de l'application.

Cette tâche va être exécutée différemment que l'on se trouve en mode de DEV ou en mode de PROD. En mode DEV, Play va en réalité attendre la première requête pour réellement démarrer l'application. Par conséquent, cette tâche ne sera exécutée qu'à partir du moment où notre serveur recevra sa première requête. Ainsi, dans l'hypothèse où cette tâche échoue, nous pourrons en être avertis au sein même de notre navigateur ! En mode PROD, la tâche sera exécutée au même moment que le démarrage de l'application, c'est-à-dire lors de l'exécution de la commande play run. En cas d'erreur, l'application ne sera pas démarrée.

Nous créons un fichier initial initial-data.yml dans le répertoire /yabe/conf. Bien entendu, nous pourrions réutiliser le fichier data.yml que nous avons créé précédemment.

Redémarrons l'application par la commande play run et rendons-nous à l'adresse http://localhost:9000.

IV-B - La page d'accueil du blog

Cette fois-ci est la bonne, nous pouvons démarrer le codage de la page d'accueil.

Souvenons-nous de la façon dont la toute première page était affichée. Le fichier de routage nous indiquait que l'URL "/" allait invoquer la méthode d'action controllers.Application.index(). Ensuite, cette méthode appelait la méthode parent render() et exécutait le template /yabe/app/views/Application/index.html.

Nous allons conserver ces composants, mais nous allons ajouter du code de façon à charger la liste des billets, et les faire afficher.

Ouvrons le fichier /yabe/app/controllers/Application.java et modifions la méthode index() afin de charger les billets :

package controllers;

import java.util.*;

import play.*;
import play.mvc.*;

import models.*;

public class Application extends Controller {

public static void index() {
Post frontPost = Post.find("order by postedAt desc").first();
List<Post> olderPosts = Post.find("order by postedAt desc").from(1).fetch(10);
render(frontPost, olderPosts);
}

}

Remarquons la façon dont on passe les objets à la méthode de création du rendu. De cette façon, nous pouvons accéder à ces objets depuis le template en utilisant leurs noms. Dans notre exemple, les variables frontPost et olderPosts seront ainsi accessibles au sein du template.

Ouvrons le fichier /yabe/app/views/Application/index.html et modifions-le afin d'afficher ces objets :

#{extends 'main.html' /}
#{set title:'Home' /}

#{if frontPost}
<div class="post">
<h2 class="post-title">
<a href="#">${frontPost.title}</a>
</h2>
<div class="post-metadata">
<span class="post-author">by ${frontPost.author.fullname}</span>
<span class="post-date">${frontPost.postedAt.format('MMM dd')}</span>
<span class="post-comments">
&nbsp;|&nbsp;
${frontPost.comments.size() ?: 'no'}
comment${frontPost.comments.size().pluralize()}</a>
#{if frontPost.comments}
, latest by ${frontPost.comments[0].author}
#{/if}
</span>
</div>
<div class="post-content">
${frontPost.content.nl2br()}
</div>
</div>

#{if olderPosts.size() > 1}
<div class="older-posts">
<h3>Older posts <span class="from">from this blog</span></h3>

#{list items:olderPosts, as:'oldPost'}
<div class="post">
<h2 class="post-title">
<a href="#">${oldPost.title}</a>
</h2>
<div class="post-metadata">
<span class="post-author">
by ${oldPost.author.fullname}
</span>
<span class="post-date">
${oldPost.postedAt.format('dd MMM yy')}
</span>
<div class="post-comments">
${oldPost.comments.size() ?: 'no'}
comment${oldPost.comments.size().pluralize()}
#{if oldPost.comments}
- latest by ${oldPost.comments[0].author}
#{/if}
</div>
</div>
</div>
#{/list}
</div>

#{/if}

#{/if}

#{else}
<div class="empty">
There is currently nothing to read here.
</div>
#{/else}

Si vous souhaitez en connaître d'avantage sur le langage du template, jetez un œil ici. Succinctement, cela nous permet d'accéder à nos objets Java de façon dynamique. Techniquement, c'est Groovy qui est utilisé. Certains éléments, tel que l'opérateur ?:, viennent du langage Groovy. Cependant, il n'est pas nécessaire d'apprendre Groovy pour écrire des templates Play. Si vous êtes déjà familiers avec des langages de template tels que JSP avec JSTL, vous ne devriez pas être trop perdus...

Rafraichissons maintenant notre page d'accueil de notre application :

Avouons-le, le résultat n'est pas très joli, mais il a le mérite de fonctionner !

Vous avez peut-être remarqué que nous avons déjà commencer à dupliquer du code... Parce que nous souhaitons afficher les billets de différentes façons (complète, complète avec commentaires, résumé), il nous faudrait créer quelque chose comme une "fonction" que nous pourrions appeler depuis différents templates. C'est exactement ce que font les tags Play !

Pour créer un tag, il suffit de créer un fichier /yabe/app/views/tags/display.html. En réalité, un tag est tout simplement un autre template. Toutefois, ce tag peut prendre des paramètres, à l'instar d'une fonction. Dans notre cas, le tag #{display /} dispose de deux paramètres :

- l'objet Post à afficher ;

- le mode d'affichage qui peut valoir soit "home", soit "teaser", soit "full".

*{ Display a post in one of these modes: 'full', 'home' or 'teaser' }*

<div class="post ${_as == 'teaser' ? 'teaser' : ''}">
<h2 class="post-title">
<a href="#">${_post.title}</a>
</h2>
<div class="post-metadata">
<span class="post-author">by ${_post.author.fullname}</span>,
<span class="post-date">${_post.postedAt.format('dd MMM yy')}</span>
#{if _as != 'full'}
<span class="post-comments">
&nbsp;|&nbsp; ${_post.comments.size() ?: 'no'}
comment${_post.comments.size().pluralize()}
#{if _post.comments}
, latest by ${_post.comments[0].author}
#{/if}
</span>
#{/if}
</div>
#{if _as != 'teaser'}
<div class="post-content">
<div class="about">Detail: </div>
${_post.content.nl2br()}
</div>
#{/if}
</div>

#{if _as == 'full'}
<div class="comments">
<h3>
${_post.comments.size() ?: 'no'}
comment${_post.comments.size().pluralize()}
</h3>

#{list items:_post.comments, as:'comment'}
<div class="comment">
<div class="comment-metadata">
<span class="comment-author">by ${comment.author},</span>
<span class="comment-date">
${comment.postedAt.format('dd MMM yy')}
</span>
</div>
<div class="comment-content">
<div class="about">Detail: </div>
${comment.content.escape().nl2br()}
</div>
</div>
#{/list}

</div>
#{/if}

Grâce à ce nouveau tag, nous pouvons revenir sur notre template principal et l'alléger :

#{extends 'main.html' /}
#{set title:'Home' /}

#{if frontPost}

#{display post:frontPost, as:'home' /}

#{if olderPosts.size()}

<div class="older-posts">
<h3>Older posts <span class="from">from this blog</span></h3>

#{list items:olderPosts, as:'oldPost'}
#{display post:oldPost, as:'teaser' /}
#{/list}
</div>

#{/if}

#{/if}

#{else}
<div class="empty">
There is currently nothing to read here.
</div>
#{/else}

Pour s'assurer que tout s'est bien passé, un rafraichissement de la page suffit.

IV-C - Améliorer la disposition

Comme nous le voyons, index.html étend le template main.html. Nous voulons proposer une disposition unique (un layout) pour toutes les pages de notre blog, avec un titre, des liens pour s'authentifier, etc. Nous devons donc modifier ce fichier (/yabe/app/views/main.html) :

<!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" xml:lang="en" lang="en">
<head>
<title>#{get 'title' /}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" media="screen"
href="@{'/public/stylesheets/main.css'}" />
<link rel="shortcut icon" type="image/png"
href="@{'/public/images/favicon.png'}" />
</head>
<body>

<div id="header">
<div id="logo">
yabe.
</div>
<ul id="tools">
<li>
<a href="#">Log in to write something</a>
</li>
</ul>
<div id="title">
<span class="about">About this blog</span>
<h1><a href="#">${blogTitle}</a></h1>
<h2>${blogBaseline}</h2>
</div>
</div>

<div id="main">
#{doLayout /}
</div>

<p id="footer">
Yabe is a (not that) powerful blog engine built with the
<a href="http://www.playframework.org">play framework</a>
as a tutorial application.
</p>

</body>
</html>

Une fois encore, rafraichissons la page et vérifions le résultat. Tout semble fonctionner, à l'exception de blogTitle et blogBaseLine qui ne sont pas affichés. Cela est dû au fait que nous n'avons pas donné ces valeurs lors de l'appel de la méthode render(...). Bien sûr, nous pourrions modifier notre classe Java pour les y ajouter. Toutefois, comme notre fichier main.html est considéré comme notre template principal pour toutes les pages, nous ne souhaitons pas ajouter les valeurs de ces variables à chaque fois.

L'une des façons d'ajouter un code commun pour chaque action d'un contrôleur (ou d'une hiérarchie de contrôleur) est d'ajouter un intercepteur @Before. Ajoutons la méthode addDefaults() dans notre contrôleur :

@Before
static void addDefaults() {
renderArgs.put("blogTitle", Play.configuration.getProperty("blog.title"));
renderArgs.put("blogBaseline", Play.configuration.getProperty("blog.baseline"));
}

Toutes les variables ajoutées au scope renderArgs sont accessibles au sein du template. Nous voyons également que la valeur de ces variables est directement tirée de l'objet Play.configuration. Cet objet contient toutes les propriétés qui sont définies dans le fichier /yabe/conf/application.conf. Par conséquent, il est nécessaire d'ajouter les lignes suivantes dans ce fichier :

# Configuration of the blog engine
# ~~~~~
blog.title=Yet another blog
blog.baseline=We won't write about anything

Rechargeons à nouveau notre page, et constatons l'effet désiré :

IV-D - Ajoutons des styles

Notre page étant presque terminée, essayons de l'embellir un petit peu ! Nous allons ajouter des styles pour cela. Vous avez peut-être remarqué que le template main.html inclue le fichier CSS /public/stylesheets/main.css. Nous conservons ce fichier, mais nous allons le compléter.

Le fichier final peut être téléchargé ici. Remplacez l'ancien /public/stylsheets/main.css par celui-ci. Nous pouvons désormais rafraichir la page d'accueil pour voir le résultat :

IV-E - Commitons !

Notre page d'accueil terminée, nous allons remonter (commiter) nos modifications :

> bzr st
> bzr add
> bzr commit -m 'Home page'

V - Visualisation et écriture des commentaires

La page d'accueil étant maintenant créée, nous allons continuer notre travail en créant la page de détails des billets. C'est sur cette page que nous pouvons visualiser les commentaires, et nous y inclurons également un formulaire pour en poster de nouveaux.

V-A - Création de l'action show

Pour afficher le détail d'un billet, nous devons écrire une nouvelle méthode d'action dans notre contrôleur. Appelons-la show() :

public static void show(Long id) {
Post post = Post.findById(id);
render(post);
}

Comme nous le voyons, l'écriture de cette action est extrêmement simple. Nous déclarons en paramètre de la méthode un ID qui sera automatiquement récupéré depuis la requête HTTP. Ce paramètre peut être extrait soit du queryString, soit de l'URL directement, soit du corps de la requête.

Si nous essayons d'envoyer une valeur incorrecte pour le paramètre ID par HTTP, alors sa valeur sera mise à null, et Play ajoutera une erreur de validation au conteneur d'erreurs.

Cette action affichera le contenu du template /yabe/apps/view/Application/show.html :

#{extends 'main.html' /}
#{set title:post.title /}

#{display post:post, as:'full' /}

Le fait que nous ayons déjà écrit le tag display, ce template se révèle très simple à écrire !

V-B - Ajout de liens dans la page des détails

Dans notre tag display, nous avions laissé vide la section des liens (en utilisant #). Nous pouvons maintenant les faire pointer sur l'action Application.show. Avec Play, la construction de liens dans un template se réalise grâce à la notation @{...}. Cette syntaxe utilise de façon inversée le routeur pour déterminer l'URL qui doit être appelée pour une action spécifique.

Editons le fichier de tag /yabe/app/views/tags/display.html :

...
<h2 class="post-title">
<a href="@{Application.show(_post.id)}">${_post.title}</a>
</h2>
...

Nous pouvons à présent rafraichir la page d'accueil et cliquer sur un billet pour l'afficher :

Voilà qui n'est pas mal du tout. Nous allons toutefois ajouter un lien pour revenir à la page d'accueil. Editons le fichier /yabe/app/views/main.html afin de compléter le lien du titre :

...
<div id="title">
<span class="about">About this blog</span>
<h1><a href="@{Application.index()}">${blogTitle}</a></h1>
<h2>${blogBaseline}</h2>
</div>
...

Les visiteurs peuvent maintenant naviguer de la page d'accueil vers la page de détails du billet, et vice-versa.

V-C - Spécification d'une plus belle URL

Lorsque nous visualisons la page des détails d'un billet, l'URL ressemble à ceci :

.../application/show?id=1

Cela résulte du fait que Play a utilisé la route globale par défaut :

*       /{controller}/{action}                  {controller}.{action}

Nous pouvons améliorer l'affichage des URL en spécifiant un chemin particulier pour l'action Application.show. Editons le fichier /yabe/conf/routes et ajoutons cette route à la fin :

GET     /posts/{id}                             Application.show

De cette façon, le paramètre ID sera extrait directement de l'URL. Vous pouvez en savoir plus sur l'utilisation de l'URI dans la documentation de la syntaxe sur le fichier de routage.

Rafraichissons à nouveau notre page afin de nous assurer que les URL sont désormais correctement écrites.

V-D - Gérons la pagination

Pour faciliter la vie des visiteurs de notre blog, nous allons leur permettre de naviguer d'un billet à un autre. Nous améliorons ainsi notre classe Post afin de permettre de récupérer le billet précédent ainsi que le suivant :

public Post previous() {
return Post.find("postedAt < ? order by postedAt desc", postedAt).first();
}

public Post next() {
return Post.find("postedAt > ? order by postedAt asc", postedAt).first();
}

Nous appellerons ces méthodes à plusieurs reprises, et il serait bon des les optimiser. Toutefois, nous nous contenterons de cette version ici. Maintenant, il faut ajouter les liens de pagination en haut du template show.html, précédemment le tag #{display /} :

<ul id="pagination">
#{if post.previous()}
<li id="previous">
<a href="@{Application.show(post.previous().id)}">
${post.previous().title}
</a>
</li>
#{/if}
#{if post.next()}
<li id="next">
<a href="@{Application.show(post.next().id)}">
${post.next().title}
</a>
</li>
#{/if}
</ul>

Voilà qui est bien mieux !

V-E - Ajout du formulaire de nouveau commentaire

Attaquons désormais le formulaire de saisie d'un nouveau commentaire. Commençons par ajouter une méthode action postComment() dans notre contrôleur :

public static void postComment(Long postId, String author, String content) {
Post post = Post.findById(postId);
post.addComment(author, content);
show(postId);
}

Comme on peut le voir, nous réutilisons la méthode addComment() que nous avions écrite dans un chapitre précédent. Occupons nous du code HTML dans le template show.html :

<h3>Post a comment</h3>

#{form @Application.postComment(post.id)}
<p>
<label for="author">Your name: </label>
<input type="text" name="author" id="author" />
</p>
<p>
<label for="content">Your message: </label>
<textarea name="content" id="content"></textarea>
</p>
<p>
<input type="submit" value="Submit your comment" />
</p>
#{/form}

Voilà ! Les visiteurs sont désormais capables de poster des commentaires sur nos billets.

V-F - Ajout de la validation

Actuellement, nous ne validons pas le contenu du formulaire avant la création du commentaire. Nous souhaitons rendre les deux champs (le nom et le message) obligatoires. Nous pouvons profiter de ce que Play met à notre disposition pour nous assurer que les différents paramètres HTTP sont correctement remplis. Modifions l'action postComment pour ajouter l'annotation de validation @Required :

public static void postComment(Long postId, @Required String author, @Required String content) {
Post post = Post.findById(postId);
if (validation.hasErrors()) {
render("Application/show.html", post);
}
post.addComment(author, content);
show(postId);
}

N'oublions pas d'importer le package play.data.validation.*.

Désormais, en cas d'erreur de validation, nous voulons réafficher la page de détails du billet. Nous devons donc modifier le code du formulaire pour afficher les erreurs :

<h3>Post a comment</h3>

#{form @Application.postComment(post.id)}

#{ifErrors}
<p class="error">
All fields are required!
</p>
#{/ifErrors}

<p>
<label for="author">Your name: </label>
<input type="text" name="author" id="author" value="${params.author}" />
</p>
<p>
<label for="content">Your message: </label>
<textarea name="content" id="content">${params.content}</textarea>
</p>
<p>
<input type="submit" value="Submit your comment" />
</p>
#{/form}

Notez que nous réutilisons les paramètres postés afin de remplir les champs HTML.

Afin de rendre l'interface un peu plus agréable pour nos visiteurs, nous allons ajouter un peu de Javascript pour donner le focus automatiquement au formulaire en cas d'erreur. Ce script utilise la célèbre librairie jQuery, nous devons donc l'intégrer à notre application. Téléchargeons les deux composants de la librairie ici et ici. Nous devons ensuite les déposer dans le répertoire /yabe/public/javascripts, puis modifions le template main.html pour les inclure dans nos pages :

...
<script src="@{'/public/javascripts/jquery-1.3.2.min.js'}"></script>
<script src="@{'/public/javascripts/jquery.tools.min.js'}"></script>
</head>

Maintenant, il nous est possible d'ajouter ce petit script dans le template show.html (ajoutons-le à la fin de la page) :

<script type="text/javascript" charset="utf-8">
$(function() {
// Expose the form
$('form').click(function() {
$('form').expose({api: true}).load();
});

// If there is an error, focus to form
if($('form .error').size()) {
$('form').expose({api: true, loadSpeed: 0}).load();
$('form input').get(0).focus();
}
});
</script>

Notre formulaire d'ajout de commentaire a plutôt une bonne tête désormais ! Nous allons toutefois y ajouter deux petites choses.

Tout d'abord, nous souhaitons ajouter un message de confirmation lorsqu'un commentaire est posté avec succès. Pour cela, nous utilisons le scope flash qui nous autorise à passer un message de l'appel d'une action à une autre.

Modifions l'action postComment pour y ajouter un message de confirmation :

public static void postComment(Long postId, @Required String author, @Required String content) {
Post post = Post.findById(postId);
if(validation.hasErrors()) {
render("Application/show.html", post);
}
post.addComment(author, content);
flash.success("Thanks for posting %s", author);
show(postId);
}

Affichons le message dans notre page show.html :

...
#{if flash.success}
<p class="success">${flash.success}</p>
#{/if}

#{display post:post, as:'full' /}
...

La seconde chose que nous souhaitions faire est de corriger l'URL utilisée par l'action postComment. Tout comme précédemment, Play utilise ici le routage par défaut. Spécifions une nouvelle route propre à l'action de publication de commentaire :

POST    /posts/{postId}/comments                Application.postComment

Et voilà qui est fait ! Comme précédemment, n'oublions pas de commiter notre code dans Bazaar !

VI - Définition d'un captcha

La façon dont nous avons implémenté l'ajout de commentaires permet à n'importe qui d'en publier. Afin de nous protéger des spams automatiques nous allons utiliser la méthode de l'image captcha.

VI-A - Génération de l'image captcha

Débutons en regardant comment nous pouvons générer une image captcha avec Play. Très simplement, nous allons utiliser une nouvelle action, sauf que celle-ci retournera un flux binaire au lieu d'une réponse HTML, comme c'était le cas auparavant.

Play offre un certain nombre d'outils qui peuvent s'avérer très utiles lors d'un développement web. La génération d'un captcha en fait justement partie. Nous pouvons ainsi utiliser l'utilitaire play.libs.Images pour générer cette image, puis pour l'insérer dans notre réponse HTTP.

Comme toujours, nous créerons une implémentation simple. Ajoutons l'action captcha dans le contrôleur (n'oublions pas d'ajouter l'import sur play.libs.*) :

public static void captcha() {
Images.Captcha captcha = Images.captcha();
renderBinary(captcha);
}

Il est à noter que nous pouvons passer l'objet binaire directement à la méthode renderBinary() car la classe Images.Captcha implémente l'interface java.io.InputStream.

Ajoutons une nouvelle route dans notre fichier de routage /yabe/conf/routes :

GET     /captcha                                Application.captcha

Voyons le résultat en nous rendant à la page http://localhost:9000/captcha :

Si tout fonctionne correctement, à chaque rafraichissement de cette page, une nouvelle image doit être générée.

VI-B - Comment gérer l'état ?

Jusqu'à présent, ce que nous avons fait était relativement simple. La partie complexe de notre application arrive ! Pour valider le captcha, nous devons conserver quelque part le texte généré aléatoirement et affiché dans l'image. De cette façon, nous serons à même de valider la valeur saisie par le visiteur au moment de la soumission du formulaire.

Bien sûr, nous pourrions simplement ajouter le texte dans la session de l'utilisateur au moment de la génération de l'image, afin de la récupérer plus tard. Cependant, cette solution a deux revers :

- Premier point : la session Play est stockée dans un cookie. Cela résout un certain nombre de problèmes en termes d'architecture, mais cela a aussi des impacts. Les données écrites dans le cookie de session sont signées (de façon à ce que l'utilisateur ne puisse pas les modifier) mais elles ne sont pas cryptées. Ainsi, si nous écrivons le code du captcha dans la session, n'importe qui pourra le retrouver facilement simplement en lisant le contenu du cookie !

- Second point : Play est un framework sans état. Nous voulons donc gérer les choses de cette façon. Par exemple, que va t'il se passer si un même utilisateur ouvre simultanément deux pages différentes de notre blog, avec bien sûr, deux images captcha différentes ? Nous devons donc conserver le code du captcha pour chacun des formulaires générés.

Pour résoudre notre problème, nous avons besoin de deux choses. Nous allons stocker le code du captcha au niveau du serveur. Il s'agit ici de données éphémères (transient), c'est-à-dire qu'elles ne seront pas persistées en base. Nous pouvons donc utiliser le cache de Play. De plus, comme les données mises en cache ont une durée de vie limitée, cela permettra d'ajouter une sécurité supplémentaire (disons que le captcha ne sera ainsi valide que durant un petit laps de temps). Pour être capable de retrouver le code du captcha, nous devons générer un ID unique. Celui-ci sera ajouté dans le formulaire dans un champ caché, et fera ainsi implicitement référence au code du captcha.

C'est ainsi que nous résolvons élégamment notre problème d'absence d'état.

Modifions désormais notre action captcha (ne pas oublier l'import de play.cache.*) :

public static void captcha(String id) {
Images.Captcha captcha = Images.captcha();
String code = captcha.getText("#E4EAFD");
Cache.set(id, code, "10mn");
renderBinary(captcha);
}

Notons que la méthode getText() prend en paramètre n'importe quelle couleur, couleur qui sera utilisée pour dessiner le texte dans l'image.

VI-C - Ajout du code du captcha dans le formulaire

A chaque génération du formulaire de création de commentaire, nous créons un ID unique. Nous sommes donc aussi amenés à modifier notre code HTML pour intégrer cette information dans un champs caché. Voyons d'abord notre action Application.show :

public static void show(Long id) {
Post post = Post.findById(id);
String randomID = Codec.UUID();
render(post, randomID);
}

Maintenant, occupons-nous du fichier de template /yabe/app/views/Application/show.html :

...
<p>
<label for="content">Your message: </label>
<textarea name="content" id="content">${params.content}</textarea>
</p>
<p>
<label for="code">Please type the code below: </label>
<img src="@{Application.captcha(randomID)}" />
<br />
<input type="text" name="code" id="code" size="18" value="" />
<input type="hidden" name="randomID" value="${randomID}" />
</p>
<p>
<input type="submit" value="Submit your comment" />
</p>
...

Bon début ! Notre formulaire intègre désormais l'image captcha :

VI-D - Validation du captcha

Il ne reste alors plus qu'à valider le captcha. Nous avions ajouté un ID aléatoire dans un champ caché de notre formulaire. Ainsi, nous pouvons le récupérer dans l'action postComment, tout comme nous allons retrouver le code depuis notre cache. Nous pourrons finalement les comparer.

public static void postComment(
Long postId,
@Required(message="Author is required") String author,
@Required(message="A message is required") String content,
@Required(message="Please type the code") String code,
String randomID) {
Post post = Post.findById(postId);
validation.equals(
code, Cache.get(randomID)
).message("Invalid code. Please type it again");
if(validation.hasErrors()) {
render("Application/show.html", post, randomID);
}
post.addComment(author, content);
flash.success("Thanks for posting %s", author);
show(postId);
}

Comme nous avons de nouveaux messages d'erreur, nous modifions également la façon dont ils sont affichés dans notre template show.html (dans notre cas, nous n'affichons que la première erreur, ce qui est amplement suffisant) :

...
#{ifErrors}
<p class="error">
${errors[0]}
</p>
#{/ifErrors}
...

Généralement, dans les formulaires plus complexes, les messages d'erreur ne sont pas gérés de cette façon, mais plutôt externalisés dans un fichier de propriétés.

Nous sommes prêts à vérifier que le captcha est désormais complètement fonctionnel :

Voilà qui est parfait !

VII - Support des tags

Etant donné que notre blog hébergera plusieurs billets, il deviendra de plus en plus difficile de les retrouver. Pour nous aider à les classer selon le sujet qu'ils traitent, nous allons ajouter le support des tags (ou catégorie).

VII-A - L'objet modèle Tag

Nous commençons par ajouter un nouvel objet dans notre modèle. La classe Tag est très simple :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;

@Entity
public class Tag extends Model implements Comparable<Tag> {

public String name;

private Tag(String name) {
this.name = name;
}

public String toString() {
return name;
}

public int compareTo(Tag otherTag) {
return name.compareTo(otherTag.name);
}

}

A chaque fois que nous aurons besoin de récupérer un nouvel objet Tag, nous utiliserons la méthode findOrCreateByName(String name). Ajoutons cette méthode à la classe Tag :

public static Tag findOrCreateByName(String name) {
Tag tag = Tag.find("byName", name).first();
if(tag == null) {
tag = new Tag(name);
}
return tag;
}

VII-B - Tagguer les billets

La nouvelle étape consiste à lier l'objet Post au nouvel objet Tag. Ajoutons cette relation dans la classe Post :

...
@ManyToMany(cascade=CascadeType.PERSIST)
public Set<Tag> tags;

public Post(User author, String title, String content) {
this.comments = new ArrayList<Comment>();
this.tags = new TreeSet<Tag>();
this.author = author;
this.title = title;
this.content = content;
this.postedAt = new Date();
}
...

Il est à noter que nous utilisons ici un TreeSet afin de conserver les objets dans un ordre spécifique (plus précisément dans l'ordre alphabétique, étant donné notre implémentation de la méthode compareTo).

Notre relation restera unidirectionnelle.

Nous modifions notre classe Post afin d'ajouter des méthodes qui nous aiderons à gérer les tags d'un billet. Tout d'abord, une méthode nous permettant de tagguer un billet :

public Post tagItWith(String name) {
tags.add(Tag.findOrCreateByName(name));
return this;
}

Egalement, une méthode nous permettant de retrouver tous les billets d'un tag donné :

public static List<Post> findTaggedWith(String tag) {
return Post.find(
"select distinct p from Post p join p.tags as t where t.name = ?", tag
).fetch();
}

Il est temps d'écrire un nouveau test pour vérifier ce que nous avons écrit. Redémarrons notre serveur en mode test :

> play test

Ajoutons un nouveau @Test dans la classe BasicTest :

@Test
public void testTags() {
// Create a new user and save it
User bob = new User("bob@gmail.com", "secret", "Bob").save();

// Create a new post
Post bobPost = new Post(bob, "My first post", "Hello world").save();
Post anotherBobPost = new Post(bob, "Hop", "Hello world").save();

// Well
assertEquals(0, Post.findTaggedWith("Red").size());

// Tag it now
bobPost.tagItWith("Red").tagItWith("Blue").save();
anotherBobPost.tagItWith("Red").tagItWith("Green").save();

// Check
assertEquals(2, Post.findTaggedWith("Red").size());
assertEquals(1, Post.findTaggedWith("Blue").size());
assertEquals(1, Post.findTaggedWith("Green").size());

}

VII-C - Corsons un peu les choses !

Bien que nous n'allions pas en faire usage pour l'instant, que se passerait-il si nous voulions récupérer les billets qui seraient marqués par plusieurs tags ? Il s'agit d'une tâche plus difficile qu'il n'y parait...

Voici le code JPQL qui nous permet de résoudre ce problème :

public static List<Post> findTaggedWith(String... tags) {
return Post.find(
"select distinct p.id from Post p join p.tags as t where t.name in (:tags) group by p.id having count(t.id) = :size"
).bind("tags", tags).bind("size", tags.length).fetch();
}

Le point délicat ici est que nous avons besoin de réaliser un test sur le nombre de tags que possède le billet.

Nous pouvons améliorer notre précédent test en y ajoutant les assertions suivantes :

...
assertEquals(1, Post.findTaggedWith("Red", "Blue").size());
assertEquals(1, Post.findTaggedWith("Red", "Green").size());
assertEquals(0, Post.findTaggedWith("Red", "Green", "Blue").size());
assertEquals(0, Post.findTaggedWith("Green", "Blue").size());
...

VII-D - Le nuage de tags

Nous avons désormais nos tags, nous voulons maintenant un nuage de tags. Pour ce faire, ajoutons une méthode à notre classe Tag qui génèrera ce nuage :

public static List<Map> getCloud() {
List<Map> result = Tag.find(
"select new map(t.name as tag, count(p.id) as pound) from Post p join p.tags as t group by t.name"
).fetch();
return result;
}

Dans cet exemple, nous utilisons une fonctionnalité d'Hibernate qui retourne un objet particulier à partir d'une requête JPA. Ici, notre méthode retourne une List qui contiendra pour chaque tag une Map contenant deux valeurs : le nom du tag ainsi que son "poids", c'est-à-dire le nombre de billets attachés à ce tag.

Finalisons notre classe de test en y ajoutant ceci :

List<Map> cloud = Tag.getCloud();
assertEquals(
"[{tag=Red, pound=2}, {tag=Blue, pound=1}, {tag=Green, pound=1}]",
cloud.toString()
);

VII-E - Ajoutons les tags à l'interface graphique

Nous sommes prêts à ajouter la gestion des tags de façon à offrir une nouvelle façon de naviguer parmi les billets de notre blog. Comme toujours, pour rendre notre travail efficace, nous allons ajouter des données propres aux tags dans notre jeu de données de tests.

Modifions le fichier /yabe/conf/initial-data.yml et ajoutons-y des informations sur les tags, comme par exemple :

...
Tag(play):
name:           Play

Tag(architecture):
name:           Architecture

Tag(test):
name:           Test

Tag(mvc):
name:           MVC
...

Modifions également la donnée d'un billet :

...
Post(jeffPost):
title:          The MVC application
postedAt:       2009-06-06
author:         jeff
tags:
- play
- architecture
- mvc
content:        >
A Play
...

Pensez à ajouter la déclaration des tags au début du fichier, car il est nécessaire qu'ils soient insérés en base avant les posts.

Nous devons forcer le redémarrage de l'application afin que les données initiales soient prises en compte. Nous constatons que Play est aussi à même de détecter les problèmes présents dans le fichier YAML :

Nous pouvons modifier le tag #{display /} afin d'afficher l'ensemble des tags liés au billet dans sa vue complète. Ouvrons le fichier /yabe/app/views/tags/display/html :

...
#{if _as != 'full'}
<span class="post-comments">
&nbsp;|&nbsp; ${_post.comments.size() ?: 'no'}
comment${_post.comments.size().pluralize()}
#{if _post.comments}
, latest by ${_post.comments[0].author}
#{/if}
</span>
#{/if}
#{elseif _post.tags}
<span class="post-tags">
- Tagged
#{list items:_post.tags, as:'tag'}
<a href="#">${tag}</a>${tag_isLast ? '' : ', '}
#{/list}
</span>
#{/elseif}
...

VII-F - La nouvelle page "taggué avec"

Nous avons tout à notre disposition pour pouvoir lister les billets selon leurs tags. Ci-dessus, nous avons laissé le lien vide, remplaçons-le désormais par un lien vers une nouvelle action listTagged :

...
- Tagged
#{list items:_post.tags, as:'tag'}
<a href="@{Application.listTagged(tag.name)}">${tag}</a>${tag_isLast ? '' : ', '}
#{/list}
...

Ajoutons la méthode Java :

public static void listTagged(String tag) {
List<Post> posts = Post.findTaggedWith(tag);
render(tag, posts);
}

N'oublions pas de spécifier la route dans le fichier adéquat :

GET     /posts/{tag}                    Application.listTagged

Nous avons ici un problème, car une route existante entre en conflit avec cette nouvelle route. Ces deux routes sont liées à la même URI :

GET     /posts/{id}                     Application.show
GET     /posts/{tag}                    Application.listTagged

Toutefois, comme nous considérons que les ID sont des valeurs numériques contrairement aux tags, nous pouvons résoudre ce problème en utilisant une expression régulière pour restreindre la première route :

GET     /posts/{<[0-9]+>id}             Application.show
GET     /posts/{tag}                    Application.listTagged

Pour finir, nous n'avons qu'à créer le template /yabe/app/views/Application/listTagged.html qui sera utilisé par cette nouvelle action :

#{extends 'main.html' /}
#{set title:'Posts tagged with ' + tag /}

*{********* Title ********* }*

#{if posts.size() > 1}
<h3>There are ${posts.size()} posts tagged '${tag}'</h3>
#{/if}
#{elseif posts}
<h3>There is 1 post tagged '${tag}'</h3>
#{/elseif}
#{else}
<h3>No post tagged '${tag}'</h3>
#{/else}

*{********* Posts list *********}*

<div class="older-posts">
#{list items:posts, as:'post'}
#{display post:post, as:'teaser' /}
#{/list}
</div>

VIII - Création d'une interface d'administration en se basant sur du CRUD

Dans l'état actuel de notre blog, il n'est pas encore possible d'écrire de billets depuis l'interface web. Il n'est pas non plus possible de modérer les commentaires. Play fournit un module CRUD (Create, Read, Update and Delete) "clés en main" qui va nous permettre de créer rapidement une interface d'administration basique.

VIII-A - Activer le module CRUD

Une application Play peut être créée par l'assemblage de plusieurs modules. Cela nous permet de réutiliser ces composants au sein d'autres applications.

Le module CRUD est un module générique qui réalise une introspection du modèle de données pour créer des listes et des formulaires simples.

Pour activer le module CRUD, nous devons ajouter la ligne suivante dans le fichier /yabe/conf/application.conf :

# Import the crud module
module.crud=${play.path}/modules/crud

Ce module s'accompagne d'une liste de routes par défaut que nous pouvons réutiliser. Pour importer ces routes, nous avons simplement à ajouter dans notre fichier /yabe/conf/routes les lignes suivantes :

# Import CRUD routes
*      /admin              module:crud

Cela importera toutes les routes dont les URL seront préfixées par /admin. Nous pouvons désormais redémarrer notre application afin que ce nouveau module soit pris en compte.

VIII-B - Déclaration des contrôleurs CRUD

Pour chaque objet de notre modèle de données que nous souhaitons intégrer dans l'interface d'administration, nous devons déclarer un contrôleur qui étend la classe controllers.CRUD. Créons ainsi un contrôleur pour chacun de nos objets du modèle. Par exemple, pour l'objet Post, nous créons le contrôleur dans le fichier /yabe/app/controllers/Posts.java :

package controllers;

import play.*;
import play.mvc.*;

public class Posts extends CRUD {
}

La convention ici est que de pluraliser le nom de l'objet associé pour définir le nom du contrôleur (ainsi, le contrôleur de l'objet Post sera Posts). De cette façon, Play trouvera automatiquement l'objet associé pour chaque contrôleur. Si nous avons besoin d'utiliser un autre nom, alors il nous faut utiliser l'annotation @CRUD.For (voir la documentation pour plus d'informations).

Répétons cette opération pour chaque objet de notre modèle :

package controllers;

import play.*;
import play.mvc.*;

public class Users extends CRUD {
}

package controllers;

import play.*;
import play.mvc.*;

public class Comments extends CRUD {
}

package controllers;

import play.*;
import play.mvc.*;

public class Tags extends CRUD {
}

Maintenant, ouvrons notre navigateur sur la page http://localhost:9000/admin, ce qui devrait nous conduire à la page d'administration :

Si nous naviguons un peu dans cette page, nous pouvons noter que les noms des objets sont basiques. La raison est toute simple : Play utilise la méthode toString() pour trouver le nom des objets. Il suffit, pour corriger cela, de fournir une implémentation correcte de la méthode toString() pour les objets du modèle. Par exemple, pour la classe User :

public String toString() {
return email;
}

VIII-C - Ajout de la validation

Notre nouvelle page d'administration ne contient, hélas, aucune forme de validation. Toutefois, le module CRUD est capable d'extraire les règles de validation depuis des annotations écrites dans notre classe de modèle de données. Modifions la classe User pour les y ajouter :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;
import play.data.validation.*;

@Entity
public class User extends Model {

@Email
@Required
public String email;

@Required
public String password;

public String fullname;
public boolean isAdmin;

...

Maintenant, si nous retournons dans notre interface d'administration et que nous éditons ou créons un utilisateur, nous voyons que Play a automatiquement ajouté des règles de navigation :

Pratiquons la même modification sur la classe Post :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;
import play.data.validation.*;

@Entity
public class Post extends Model {

@Required
public String title;

@Required
public Date postedAt;

@Lob
@Required
@MaxSize(10000)
public String content;

@Required
@ManyToOne
public User author;

@OneToMany(mappedBy="post", cascade=CascadeType.ALL)
public List<Comment> comments = new ArrayList();

@ManyToMany(cascade=CascadeType.ALL)
public Set<Tag> tags = new HashSet();

...

Et voyons le résultat :

Nous rencontrons un effet de bord intéressant : l'annotation @MaxSize a changé la façon dont Play affiche le formulaire. En effet, nous disposons désormais d'un champ de saisie de texte (textarea) pour saisir cette propriété.

Pour conclure, nous modifions les classes Comment et Tag :

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;
import play.data.validation.*;

@Entity
public class Tag extends Model implements Comparable<Tag> {

@Required
public String name;

...

package models;

import java.util.*;
import javax.persistence.*;

import play.db.jpa.*;
import play.data.validation.*;

@Entity
public class Comment extends Model {

@Required
public String author;

@Required
public Date postedAt;

@Lob
@Required
@MaxSize(10000)
public String content;

@ManyToOne
@Required
public Post post;

...

VIII-D - Des libellés plus lisibles

Comme nous pouvons le constater sur les formulaires, les libellés sont un peu rudes ! Play utilise en effet le nom de la variable Java pour afficher le libellé correspondant dans le formulaire. Nous pouvons toutefois spécifier d'autres libellés en modifiant le fichier /yabe/conf/messages :

Nous pouvons différencier le fichier messages selon la langue. Par exemple, nous pouvons mettre les messages en français dans le fichier messages.fr.

Ajoutons ainsi les libellés dans notre fichier :

title=Title
content=Content
postedAt=Posted at
author=Author
post=Related post
tags=Tags set
name=Common name
email=Email
password=Password
fullname=Full name
isAdmin=User is admin

A nouveau, rafraichissons notre page pour voir les modifications :

VIII-E - Paramétrer la liste des commentaires

Le module CRUD a été développé de manière à être complètement paramétrable. Jetons un œil sur la page listant les commentaires, pour y constater que l'affichage n'est pas terrible. Nous voudrions ajouter plus de colonnes dans cette vue, particulièrement une colonne "billet concerné" qui nous permettra de filtrer facilement notre liste.

Comme c'est notre application qui reste maître, nous pouvons facilement surcharger n'importe quelle action ou n'importe quel template proposé par le module CRUD. Dans notre exemple, nous voulons paramétrer la vue des commentaires, et donc nous devons créer notre propre template /yabe/app/views/Comments/list.html.

Le module CRUD propose d'autres commandes, dès lors qu'il est activé. La commande crud:ov nous permet de surcharger (override) n'importe quel template. Depuis notre ligne de commandes, tapons ceci :

> play crud:ov --template Comments/list

Nous disposons alors d'un template /yabe/app/views/Comments/list.html :

#{extends 'CRUD/layout.html' /}

<div id="crudList" class="${type.name}">

<h2 id="crudListTitle">&{'crud.list.title', type.name}</h2>

<div id="crudListSearch">
#{crud.search /}
</div>

<div id="crudListTable">
#{crud.table /}
</div>

<div id="crudListPagination">
#{crud.pagination /}
</div>

<p id="crudListAdd">
<a href="@{blank()}">&{'crud.add', type.modelName}</a>
</p>

</div>

La balise #{crud.table /} génère la table et son contenu. Il est possible de la paramétrer en utilisant l'attribut fields et en y spécifiant de nouvelles colonnes :

#{crud.table fields:['content', 'post', 'author'] /}

Notre tableau de commentaires affiche désormais trois colonnes :

Nous pouvons rencontrer ici un problème concernant la longueur du contenu du commentaire, qui peut s'avérer trop long pour un affichage correct. Nous avons la possibilité de dire à #{crud.table /} comment les informations doivent être affichées, et si cela est nécessaire, de les tronquer.

Nous allons spécifier à la balise #{crud.custom /} la manière d'afficher chaque champ :

#{crud.table fields:['content', 'post', 'author']}
#{crud.custom 'content'}
<a href="@{Comments.show(object.id)}">
${object.content.length() > 50 ? object.content[0..50] + '&#8230;' : object.content}
</a>
#{/crud.custom}
#{/crud.table}

Nous avons utilisé ici quelques raffinements du langage Groovy pour simplifier l'écriture du code !

VIII-F - Paramétrer le formulaire de publication

Nous avons vu que nous pouvions paramétrer les listes affichées par le module CRUD. Nous allons voir qu'il est aussi possible de configurer les formulaires de création. Par exemple, la façon dont nous sélectionnons les tags n'est guère pratique. Modifions le template Post/show :

> play crud:ov --template Posts/show

Cette commande nous crée un nouveau template /yabe/app/views/Posts/show.html :

#{extends 'CRUD/layout.html' /}

<div id="crudShow" class="${type.name}">

<h2 id="crudShowTitle">&{'crud.show.title', type.modelName}</h2>

<div class="objectForm">
#{form action:@save(object.id), enctype:'multipart/form-data'}
#{crud.form /}
<p class="crudButtons">
<input type="submit" name="_save" value="&{'crud.save', type.modelName}" />
<input type="submit" name="_saveAndContinue" value="&{'crud.saveAndContinue', type.modelName}" />
</p>
#{/form}
</div>

#{form @delete(object.id)}
<p class="crudDelete">
<input type="submit" value="&{'crud.delete', type.modelName}" />
</p>
#{/form}

</div>

Nous pouvons également corriger la balise #{crud.form /} pour faire en sorte de modifier le champ de sélection des tags :

#{crud.form}
#{crud.custom 'tags'}
<label for="tags">
&{'tags'}
</label>
<style type="text/css">
.tags-list .tag {
cursor: pointer;
padding: 1px 4px;
}
.tags-list .selected {
background: #222;
color: #fff;
}
</style>
<script type="text/javascript">
var toggle = function(tagEl) {
var input = document.getElementById('h'+tagEl.id);
if(tagEl.className.indexOf('selected') > -1) {
tagEl.className = 'tag';
input.value = '';
} else {
tagEl.className = 'tag selected';
input.value = tagEl.id;
}
}
</script>
<div class="tags-list">
#{list items:models.Tag.findAll(), as:'tag'}
<span id="${tag.id}" onclick="toggle(this)"
class="tag ${object.tags.contains(tag) ? 'selected' : ''}">
${tag}
</span>
<input id="h${tag.id}" type="hidden" name="${fieldName}"
value="${object.tags.contains(tag) ? tag.id : ''}" />
#{/list}
</div>
#{/crud.custom}
#{/crud.form}

Tout ceci est un peu artisanal, mais cela a le mérite de fonctionner...

Nous avons réussi à créer une interface d'administration tout à fait convenable !

IX - Ajout de l'authentification

Attaquons-nous désormais à un aspect important de notre site : l'authentification. Heureusement, Play propose un module pour cela ! Il s'agit du module Secure.

IX-A - Activation du module Secure

A l'image du module CRUD, il faut activer le module Secure en modifiant notre fichier /yabe/conf/application.properties, puis redémarrer le serveur :

# Import the secure module
module.secure=${play.path}/modules/secure

Normalement, lors du redémarrage, nous devrions être informé que ce module a été chargé.

Comme précédemment, ce module vient avec une série de routes prédéfinies que nous pouvons activer en éditant le fichier /yabe/conf/routes (nous pourrions bien sûr créer nos propres routes, si cela était nécessaire) :

# Import Secure routes
*       /                                       module:secure

IX-B - Protection de l'interface d'administration

En réalité, ce module propose un contrôleur controllers.Secure qui déclare tous les intercepteurs requis. Nous pourrions étendre cette classe Java, mais nous serions alors confrontés à la limitation du simple héritage de Java.

Ainsi, au lieu d'hériter directement de ce contrôleur, nous pouvons annoter notre contrôleur d'administration avec @With, qui indiquera à Play d'invoquer également les intercepteurs adéquats :

package controllers;

import play.*;
import play.mvc.*;

@With(Secure.class)
public class Posts extends CRUD {
}

Nous modifions de la même façon les classes Comment, User et Tag.

Si nous souhaitons accéder à la page d'administration, nous arrivons en réalité sur une page de connexion :

Pour l'heure, il suffit de renseigner n'importe quel couple login et mot de passe pour pouvoir passer cet écran.

IX-C - Configuration du processus d'authentification

Notre application doit disposer d'une instance du contrôleur controllers.Secure.Security afin de nous permettre de configurer le processus d'authentification. C'est grâce à cette implémentation que nous pourrons spécifier la manière exacte dont les utilisateurs devront s'authentifier.

Créons un fichier /yabe/controllers/Security.java et surchargeons la méthode authentify() :

package controllers;

import models.*;

public class Security extends Secure.Security {

static boolean authentify(String username, String password) {
return true;
}

}

Etant donné que nous disposons déjà d'un objet User, il est simple de créer une version fonctionnelle de cette méthode :

static boolean authentify(String username, String password) {
return User.connect(username, password) != null;
}

Maintenant, rendons-nous à l'adresse http://localhost:9000/logout pour se déconnecter, et réessayons de se connecter en utilisant cette fois-ci un utilisateur existant dans le fichier data.yml, tel que bob@gmail.com / secret.

IX-D - Refactorisation de l'interface d'administration

Nous avions construit notre interface d'administration en nous appuyant sur le module CRUD. Toutefois, cela s'intègre assez mal avec l'aspect visuel du reste du site. Nous allons donc travailler sur une nouvelle interface d'administration. Cette nouvelle mouture permettra à chaque auteur d'avoir accès à ses propres billets. Toutefois, nous conserverons l'administration basée sur le module CRUD pour les "super administrateurs".

Créons un nouveau contrôleur qui gèrera la partie administration :

package controllers;

import play.*;
import play.mvc.*;

import java.util.*;

import models.*;

@With(Secure.class)
public class Admin extends Controller {

@Before
static void setConnectedUser() {
if(Security.isConnected()) {
User user = User.find("byEmail", Security.connected()).first();
renderArgs.put("user", user.fullname);
}
}

public static void index() {
render();
}

}

Refactorisons les routes du fichier /yabe/conf/routes :

# Administration
GET     /admin/?                                Admin.index
*       /admin                                  module:crud

Maintenant, lions le texte "Se connecter pour écrire quelque chose" (Log in to write something) situé dans le fichier /yabe/app/views/main.html à ce contrôleur :

...
<ul id="tools">
<li>
<a href="@{Admin.index()}">Log in to write something</a>
</li>
</ul>
...

La dernière étape est de créer le template /yabe/app/views/Admin/index.html, en commençant par quelque chose de simple :

Welcome ${user}!

Revenons sur la page d'accueil de notre blog, et cliquons sur ce fameux lien. Si tout s'est bien passé, nous devrions être redirigés vers la nouvelle page d'administration :

C'est un bon début. Comme nous souhaitons disposer de plusieurs pages dans l'interface d'administration, nous allons créer un super template. Choisissons le fichier /yabe/app/views/admin.html pour l'y accueillir :

<!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" xml:lang="en" lang="en">
<head>
<title>Administration</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
#{get 'moreStyles' /}
<link rel="stylesheet" type="text/css" media="screen"
href="@{'/public/stylesheets/main.css'}" />
<link rel="shortcut icon" type="image/png"
href="@{'/public/images/favicon.png'}" />
<script src="@{'/public/javascripts/jquery-1.3.2.min.js'}"></script>
<script src="@{'/public/javascripts/jquery.tools.min.js'}"></script>
</head>
<body id="admin">

<div id="header">
<div id="logo">
yabe. <span>administration</span>
</div>
<ul id="tools">
<li>
<a href="@{Secure.logout()}">Log out</a>
</li>
</ul>
</div>

<div id="main">
#{doLayout /}
</div>

<p id="footer">
Yabe is a (not so) powerful blog engine built with the
<a href="http://www.playframework.org">play framework</a>
as a tutorial application.
</p>

</body>
</html>

Vous pourrez constater que cela est assez proche de ce que nous avons déjà fait pour la page de garde de notre blog. Nous avons changé le lien de connexion par un lien de déconnexion, qui appelle l'action du contrôleur Secure offert par le module de sécurité.

Faisons désormais usage de ce nouveau template dans le fichier /yabe/app/views/Admin/index.html :

#{extends 'admin.html' /}

Welcome ${user}!

Rafraichissons la page :

Si nous nous déconnectons, nous sommes redirigés vers l'écran de connexion :

C'est là la manière classique utilisée par le module de sécurité pour gérer les événements de déconnexion. Cela reste configurable très simplement en surchargeant la méthode onDisconnected() :

static void onDisconnected() {
Application.index();
}

Nous pouvons procéder de même avec l'événement onAuthenticated() :

static void onAuthenticated() {
Admin.index();
}

IX-E - Ajout des rôles

Concrètement, nous avons besoin de deux types d'interface d'administration : la première permettant l'écriture de billet, la seconde pour les super administrateurs. Nous avions ajouté dans la classe User une méthode isAdmin() qui indique justement si cet utilisateur dispose des droits d'administration.

Le module de sécurité offre non seulement le mécanisme d'authentification, mais également la gestion des autorisations. Cela s'appelle les profils. Pour créer un profile administrateur, nous avons juste à surcharger la méthode check() dans la classe controllers.Security :

static boolean check(String profile) {
if("admin".equals(profile)) {
return User.find("byEmail", connected()).<User>first().isAdmin;
}
return false;
}

Nous pouvons dès à présent créer un menu dans l'interface d'administration qui dépendra du profil. Modifions le fichier /yabe/view/main.html pour y ajouter un menu en haut :

...
<div id="main">

<ul id="adminMenu">
<li class="${request.controller == 'Admin' ? 'selected' : ''}">
<a href="@{Admin.index()}">My posts</a>
</li>
#{secure.check 'admin'}
<li class="${request.controller == 'Posts' ? 'selected' : ''}">
<a href="@{Posts.list()}">Posts</a>
</li>
<li class="${request.controller == 'Tags' ? 'selected' : ''}">
<a href="@{Tags.list()}">Tags</a>
</li>
<li class="${request.controller == 'Comments' ? 'selected' : ''}">
<a href="@{Comments.list()}">Comments</a>
</li>
<li class="${request.controller == 'Users' ? 'selected' : ''}">
<a href="@{Users.list()}">Users</a>
</li>
#{/secure.check}
</ul>

#{doLayout /}
</div>
...

Remarquez comment nous avons utilisé la balise #{secure.check /} pour afficher les options spécifiques aux administrateurs.

Cela n'est toutefois pas suffisant pour protéger la partie CRUD. Si un utilisateur connaît la bonne URL, il pourra y accéder ! Nous devons ainsi protéger les contrôleurs de la même façon. Cela se fait très simplement, grâce à l'annotation @Check. Ainsi, pour le contrôleur Post :

package controllers;

import play.*;
import play.mvc.*;

@Check("admin")
@With(Secure.class)
public class Posts extends CRUD {
}

Procédons de même pour les contrôleurs Tags, Comments et Users. Déconnectons-nous, puis essayons de nous connecter avec un utilisateur simple, par exemple jeff@gmail.com / secret. Normalement, nous ne devons pas voir le menu des administrateurs. De plus, une tentative d'accès direct à l'adresse http://localhost:9000/admin/users devrait nous retourner une erreur 403 (accès refusé) :

IX-F - Configurer la mise en page du CRUD

Lorsque nous accédons à la partie CRUD de l'interface d'administration, nous perdons la mise en page de cette dernière. Cela est dû au fait que le module CRUD propose sa propre mise en page. Bien sûr, nous pouvons la surcharger. Commençons par lancer cette commande :

> play crud:ov --layout

Cela nous crée un fichier /yabe/app/views/CRUD/layout.html. Remplaçons son contenu pour améliorer son intégration au reste de l'application :

#{extends 'admin.html' /}
#{set 'moreStyles'}
<link rel="stylesheet" type="text/css" media="screen"
href="@{'/public/stylesheets/crud.css'}" />
#{/set}

<div id="crud">

#{if flash.success}
<div class="crudFlash flashSuccess">
${flash.success}
</div>
#{/if}
#{if flash.error || error}
<div class="crudFlash flashError">
${error ?: flash.error}
</div>
#{/if}

<div id="crudContent">
#{doLayout /}
</div>

</div>

Nous réutilisons le fichier crud.css et l'incluons dans le admin.html en utilisant les mécanismes de get et set des templates. Maintenant, si nous retournons sur les pages du CRUD, nous devrions constater une amélioration de leur intégration :

IX-G - Style de l'écran de connexion

Maintenant que notre interface d'administration est cohérente avec le reste de l'application, intéressons-nous à l'écran de connexion. Comme toujours, cet écran est configurable. Lançons la commande suivante :

> play secure:ov --css

Nous allons juste conserver le fichier CSS dans son état, mais avant cela, nous importons notre propre CSS (main.css). Ajoutons donc la ligne suivante dans /yabe/public/stylesheets/secure.css :

@import url(main.css);
...

Paramétrons finalement les messages de cet écran en ajoutant au fichier /yabe/conf/messages les lignes suivantes :

secure.username=Your email:
secure.password=Your password:
secure.signin=Log in now

X - Création d'une zone de saisie personnalisée

Dans le chapitre précédent, nous avons créé une interface d'administration pour notre application yabe, et préparé la section "Mes billets" (My posts). Cette page va afficher, pour un auteur donné, l'ensemble de ses billets, avec la possibilité de les modifier ou d'en écrire de nouveaux.

Nous pourrions partir du module CRUD pour créer cette page, mais nous décidons ici de partir de zéro, car nous avons besoin de la rendre très personnalisable.

X-A - Lister les billets de l'auteur

Notre première étape consiste à récupérer la liste des billets écrits par l'utilisateur actuellement connecté. Améliorons donc notre action Admin.index :

public static void index() {
List<Post> posts = Post.find("author.email", Security.connected()).fetch();
render(posts);
}

Complétons également le template /yabe/app/views/Admin/index.html :

#{extends 'admin.html' /}

<h3>Welcome ${user}, <span>you have written ${posts.size() ?: 'no'} ${posts.pluralize('post', 'posts')} so far</span></h3>

#{list items:posts, as:'post'}
<p class="post ${post_parity}">
<a href="#">${post.title}</a>
</p>
#{/list}

<p id="newPost" >
<a href="#"><span>+</span> write a new post</a>
</p>

Notre premier écran est désormais disponible :

X-B - La page d'écriture d'un billet

Nous allons construire un formulaire destiné à l'écriture d'un nouveau billet. Généralement, pour un formulaire donné, nous avons deux actions : l'une pour afficher le formulaire, l'autre pour gérer sa soumission. Créons ainsi les actions Admin.form et Admin.save qui vont gérer ces deux actions.

Tout d'abord modifions le fichier /yabe/conf/routes :

GET     /admin/new                          Admin.form
POST    /admin/new                          Admin.save

Dans la classe Admin.java, ajoutons les méthodes form() et save() :

public static void form() {
render();
}

public static void save() {
// Not implemented yet
}

Bien sûr, créons le template /yabe/app/views/Admin/form.html :

#{extends 'admin.html' /}

<h3>Write, <span>a new post</span></h3>

#{form @save()}

#{ifErrors}
<p class="error">
Please correct these errors.
</p>
#{/ifErrors}

<p>
#{field 'title'}
<label>Post title:</label>
<input type="text" name="${field.name}"
value="${post?.title}" class="${field.errorClass}" />
#{/field}
</p>

<p>
#{field 'content'}
<label>Write here:</label>
<textarea name="${field.name}"
class="${field.errorClass}">${post?.content}</textarea>
#{/field}
</p>

<p>
#{field 'tags'}
<label>Enter some tags:</label>
<input type="text" size="50"
name="${field.name}" value="${post?.tags?.join(' ')}" />
#{/field}
</p>

<p>
<input type="submit" value="Publish this post to the blog" />
</p>

#{/form}

Editons le fichier /yabe/app/views/Admin/index.html pour lier le bouton "Ecrire un nouveau billet" (Write a new post) à ce nouveau formulaire :

...
<p id="newPost" >
<a href="@{form()}"><span>+</span> write a new post</a>
</p>
...

Vérifions le résultat :

Nous avons terminé la déclaration de l'action Admin.save pour gérer correctement la soumission du formulaire. Celle-ci va créer un nouvel objet Post, convertir la liste des tags en une liste d'objets Tag, valider les champs, puis finalement sauvegarder le tout. En cas de problème, cette action réaffichera le formulaire ainsi que les messages d'erreur.

public static void save(String title, String content, String tags) {
// Create post
User author = User.find("byEmail", Security.connected()).first();
Post post = new Post(author, title, content);
// Set tags list
for(String tag : tags.split("\\s+")) {
if(tag.trim().length() > 0) {
post.tags.add(Tag.findOrCreateByName(tag));
}
}
// Validate
validation.valid(post);
if(validation.hasErrors()) {
render("@form", post);
}
// Save
post.save();
index();
}

Ici, nous utilisons render("@form") comme un raccourci de render("Admin/form.html"). Cette forme d'écriture indique juste à Play d'utiliser le template par défaut pour cette action.

Nous pouvons tester...

X-C - Réutilisation pour l'édition d'un billet

Nous avons réalisé un formulaire HTML ainsi que l'action Java permettant d'écrire un nouveau billet. Nous voulons maintenant ajouter la fonctionnalité d'édition d'un billet. Réutiliser le code existant s'avère relativement facile et ne demandera que quelques modifications.

Tout d'abord, modifions l'action Admin.form pour récupérer un billet existant :

public static void form(Long id) {
if(id != null) {
Post post = Post.findById(id);
render(post);
}
render();
}

Comme vous pouvez le voir, nous avons rendu le paramètre id optionnel. De cette façon, nous n'afficherons le Post que si ce paramètre est renseigné. Nous pouvons lier la liste des billets de l'écran principal à la page d'édition. Editons le template /yabe/app/views/Admin/index.html :

#{extends 'admin.html' /}

<h3>Welcome ${user}, <span>you have written ${posts.size() ?: 'no'} ${posts.pluralize('post', 'posts')} so far</span></h3>

#{list items:posts, as:'post'}
<p class="post ${post_parity}">
<a href="@{Admin.form(post.id)}">${post.title}</a>
</p>
#{/list}

<p id="newPost" >
<a href="@{form()}"><span>+</span> write a new post</a>
</p>

Cela parait facile, mais nous avons toutefois un petit problème ici. Si nous regardons l'URL générée par le Router pour ces liens, nous voyons quelque chose comme ceci :

/admin/new?id=3

Cela fonctionne, mais ce n'est pas très joli ! Nous définissons donc un nouveau type de route lorsque l'attribut id est soumis :

GET     /admin/myPosts/{id}                 Admin.form
GET     /admin/new                          Admin.form

Constatez que nous définissons cette route avant l'ancienne, de telle façon à ce qu'elle ait une priorité plus forte. Cela signifie que si une id est soumise, Play privilégiera cette première route. Dans le cas contraire, ce sera la seconde route qui sera utilisée.

Rafraichissons la page de "Mes billets", et nous devrions voir une URL plus propre maintenant.

Il nous faut modifier le template /yabe/app/views/Admin/form.html :

#{extends 'admin.html' /}

#{ifnot post?.id}
<h3>Write, <span>a new post</span></h3>
#{/ifnot}
#{else}
<h3>Edit, <span>this post</span></h3>
#{/else}

#{form @save(post?.id)}

#{ifErrors}
<p class="error">
Please correct these errors.
</p>
#{/ifErrors}

<p>
#{field 'title'}
<label>Post title:</label>
<input type="text" name="${field.name}"
value="${post?.title}" class="${field.errorClass}" />
#{/field}
</p>

<p>
#{field 'content'}
<label>Write here:</label>
<textarea name="${field.name}"
class="${field.errorClass}">${post?.content}</textarea>
#{/field}
</p>

<p>
#{field 'tags'}
<label>Enter some tags:</label>
<input type="text" size="50"
name="${field.name}" value="${post?.tags?.join(' ')}" />
#{/field}
</p>

<p>
<input type="submit" value="Publish this post to the blog" />
</p>

#{/form}

Nous avons ici mis à jour l'action de destination du formulaire afin d'ajouter l'identifiant du billet comme premier paramètre, si celui-ci existe. Ainsi, si le billet a effectivement un identifiant (ce qui signifie qu'il s'agit d'un billet déjà existant), alors il sera envoyé à l'action Admin.save.

Nous pouvons désormais modifier un petit peu la méthode save() pour gérer aussi bien la création que l'édition d'un billet :

public static void save(Long id, String title, String content, String tags) {
Post post;
if(id == null) {
// Create post
User author = User.find("byEmail", Security.connected()).first();
post = new Post(author, title, content);
} else {
// Retrieve post
post = Post.findById(id);
// Edit
post.title = title;
post.content = content;
post.tags.clear();
}
// Set tags list
for(String tag : tags.split("\\s+")) {
if(tag.trim().length() > 0) {
post.tags.add(Tag.findOrCreateByName(tag));
}
}
// Validate
validation.valid(post);
if(validation.hasErrors()) {
render("@form", post);
}
// Save
post.save();
index();
}

Pour améliorer l'URL nous pouvons utiliser la même astuce que précédemment, en ajoutant une route qui sera privilégiée si un id est fourni :

POST    /admin/myPosts/{id}                  Admin.save
POST    /admin/new                          Admin.save

Voilà qui est terminé ! Nous utilisons désormais la même action pour créer de nouveaux billets, et éditer les existants. Notre travail sur l'interface d'administration est achevé !

XI - Terminer les tests de l'application

Nous avons fini le moteur de blog que nous voulions créer dans ce tutoriel. Toutefois, le projet n'est pas encore terminé. Pour que nous ayons une plus grande confiance dans notre code, il faut ajouter de nouveaux projets.

Bien sûr, nous avons déjà écrit des tests unitaires pour tester les fonctionnalités du modèle de données. Cela nous assure également que les fonctionnalités du cœur de notre application sont aussi testées. Mais une application web n'est pas uniquement constituée de cette partie. Nous avons besoin de nous assurer que l'interface visuelle fonctionne tel qu'attendu. Cela signifie qu'il faut également tester la couche des contrôleurs. Nous allons également avoir besoin de tester la partie graphique elle-même, ce qui inclue entre autre le code Javascript.

XI-A - Test de la couche contrôleur

Play offre un moyen de tester directement la couche de contrôleur d'une application grâce à JUnit. Il s'agit de tests fonctionnels, car nous voulons ici tester des fonctionnalités complètes de notre application web.

D'un point de vue technique, un test fonctionnel va appeler directement ActionInvoker, qui a pour but de simuler une requête HTTP. De fait, nous lui fournissons une méthode HTTP, une URI ainsi que des paramètres. Play routera ensuite cette requête, invoquera l'action correspondante, et retournera la réponse complète. Nous pourrons alors l'analyser et vérifier qu'elle contient ce que nous attendons.

Ecrivons un premier test fonctionnel. Ouvrons le fichier /yabe/test/ApplicationTest.java de test unitaire :

import org.junit.*;
import play.test.*;
import play.mvc.*;
import play.mvc.Http.*;
import models.*;

public class ApplicationTest extends FunctionalTest {

@Test
public void testThatIndexPageWorks() {
Response response = GET("/");
assertIsOk(response);
assertContentType("text/html", response);
assertCharset("utf-8", response);
}

}

Dans l'état actuel, cela ressemble beaucoup à un test JUnit classique. Notez que ce test hérite de la classe FunctionalTest afin de disposer des bonnes méthodes. Ce test est correct et va simplement vérifier que notre page d'accueil (ici l'URL "/") nous retourne un statut 200 (qui correspond à une réponse HTML signifiant que tout s'est bien passé).

Nous allons maintenant tester que la sécurité de l'interface d'administration fonctionne correctement. Ajoutons ce test au fichier ApplicationTest.java :

...
@Test
public void testAdminSecurity() {
Response response = GET("/admin");
assertStatus(302, response);
assertHeaderEquals("Location", "http://localhost/login", response);
}
...

Maintenant, lançons l'application yabe grâce à la commande play test, puis ouvrons la page http://localhost:9000/@tests. Sélectionnons ensuite le test ApplicationTest.java, lançons-le et vérifions que tout est OK :

Nous pourrions continuer à tester le reste des fonctionnalités de notre application de la même manière, mais ce n'est pas forcément la façon la plus simple de tester les applications web basées sur du HTML. Comme le blog est consultable via un navigateur web, l'idéal serait de le tester directement au sein de ce navigateur. C'est exactement ce que font les tests Selenium de Play.

Les tests fonctionnels basés sur JUnit restent toutefois utiles, par exemple dans le cas où nous aurions à tester un Web Service qui retourne une réponse qui n'est pas du HTML comme du JSON ou du XML...

XI-B - Ecriture des tests Selenium

Selenium est un outil dédié aux tests des applications web. L'un des ses intérêts est qu'il est possible d'exécuter les tests au sein de n'importe quel navigateur existant. Comme il n'utilise pas de "simulateur de navigateur", nous pouvons être sûr que ce qu'il teste est bien ce que dont les utilisateurs auront accès lors de leur visite sur notre site.

Un test Selenium est généralement écrit dans un fichier HTML. La syntaxe HTML utilisée par Selenium est un petit peu ennuyeuse à écrire (l'utilisation des tables HTML pour le formatage n'est pas très agréable par exemple). La bonne nouvelle, c'est que Play peut nous aider à générer ces tests grâce à un moteur de templates ainsi qu'un ensemble de tags qui simplifient la syntaxe de Selenium. Un effet de bord intéressant consécutif à l'utilisation de ces templates est que nous ne sommes plus cloisonnés aux scénarios statiques, et que nous pouvons dès lors utiliser les fonctionnalités de Play (boucles, blocs conditionnels...) pour écrire des tests plus complexes.

Il est toutefois possible de se limiter à la seule syntaxe Selenium pour écrire ces tests. Il devient alors très intéressant d'utiliser des outils de génération de tests tels que SeleniumIDE.

Lorsqu'on crée une nouvelle application avec Play, un test Selenium est également créé. Ouvrons donc le fichier /yabe/test/Application.test.html :

*{ You can use plain selenium commands using the selenium tag }*

#{selenium}
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}

Ce test devrait s'exécuter sans problème dans notre application courante. Il ouvre simplement la page d'accueil et vérifie que cette page n'est pas titrée "Application error".

Toutefois, comme n'importe quel test complexe, nous avons besoin d'un certain nombre de données de test avant de se lancer plus avant. Nous allons évidemment réutiliser le mécanisme de Fixtures et le fichier /yabe/test/data.yml pour cela. Afin d'importer ces données avant l'exécution des tests, nous allons utiliser la balise #{fixture /} :

#{fixture delete:'all', load:'data.yml' /}

#{selenium}
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}

Un autre point important à surveiller est de s'assurer que nous démarrons le test avec une session utilisateur toute neuve. Etant donné que les informations propres à une session sont conservées parmi les cookies du navigateur, nous pourrions malencontreusement conserver cette session au cours de tests successifs.

Démarrons les tests par une commande spécifique pour pallier ce problème :

#{fixture delete:'all', load:'data.yml' /}

#{selenium}
clearSession()

// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}

Exécutons ce test pour nous assurer que tout est toujours correct. Ce devrait être le cas !

Maintenant, écrivons un test plus spécifique. Ouvrons la page d'accueil et vérifions que les billets par défaut sont bien présents :

#{fixture delete:'all', load:'data.yml' /}

#{selenium 'Check home page'}
clearSession()

// Open the home page
open('/')

// Check that the front post is present
assertTextPresent('About the model layer')
assertTextPresent('by Bob, 14 Jun 09')
assertTextPresent('2 comments , latest by Guest')
assertTextPresent('It is the domain-specific representation')

// Check older posts
assertTextPresent('The MVC application')
assertTextPresent('Just a test of YABE')
#{/selenium}

Nous utilisons ici la syntaxe standard de Selenium, appelée Selenese.

Exécutons ce nouveau test :

Nous allons à présent tester le formulaire de commentaires. Ajoutons une nouvelle balise #{selenium /} à notre template :

#{selenium 'Test comments'}

// Click on 'The MVC application post'
clickAndWait('link=The MVC application')
assertTextPresent('The MVC application')
assertTextPresent('no comments')

// Post a new comment
type('content', 'Hello')
clickAndWait('css=input[type=submit]')

// Should get an error
assertTextPresent('no comments')
assertTextPresent('Author is required')
type('author', 'Me')
clickAndWait('css=input[type=submit]')

// Check
assertTextPresent('Thanks for posting Me')
assertTextPresent('1 comment')
assertTextPresent('Hello')
#{/selenium}

Exécutons ce test. Ah, cette fois-ci nous avons un échec, et même un sérieux problème :

Nous ne pouvons pas véritablement tester le mécanisme du captcha, nous devons du coup le feinter. En mode de test, nous considérons comme valide n'importe quelle valeur pour ce captcha. Nous savons que nous nous trouvons dans le mode test de Play quand l'ID du framework est égal à test. Ainsi, modifions notre action postComment() du fichier /yabe/app/controllers/Application.java pour considérer l'ID du framework :

...
if(!Play.id.equals("test")) {
validation.equals(code, Cache.get(randomID)).message("Invalid code. Please type it again");
}
...

Terminons par corriger notre code de test pour remplir le champ du captcha par une valeur fictive :

...
type('author', 'Me')
type('code', 'XXXXX')
clickAndWait('css=input[type=submit]')
...

Nous pouvons à nouveau relancer le test, qui, cette fois-ci, devrait passer.

XI-C - Mesurer la couverture de test

Bien entendu, nous n'avons pas écrit tous les tests requis pour cette application. Cependant, nous nous arrêterons là pour ce tutoriel. Dans un monde réel, comment pouvons-nous nous assurer que nous avons écrit suffisamment de cas de tests ? Nous avons pour cela besoin de mesurer la couverture de test.

Play propose un module de calcul de couverture de test basé sur l'outil Cobertura. Nous allons devoir activer ce module, mais uniquement pour le mode de test. Ajoutons ainsi cette ligne dans le fichier application.conf, puis redémarrons l'application en mode test :

# Import the cobertura module in test mode
%test.module.cobertura=${play.path}/modules/cobertura

Rouvrons notre navigateur, et rendons-nous sur la page http://localhost:9000/@tests. Sélectionnons l'ensemble des tests, et exécutons-les. Normalement, tous les indicateurs devraient être au vert.

Maintenant que tous les tests sont passés, stoppons l'application. Cobertura va alors générer ses rapports de couverture de test que nous pourrons retrouver dans le fichier /yabe/test-result/code-coverage/index.html :

Nous constatons que nous sommes encore loin d'une couverture complète de notre application. Une bonne suite de tests devrait nous permettre d'approcher les 100%, bien que cette valeur soit généralement quasiment impossible à atteindre (c'est là un autre débat ;) ).

XII - Préparation pour la production

Cette fois-ci, notre moteur de blog est terminé ! Finissons ce tutoriel par les étapes à réaliser avant de passer une telle application en production.

XII-A - Définition du framework id

Fort logiquement, nous voulons déployer notre application sur une autre machine que celle que nous avons utilisée jusqu'à présent pour développer. Nous allons ainsi avoir besoin d'une installation de Play différente.

Play nous autorise à assigner à chaque installation du framework un identifiant particulier (le framework id), puis de gérer différentes configurations dans le même fichier application.conf. Imaginons que notre serveur de production soit server01.

Une fois que le framework Play est installé sur ce serveur, définissons son framework id en tapant la commande play id.

Assignons alors server01 comme identifiant. Il nous est maintenant possible de définir des clés particulières dans le fichier de configuration qui ne seront disponibles que si l'application tourne sur ce serveur.

XII-B - Définir l'application en mode PROD

La première clé de configuration que nous voulons spécialiser pour le déploiement sur le serveur est la propriété application.mode. Jusqu'à présent, nous avons utilisé le mode DEV qui nous permet de faire du chargement à chaud de nos modifications, de recompiler les classes Java, et qui nous affiche des messages détaillés en cas d'erreur. En mode PROD, Play compilera toutes les classes Java ainsi que les templates lors du démarrage, et ne revérifiera pas, en cours d'exécution, si des modifications y ont été apportées.

Dans le fichier /yabe/conf/application.conf, définissons ceci :

%server01.application.mode=PROD

Maintenant, si nous démarrons l'application yabe sur le serveur, il démarrera automatiquement en mode PROD.

XII-C - Configurer une base MySQL

En production, nous souhaitons disposer d'une base de données MySQL, et non plus d'une base embarquée, en mémoire telle que HSQLDB, que nous avions utilisée jusqu'à présent. Play est livré avec un pilote (driver) JDBC pour MySQL, donc nous n'avons pas besoin d'installer quoique ce soit.

Editons le fichier /yabe/conf/application.conf :

%server01.db=mysql:root:secret@yabe

Nous allons changer la façon dont Hibernate gère le schéma de la base de données. Il est pratique qu'Hibernate mette à jour automatiquement le schéma lorsque le modèle de données évolue. Toutefois, cela reste un processus qui n'est pas toujours prévisible, et mettre ce genre de "magie" sur une base de données en production est quelque chose de dangeureux. C'est pour cela que nous laisserons Hibernate créer le schéma si celui-ci n'existe pas, mais nous ne lui laisserons pas la possibilité de le mettre à jour automatiquement.

Modifions ainsi la clé de configuration jpa.ddl :

%server01.jpa.ddl=create

XII-D - Mise en place d'un serveur HTTP frontal

Nous pourrions très bien changer le port par défaut (9000) pour le mettre sur le port 80. Cela nous limitera toutefois à ne disposer que d'une seule application Play sur un serveur donné. Pour pallier ce problème, nous allons utiliser un serveur HTTP frontal que nous utiliserons comme proxy inversé (reverse proxy).

Nous pouvons choisir n'importe quel serveur HTTP, mais il est souvent préférable d'en privilégier un léger et rapide, tel que LIGHTTPD.

La configuration exacte de ce serveur n'est pas l'objet de ce tutoriel, mais nous devrions avoir quelque chose comme cela :

server.modules = (
"mod_access",
"mod_proxy",
"mod_accesslog"
)
...
$HTTP["host"] =~ "www.yabe.com" {
proxy.balance = "round-robin" proxy.server = ( "/" =>
( ( "host" => "127.0.0.1", "port" => 9000 ) ) )
}

Finalement, autorisons le proxy à se connecter à notre application Play en ajoutant cette ligne à notre fichier de configuration :

%server01.XForwardedSupport=127.0.0.1

XII-E - Ce n'est que le début !

Si vous êtes arrivés à ce niveau du tutoriel, vous pouvez vous considérer comme un développeur Play chevronné ! Vous connaissez en tout cas les principaux concepts de ce framework, et du développement d'applications avec Play.

Il existe bien sûr d'autres fonctionnalités qui n'ont pas été traitées ici, particulièrement celles traitant des Web Services, de JSON, de XML... De plus, il existe d'autres modules qui proposent d'autres fonctionnalités. Enfin, Play continue d'évoluer très vite.

Si vous êtes convaincus que Play peut vous faire gagner du temps sur vos prochains développements d'applications web Java, alors vous êtes prêts à démarrer ! N'hésitez pas à poser vos questions sur le groupe Google Play.

XIII - Références

XIII-A - Références

- Site de Play

- Documentation

- Ce tutoriel en anglais

- Google group

- Play sur Twitter

XIII-B - Remerciements

Merci à l'équipe de Play, en particulier à Guillaume Bort pour son accord pour cette traduction.

Un grand merci à Bérénice Maurel (BiM) pour sa relecture attentive !