GWT Data Binding (3/3) : présentation de la problématique

Dans ce dernier article, nous verrons comment générer dynamiquemt une classe, et comment l’utiliser dans notre projet GWT.

Utiliser un Generator maison

Quand on utilise GWT.create(Classe.class), GWT utilise le générateur par défaut qui renvoie le nom de la classe. Pour spécifier à GWT d’utiliser un autre générateur, il faut ajouter ces lignes dans le fichier de configuration XML du module.

<generate-with class=”com.zenika.tutorial.rebind.WrapperGenerator”>
	<when-type-assignable class=”com.zenika.tutorial.gwt.client.BusinessObject”/>
</generate-with>

Cette partie de code indique à GWT d’utiliser WrapperGenerator pour tout appel d’objet implémentant l’interface BusinessObject. L’interface BusinessObject est vide et ne sert qu’à identifier les classes pour l’appel de WrapperGenerator. En ce sens, la technique que nous présentons est légèrement instrusive.
La classe WrapperGenerator étend la classe abstraite Generator de GWT (contenu dans gwt-dev-windows.jar) et se trouve par convention dans un package .rebind en dehors du module GWT. Ceci est normal étant donné que cette classe n’est pas amenée à être compilée ou à fournir un service RPC.

/**
 * Méthode appelé lors de la séquence de Deferred Binding initié par l’appel GWT.create(). Renvoie le nom de la classe à instancier.
 */
public class WrapperGenerator extends Generator {
	public String generate(TreeLogger logger, GeneratorContext context, String typeClass) throws UnableToCompleteException {
		WrapperCreator binder = new WrapperCreator(logger, context, typeClass);
		String className = binder.createWrapper();
		return className;
	}
}

La méthode generate prend en paramètre :
TreeLogger qui sert à loguer les messages
GeneratorContext qui gère les méta-données
typeClass qui est le nom de la classe passée en paramètre de GWT.create
Et elle renvoie le nom de la classe à instancier. Ici, nous voulons qu’elle renvoie PersonneWrapper au lieu de Personne. Cette classe PersonneWrapper n’existe pas encore et sera créée dynamiquement à partir de la classe Personne par la classe WrapperCreator.

Générer une classe dynamiquement

La dernière étape est la création du wrapper avec la classe WrapperCreator. Nous allons décomposer son fonctionnement.
Les paramètres de la méthode generate sont récupérés dans le constructeur. L’objet TypeOracle extrait du GeneratorContext est une Classe de Gwt qui permet d’accéder aux informations (nom de la classe, méthodes, paramètres de méthodes, etc.) de la classe passée en paramètre de la méthode GWT.create() (ici la classe Personne) de manière similaire à la réflexion java.

public WrapperCreator(TreeLogger logger, GeneratorContext context, String typeName) {
	this.logger = logger;
	this.context = context;
	this.typeName = typeName;
	this.typeOracle = context.getTypeOracle();
}

Ensuite la méthode createWrapper() est appelée par WrapperGenerator. Celle-ci récupère dans l’oracle le nom de la classe demandée et appelle la méthode getSourceWriter().

public String createWrapper() {
	try {
		JClassType classType = typeOracle.getType(typeName);
        SourceWriter source = getSourceWriter(classType);
	}
}
public SourceWriter getSourceWriter(JClassType classType) {
	String packageName = classType.getPackage().getName();
	String simpleName = classType.getSimpleSourceName() + "Wrapper";
	ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, simpleName);
	composer.addImplementedInterface("com.zenika.tutorial.gwt.client.Wrapper");
	PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);
	if (printWriter == null) {
		return null;
	} else {
		SourceWriter sw = composer.createSourceWriter(context, printWriter);
		return sw;
	}
}

Le SourceWriter gère le flux textuel associé à la définition de la classe à générer. Il fonctionne comme un FileWriter sauf qu’il gère en plus l’indentation. Le SourceWriter renvoyé contient la déclaration du package, du nom de la classe et de l’interface à implémenter.
Il ne reste plus qu’à écrire le code de la classe. Pour cela, on se sert de l’oracle pour générer les méthodes getAttribute() et setAttribute() en fonction des propriétés de l’objet Personne. Voici la partie de code qui crée la méthode getAttribute() (le code complet de la classe est disponible en téléchargement ).

public void getSourceWriter(JClassType classType) {
	JMethod[] methods = classType.getMethods();
	for (int i = 0; i < methods.length; i++) {
		String methodName = methods[i].getName();
		JParameter[] methodParameters = methods[i].getParameters();
		JType returnType = methods[i].getReturnType();
		if (methodName.startsWith("get") & methodParameters.length == 0) {
			source.println("if (attr.equals(\"" + methodName.substring(3).toLowerCase() + "\")) {");
			source.indent();
			source.println("return content." + methodName + "();");
			source.outdent();
			source.print("} else ");
		}
	}
	source.println("{");
	source.indent();
	source.println("return null;");
	source.outdent();
	source.println("}");
}

Utiliser le wrapper

Maintenant pour utiliser le wrapper il vous suffit de taper ceci :

Wrapper wrapper = (Wrapper) GWT.create(Personne.class);
wrapper.setContent(personne);

Et vous pouvez accéder aux données contenues dans personne avec la fonction getAttribute de wrapper (par exemple, getAttribute(”nom”)).
Voici un exemple d’utilistaion concrète du Wrapper :

// Création du modèle, un pojo simple normalement récupéré par un service RPC GWT
final Personne personne = new Personne("Robert", "Charlebois");
// Création du wrapper du modèle pour pouvoir binder (c’est à dire le lier à l’interface)
Wrapper wrapper = (Wrapper) GWT.create(Personne.class);
// wrappe le modèle
wrapper.setContent(personne);
// on bind le nom et le prenom avec deux champs de saisis
RootPanel.get().add(Binder.bind(new TextBox(), wrapper, "prenom"));
RootPanel.get().add(Binder.bind(new TextBox(), wrapper, "nom"));
Button button = new Button("Inspecter les nouvelles valeurs du modele");
button.addClickListener(new ClickListener() {
	public void onClick(Widget sender) {
		// on teste pour voir si l’IHM a bien répercuté les nouvelles valeurs sur le modèle.
		Window.alert(personne.getPrenom() + " " + personne.getNom());
	}
});
RootPanel.get().add(button);

 

Pour la suite

Nous avons mis à votre disposition en annexe un fichier zip contenant les codes sources du générateur de wrapper et un example de binding de données.
Je tiens particulièrement à remercier Ray Cromwell qui sur son blog Timepedia a été une des premières personnes à documenter le Generator.
Ce tutorial est une mise en bouche vers un tutorial plus complet axé lui-aussi sur le data binding. Nous verrons notamment comment créer une classe Bind qui prendra en charge la création du wrapper de manière complètement générique et la création dynamique de validator.
Pour toutes questions, n’hésitez pas à me contacter à cette adresse : pierre.queinnec@zenika.com

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :