vendredi 18 janvier 2008

Seam + ExtJS, i18n

How to internationalise a Seam + ExtJS application ?

I chose the following solution :
- for server-side generated messages and jsf labels, I need one or more resource bundles (properties file) for each langage

Example :

<messages_en.properties>

org.jboss.seam.loginFailed=Login failed
org.jboss.seam.loginSuccessful=Welcome, #0


<messages_fr.properties>

org.jboss.seam.loginFailed=Echec de la tentative de login
org.jboss.seam.loginSuccessful=Bienvenue, #0


In my Eclipse environment, I set content encoding of these files to ISO-8859-1

Now they can be used within the Seam framework scope (JSF, Pojos, EJBs, and so on). Nothing more to say that is explicitly documented in the Seam documentation.

- now for the client-side messages and labels, I create one javascript object type and an instance of it in a javascript file for each specific langage versions like this one :

<application-lang-en.js>

ApplicationLang = function()
{
// general dictionnary
this.codeIso = "ISO Code";

// helloAction labels and messages
this.helloAction_titreGrille = "Drivers list";
this.helloAction_titreFormulaire = "Driver details";
}

var lang = new ApplicationLang();


<application-lang-fr.js>

ApplicationLang = function()
{
// dictionnaire général
this.codeIso = "Code ISO";

// helloAction
this.helloAction_titreGrille = "Liste des chauffeurs";
this.helloAction_titreFormulaire = "Détail du chauffeur";
}

var lang = new ApplicationLang();


The lang variable can then be used in ExtJS object declarations :

var countryFieldSet = new Ext.form.FieldSet(
{
id: 'formulaireFields',
columnWidth: 0.3,
labelWidth: 50,
title: lang.helloAction_titreFormulaire,
...
}



To use the right javascript langage file, add these lines to the xhtml file :


<script type="text/javascript" src="ext/locale/ext-lang-#{localeSelector.localeString}.js"></script>
<script type="text/javascript" src="ext/locale/application-lang-#{localeSelector.localeString}.js"></script>


And to tell Seam which langages are supported, modify faces-config.xml :


<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr</supported-locale>
</locale-config>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>




Finally, I set the carset encoding for xhtml and javascript files to UTF-8. To be sure that the browser knows that my javascript files are UTF-8 I just had these lines to web.xml :




<mime-mapping>
<extension>js</extension>
<mime-type>text/javascript;charset=UTF-8</mime-type>
</mime-mapping>

Seam + ExtJS, Store, Reader, Proxy

Passons maintenant à la pratique. Je commencerai pas exposer les différentes classes côté ExtJS qui permettent de dialoguer avec Seam Remoting. Pour cela, je suis parti de l'exemple d'extension pour DWR proposé par Axel ici : http://extjs.com/forum/showthread.php?t=19529

Now it's time for some code examples. I'm begining to expose the different ExtJS extensions classes which permit to dialog with Seam Remoting. For this purpose, I started with the example extension for DWR proposed by Axel in the ExtJS User Extensions forum : http://extjs.com/forum/showthread.php?t=19529


/**
* Seam remoting proxy.
*
* Can be bound to any WebMethod which signature is like :
* - ResultList<T> webMethod(QueryParam param) for a non paging method.
* - PagingResultList<T> webMethod(PagingQueryParam param) for a paging method.
*
* @param {function} f Points to remote function.
* @param {Object} o Seam component.
* @param {boolean} paging Tells wether the remote method uses paging.
*/

Ext.data.SeamRemotingProxy = function(f, o, paging)
{
Ext.data.SeamRemotingProxy.superclass.constructor.call(this);
this.func = f;
this.instance = o;
this.paging = paging;
};

Ext.extend(Ext.data.SeamRemotingProxy, Ext.data.DataProxy,
{
/**
* Builds a query parameter object (queryParam or pagingQueryParam
* if paging is used by the remote method).
*
* Calls the seam remote method by passing to it the query parameter
* objet and the callback function.
*
* @param {Object} params Object with following fields defined : queryString,
* and if paging enabled, with following more fields : start, limit.
* @param {Object} reader
* @param {Object} loadCallback
* @param {Object} scope
* @param {Object} arg
* @param {Object} remoteArgs
*/

load: function(params, reader, loadCallback, scope, arg, remoteArgs)
{
var dataProxy = this;
dataProxy.fireEvent("beforeload", dataProxy, params);

var args = [];

// Wether the remote method method support paging or not,
// choose the right parameter class.
var queryParamComponentName;
if (this.paging)
{
queryParamComponentName = "pagingQueryParam";
}
else
{
queryParamComponentName = "queryParam";
}

var queryParam = Seam.Component.newInstance(queryParamComponentName);

//
for (var param in params)
{
queryParam[param] = params[param];
}

args[args.length] = queryParam;

args[args.length] = function(response)
{
dataProxy.fireEvent("load", dataProxy, response, loadCallback);
var records = reader.read(response);

if (records.records.length > 0)
{
scope.fields = records.records[0].fields;
}

loadCallback.call(scope, records, arg, true);
}

this.func.apply(this.instance, args);
}
});

/**
* Extension of Ext.data.Record to handle bean data.
* Extends record.get(name) in order to be able to access
* properties of underlying objects like bean.child.property.
*
* @param {Object} data
* @param {Object} id
*/

Ext.data.BeanRecord = function(data, id)
{
Ext.data.BeanRecord.superclass.constructor.call(this, data, id);
};

Ext.extend(Ext.data.BeanRecord, Ext.data.Record, {});

Ext.data.BeanRecord.create = function(o)
{
var f = Ext.data.Record.create.call(this, o);

/**
*
* @param {String} name
*/

f.prototype.get = function(name)
{
var names = name.split('.', 2);

if (names.length > 1)
{
var o = this.data[names[0]];

if (o)
{
var result = eval("o." + names[1]);
return result;
}
else
{
return "";
}
}
else
{
return this.data[name];
}
}

/**
* Copies a record and its underlying seam data bean.
* @param {String} componentName The seam component name of the underlying bean.
*/

f.prototype.copyRecord = function(componentName)
{
var copieRecord = this.copy();
var copieData = copyBean(this.data, componentName);
copieRecord.data = copieData;

return copieRecord;
}

return f;
};


/**
* Extends Ext.data.DataReader in order to read bean data.
*/

Ext.data.BeanReader = function()
{
Ext.data.BeanReader.superclass.constructor.call(this, null, []);
};

Ext.extend(Ext.data.BeanReader, Ext.data.DataReader,
{
read: function(response)
{

var records = [];
var bean;

for (var i = 0; i < response.list.length; i++)
{
bean = response.list[i];

// retreives the bean field names
var fields = [];
for (var prop in bean)
{
if ((new String(bean[prop])).substring(0, 8) != "function")
{
fields[fields.length] = prop;
}
}

// builds dynamically the record definition
var recordDefinition = "[";

var field;
for (var j = 0; j < fields.length; j++)
{
field = fields[j];
recordDefinition = recordDefinition + "{name: '" + field + "'}";
if (j < fields.length - 1)
{
recordDefinition = recordDefinition + ",";
}
}
recordDefinition = recordDefinition + "]";

var ObjectRecord = eval("Ext.data.BeanRecord.create(" + recordDefinition + ")");

// record instantiation
var myNewRecord = new ObjectRecord();

// record data is the bean
myNewRecord.data = bean;

records[records.length] = myNewRecord;
}
return {
records: records,
totalRecords: response.totalRecords
};
}
});

/**
* [en]
* Added configuration parameters :
* - remoteComponent (Object) : Seam component resulting from a call to
* Seam.Component.getInstance(name)
* - remoteComponentName (String) : Seam component name to remote-access.
* This parameter is non used if remoteComponent is defined.
* - remoteMethodName (String) : Web remote method name (annotated @WebRemote)
* - paging (bool) - optional (false by default) : tells wether the remote method uses paging.
*
* Use example :
*
* var store = new Ext.data.SeamRemotingStore(
* {
* remoteComponent: customerWebAction,
* remoteMethodName: "listAllCustomersWithPaging",
* paging: true,
* sortInfo:
* {
* field: 'nom',
* direction: "ASC"
* },
* groupField: 'customerType'
* });
*
* store.load(
* {
* params:
* {
* queryString: '',
* start: 0,
* limit: 15
* }
* });
*
* [fr]
* Paramètre de configurations supplémentaires :
* - remoteComponent (object) : Composant seam obtenu par un appel à Seam.Component.getInstance(name)
* - remoteComponentName (String) : nom du composant Seam à consulter (paramètre non pris en compte si le précédent est défini)
* - remoteMethodName (String) : nom de la méthode remote (marquée @WebRemote)
* - paging (bool) - optionnel (false par défaut) : la méthode remote supporte la pagination
*
* @param {Config} c
*/

Ext.data.SeamRemotingStore = function(c)
{
// configuration
var seamComponent = c.remoteComponent;

if (!c.remoteComponent)
{
var remoteComponentName = c.remoteComponentName;
seamComponent = Seam.Component.getInstance(remoteComponentName);
}

var remoteMethodName = c.remoteMethodName;

var paging;
if (c.paging)
{
paging = c.paging;
}
else
{
paging = false;
}

var remoteMethod = eval("seamComponent." + remoteMethodName);

// instantiale a proxy and a reader
this.proxy = new Ext.data.SeamRemotingProxy(remoteMethod, seamComponent, paging);
this.reader = new Ext.data.BeanReader();

// record currently selected
this.currentRecord = null;

this.getCurrentRecord = function()
{
return this.currentRecord;
}

this.setCurrentRecord = function(record)
{
this.currentRecord = record;
}

Ext.data.SeamRemotingStore.superclass.constructor.call(this, c);
};
Ext.extend(Ext.data.SeamRemotingStore, Ext.data.GroupingStore);

/**
* Utility method to create a copy of a bean knowing it's seam
* component name.
*
* @param {Object} bean The bean to be copied
* @param {String} beanName Seam component name
*/

function copyBean(bean, beanName)
{
var newBean = Seam.Component.newInstance(beanName);

for (var prop in bean)
{
if ((new String(bean[prop])).substring(0, 8) != "function")
{
newBean[prop] = bean[prop];
}
}

return newBean;
}


jeudi 3 janvier 2008

Seam + ExtJS, fondations

Après avoir pas mal recherché d'informations sur Internet à propos d'une intégration possible de Seam et ExtJS, il est apparu qu'aucun exemple de code ne circule ni même beaucoup de conseils en terme de conception.

Maintenant que j'ai un prototype qui fonctionne, je vais écrire quelques articles montrant pourquoi ce choix de technologie, comment le faire (avec de la théorie et du code source d'exemple), et aussi comment cela se passe sur un projet d'entreprise (montée en compétence sur cette technologie, limitations, bugs encore présents, etc), et enfin je pourrais donner des indications en terme de performances.

Pour commencer, ce premier article montre comment je compte lier ces deux frameworks prometteurs.

En premier lieu, mon souhait est de réhabiliter le poste client. Généralement on se contente de lui faire afficher les fichiers HTML, d'exécuter quelques javascripts simples pour valider les données saisies puis de soumettre le formulaire au serveur d'application en attendant d'afficher une nouvelle page complète.

Pour moi, le défaut de ce design est d'introduire une couche applicative relativement complexe, la couche de présentation, pour pouvoir manipuler ces soumissions de formulaires. Bien sûr de nombreux frameworks existent, comme Struts ou JSF, mais ils restent tout de même relativement lourds et représentent une part importante dans le temps de développement d'une application.

De plus ce mode de fonctionnement oblige à envoyer l'ensemble des données du formulaire au serveur d'application qui renvoie l'ensemble du code HTML à afficher côté client. On observe à cause de ce mécanisme :

  • une complexité accrue dans la gestion de l'état de la page : une donnée chargée pour être affichée dans la page dans un temps t0, pour ne pas avoir à être de nouveau chargée, doit être gérée dans un cache : par exemple dans un viewstate (applicable à jsf ou asp .net) ou alors un cache applicatif qu'il faut aller consulter lorsque l'on construit la page (Struts + jsp par exemple)
  • des pertes de performance et de ressources notables car d'une part on fait transiter sur le réseau des données inutiles (des données de formulaires inchangées, puis des blocs complets d'html non modifiés...), et d'autre part on s'oblige à valider côté serveur des données qui n'ont pas lieu d'être (encore une fois ces données de formulaire inchangées...) !

Mon souhait est donc d'évincer cette couche applicative et de rendre au client le rôle de contrôleur. Avec les technologies Ajax il est désormais possible d'effectuer un grand nombre d'actions sans pour autant avoir besoin de faire des soumissions de formulaires HTTP tout en limitant les données en transit au minimum nécessaire.

Dans ce cas de figure, le serveur d'application se contente d'être un serveur de services (on s'oriente donc vers une architecture de type SOA), en exposant une "façade web" à ses services métiers. Ces façades se contentent de centraliser la sécurité d'accès aux services, de gérer les exceptions métier et de remonter les objets et messages au client javascript. Ainsi pour un même service métier, on pourrait avoir une façade webservice et une autre façade Seam Remoting.

C'est là qu'entre en jeu Seam Remoting : plutôt que d'utiliser un webservice comme source de données aux composants clients ExtJS (relativement lourd à mettre en place côté serveur comme côté client), nous allons utiliser la possibilité d'interroger à distance un Session Bean (avec en plus la possibilité de s'inscrire dans un contexte de conversation).

Pour information et pour avoir une idée de ce que je tente de faire sur ce prototype voici une copie statique de l'interface :



La liste est chargée en consultant un SessionBean (EJB 3.0), la pagination est activée ainsi que la possibilité de trier les colonnes et de grouper les lignes selon un critère.

Quant on clique sur une ligne, son détail s'affiche dans le formulaire de droite. Save permet d'envoyer une requête à un SessionBean qui met à jour la donnée en base. Selon le message renvoyé par ce SessionBean (succès, erreur, ...), on met à jour côté client les données telles que sauvegardées.

L'avantage de cette solution : seules les données du formulaire de modification sont envoyées au serveur (par exemple pas besoin d'envoyer les informations d'état sur la page...), et surtout, aucun besoin de renvoyer tout le code html pour raffraîchir la liste !

Parce qu'un diagramme vaut mieux qu'un long discours voilà ce que cela donne de manière synthétique. Je ne suis pas rentré dans les détails de ce qui ce passe côté ExtJS :

La partie Faces/jsf est réduite à sa partie minimale, c'est-à-dire initialiser la page à son miminum (ce qui ne comprend même pas le chargement de la liste par exemple). Tous les événements se produidant sur la page seront traités par Seam Remoting.

Suite dans un prochain article où je montrerai le code source des classes Store, Reader, etc qui permettent de faire le lien entre ExtJS et Seam Remoting...

Vers l'article suivant : http://ntispace.blogspot.com/2008/01/seam-extjs-ext-store-reader-proxy.html