Aller au menu - Aller au contenu

Icône Les filtres

Mise à jour : 18/02/2011
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-NC-SA
10 160 visites depuis 7 jours, dont 215 sur ce chapitre classé 25/786
Déjà, les filtres en JEE n'ont pas grand-chose à voir avec les filtres au sens où vous l'entendez (si vous entendez filtre à café).
En fait, les filtres permettent d'intercepter les requêtes faites au serveur et d'y assigner des tâches communes à une, plusieurs ou toutes les servlets.

Ceci peut s'avérer très utile par exemple pour encapsuler l'identification, faire des statistiques sur votre application ou encore d'autres choses intéressantes.

Bon, je me doute que tout ceci doit vous paraître très flou, je vous propose donc de voir tout ça et de mettre un exemple en place.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Fonctionnement

Afin que vous puissiez comprendre comment fonctionnent les filtres, je vous ai préparé un schéma :

Image utilisateur


Dans le cas présent, le client demande d'accéder à l'adresse "http://monsite.fr/index". Ce chemin revient, sur le serveur, à invoquer la servlet nommée ServletIndex. Au préalable, via le fichier web.xml, nous avons informé Tomcat que cette servlet utilisera un filtre (qui est une classe java, vous vous en doutiez j'en suis sûr).

Ce qu'il se passe alors est simple :
  • la requête est reçue par le conteneur ;
  • le conteneur sait qu'il doit utiliser un filtre pour la servlet demandée ;
  • il invoque le filtre ;
  • ensuite, pendant l'exécution du filtre, le contenu de la servlet est exécuté ;
  • on finit l'exécution du filtre ;
  • la réponse est retournée.


On passe deux fois dans le filtre ?

En fait, non… C'est l'invocation de la servlet qui est, dans ce cas, encapsulée dans le filtre.
Maintenant, je vous disais dans l'introduction que les filtres peuvent être cumulés avant l'invocation d'une servlet, voici un schéma vous montrant ce qui peut arriver :

Image utilisateur


Cette fois, la ressource demandée est à l'adresse "http://monsite.fr/panier". La servlet associée est ServletPanier à laquelle nous avons attaché plusieurs filtres (dans un certain ordre dans le fichier web.xml). Ceux-ci sont exécutés à tour de rôle pour, enfin, invoquer notre servlet et obtenir la réponse.

Le plus beau dans l'histoire, c'est que tout est automatique…

Euh… comment le tout est invoqué ?

En fait, vous devez savoir que ce mode de fonctionnement est lié au pattern Interceptor. Celui-ci permet d'ajouter des actions à effectuer dans un environnement, sans modifier le fonctionnement de cet environnement. C'est un peu flou, j'en conviens mais, avec les deux exemples ci-dessus, vous devriez comprendre où je veux en venir : le pattern permet d'ajouter du code à exécuter sans avoir à modifier nos objets existants ni leurs façons de travailler.

Avant d'aller plus loin dans l'explication, voici les objets que nous allons utiliser et comment ils sont liés entre eux :

Image utilisateur


En fait, voici ce qu'il se passe dans notre conteneur pour le cas d'une servlet ayant plusieurs filtres :

  • le conteneur crée un objet FilterChain ;
  • il crée un objet concret par filtre et l'ajoute, dans l'ordre, dans l'objet FilterChain. C'est lui qui a la tâche d'invoquer les filtres dans l'ordre ;
  • le conteneur appelle, à tour de rôle et dans l'ordre, la méthode doFilter() des objets implémentant l'interface Filter ;
  • lorsque le dernier filtre a été invoqué (les méthodes doFilter() sont empilées les unes sur les autres), la servlet est à son tour appelée.


Les méthodes de filtres sont empilées jusqu'à l'invocation de la servlet. Ensuite, les invocations se dépilent. Voici un schéma représentant ce que je viens de dire :

Image utilisateur


Maintenant que vous savez comment les filtres fonctionnent, nous allons voir un exemple.

Utiliser les filtres

Afin d'avoir une exemple parlant et facile à mettre en place, je vous propose de reprendre l'exemple que nous avons utilisé dans le chapitre sur les sessions…
Nous allons cependant ajouter des filtres à tout ceci :
  • un filtre à la servlet récupérant les données du formulaire ;
  • deux filtres à la servlet affichant les données mises en session.


connexion.jsp



Code : JSP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Connexion à l'application</title>
</head>
<body>
<form name="session" method="POST" action="session.do" /> 
	<table>
		<tr>
			<td>Login : </td>
			<td><input type="text" name="login" /> </td>
		</tr>
		<tr>
			<td>Mot de passe : </td>
			<td><input type="password" name="password" /> </td>
		</tr>
		<tr>
			<td colspan="2" style="text-align:center"><input type="submit" name="valider" /></td>
		</tr>
	</table>
</form>
</body>
</html>



TestFilter.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.sdz.session;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class TestFilter extends HttpServlet {

	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		System.out.println("Traitement dans la servlet : " + this.getServletName() + ".");
		
		//On récupère les identifiants et on les stocke dans notre objet de session
		String login = req.getParameter("login");
		String password = req.getParameter("password");
		HttpSession session = req.getSession();
		session.setAttribute("login", login);
		session.setAttribute("password", password);
		
		//Maintenant, nous allons utiliser cet objet dans une autre servlet
		resp.sendRedirect(resp.encodeURL("result.do"));		
	}

	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
	
		doGet(req, resp);
	}
}



ResultatFilter.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.sdz.session;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class ResultatFilter extends HttpServlet {

	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		System.out.println("Traitement dans la servlet : " + this.getServletName() + ".");
		HttpSession session = req.getSession();
		PrintWriter out = resp.getWriter();
		
		//On va rajouter un attribut s'il n'existe pas
		//Et, s'il existe, on va l'incrémenter
		Object increment = session.getAttribute("increment");
		if(increment == null){
			session.setAttribute("increment", new Integer(1));
		}
		else{
			int value = ((Integer)increment).intValue();
			session.setAttribute("increment", ++value);
		}
		
		//On affiche l'identifiant de session unique
		out.println("<h2>Numéro de la session en cours : " + session.getId() + "</h2>");
		
		//On parcourt maintenant le contenu de notre objet session 
		Enumeration e = session.getAttributeNames();
		while(e.hasMoreElements()){
			String key = (String)e.nextElement();
			out.println("<p>Clé : " + key + " - Valeur : " + session.getAttribute(key) + "</p>");
			
		}
	}

	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		doGet(req, resp);
	}

}


FirstFilter.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.sdz.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class FirstFilter implements Filter{

	private FilterConfig config;
	
	public void destroy() {	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		System.out.println("Premier filtre.");
		//C'est via cette instruction que les filtres suivants et la servlet seront invoqués
		chain.doFilter(request, response);		
		System.out.println("Fin du premier filtre.\n");
	}

	public void init(FilterConfig config) throws ServletException {
		this.config = config;
	}

}



SecondFilter.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.sdz.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class SecondFilter implements Filter{

	private FilterConfig config;
	
	public void destroy() {	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		System.out.println("Second  filtre.");
		//C'est via cette instruction que les filtres suivants et la servlet seront invoqués
		chain.doFilter(request, response);
		System.out.println("Fin du second filtre.\n");
	}

	public void init(FilterConfig config) throws ServletException {
		this.config = config;
	}

}


ThirdFilter.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.sdz.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class ThirdFilter implements Filter{

	private FilterConfig config;
	
	public void destroy() {	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		System.out.println("Troisième  filtre.");
		//C'est via cette instruction que les filtres suivants et la servlet seront invoqués
		chain.doFilter(request, response);
		System.out.println("Fin du troisième filtre.");
	}

	public void init(FilterConfig config) throws ServletException {
		this.config = config;
	}

}


web.xml



Code : XML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<web-app>

	<servlet>
		<servlet-class>com.sdz.session.TestFilter</servlet-class>
		<servlet-name>firstFilter</servlet-name>
	</servlet>
	
	<servlet>
		<servlet-class>com.sdz.session.ResultatFilter</servlet-class>
		<servlet-name>resultFilter</servlet-name>
	</servlet>
	
			
	<servlet-mapping>
		<servlet-name>firstFilter</servlet-name>
		<url-pattern>/session.do</url-pattern>
	</servlet-mapping>
	
	<servlet-mapping>
		<servlet-name>resultFilter</servlet-name>
		<url-pattern>/result.do</url-pattern>
	</servlet-mapping>
	
	
	<filter>
		<filter-name>firstFilter</filter-name>
		<filter-class>com.sdz.filter.FirstFilter</filter-class>
	</filter>
	
	<filter>
		<filter-name>secondFilter</filter-name>
		<filter-class>com.sdz.filter.SecondFilter</filter-class>
	</filter>
	
	<filter>
		<filter-name>thirdFilter</filter-name>
		<filter-class>com.sdz.filter.ThirdFilter</filter-class>
	</filter>
	
	<filter-mapping>
		<filter-name>firstFilter</filter-name>
		<servlet-name>firstFilter</servlet-name>
	</filter-mapping>
	
	<filter-mapping>
		<filter-name>secondFilter</filter-name>
		<url-pattern>/result.do</url-pattern>
	</filter-mapping>
	
	<filter-mapping>
		<filter-name>thirdFilter</filter-name>
		<url-pattern>/result.do</url-pattern>
	</filter-mapping>
</web-app>


L'affichage nous donne la même chose que dans le chapitre sur les sessions :

Image utilisateur


Après avoir validé le formulaire :

Image utilisateur


Et voici ce qui s'affiche dans la console d'Eclipse :

Image utilisateur


Vous constatez que les filtres sont bien invoqués et que la servlet est bien appelée pendant l'exécution de la méthode doFilter(). Je vais vous expliquer ce que j'ai fait.

Les classes qui servent de filtres doivent implémenter l'interface Filter et, par conséquent, redéfinir les méthodes de l'interface ; ça, vous l'aviez deviné, j'en suis sûr.
Dans la méthode init(FilterConfig config), nous avons initialisé une variable d'instance que nous avons ajoutée à nos filtres. Ceci juste au cas où nous aurions besoin d'accéder aux informations véhiculées dans cet objet lorsque nous serons dans les méthodes doFilter() ou destroy(). Puis nous avons défini ce qui devait être fait dans les méthodes doFilter() de chaque filtre en prenant soin d'invoquer la méthode doFilter() de l'objet FilterChain.

Si cette instruction est absente, la pile d'invocation est stoppée et, par conséquent, les filtres suivants (s'il y en a) et finalement la servlet ne seront JAMAIS appelés : donc, on se retrouve avec une page blanche… Essayez de commenter cette ligne de code et vous verrez !


Une fois que toutes les classes sont définies, il ne reste plus qu'à tout relier via le descripteur de déploiement (le fichier web.xml). Ceci se fait grâce au bloc XML :

Code : XML
1
2
3
4
<filter>
	<filter-name>nomDuFiltre</filter-name>
	<filter-class>mon.package.MonFiltre</filter-class>
</filter>


Avec ce bloc, nous informons le conteneur qu'un filtre existe. Maintenant, avec ce bloc :

Code : XML
1
2
3
4
<filter-mapping>
	<filter-name>nomDuFiltre</filter-name>
	<servlet-name>nomDeLaServlet</servlet-name>
</filter-mapping>


ou celui-ci :

Code : XML
1
2
3
4
<filter-mapping>
	<filter-name>nomDuFiltre</filter-name>
	<url-pattern>*.do</url-pattern>
</filter-mapping>


... nous définissons sur quel critère appliquer le (ou les) filtre(s) :
  • sur un nom de servlet ;
  • sur une page spécifique ou un sous-ensemble d'URL.


Qu'est-ce que tu veux dire par un sous-ensemble ?

Dans notre exemple utilisant un mapping d'URL, nous avons utilisé le critère result.do, mais si nous avions mappé notre servlet avec un sous-nom, comme ceci :

Code : XML
1
2
3
4
<servlet-mapping>
	<servlet-name>resultFilter</servlet-name>
	<url-pattern>/RESULT/result.do</url-pattern>
</servlet-mapping>


... il nous serait alors possible d'ordonner au conteneur d'utiliser le filtre pour les servlets qui ont un chemin contenant le terme "RESULT" :

Code : XML
1
2
3
4
<filter-mapping>
	<filter-name>secondFilter</filter-name>
	<url-pattern>/RESULT/*</url-pattern>
</filter-mapping>


IMPORTANT : pour déterminer l'ordre des filtres, le conteneur prend en priorité les filtres mappés sur une URL. Une fois tous ces filtres trouvés et créés, le conteneur regarde ensuite les filtres éligibles via un nom de servlet.


Ça ressemble beaucoup aux blocs utilisés pour définir et mapper nos servlets…

N'est-ce pas ? C'est tellement vrai que vous avez aussi la possibilité d'affecter des paramètres d'initialisation à vos filtres, comme ceci :

Code : XML
1
2
3
4
5
6
7
8
9
<filter>
	<filter-name>firstFilter</filter-name>
	<filter-class>com.sdz.filter.FirstFilter</filter-class>
	
	<init-param>
		<param-name>param</param-name>
		<param-value>Valeur du paramètre de filtre</param-value>
	</init-param>
</filter>


Ces paramètres sont accessibles via l'objet FilterConfig, objet que nous avons ajouté en variable d'instance, vous comprenez mieux pourquoi. ;)

Vous aurez pu constater que je n'ai rien mis de particulier dans la méthode destroy(), ceci juste parce qu'il n'y avait rien de spécial à mettre… Pour être honnête, il y a fort à parier que vous ne vous en servirez jamais.

Filtres et RequestDispatcher

Pourquoi tu veux nous parler de ça ?

:)

Je vous demande, dans la servlet TestFilter, de remplacer cette ligne de code :

Code : Java
1
resp.sendRedirect(resp.encodeURL("result.do"));


par celle-ci :

Code : Java
1
req.getRequestDispatcher("result.do").forward(req, resp);


Maintenant, lorsque vous testez à nouveau notre code, vous devriez avoir ceci dans la console :

Image utilisateur


Comment ça se fait ? Le deuxième filtre ne marche plus !

Ce n'est pas tout à fait ça… Le filtre fonctionne toujours, c'est juste qu'utiliser l'objet RequestDispatcher ne simule pas une nouvelle requête vers la ressource demandée (la deuxième servlet dans notre cas), elle se contente de travailler avec.

Ce que je ne vous avais pas encore dit à propos des filtres, c'est que le déclencheur par défaut est une requête faite au serveur. En fait, le mapping des filtres prend un paramètre par défaut qui est celui-ci :

Code : XML
1
2
3
4
5
<filter-mapping>
	<filter-name>secondFilter</filter-name>
	<url-pattern>/result.do</url-pattern>
	<dispatcher>REQUEST</dispatcher>
</filter-mapping>


Étant donné que c'est la valeur par défaut, il n'est pas utile de mettre cet attribut. Par contre, vu que nous passons par un RequestDispatcher et que nous souhaitons tout de même appliquer le filtre, il va falloir compléter la définition du mapping. Les valeurs autorisées pour l'attribut "dispatcher" sont les suivantes :

  • REQUEST : c'est la valeur par défaut. Cela signifie que le filtre est appliqué : dès qu'il y a une requête faite au serveur ;
  • FORWARD : active le filtre lorsque la méthode forward(request, response) de l'objet RequestDispatcher est invoquée ;
  • INCLUDE : active le filtre lorsque la méthode include(request, response) de l'objet RequestDispatcher est invoquée ;
  • ERROR : active le filtre lorsqu'il y a une redirection vers une page d'erreurs.


Vous connaissez déjà REQUEST et FORWARD, vous vous demandez donc ce que fait la méthode include(request, response). En fait, elle est très similaire à forward(request, response) et afin que vous puissiez en voir clairement la différence, je vous invite à modifier la servlet TestFilter comme ceci :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.sdz.session;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class TestFilter extends HttpServlet {

	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		System.out.println("Traitement dans la servlet : " + this.getServletName() + ".");
		
		//On récupère les identifiants et on les stocke dans notre objet de session
		String login = req.getParameter("login");
		String password = req.getParameter("password");
		HttpSession session = req.getSession();
		session.setAttribute("login", login);
		session.setAttribute("password", password);
		PrintWriter out = resp.getWriter();
		out.println("<h2>include</h2>");
		//Maintenant, nous allons utiliser cet objet dans une autre servlet
		req.getRequestDispatcher("result.do").forward(req, resp);
	}

	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
	
		doGet(req, resp);
	}
}


On a juste écrit quelque chose dans la réponse avant d'invoquer la deuxième servlet. ^^
Lorsque vous testez votre code, rien ne change. Maintenant, à la place de forward, mettez include, comme ceci req.getRequestDispatcher("result.do").include(req, resp);, testez à nouveau votre code et voici ce que ça nous donne :

Image utilisateur


La voilà, la différence ! :)
L'instruction include n'efface pas ce que vous avez écrit dans la réponse alors que forward, oui !
ERROR, lui, est utilisé lorsque nous capturons une erreur.

Qu'est-ce que tu entends par là ?

Ne vous en faites pas, nous allons aborder ce point dans le prochain chapitre, mais sachez que JEE offre un moyen de gérer des erreurs, que ce soit des erreurs du serveur WEB ou des exceptions de Java.

Maintenant, pour régler notre problème de filtre, c'est très simple, il suffit d'ajouter le (ou les) type(s), comme ceci :
Code : XML
1
2
3
4
5
6
7
<filter-mapping>
	<filter-name>secondFilter</filter-name>
	<url-pattern>/result.do</url-pattern>
	<dispatcher>REQUEST</dispatcher>
	<dispatcher>FORWARD</dispatcher>
	<dispatcher>INCLUDE</dispatcher>
</filter-mapping>


Par contre, le contenu de la console d'Eclipse a quelque peu changé :

Image utilisateur


Et c'est tout à fait normal puisqu'il n'y a pas de renvoi de requête et que les servlets se suivent dans le traitement.

Bon : je crois qu'il est temps de faire un petit QCM. ;)

Q.C.M.

Quelle interface doivent implémenter les classes destinées à être des filtres ?
Quel objet est utilisé par le conteneur pour invoquer tous les filtres ?
Nous pouvons mapper un filtre uniquement grâce à un nom de servlet. Est-ce vrai ou faux ?
Quel type d'appel est intercepté par les filtres par défaut ?
Comment le conteneur détermine-t-il l'ordre des filtres à utiliser ?

Statistiques de réponses au QCM

Rien de bien sorcier là encore. Mais vous conviendrez que ça peut s'avérer fort utile.
Chapitre précédent Sommaire Chapitre suivant

Partager

5 commentaires pour "Les filtres"
Note moyenne : 3.17 / 4 (294 votes)
Pseudo Commentaire
Hors ligne knizou971 # Posté le 14/03/2011 à 16:43:29

Avis : Très bon

Après les cookies , le café (filtré bien sûr) , c'est bien ici ^_^
Hors ligne knizou971 # Posté le 14/03/2011 à 17:06:05

Avis : Très bon

blague à part sur le gouter

Il y a une petite erreur au niveau d'une question du qcm
"Nous pouvons mapper un filtre unique grâce à un nom de servlet. Est-ce vrai ou faux ?"

Je pense qu'à la place d'"unique" il faut lire "uniquement" ?
Hors ligne Don_Diego # Posté le 18/04/2011 à 04:42:05

salut je comprend pas grand chose à cette histoire de filtre
tu dis que cela va nous servir et tout mais je pense qu'il aurait fallut un exemple plus concrèt que de simplement écrire dans la console

merci quand même et vivement la suite
Hors ligne toufik3119 # Posté le 25/06/2011 à 19:22:27

salut tt le monde , j'utilise un serveur JBOSS et je suit ce tuto mais il me pose un pb dans les filtre avec le fichier web.xml dans la déclaration <FILTER>...
et l'exemple ne veut pas marché
aidé moi !!
Hors ligne monsieurx # Posté le 13/07/2011 à 03:32:38
Avatar

Avis : Bon

Ville : Shefford
Pays : Canada

Beaucoup d'explications, mais très peu d'exemple concret de ce que l'on peut faire avec les filtres
:(

Seul l'impossible est impossible
 

Voir tous les commentaires