Accueil > Intranet Michel Buffa > Cours web 2.0 année 2010-2011 > TP5 (long) : étude de la librairie GWT

TP5 (long) : étude de la librairie GWT

De $1

Introduction

Nous allons étudier la librairie GWT de Google utilisée par un nombre important de services Google tels que gmail ou google agenda. Cette tehnologie permet de développer des applications web faisant appel à des web services REST et des clients Ajax tout en développant exclusivement en Java. Le code serveur sera transformé en fichiers .class alors que le code client sera compilé en javascript. Comme GWT compile du java vers du javascript et ce pour les différents navigateurs du marché, la compilation peut être longue sur des ordinateurs peu puissants. Vous êtes prévenus ! En revanche le code compilé est très compact et bien optimisé. Les applications générées sont légères, etc.

Supports de cours

Premiers pas avec GWT

  • VOUS DEVEZ TELECHARGER LE GWT SDK : http://google-web-toolkit.googlecode.../gwt-2.1.0.zip
  • Pour cette étape, vous allez suivre ce tutorial (en anglais) de Sun : http://netbeans.org/kb/docs/web/quic...bapps-gwt.html, il vous apprendra par ailleurs quelques astuces avec netbeans :
    • Installer le plugin GWT,
    • Créer un projet web avec le support GWT,
    • Comprendre la structure d'un projet GWT,
    • Créer un service RPC qui va faire de l'ajax en coulisse,
    • Utiliser une feuille de style CSS pour spécifier le look & feel d'éléments d'interface utilisateur,
    • Utiliser l'éditeur de CSS interactif de netbeans (super outil !),
    • Afficher le javadoc en temps réel d'un projet Java, y compris la javadoc de GWT, juste en positionnant le curseur sur le code,
    •  Debugger une application en "Hosted Mode", mettre des points d'arrêt etc. Attention, par rapport au tutorial, avec GWT 2.1.0 cela requiert l'installation d'une extension firefox (vous verrez).

Utiliser l'application GWT Showcase comme source de nombreux exemples de code

Première étape : exécuter l'application en ligne

Jouez avec, regardez les sources des démos en cliquant sur l'onglet "source"

Seconde étape (à faire à la maison car longue) : compiler et exécuter l'application dans netbeans

Attention, la compilation peut durer 15 minutes ! Il est cependant intéressant, même si vous ne le compilez pas, d'avoir le projet ouvert dans netbeans car il est truffé de bouts de code réutilisables. C'est une mine d'exemple dont on se sert et ressert régulièrement. Le projet est dans l'installation de GWT, dans le répertoire samples/showcase.

La vidéo youtube qui montre comment installer, compiler et exécuter l'application de démonstration de GWT (GWT Showcase, aussi testable en ligne ici : http://gwt.google.com/samples/Showcase/Showcase.html) :

Attention, il faudra aussi modifier la ligne suivante dans le fichier gwt.properties (par defaut il y a gwt.version=2.1 or le plugin netbeans actuel ne reconnait que 2.0) :

# GWT version: 1.5,1.6,1.7 or 2.0
gwt.version=2.0

IL FAUDRA FAIRE CETTE MODIF POUR QUASIMENT TOUS LES PROJETS DE CETTE ANNEE !

Pour info, voici le fichier gwt.properties que j'ai dans mon projet :

# The names of the modules to compile (separated by a space character)
gwt.module=com.google.gwt.sample.showcase.Showcase

# Folder within the web app context path where the output
# of the GWT module compilation will be stored.
# This setting is only used for GWT 1.5. For newer versions please use
# the rename-to attribute in the GWT module file (.gwt.xml).
gwt.output.dir=/org.yournamehere.Main

# Script output style: OBF[USCATED], PRETTY, or DETAILED
gwt.compiler.output.style=OBF

# Additional JVM arguments for the GWT compiler
gwt.compiler.jvmargs=-Xmx256M

# Specifies the number of local workers to use whe compiling permutations and module(s)
gwt.compiler.local.workers=1

# The level of logging detail: ERROR, WARN, INFO, TRACE, DEBUG,
gwt.compiler.logLevel=WARN

# Script output style: OBF[USCATED], PRETTY, or DETAILED
gwt.shell.output.style=OBF

# The level of logging detail: ERROR, WARN, INFO, TRACE, DEBUG,
gwt.shell.logLevel=WARN

# Additional JVM arguments for the GWT shell/GWT hosted mode (GWT 1.6)
# Add -d32 here and use at least GWT 1.7.1 to debug on a Mac
# (32-bit JRE is required by GWT for debugging)
gwt.shell.jvmargs=-Xmx256M

# GWT version: 1.5,1.6,1.7 or 2.0
gwt.version=2.0

# GWT 2.0 only
# Specifies the TCP port for the code server
gwt.shell.code.server.port=9997

# GWT 2.0 only
# Specifies the TCP port for the embedded web server
gwt.shell.port=8888

# Additional GWT compiler arguments
# GWT 2.0 compiler supports these:
#  -workDir                The compiler's working directory for internal use (must be writeable; defaults to a system temp dir)
#  -gen                    Debugging: causes normally-transient generated types to be saved in the specified directory
#  -ea                     Debugging: causes the compiled output to check assert statements
#  -XdisableClassMetadata  EXPERIMENTAL: Disables some java.lang.Class methods (e.g. getName())
#  -XdisableCastChecking   EXPERIMENTAL: Disables run-time checking of cast operations
#  -validateOnly           Validate all source code, but do not compile
#  -draftCompile           Enable faster, but less-optimized, compilations
#  -compileReport          Create a compile report that tells the Story of Your Compile
#  -localWorkers           The number of local workers to use when compiling permutations
#  -extra                  The directory into which extra files, not intended for deployment, will be written
gwt.compiler.args=

# Additional JVM arguments for JUnit tests
gwt.test.jvmargs=-Xmx256M

# Additional arguments for the GWT shell
# e.g. -bindAddress 0.0.0.0 since GWT 2.0.1
gwt.shell.args=

GWT et JPA, session et entity beans

Création d'un projet web utilisant GWT

Snap1.jpg

Snap3.jpg

Snap4.jpg

Snap8.jpg

Snap9.jpg

Modification du fichier gwt.properties pour indiquer qu'on est en gwt 2.0 pas 2.1

 Le plugin gwt4nb n'est pas totalement à jour, il ne supporte que gwt 1.5, 1.6, 1.7 et 2.0, pas encore 2.1. Pour qu'il compile correctement le projet, modifiez la ligne suivante du fichier gwt.properties, dans "configuration files"

# GWT version: 1.5,1.6,1.7 or 2.0
gwt.version=2.0

Ajout d'un entity bean Etudiant au projet

Comme nous avons fait dans les projets précédents, ajoutez une classe entity au projet, n'oubliez pas de créer une persistence unit en mode "create" sur la base de données jdbc/sample. Vous placerez cet entity bean dans un package "entities". Vous devriez obtenir ceci :

Snap10.jpg

Snap11.jpg

Snap12.jpg

Maintenant rajoutez deux "propriétés" nom et prénom à la classe Etudiant.java. Vous pouvez utiliser le wizard "insert code/add property", cela vous génèrera la déclaration des attributs mais aussi les get et set.

Vous devez avoir le début de la classe Etudiant.java qui ressemble à ceci :

@Entity
public class Etudiant implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String nom;
    private String prenom;

    /**
     * Get the value of prenom
     *
     * @return the value of prenom
     */
    public String getPrenom() {
        return prenom;
    }...

Ajout d'un session bean GestionnaireEtudiants.java

 Rajoutez au projet un session bean stateless, sans interfaces (pour simplifier), que vous nommerez GestionnaireEtudiants.java. Si vous ne voyez pas "add session bean" dans le menu pour ajouter des éléments au projet, allez dans "other/java EE/Session bean", cf la capture d'écran ci-dessous :

Snap13.jpg

Snap14.jpg

Snap15.jpg

Dans ce session bean, vous ajouterez un entity manager et une méthode pour insérer un étudiant dans la base de données :

@Stateless
public class GestionnaireEtudiants {

    @PersistenceContext
    EntityManager em;

    public Etudiant ajouteStudent(String prenom, String nom) {
        Etudiant s = new Etudiant();
        s.setPrenom(prenom);
        s.setNom(nom);

        em.persist(s);

        return s;
    }
}

Création d'un service RPC Gwt EtudiantServiceImpl.java

Remarque : 'est en réalité une servlet qui répond à la manière d'un web service, on l'invoque depuis du code javascript compilé à partir du code java/GWT que vous allez écrire...

Utilisez le wizard de netbeans pour créer ce service :

 Snap16.jpg

Snap17.jpg

Snap18.jpg

On remarque que plusieurs classes ont été générées. Celles du package gwt.jpa1.client.* seront compilées en javascript et tourneront dans un navigateur web. Celles du package gwt.jpa1.server.* implémentent les services côté serveur (ils sont appelées par une GWT Servlet).

On va remplacer le code du service côté serveur (la classe EtudiantServiceImpl.java), au lieu de :

public class EtudiantServiceImpl extends RemoteServiceServlet implements EtudiantService {
    public String myMethod(String s) {
        // Do something interesting with 's' here on the server.
        return "Server says: " + s;
    }
}

On va appeler le session bean gestionnaire d'étudiants pour insérer un étudiant (vous pouvez utiliser le wizard "call entreprise bean") :

public class EtudiantServiceImpl extends RemoteServiceServlet implements EtudiantService {

    @EJB
    GestionnaireEtudiants gestionnaireEtudiants;

   public String ajouteEtudiant(String prenom, String nom) {

        Etudiant e = gestionnaireEtudiants.ajouteEtudiant(prenom, nom);

        return "Un nouvel étudiant a été ajouté par le service GWT : #" + e.getId() + " - " + e.getPrenom() + " " + e.getNom();
    }
}

Comme les services GWT implémentent une interface du service (pour permettre la génération de code d'interception, voir le cours !), il faut également modifier les deux interfaces : celles qui sert au code généré côté serveur (le fichier EtudiantService.java) et celle qui est implémenté par le code javascript côté client (le fichier EtudiantServiceAsync.java). Tant que l'on a pas modifié ces deux interfaces, une erreur figure dans la classe EtudiantServiceImpl.java indiquant qu'elle n'implémente pas les bonnes interfaces.

 Fichier EtudiantService.java (notez l'annotation de code qui correspond à l'URL de la servlet qui implémente ce service, on va retrouver cet URL dans la classe d'exemple d'utilisation du service, et dans le fichier web.xml dans la partie "Servlets") :

@RemoteServiceRelativePath("servicesgwt/etudiantservice")
public interface EtudiantService extends RemoteService {
    public String ajouteEtudiant(String prenom, String nom);
}

Fichier EtudiantServiceAsync.java :

public interface EtudiantServiceAsync {
    public void ajouteEtudiant(String nom, String prenom, AsyncCallback<String> callback);
}

Noter que toutes les méthodes dans l'interface Asynchrone qui sera implémenté par le code compilé en javascript ont comme type de retour void ! C'est normal puisque la valeur de retour est fournie dans la fonction de callback !

Maintenant, nous allons modifier la classe d'exemple d'utilisation du service pour qu'elle fonctionne après que nous ayons modifié les interfaces d'utilisation. Prenez la peine d'étudier ce code ! Notez tout en bas dans la méthode getEtudiantService(), la présence de l'URL du service :

public class EtudiantServiceUsageExample extends VerticalPanel {
private Label labelReponseDuService = new Label();
    private TextBox txtPrenom = new TextBox();
    private TextBox txtNom = new TextBox();
    private Button boutonEnvoyer = new Button("Envoyer au service");

    public EtudiantServiceUsageExample() {
        add(new Label("Prénom et Nom: "));
        add(txtPrenom);  // modifie
        add(txtNom); // rajoute
        add(boutonEnvoyer);
        add(labelReponseDuService);

        // Create an asynchronous callback to handle the result.
        final AsyncCallback callback = new AsyncCallback() {
            public void onSuccess(Object result) {
                labelReponseDuService.setText((String)result);
            }

            public void onFailure(Throwable caught) {
                labelReponseDuService.setText("Problème de communication avec le service");
            }
        };

        // Listen for the button clicks
        boutonEnvoyer.addClickListener(new ClickListener(){
            public void onClick(Widget w) {

                getEtudiantService().ajouteEtudiant(txtPrenom.getText(), txtNom.getText(), callback);
            }
        });
    }

    /* aucune modification ici */
    public static EtudiantServiceAsync getEtudiantService(){
        // Create the client proxy. Note that although you are creating the
        // service interface proper, you cast the result to the asynchronous
        // version of
        // the interface. The cast is always safe because the generated proxy
        // implements the asynchronous interface automatically.
        EtudiantServiceAsync service = (EtudiantServiceAsync) GWT.create(EtudiantService.class);
        // Specify the URL at which our service implementation is running.
        // Note that the target URL must reside on the same domain and port from
        // which the host page was served.
        //
        ServiceDefTarget endpoint = (ServiceDefTarget) service;
        String moduleRelativeURL = GWT.getModuleBaseURL() + "servicesgwt/etudiantservice";
        endpoint.setServiceEntryPoint(moduleRelativeURL);
        return service;
    }

Enfin, nous allons modifier le point d'entrée de l'application, la classe MainEntryPoint.java, afin qu'elle ajoute dans la page html principale une instance de la classe d'exemple d'utilisation ci-dessus (qui est en réalité un élément de GUI, un VerticalPanel) :

MainEntry.java :

public void onModuleLoad() {
        RootPanel.get().add(new EtudiantServiceUsageExample());
    }

On exécute ensuite le projet  et on doit obtenir ceci :

Snap19.jpg

Et on voit dans firebug l'appel au service et la manière dont GWT encode les paramètres d'envoi par HTTP POST.

Snap21.jpg

SI CELA NE MARCHE PAS ET INDIQUE QU'IL Y A UN PROBLEME DE COMMUNICATION AVEC LE SERVEUR :

  • Vérifiez que le nom du service dans l'annotation (1ère ligne) de :

 

/div[5]/div[5]/pre[7], line 2, column 1: EOF expected

est la même que dans la ligne spécifiant l'URL du service dans le code client (avant, avant dernière ligne) :

/* aucune modification ici */
    public static EtudiantServiceAsync getEtudiantService(){
        // Create the client proxy. Note that although you are creating the
        // service interface proper, you cast the result to the asynchronous
        // version of
        // the interface. The cast is always safe because the generated proxy
        // implements the asynchronous interface automatically.
        EtudiantServiceAsync service = (EtudiantServiceAsync) GWT.create(EtudiantService.class);
        // Specify the URL at which our service implementation is running.
        // Note that the target URL must reside on the same domain and port from
        // which the host page was served.
        //
        ServiceDefTarget endpoint = (ServiceDefTarget) service;
        String moduleRelativeURL = GWT.getModuleBaseURL() + "servicesgwt/etudiantservice";
        endpoint.setServiceEntryPoint(moduleRelativeURL);
        return service;
    }

et que dans le fichier web.xml vous avez aussi un URL cohérent pour la Servlet (3ème ligne) :

<servlet-mapping>
        <servlet-name>EtudiantService</servlet-name>
        <url-pattern>/gwt.jpa1.Main/servicesgwt/etudiantservice</url-pattern>
</servlet-mapping>

Si ces trois références ne sont pas compatibles, alors la communication client/serveur ne pourra pas se faire. Vous pouvez regarder quel URL est appelé lorsque vous cliquez sur le bouton ajouter, à l'aide de firebug (touche F12 dans Firefox), n'oubliez pas d'activer le debug réseau et la console.

Manipuler des instances d'entity beans dans la partie "cliente" de l'application

Là le problème devient un peu plus délicat, nous allons le voir. En effet, la partie cliente de l'application, ne l'oublions pas, va être compilée en javascript. Il est donc nécessaire que le compilateur GWT ait accès aux sources ou aux jars des différentes classes utilisées par l'application. Nous allons voir comment nous y prendre... en commençant par ajouter une méthode dans le service GWT...

Ajout d'une méthode getAllEtudiants() dans le service GWT

public Collection<Etudiant> getAllEtudiants() {
       return gestionnaireEtudiants.getAllEtudiants();
   }

 N'oublions pas de rajouter cette méthode dans les deux interfaces...

EtudiantService.java (rappel : cette méthode sert à générer la servlet d'interception GWT) :

@RemoteServiceRelativePath("servicesgwt/etudiantservice")
public interface EtudiantService extends RemoteService {
    public String ajouteEtudiant(String prenom, String nom);
    public Collection<Etudiant> getAllEtudiants();
}

EtudiantServiceAsync.java (et toujours void comme valeur de retour et un paramètre supplémentaire pour la fonction de callback). Rappel, cette interface sert au code généré en javascript.

public interface EtudiantServiceAsync {
    public void ajouteEtudiant(String nom, String prenom, AsyncCallback<String> callbackAjoutEtudiant);
    public void getAllEtudiants(AsyncCallback<String> callback);
}

 

Ajout d'un bouton pour lister tous les étudiants dans la GUI du client :

Travail à faire : rajouter l'appel de cette méthode dans la classe cliente EtudiantServiceUsageExample.java. Vous ajouterez un bouton, un écouteur qui appelle dans sa méthode onclick() la méthode getAllEtudiants() du service distant, et vous écrirez aussi la fonction de callback qui modifie un label pour afficher cette liste. On doit avoir un résultat de ce type :

Snap40.jpg

Mais, ce n'est pas aussi simple !!!! Il y a des erreurs de compilation, voyons pourquoi !

Et oui, normalement, vous devriez avoir des erreurs de compilation du genre :

Snap41.jpg

Ah ah ! Pour compiler en javascript la classe qui crée la GUI (la classe EtudiantServiceUsageExample.java), on a besoin de la classe Etudiant.java. Pourtant, le compilateur java la trouve non ? Netbeans ne sort pas d'erreurs, les lignes ne sont pas soulignées en rouge ! Oui, mais ici, le source est compilé par le compilateur GWT. Il faut lui indiquer où se trouvent les sources de cette classe. On précise cela dans le fichier du "module GWT", soit le fichier Main.gwt.xml qui se trouve dans le package principal de notre application (gwt.jpa1). Double cliquons sur ce fichier et rajoutons deux lignes pour qu'il trouve les sources :

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.7.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.7.0/distro-source/core/src/gwt-module.dtd">

<module>
    <!-- ce sont des chemins relatifs par rapport au package gwt.jpa1 -->
    <source path="entities" />
    <source path="client" />

      

    <inherits name="com.google.gwt.user.User"/>
    <!-- Inherit the default GWT style sheet. You can change -->
    <!-- the theme of your GWT application by uncommenting -->
    <!-- any one of the following lines. -->
    <!-- <inherits name='com.google.gwt.user.theme.standard.Standard'/> -->
    <!-- <inherits name="com.google.gwt.user.theme.chrome.Chrome"/> -->
    <!-- <inherits name="com.google.gwt.user.theme.dark.Dark"/> -->

    <entry-point class="gwt.jpa1.client.MainEntryPoint"/>

    <!-- Do not define servlets here, use web.xml -->
</module>

Ce sont les deux lignes commençant par <source path="...."/> qui nous intéressent. Nous indiquons ici qu'il faut aller voir dans le package "entities" et dans le package "client". Il s'agit en fait de sous-packages du package principal. Ces deux lignes rajoutent donc :

  1. Le package gwt.jpa1.entities dans le chemin de recherche des sources,
  2. Le package gwt.jpa1.client (celui-ci y était par défaut, mais à partir du moment où on commence à préciser des chemins de sources dans le fichier xml du module, il faut le rajouter, il n'y est plus par défaut).

Etant donné que la classe Etudiant.java était dans le package "entities" et non "gwt.jpa1.entities", nous renommons le package dans netbeans :

Snap42.jpg

Voilà, si vous recompilez après avoir modifié Main.gwt.xml et le nom du package entities pour qu'il soit sous package de gwt.jpa1, les choses devraient progresser ! On a maintenant les erreurs suivantes :

Snap44.jpg

Rajouter le "source" des annotations de persistence pour compilation vers javascript

Hmmm rappelez-vous que les annotations de code font partie intégrante du langage java, au même titre que les attributs, les méthodes, les constructeurs etc... Pour compiler correctement la classe Etudiant.java (en javascript), le compilateur GWT a besoin des sources des annotations. Ah... jusqu'en 2009, il fallait réellement les sources (que l'on pouvait trouver sur internet ou dans les repositories de source code de java EE), mais depuis cette année (depuis GWT 2.0), le compilateur sait aussi compiler des .class vers du javascript ! Qu'à cela ne tienne, nous allons simplement rajouter au projet le jar contenant la librairie de persistence de Java EE. Si la classe Etudiant avait contenu des annotations de code JAXB (transformation xml) ou Jersey (web services), il aurait fallu rajouter les classes correspondantes dans les librairies du projet (le compilateur GWT ajoute les librairies dans ses chemins de recherche de sources)

Nous pouvons trouver toutes les librairies de java EE dans le répertoire d'installation de glassfish, çà tombe bien ! Adaptez le répertoire à votre machine :

Snap45.jpg

Copiez ce fichier javax.persistence, ajoutez-le dans un répertoire lib de votre projet, ajoutez le jar aux librairies du projet. Recompilez, cela devrait marcher.

Affichage des résultats dans une FlexTable (un tableau de taille variable)

  • Regardez l'utilisation des FlexTable dans l'application ShowCase de GWT. Essayez de comprendre comment cela fonctionne. Ajoutez une FlexTable dans la GUI de votre projet, et remplissez-là lorsque vous parcourez la liste des Etudiants renvoyées par l'appel du service GWT.
  • Ajoutez comme vous l'avez fait au début du TP une feuille de style Student.css, un style pour la flexTable, et essayez d'obtenir un résultat de ce type :
    • ajoutez un style à la flex table (dans le code),
    • Ajoutez une feuille de style Student.css dans les web pages,
    • Rajoutez un tag meta pour que la page html utilise la css,
    • Utilisez l'éditeur de CSS de netbeans pour jouer avec les polices, les couleurs, etc

Voici une CSS minimale :

.cw-FlexTable td {
    border: 1px solid #BBBBBB;
    padding: 3px;
}

Et la ligne de code qui associe le style à la FlexTable :

// Pour le lien avec la feuille de style Student.css
        studentTable.addStyleName("cw-FlexTable");

Snap46.jpg

 A venir : comment modifier un projet Java EE Existant pour qu'il fonctionne avec GWT

  1. Pour qu'un projet entreprise (celui avec trois parties) fonctionne en GWT il faut : 1) ajouter le framework GWT à la partie war. Clic droit puis properties sur le projet, et frameworks/add framework pour ajouter GWT.
  2. Si la partie cliente a besoin de manipuler des entities bean, il faut indiquer au module XML du projet GWT (dans le projet war) d'aller chercher des sources dans le projet EJB, dans le package qui contient les classes entités. Pour ce faire, allez juste regarder les modifications à faire 1) au module XML du projet war et 2) au projet EJB (il faut juste rajouter un fichier module GWT) ici : Exemple plus complexe EJB3 + GWT + annotations + entities dans le client GWT, attention, c'est un TP de 2009 qui utilise une vieille version de GWT. Ne faites pas le TP ! Regardez juste les modifications à faire dans le projet war et gwt.

 

 

Mots clés:
FichierTailleDateAttaché par 
 GWT, présentation générale.pptx
Aucune description
412.21 Ko10:33, 17 Nov 2010MichelBuffaActions
 gwt-2.1.0.zip
Aucune description
56.64 Mo10:46, 17 Nov 2010MichelBuffaActions
Images (22)
Voir 22 sur 22 images | Voir tout
Aucune description
Snap46.jpg  Actions
Aucune description
Snap45.jpg  Actions
Aucune description
Snap44.jpg  Actions
Aucune description
Snap42.jpg  Actions
Aucune description
Snap41.jpg  Actions
Aucune description
Snap40.jpg  Actions
Aucune description
Snap21.jpg  Actions
Aucune description
Snap19.jpg  Actions
Aucune description
Snap18.jpg  Actions
Aucune description
Snap17.jpg  Actions
Aucune description
Snap16.jpg  Actions
Aucune description
Snap15.jpg  Actions
Aucune description
Snap14.jpg  Actions
Aucune description
Snap13.jpg  Actions
Aucune description
Snap12.jpg  Actions
Aucune description
Snap10.jpg  Actions
Aucune description
Snap11.jpg  Actions
Aucune description
Snap9.jpg  Actions
Aucune description
Snap8.jpg  Actions
Aucune description
Snap4.jpg  Actions
Aucune description
Snap3.jpg  Actions
Aucune description
Snap1.jpg  Actions
Commentaires (0)
Vous devez être connecté pour poster un commentaire.