
Apache Pivot est un framework de développement d’applications Internet riches (RIA) qui a su faire parler de lui dès sa promotion au rang de « Top Level Project » au sein de la fondation Apache. Ce que Apache Pivot apporte par rapport à JavaFX, c’est surtout :
- Le code est en Java (et non un langage de script à apprendre);
- Le développeur a l’option de concevoir son interface graphique de façon déclarative (en XML);
- L’intégration avec les composants serveur est plus souple.
Dans cet article, nous allons surtout nous focaliser sur ce dernier point qui n’a pas été illustré dans les démos et les tutoriaux du site officiel du framework. Pour cela, nous allons essayer de développer une simple application client/serveur de gestion de contacts. Pour faire au plus simple, les fonctionnalités seront réduites à ajouter un contact ou modifier un contact existant.
Une version abrégée de cet article a été publiée dans le numéro 132 (Juillet 2010) du magazine «PROgrammez». La version PDF de l’article est mise à votre disposition en téléchargement gratuit. Le code source de l’exemple donné est fourni (voir ci-dessous).
PARTIE CLIENTE
L’écran principal se composera d’une liste de contacts et un bouton pour ajouter un contact à la liste. Un clic sur une entrée de la liste permettra à l’utilisateur de modifier les détails du contact.


Les deux écrans sont conçus déclarativement dans les deux fichiers table.xml et addContact.xml.
<TableView wtkx:id="tableView"
styles="{includeTrailingVerticalGridLine:true}">
<columns>
<TableView.Column name="firstname" width="100"
headerData="Prenom" />
<TableView.Column name="lastname" width="100"
headerData="Nom" />
<TableView.Column name="email" width="60"
headerData="E-Mail" />
<TableView.Column name="phone" width="60"
headerData="Téléphone" />
</columns>
</TableView>
Extrait (correspondant au tableau de contacts) de table.xml
<Form styles="{rightAlignLabels:true}" wtkx:id="submitform">
<sections> <Form.Section> <Label Form.label="ID" textKey="id" /> <BoxPane Form.label="Nom"> <TextInput textKey="lastname" /> </BoxPane> <BoxPane Form.label="Prénom"> <TextInput textKey="firstname" /> </BoxPane> <BoxPane Form.label="Téléphone"> <TextInput textKey="phone" /> </BoxPane> <BoxPane Form.label="Adresse E-mail"> <TextInput textKey="email" /> </BoxPane> </Form.Section> </sections> </Form>
Extrait (correspondant au formulaire) de addContact.xml
Notre classe Contact a les propriétés ID (id), Prénom (firstName), Nom (lastName), E-mail (email) et téléphone (phone). Noter que dans le composant TableView de table.xml, la propriété ‘name’ de chaque colonne correspond à la propriété de la classe Contact correspondante; et que dans chaque TextInput du formulaire d’ajout/modification de addContact.xml, la propriété textKey correspond aussi à la propriété de la classe Contact correspondante.
A part la classe Contact (modèle de donnée), la partie cliente de notre application se compose de 4 classes :
- Une classe Contacts qui joue le rôle du contrôleur (qui implémente trois interfaces d’écoutes d’événements)
- Une classe TableContactsWindow (vue) qui représente la fenêtre principale contenant la liste des contacts
- Une classe EditContactWindow (vue) qui représente la fenêtre d’ajout/modification d’un contact
- Une classe RemoteContactModel (modèle) qui sera responsable de la communication avec le serveur
Nos trois interfaces d’écoute d’événements (implémentées par le contrôleur) sont:
- ContactUpdateListener pour répondre à deux événements : demande (par l’utilisateur) d’ajout d’un contact et demande (par l’utilisateur) d’édition d’un contact.
- ContactSubmitRequestListener pour répondre à l’événement de demande (par l’utilisateur) de sauvegarde d’un contact.
- ContactAccessListener pour répondre à deux événements : la fin du chargement de la liste des contacts et la réussite de la sauvegarde d’un objet contact.
LES VUES
La classe TableContactsWindow garde une référence sur un écouteur d’événements ContactUpdateListener (c’est la table de contacts qui déclenche les événements associés) et a deux propriétés : le tableau de contacts et la fenêtre contenant le tableau. Le constructeur prend en paramètre l’écouteur et demande à Pivot de parser table.xml pour construire la fenêtre et la table :
WTKXSerializer wtkxSerializer = new WTKXSerializer();
try {
window = (Window)wtkxSerializer.readObject(this, "table.xml");
} catch (IOException e) {
e.printStackTrace();
} catch (SerializationException e) {
e.printStackTrace();
}
table = (TableView)wtkxSerializer.get("tableView");
Ensuite, on ajoute un gestionnaire pour l’événement de clic sur une ligne de la table (modification d’un contact) et pour l’événement de clic sur le bouton ‘Ajouter’ :
//Ajoute un gestionnaire d'événement pour le clic sur une ligne du tableau
table.getComponentMouseButtonListeners().add(new ComponentMouseButtonListener.Adapter() {
@SuppressWarnings("unchecked")
@Override
public boolean mouseClick(Component component,
org.apache.pivot.wtk.Mouse.Button button, int x, int y, int count) {
List<Contact> contacts = (List<Contact>)table.getTableData();
//Récupère l'indice de la ligne cliqué
int index = table.getRowAt(y);
//Notifie le contrôleur de la demande d'édition d'un contact
controller.editContactRequest(contacts.get(index), TableContactsWindow.this);
return false;
}
});
PushButton addButton = (PushButton)wtkxSerializer.get("addButton");
//Ajoute un gestionnaire d'événement pour le clic sur le bouton 'Ajouter'
addButton.getButtonPressListeners().add(new ButtonPressListener() {
@Override
public void buttonPressed(Button button) {
//Notifie le contrôleur du clic sur le bouton 'Ajouter'
controller.addContactRequest(TableContactsWindow.this);
}
});
Elle admet aussi une méthode setContacts pour remplir la table avec une liste de contacts :
public void setContacts(List<Contact> result) {
//Met à jour la table des contacts
table.setTableData(result);
}
Notons ici, qu’en appelant les méthodes setTableData et getTableData (qu’on a appelé dans le gestionnaire d’événement lors d’un clic sur une ligne du tableau), Apache Pivot se charge du mapping entre les propriétés de la classe Contact et les colonnes du tableau.
La classe EditContactWindow garde une référence sur un écouteur d’événements ContactSubmitRequestListener (c’est la fenêtre d’édition qui déclenche l’événement de demande de soumission) et a les propriétés suivantes :
- la fenêtre de tableau de contacts associée (à laquelle ajouter ou depuis laquelle modifier le contact)
- la fenêtre d’édition
- le formulaire d’édition
- l’objet Contact en cours d’ajout/d’édition
Le constructeur prend en paramètre le contrôleur et la fenêtre de tableau de contacts associée, demande à Pivot de parser addContact.xml pour construire la fenêtre d’édition et le formulaire:
WTKXSerializer wtkxSerializer = new WTKXSerializer();
try {
editWindow = (Window)wtkxSerializer.readObject(this,"addContact.xml");
} catch (IOException e) {
e.printStackTrace();
} catch (SerializationException e) {
e.printStackTrace();
}
form = (Form)wtkxSerializer.get("submitform");
Ensuite, on ajoute un gestionnaire pour l’événement de clic sur le bouton de soumission du formulaire :
PushButton submit = (PushButton)wtkxSerializer.get("submitButton");
submit.setAction(new Action() {
@Override
public void perform() {
//Met à jour l'objet contact avec les valeurs des champs du formulaire
//Apache Pivot se charge de faire l'association
//entre les propriétés de la classe Contact et les champs du formulaire
form.store(contact);
//Notifie le contrôleur de la soumission du formulaire
listener.contactSubmit(contact,EditContactWindow.this);
//Ferme la fenêtre d'edition
editWindow.close();
}
});
La classe EditContactWindow admet aussi une méthode setContact pour mettre à jour l’objet Contact en cours d’édition :
public void setContact(Contact contact) {
this.contact = contact;
//Les champs du formulaire se remplissent avec les infos du contact
//Apache Pivot se charge de faire l'association
//entre les propriétés de la classe Contact et les champs du formulaire
form.load(contact);
}
Notons ici qu’en appelant les méthodes load et store de l’objet Form, Pivot se charge de mapper les propriétés de la classe Contact avec les champs du formulaire.
LE MODELE
A ce niveau de l’article, nous n’allons pas détailler la classe RemoteContactModel (nous allons le faire dans la partie qui traite la communication client/serveur) qui incarne le modèle dans notre application. Sachons seulement que cette classe implémente l’interface ContactModel, et est chargée de deux opérations :
- Récupérer la liste des contacts (opération à la fin de laquelle elle déclenche l’événement contactsRetrieved)
- Poster un objet Contact pour ajout ou pour mise à jour (opération à la fin de laquelle elle déclenche l’événement contactSaved)
LE CONTROLEUR
La classe Contacts joue le rôle de contrôleur en implémentant les 3 interfaces d’écoutes et en gardant une référence vers la vue TableContactsWindow et le modèle ContactModel. Pour être notre point d’entrée de l’application, elle implémente l’interface org.apache.pivot.wtk.Application. Pivot appellera alors la méthode startup :
//Crée la fenêtre de table de contacts tcw = new TableContactsWindow(this); //Instancie le model model = new RemoteContactModel(this); //Demande au modèle de récupérer la liste des contacts model.retrieveAllContacts(); //Affiche la fenêtre sur l'ecran principal tcw.getWindow().open(display);
Dans cette méthode, on instancie une fenêtre de tableau de contacts, on la remplit avec la méthode updateTable (qui ne fait que demander au modèle de récupérer la liste), et enfin on l’affiche sur l’écran principal (l’objet display qu’on reçoit en paramètre).
La classe Contacts, jouant le rôle de contrôleur, est responsable de gérer les événements :
1. Demande d’édition d’un contact : instanciation d’une fenêtre d’édition et lui passant le contact à éditer
public void editContactRequest(Contact contact, TableContactsWindow source) {
//Crée une fenêtre d'édition de contact
EditContactWindow ecw = new EditContactWindow(this,source);
//Met l'objet Contact cliqué pour édition
ecw.setContact(contact);
//Affiche la fenêtre
ecw.show();
}
2. Demande d’ajout d’un contact : instanciation d’une fenêtre d’édition et lui passant un nouvel objet contact « vide »
public void addContactRequest(TableContactsWindow source) {
//Crée un nouvel objet Contact "vide"
Contact newContact = new Contact(null, "", "", "", "");
//Crée une fenêtre d'édition
EditContactWindow ecw = new EditContactWindow(this,source);
//Met le nouveau contact pour édition
ecw.setContact(newContact);
//Affiche la fenêtre d'édition
ecw.show();
}
3. Demande de soumission d’un contact : demande au modèle de sauvegarder (poster) l’objet
public void contactSubmit(Contact contact, EditContactWindow source) {
model.saveContact(contact);
}
4. Chargement de la liste des contacts : met à jour la table des contacts avec la nouvelle liste.
public void contactsRetrieved(List<Contact> contacts) {
tcw.setContacts(contacts);
}
5. Sauvegarde d’un contact : demande au modèle un rechargement de la liste des contacts
public void contactSaved() {
model.retrieveAllContacts();
}
LA PARTIE SERVEUR
Du côté serveur, on a deux composants essentiels :
- Une Servlet qui gère les requêtes de la partie cliente
- Un DAO pour récupérer et sauvegarder les contacts
Nous verrons les détails de la servlet plus en détail dans la troisième partie. Pour le moment, sachez qu’elle communique avec le DAO pour sauvegarder un objet posté par le client, ou pour récupérer une liste de contacts à renvoyer au client.
Pour simplifier les choses, nous avons opté pour une implémentation en mémoire du DAO, en tant que map dont les clés sont les ID et les valeurs sont les contacts associés.
public class MemoryContactDAO implements ContactDAO {
private static Map<Long, Contact> data = new HashMap<Long, Contact>();
static Long lastId;
public List<Contact> getAll() {
List<Contact> contacts = new ArrayList<Contact>();
for(Contact c:data.values()){
contacts.add(c);
}
return contacts;
}
@Override
public Contact getById(Long id) {
return data.get(id);
}
@Override
public void save(Contact contact) {
if(contact.getId() != null){
data.put(contact.getId(), contact);
}else{
lastId ++;
contact.setId(lastId);
data.put(lastId, contact);
}
}
L’INTERACTION CLIENT/SERVEUR
Apache Pivot dispose d’une API pour lancer des requêtes HTTP depuis la partie cliente. Deux classes de cet API sont utilisées dans notre classe modèle de la partie cliente.
Pour récupérer la liste des contacts, on lance une requête HTTP de type GET, et une fois exécutée avec succès, on notifie le contrôleur de la réponse reçue.
public void retrieveAllContacts() {
//Crée une requête GET
GetQuery getQuery = new GetQuery(HOST,PORT, "/"+APP_NAME+"/Contact",false);
//Affecte le serialiseur de contacts
getQuery.setSerializer(SerializerFactory.getContactSerializer());
//Lance la requête en affectant un écouteur pour gérer le retour de la réponse
getQuery.execute(new TaskListener<Object>() {
@Override
public void taskExecuted(Task<Object> task) {
//task.getResult() = Réponse Objet désérialisé assumé en tant que liste de contacts
//On notifie le controller de la liste récupérée
controller.contactsRetrieved(((List<Contact>)task.getResult()));
}
@Override
public void executeFailed(Task<Object> task) {
//Ne fait rien du tout si erreur
}
});
}
Pour sauvegarder (poster) un objet contact, on lance une requête HTTP de type POST, et une fois exécutée avec succès, on notifie le contrôleur.
public void saveContact(Contact contact) {
//Crée une requête POST
PostQuery post = new PostQuery(HOST,PORT, "/"+APP_NAME+"/Contact",false);
//Affecte le serialiseur de contacts
post.setSerializer(SerializerFactory.getContactSerializer());
//Affecte l'objet Contact soumis (qui sera sérialisé)
post.setValue(contact);
//Lance la requête
post.execute(new TaskListener<URL>() {
@Override
public void executeFailed(Task<URL> arg0) {
//Ne fait rien du tout si erreur
}
@Override
public void taskExecuted(Task<URL> arg0) {
//On notifie le controller que la requête s'est bien déroulée
controller.contactSaved();
}
});
}
Vous avez peut être remarqué dans le code qu’on affecte à notre objet requête un sérialiseur. Les sérialiseurs de Apache Pivot peuvent faire l’objet d’un article qui leur est dédié. Sachez que Apache Pivot ne fait pas de différence entre sérialiseurs et désérialiseurs, et que dans ce contexte (requête GET), il s’agit plutôt d’un désérialiseur pour permettre à Pivot de « comprendre » la réponse renvoyé par le serveur (task.getResult()). Dans cette application, on a utilisé le BinarySerializer, qui ne fait qu’une sérialisation (pour les objets postés) et une désérialisation (pour les objets reçus) en binaire. Evidemment, ce type de sérialiseur est rarement pratique, nous l’avons choisi juste par simplicité et parce que c’est nous qui développons la partie cliente ainsi que la partie serveur. Par contre, si nous voudrons utiliser notre partie serveur pour exposer des services consommés par d’autres types de clients (Web, Application .NET…), le BinarySerializer est un choix à proscrire. Ce que Pivot apporte dans cette partie, c’est la sérialisation et la désérialisation transparente pour le développeur qui ne manipule que des objets. Remarquez que task.getResult() nous a retourné une liste de contacts, et que pour poster un objet contact, on n’a eu à faire que post.setValue(contact).
Apache Pivot dispose aussi d’une API pour traiter les requêtes HTTP du coté serveur. En effet, Pivot nous offre la classe QueryServlet. Dans notre application, la servlet de la partie serveur hérite de QueryServlet. Lorsqu’on hérite de QueryServlet, on doit implémenter (redéfinir) :
- La méthode newSerializer qui retourne le sérialiseur/désérialiseur que Pivot utilise pour sérialiser les objets retournés au client, et désérialiser les objets reçus du client
- Une méthode par type de requête HTTP à gérer
Dans notre cas, nous utiliserons le BinarySerializer comme retour de la méthode newSerializer : c’est ainsi que le client a sérialisé ses données, c’est ainsi donc que le serveur devrait les désérialiser. De plus, nous devrons redéfinir la méthode doGet et doPost pour gérer les deux types de requêtes que le client envoie.
protected Object doGet() throws ServletException, ClientException {
//Récupère le DAO
ContactDAO dao = DAOFactory.getContactDao();
//Cas où la requête porte sur tous les contacts
List<Contact> all = dao.getAll();
org.apache.pivot.collections.List<Contact> result = new org.apache.pivot.collections.ArrayList<Contact>();
//Transforme une java.util.List en l'implémentation List de Pivot
for(Contact c:all){
result.add(c);
}
//Retourne la liste
//Apache Pivot se chargera par la suite de sérialiser la liste
//et de l'inclure dans une réponse HTTP
return result;
}
protected URL doPost(Object value) throws ServletException, ClientException {
//Apache Pivot s'est déjà chargé de désérialiser l'objet envoyé
//dans la requête POST et nous l'a passé en paramètre
Contact contact = (Contact)value;
//Récupère le DAO
ContactDAO dao = DAOFactory.getContactDao();
//Enregistre(ajoute ou met à jour) le contact envoyé
dao.save(contact);
try {
String app = this.getContextPath();
return new URL("http://localhost:8080"+app+"/Contact");
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
Remarquez dans les deux méthodes, qu’on ne manipule pas les objets HTTPServletRequest, ni HTTPServletResponse. Les signatures des méthodes doGet et doPost sont différentes de celles des servlets « normales » (javax.servlet.http.HttpServlet). Le développeur, encore une fois, ne manipule que ses objets du modèle de donnée. Apache Pivot se charge de nous passer l’objet posté par le client (après désérialisation) comme paramètre de la méthode doPost et se charge aussi de formuler la réponse au client (après sérialisation) à partir de l’objet que nous retournons dans la méthode doGet.
Par cet article, nous avons montré quelques aspects de Pivot, comme la conception déclarative des interfaces utilisateurs ou le binding entre les objets (Contact) et les composants (TableView et Form), mais nous avons voulu surtout insisté sur le développement coté serveur à l’aide de Pivot, un aspect qui a été jusqu’à maintenant négligé dans les tutoriaux et les démos de référence du framework.
Télécharger le code source de l’exemple.
Commentaires