Valid XHTML 1.0! Valid CSS!

Application distante avec XUL, RDF et Jena
Auteur : Martial BRAUX <mbraux@systemplus.fr> <martial.braux@free.fr>
Version 0.9
Introduction
Pourquoi cet article ?

Cet article va tenter de présenter mon expérience dans le développement d'applications en XUL au sein de System Plus.

Son but est de montrer une méthode de création d'application XUL avec accès à des données distantes. Il n'a pas la prétention d'être une référence, mais simplement un point de repère pour tout développeur qui cherche à appréhender les technologies XUL et RDF sur un exemple ni trop simple, ni trop complexe.

Tout retour d'expérience ou de remarques sera le bienvenu. Vous pouvez m'écrire à martial.braux@free.fr.

Un complément de ce document est l'article sur la certification d'applications distantes avec Mozilla.

Pourquoi pas du HTML ?

Le HTML est utilisé pour la présentation de documents sur Internet. Interprété sur le navigateur de l'utilisateur, c'est un langage figé, c'est à dire que l'affichage du document est statique si l'on ne fait pas intervenir de langages de scripts tels que JavaScript.

Cet aspect du HTML devient handicapant lorsque l'on désire interagir sur l'affichage de données. Toute manipulation nécessite un rechargement de la page, ou bien l'utilisation de scripts interprétés côté client.

Une démonstration de cette problématique est le tri des colonnes de données dans un tableau. En HTML pur, le tri ne peut être réalisé que sur le serveur Web, une requête est donc envoyée au serveur afin de lui demander de renvoyer la page avec les lignes du tableau correctement ordonnées. Cette tâche est relativement fastidieuse et c'est typiquement ce que l'on cherche à éviter.

Le projet

Le but du projet que nous allons étudier ici est de créer un principe client-serveur qui permette à l'utilisateur de manipuler des données distantes.

Le client doit présenter les avantages d'un client léger (type client web) avec les avantages d'un client lourd (composantes graphiques, manipulation des données en local...). Cela signifie qu'il doit permettre d'interroger le serveur, recevoir les données, les afficher dans l'interface, éventuellement les modifier puis les renvoyer au serveur.

Nous allons nous placer dans l'optique d'une entreprise qui possède une base de données de projets créés par des utilisateurs. Nous ne gérerons pas ici l'aspect base de données, ni même l'accès aux données stockées. Nous émettons l'hypothèse que cet aspect des choses est géré par un serveur d'objets métiers reposant sur OJB, auquel nous accéderons par notre servlet.

Nous créerons une interface permettant l'accès à la liste des projets et leur modification.

Le serveur sera composé d'une servlet dédiée à une tâche précise et d'un serveur d'objets métier.

Les transferts seront réalisés avec le protocole HTTP et les données seront formatées en RDF pour transiter sur le réseau.

Nous expliquerons nos choix dans chaque partie.

fig 1 : fonctionnement général de l'application
Modélisation de notre application
XUL
Qu'est-ce que XUL ?

XUL est un langage de description d'interfaces basé sur XML. C'est l'acronyme de XML User Interface Language.

XUL fait partie de XPFE, un outil utilisé dans le développement de Mozilla, navigateur Web libre issu de feu Netscape.

Pour ceux qui connaissent déjà le HTML, le dépaysement ne devrait pas être total puisqu'une interface XUL est composée d'éléments décrits par des balises XML.

L'un des avantages de XUL est de pouvoir séparer l'interface utlisateur en quatre parties :

Organisation du paquetage

Nous allons adopter une organisation normée pour hiérarchiser notre application.

Nous allons créer un répertoire du nom de notre application (appelons la AppXul).

Ensuite nous créerons trois sous-répertoires :

Dans les répertoires content et skin nous allons créer un sous-répertoire du nom de notre application (encore !),

Le répertoire locale est un peu particulier puisqu'il contiendra les fichiers de traduction. Il faudra donc prévoir autant de sous-répertoires que de langues disponibles. Dans notre cas, nous prévoirons deux sous-répertoires :

Chacun des sous-répertoires de langue devra lui-même contenir un sous-répertoire du nom de notre application.

Voici notre arborescence finale :

AppXul
|-- content
|   `-- appxul
|-- locale
|   |-- en-US
|   |   `-- appxul
|   `-- fr-FR
|       `-- appxul
`-- skin
    `-- appxul

Si nous utilisons cette norme, ce n'est pas pour nous compliquer la tâche, c'est simplement pour assurer la compatibilité avec l'arborescence chrome de Mozilla. Celle-ci décrit chaque application utilisée dans Mozilla y compris Mozilla lui-même.

RDF
Qu'est-ce que RDF ?

Le RDF (Resource Description Framework) ou environnement de description de ressources en français est un langage généraliste basé sur XML dont le but est d'offrir une représentation des informations contenues sur Internet.

Le RDF est intimement lié à la notion de web sémantique. L'idée derrière le web sémantique est de donner le maximum de sens aux données disponibles sur Internet par l'utilisation de meta-données. Le web sémantique comporte un autre composante en plus du RDF : le OWL (Web Ontology Language).

Le OWL est un langage qui a pour but l'interprétabilité des données par un ordinateur, mais nous ne l'aborderons pas ici.

Intérêt du RDF pour notre application

Comme nous venons de le voir, le RDF permet de décrire des données. Son cadre d'utilisation est donc très vaste.

Cependant, l'utilisation que nous allons en faire pourra sembler restreinte, voire abusive pour les puristes.

En effet, nous allons l'utiliser pour servir de conteneur à nos données. Si on fait l'analogie avec un pli postal, le RDF sera le papier à bulles dans lequel seront rangées nos lettres.

Pourquoi utiliser le RDF pour une telle tâche alors que le XML pourrait bien être suffisant ?

Le RDF est l'outil le mieux adapté pour la persistence de nos données.

Jena
Qu'est-ce que Jena ?

Jena est une collection de librairies pour la création d'applications pour le web sémantique (http://jena.sourceforge.net).

Jena est Open Source et a été originellement développée par les laboratoires Hewlett Packard. Elle permet, entre autres, la manipulation de RDF et OWL via des API documentées.

Intérêt de Jena pour notre application

Jena est écrit en Java et par conséquent directement intégrable à une servlet.

Puisque le principe client/serveur que l'on se propose de développer ici est basé sur un serveur Tomcat, nous devrons manipuler du RDF sur une servlet écrite en Java.

Les API Jena sont complètement adaptées à nos besoins.

La servlet
Modélisation
fig 2 : Modélisation des composants de la servlet RDF
composants de la servlet RDF

Notre servlet va avoir deux rôles :

Pour effectuer ces tâches, la servlet sera assistée par deux autres classes :

Un lien est nécessaire pour permettre la modification des objets métier d'origine. Ce lien sera réalisé grâce à l'interface Identifier, implémentée dans notre exemple par la classe OjbIdentifierImpl (notre middleware étant OJB d'Apache --- http://db.apache.org/ojb/)

Implémentation
La classe RDFModelFactory

Jena permet de travailler sur des modèles RDF. Ces modèles sont la représentation en mémoire côté serveur d'un document RDF. Pour assurer la compréhension des documents RDF/XML envoyés/reçus par le serveur, nous allons concevoir une classe utilitaire permettant cette transcription RDF/XML-Model.

Voici son code source :

RDFModelFactory.java
0   | /*
1   |  * Created on 7 juil. 2004
2   |  *
3   |  */
4   | package systemplus.rdf;
5   | 
6   | import java.io.ByteArrayInputStream;
7   | import java.io.InputStream;
8   | import java.io.InputStreamReader;
9   | import java.io.StringWriter;
10  | import java.io.UnsupportedEncodingException;
11  | 
12  | import com.hp.hpl.jena.rdf.model.Model;
13  | import com.hp.hpl.jena.rdf.model.ModelFactory;
14  | 
15  | /**
16  |  * @author mb
17  |  */
18  | public class RDFModelFactory {
19  | 	
20  | 	/**
21  | 	 * Lit du RDF/XML et retourne le modèle correspondant.
22  | 	 * @param bufferRDFXML le RDF/XML.
23  | 	 * @param encoding l'encodage de caractères utilisé.
24  | 	 * @return model le modèle RDF.
25  | 	 */
26  | 	public static Model createModel(String bufferRDFXML, String encoding) {
27  | 		// création d'un modèle vide
28  | 		Model model = ModelFactory.createDefaultModel();
29  | 		// Récupration d'un flux depuis le buffer
30  | 		InputStream in = new ByteArrayInputStream(bufferRDFXML.getBytes());
31  | 		// Lecture du RDF/XML
32  | 		try {
33  | 			model.read(new InputStreamReader(in, encoding), "");
34  | 		} catch (UnsupportedEncodingException e) {
35  | 			e.printStackTrace();
36  | 		}
37  | 		return model;
38  | 	}
39  | 		
40  | 	/**
41  | 	 * Sérialise un modèle RDF/Jena en RDF/XML compréhensible par Mozilla.
42  | 	 *  
43  | 	 * @param aModel le modèle à sérialiser.
44  | 	 * @return String la chaîne RDF/XML.
45  | 	 */
46  | 	public static String createString(Model aModel) {
47  | 		// Création de l'imprimante
48  | 		StringWriter writer = new StringWriter();
49  | 
50  | 		// Ecriture RDF/XML sur l'imprimante
51  | 		// le RDF/XML-ABBREV est le RDF/XML compréhensible par Mozilla et 
52  | 		// directement interprétable en XUL
53  | 		aModel.write(writer, "RDF/XML-ABBREV");
54  | 
55  | 		// Retour du résultat
56  | 		return writer.toString();
57  | 	}
58  | 		
59  | }

L'appel à cette classe se fait de manière statique.

Pour créer un modèle il faut spécifier la chaîne de caractères représentant le document RDF/XML et l'encodage de caractères utilisé. Par exemple :

Model monModele = RDFModelFactory.createModel(monContenuDeDocumentRDF, "UTF-8");

La création de la chaîne représentant le document se fait plus simplement :

String monContenuDeDocumentRDF = RDFModelFactory.createString(monModele);

RDFModelFactory peut donc servir de base pour tout travail sur un serveur de RDF. Les modèles de Jena sont un support idéal pour un travail générique sur des données RDF et peuvent être utilisés dans toute application utilisant ces API.

La classe RDFJavatizer

Le but que nous cherchons à atteindre est la persistence d'objets Java sous forme de document RDF. Grâce à la classe RDFModelFactory, nous allons pouvoir nous concentrer sur la manipulation des modèles RDF.

La classe RDFJavatizer va permettre la conversion d'objets Java vers un modèle RDF et inversement.

RDFJavatizer.java
0   | /*
1   |  * Created on 7 juil. 2004
2   |  */
3   | package systemplus.rdf;
4   | 
5   | import java.lang.reflect.Field;
6   | import java.security.AccessController;
7   | import java.security.PrivilegedAction;
8   | import java.sql.Date;
9   | import java.util.Collection;
10  | import java.util.Iterator;
11  | import java.util.Map;
12  | import java.util.Vector;
13  | 
14  | import systemplus.util.BoFactory;
15  | 
16  | import com.hp.hpl.jena.rdf.model.Literal;
17  | import com.hp.hpl.jena.rdf.model.Model;
18  | import com.hp.hpl.jena.rdf.model.ModelFactory;
19  | import com.hp.hpl.jena.rdf.model.NsIterator;
20  | import com.hp.hpl.jena.rdf.model.Property;
21  | import com.hp.hpl.jena.rdf.model.RDFNode;
22  | import com.hp.hpl.jena.rdf.model.ResIterator;
23  | import com.hp.hpl.jena.rdf.model.Resource;
24  | import com.hp.hpl.jena.rdf.model.Selector;
25  | import com.hp.hpl.jena.rdf.model.Seq;
26  | import com.hp.hpl.jena.rdf.model.SimpleSelector;
27  | import com.hp.hpl.jena.rdf.model.Statement;
28  | import com.hp.hpl.jena.rdf.model.StmtIterator;
29  | 
30  | /**
31  |  * @author mb
32  |  */
33  | public class RDFJavatizer {
34  | 	
35  | 	/** Le modèle RDF vide. */
36  | 	private Model model;
37  | 	/** Niveau d'exploration */
38  | 	private int maxLevel;
39  | 	/** Niveau d'exploration */
40  | 	private int level;
41  | 	/** URI des ressources RDF. */
42  | 	private String uri;
43  | 	/**	service d'identifiant d'objet. */
44  | 	private Identifier identifier;
45  | 	/** Un champ... */
46  | 	private Field field;
47  | 	
48  | 	/**
49  | 	 * Constructeur.
50  | 	 * @param anUri l'URI de base du RDF.
51  | 	 */
52  | 	public RDFJavatizer (String anUri) {
53  | 		uri = anUri;
54  | 		maxLevel = 1;
55  | 		level = 0;
56  | 		// Création de l'identifieur	
57  | 		identifier = OjbIdentifierImpl.getInstance();
58  | 		//	Création d'un modèle RDF vide	
59  | 		model = ModelFactory.createDefaultModel();
60  | 	}
61  | 	
62  | 	/**
63  | 	 * Crée le modèle RDF/Jena d'un objet.
64  | 	 *
65  | 	 * @param objet l'objet à modéliser.
66  | 	 * @return Model le modèle généré.
67  | 	 */
68  | 	public Model modelize(Object objet) {
69  | 		toResource(objet);
70  | 		return model;
71  | 	}
72  | 
73  | 	/**
74  | 		 * Convertit un objet en ressource RDF/Jena.
75  | 		 * @param objet l'objet à convertir.
76  | 		 * @return la ressource RDF correspondante.
77  | 		 */
78  | 	private Resource toResource(Object objet) {
79  | 		return toResource(objet, null);
80  | 	}
81  | 	/**
82  | 	 * Convertit un objet en ressource RDF/Jena.
83  | 	 * @param objet l'objet à convertir.
84  | 	 * @return la ressource RDF correspondante.
85  | 	 */
86  | 	private Resource toResource(Object objet, Object parent) {
87  | 		try {
88  | 			// incrémentation du niveau d'exploration
89  | 			level++;
90  | 			// teste si l'objet est un conteneur
91  | 			if (isContainer(objet)) {
92  | 				// Isole la collection du conteneur
93  | 				Collection coll = null;
94  | 				if (java.util.Map.class.isInstance(objet)) {
95  | 					coll = ((Map) objet).values();
96  | 				} else {
97  | 					coll = (Collection) objet;
98  | 				}
99  | 				Iterator iter = coll.iterator();
100 | 				// Initialise le nom de la séquence en fonction du type dans la collection
101 | 				Object obj = iter.next();
102 | 				String resourceName = obj.getClass().getName();
103 | 				resourceName =
104 | 					resourceName.substring(resourceName.lastIndexOf('.') + 1);
105 | 				Seq container =	model.createSeq
106 | 				(uri+"/all-"+resourceName+"s"+(parent!=null?"-of-"+parent.hashCode():""));
107 | 				while (obj != null) {
108 | 					Resource rscTmp = toResource(obj);
109 | 					container.add(rscTmp);
110 | 					obj = null;
111 | 					if (iter.hasNext())
112 | 						obj = iter.next();
113 | 				}
114 | 				// désincrémente le niveau d'exploration
115 | 				level--;
116 | 				// retour du conteneur (une séquence est une ressource)
117 | 				return container;
118 | 			} else {
119 | 				// initialisation du nom de la ressource
120 | 				String resourceName = objet.getClass().getName();
121 | 				resourceName =
122 | 					resourceName.substring(resourceName.lastIndexOf('.') + 1);
123 | 
124 | 				// création de la ressource	
125 | 				String objectId = identifier.getURI(objet);
126 | 				Resource rscTmp = model.createResource(uri + "/" + objectId);
127 | 
128 | 				// création du namespace
129 | 				String ns =
130 | 					uri
131 | 						+ '/'
132 | 						+ objectId.substring(0, objectId.lastIndexOf('/'))
133 | 						+ '#';
134 | 
135 | 				// on a un objet complexe
136 | 				// récupération des attributs déclarés dans la classe de l'objet.
137 | 				Field[] fields = objet.getClass().getDeclaredFields();
138 | 				// parcours des champs
139 | 				for (int i = 0; i < fields.length; i++) {
140 | 					// récupération de l'instance
141 | 					Object objTmp = getInstanceFrom(fields[i], objet);
142 | 
143 | 					// création du nom de la propriété
144 | 					String propertyName = fields[i].getName();
145 | 					propertyName =
146 | 						propertyName.substring(
147 | 							propertyName.lastIndexOf('.') + 1);
148 | 					// conversion puis ajout de la valeur de la propriété
149 | 					if ((objTmp != null)) {
150 | 						if (fields[i].getType().isPrimitive()) {
151 | 							rscTmp.addProperty(
152 | 								model.createProperty(ns, propertyName),
153 | 								objTmp);
154 | 						} else if (objTmp instanceof String) {
155 | 							rscTmp.addProperty(
156 | 								model.createProperty(ns, propertyName),
157 | 								objTmp);
158 | 						} else if (objTmp instanceof Date) {
159 | 							rscTmp.addProperty(
160 | 								model.createProperty(ns, propertyName),
161 | 								TypeConverter.convert((Date) objTmp));
162 | 						} else if (level <= maxLevel) {
163 | 
164 | 							// Traitement des objets conteneurs 
165 | 							if (isContainer(objTmp)) {
166 | 								Collection coll = null;
167 | 								if (java.util.Map.class.isInstance(objet)) {
168 | 									coll = ((Map) objTmp).values();
169 | 								} else {
170 | 									coll = (Collection) objTmp;
171 | 								}
172 | 								Iterator iter = coll.iterator();
173 | 								if (iter.hasNext())
174 | 									rscTmp.addProperty(
175 | 										model.createProperty(ns, propertyName),
176 | 										toResource(objTmp, objet));
177 | 							}
178 | 							// Objets métiers
179 | 							else if (isBO(objTmp)) {
180 | 								rscTmp.addProperty(
181 | 									model.createProperty(ns, propertyName),
182 | 									toResource(objTmp));
183 | 							}
184 | 						}
185 | 					} else {
186 | 						rscTmp.addProperty(
187 | 							model.createProperty(ns, propertyName),
188 | 							"");
189 | 					}
190 | 				}
191 | 				
192 | 				// la ressource est prête
193 | 				level--;
194 | 				return rscTmp;
195 | 			}
196 | 
197 | 		} catch (Exception e) {
198 | 			// message de déboguage
199 | 			e.printStackTrace();
200 | 			// retour d'une ressource nulle
201 | 			return (Resource) null;
202 | 		}
203 | 	}
204 | 
205 | 	/**
206 | 	 * Teste si un objet est un conteneur.
207 | 	 *
208 | 	 * @param objet l'objet  tester.
209 | 	 * @return boolean le résultat du test.
210 | 	 */
211 | 	private boolean isContainer(Object objet) {
212 | 		// 2 types de conteneurs sont identifiés ici :
213 | 		// - les conteneurs issus de java.util.Collection ;
214 | 		// - les conteneurs issus de java.util.Map.
215 | 		return (
216 | 			java.util.Collection.class.isInstance(objet)
217 | 				|| java.util.Map.class.isInstance(objet));
218 | 	}
219 | 	
220 | 	/**
221 | 	 * Récupère la valeur d'un champ Java dans un objet.
222 | 	 * Enveloppe la méthode get() du champ en gérant l'inaccessibilité.
223 | 	 * @param champ le champ dont on veut récuprer la valeur.
224 | 	 * @param objet l'objet auquel le champ appartient.
225 | 	 * @return la valeur du champ.
226 | 	 */
227 | 	private Object getInstanceFrom(Field champ, Object objet) {
228 | 		try {
229 | 			// on tente une récupération "gentillette"
230 | 			return champ.get(objet);
231 | 		} catch (IllegalAccessException e) {
232 | 			// ça ne fonctionne pas... on passe au niveau supérieur ;0)
233 | 			return getInstanceFromNonAccessible(champ, objet);
234 | 		}
235 | 	}
236 | 	/**
237 | 	 * Récupère la valeur d'un champ non accessible dans un objet.
238 | 	 * Méthode inspirée de l'implémentation du projet OJB d'Apache.
239 | 	 * @param champ le champ dont on veut récupérer la valeur.
240 | 	 * @param objet l'objet auquel le champ appartient.
241 | 	 * @return la valeur du champ.
242 | 	 */
243 | 	private Object getInstanceFromNonAccessible(Field champ, Object objet) {
244 | 
245 | 		try {
246 | 			field = champ;
247 | 			// récupération de l'état d'accessibilité du champ
248 | 			boolean before = champ.isAccessible();
249 | 			// On rend le champ accessible
250 | 			PrivilegedAction action = new PrivilegedAction() {
251 | 				public Object run() {
252 | 					getField().setAccessible(true);
253 | 					return null;
254 | 				}
255 | 			};
256 | 			AccessController.doPrivileged(action);
257 | 			// récupération de l'instance
258 | 			Object objTmp = champ.get(objet);
259 | 			// on remet l'accessibilité dans son état précédent 
260 | 			champ.setAccessible(before);
261 | 			// on retourne l'objet
262 | 			return objTmp;
263 | 		} catch (Exception e) {
264 | 			// problème non prévu ?
265 | 			e.printStackTrace();
266 | 			return (Object) null;
267 | 		}
268 | 	}
269 | 	/**
270 | 	 * Retourne le champ utilisé pour getInstanceFromNonAccessible().
271 | 	 * @return le champ
272 | 	 */
273 | 	private Field getField() {
274 | 		return field;
275 | 	}
276 | 
277 | 	/**
278 | 	 * Teste si l'objet est un "business object", un objet métier, c'est  dire un objet non directement interprétable.
279 | 	 *
280 | 	 * @param objet l'objet à tester.
281 | 	 * @return boolean le résultat.
282 | 	 */
283 | 	private boolean isBO(Object objet) {
284 | 		// TODO voir si la notion d'objet-métiers ne peut pas être plus générale
285 | 		if (objet == null)
286 | 			return false;
287 | 		return (
288 | 			objet.getClass().getName().startsWith("systemplus.bo")
289 | 				|| objet.getClass().getName().startsWith("systemplus.icp")
290 | 				|| objet.getClass().getName().startsWith("systemplus.account"));
291 | 	}
292 | 
293 | 	/**
294 | 	 * Lance la désérialisation.
295 | 	 * @param bufferRDFXML la chaîne RDF/XML à désérialiser.
296 | 	 */
297 | 	public void javatize(Model aModel) {
298 | 		model = aModel;
299 | 
300 | 		// Collecte des espaces de noms qui nous intéressent
301 | 		Vector nsVect = new Vector();
302 | 		NsIterator nsi = model.listNameSpaces();
303 | 		while (nsi.hasNext()) {
304 | 			String ns = nsi.nextNs();
305 | 			if (ns.matches(uri+".*")) {
306 | 				nsVect.add(ns.substring(0, ns.indexOf("#")));
307 | 			}
308 | 		}
309 | 		
310 | 		// récupération des ressources RDF
311 | 		ResIterator ni = model.listSubjects();
312 | 		while (ni.hasNext()) {
313 | 			Resource rsc = ni.nextResource();
314 | 			// on s'assure que la ressource fait bien parti des espaces de noms
315 | 			int last = rsc.getNameSpace().lastIndexOf('/');
316 | 			if (last == -1) {
317 | 				last = rsc.getNameSpace().length();
318 | 			}
319 | 			if (nsVect.contains(rsc.getNameSpace().substring(0, last))) {
320 | 				// PreProcessing
321 | 				javatizePreProcess();
322 | 				// on peut maintenant modifier l'objet java correspondant
323 | 				Object o = modifJavaObject(rsc);
324 | 				// PostProcessing
325 | 				javatizePostProcess(o);
326 | 			}
327 | 		}
328 | 	}
329 | 
330 | 	/**
331 | 	 * Modifie un objet java dans un référentiel d'après une ressource RDF.
332 | 	 * @param rsc la ressource représentant l'objet java.
333 | 	 */
334 | 	private Object modifJavaObject (Resource rsc) {
335 | 		System.out.println();
336 | 		// récupération de l'identifiant de l'objet java
337 | 		String id = rsc.toString().substring(uri.length()+1);
338 | 
339 | 		// récupération du nom de la classe (utile pour déboguage)
340 | 		String className = rsc.toString().substring(uri.length()+1, rsc.toString().lastIndexOf("/"));
341 | 
342 | 		// Récupération de l'objet Java dans le référentiel
343 | 		Object jObject = identifier.getObject(id);
344 | 		// TODO A compléter: La gestion des nouveaux objets
345 | 		if (jObject==null) return jObject;
346 | 		 System.out.println(jObject);
347 | 		// Récupération des arcs dont la ressource est sujet
348 | 		// ces derniers sont les représentations des champs de l'objet java
349 | 		StmtIterator iter = getArcsWithSubject(rsc);
350 | 		
351 | 		// parcours des arcs et modification de l'objet
352 | 		while (iter.hasNext()) {
353 | 			// récupération de l'arc
354 | 			Statement stmt = iter.nextStatement();
355 | 			// récupération du nom de l'attribut
356 | 			String fieldName = stmt.getPredicate().getLocalName();
357 | 			// récupération du noeud RDF de l'objet de l'arc
358 | 			RDFNode valueNode = stmt.getObject();
359 | 			// L'objet est-il un litéral ?
360 | 			if (valueNode.canAs(Literal.class)) {
361 | 				// vérification de l'existence de l'attribut dans l'objet java
362 | 				Field champ=null;
363 | 				try {
364 | 					champ = jObject.getClass().getDeclaredField(fieldName);//	la mthode douce
365 | 					champ.set(jObject, TypeConverter.convertValue(champ.getType(), stmt.getLiteral().toString()));
366 | 				} catch (SecurityException e1) {
367 | 					e1.printStackTrace();
368 | 				} catch (NoSuchFieldException e1) {
369 | 					// Le champs présents dans le RDF n'existe pas dans l'objet
370 | 					System.out.println("Le champs prsent dans le RDF n'existe pas dans l'objet : "+fieldName);
371 | 				} catch (IllegalArgumentException e) {
372 | 					e.printStackTrace();
373 | 				} catch (IllegalAccessException e) {
374 | 					// la méthode brutale
375 | 					 setToNonAccessible(champ, jObject, stmt.getLiteral().toString());
376 | 				}
377 | 				
378 | 			}
379 | 		}
380 | 		// Return modified object
381 | 		return jObject;	
382 | 	}
383 | 
384 | 	/** 
385 | 	 * Méthode mise en oeuvre avant la modification de l'objet selon les données RDF. 
386 | 	 */
387 | 	private void javatizePreProcess() {
388 | 		// TODO implémenter selon besoin
389 | 	}
390 | 
391 | 	/** 
392 | 	 * Méthode mise en oeuvre après la modification de l'objet selon les données RDF.
393 | 	 * @param o l'objet ciblé par le postprocessing.
394 | 	 */
395 | 	private void javatizePostProcess(Object o) {
396 | 		// Enregistrement de l'objet
397 | 		BoFactory.getInstance().store(o);
398 | 	}
399 | 	
400 | 	/**
401 | 	 * Recherche les arcs d'après un sujet.
402 | 	 * @param subject la ressource sujet.
403 | 	 * @return un StmtIterator, itérateur des arcs trouvés.
404 | 	 */
405 | 		private StmtIterator getArcsWithSubject (Resource subject) {
406 | 			// Création du sélecteur
407 | 			Selector sel = new SimpleSelector(subject, (Property)null, (RDFNode)null);
408 | 			return model.listStatements(sel);
409 | 		}
410 | 	
411 | 	/**
412 | 	 * Modifie la valeur d'un champ non accessible dans un objet.
413 | 	 * Méthode inspire du projet OJB d'Apache.
414 | 	 * @param champ le champ dont on veut modifier la valeur.
415 | 	 * @param objet l'objet auquel le champ appartient.
416 | 	 * @param value la valeur du champ.
417 | 	 */
418 | 	private void setToNonAccessible(Field champ, Object objet, String value) {
419 | 
420 | 		try {
421 | 			field = champ;
422 | 			// récupération de l'état d'accessibilité du champ
423 | 			boolean before = champ.isAccessible();
424 | 			// On rend le champ accessible
425 | 			PrivilegedAction action = new PrivilegedAction() {
426 | 				public Object run() {
427 | 					getField().setAccessible(true);
428 | 					return null;
429 | 				}
430 | 			};
431 | 			AccessController.doPrivileged(action);
432 | 			// modification
433 | 			champ.set(objet, TypeConverter.convertValue(champ.getType(), value));
434 | 			// on remet l'accessibilité dans son état précédent 
435 | 			champ.setAccessible(before);		
436 | 		} catch (Exception e) {
437 | 			// problème non prévu ?
438 | 			e.printStackTrace();
439 | 		}
440 | 	}
441 | 	
442 | 	/**
443 | 	 * @return Returns the maxlevel.
444 | 	 */
445 | 	public int getMaxLevel() {
446 | 		return maxLevel;
447 | 	}
448 | 
449 | 	/**
450 | 	 * @param maxlevel The maxlevel to set.
451 | 	 */
452 | 	public void setMaxLevel(int maxLevel) {
453 | 		this.maxLevel = maxLevel;
454 | 	}
455 | 	
456 | 	/**
457 | 	 * @return Returns the uri.
458 | 	 */
459 | 	public String getUri() {
460 | 		return uri;
461 | 	}
462 | }

L'importation à la ligne 14 est l'utilitaire d'objets métiers propre à notre application. Ce que nous avons besoin de savoir à son sujet est que cette classe mettra à notre disposition les objets Java que nous souhaitons convertir et se chargera de renvoyer les objets modifiés au serveur métier.

Cette classe possède cinq méthodes publiques :

Pour les développeurs qui souhaiteraient adapter cet exemple à leur convenance, il est bon de noter l'existence des deux méthodes privées void javatizePreProcess() et void javatizePostProcess(java.lang.Object) qui on pour but d'effectuer des traitements avant et après la transformation du modèle Jena/RDF en objets métiers. Ainsi il est possible d'activer et désactiver une connexion à une base de données, par exemple.

L'interface Identifier et son implémentation OjbIdentifierImpl

Tout ce que nous venons d'évoquer n'est possible que parce que nous savons exactement à quel objet accéder.

L'interface Identifier va nous permettre de créer un lien entre les objets modélisés en RDF et notre base de données, grâce à une astuce glissée dans notre représentation RDF.

En effet, notre interface va créer un identifiant pour l'objet, ici depuis OJB. Cet identifiant, composera ensuite l'URI de la ressource RDF représentant l'objet. C'est cette URI que nous nous efforcerons de conserver sur chaque partie du principe client-serveur.

Si vous souhaitez développer votre propre identificateur depuis cette interface, il vous suffira de modifier la ligne 57 de RDFJavatizer.java en conséquence :

57 | identifier = MyIdentifierImpl.getInstance();

L'interface Identifier met en oeuvre deux méthodes :

Vous pouvez télécharger l'interface ici : Identifier.java.

Voici l'implémentation de l'exemple avec OJB :

OjbIdentifierImpl.java
0   | /*
1   | * @author DC
2   |  *
3   |  * Date création : 2 juil. 04
4   |  * Classe : OjbIdentifierImpl
5   |  */
6   | package systemplus.rdf;
7   | 
8   | import org.apache.ojb.broker.Identity;
9   | import org.apache.ojb.broker.core.proxy.ProxyHelper;
10  | import org.apache.ojb.broker.metadata.ClassDescriptor;
11  | 
12  | import systemplus.util.BoFactory;
13  | 
14  | /**
15  |  * @author DC
16  |  *
17  |  * Date création : 2 juil. 04
18  |  * Classe : OjbIdentifierImpl
19  |  */
20  | public class OjbIdentifierImpl implements Identifier{
21  | 
22  | 	// Lien vers BoFactory
23  | 	private static BoFactory boFactory;
24  | 	
25  | 	private OjbIdentifierImpl(){
26  | 		boFactory = BoFactory.getInstance();
27  | 	}
28  | 	
29  | 	// Cette classe est utilitaire, on cherche juste à avoir un singleton
30  | 	 protected static Identifier identifier;
31  | 	 public static Identifier getInstance(){
32  | 	 	// Initialisation
33  | 	 	if (identifier == null)
34  | 	 		identifier = new OjbIdentifierImpl();
35  | 		 return identifier;
36  | 	 }
37  | 	 
38  | 
39  | 	/* (non-Javadoc)
40  | 	 * @see systemplus.rdf.Identifier#getURI(java.lang.Object)
41  | 	 */
42  | 	public String getURI(Object objet) {
43  | 		// Recherche de l'objet à partir de son identifiant dans OJB
44  | 		Identity identity = new Identity(objet,boFactory.getBroker());
45  | 		String uri = identity.toString();
46  | 		uri = uri.replace('{','/');
47  | 		uri = uri.substring(0, uri.length()-1);
48  | 		return uri;
49  | 	}
50  | 	
51  | 	/* (non-Javadoc)
52  | 	 * @see systemplus.rdf.Identifier#getObject(java.lang.String)
53  | 	 */
54  | 	public Object getObject(String uri)  {
55  | 		Class clazz;
56  | 		try {
57  | 			clazz = Class.forName(uri.substring(0, uri.lastIndexOf('/')));
58  | 			ClassDescriptor cld = boFactory.getBroker().getClassDescriptor(clazz);
59  | 			String primaryKey = cld.getPrimaryKey().getAttributeName();
60  | 			String value = uri.substring(uri.indexOf('/')+1);
61  | 			Object o = boFactory.getObject(clazz,primaryKey,value);
62  | 			return ProxyHelper.getRealObject(o);
63  | 		} catch (ClassNotFoundException e) {
64  | 			e.printStackTrace();
65  | 		}
66  | 		
67  | 		
68  | 		return null;
69  | 		
70  | 	}
71  | 	
72  | 	
73  | }
La classe utilitaire TypeConverter

Cette classe a pour but essentiel de faire des conversions à notre convenance depuis les principaux types Java vers une chaîne de caractères et inversement.

Elle met en oeuvre un prototype et une méthode :

Voici son code source :

TypeConverter.java
0   | package systemplus.rdf;
1   | 
2   | import java.util.Collection;
3   | import java.sql.Date;
4   | 
5   | /**
6   |  * Classe utilitaire pour la conversion de types.
7   |  * @author mb
8   |  */
9   | public class TypeConverter {
10  | 	
11  |   /**
12  |    * Retourne la représentation en chaîne de caractères du paramètre.
13  |    *
14  |    * @param val
15  |    * @return String
16  |    */
17  |   public static String convert ( int val ){
18  |   	return String.valueOf(val);
19  |   }
20  |   
21  |   /**
22  |    * Retourne la représentation en chaîne de caractères du paramètre.
23  |    *
24  |    * @param val
25  |    * @return String
26  |    */
27  |   public static String convert ( Integer val ){
28  |   	return val.toString();
29  |   }
30  |   
31  |   /**
32  |    * Retourne la représentation en chaîne de caractères du paramètre.
33  |    *
34  |    * @param val
35  |    * @return String
36  |    */
37  |   public static String convert ( double val ){
38  | 	return String.valueOf(val);
39  |   }
40  |   
41  |   /**
42  |    * Retourne la représentation en chaîne de caractères du paramètre.
43  |    *
44  |    * @param val
45  |    * @return String
46  |    */
47  |   public static String convert ( String val ){
48  |   	return val;
49  |   }
50  |   
51  |   /**
52  |    * Retourne la représentation en chaîne de caractères du paramètre.
53  |    *
54  |    * @param val
55  |    * @return String
56  |    */
57  |   public static String convert ( char val ){
58  |   	return String.valueOf(val);
59  |   }
60  |   
61  |   /**
62  |    * Retourne la représentation en chaîne de caractères du paramètre.
63  |    *
64  |    * @param val
65  |    * @return String
66  |    */
67  |   public static String convert ( boolean val ){
68  |   	return String.valueOf(val);
69  |   }
70  |   
71  |   /**
72  |    * Retourne la représentation en chaîne de caractères du paramètre.
73  |    *
74  |    * @param conteneur
75  |    * @return String
76  |    */
77  |   public static String convert ( Collection conteneur ){
78  |   	return conteneur.toString();
79  |   }
80  |   
81  |   /**
82  |    * Retourne la représentation en chaîne de caractères du paramètre.
83  |    *
84  |    * @param val
85  |    * @return String
86  |    */
87  |   public static String convert ( Boolean val ){
88  |   	return val.toString();	
89  |   }
90  |   
91  |   /**
92  |    * Retourne la représentation en chaîne de caractères du paramètre.
93  |    *
94  |    * @param date
95  |    * @return String
96  |    */
97  |   public static String convert ( Date date ){
98  |   	return date.toString();
99  |   }
100 |   
101 |   /**
102 | 	* Retourne la représentation en chaîne de caractères du paramètre.
103 | 	*
104 | 	* @param objet
105 | 	* @return String
106 | 	*/
107 |    public static String convert ( Object objet ){
108 | 	 return objet.toString();
109 |    }
110 |    
111 | 	/**
112 | 	 * Convertit la valeur sous forme de chaîne de caractères en objet Java.
113 | 	 * @param type le type d'objet retourné.
114 | 	 * @param value la valeur sous forme de String.
115 | 	 * @return l'objet correspondant.
116 | 	 */
117 | 	public static Object convertValue(Class type, String value) {
118 | 		Object newValue = null;
119 | 		if (!type.isPrimitive()) {
120 | 			if (java.lang.Integer.class.isAssignableFrom(type) ) { 
121 | 				newValue = new Integer(value);
122 | 			}
123 | 			if (java.lang.Double.class.isAssignableFrom(type)) { 
124 | 				newValue = new Double(value);
125 | 			}
126 | 			if (java.lang.String.class.isAssignableFrom(type)) {
127 | 				newValue = value;
128 | 			}
129 | 			if (java.util.Date.class.isAssignableFrom(type)) {
130 | 				if (value.matches("[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]")) {
131 | 					newValue = Date.valueOf(value);
132 | 				} else {
133 | 					newValue = (Date)null;
134 | 				}
135 | 			}
136 | 			if (java.lang.Boolean.class.isAssignableFrom(type)) { 
137 | 				newValue = Boolean.valueOf(value);
138 | 			}
139 | 		} else {
140 | 			if (type.getName().compareTo("int") == 0) {
141 | 				newValue = new Integer(value);
142 | 			}
143 | 			if (type.getName().compareTo("double") == 0) {
144 | 				newValue = new Double(value);
145 | 			}
146 | 			if (type.getName().compareTo("boolean") == 0) {
147 | 				newValue = new Boolean(value);
148 | 			}
149 | 		}
150 | 		return newValue;
151 | 	}
152 |   
153 | }

Comme pour la classe RDFModelFactory, les méthodes sont statiques. Les appels se font donc comme ceci :

String val = TypeConverter.convert(monObjet);
Object obj = TypeConverter.convertValue(java.lang.Object.class, "laValeur");
Mise en oeuvre
La servlet : RDFServlet

Les différentes classes que nous venons de voir sont mises en oeuvre au sein de la servlet.

Comme dans toute servlet qui se respecte, nous y implémentons les méthodes doGet et doPost :

Bien que nous n'utiliserons que le POST, nous allons implémenter les deux méthodes. Notons que rien ne nous oblige, malgré tout, à implémenter le doGet, mais puisque l'algorithme est strictement le même, pourquoi s'en priver si ce n'est pour des raisons de sécurité.

RDFServlet.java
0   | /*
1   | * Created on 19 mai 04
2   | */
3   | package systemplus.rdf;
4   | 
5   | import java.io.IOException;
6   | import java.io.PrintWriter;
7   | import java.util.Collection;
8   | 
9   | import javax.servlet.ServletException;
10  | import javax.servlet.http.HttpServlet;
11  | import javax.servlet.http.HttpServletRequest;
12  | import javax.servlet.http.HttpServletResponse;
13  | import javax.servlet.http.HttpSession;
14  | 
15  | import systemplus.account.UserOfAccount;
16  | import systemplus.icp.Project;
17  | import systemplus.struts.AppConstants;
18  | import systemplus.util.BoFactory;
19  | 
20  | /**
21  | * @author MB
22  | */
23  | public class RDFServlet extends HttpServlet {
24  | 
25  | 	/**
26  | 	 * The doGet method of the servlet. <br>
27  | 	 *
28  | 	 * This method is called when a form has its tag value method equals to get.
29  | 	 * 
30  | 	 * @param request the request send by the client to the server
31  | 	 * @param response the response send by the server to the client
32  | 	 * @throws ServletException if an error occurred
33  | 	 * @throws IOException if an error occurred
34  | 	 */
35  | 	public void doGet(HttpServletRequest request, HttpServletResponse response)
36  | 		throws ServletException, IOException {
37  | 		// Paramètres
38  | 		String bufferRDF = request.getParameter("RDFStream");
39  | 		// initialisation du convertisseur
40  | 		RDFJavatizer converter = new RDFJavatizer("http://systemplus.fr");
41  | 		// enregistrement des données
42  | 		if (bufferRDF != null) {
43  | 			converter.javatize(RDFModelFactory.createModel(bufferRDF, "UTF-8"));
44  | 		}	
45  | 		
46  | 		// 1 - détermination du type mime.
47  | 		// application/rdf+xml semble être le type mime officiel.
48  | 		// mais mozilla ne fonctionne bien qu'avec text/rdf !!? 
49  | 		response.setContentType("text/rdf");
50  | 	
51  | 		// 2 - récupération du flux de sortie
52  | 		PrintWriter out = response.getWriter();
53  | 	
54  | 		// 3 - Assignation du niveau d'exploration
55  | 		converter.setMaxLevel(2);
56  | 		// 4 - récupération de la liste des projets appartenant à un utilisateur
57  | 		HttpSession mySession = request.getSession();
58  | 		BoFactory icpUf = (BoFactory)mySession.getAttribute("ICPNetFactory");
59  | 		UserOfAccount myUser = (UserOfAccount)mySession.getAttribute(AppConstants.USER_KEY);
60  | 		//IcpUser icpuser = (IcpUser)mySession.getAttribute("icpuser");
61  | 		Collection c_projets = icpUf.getCollection(Project.class,myUser, "author.accountAccountId", new Integer(myUser.getAccountAccountId()));
62  | 		
63  | 		// 5 - Conversion
64  | 		String rdfOutput = RDFModelFactory.createString(converter.modelize(c_projets));
65  | 
66  | 		// 4 - renvoie du résultat
67  | 		out.println(rdfOutput);
68  | 	}
69  | 
70  | 	/**
71  | 	 * The doPost method of the servlet. <br>
72  | 	 *
73  | 	 * This method is called when a form has its tag value method equals to post.
74  | 	 * 
75  | 	 * @param request the request send by the client to the server
76  | 	 * @param response the response send by the server to the client
77  | 	 * @throws ServletException if an error occurred
78  | 	 * @throws IOException if an error occurred
79  | 	 */
80  | 	public void doPost(
81  | 		HttpServletRequest request,
82  | 		HttpServletResponse response)
83  | 		throws ServletException, IOException {
84  | 		// Paramètres
85  | 		String bufferRDF = request.getParameter("RDFStream");
86  | 		// initialisation du convertisseur
87  | 		RDFJavatizer converter = new RDFJavatizer("http://systemplus.fr");
88  | 		// enregistrement des données
89  | 		if (bufferRDF != null) {
90  | 			converter.javatize(RDFModelFactory.createModel(bufferRDF, "UTF-8"));
91  | 		}	
92  | 		
93  | 		// 1 - détermination du type mime.
94  | 		// application/rdf+xml semble être le type mime officiel.
95  | 		// mais mozilla ne fonctionne bien qu'avec text/rdf !!? 
96  | 		response.setContentType("text/rdf");
97  | 	
98  | 		// 2 - récupération du flux de sortie
99  | 		PrintWriter out = response.getWriter();
100 | 	
101 | 		// 3 - Assignation du niveau d'exploration
102 | 		converter.setMaxLevel(2);
103 | 		
104 | 		// 4 - récupération de la liste des projets appartenant à un utilisateur
105 | 		HttpSession mySession = request.getSession();
106 | 		BoFactory icpUf = (BoFactory)mySession.getAttribute("ICPNetFactory");
107 | 		UserOfAccount myUser = (UserOfAccount)mySession.getAttribute(AppConstants.USER_KEY);
108 | 		//IcpUser icpuser = (IcpUser)mySession.getAttribute("icpuser");
109 | 		Collection c_projets = icpUf.getCollection(Project.class,myUser, "author.accountAccountId", new Integer(myUser.getAccountAccountId()));
110 | 		
111 | 		// 5 - Conversion
112 | 		String rdfOutput = RDFModelFactory.createString(converter.modelize(c_projets));
113 | 
114 | 		// 4 - renvoie du résultat
115 | 		out.println(rdfOutput);
116 | 	}
117 | 
118 | 	/**
119 | 	 * Returns information about the servlet, such as 
120 | 	 * author, version, and copyright. 
121 | 	 *
122 | 	 * @return String information about this servlet
123 | 	 */
124 | 	public String getServletInfo() {
125 | 		return "RDFServlet is a servlet for RDF communication for XUL applications.";
126 | 	}
127 | 
128 | }

Le client XUL

Nous arrivons à la partie cliente de notre application. Celle-ci va être décomposée en une interface en XUL, chargée d'afficher les données envoyées par le serveur et d'une partie traitement (actions et envoie des données au serveur) en JavaScript.

Voici le résultat auquel nous nous proposons d'arriver :

fig 3 : l'interface XUL de la liste des projets
interface XUL
Implémentation
L'interface

Voici l'implémentation proposée :

projectsList.xul
0   | <?xml version="1.0" encoding="UTF-8"?>
1   | 
2   | <!-- Insertion des CSS -->
3   | <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
4   | <?xml-stylesheet href="chrome://appxul/skin/appxul.css" type="text/css"?>
5   | 
6   | <!-- insertion de la DTD -->
7   | <!DOCTYPE window SYSTEM "chrome://appxul/locale/appxul.dtd">
8   | <window
9   |   id="mainWnd"
10  |   title="AppXul"
11  |   orient="horizontal"
12  |   xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
13  |   width="800"
14  |   height="600"
15  |   onload="init('http://monserveur:8080/monappli/servlet/RDFServlet');">
16  | 
17  |   <!-- déclaration des scripts -->
18  |   <script src="chrome://appxul/content/appxul.js" 
19  |           type="application/x-javascript"/>
20  |   <script src="chrome://appxul/content/logoff.js" 
21  |           type="application/x-javascript"/>
22  |   <script src="chrome://appxul/content/projectsList.js" 
23  |           type="application/x-javascript"/>
24  |  
25  |  
26  |   <!-- déclaration des commandes -->
27  |   <commandset>
28  |     <command id="closeCmd" 
29  |              oncommand="window.close();"/>
30  |     <command id="listPrjCmd" 
31  |              oncommand="window.location.replace('chrome://appxul/content/projectsList.xul');"/>
32  |     <command id="refreshCmd" 
33  |              oncommand="reload();"/>
34  |     <command id="homeCmd" 
35  |              oncommand="window.open('chrome://appxul/content/appxul.xul', 'appxul', 'chrome,centerscreen');window.close();"/>
36  |     <command id="installCmd" 
37  |              oncommand="browse('http://monserveur/appxul/install.php');"/>
38  |     <command id="validFormCmd" 
39  |              oncommand="submitDatas('editionBox');"/>
40  |   </commandset>
41  | 
42  |   <!-- Boîte principale -->
43  |   <vbox flex="1" pack="center" class="appxulBox">
44  |     <!-- Menu intra-fenêtre -->
45  |     <grid>
46  |       <columns>
47  |         <column/>
48  |       </columns>
49  |       <rows>
50  |         <row>
51  |          <toolbox flex="1">
52  |           <menubar id="appxulbar" class="appxulBoxContent">
53  |            <menu id="appxulmenu" label="AppXul">
54  |             <menupopup id="appxulbrowse" class="appxulBoxContent">
55  |              <menu id="browse-menu" label="Naviguer">
56  |               <menupopup id="browse-popup" class="appxulBoxContent">
57  |                <menuitem label="Accueil" command="homeCmd"/>
58  |               </menupopup>
59  |              </menu>
60  |              <menuseparator/>
61  |              <menuitem label="Installation" command="installCmd"/>
62  |              <menuseparator/>
63  |              <menuitem label="Fermer" command="closeCmd"/>
64  |             </menupopup>
65  |            </menu>
66  |            <progressmeter id="loadprogress" 
67  |                           mode="determined" 
68  |                           value="100" 
69  |                           flex="1"/>
70  |           </menubar>
71  |          </toolbox>
72  |         </row>
73  |       </rows>
74  |     </grid>
75  | 
76  |     <!-- Affichage -->
77  |     <hbox flex="2" class="appxulBoxContent">
78  |       <groupbox flex="2">
79  |       <caption label="Liste des projets" id="projectsCaption"/>
80  |       <button label="Rafraîchir" 
81  |               command="refreshCmd" 
82  |               class="button" 
83  |               style="width:20px;"/>
84  | 
85  |         <tree flex="3"
86  |               id="projectTree"
87  |               datasources="rdf:null"
88  |               ref="http://systemplus.fr/all-Projects"
89  |               containment="http://systemplus.fr/Project"
90  |               flags="dont-build-content"
91  |               onselect="fillForm('projectTree', event.target.currentIndex);">
92  |            <!-- Définition des colonnes -->
93  |           <treecols>
94  |             <treecol id="projectName" 
95  |                      label="Nom du projet" 
96  |                      flex="1" 
97  |                      primary="true"
98  |                      class="sortDirectionIndicator"
99  |                      sortActive="true" 
100 |                      sortDirection="ascending" 
101 |                      sort="?projectName" />
102 |             <splitter class="tree-splitter"/>
103 |             <treecol id="description" 
104 |                      label="Description" 
105 |                      flex="1" 
106 |                      primary="false" 
107 |                      class="sortDirectionIndicator" 
108 |                      sortActive="false" 
109 |                      sortDirection="ascending" 
110 |                      sort="?description" />
111 |             <splitter class="tree-splitter"/>
112 |             <treecol id="status" 
113 |                      label="Etat" 
114 |                      flex="1" 
115 |                      primary="false" 
116 |                      hidden="true" 
117 |                      class="sortDirectionIndicator" 
118 |                      sortActive="false" 
119 |                      sortDirection="ascending" 
120 |                      sort="?status"/>
121 |             <splitter class="tree-splitter"/>
122 |             <treecol id="statusDescription" 
123 |                      label="Description de l'état" 
124 |                      flex="1" 
125 |                      primary="false" 
126 |                      hidden="true" 
127 |                      class="sortDirectionIndicator" 
128 |                      sortActive="false" 
129 |                      sortDirection="ascending" 
130 |                      sort="?statusDescription"/>
131 |             <splitter class="tree-splitter"/>
132 |             <treecol id="authorFirstName" 
133 |                      label="Auteur" 
134 |                      flex="1" 
135 |                      primary="false"
136 |                      hidden="true"
137 |                      class="sortDirectionIndicator" 
138 |                      sortActive="false" 
139 |                      sortDirection="ascending" 
140 |                      sort="?authorFirstName"/>
141 |             <splitter class="tree-splitter"/>
142 |             <treecol id="creationDate" 
143 |                      label="Date de création" 
144 |                      flex="1" 
145 |                      primary="false" 
146 |                      hidden="true"
147 |                      class="sortDirectionIndicator" 
148 |                      sortActive="false" 
149 |                      sortDirection="ascending" 
150 |                      sort="?creationDate" />
151 |             <splitter class="tree-splitter"/>
152 |             <treecol id="derUserFirstName" 
153 |                      label="Dernier utilisateur" 
154 |                      flex="1" 
155 |                      primary="false" 
156 |                      hidden="true"
157 |                      class="sortDirectionIndicator" 
158 |                      sortActive="false" 
159 |                      sortDirection="ascending" 
160 |                      sort="?derUserFirstName" />
161 |             <splitter class="tree-splitter"/>
162 |             <treecol id="dateLastOpened" 
163 |                      label="Date de dernière ouverture" 
164 |                      flex="1" 
165 |                      primary="false" 
166 |                      hidden="true"
167 |                      class="sortDirectionIndicator" 
168 |                      sortActive="false" 
169 |                      sortDirection="ascending" 
170 |                      sort="?dateLastOpened" />
171 |             <splitter class="tree-splitter"/>
172 |             <treecol id="about" 
173 |                      label="About" 
174 |                      flex="1" 
175 |                      primary="false" 
176 |                      hidden="true"
177 |                      class="sortDirectionIndicator" 
178 |                      sortActive="false" 
179 |                      sortDirection="ascending" 
180 |                      sort="?dateLastOpened" />
181 |           </treecols>
182 | 
183 | 
184 | 
185 |           <!-- Les résultats -->
186 | 
187 |          <!-- Template -->
188 |           <template>
189 |            <rule>
190 | 
191 | <!-- CONDITIONS -->
192 |              <conditions>
193 |               <content uri="?uri" />
194 |               <member container="?uri" child="?project" />
195 |               <triple subject="?project"
196 |                       predicate="http://systemplus.fr/systemplus.icp.Project#author"
197 |                       object="?author" />
198 |               <triple subject="?project"
199 |                       predicate="http://systemplus.fr/systemplus.icp.Project#derUser"
200 |                       object="?derUser" />
201 |             </conditions>
202 | <!-- BINDINGS -->
203 |             <bindings>
204 |               <binding subject="?project"
205 |                       predicate="http://systemplus.fr/systemplus.icp.Project#projectName"
206 |                       object="?projectName" />
207 |               <binding subject="?project"
208 |                       predicate="http://systemplus.fr/systemplus.icp.Project#description"
209 |                       object="?description" />
210 |               <binding subject="?project"
211 |                       predicate="http://systemplus.fr/systemplus.icp.Project#status"
212 |                       object="?status" />
213 |               <binding subject="?project"
214 |                       predicate="http://systemplus.fr/systemplus.icp.Project#statusDescription"
215 |                       object="?statusDescription" />
216 |               <binding subject="?project"
217 |                       predicate="http://systemplus.fr/systemplus.icp.Project#creationDate"
218 |                       object="?creationDate" />
219 |               <binding subject="?project"
220 |                       predicate="http://systemplus.fr/systemplus.icp.Project#dateLastOpened"
221 |                       object="?dateLastOpened" />
222 |               <binding subject="?author"
223 |                       predicate="http://systemplus.fr/systemplus.bo.UserOfAccount#firstName"
224 |                       object="?authorFirstName" />
225 |               <binding subject="?derUser"
226 |                       predicate="http://systemplus.fr/systemplus.bo.UserOfAccount#firstName"
227 |                       object="?derUserFirstName" />
228 |             </bindings>
229 | 
230 | <!-- ACTION -->
231 |             <action>
232 |              <treechildren>
233 |               <treeitem uri="?project">
234 |                <treerow>
235 |                 <treecell label="?projectName"/>
236 |                 <treecell label="?description"/>
237 |                 <treecell label="?status"/>
238 |                 <treecell label="?statusDescription"/>
239 |                 <treecell label="?authorFirstName"/>
240 |                 <treecell label="?creationDate"/>
241 |                 <treecell label="?derUserFirstName"/>
242 |                 <treecell label="?dateLastOpened"/>
243 |                 <treecell label="?project"/>
244 |                </treerow>
245 |               </treeitem>
246 |              </treechildren>
247 |             </action>
248 | 
249 |           </rule>
250 |           </template>
251 | 
252 |  
253 |         </tree>
254 | 
255 |        </groupbox>
256 | 
257 |   <splitter collapse="after"/>
258 | 
259 | <!-- EDITION PROJET -->
260 | 
261 |   <groupbox flex="1" id="editionBox">
262 |       <caption label="Edition d'un projet" id="editionCaption"/>
263 | 
264 |   <groupbox flex="1">
265 |    <vbox align="center" flex="1" valign="top">
266 |      <hbox align="left">
267 |       <label style="width:10em" value="Nom du projet :" />
268 |       <textbox id="projectNameForm" size="20" flex="1" />
269 |      </hbox>
270 | 
271 |      <hbox align="left">
272 |       <label style="width:10em" value="Auteur :" />
273 |       <textbox id="authorFirstNameForm" size="20" flex="1" readonly="true" />
274 |      </hbox>
275 | 
276 |      <hbox align="left">
277 |       <label style="width:10em" value="Date de creation :" />
278 |       <textbox id="creationDateForm" size="20" flex="1" />
279 |      </hbox>
280 | 
281 |      <hbox align="left">
282 |       <label style="width:10em" value="rdf:about :" />
283 |       <textbox id="aboutForm" size="50" flex="1" readonly="true" />
284 |      </hbox>
285 |    </vbox>
286 |    <vbox align="center" flex="1" valign="top">
287 |      <hbox align="left" flex="1">
288 |       <label style="width:10em" value="Description du projet :" />
289 |       <textbox id="descriptionForm" multiline="true" rows="5" cols="30" flex="1" />
290 |      </hbox>
291 |    </vbox>
292 |   </groupbox>
293 |   <groupbox flex="1">
294 |    <vbox align="center" flex="1" valign="top">
295 |      <hbox align="left">
296 |       <label style="width:10em" value="Etat :" />
297 |       <textbox id="statusForm" size="20" flex="1" />
298 |      </hbox>
299 | 
300 |      <hbox align="left">
301 |       <label style="width:10em" value="Description de l'etat :" />
302 |       <textbox id="statusDescriptionForm" multiline="true" rows="3"  cols="30" flex="1" />
303 |      </hbox>
304 | 
305 |    </vbox>
306 |    <vbox align="center" flex="1" valign="top">
307 |      <hbox align="left">
308 |       <label style="width:10em" value="Dernier utilisateur :" />
309 |       <textbox id="derUserFirstNameForm" size="20" flex="1" readonly="true" />
310 |      </hbox>
311 | 
312 |      <hbox align="left">
313 |       <label style="width:10em" value="Date de derniere ouverture :" />
314 |       <textbox id="dateLastOpenedForm" maxlength="20"  size="20" flex="1" />
315 |      </hbox>
316 | 
317 |    </vbox>
318 |   </groupbox>
319 |    <hbox flex="1">
320 |     <!--<spacer flex="1" />
321 |     <hbox flex="2">-->
322 |      <button label="Valider" command="validFormCmd" flex="2" class="button"/>
323 |    <!-- </hbox>
324 |     <spacer flex="1" />-->
325 |    </hbox>
326 | 
327 |   </groupbox>
328 | 
329 |      </hbox>
330 | 
331 |   </vbox>
332 | 
333 | <!-- fin de la fenêtre -->
334 | </window>

Nous supposerons que vous maîtrisez déjà les bases de la création d'interface en XUL afin de focaliser notre attention sur les templates.

Ces derniers vont nous permettre de manipuler les données d'une datasource pour un affichage optimisé.

La datasource est la représentation en mémoire côté Mozilla du document RDF transmis par le serveur.

Le document RDF va représenter des données sur x niveaux de profondeur. Sélectionner telle ou telle donnée peut se relever être un véritable défi sans l'utilisation des templates.

Un template est composé en trois parties :

  1. des conditions qui vont nous permettre de faire les sélections sur les conteneurs qui nous intéressent ;
  2. des bindings afin de de manipuler les données par des alias ;
  3. des actions pour l'affichage des données dans les colonnes de notre arbre.
Conditions

Les conditions peuvent être composées de trois éléments différents :

Important !
Il est à noter que les templates sont automatiquement récursifs. Ainsi toute condition ne sera interprétée que si c'est possible, c'est à dire si le sujet et le prédicat existent au niveau d'exploration en cours.

Bindings

Les Bindings ou liaisons vont nous permettre de spécifier un alias pour chaque donnée à manipuler dans les actions.

Ils ont une forme semblable aux triple des conditions, c'est à dire sujet, prédicat, objet, mais ne seront accessibles qu'au niveau d'exploration courant, contrairement aux triple qui le seront pour tous les niveaux inférieurs.

Actions

L'ensemble des éléments se trouvant dans la balise <action> sera dupliqué pour chaque occurence correspondant au content des conditions.

Les alias seront interprétés avec les valeurs courantes.

Nous y placerons donc le squelette d'une ligne d'arbre (lignes 232 à 246).

Scripts et communication avec la servlet

Le langage XUL n'est qu'un langage de description d'interfaces et ne permet pas les traitements. Pour palier à ce manque, nous avons Javascript et les composants XPCOM de Mozilla.

Grâce à eux, nous pourrons gérer les événements et les communications avec la servlet.

Les événements que nous aurons à gérer sont :

Les éventuelles actions d'un menu seront gérées dans un autre script que nous ne verrons pas ici.

Les composants XPCOM sont, pour simplifier, des librairies propres à Mozilla réutilisable avec Javascript dans l'interface XUL grâce à des définitions IDL (Interface Definition Language). Ils permettent d'accéder à des fonctionnalités propres à Mozilla comme le registre ou les préférences utilisateurs, mais permettent également l'utilisation de librairies spécifiques comme RDFlib.

Une utilisation typique de ces derniers est la suivante :

var RDF = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService();
RDF = RDF.QueryInterface(Components.interfaces.nsIRDFService);

On va d'abord chercher le service du composant, puis, si besoin, son interface. Il nous suffit alors d'appeler les méthodes de cette interface comme vous pourrez le constater dans le code source suivant :

projectsList.js
0   | /**
1   |  * @author System Plus <mbraux@systemplus.fr>
2   |  * @date may 2004
3   |  * @version 0.0.0.1
4   |  */
5   |  
6   | /**
7   |  * le servlet de retour
8   |  */
9   | var servlet = "http://monserveur:8080/icmnet/servlet/RDFServlet";
10  | 
11  | /**
12  |  * La source de données distante.
13  |  */
14  | var rDatasource = null;
15  | var remote = null;
16  | 
17  | /**
18  |  * La source de données sur laquelle nous travaillerons.
19  |  */
20  | var inMemDS = null;
21  | 
22  | /** 
23  |  * Création d'une requête HTTP.
24  |  */
25  | var req = new XMLHttpRequest();
26  | 
27  | /**
28  |  * Initialisation des données depuis le fichier RDF.
29  |  * @param RDFFile le fichier RDF.
30  |  */
31  | function init (urlRDF) {
32  |    var conv = null;
33  |    var rdf = null;
34  |    var file = null;
35  |    var url = null;
36  |    var Cc = Components.classes;
37  |    var Ci = Components.interfaces;
38  |   
39  |    // mise en route des indicateurs de chargement
40  | 			beginLoading();
41  |   
42  |    // extraction des services
43  |    rdf = Cc["@mozilla.org/rdf/rdf-service;1"];
44  |    rdf = rdf.getService(Ci.nsIRDFService);
45  |   
46  |    // récupération de la source de données distante
47  |    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
48  |    rDatasource = rdf.GetDataSource(urlRDF);
49  |    remote = rDatasource
50  |      .QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
51  |    
52  |    // on assigne la source de données à l'élément XUL qui l'affiche.
53  |    // L'id est celui du conteneur ex:<tree> (pour un arbre, évidemment)
54  |    var tree = document.getElementById('projectTree');
55  |    tree.database.AddDataSource(remote);
56  |   
57  |    // Arrêt des indicateurs de chargement
58  | 			endLoading();
59  | }
60  | 
61  | 
62  | 
63  | /**
64  |  * Recharge les données RDF.
65  |  * init() doit d'abord avoir été appelé.
66  |  */
67  | function reload() {
68  |   
69  |   // mise en route des indicateurs de chargement
70  | 		beginLoading();
71  |   
72  |   // récupération de l'élément à modifier
73  |   var tree = document.getElementById('projectTree');
74  |   
75  |   remote.Refresh(true);
76  |   
77  |   // reconstruction de l'arbre
78  |   // cette manipulation n'est pas obligatoire, selon les newsgroup et la FAQ,
79  |   // mais bizarrement l'appli fonctionne mieux avec...
80  |   tree.builder.rebuild();
81  |   
82  |   // Arrêt des indicateurs de chargement
83  | 		endLoading();
84  | }
85  | 
86  | function beginLoading() {
87  |   document.getElementById( "loadprogress" )
88  |     .setAttribute("mode","undetermined");  
89  |   document.getElementById( "projectsCaption" )
90  |     .setAttribute("label","Loading...");
91  | }
92  | 
93  | function endLoading() {
94  | 		  document.getElementById( "loadprogress" )
95  |     .setAttribute("mode","determined");  
96  |   document.getElementById( "projectsCaption" )
97  |     .setAttribute("label","Liste des projets");
98  | }
99  | 
100 | ////////////////////////////////////////////////////////////////
101 | // fonctions de mise à jour du formulaire d'édition
102 | ////////////////////////////////////////////////////////////////
103 | 
104 | /**
105 |  * Compte le nombre de colonnes d'un arbre XUL.
106 |  * @param tree le noeud DOM de l'arbre.
107 |  * @return colCount le nombre de colonnes.
108 |  */ 
109 | function getColCount(tree) {
110 |   var colCount = 0;
111 |   // le noeud a des fils
112 |   if (tree.hasChildNodes()) {
113 |     // on les récupère
114 |     children = tree.childNodes;
115 |     // initialisation des variables de parcours
116 |     treecolsFound = false;
117 |     var i = 0;
118 |     // parcours des fils
119 |     while ((i < children.length) && (!treecolsFound)) {
120 |       // on trouve le conteneur des colonnes
121 |       if ((children.item(i).nodeName == "treecols") 
122 | 	  && 
123 | 	  (children.item(i).hasChildNodes())) 
124 | 	{
125 | 	  // on récupère ses fils
126 | 	  childrenChildren = children.item(i).childNodes;
127 | 	  // on se méfie des noeuds pièges donc on ne retourne pas 
128 | 	  // childrenChildren.length
129 | 	  // on parcours donc le treecols, ce qui en terme de complexité est 
130 | 	  // négligeable : O(n) en plus.
131 | 	  for (j = 0 ; j < childrenChildren.length ; j++) {
132 | 	    // on incrémente en présence d'un fils treecol
133 | 	    if (childrenChildren.item(j).nodeName == "treecol") {
134 | 	      colCount++;
135 | 	    }
136 | 	  }
137 | 	  // on trouvé le conteneur de colonnes, pas besoin d'aller plus loin
138 | 	  treecolsFound = true;
139 | 	}
140 |       // on passe au suivant
141 |       i++;
142 |     }
143 |   }
144 |   return colCount;
145 | }
146 | 
147 | /**
148 |  * Remplit un formulaire depuis une ligne d'un arbre XUL.
149 |  * Attention : 
150 |  * la convention de nommage veut que pour chaque colonne de l'arbre, le champ 
151 |  * du formulaire ait le même identifiant suffixé de "Form".
152 |  * @param treeId l'identifiant de l'arbre.
153 |  * @param row l'index de la ligne.
154 |  */
155 | function fillForm(treeId, row) {
156 |   try {
157 |     if (row >= 0) {
158 |       // récupération du noeud DOM de l'arbre
159 |       var tree = document.getElementById(treeId);
160 |       // comptage du nombre de colonnes de l'arbre
161 |       var colCount = getColCount(tree);
162 |       // parcours des colonnes
163 |       for (var col = 0 ; col < colCount ; col++) {
164 | 	// récupération de l'identifiant de la colonne
165 | 	var colId = tree.treeBoxObject.getColumnID(col);
166 | 	// récupération du contenu de la cellule
167 | 	var content = tree.view.getCellText(row, colId);
168 | 	// récupération du noeud DOM du champ de formulaire
169 | 	// la convention de nommage prend tout son sens ici !
170 | 	document.getElementById(colId+"Form").value = content;
171 |       }
172 |     }
173 |   } catch (e) {
174 |     // exception, peut survenir su le formulaire est mal créé
175 |     // c'est à dire :
176 |     // - convention de nommage non respectée
177 |     // - champ manquant
178 |     // ou si l'arbre n'existe pas !?!
179 |     // ou si le nombre de colonnes n'est pas le bon.
180 |     alert("Exception : "+e);
181 |   }
182 | }
183 | 
184 | /////////////////////////////////////////////////////
185 | // fonctions d'enregistrement des données
186 | /////////////////////////////////////////////////////
187 | 
188 | /**
189 |  * Soumet les données collectées par un formulaire au serveur et met l'arbre à 
190 |  * jour.
191 |  * @param formId l'identifiant du formulaire à soumettre.
192 |  */
193 | function submitDatas(formId) {
194 | 		beginLoading();
195 |   // collecte des données
196 |   var champs = collectDatas(formId);
197 |   // Conversion en RDF
198 |   initInMemDS(champs);
199 |   // Mise à jour sur le serveur
200 |   updateServer(servlet);
201 |   // rafraîchissement de l'arbre
202 |   reload();
203 |   
204 |   endLoading();
205 | }
206 | 
207 | /**
208 |  * Collecte les données d'un formulaire.
209 |  * @param formId l'identifiant du formulaire.
210 |  * @return champs le tableau associatif des données où l'indice représente le 
211 |  *  nom du champ.
212 |  */
213 | function collectDatas(formId) {
214 |   // création du tableau des données
215 |   var champs = new Array();
216 |   // récupération du noeud du formulaire
217 |   var editionBox = document.getElementById(formId);
218 |   // parcours du noeud récupéré pour recherche des données
219 |   champs = parseChamps(editionBox, champs);
220 |   
221 |   return champs;
222 | }
223 | 
224 | /**
225 |  * Parcours un noeud DOM à la recherche des attributs id et value de ce dernier 
226 |  * et de ses fils.
227 |  * @param node le noeud à parcourir.
228 |  * @param champs le tableau associatif à compléter (données id et value).
229 |  * @return champs le tableau complété.
230 |  */
231 | function parseChamps (node, champs) {
232 |   // on teste la validité du champ
233 |   if ((node != null)&&(node.nodeType == 1)) {
234 |     // le noeud a-t'il des attributs ?
235 |     if ( node.hasAttributes()) {
236 |       // oui, on les récupère
237 |       var atts = node.attributes;
238 |       // initialisation des variables de parcours/recherche
239 |       var i = 0;
240 |       var found = false;
241 |       var name = "";
242 |       var value = "";
243 |       // on commence le parcours des attributs
244 |       while ((i < atts.length) && (!found)) {
245 | 	// récupération de l'attribut courant
246 | 	att = atts.item(i);
247 | 	// on a un noeud "id"
248 | 	if (att.nodeName == "id") {
249 | 	  name = att.nodeValue;
250 | 	  // récupération de la valeur du champ.
251 | 	  // il est préférable d'utiliser cette méthode car att.nodeValue ne 
252 | 	  // prend pas en compte les modifications faites par l'utilisateur.
253 | 	  value = document.getElementById(name).value;
254 | 	  // mise à jour du tableau
255 | 	  if (value != null) {
256 | 	    champs[name] = value;
257 | 	  }
258 | 	  // fini
259 | 	  found = true;
260 | 	}
261 | 	// on passe au suivant
262 | 	i++;
263 |       }
264 |     }
265 |     // le noeud a-t'il des fils ?
266 |     if (node.hasChildNodes()) {
267 |       // oui, on les récupère...
268 |       var children = node.childNodes;
269 |       // ... puis on les parcours récursivement...
270 |       for (i = 0 ; i < children.length ; i++) {
271 | 								// ... en mettant à jour le tableau.
272 | 								champs = parseChamps(children.item(i), champs);
273 |       }
274 |     }
275 |   }
276 |   // les résultats sont prêts
277 |   return champs;
278 | }
279 | 
280 | /**
281 |  * Crée une source de données en mémoire depuis un tableau associatif.
282 |  * @param champs le tableau associatif des données d'un formulaire.
283 |  */
284 | function initInMemDS(champs) {
285 |   // création du service RDF
286 |   var RDF = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService();
287 |   RDF = RDF.QueryInterface(Components.interfaces.nsIRDFService);
288 |   
289 |   // création de la source de données
290 |   inMemDS = Components.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].createInstance();
291 |   inMemDS = inMemDS.QueryInterface(Components.interfaces.nsIRDFDataSource);
292 |   
293 |   // récupération de l'URI root de la ressource (rdf:about).
294 |   var about = champs["aboutForm"];
295 |   var last = about.lastIndexOf("/");
296 |   var uri = about.substring(0, last)+"#";
297 |   var message = null;
298 |   
299 |   // parcours des champs
300 |   for (champ in champs) {
301 |     // si ce n'est pas le champ utilisé précédemment
302 |     if (champ != "aboutForm") {
303 |       // récupération du nom de la propriété
304 |       last = champ.lastIndexOf("Form");
305 |       var propertyName = champ.substring(0, last);
306 |       // ajout à la datasource
307 |       message += uri+propertyName+" = "+champs[champ]+"\n";
308 |       addInMemDSResource(about, uri+propertyName, champs[champ]);
309 |     }
310 |   }
311 | }
312 | 
313 | 
314 | /**
315 |  * Met à jour les données RDF sur le serveur depuis la datasource globale.
316 |  * @param serveur l'URL du serveur.
317 |  */
318 | function updateServer(serveur) {
319 |   // récupération de la représentation RDF/XML de inMemDS et encodage
320 |   content = encodeURI(serializeDataSource(inMemDS));
321 |   // mise forme pour la requête
322 |   var param = 'RDFStream='+content;
323 |   
324 |   // Activation des privilèges UniversalBrowserRead 
325 |   netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
326 |   
327 |   // initialisation de la fin de requête
328 |   req.COMPLETED = 4;
329 |   
330 |   // envoie de la requête par la méthode POST
331 |   req.open("POST", serveur, false); // false == asynchrone
332 |   
333 |   // important : on initialise le type mime à application/x-www-form-urlencoded
334 |   req.setRequestHeader("content-type","application/x-www-form-urlencoded"); 
335 |   
336 |   // envoie de la requête
337 |   req.send(param);
338 |   
339 |   // suivi du déroulement de la requête
340 |   req.onload = requestProgress();
341 | }
342 | 
343 | /**
344 |  * Progression de la requête.
345 |  */
346 | function requestProgress() { 
347 |   // On s'assure de recevoir une réponse valide du serveur
348 |   switch(req.readyState) {
349 |   case req.COMPLETED :
350 |     // requête complétée
351 |     if(req.status != 200) {
352 |       // serveur pas OK
353 |       alert('The server respond with a bad status code : '+req.status);
354 |     } else {
355 |       // la réponse est valide et le serveur OK
356 |       result = req.responseText;
357 |     }
358 |     break;
359 |   default :
360 |     alert('Bad Ready State : '+req.status);
361 |   }
362 | }
363 | 
364 | /**
365 |  * Convertit un tableau en chaîne humainement lisible dans un but de déboguage.
366 |  * @param array le tableau à convertir.
367 |  * @return contenu la chaîne résultat.
368 |  */
369 | function printArray( array ) {
370 |   var contenu = "";
371 |   for (indice in array) {
372 |     contenu += "["+indice+"] -> "+array[indice]+"\n";
373 |   }
374 |   return contenu;
375 | }
376 | 
377 | /**
378 |  * Ajoute un triplet à la source de données en mémoire.
379 |  * @param subjURI l'URI du sujet.
380 |  * @param predURI l'URI du prédicat.
381 |  * @param target la cible (litéral).
382 |  */
383 | function addInMemDSResource (subjURI, predURI, target) {
384 |   // création du service RDF
385 |   var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
386 |     .getService(Components.interfaces.nsIRDFService);
387 |   var RDFService = rdf.QueryInterface(Components.interfaces.nsIRDFService);
388 |   
389 |   // création de la ressource sujet
390 |   var subj = RDFService.GetResource(subjURI);
391 |   
392 |   // création de la ressource prédicat
393 |   var pred = RDFService.GetResource(predURI);
394 |   
395 |   // création de la cible
396 |   var newValue = RDFService.GetLiteral(target);
397 |   
398 |   // ajout du triplet
399 |   inMemDS.Assert(subj, pred, newValue, true);
400 | }
401 | 
402 | /**
403 |  * Sérialise une source de données en RDF/XML.
404 |  * @param ds la source de données.
405 |  * @return outputstream.content la chaîne RDF/XML.
406 |  */
407 | function serializeDataSource (ds) {
408 |   // activation des privilèges
409 |   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
410 |   
411 |   // création du sérialiseur
412 |   var serializer= Components.classes["@mozilla.org/rdf/xml-serializer;1"].getService(Components.interfaces.nsIRDFXMLSerializer);
413 |   serializer.QueryInterface(Components.interfaces.nsIRDFXMLSource);
414 |   
415 |   // gestion du flux sur lequel sera redirigé le contenu RDF/XML
416 |   var outputstream = {
417 |     content:"",
418 |     write:function(s, count) {
419 |       this.content+=s;
420 |       return count;
421 |     },
422 |     flush:function(){},
423 |     close:function(){}
424 |   };
425 |   
426 |   // sérialisation
427 |   serializer.init(ds);
428 |   serializer.Serialize(outputstream);
429 |   
430 |   // le contenu est prêt
431 |   return outputstream.content;
432 | }

Jusqu'à la ligne 98, nous déclarons quatre fonctions pour l'initialisation de l'arbre, son rafraîchissement et l'activation/désactivation des indicateurs de chargement. Je vous laisse découvrir les commentaires du code source pour découvrir leur fonctionnement.

Nous avons ensuite les fonctions de mise à jour du formulaire d'édition et d'envoi des données. En voici l'algorithme général :

Là aussi, je vous laisse découvrir les détails avec les commentaires du code source.

Conclusion

Il est évident que tout ce que nous avons vu précédemment néglige soigneusement de reprendre les bases de XUL, RDF, Java, l'architecture de Mozilla, Javascript, etc. Cependant vous trouverez en annexes des références qui m'ont servies pour la réalisation de cette application ou qui comportent des éléments pour la prise en main de ces différentes notions. Je vous conseille d'aller y faire un tour.


Annexes
Terminologie
HTML
HyperText Markup Language. Le langage utilisé pour écrire les documents Internet. C'est avant tout un langage de présentation.
XML
Extensible Markup Language. À la base, XML est un langage de formatage de documents recommandé par le W3C, consortium chargé de développer les protocoles de l'Internet. Il est incontournable dans l'étude des technologies du Web. Pour plus d'informations, voici la page dédiée à ce langage sur le site du W3C.
XPFE
XPFE est un outil utilisé dans le développement de Mozilla. Il a été conçu pour permettre la portabilité d'interfaces utilisateurs sur un grand nombre de plateformes (Mac, Linux, Windows®...)
Références
Mozdev. [en ligne]. Mozdev.org. Disponible sur http://mozdev.org.
Mozilla. [en ligne]. Mozilla.org. Disponible sur http://mozilla.org.
XUL Programmer's reference. [en ligne]. Oeschger Ian. 04 mai 2001. Disponible sur http://www.mozilla.org/xpfe/xulref/.
Manuel de référence du programmeur XUL. [en ligne]. Oeschger Ian, Camus Matthieu (traduction), 03 juillet 2000 (version française). Disponible sur http://www.mozilla.org/xpfe/xulref-french.
XulPlanet. [en ligne]. XulPlanet.com. 2004. Disponible sur http://xulplanet.com.
McFarlane Nigel. Rapid Application Developpement with Mozilla. Prenctice Hall, 2004. 770 p. (Bruce Peren's Open Source Series).
Boswell David, King Brian, Oeschger Ian, Collins Pete, Murphy Eric. Creating Applications with Mozilla. O'Reilly, Septembre 2002. 474 p.
Lejeune Grégoire. Histoire et architecture de Mozilla. Linux Magazine France N°56. Diamond Editions. Décembre 2003. p.16-19.
Lejeune Grégoire. Étendre l'interface de Mozilla avec XPFE. Linux Magazine France N°56. Diamond Editions. Décembre 2003. p.20-29.
Lejeune Grégoire. Créer des composants XPCOM pour Mozilla. Linux Magazine France N°56. Diamond Editions. Décembre 2003. p.30-43.
Servlet Overview [en ligne]. Marty Hall. 1999. Disponible sur http://www.apl.jhu.edu/~hall/java/Servlet-Tutorial/.
Mozilla : Javascript [en ligne]. Mozilla.org. 2003. Disponible sur http://www.mozilla.org/js/.
Site officiel de Java. Sun Microsystems, Inc. Disponible sur http://java.sun.com/.
Recommandations RDF-Schema. W3C. Disponible sur http://www.w3.org/TR/rdf-schema/