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.
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 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.
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 :
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.
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.
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 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.
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.
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/)
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 :
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 :
La création de la chaîne représentant le document se fait plus simplement :
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.
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.
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.
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 :
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 :
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 | }
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 :
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 :
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é.
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 | }
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 :
Voici l'implémentation proposée :
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 :
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.
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.
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).
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 :
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.
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.