Apache Pivot, l’autre RIA

Publié dans: 

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

Soumis le 04 Février 2012 - 16:56
Comment: 
Merci pour l'article, très instructif.