Document mis à jour le 28 mai 2001. Je suis ouvert à n'importe quelle suggestion qui pourrait aider à améliorer ces matériaux. N'hésitez pas d'écrire à l'auteur, Jonathan Revusky.
This document in EnglishRendu ici, on suppose que vous avez déjà fait fonctionner l'exemple, comme on explique ici. Une fois que vous avez franchi ces démarches mécaniques, vous voudrez peut-être un peu d'aide afin de mieux comprendre les éléments qui composent cet exemple.
Si cet exemple représente un grand saut relatif à l'exemple antérieur, c'est parce que ce servlet garde et récupère les données de l'application d'un répositoire persistant. Tout de même vous resterez peut-être étonné en voyant que cet exemple ne requiert pas beaucoup plus de code que l'exemple antérieur. Cela est dû à l'usage des API's que Niggle fournit pour la persistance de données. Cela nous permet de garder les "détails grossiers" bien encapsulés dans des fichiers externes en XML.
Les API's d'Oreo de données persistantes qu'on introduit se trouvent dans le
package com.revusky.oreo
. Il y a 2 abstractions importantes dont on
se sert ici: com.revusky.oreo.MutableDataSource
et com.revusky.oreo.Record
.
Bien qu'il existe de diverses terminologies enracinées dans l'informatique et la théorie des bases de données, on espère que la plupart des programmeurs se sentent confortables avec la terminologie qu'on a adoptée ici: un mutable data source, c'est un objet qui nous permet de garder, recupérer, et modifier des records -- ou bien, des enregistrements en français. Un record, c'est la même chose conceptuellement qu'une ligne dans une table d'une base de données relationelle. Parfois on l'appelle aussi un tuple ou bien entity.
Tout comme une ligne d'une table dans une base de données, un enregistrement dans l'API Oreo représente un ensemble de paires clé/valeur régi par un schème de metadonnées -- c'est à dire une description des noms des clés et les restrictions sur les valeurs associées. C'est ce qu'on veut dire quand on fait mention des champs d'un enregistrement. Tous les enregistrements d'un type donné ont les mêmes metadonnées -- c'est à dire, la même disposition de champs. D'ailleurs, on suppose que, pour chaque type d'enregistrement, exactement un de ses champs est associé à la clé primaire de l'enregistrement. La combination du type d'enregistrement et la valeur de sa clé primaire devrait l'identifier sans ambiguité pour le garder/récupérer dans un objet de type MutableDataSource.
Si vous ne l'avez pas encore fait, regardez le fichier recorddefs.xml. L'effet que ça vous fait dépendra de votre expérience antérieure avec le XML. Il n'est pas besoin d'être intimidé. On se sert de l'XML comme format de fichier de configuration lisible par des humains. Bref, chaque fichier XML, c'est un hiérarchie d'éléments avec une seule racine. L'élément racine en ce cas ci, c'est RECORDDEFS, qui contient un ou plus d'un élément du type RECORD. Etant donné que c'est un petit exemple, on ne définit qu'un seul type d'enregistrement, appelé rolodex_entry. La clé primaire, indiquée par PRIMARY_KEY, est le champ unique_id. Ce champ, le premier défini dans l'élément RECORD, est défini comme champ numérique, du type INTEGER, qui doit être non-négatif, vu que le MINIMUM, c'est zéro. Les deux prochains champs, first_name et last_name sont des chaînes. Remarquez que, à part le fait d'être REQUIRED (obligatoires), ils ont aussi quelques directifs NORMALIZE associés. Ça peut être très utile spécifier ces choses, parce que ça vous permet de "nettoyer" au moment de lire des valeurs d'une interface d'usager. Par exemple, vous ne voudrez pas vraiment traiter la chaîne "Smith " comme quelque chose de différent de "Smith" tout juste ou même "SMITH". Les deux directifs de NORMALIZE ici nous permettent de dire qu'on voudrait traiter toutes les chaînes ci-dessus comme étant la même, une disposition bien évident du point de vue humain (mais pas si evident du point de vue ordinateur.)
Le prochain champ, email, est obligatoire aussi, comme on voit. On le spécifie comme type "email". C'est une astuce que la librairie fournit pour la verification automatique qu'un email est bien formé. Les prochains champs son optionnels.
Bon, je crois que ça nous dit tout ce qu'il faut savoir pour le moment au sujet des définitions des enregistrements. Maintenant, on passe à la configuration des sources de données.
Si vous regardez le fichier datasources.xml, vous verrez que c'est assez bref aussi. On définit une seule DATASOURCE et il n'y a que deux proprietés de configuration en ce cas-ci: STORE et COMPACT_FREQUENCY. La proprieté STORE spécifie tout simplement un fichier où on devrait garder les données. Evidemment, ça devrait être un endroit où votre servlet a le droit de lire/écrire. COMPACT_FREQUENCY est un paramètre de nature plutôt technique. C'est optionnel, puisque si on ne le spécifie pas, le système utilisera une valeur par défaut. Ce paramètre fixe la fréquence avec laquelle votre fichier devrait être écrit à nouveau depuis zéro. Avec une valeur de 100 spécifiée, ça veut dire que 99 de 100 écritures sur disque ne seront que des écritures qui ajoutent à la fin du fichier. Et la centième fois, ça écrira le fichier à nouveau d'une forme plus compacte.
Voici quelque chose qu'il vaut la peine de se rappeler lorsqu'on passe au code java. Dans ces deux fichiers de configuration en XML, on a défini un type d'enregistrement record et une source de données. Le nom du type d'enregistrement est "rolodex_entry" et le nom de la source de données, c'est "rolodex_data". C'est par moyen de ces noms qu'on pourra mettre nos sales mains dessus dans le code java!
D'accord, il est bel et bien de spécifier les choses avec un format bien défini, mais quand-même, on n'a rien fait jusqu'ici. Maintenant c'est le moment de regarder comment on se sert de tout ça dans le code java.
Jettez un coup d'oeil au fichier MiniRoloServletInteraction.java. A vrai dire, le servlet minirolo est assez simple. On peut caractériser sa fonctionnalité par les 4 actions de base suivantes:
Les actions mentionnées ci-dessus correspondent aux méthodes execDefault(), execDelete(), execEdit() et execProcess()respectivement.
Commençons en regardant le code du méthode execDefault()
. Il s'agit
du méthode qui fournit la vue par défaut, qui est simplement une liste de toutes
les entrées du système. C'est peut-être étonnant de voir jusqu'à quel point c'est
bref. Ce qu'il fait, c'est qu'il commence en obtenant une référence à la source
de données qu'on a définie das l'étape précédente. Donc, dans la ligne suivante,
il invoque le méthode select
sur l'objet DataSource avec un
argument nulle. Je devrais mentionner en passant que select()
prend
généralement un argument du type com.revusky.oreo.RecordFilter
qui
implique un sous-ensemble d'enregistrements à récupérer d'une source de données.
Mais si, comme dans ce cas-ci, on lui donne un argument null, la liste rendue
contiendra simplement tous les enregistrements de la source de données en question.
Vous devriez reconnaître la ligne suivante si vous avez passé par les tutoriels précédants. Ce qu'on fait, c'est qu'on obtient la page modèle dont on va se servir pour montrer nos entrées. Donc, la ligne suivante expose la liste d'un seul coup. Rendu ici, il serait peut-être une bonne idée de regarder le fichier entries.nhtml afin de voir où se passe toute la "magie".
Maintenant, regardons comment le méthode execDelete()
traite l'action
d'effacer un enregistrement. En fait, il commence de la même façon que le méthode execDefault()
qu'on vient de regarder ci-haut. Il obtient une référence à notre seule source
de données, qui s'appelle "rolodex_data". Bon, l'action d'effacer veut
dire qu'il faut enlever une entrée spécifique de la source de données où elle se
trouve. Vous vous rappelez de ce qu'on a dit avant? La combination du type d'enregistrement
et la clé primaire devrait indentifier un registrement d'une source de données.
Bon, ce qu'on suppose ici, c'est que la clé primaire unique_id est
incrustée dans la pétition HTTP. Vu qu'on se sert de cette même disposition
ailleurs, on met le petit bout de code pertinent dans son propre méthode, method,
called getEntryID()
, qui nous donne un Integer (ou null) en se
basant sur le paramètre unique_id qui se trouve dans la pétition.
Maintenant, vu qu'on dispose d'une clé unique et une source de données, on peut
obtenir l'enregistrement en question en utilisant le méthode get()
de l'objet
type MutableDataSource. Tout en supposant qu'il nous rend un enregistrement non-nulle,
on peut invoquer le méthode delete() pour enlever cet enregistrement du conteneur.
Ce qu'on fait finalement, c'est qu'on envoit quelque message de confirmation à l'usager. On obtient la page modèle ack.nhtml et on expose l'information dont elle a besion. On expose expose une variable qui indique que l'opération effectuée a été d'effacer. D'ailleurs, on expose l'enregistrement qui a été enlevé du conteneur, au cas où le client pourra y jetter un dernier coup d'oeil sur la page de confirmation. (En fait, c'est le genre de disposition qui s'applique à toute sorte de site de commerce electronique typique.)
A vrai dire, le code qui traite l'action edit n'est pas tellement différent du code qu'on vient de voir. La différence principale, c'est qu'on se sert de la même action pour 2 cas différents:
On a besoin de chercher l'enregistrement par clé primaire seulement dans le second des deux cas ci-dessus. Essentiellement, ce qu'on fait, c'est qu'on cherche une clé primaire dans les paramètres de la petition HTTP (exactement comme dans le cas où il s'agissait d'effacer un enregistrement) et si on la trouve, on suppose que l'usager veut modifier l'entrée correspondante. S'il n'y a pas de paramètre unique_id= dans la pétition, on suppose que l'usager veut ajouter une nouvelle entrée.
Maintenant, je vous encourage de jetter un coup d'oeil à la page modèle edit.nhtml afin de mieux voir la façon dont tous les éléments travaillent ensemble.
Tout en étant assez bref tout de même, execProcess()
est le plus
compliqué des 4 méthodes execXXX de cet exemple. C'est dû surtout au fait qu'il
doit distinguer entre deux cas, si quelqu'un ajoute une nouvelle entrée ou
modifie une entrée existante, et il doit les traiter séparemment. Encore une
fois, c'est basé sur la présence ou absence du paramètre "unique_id="
dans la pétition HTTP. Le premier cas traité, c'est où le paramètre est absent,
et donc, il s'agit d'une nouvelle entrée.
Il y a deux choses importantes à remarquer dans ce méthode. La première, c'est
le méthode d'utilité fillInFields()
qui remplit automatiquement les
champs d'un enregistrement en se basant sur les paramètres dans la pétition HTTP. Il le
fait en se servant des métadonnées de l'enregistrement pour itérer a travers ses champs
et les remplir en se basant sur les paramètres de la pétition HTTP. Dès
qu'on fait ça, il s'agit d'invoquer ou bien insert()
ou update()
sur la source de données, dépendant de si c'est une entrée nouvelle ou une
modification. Finalement, après avoir effectué les modifications des données, on
obtient la page modèle appropiée pour confirmer que l'opération a été effectuée
et donc, on expose l'enregistrement qui est interpreté comme une variable type
corréspondance dans la couche de présentation.
A part la disposition ci-dessus, l'autre chose importante à remarquer, c'est la
différence de base entre le premier est le deuxième cas traité. Remarquez la présence
de l'appel au méthode getMutableCopy
dans le cas où on modifie un
enregistrement qui existe déjà. Vous voyez, un enregistrement Oreo qui vient d'être créé est
dans un état mutable. Dès qu'on le met dans un conteneur type
MutableDataSource, il est validé et devient immutable. Bon, pour
modifier cet enregistrement, ce qu'on fait, c'est qu'on créé un clone mutable, on
y effectue les modifications et ensuite, on sustitue le nouvel enregistrement pour l'ancien.
Bon, je ne sais pas combien de lecteurs se rendront compte tout de suite que
toute cette disposition -- qui, à première vue peut paraître quelque peu baroque --
a à voir avec des garanties de sécurité dans un environnement multitâche. On développera
ces idées en plus de détail ailleurs. Rendu à cette étape, il faut simplement
retenir que l'acte de modifier un enregistrement existant est fondamentalement différent
de l'acte d'ajouter un enregistrement nouveau. Le code de ce méthode peut être la première
fois que vous voyez ceci, mais ce ne sera pas la dernière, au moins si vous
continuez d'utiliser cette librairie!
La dernière nouveauté de cet exemple se trouve chez le méthode recover()
.
Il s'agit d'un méthode qu'on peut redéfinir et qui nous fournit un endroit pour
récupérer d'erreurs. Par exemple, si, dans notre configuration de métadonnées,
on définit un champ ("last name" par exemple) comme étant obligatoire,
et l'usager ne le remplit pas, la moteur de persistance sous-jacente lancera une
exception du type com.revusky.oreo.MissingDataException
. Le méthode
recover() nous fournit une opportunité de traiter ces conditions "doucement"
disons.
Ça peut être très instructif de comparer et contraster ce servlet avec celui de
l'exemple antérieur, le livret d'invités. Par exemple, le méthode execDefault()
est presque le même dans les deux cas. La différence clé, c'est que cet exemple
exploite la couche de persistance que Niggle fournit pour obtenir la liste d'entrées
à exposer au mécanisme de pages modèles. Dans l'exemple antérieur, tous
les éléments de la liste n'étaient que des instances de java.util.Hashtable
.
Mais ici, ce sont des instances de com.revusky.oreo.Record
. C'est
important de bien remarquer que les enregistrements Oreo peuvent être exposées au mécanisme
de pages modèles de la même façon qu'un Hashtable (ou n'importe quel objet qui
implémente java.util.Map.) Ce code profite de cette prestation aussi dans le méthode
execEdit()
.
Il y a un autre aspect qui rend cet exemple plus complet que l'antérieur. Tandis
que le livret d'invités ne permettait que l'insertion, cet exemple offre la
possibilité de modifier et d'effacer les entrées. Donc, le méthode execProcess()
est un peu plus complèxe et il existe aussi un méthode execDelete()
pour effacer.
Cet exemple présente déjà presque tous les éléments conceptuels d'une application plus complèxe basée sur Niggle. Si vous atteignez un certain niveau de confort avec tous ces éléments, vous aurez supéré les principaux obstacles dans l'apprentissage de Niggle. Bien sûr qu'il y a pas mal d'autres prestations, et d'autres apparaîtront avec le temps, vu que Niggle, c'est un projet vivant qui se développe encore. Tout de même, les choses que vous apprendrez à partir de ce moment sont de nature plutôt incrémentale.
Remarquez que le fichier datasources2.xml contient une configuration alternative qui utilise une BD externe pour la persistance. Pour faire fonctionner cela, vous serez obligé sans doute de changer certains les paramètres JDBC_URL et JDBC_DRIVER_CLASS.
Je ne vois aucun raison pour que ça ne marche pas avec une BD quelleconque, tout en supposant qu'elle dispose d'un driver JDBC. Néanmoins, ça m'intereserrait beaucoup obtenir quelque confirmation de ça. Bien evidemment! Si vous avez fait fonctionner cet exemple avec une autre BD, ou bien si vous l'avez essayé mais il y a eu des problèmes, s'il vous plaît, écrivez-moi pour conter votre expérience!
En tout cas, une bonne approche pour le moment sera de faire que votre logique d'application fonctionne avec les fichiers plats, vu que cela n'exige pas la configuration d'aucun outil externe. Ensuite, vous pourrez changer è une BD externe au moment où il sera nécessaire.