Introduction

Objectifs

Ce document est un guide d'utilisation simple permettant de prendre en main le Framework LDAP. Il explique les points suivants :

  • comment intégrer un fichier de paramétrage du framework, produit par ailleurs, dans une application ;
  • comment paramétrer la configuration ;
  • comment instancier, configurer, démarrer et arrêter le framework ;
  • comment intégrer le framework dans un environnement de développement (eclipse) ;
  • comment utiliser les services d'une instance du framework.
Ce document ne reprend pas les points suivants qui sont décrits dans d'autres sources, passées ou à venir :
  • la structure d'un fichier de configuration (voir Spécifications SPI et annexe) ;
  • l'utilisation de la console d'administration pour produire un tel fichier ;
  • le fonctionnement interne du framework (voir Javadoc et les documents de conception et de développement) ;
  • le fonctionnement du protocole LDAP et plus généralement de l'API JNDI et des services d'annuaires de type X.500 (ceci est supposé connu) ;
  • les bonnes pratiques de la programmation d'une instance de framework.

Public visé et pré-requis

Déploiement et configuration

La première partie concernant le déploiement et la personnalisation d'une instance de composant de mapping métier concerne plus particulièrement le rôle d'intégrateur. Celui-ci doit avoir la connaissance technique de l'infrastructure nécessaire au bon fonctionnement de l'application, en particulier :
  • les URI et identifiants nécessaires à la connexion à un annuaire LDAP ;
  • la structure du classpath de l'application cible.
Pré-requis
  • connaissance du format XML (DTD, résolution d'entités, espaces de nommages, ...)
  • connaissance de la structure normalisée d'organisation des projets J2EE (arborescence des répertoires et en particulier du répertoire WEB-INF, gestion du classpath)

Développement

La seconde partie concerne le développeur applicatif qui a en charge la réalisation d'une application utilisant le mapping prédéfini, le plus souvent dans un cadre d'applications multicouches de type J2EE. Notons que le framework LDAP, s'il s'intègre harmonieusement dans un tel environnement, peut être utilisé dans une application autonome.
Pré-requis
  • langage Java (1.4.2) et programmation dans l'environnement J2EE (1.4)
  • fichier de configuration Maven (1.0.2)
  • environnement de développement Eclipse (3.1)

Déploiement et intégration

Ce chapitre traite du déploiement concret d'un fichier de paramétrage de mapping et de sa configuration pour l'adapter aux exigences d'un site spécifique. Le scénario de base est le suivant :
  1. l'intégrateur reçoit un fichier décrivant un mapping de composant métier produit par un concepteur ;
  2. l'intégrateur définit les variables du composant qui sont propres à la cible d'intégration ;
  3. l'intégrateur structure ces variables et place le fichier de configuration de manière à ce qu'il puisse être utilisé à l'exécution.

Déploiement du fichier de configuration

Structure d'un fichier de configuration

Le fichier de configuration d'un mapping métier est un simple fichier au format XML. La grammaire DTD des fichiers de configuration est fournie dans les annexes, ainsi qu'un extrait du fichier de configuration du démonstrateur reprenant les principales balises utilisées. Ce fichier contient la description du fonctionnement du mapping. Du point de vue de l'intégrateur, les éléments importants sont les balises <var> qui déclarent et définissent des variables. Ces variables peuvent être utilisées dans n'importe quelle partie du descripteur XML sous deux formes différentes :
  1. sous la forme ${nomvar}, la valeur utilisée est celle de la variable correspondante, dans le contexte d'exécution, au moment où la variable est lue ;
  2. sous la forme #{nom_variable}, la substitution est effectuée à la volée au moment de la lecture du fichier XML : dans ce cas, le configurateur fonctionne comme des macros dans un fichier C.

Déploiement d'un fichier de configuration

Le fichier de configuration doit être accessible au composant de mapping métier. Deux solutions sont possibles :
  1. si l'application intégrant le composant mapping métier a accès à un système de fichiers en lecture, le descripteur XML peut être simplement copié dans un répertoire accessible à l'application ;
  2. sinon, le descripteur doit être placé dans le classpath de l'application.

Cas des applications Web

Dans le cas des applications web, le répertoire WEB-INF/classes peut être utilisé pour stocker le descripteur qui sera dans ce cas accessible par l'intermédiaire du class loader du composant. Le fragment de code ci-dessous charge le fichier de configuration depuis le répertoire WEB-INF/classes/fr/oqube/ et en effectue l'analyse syntaxique :
  XMLConfigurator xc = new XMLConfigurator();
  xc.parse(Thread.currentThread()
                 .getContextClassLoader()
                 .getResourceAsStream("fr/oqube/configuration.xml"));
Une autre possibilité est de stocker directement le fichier de configuration dans le répertoire WEB-INF. Dans ce cas, il est possible d'y accéder à partir du contexte de servlet :
  XMLConfigurator xc = new XMLConfigurator();
  xc.parse(getServletContext().getResourceAsStream("/WEB-INF/configuration.xml"));

Paramétrage du fichier de configuration

Seules les variables dont la valeur est substituée directement au moment de l'analyse du fichier XML doivent être définies et déclarées avant leur utilisation. Les autres paramètres peuvent être instanciés tant que le composant n'est pas en mode exécution (voir l'API Spécification API). Trois solutions s'offrent à l'intégrateur et au concepteur pour effectuer cette phase de personnalisation et définir la valeur des variables dont vont dépendre les opérations du mapper :
  1. modification du fichier de configuration principal ;
  2. définition des variables dans un fichier XML externe ;
  3. définition des variables dans un fichier properties externe.

Modification des variables dans le fichier

Le paramétrage en fonction du contexte d'exécution est effectué par l'intégrateur directement dans le descripteur XML par définition des variables et modification de l'attribut value. Dans le cas du descripteur exemple (cf. annexes), on aurait directement dans le fichier XML les modifications suivantes :
  ...
  <var name="user" value="cn=cdmadmin,ou=users,o=services" />
  <var name="pass" value="cdmadmin" />
  <var name="url"  value="ldap://130.81.0.6:389" />


	<!-- basic sample XML configuration file for LDAP -->
	<!-- this file is used to test source configuration and -->
	<!-- search operations on a genuine LDAP source         -->
	<j:jndi-mapper
Avertissement : cette solution est une source potentielle d'erreurs dans la mesure où le fichier de configuration est une ressource critique pour le bon fonctionnement du composant. Toute erreur dans le format XML se traduira nécessairement par une exception au moment de la configuration du composant (cf. ci-dessous).

Externalisation des variables dans un fichier

Une autre solution consiste à utiliser les mécanismes de résolution d'entités standards d'XML pour externaliser la définition des variables. Pour ce faire, il est tout d'abord nécessaire de déclarer une entité externe, c'est à dire soit un fragment XML qui sera intégré tel quel dans le fichier principal, soit un fichier de propriétés Java dont le contenu sera utilisé comme référentiel de variable pour l'analyse du fichier XML. La déclaration ci-dessous définit une entité nommée &connexion; dont le contenu est stockée dans la ressource resources/test/connexion.xml. La ressource doit être accessible dans le classpath.
<!DOCTYPE mapper-config [
    <!ENTITY connexion SYSTEM "file://resources/test/connexion.xml" >
]>
Le fragment XML suivant indique que le contenu du fichier resources/test/connexion.xml sera inséré dans le fichier de configuration.
  &connexion;
Cette définition doit être réalisée par le concepteur du mapping et apparaître dans le fichier de configuration transmis à l'intégrateur. De la sorte, ce fichier n'a plus à être modifié et il appartient à l'intégrateur de définir les ressources nécessaires dans un autre fichier, soit au format XML, soit au format properties.
Ce mécanisme permet d'isoler certaines variables et donc favorise la modularité de la configuration2.
Format XML
Le contenu du fichier connexion.xml, quant à lui, peut être par exemple :
 <var name="user" value="cn=cdmadmin,ou=users,o=services" />
 <var name="pass" value="cdmadmin" />
 <var name="url"  value="ldap://130.81.0.6:389" />
Ce contenu est identique à ce qu'aurait écrit l'intégrateur dans le cas d'une modification directe du fichier de configuration principal.
Format properties
Une solution encore plus simple consiste à définir les variables sous la forme d'un fichier properties standard de Java. Pour mémoire, les fichier de propriétés de Java sont de simples fichiers textes contenant des couples clés-valeurs : un couple clé-valeur séparé par le signe égal par ligne1. Par exemple, voici l'équivalent de la définition des variables ci-dessus sous la forme de properties :
user=cn=cdmadmin,ou=users,o=services
pass=cdmadmin
url=ldap://130.81.0.6:389
Pour pouvoir utiliser des variables sous cette forme, il est nécessaire d'avoir déclarer l'entité connexion de la manière suivante :
<!DOCTYPE mapper-config [
    <!ENTITY connexion SYSTEM "property://resources/test/connexion.properties" >
]>
Le chemin d'accès du fichier properties, resources/test/connexion.properties est résolu relativement au classpath.

Utilisation en développement

Ce chapitre décrit comment configurer l'environnement de développement pour utiliser le framework LDAP, du point de vue du développeur. Notons que le framework LDAP utilise l'API standard JNDI qui se trouve intégrée dans le Java Runtime Environment à partir de la version 1.4 et que par conséquent ne nécessite aucune autre bibliothèque pour fonctionner.

Configuration Maven

Lorsque l'application est pilotée par Maven, il suffit de rajouter une dépendance dans la section correspondante vers la bibliothèque framework-ldap.jar. Le fragment suivant décrit cette configuration pour la version 1.0.2 de Maven.
  <dependency>
   <groupId>norsys</groupId>
   <artifactId>framework-ldap</artifactId>
   <version>1.0</version>
   <properties>
    <eclipse.dependency>true</eclipse.dependency>
    <war.bundle>true</war.bundle>
   </properties>
  </dependency>

On supposera qu'un référentiel contenant l'archive est accessible depuis le poste de développement.

Configuration Eclipse

Avec un projet Maven

La configuration dans eclipse se fait soit à partir d'un projet Maven par la commande :

$> maven eclipse
 __  __
|  \/  |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \  ~ intelligent projects ~
|_|  |_\__,_|\_/\___|_||_|  v. 1.0.2

build:start:

eclipse:generate-project:
    [echo] Creating /home/nono/norsys/oqube/framework-ldap/workspace/demonstrateur/.project ...

eclipse:generate-classpath:
    [echo] Creating /home/nono/norsys/oqube/framework-ldap/workspace/demonstrateur/.classpath ...
    [echo] Contains JUnit tests
    [echo] Setting compile of src/test to target/test-classes
Plugin 'cactus-maven' in project 'Demonstrateur-Framework-LDAP' is not available
    [echo] Setting default output directory to target/classes

eclipse:
    [echo] Now refresh your project in Eclipse (right click on the project and select "Refresh")
BUILD SUCCESSFUL
Total time: 2 seconds

Environnement autonome

Dans un environnement eclipse 3.1 autonome, il suffit de rajouter le fichier framework-ldap.jar aux propriétés du projet. Dans la fenêtre de navigation Java

sélectionner le projet et le menu contextuel properties

sélectionner Build path

Selon les cas, choisir Add jar (si le fichier d'archive se trouve dans le projet), Add external jar si le fichier se trouve ailleurs, Add variable si le chemin d'accès au fichier d'archive est accessible à partir d'une variable.

Mise en oeuvre applicative

Ce chapitre décrit les principes de base de l'utilisation de l'API du framework LDAP et des mappers métiers définis dans un fichier de configuration. Nous prendrons comme exemple la configuration du démonstrateur livré avec le framework LDAP version 1.0.

Nous supposons que le contexte de développement est une application web standard. En particulier, nous supposerons que le framework Spring ou tout autre framework de composants n'est pas utilisé bien que le framework LDAP soit prévu pour s'intégrer harmonieusement avec de tels systèmes. Ce choix nous permettra d'illustrer l'ensemble du cycle de vie d'un composant de mapping métier.

Le détail de la conception d'un fichier de configuration XML est précisé dans le chapitre suivant. Ces informations ne sont normalement pas nécessaires au développeur d'application intégrant un composant de mapping.

Initialisation du composant de mapping métier

Pour pouvoir être utilisable dans le code, le composant de mapping métier doit être initialisé, configuré et mis en marche. L'exemple de la classe MapperLoader ci-dessous représente un code typique pour réaliser ces opérations.

public class MapperLoader {
	private MapperManager mapperManager;
	private XMLConfigurator configurator;
	private String mapFile;

	public void init() throws MapperException, XMLConfiguratorException {
		configurator.parse(Thread.currentThread().getContextClassLoader().getResourceAsStream(mapFile));
		configurator.configure(mapperManager);
		mapperManager.setConfigured();
		mapperManager.start();
	}
}

Les opérations suivantes sont réalisées :

  1. le fichier de configuration est analysé par le configurateur XML : la chaîne mapFile est un nom de chemin dans le classpath le référencant ;
  2. l'instance de MapperManager est configurée par le configurateur ;
  3. le manager est démarré.
Les instances de MapperManager et de XMLConfigurator peuvent être créées naturellement par appel du constructeur. Dans le cas d'une application web, le code du chargeur sera invoqué soit dans une méthode init() soit dans une servlet d'initialisation.

Utilisation des mappers

Plutôt que d'utiliser directement l'API du framework LDAP, on préférera utiliser un patron de conception Facade : une interface métier expose des méthodes spécifiques encapsulant les appels au code de l'API. Le fragment de code suivant illustre cette stratégie. L'interface UserManager déclare des méthodes métiers permettant de rechercher un utilisateur, de le sauvegarder, ...
public interface UserManager {
	List searchUsersByCriteria(User criteria) throws MapperException;
	void saveUser(User user) throws MapperException;
	User getUser(String id) throws MapperException;
	void deleteUser(String id) throws MapperException;
}

A chaque méthode de l'interface Facade correspondront une ou plusieurs invocations de l'API du framework et des maps définis dans le fichier de configuration. Nous détaillons ci-après les différents méthodes représentant un ensemble d'opérations CRUD standards sur un objet métier spécifique, en l'occurence un utilisateur représenté par un objet User.

Recherche

Le fragment Java ci-dessous est une implantation de searchUsersByCriteria. On remarquera que l'encapsulation dans une méthode métier permet de s'assurer de la libération des ressources par appel à la méthode release().

Les différentes étapes sont toujours les mêmes. On commence par récupérer une instance du mapper à partir de son nom symbolique :

	public List searchUsersByCriteria(User criteria) throws MapperException  {
		SearchMapper searchMapper = (SearchMapper) mapperManager
			.getMapper("searchUsersByCriteria");

L'objet SearchMapper récupéré est construit par le framework à partir de la description XML ci-dessous, qui définit le filtre, utilisé les paramètres d'entrée, la structure de sortie, la configuration de la recherche.

	<!-- searchUsersByCriteria -->
	<j:search input="userSearchCriteria"
		output="userSearchResult" name="searchUsersByCriteria">
		<j:filter><![CDATA[(&(objectclass=person)(sn=${lastName})(givenName=${firstName})(oqubeattsite=${site}))]]>
		</j:filter>
		<j:root>ou=Personnes,o=oqube</j:root>
		<j:searchScope>1</j:searchScope>
		<j:derefLinkFlag>true</j:derefLinkFlag>
		<j:returningObjFlag>true</j:returningObjFlag>
		<j:countLimit>0</j:countLimit>
	</j:search>

Ensuite, on construit la map en input contenant un ensemble de couples clés-valeurs qui seront utilisés pour la construction de la requête. Dans le cas présents, on effectue une recherche à partir du nom et du prénom, sur un certain site.

		Map map = new HashMap();
		String firstName = user.getFirstName();
		String site = user.getSite();
		String lastName = user.getLastName();
		if(firstName == null || firstName.equals(""))
			firstName = "*";
		if(lastName == null || lastName.equals(""))
			lastName = "*";
		if(site == null || site.equals(""))
			site = "*";
		map.put("firstName",firstName);
		map.put("lastName",lastName);
		map.put("site",site);

Enfin, on invoque le mapper qui retourne une liste d'objets Map contenant les attributs définis dans le paramètre output de la définition de la map.

	List results;
	try {
		results = searchMapper.search(map, null);
	} finally {
		mapperManager.release(searchMapper);
	}
	return results;
}

Rappelons que l'appel à la méthode release() est très important, d'où sa présence dans un bloc finally car c'est lui qui permet au système de libérer les ressources nécessaires à l'exécution de la requête.

Ajout

L'ajout d'une entité suit le même schéma en utilisant un objet AddMapper à la place d'un objet SearchMapper. Nous détaillons le code d'une méthode d'ajout d'un utilisateur.

On commence par récupérer l'objet mapper utilisé.

public void addUser(User user) throws MapperException {
	AddMapper addMapper = (AddMapper) mapperManager.getMapper("addUser");

Celui-ci correspond à la définition suivante dans le fichier de description XML. On notera que celle-ci est beaucoup plus simple que dans le cas d'une recherche :

	<!--  addUser -->
	<j:add input="input-user" name="addUser" output="output-user">
		<j:root>cn=${id},ou=Personnes,o=oqube</j:root>
	</j:add>

On crée ensuite la map en entrée en utilisant une méthode générique de transformation d'un objet en map fournie par la classe fr.norsys.mapper.util.Convert. Cette méthode effectue par réflexion la transformation des propriétés de l'objet en une instance de Map :

	Map input = Convert.toMap(user);

La map est complété par des informations propres à l'application concernée, en l'occurence un identifiant unique construit à partir de certaines valeurs stockées dans l'annuaire. Enfin, on invoque la méthode add et l'on oublie pas de libérer les ressources.

	input.put("objectClass","oqubeobjperson");
	user.setId(getNextId.next());
	input.put("id",user.getId());
	try {
		addMapper.add(input,null);
	} finally {
		mapperManager.release(addMapper);
	}
}

Modification

La modification suit un schéma très proche de celui de l'ajout. Dans le code du démonstrateur, c'est la même méthode façade qui est invoquée dans les deux cas, le tri se faisant en fonction de l'existence ou nom d'un identifiant unique dans l'objet User.

Récupération du mapper :

	ModifyMapper modifyMapper = (ModifyMapper) mapperManager.getMapper("modifyUser");

Celui-ci correspond à la définition XML suivante. Par défaut, tous les attributs de la map en input remplaceront les attributs correspondant définis dans la map en output. Le concepteur a aussi la possibilité de préciser les opérations de modification qu'il souhaite réaliser.

	<!--  modifyUser -->
	<j:modify input="input-user" name="modifyUser" output="output-user">
		<j:root>cn=${id},ou=Personnes,o=oqube</j:root>
	</j:modify>

Enfin la map en entrée est construite et la méthode modify invoquée sur l'objet mapper, sans oublier bien sûr de libérer le mapper.

	Map input = Convert.toMap(user);
	input.put("id", user.getId());
	input.put("objectClass","oqubeobjperson");
	try {
		modifyMapper.modify(input,null);
	} finally {
		mapperManager.release(modifyMapper);
	}
}

Suppression

La suppression est là aussi très simple.

public void deleteUser(String id) throws MapperException {
	DeleteMapper deleteMapper = (DeleteMapper) mapperManager.getMapper("deleteUser");
	Map input = new HashMap();
	input.put("id",id);
	try {
		deleteMapper.delete(input,null);
	} finally {
		mapperManager.release(deleteMapper);
	}
}

La map correspondante est définie comme :

	<j:delete input="" name="deleteUser" output="">
		<j:root>cn=${id},ou=Personnes,o=oqube</j:root>
	</j:delete>

Attributs multi-valués

Certains attributs dans un annuaire peuvent-être multivalués : à un même nom d'attribut correspondent plusieurs valeurs. Dans l'espace applicatif des objets, ces valeurs sont stockés dans une instance de la classe java.util.List. Une telle liste peut être utilisée soit dans une opération d'ajout d'un noeud, soit dans une opération de recherche.

Ajout

Le cas de l'ajout est le plus simple : il suffit de stocker dans la map en input une instance de List contenant les valeurs d'attributs à stocker et le framework se chargera de convertir cette liste en un ensemble de valeurs pour un même attribut.

Attention : si le schéma de stockage "physique" de l'annuaire n'autorise pas l'attribut à être multivalué, une erreur surviendra.

Recherche

Le cas de la recherche exige plus de travail de la part du développeur : si un attribut multivalué est récupéré dans une recherche, ses valeurs sont automatiquement stockées dans la map de retour sous la forme d'une List. Il appartient au client de vérifier si le type de la valeur stockée dans la map est une liste ou non.

    User mapToUser(Map m ) {
      User u = new User();
      u.setId(m.get("id"));
      u.setNom(m.get("nom"));
      ...
      // il peut y avoir zero ou plusieurs sites
      Object o = m.get("sites");
      if(o != null && (o instanceof java.util.List)) {
         u.addSites((List)o);
      } else if(o != null) {
         // un seul site
         u.addSite((String)o);
      }

L'encapsulation de ces traitements dans une méthode générique sera dans la plupart des cas souhaitable.

Terminaison

Lorsque l'application s'arrête, il est préférable, si le contexte d'exécution est maintenue, d'arrêter explicitement le manager, ce qui permet de propager l'information et éventuellement de laisser la possibilité aux composants techniques internes de libérer des ressources systèmes.

public class MapperLoader {
 ...
	public void destroy()  {
		mapperManager.stop();
	}
}

Ce code pourra par exemple être invoqué au moment de la destruction d'une servlet.

Définition d'un fichier de configuration de mapping

Ce chapitre décrit comment on peut construire manuellement un fichier de configuration XML d'un composant de mapping métier. Ce fichier de configuration XML comprend deux parties imbriquées l'une dans l'autre :

  • une partie de configuration générale propres à l'ensemble des types de sources de données qu'offre le composant de mapping ;
  • une partie spécifique à chacun des types de sources.

DTD

Pour des raisons de modularité et de souplesse d'utilisation, les DTD (ie. grammaires) des fichiers de configuration de mapping XML sont divisées en plusieurs sous-ensembles :
  • une DTD de base nommée base-configuration.dtd ;
  • une DTD par sous-système de mapping (nommée jndi-configuration.dtd pour le système de mapping JNDI/LDAP).

Validation

Lors de l'analyse d'un fichier de configuration, la classe de configuration XMLConfigurator peut-être placée en mode validant ou non-validant à l'aide de la méthode XMLConfigurator. Dans le premier cas, le fichier de mapping est validé par rapport aux DTD utilisées et l'analyse s'arrêtera en cas d'erreur de syntaxe. En particulier, l'analyse s'arrête si aucune déclaration de type de document n'est présente dans le fichier. Dans le second cas, le parseur ne vérifiera pas la syntaxe par rapport à la DTD mais il est évident que le système de configuration interne effectuera des vérifications.

L'utilisation d'un parseur non validant peut-être utile pour faire en sorte que des éléments soient totalement ignorés lors de l'analyse sans provoquer de problèmes.

Déclaration de type de document

La DTD principale est framework-ldap.dtd, à utiliser comme identifiant SYSTEM dans un fichier de configuration :

 <!DOCTYPE mapper-config SYSTEM "framework-ldap.dtd" >

Cette DTD principale inclus la DTD de base base-configuration.dtd et les autres, pour l'instant uniquement jndi-configuration.dtd avec des directives conditionnelles basées sur des entités. Si par exemple on a une configuration sql et pas de configuration jndi, on pourrait imaginer une déclaration de DOCTYPE suivante :

<!DOCTYPE mapper-config SYSTEM "framework-ldap.dtd" [
  <!ENTITY % mapper.jndi "IGNORE"  >
  <!ENTITY % mapper.sql  "INCLUDE" >
>

Résolution des références

L'identifiant SYSTEM est utilisé pour résoudre la référence donnée en un flot de données remplacant l'entité. Cet identifiant est normalement une URI absolue mais peut être, comme ci-dessus, une URI relative. Dans le premier cas, l'identifiant est préfixé par un mode d'accés, p.ex. http:// ou file:// qui indiquent respectivement une ressource accessible par HTTP ou une ressource sur le système de fichier courant. En l'absence de protocole, le parseur XML SAX produit des URI absolues en référence au répertoire courant : si le répertoire courant est /home/nono/framework-ldap, l'identifiant SYSTEM "framework-ldap.dtd" est transformé en file:///home/nono/framework-ldap/framework-ldap.dtd avant passage aucd résolveur d'entités.

Il est donc nécessaire de faire attention à l'emplacement des fichiers de configuration DTD lors de l'utilisation d'un configurateur XML validant. Le Framework LDAP inclut un résolveur d'entités qui reconnaît le préfixe classpath:// ce qui permet de définir des identifiants systèmes relativement au classpath de l'application et non relativement à un système de fichiers. Par défaut, les DTD sont incluses dans l'archive contenant le framework, dans un répertoire dtd/.

En pratique

On peut utiliser la déclaration suivante :

 <!DOCTYPE mapper-config SYSTEM "classpath://dtd/framework-ldap.dtd" >
Attention
  • il est nécessaire de respecter l'ordre des sous-éléments lorsque ceux-ci sont optionnels et uniques
  • des problèmes peuvent survenir en cas de conflits dans l'ordre d'utilisation du CLASSPATH, par exemple si un fichier DTD se trouvent présents en même temps dans deux archives ou dans une archive et le répertoire courant.

Configuration de base

La balise racine est la balise mapper-config qui ne possède aucun attribut mais définit un espace de nommage qui par convention se trouve associé à l'URI http://norsys.fr/framework-ldap/configuration.dtd. Cette balise peut contenir deux autres balises, dans un ordre quelconque :
  • des balises <var> de déclaration de variables ;
  • des balises <sub-config> déclarant un sous-configurateur et un espace de nommage associé.

Variables

La balise <var> permet de définir des variables dans le fichier XML. Ces variables sont toujours déclarées dans la portée du tag englobant :
  • le composant (avec addParameter()) si la variable est définie hors de toute source;
  • une source si la variable est déclarée dans une balise <mapper-subconfig>. Dans ce cas, le configurateur racine XML maintient un environnement accumulant les variables déclarées dans cette portée et les ajoute lorsque la fin d'une balise source est détectée. Les variables déclarées hors d'une source mais dans une sous-configuration sont simplement ajoutées toutes les sources déclarées dans cet environnement.
Dans un fichier XML, les variables sont disponibles immédiatement après leur définition et le configurateur effectuera la substitution immédiatement pour toutes les références de la forme #{nom_variable} : dans ce cas, le configurateur fonctionne comme des macros dans un fichier C. Par exemple, soit les déclarations suivantes :
	<var name="user" value="cn=cdmadmin,ou=users,o=services" />
	<var name="pass" value="cdmadmin" />
	<var name="url" value="ldap://130.81.0.6:389" />

	<var name="regval" value="[0-9]{10}"/>
	<var name="regemail" value="[a-zA-Z0-9._%-]+\@[a-zA-Z0-9._%-]+\.[a-zA-Z]{2,4}"/>
Alors, la chaîne [0-9]{10} sera substitué à la chaîne #{regval} dans la déclaration suivante et de même pour email.
	<j:regex key="mobileNumber" value="#{regval}" ignoreNull="true"/>
	<j:regex key="email" value="#{regemail}" ignoreNull="true"/>

A contrario, les valeurs des variables url, pass et user ne seront pas instanciées au moment de leur déclaration mais au moment de l'utilisation des propriétés les contenant dans le fragment suivant :

	<j:property key="java.naming.provider.url" val="${url}" />
	<j:property key="java.naming.security.credentials"
		val="${pass}" />
	<j:property key="java.naming.security.principal"
		val="${user}" />

Sous-configurateur

Un sous-configurateur est associé à un certain espace de nommage dans le configurateur général. Cette association va permettre au configurateur racine d'identifier les balises XML dont il peut déléguer le traitement à un sous-configurateur, elle se fait en utilisant la balise <sub-config> qui a deux attributs, namespace et class associant une URI d'espace de nommage à un nom qualifié de classe. La classe sera instanciée dynamiquement par le configurateur racine pour traiter les différentes balises correspondantes.

	<sub-config
		name="http://norsys.fr/framework-ldap/jndi-configuration.dtd"
		class="fr.norsys.mapper.jndi.JNDIXMLConfigurator" />

Lors de l'analyse de chaque balise, le configurateur racine effectue la substitution du contenu des attributs avant de passer la main au sous-configurateur associé à l'espace de nommage de la balise (s'il existe). Pour permettre un paramétrage incrémental en développement, une balise à laquelle n'est associée aucun espace de nommage est simplement ignorée.

Le configurateur racine a aussi pour fonction de réaliser les substitutions de variables dans les feuilles (contenu des balises). La méthode String getBuffer() dans la classe XMLConfigurator permet de récupérer leur contenu après substitution.

Configuration JNDI/LDAP

Nous décrivons dans cette section le détail de la configuration pour le composant de fourniture de service JNDI/LDAP. L'ensemble des classes d'implantation de ce composant se trouve dans le paquetage fr.norsys.mapper.jndi et ses descendants. La DTD pour les configurations JNDI se trouve reprise en annexe B du présent document.

Généralités

Dans la présente version, le mapper JNDI/LDAP réalise une association bidirectionnelle entre des objets de type java.util.Map et des noeuds d'un annuaire de type X.500 5. Un annuaire est une structure arborescente à chaque noeud de laquelle sont attachés un ensemble d'attributs, c'est à dire de couples clés-valeurs. La structure de l'annuaire respecte un certain schéma qu'il appartient au concepteur et au développeur de respecter. Il n'entre pas dans le cadre de ce document de définir plus précisément le fonctionnement d'un annuaire X.500.

Le composant de service de mapping JNDI/LDAP s'appuie sur la spécification Java Naming and Directory Interface6. Cette spécification est intégrée par défaut dans le kit de développement Java depuis la version 1.4 et est disponible sur toutes les plateformes Java : J2SE, J2EE, J2ME, ... Elle généralise la notion d'annuaire pour permettre de stocker dans un référentiel de nommage commun des couples noms-objets, ceci afin de faciliter le paramétrage des applications J2EE par exemple. Le CSM JNDI utilise donc l'infrastructure et les appels JNDI standard, un annuaire LDAP assurant éventuellement la communication avec un mode de stockage persistant.

Une instance configurée de CSM JNDI offre ses services sous la forme d'un ensemble de mappers rattachés à des sources, chaque source représentant une "base de donnée" de type annuaire, ie. une connexion pour accéder à un certain serveur LDAP.

Un mapper réalise *une et une seule* une unique requête sur la source de donnée en tenant compte de données fournies en entrée dans une map et en produisant des données dans une map de sortie. Chaque objet map représente une vue "métier" sur les paramètres et le résultat de la requête. Un mapper peut-être réutilisé pour exécuter une même requête avec différents paramètres d'entrée, par exemple en association avec un autre mapper dans une requête de type jointure (voir JoinMapper).

Les différentes requêtes possibles sont :

  • search : prend en entrée une map dont le contenu est utilisé pour paramétrer un filtre, exécuté sur un noeud racine, retourne une liste ordonnée de maps dont la structure est fonction d'une map de sortie fournie dans la requête ;
  • modify : prend en entrée une map dont le contenu est utilisée pour réaliser un ensemble atomique d'opérations de mises à jour sur les attributs d'un noeud racine de l'annuaire. La map de sortie contient le résultat, soit un objet de type booléen, soit un objet exception détaillant l'erreur survenue (voir ci-dessous) ;
  • add : ajoute un nouveau noeud dans l'annuaire. La map en entrée permet de définir les attributs du nouveau noeud ;
  • delete : supprime un noeud dans l'annuaire ;
  • rename : modifie la position d'un noeud dans l'annuaire en changeant son nom relatif ;
  • compare : compare les attributs d'un noeud dans l'annuaire avec les attributs fournies dans la map en input.
Dans tous les cas, les entrées de la map en input peuvent être éventuellement utilisées pour construire la valeur racine (balise root) présente dans toutes les requêtes.. Nous donnons dans les sections suivantes un exemple commenté de configuration.

Structure de base

Un fichier de configuration JNDI/LDAP est constitué d'une balise racine jndi-mapper elle même contenant des déclarations de configuration config, des déclarations de map, de règles de validations valid et de sources de données. Un espace de nommage avec préfixe est déclaré à partir de la balise jndi-mapper ce qui permet de diriger correctement les traitements dans le configurateur global (voir ci-dessus).
<j:jndi-mapper xmlns:j="http://norsys.fr/framework-ldap/jndi-configuration.dtd" >
 <j:map name="personne"> ....
 <j:map name="outmap1"> ...
 <j:valid key="email" val=".*@.*\\..*"
 <j:source name="simplesource"> ...
</j:jndi-mapper>
Les identifiants de map, de source, de règles de validation doivent être uniques.

Maps

Une map est une déclaration d'une liste d'attributs, avec ou sans valeurs :
 <j:map name="personne">
  <j:attr key="nom"    val="" />
  <j:attr key="prenom" val="" />
  <j:attr key="email" val="" />
 </j:map>

Une map possède un nom unique dans l'attribut name. Une map peut hériter de une ou plusieurs autres maps :

 <j:map name="employe" inherits="personne">
  <j:attr key="boss" val="" />
 </j:map>

Dans ce cas, la map finale est construite par aggrégation de la hiérarchie de map en partant des ancêtres et en suivant l'ordre de déclaration dans l'attribut inherits de la balise. Cet ordre a son importance lorsqu'une valeur est présente dans l'attribut val de la balise attr.

Une map peut être utilisée en entrée comme pour les maps ci-dessus ou en sortie :

 <j:map name="outmap1">
  <j:attr key="CN" val="nom" />
  <j:attr key="personEmail" val="email" />
 </j:map>

Dans ce dernier cas, l'attribut key correspond au nom d'un attribut de la source de données qui sera mappé sur l'attribut val correspondant dans une map. Ces map présentent la particularité d'être bijectives.

Règles de validation

Une règle de validation restreint les valeurs admises dans les entrées des maps. Ces règles de validation peuvent être attachées à différents éléments d'un composant de mapping métier : à un mapper spécifique, à une source, à un composant de service ou au niveau global.

Les règles définies dans un certain contexte s'appliquent dans tous les contextes englobés : une règle définie dans une source sera présente dans tous les mappers associés à cette source. Lorsqu'elles sont définies dans une balise englobante, elles sont héritées par toutes les balises descendantes :

  • une règle définie dans la balise <jndi-mapper> s'appliquera toutes les sources ;
  • une règle définie dans une balise <source> s'appliquera à tous les mappers de la source ;
  • une règle définie dans une balise <search>, <add>, ... ne s'appliquera que sur ce mapper.
Ces règles sont des expressions logiques dont les atomes portent sur la valeur d'un ou plusieurs champs de la map en paramètre d'un mapper et qui peuvent être composées par des connecteurs logiques classiques : conjonction, disjonction, négation.
Evaluation
Ces règles sont évaluées séquentiellement lors de l'exécution de chaque méthode map() et chaque mapper possède sa propre structure de règles de validation dépendant des règles définies au moment de sa configuration et de sa création. L'ensemble des règles s'appliquant à un mapper constitue une expression logique et la méthode map() ne sera in fine exécutée que si l'évaluation de l'expression logique avec en paramètres la map input et l'environnement env retourne True.

Ces expressions sont décrites dans un fichier de configuration par une syntaxe XML. Les règles décrites ci-après sont illustrées par des exemples de configuration XML.

Expression rationnelle
 <regex key="email" val=".*@.*\\..*" />

Le contenu d'un champ étiqueté par l'attribut key est transformé en chaîne par la méthode toString() ou une autre méthode appropriée et cette chaîne doit être conforme à l'expression rationnelle val. La syntaxe des expressions rationnelles qu'il est possible de définir est décrite dans la classe java.util.regex.Pattern.

L'attribut ignoreNull permet de définir le comportement de la règle lorsque l'attribut sur lequel elle s'applique n'a pas de valeur. Si l'attribut est true alors les valeurs nulles sont ignorées et la règle sera considérée comme vraie, sinon elle est fausse.

Valeurs de map par défaut
 <defaults map="personne" />

Cette régle de validation est particulière dans la mesure où elle renvoie toujours vrai mais où elle peut modifier la map input.

L'attribut map doit être un nom de map présent dans le contexte de configuration de la règle et les valeurs de cette map sont utilisées comme valeurs par défaut pour toutes les map en input du contexte dans lequel s'applique la règle de validation.

Lorsque la règle est évaluée, si la valeur dans la map en input d'un champ de la map defaults est null (c'est à dire si le champ a vraiment pour valeur null ou s'il n'existe pas), alors la valeur par défaut lui est attribuée. Lors de la configuration de la règle, les valeurs par défauts peuvent contenir des références de variables de substitution à l'exécution, auquel cas ces variables seront évaluées et substituées en utilisant la map en input comme environnement d'évaluation. Ce mécanisme permet de construire des valeurs par défauts en fonction de la valeur de certains champs d'une map en input.

Valeurs de map obligatoire
 <mandatory map="personne" />

Cette règle permet d'imposer la présence de certains champs dans une map en input. L'attribut map doit être un nom de map présent dans le contexte de configuration de la règle.

Lorsque la règle est évaluée, si la valeur dans la map en input d'un champ de la map mandatory est null (c'est à dire si le champ a vraiment pour valeur null ou s'il n'existe pas), alors la règle est false.

Et logique
 <and>
    <valid key="email" val=".*@.*\\..*" />
    <valid key="nom complet"   val=".* .*" />
 </and>

Une règle and est valide si et seulement si toutes les règles qui la composent sont valides. Notons que les éléments de la règle situés après un élément évalué false ne sont pas évalués.

Ou logique
 <or>
    <valid key="email" val=".*\\..*@toto.fr" />
    <valid key="email" val=".*\\..*@tutu.fr" />
 </or>

Une règle or est valide si et seulement si l'une des règles qui la composent sont valides. Notons que les éléments de la règle situés après un élément évalué true ne sont pas évalués.

Non logique
 <not>
    <valid key="email" val=".*@spam.fr" />
 </not>

Une règle not est valide si et seulement si toutes les règles qui la composent ne sont pas valides.

Composition

Les règles de validation peuvent être arbitrairement composées pour décrire des règles complexes :

 <and>
   <not>
    <valid key="email" val=".*@spam.fr" />
   </not>
   <or>
    <valid key="email" val=".*\\..*@toto.fr" />
    <valid key="email" val=".*\\..*@tutu.fr" />
   </or>
 </and>

Sources

Une source représente schématiquement une configuration de connexion, c'est à dire les informations nécessaire à une opération de liaison (bind) et les mappers associés.

Une source est identifiée par un nom unique et une balise config contient un ensemble de valeurs de configuration, sous la forme de propriétés de type clés-valeurs. Les propriétés disponibles sont celles prévues par la spécification JNDI dans la description de l'interface javax.naming.DirContext, plus des propriétés propres au composant qui permettent de faciliter la définition de politiques d'accès.

 <j:source name="simplesource">
  <j:config>
   <j:property key="java.naming.factory.initial" val="com.sun.jndi.ldap.LdapCtxFactory" />
   <j:property key="java.naming.provider.host" val="${host}" />
   <j:property key="java.naming.provider.protocol" val="ldap" />
   <j:property key="java.naming.provider.port" val="389" />
   <j:property key="java.naming.provider.baseDN" val="${basedn}" />
   <j:property key="java.naming.security.principal" val="${user}" />
   <j:property key="java.naming.security.credentials" val="${passwd}" />
  </j:config>

 </j:source>

Ces propriétés sont :

  • java.naming.provider.host
  • java.naming.provider.protocol
  • java.naming.provider.port
  • java.naming.provider.baseDN
Elles sont utilisées pour définir la propriété java.naming.provider.url par concaténation des différents champs pour produire une url. On remarquera l'utilisation de références de variables dans la configuration de la source.

Requêtes

Une source contient enfin un ensemble de requêtes (search, modify, add, rename, delete, compare) paramétrables. Chaque requête est identifiée par un nom unique dans l'attribut name et éventuellement d'autres attributs.
Recherche
Une requête search possède les attributs suivants :
  • input qui est une référence vers une map dont les entrées sont utilisées dans la requête. Cette map est utilisée comme règle de validation mandatory ;
  • output est une référence vers une map définissant la structure de sortie de la requête. Notons que les clés de cette map sont toujours utilisées pour contraindre la requête (et éviter les requêtes génériques grosses consommatrices de ressources).
  <j:search input="employe" output="outmap1" name="searchEmail">
   <j:filter>(&(CN=#{nom} #{prenom})(job=${jobs}))</j:filter>
   <j:root>OU=NPIF, O=Norsys</j:root>
   <j:count-limit>20</j:count-limit>
   <j:search-scope value="Children" />
  </j:search>
La balise filter est le texte de la requête 7 conforme à un filtre LDAP standard 8. Les valeurs des entrées de la map input sont utilisées comme variables et substituées dans la requêtes (variables #{xxxx} ainsi que les variables globales de la configuration ${zzz}) avant son exécution. Cette balise n'est pas obligatoire, si elle est absente, l'ensemble des paires clés-valeurs de la map input est utilisé comme filtre de la requête lié par une condition and. La balise root est le nom distingué (DN) du noeud à partir duquel sera effectuée la requête. Les variables locales (map en input) et globales (configuration) sont aussi utilisables dans la définition de la balise.

La balise search-controls contient des paramètres de contrôles tels que décrits dans javax.naming.directory.SearchControls permettant de contrôler plus finement la requête.

Une requêtes search retourne une liste de maps contenant des couples clés-valeurs. Les clés sont fonction de la map output et les valeurs sont la représentation sous forme de chaîne des valeurs stockés dans l'annuaire. Si un attribut en retour est multivalué, la valeur est une instance de l'interface List, sinon il s'agit d'un Object tel que retourné par le système LDAP. Le DN de chaque objet est stocké dans l'entrée __DN__ de la map, ce nom est relatif à la source à laquelle appartient le mapper.

Modification

Une requête modify décrit un ensemble de modifications des attributs d'un noeud. --Les attributs à modifier sont définis par l--La map input définit les variables d'entrée de la méthode, la correspondance entre le métier et l'annuaire est donné par la map output, le noeud cible par la balise root et les changements par une suite de balises operation. Chaque balise permet de réaliser une opération sur un attribut :

  • soit modifier sa valeur avec la valeur de la map en input ;
  • soit ajouter une valeur à un attribut une valeur ;
  • soit supprimer une valeur d'un attribut.
Dans le premier cas, la valeur de l'attribut est remplacée totalement par la valeur définie dans l'opération. Si cette valeur est vide, par convention l'attribut voit toutes ses valeurs supprimées. Dans le second cas, la valeur est ajoutée à l'attribut qui peut devenir ainsi multi-valué. Dans le dernier cas, la valeur est supprimée de l'attribut. Un attribut mono-valué dont la valeur est supprimée peut être supprimé si le schéma exige que l'attribut possède une valeur.

Un même attribut peut être modifié par plusieurs opérations successives identiques ou différentes : si les opérations sont identiques, les valeurs sont concaténées pour former une liste de valeurs.

Important : La valeur de l'attribut name d'une operation doit correspondre à une clé dans la map input et ou à une valeur dans la map output.

  <j:modify input="inmap1" output="outmap" name="updateUser">
   <j:root>cn=#{nom} #{prenom},cn=Users</j:root>
   <j:operation name="boss" op="modify" /></j:operation>
  </j:modify>

Enfin, si aucune opération n'est précisée, tous les champs en entrée possédant une valeur dans la map en sortie seront utilisés pour construire une requête de remplacement de valeur.

Ajout et Suppression

Les requêtes d'ajout add et suppression delete de noeuds sont très simples:

  <j:add  input="employe" output="empout" name="newUser">
   <j:root>cn=#{nom} #{prenom},cn=Users</j:root>
  </j:add>

  <j:delete input="employe"  output="empout"  name="delUser">
   <j:root>cn=#{nom} #{prenom},cn=Users</j:root>
  </j:delete>

La map en input de add permet de définir les attributs du nouvel objet créé au noeud root. Dans le cas de delete, la map en input peut être null. Les maps output permettent de contrôler la correspondance entre les noms dans l'annuaire et les noms "métier". Par défaut, les mêmes noms sont utilisés.

Si une valeur stockée dans la map en input est une instance de Collection, alors on considérera qu'il s'agit d'un attribut multivalué qui sera traité en conséquence.

Déplacement

La requête rename modifie la position d'un noeud dans la hiérarchie de l'annuaire.

  <j:rename input="employe" name="fire">
   <j:root>cn=#{nom},cn=Users</j:root>
   <j:newRDN>cn=#{nom}</j:newRDN>
  </j:rename>

Le nouveau DN du noeud est fonction des paramètres d'attributs =newRdn= et =newSuperior= : ce dernier est un nom relatif qui va être le parent du noeud déplacé, et du paramètre newRDN qui indique la nouvelle position du noeud.

Comparaison

La requête compare enfin, vérifie que les attributs d'un noeud correspondent aux attributs demandés. Ce type de requête peut-être utilisé par exemple pour lire des mots de passe ou vérifier des certificats.

  <j:compare input="employe" output="outmap1" name="chkPass">
   <j:root>cn=#{nom},cn=Users</j:root>
  </j:compare>

La requête compare les valeurs de tous les attributs de la map input après renommage par la map output.

Conclusion

Annexe

DTD globale

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!-- This DTD can be used as a general  DTD   -->
<!-- for the framework ldap system. Various   -->
<!-- subsystems can be configured by defining -->
<!-- the value mapper.xxxx entities to either -->
<!-- INCLUDE or IGNORE                        -->

<!-- Base declarations, always included -->

<![INCLUDE[
<!ENTITY % base SYSTEM "classpath://dtd/base-configuration.dtd" >
%base;
]]>

<!-- DTD for JNDI mapper subsystem -->

<!ENTITY % mapper.jndi "INCLUDE">
<![%mapper.jndi;[
<!ENTITY % jndi SYSTEM
"classpath://dtd/jndi-configuration.dtd" >
%jndi;
]]>

DTD pour le fichier de configuration de base

<!-- DTD for mapper configuration  -->
<!--        Norsys.2005          -->

<!-- root element is mapper-config  -->
<!ELEMENT mapper-config ANY >

<!-- default namespace declaration for root tag -->
<!ATTLIST mapper-config xmlns CDATA #FIXED "http://norsys.fr/framework-ldap/configuration.dtd"
                        name CDATA #IMPLIED >

<!-- subconfigurator reference : name and class -->
<!ELEMENT sub-config EMPTY >
<!ATTLIST sub-config name       CDATA #REQUIRED
                     class      CDATA #REQUIRED >

<!--  variable declaration : name and value (optional) -->
<!ELEMENT var EMPTY >
<!ATTLIST var name CDATA  #REQUIRED
              value CDATA #IMPLIED >

DTD pour le fichier de configuration JNDI/LDAP

<!-- DTD for jndi mapper configuration  -->
<!--             Norsys - 2005          -->

<!-- parameter denoting key/value pairs -->
<!ENTITY % key-val "key CDATA #REQUIRED val CDATA #IMPLIED" >

<!-- list of validation nodes  -->
<!ENTITY % valid "(j:regex | j:not | j:or | j:and)" >

<!-- root element : config              -->
<!ELEMENT j:jndi-mapper ((j:source | j:map | %valid; )*) >

<!-- default namespace declaration for j:jndi-mapper  -->
<!ATTLIST j:jndi-mapper xmlns:j CDATA #REQUIRED  >

<!-- a j:source defines parameters and env -->
<!-- to access a JNDI server             -->
<!ELEMENT j:source  (j:config?,(j:search | j:modify | j:add | j:delete | j:rename | j:compare)*) >
<!ATTLIST j:source name ID #REQUIRED
                 pooled ( true | false ) "false" >

<!-- configuration for accessing server    -->
<!-- these are various properties controling     -->
<!-- the behavior of the JNDI subsystem. It usually -->
<!-- includes at least the following elements :  -->
<!-- java.naming.factory.initial      = class name of initial context factory    -->
<!-- java.naming.provider.host        = host name of jndi server     -->
<!-- java.naming.provider.protocol    = protocol of jndi server access (eg. ldap)    -->
<!-- java.naming.provider.port        = port  where jndi server is listening    -->
<!-- java.naming.provider.baseDN      = base DN from where queries are issued    -->
<!-- java.naming.provider.url         = alternative to the preceding elements     -->
<!-- java.naming.security.principal   = principal identification for authenticating request    -->
<!-- java.naming.security.credentials = credentials for authenticating request    -->
<!-- All the properties listed in the javax.naming.Context interfaces and  -->
<!-- javax.naming.ldap.LdapContext  interfaces may be specified here  -->
<!-- @see http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/Context.html#field_detail   -->
<!-- @see http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/ldap/LdapContext.html#field_detail   -->
<!--                     POOLING         -->
<!-- when source is pooled, the following properties may be useful, -->
<!-- although they all have sensible defaults  -->
<!-- jndi.connection.pool.maxPoolSize = maximum size of pool, must be superior at min size (default= 30) -->
<!-- jndi.connection.pool.minPoolSize = minimum and starting size of pool, must be greater than 0 (default = 10) -->
<!-- jndi.connection.pool.timeout     = timeout for blocking on unavailable connections (default = 300) -->
<!-- jndi.connection.pool.factory     = fqcn of ConnectionFactory instance to use -->
<!ELEMENT j:config (j:property*) >

<!-- properties are key/values pairs       -->
<!ELEMENT j:property EMPTY >
<!ATTLIST j:property %key-val; >

<!-- regex validation rules : a name/re pair     -->
<!-- rules are enforced for a configuration file -->
<!ELEMENT j:regex EMPTY >
<!ATTLIST j:regex %key-val;
                ignoreNull ( true | false) "false" >

<!-- a binary validatation rule -->
<!ELEMENT j:or (%valid;,%valid;) >
<!ELEMENT j:and (%valid;,%valid;) >
<!ELEMENT j:not (%valid;) >

<!-- mapping definition  -->
<!-- defines a list of names/default values that may be used as -->
<!-- input/output maps in queries -->
<!ELEMENT j:map (j:attr*)   >
<!-- a map as a unique identifier  -->
<!-- a map may inherit from zero or more other -->
<!ATTLIST j:map name     ID     #REQUIRED
              inherits IDREFS #IMPLIED >


<!ELEMENT j:attr EMPTY  >
<!ATTLIST j:attr %key-val; >

<!-- SEARCH -->
<!-- defines a search mapper query   -->
<!ELEMENT j:search (j:filter?,j:root,j:countLimit?,j:derefLinkFlag?,j:returningObjFlag?,j:searchScope?,j:timeLimit?,j:sort*,j:group*) >

<!-- attribute input references a map (see above) and -->
<!-- is the input map for the search -->
<!-- output is of course the output map for the query -->
<!-- the output map is a string to string map that converts -->
<!-- jndi attributes names to business names -->
<!-- the output map also controls which attributes are effectively -->
<!-- retrieved in a query -->
<!-- attributed name is the unique name of this query -->
<!ATTLIST j:search input  IDREF #REQUIRED
                   output IDREF #REQUIRED
                   name   ID    #REQUIRED >

<!-- filter contains the JNDI/LDAP query to run  -->
<!-- the query text may contains variables of the form #{xxx} -->
<!-- these variables will be substituted  with the corresponding -->
<!-- input field name before query execution -->
<!ELEMENT j:filter (#PCDATA) >

<!-- this element contains the DN of the root context -->
<!-- from which the query must be run  -->
<!ELEMENT j:root  (#PCDATA) >

<!-- maximum number of nodes retrieved inthe query -->
<!ELEMENT j:countLimit (#PCDATA) >

<!-- maximum amount of seconds to wait for answer -->
<!ELEMENT j:timeLimit (#PCDATA) >

<!-- flag indicating wether or not links are dereferenced  -->
<!-- automatically  -->
<!ELEMENT j:derefLinkFlag  (#PCDATA) >

<!-- flag indicating wether or not objects will be returned in  -->
<!-- requests. only relevant for general JNDI operations, not LDAP (??) -->
<!ELEMENT j:returningObjFlag (#PCDATA) >

<!-- search scope -->
<!-- maybe level zero (search only base DN), one level or subtree -->
<!-- default is one level  -->
<!ELEMENT j:searchScope (#PCDATA) >

<!-- sort order of search results -->
<!-- each sort attribute defines a key/value pair indicating  -->
<!-- how sorting should be performed -->
<!ELEMENT j:sort EMPTY >

<!-- name is the attributes name to be sorted -->
<!-- order is ascending ordescending order, default to ascending -->
<!-- filter is not implemented  -->
<!ATTLIST j:sort name   CDATA        #REQUIRED
               order  (asc | desc) "asc"
               filter CDATA        #IMPLIED >


<!--  MODIFY -->
<!-- defines a modify   query   -->
<!-- this kind of query modifies the attributes of an entry -->
<!-- result is a map with the single entry 'result' which -->
<!-- is either the string "OK" or an exception indicating -->
<!-- reason of failure -->
<!ELEMENT j:modify  (j:root,j:operation*) >

<!-- attribute input references a map (see above) and -->
<!-- is the input map for the search -->
<!-- attribute output gives correspondance between attributes -->
<!-- name in input map and attributes names in directory  -->
<!-- attributed name is the unique name of this query -->
<!ATTLIST j:modify input  IDREF #REQUIRED
                 output  IDREF #REQUIRED
                 name   ID    #REQUIRED >

<!-- defines a single operation on an input attribute -->
<!-- each operation defines a name we operate on and -->
<!-- an operation which may be either 'modify', 'add' or 'delete' -->
<!-- the content of the operation tag defines the (string representaiton of)  -->
<!-- attribute value to use -->
<!-- an attribute may appear multiple times with the same or -->
<!-- different operation, in which case values are concatenated to form -->
<!-- a multivalued attribute -->
<!ELEMENT j:operation (#PCDATA) >
<!ATTLIST j:operation name CDATA #REQUIRED
                    op   (modify | delete | add) "modify" >



<!--  ADD  -->
<!-- defines an add query                          -->
<!-- attributes are the input map containing attributes -->
<!-- to be added to the new entry -->
<!-- this kind of query only contains the root (DN) to where -->
<!-- the entry is created -->
<!-- result is a map with the single entry 'result' which -->
<!-- is either the string "OK" or an exception indicating -->
<!-- reason of failure -->
<!-- attributed name is the unique name of this query -->
<!-- attribute output gives correspondance between attributes -->
<!-- name in input map and attributes names in directory (for deletion) -->
<!ELEMENT j:add (j:root) >
<!ATTLIST j:add input   IDREF #REQUIRED
              output  IDREF #IMPLIED
              name    ID    #REQUIRED >

<!-- DELETE -->
<!-- defines a delete query                          -->
<!-- attributes are : -->
<!-- the input map (if present) containing which must be match by the  -->
<!-- entry to be deleted              -->
<!-- this kind of query only contains the root (DN) to where -->
<!-- the entry is created -->
<!-- result is a map with the single entry 'result' which -->
<!-- is either the string "OK" or an exception indicating -->
<!-- reason of failure -->
<!-- attributed name is the unique name of this query -->
<!ELEMENT j:delete (j:root) >
<!ATTLIST j:delete name    ID    #REQUIRED
                 output  IDREF #IMPLIED
                 input   IDREF #IMPLIED >


<!-- MODIFY -->
<!-- defines a modify DN query                          -->
<!-- attributes are :  -->
<!--  - name is the unique name of this query -->
<!-- result is a map with the single entry 'result' which -->
<!-- is either the string "OK" or an exception indicating -->
<!-- reason of failure -->
<!ELEMENT j:rename (j:root,j:newRDN,j:deleteOldRDN?,j:newSuperior?) >
<!ATTLIST j:rename name    ID    #REQUIRED >


<!-- the new relative DN of the modified entry -->
<!ELEMENT j:newRDN (#PCDATA) >

<!-- flag indicating whether or not the old entry is deleted -->
<!ELEMENT j:deleteOldRDN EMPTY >
<!ATTLIST j:deleteOldRDN value (true | false) "false" >

<!-- the name of the new parent of the modified entry -->
<!ELEMENT j:newSuperior (#PCDATA) >


<!-- COMPARE -->
<!-- defines a compare  query                        -->
<!-- attributes are :                                -->
<!-- input map is the key/value pairs to be compared -->
<!-- name is the unique ID of this query             -->
<!ELEMENT j:compare (j:root) >
<!ATTLIST j:compare name    ID    #REQUIRED
                  input   IDREF #REQUIRED
                  output   IDREF #IMPLIED>

Example de fichier de configuration

Cette annexe contient le fichier de configuration complet pour l'application de démonstration de la version 1.0 du framework.

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE mapper-config SYSTEM "classpath://dtd/framework-ldap.dtd" >

<mapper-config
	xmlns="http://norsys.fr/framework-ldap/configuration.dtd">
	<sub-config
		name="http://norsys.fr/framework-ldap/jndi-configuration.dtd"
		class="fr.norsys.mapper.jndi.JNDIXMLConfigurator" />

	<var name="user" value="cn=admin,ou=users,o=services" />
	<var name="pass" value="12345678" />
	<var name="url" value="ldap://130.81.9.192:389" />

	<var name="regval" value="[0-9]{10}"/>
	<var name="regemail" value="[a-zA-Z0-9._%-]+\@[a-zA-Z0-9._%-]+\.[a-zA-Z]{2,4}"/>

	<!-- basic sample XML configuration file for LDAP -->
	<!-- this file is used to test source configuration and -->
	<!-- search operations on a genuine LDAP source         -->
	<j:jndi-mapper
		xmlns:j="http://norsys.fr/framework-ldap/jndi-configuration.dtd">

		<j:regex key="mobileNumber" value="#{regval}" ignoreNull="true"/>
		<j:regex key="email" value="#{regemail}" ignoreNull="true"/>

		<!-- an input map definition  -->
		<j:map name="input-user-search">
			<j:attr key="firstName" val="" />
			<j:attr key="lastName" val="" />
			<j:attr key="site" val="" />
		</j:map>
		<j:map name="input-id">
			<j:attr key="id" val="" />
		</j:map>
		<j:map name="input-user">
			<j:attr key="id" val="" />
			<j:attr key="lastName" val="" />
			<j:attr key="firstName" val="" />
			<j:attr key="site" val="" />
			<j:attr key="fullName" val="" />
			<j:attr key="employeeType" val="" />
			<j:attr key="internetProfile" val="" />
			<j:attr key="civility" val="" />
			<j:attr key="email" val="" />
			<j:attr key="company" val="" />
			<j:attr key="function" val="" />
			<j:attr key="telephoneNumber" val="" />
			<j:attr key="mobileNumber" val="" />
			<j:attr key="faxNumber" val="" />
			<j:attr key="floor" val="" />
			<j:attr key="office" val="" />
			<j:attr key="ip" val="" />
			<j:attr key="service" val="" />
			<j:attr key="login" val="" />
			<j:attr key="objectClass" val="" />
		</j:map>
		<j:map name="input-nextid">
			<j:attr key="nextId" val="" />
		</j:map>

		<!-- an output map definition -->
		<j:map name="output-user-search">
			<j:attr key="cn" val="id" />
			<j:attr key="fullName" val="fullName" />
			<j:attr key="oqubeattsite" val="site" />
		</j:map>

		<j:map name="output-user">
			<j:attr key="cn" val="id" />
			<j:attr key="fullName" val="fullName" />
			<j:attr key="oqubeattsite" val="site" />
			<j:attr key="oqubeattcivilite" val="civility" />
			<j:attr key="givenName" val="firstName" />
			<j:attr key="sn" val="lastName" />
			<j:attr key="mail" val="email" />
			<j:attr key="employeeType" val="employeeType" />
			<j:attr key="oqubeattsociete" val="company" />
			<j:attr key="oqubeattfonction" val="function" />
			<j:attr key="oqubeattprofilmail" val="internetProfile" />
			<j:attr key="telephoneNumber" val="telephoneNumber" />
			<j:attr key="mobile" val="mobileNumber" />
			<j:attr key="facsimileTelephoneNumber" val="faxNumber" />
			<j:attr key="siteLocation" val="floor" />
			<j:attr key="roomNumber" val="office" />
			<j:attr key="oqubeattiplan" val="ip" />
			<j:attr key="oqubeattservice" val="service" />
			<j:attr key="uid" val="login" />
			<j:attr key="objectclass" val="objectClass" />
		</j:map>
		<j:map name="output-yearnextid">
			<j:attr key="oqubeattnum" val="year"/>
			<j:attr key="oqubeattnextnb" val="nextId"/>
		</j:map>
		<j:map name="output-nextid">
			<j:attr key="oqubeattnextnb" val="nextId"/>
		</j:map>
		<!-- non emptiness of pid validation rule -->

		<!-- a first source -->
		<j:source name="ldifsource" pooled="true" >
			<!-- configuration for this source (ie. connection) -->
			<j:config>
				<j:property key="java.naming.factory.initial"
					val="com.sun.jndi.ldap.LdapCtxFactory" />
				<j:property key="java.naming.provider.url" val="${url}" />
				<j:property key="java.naming.security.credentials"
					val="${pass}" />
				<j:property key="java.naming.security.principal"
					val="${user}" />
				<j:property key="jndi.connection.pool.minPoolSize"
					val="10" />
				<j:property key="jndi.connection.pool.maxPoolSize"
					val="30" />
				<j:property key="jndi.connection.pool.timeout"
					val="1000" />
			</j:config>

			<!-- searchUsersByCriteria -->
			<j:search input="input-user-search"
				output="output-user-search" name="searchUsersByCriteria">
				<j:filter><![CDATA[(&(objectclass=person)(sn=${lastName})(givenName=${firstName})(oqubeattsite=${site}))]]>
				</j:filter>
				<j:root>ou=Personnes,o=oqube</j:root>
				<j:searchScope>1</j:searchScope>
				<j:derefLinkFlag>true</j:derefLinkFlag>
				<j:returningObjFlag>true</j:returningObjFlag>
				<j:countLimit>0</j:countLimit>
			</j:search>
			<!-- getUser -->
			<j:search input="input-id" output="output-user" name="getUser">
				<j:filter><![CDATA[(&(objectclass=oqubeobjperson)(cn=${id}))]]></j:filter>
				<j:root>ou=Personnes,o=oqube</j:root>
				<j:searchScope>1</j:searchScope>
				<j:derefLinkFlag>true</j:derefLinkFlag>
				<j:returningObjFlag>true</j:returningObjFlag>
				<j:countLimit>0</j:countLimit>
			</j:search>
			<!-- getNextId -->
			<j:search input="" output="output-yearnextid" name="getNextId">
				<j:filter>(objectclass=*)</j:filter>
				<j:root>ou=Personnes,o=oqube</j:root>
				<j:searchScope>0</j:searchScope>
				<j:derefLinkFlag>true</j:derefLinkFlag>
				<j:returningObjFlag>true</j:returningObjFlag>
				<j:countLimit>0</j:countLimit>
			</j:search>
			<!--  addUser -->
			<j:add input="input-user" name="addUser" output="output-user">
				<j:root>cn=${id},ou=Personnes,o=oqube</j:root>
			</j:add>
			<!--  modifyUser -->
			<j:modify input="input-user" name="modifyUser" output="output-user">
				<j:root>cn=${id},ou=Personnes,o=oqube</j:root>
				<j:operation op="modify" name="fullName">${fullName}</j:operation>
				<j:operation op="modify" name="site">${site}</j:operation>
				<j:operation op="modify" name="civility">${civility}</j:operation>
				<j:operation op="modify" name="firstName">${firstName}</j:operation>
				<j:operation op="modify" name="lastName">${lastName}</j:operation>
				<j:operation op="modify" name="fullName">${fullName}</j:operation>
				<j:operation op="modify" name="email">${email}</j:operation>
				<j:operation op="modify" name="employeeType">${employeeType}</j:operation>
				<j:operation op="modify" name="company">${company}</j:operation>
				<j:operation op="modify" name="function">${function}</j:operation>
				<j:operation op="modify" name="telephoneNumber">${telephoneNumber}</j:operation>
				<j:operation op="modify" name="mobileNumber">${mobileNumber}</j:operation>
				<j:operation op="modify" name="faxNumber">${faxNumber}</j:operation>
				<j:operation op="modify" name="floor">${floor}</j:operation>
				<j:operation op="modify" name="office">${office}</j:operation>
				<j:operation op="modify" name="ip">${ip}</j:operation>
				<j:operation op="modify" name="service">${service}</j:operation>
				<j:operation op="modify" name="login">${login}</j:operation>
			</j:modify>
			<!--  modifyNextId -->
			<j:modify input="input-nextid" name="modifyNextId" output="output-nextid">
				<j:root>ou=Personnes,o=oqube</j:root>
				<j:operation op="modify" name="nextId">${nextId}</j:operation>
			</j:modify>
			<!--  deleteUser -->
			<j:delete input="" name="deleteUser" output="">
				<j:root>cn=${id},ou=Personnes,o=oqube</j:root>
			</j:delete>
		</j:source>

	</j:jndi-mapper>

</mapper-config>

1. voir Spécifications SPI pour le détail et conception/config/jndi-configuration.dtd

2. on notera que ce mécanisme peut être utilisé pour autre chose que des variables, par exemple pour modulariser la structure des sources et des mappers.

3. voir la description de la classe Properties dans la Javadoc

5. voir les spécifications relatives à la norme X.500 pour plus de détails : http://archive.dante.net/np/ds/osi/9594-1-X.500.A4.ps

6. voir http://java.sun.com/products/jndi/ pour plus de détails concernant cette spécification.

7. n'y-a-t'il pas des possibilités de problème avec l'encodage des caractères ?

8. voir la spécification LDAPv3 RFC 2251 et RFC 3377.