Ce tutoriel est destiné à appréhender le framework Grails de manière ludique, en construisant pas à pas une application simplifiée qui aura pour objectif de gérer une cave à vin personnelle.
Les étapes sont progressives et permettent à chacun d'avancer à son rythme
Au fur et à mesure des étapes, des exercices plus ou moins simples sont proposés, et des liens vers la documentation Grails vous permettent de vous aider à les résoudre.
En tout état de cause, une solution est proposée pour chaque exercice, vous êtes libre de la regarder ou pas lors de la résolution de l'exercice.
Ce chapitre décrit comment initialiser l'environnement avec Eclipse GGTS.
Vous pouvez récupérer la dernière version à jour ici.
Pour ceux qui préfèrent utiliser un autre IDE, vous pouvez passer directement à l'étape 2.
Une fois Eclipse installé :
Grails utilise un mécanisme de scripts pour générer automatiquement du code ou pour exécuter des commandes du framework (build, test, démarrage du serveur, ...)
Sous Eclipse, il est possible d'appeler directement les commandes Grails depuis une console intégrée.
Pour afficher la console, utiliser le raccourci Ctrl + Alt + Shift + G :
La commande help
affiche la liste des commandes disponibles, le raccourci Ctrl + Espace
permet d'auto-completer les commandes disponibles.
create-controller dojo.grails.Home
HomeControlleur.groovy
package dojo.grails class HomeController { def index() { render "Hello Cave à vin !" } }
Par convention, les vues sont placées dans le répertoire views, et prennent pour extension gsp (Grails Server Page).
index.gsp
dans le répertoire views/homeHomeController.groovy
et qui contiendra la chaine "Cave à vin".Grails utilise la notation Grails EL (Expression Language) pour mapper les variables de la vue avec les informations transmises par les actions du contrôleur.
HomeControlleur.groovy
package dojo.grails class HomeController { def index() { def value = “Cave à vin“ [value:value] } }
views/home/index.gsp
L'une des forces du framework Grails tient dans le nombre importants de plugins mis à disposition par la communauté ou directement par SpringSource.
Pour en être persuadé, jetez un oeil à la page Plugins qui propose plus de 900 plugins.
Bien souvent, lorsque l'on recherche une fonctionnalité particulière, l'un des premiers réflexes est d'aller vérifier si un plugin existe déjà pour le besoin identifié et si ce dernier répond au besoin.
En particulier, on y trouve le plugin spring-security-core qui va prendre en charge dans le cadre de notre application la gestion de la sécurité, des utilisateurs, des rôles et bien plus encore.
Pour installer un plugin, rien de plus simple, il suffit d'ouvrir le fichier BuildConfig.groovy
du répertoire conf et d'ajouter une entrée dans la section plugins.
Pour Spring Security, ajouter la ligne suivante : compile ":spring-security-core:1.2.7.3"
Exécuter la commande compile
afin que Grails récupère depuis Internet les dépendances nécessaires :
Comme nous pouvons le voir avec ce plugin, le mécanisme de commandes Grails est extensible.
En particulier, nous voyons que Spring Security a enrichi le jeu de commandes avec le script s2-quickstart
Ce script permet de générer le code des objets User, Role et UserRole, ainsi que les contrôleurs et vues qui permettent de tirer profit des mécanismes de sécurité fournis par Spring Security.
Exécutons ce script en précisant le package cible s2-quickstart dojo.grails User Role
:
Nous allons maintenant ouvrir le fichier Config.groovy
et modifier la configuration Spring Security de sorte à :
Plus d'infos sur le plugin Spring Security ici.
Config.groovy
... // Filtrage de l'acces à l'application sur le role USER grails.plugins.springsecurity.securityConfigType = "InterceptUrlMap" grails.plugins.springsecurity.interceptUrlMap = [ '/login/*': ['ROLE_ANONYMOUS'], '/**': ['ROLE_USER'] ]
Modifions les messages affichés à l'écran sur la page de connexion :
messages.properties
... # Messages Authentification springSecurity.login.header=Connexion Cave à Vin springSecurity.login.username.label=Login springSecurity.login.password.label=Mot de passe springSecurity.login.remember.me.label=Se souvenir de moi springSecurity.login.button=Ok springSecurity.errors.login.fail=Impossible de se connecter
Lorsqu'on redémarre l'application, on obtient bien :
De manière native, Grails offre une gestion multi-environnement qui permet d'adapter la configuration de l'application de manière spécifique à chaque environnement cible :
Config.groovy
et DataSource.groovy
DataSource.groovy
: environments { development { dataSource { dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', '' url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000" } } test { dataSource { dbCreate = "update" url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000" } } production { dataSource { dbCreate = "update" ... } } }
Grails propose de manière native un mécanisme qui permet d'initialiser un jeu de données au démarrage de l'application.
Pour cela, Grails fournit le fichier BootStrap.groovy
exécuté lors de chaque démarrage.
Notre application étant démarrée en environnement de type development, les données sont stockées dans une base mémoire H2.
Nous allons créer les données qui permettent d'alimenter la base de données avec un utilisateur de test :
BootStrap.groovy
import grails.util.Environment import dojo.grails.Role import dojo.grails.User import dojo.grails.UserRole class BootStrap { def init = { servletContext -> if (Environment.current == Environment.DEVELOPMENT) { def roleUser = new Role(authority: 'ROLE_USER').save() def user = new User(username: 'dojo', password: 'dojo', enabled: true).save() UserRole.create user, roleUser, true } } ... }
Afin de factoriser les fragments de pages, Grails offre le mécanisme de templates : il s'agit d'un fichier gsp dont le nom commence par le caractère "_".
On peut ensuite faire appel au template à partir du tag g:render
Factorisons le menu de l'application en créant un template dans le répertoire grails-app/views/menu :
_nav.gsp
Maintenant, il suffit d'inclure le tag <g:render template="/menu/nav" />
directement dans le layout global grails-app/views/layouts/main.gsp
pour appliquer le menu à toute l’application.
Afin d'harmoniser l'ensemble des pages, le layout et les CSS de notre application de démo ont été retravaillés.
Pour éviter de surcharger ce tutoriel, vous pouvez recopier directement les ressources suivantes dans votre projet :
La création d'interfaces HTML plus ou moins riches est simplifiée par l'utilisation de Taglibs
.
Grails propose un mécanisme de namespaces pour permettre aux plugins de proposer leur propre jeu de tags.
Le namespace par défault de Grails est "g", celui de Spring Security est sec.
Utiliser les différentes taglibs Grails et Spring Security pour arriver au résultat ci-dessous :
sec:ifLoggedIn
_nav.gsp
Grails intègre une surcouche à Hibernate nommée GORM (Grails ORM).
Cette couche est destinée à masquer les complexité d'Hibernate et permet de :
Plus d'informations sur la documentation GORM ici
Dans le cadre de notre application, Spring Security s'étant occupé de générer les objets de domaine User et Role, il reste à modéliser les entités Cave et Bouteille.
create-domain-class dojo.grails.Cave
Cave.groovy
et CaveTests.groovy
name
afin qu'il respecte les contraintes suivantes : unique, obligatoire, n'autorise pas de chaine vide, avec une longueur maximale de 15 caractères.User.groovy
... Cave cave ... static constraints = { ... cave nullable: true }
Cave.groovy
class Cave { String name static belongsTo = [user: User] static constraints = { name blank: false, unique: true, maxSize: 15 } }
Bootstrap.groovy
dans une base de données réelle, ...Comme nous l'avons vu lors de la génération de la classe de domaine Cave, le script create-domain-class
a également généré le squelette de la classe de test CaveTests.groovy
.
Il est temps à présent de compléter cette classe de tests.
Compléter le test unitaire du fichier CaveTests.groovy
de sorte à tester les différentes contraintes, et faire en sorte que la méthode validate fournie par Grails sur les objets de domaine soit rendue passante.
Vous pouvez exécuter les tests soit à partir de l'IDE : clic droit sur la classe de test puis Run As / Grails command (test-app)
, soit via le script test-app -unit CaveTests
.
CaveTests.groovy
@TestFor(Cave) class CaveTests { void testCaveConstraints() { def cave = new Cave(name: "cave") // Permet de mocker la methode validate mockForConstraintsTests(Cave, [cave]) // Test sans user attache a l'instance de cave assertFalse cave.validate() // Test nullable def testCave = new Cave() assertFalse testCave.validate() assertEquals "nullable", testCave.errors["name"] // Test blank testCave = new Cave(name: "") assertFalse testCave.validate() assertEquals "blank", testCave.errors["name"] // Test maxSize testCave = new Cave(name: "Mon nom de cave beaucoup trop long") assertFalse testCave.validate() assertEquals "maxSize", testCave.errors["name"] // Test unique testCave = new Cave(name: "cave") assertFalse testCave.validate() assertEquals "unique", testCave.errors["name"] // Test ok User fakeUser = new User(username: 'fake', password: 'fake') testCave = new Cave(name: "Ma cave", user: fakeUser) assertTrue testCave.validate() assertEquals 'fake', testCave?.user?.username } }
Ajoutons à présent le relation OneToMany entre Cave et Bouteille.
Bouteille.groovy
class Bouteille { String label Integer millesime byte[] photo static belongsTo = [cave: Cave] static constraints = { label blank: false, unique: true millesime min: 1900, max: 2013 photo nullable: true, maxSize: 65536 } }
Cave.groovy
class Cave { ... static hasMany = [bouteilles: Bouteille] static mapping = { bouteilles cascade: 'all-delete-orphan' } ... }
Afin d'éprouver notre modèle complet en condition réelle, écrivons notre premier test d'intégration.
Pour créer le squelette de la classe de test, exécutons le script create-integration-test dojo.grails.CaveIntegration
Compléter la classe de test en créant une instance Cave, contenant deux bouteilles et en faisant des tests sur le résultat après enregistrement en base de données.
Vous pourrez utiliser la methode .save(failOnError: true, flush: true)
pour la sauvegarde, la méthode dynamique addTo
pour ajouter une bouteille à la collection de bouteilles de la cave, ainsi que le finder dynamique findByUsername
pour retrouver l'utilisateur que vous aurez créer au préalable dans la méthode setUp()
de la classe de test.
CaveIntegrationTests.groovy
class CaveIntegrationTests { @Before void setUp() { // Setup logic here new User(username: 'test', password: 'test').save(flush: true) } @After void tearDown() { // Tear down logic here } @Test void "Creation nominale d'une Cave avec liste de bouteilles"() { // On recupere le user de test def user = User.findByUsername('test') assertNotNull user def cave = new Cave(name: "Ma cave", user: user) def bouteille1 = new Bouteille(label: "Ma bouteille 1", millesime: 2012) def bouteille2 = new Bouteille(label: "Ma bouteille 2", millesime: 2012) cave.addToBouteilles(bouteille1); cave.addToBouteilles(bouteille2); cave.save(failOnError: true, flush: true) assertNotNull cave assertNotNull cave.name def found = Cave.findByName("Ma cave") assertEquals 'Ma cave', found.name assertEquals found.bouteilles*.millesime, [2012, 2012] } }
Le mécanisme de Scaffolding apporté par Grails permet de générer automatiquement les vues et les actions de contrôleur permettant de traiter automatiquement les aspects CRUD d'un objet de domaine, en tenant compte des contraintes, et du mapping d'un objet métier.
Pour simplifier le mécanisme dans l'application de démo "Cave à Vin", nous ne génèrerons que le Scaffolding de l'objet Bouteille ; pour cela, modifions le fichier BootStrap.groovy
pour associer directement une instance de Cave à notre utilisateur ogil :
BootStrap.groovy
... // Simplification du jeu de données Cave cave = new Cave(id: 1L, name: "Ma Cave perso", user: user).save(flush: true) ...
Modifions le mapping d'URL de la page d'accueil par défaut pour utiliser notre contrôleur Home déclaré au début du tutoriel :
UrlMappings.groovy
static mappings = { "/$controller/$action?/$id?"{ constraints { // apply constraints here } } "/"(controller:"home", action:"/index") "500"(view:'/error') }
Modifions notre contrôleur Home pour récupérer notre cave à vin :
HomeControlleur.groovy
def index() { Cave cave = Cave.get(1L) [cave: cave] }
Modifions la vue views/home/index.gsp pour afficher le nom de notre cave :
views/home/index.gsp
Démarrer le serveur et vérifier le résultat :
Faisons un premier essai en testant le Scaffolding dynamique sur l'entité Bouteille :
create-controller dojo.grails.Bouteille
Modifier le contrôleur :
BouteilleController.groovy
class BouteilleController { static scaffold = true }
Ajoutons une entrée dans le menu gauche pour accéder à la page de gestion des bouteilles :
views/menu/_nav.gsp
Redémarrer et essayer de créer une bouteille avec des données erronées, on obtient :
Créer une bouteille avec des données correctes : les différentes fonctions CRUD sont opérationnelles :
Sans avoir écrit une seule ligne de code, nous avons déjà obtenu résultat interessant avec le Scaffolding statique.
Toutefois, on se rend bien compte que ce n'est pas adapté à une utilisation directe en production.
Voyons à présent comment Grails peut nous aider à améliorer notre productivité, tout en continuant à nous faire profiter de la puissance du Scaffolding, mais en utilisant cette fois le Scaffolding statique.
Utilisons cette fois la commande suivante :
generate-all dojo.grails.Bouteille
Accepter l'overwrite des fichiers au prompt dans la console.
Cette fois, le contrôleur BouteilleController.groovy
, la classe de test BouteilleControllerTests.groovy
ainsi que l'ensemble des vues views/bouteille/*
ont été générés.
Cela nous permet d'avoir un contrôle complet sur l'ensemble des éléments générés par le Scaffolding.
Retoucher les différentes vues pour améliorer le rendu des différentes pages. A minima, retoucher les vues list, show ainsi que le contrôleur pour faire en sorte d'afficher les photos des bouteilles.
Vous pouvez utiliser les images de bouteilles mis à disposition dans le dossier image/bouteilles du tutoriel.
BouteilleController.groovy
class BouteilleController { ... def displayPhoto() { if (!params.id) { response.sendError(404) return } def bouteille = Bouteille.get(params.id) if (bouteille.photo) { byte[] image = bouteille.photo response.setHeader('Cache-Control', 'no-cache') response.outputStream << image response.outputStream.flush() } } }
views/bouteille/list.gst
Pour en savoir plus sur le scaffolding : ici.
En particulier, vous verrez qu'il est possible de customiser entièrement les templates utilisés par Grails pour la génération du code du contrôleur, vues, et classes de test : http://grails.org/doc/latest/ref/Command%20Line/install-templates.html
Bien que les services métiers ne soient pas directement intégrés dans la génération du Scaffolding, Grails recommande fortement de déplacer la logique métier dans des méthodes de services, qui permettent de mieux réutiliser le code, et qui prennent aussi en charge les aspects transactionnels.
Dans le cadre de notre application de démo, crééons notre premier et unique service CaveVinService, à partir de la commande Grails create-service dojo.grails.CaveVinService
.
Ajoutez une méthode de service qui permettra de récupérer les 5 dernières bouteilles créées par l'utilisateur connecté.
Amusez-vous à écrire la requête de récupération en HQL, Criteria et via le mécanisme de Finders dynamiques.
Tester les trois méthodes avec un test d'intégration généré à partir de la commande : create-integration-test dojo.grails.CaveVinServiceIntegration
SpringSecurityService springSecurityService
springSecurityService.getCurrentUser()
CaveVinService.groovy
class CaveVinService { static transactional = false // Injection du service Spring Security SpringSecurityService springSecurityService final static int LIMIT = 5 def findBouteillesRecentesByDynamicFinder() { def current = springSecurityService.getCurrentUser() return Bouteille.findAllByCave(current.cave, [max: LIMIT, sort: "id", order: "desc"]) } def findBouteillesRecentesByHQL() { def current = springSecurityService.getCurrentUser() return Bouteille.executeQuery("FROM Bouteille b where b.cave = :cave order by b.id desc", [cave: current.cave, max: LIMIT]) } def findBouteillesRecentesByCriteria() { def current = springSecurityService.getCurrentUser() def criteria = Bouteille.createCriteria() criteria.list { cave { eq('id', current.cave?.id) } maxResults LIMIT order 'id','desc' } } }
CaveVinServiceIntegrationTests.groovy
class CaveVinServiceIntegrationTests { // Injection des services def springSecurityService CaveVinService caveVinService // Donnees de test attendues static final expectedList = ['bouteille6', 'bouteille5', 'bouteille4', 'bouteille3', 'bouteille2'] static final expectedNumber = 5 @Before void setUp() { def user = new User(username: 'test', password: 'test').save(flush: true) // Force l'authentification du user 'test' springSecurityService.reauthenticate 'test' def cave = new Cave(name: "Ma cave", user: user) int index = 1 def bouteille 6.times { bouteille = new Bouteille(label: 'bouteille' + index, millesime: 1950) cave.addToBouteilles(bouteille) // On force la sauvegarde à chaque iteration pour garantir l'ordre de creation pour notre test cave.save(flush: true) index++ } } @Test void "Recherche des bouteilles recentes par dynamic finders"() { def bouteilles = caveVinService.findBouteillesRecentesByDynamicFinder() assertEquals bouteilles.size, expectedNumber assertEquals bouteilles*.label, expectedList } @Test void "Recherche des bouteilles recentes par HQL"() { def bouteilles = caveVinService.findBouteillesRecentesByHQL() assertEquals bouteilles.size, expectedNumber assertEquals bouteilles*.label, expectedList } @Test void "Recherche des bouteilles recentes par Criteria"() { def bouteilles = caveVinService.findBouteillesRecentesByCriteria() assertEquals bouteilles.size, expectedNumber assertEquals bouteilles*.label, expectedList } @After void tearDown() { // Tear down logic here } }
Terminons notre application de démo en intégrant une fonctionnalité Ajax, de sorte à voir comment Grails addresse cette problématique récurrente.
Pour faire simple, nous allons afficher en Ajax sur la page d'accueil, la liste des 5 dernières bouteilles ajoutées, en faisant appel à notre service précédenment créé.
bouteillesRecentes.gsp
dans views/home/ qui permettra d'afficher la liste des dernières bouteillesviews/home/index.gsp
pour afficher au chargement de la page la liste des 5 dernières bouteilles en utilisant un appel Ajax.HomeController.groovy
class HomeController { CaveVinService caveVinService ... def bouteillesRecentes() { [bouteilles: caveVinService.findBouteillesRecentesByCriteria()] } }
views/home/bouteillesRecentes.gsp
views/home/index.gsp
Les fichiers sources de l'application Cave à vin se trouvent ici.