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"
>
|
${_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>