Ce chapitre décrit les étapes principales pour constituer une branche d'utilisateurs, à partir de données à disposition en base de données. Ce chapitre s'appuie fortement sur la mise en pratique d'un exemple complet.
Objectifs
L'objectif ici est de pouvoir synchroniser des données utilisateur extraites d'une base de données vers une branche LDAP recensant l'ensemble des utilisateurs du SI. Dans l'exemple qui suit, il s'agit d'inclure uniquement les salariés de l'entreprise.
La petite particularité réside dans le fait que seul l'attribut LDAP mail est effectivement multi-valué en base de données. Pou SalarieJDBCService.getInstance().getSalarieMails(id) ;
r cela, les mails des utilisateurs sont stockés dans une table particulière en base de données.
Définition de classes d'objet LDAP
Il ne s'agit pas ici de détailler la réflexion menant à la création de classes d'objet LDAP particulières. Il est supposé que la classe d'objet est créée.
L'exemple suivant est la définition complète au format LDAP (RFC 2252) d'une classe d'objet structurelle nommée salarie. Cette classe d'objet hérite d'une classe d'objet structurelle utilisateur, qui elle-même hérite d'une classe d'objet structurelle compte. Enfin, cette dernière est dérivée de la classe d'objet structurelle inetOrgPerson.
objectClass ( objectclassOid:1 NAME 'compte'
DESC 'Generalized account objectClass'
SUP inetOrgPerson
STRUCTURAL
MAY statut )objectClass ( objectclassOid:1.1 NAME 'utilisateur'
DESC 'Common user account'
SUP compte
STRUCTURAL
MAY ( civilite $ dateDebutContrat $ dateFinContrat ) )objectClass ( objectclassOid:1.1.1 NAME 'salarie'
DESC 'Employee account'
SUP utilisateur
STRUCTURAL
MAY ( assistant $ fonction ) )
De façon générale, tous les exemples suivants vont s'appuyer sur ces définitions. Par la suite, des pistes seront abordées afin d'inclure d'autres types d'utilisateur.
Quelques petites remarques supplémentaires peuvent être appréciables :
statut est un attribut LDAP mono-valué indiquant le statut d'un compte, c'est un entier ;
civilite est un attribut LDAP mono-valué indiquant un code correspondant à la civilité de la personne (équivalent à l'attribut title, mais dans un format entier) ;
dateDebutContrat et dateFinContrat sont des attributs dont la valeur est une date au format LDAP – comme définit dans la RFC 2252 au chapitre 5.1.2 – avec une petite particularité : ce n'est pas un attribut système et l'utilisateur peut en changer la valeur ;
assistant est un attribut LDAP multi-valué indiquant un DN d'un autre utilisateur ;
fonction est un attribut LDAP multi-valué indiquant la fonction d'un utilisateur au sein d'une entreprise.
Auto-génération des classes JAVA
Une fois ces définitions incluses dans le schéma LDAP, il faut alors générer les classes JAVA de base permettant de manipuler les données. Les commandes suivantes y aident grandement :
$ ant -DocName="compte" generator
[...]
$ ant -DocName="utilisateur" generator
[...]
$ ant -DocName="salarie" generator
[...]
Comme indiqué dans le chapitre 2, les fichiers générés sont disponibles dans le répertoire src/impl, dans les sous-paquetages beans, jndi, objects, objects.flat, persistance.xml et service.
Attention, la génération doit se faire dans l'ordre, c'est à dire qu'une classe d'objet héritant d'une autre classe d'objet non générée ne pourra être elle-même générée – problème de compilation.
Configuration générale
Au niveau de la configuration, l'outil en dispose de deux types bien distincts :
Une configuration générale à base de fichiers, comme tout logiciel classique, permettant de personnaliser le produit. On y retrouve les configurations d'accès aux différents référentiels de données, la gestion des traces, etc… ;
Une configuration dîtes « logicielle », qui nécessite l'intervention du développeur. Cette configuration paramètre de façon générale le comportement de l'outil.
Configuration des fichiers
Voir début de chapitre 2.2.
Configuration « logicielle »
Il existe une classe JAVA disponible dans le répertoire src/app, sous le nom de Configuration – et plus précisément org.linagora.ldap.lsc.Configuration. Pour des raisons historiques, et de facilités de manipulation, cette classe définie des constantes pour chaque branche LDAP.
Ici, il s'agit d'y définir une nouvelle branche qui va accueillir les utilisateurs :
public final class Configuration {
[...]
public static final String DN_USERS =
Configuration.getString("dn.users", "ou=ComptesUtilisateurs");
[...]
Adaptation du comportement
Désormais, il faut adapter le code pour chacun des fichiers auto-générés durant la section 3.2.
Classes JAVA modélisant les classes d'objet LDAP
Les classes JAVA sont dans le paquetage org.linagora.ldap.lsc.objects du répertoire src/impl. Dans ce paquetage, trois classes supplémentaires sont désormais présentes : compte.java, utilisateur.java et salarie.java. En tout logique, les classes d'objets respectent l'héritage de classe. Ces classes d'objets possèdent toutes des « getteurs » et des « setteurs » afin de récupérer des valeurs, ou d'en positionner, pour des attributs particuliers.
Par exemple, la classe compte.java est de la forme :
package org.linagora.ldap.lsc.objects;public class compte extends inetOrgPerson { protected String statut; public compte() {
super();
objectClass.add("compte");
} public String getStatut() { return statut; }
public void setStatut(String value) { this.statut = value; }
}La classe utilisateur.java est de la forme :
package org.linagora.ldap.lsc.objects;public class utilisateur extends compte { protected String civilite;
protected String dateDebutContrat;
protected String dateFinContrat; public utilisateur() {
super();
objectClass.add("utilisateur");
} public String getCivilite() { return civilite; }
public String getDateDebutContrat() { return dateDebutContrat; }
public String getDateFinContrat() { return dateFinContrat; } public void setCivilite(String value) { this.civilite = value; }
public void setDateDebutContrat(String value) {
this.dateDebutContrat = value;
}
public void setDateFinContrat(String value) {
this.dateFinContrat = value;
}
}La classe salarie.java est alors :
package org.linagora.ldap.lsc.objects;import java.util.List;public class salarie extends utilisateur { protected List assistant;
protected List fonction; public lapSalarie() {
super();
objectClass.add("salarie");
} public List getAssistant() { return assistant; }
public List getFonction() { return fonction; }
public void setAssistant(List values) { this.assistant = values; }
public void setFonction(List values) { this.fonction = values; }
}Ces classes d'objet ne contiennent pas de code de transformation de données.
Une partie de l'outil s'appuie essentiellement sur des objets dits « flat ». Ce sont ces objets qui vont servir dans un premier temps, pour le moteur générique, à stocker les données en provenance de la base de données. Ces objets respectent aussi l'héritage de classe, mais ne se soucient plus des types des données. Ils sont à disposition dans le paquetage org.linagora.ldap.lsc.objects.flat. Ainsi, chaque attribut de classe est une chaîne de caractères, systématiquement.
Il faut respecter une certaine nomenclature dans le nom des classes JAVA. Ainsi, à la classe JAVA compte.java est associée une classe JAVA « flat » nommée fCompte.java. Celle-ci, toujours par l'exemple, est de la forme :
package org.linagora.ldap.lsc.objects.flat;public class fCompte extends fInetOrgPerson { protected String statut; public String getStatut() { return statut; }
public void setStatut(String value) { this.statut = value; }
}La classe fUtilisateur.java est donc :
package org.linagora.ldap.lsc.objects.flat;public class fUtilisateur extends fCompte { protected String civilite;
protected String dateDebutContrat;
protected String dateFinContrat; public String getCivilite() { return lapCiviliteCode; }
public String getDateDebutContrat() { return dateDebutContrat; }
public String getDateFinContrat() { return lapDateFinContrat; } public void setCivilite(String value) { this.civilite = value; }
public void setDateDebutContrat(String value) {
this.dateDebutContrat = value;
}
public void setDateFinContrat(String value) {
this.dateFinContrat = value;
}
}Et enfin, la classe fSalarie.java contient le code suivant :
package org.linagora.ldap.lsc.objects.flat;public class fSalarie extends fUtilisateur { protected String assistant;
protected String fonction; public String getAssistant() { return assistant; }
public String getFonction() { return fonction; } public void setAssistant(String value) { this.assistant = value; }
public void setFonction(String value) { this.fonction = value; }
}Ces classes d'objet ne contiennent pas de codes de transformation de données.
Il est important de remarquer que les objets « flat » ne disposent pas de constructeur particulier. En effet, ces objets doivent contenir des données extraites de la base de données source, et n'ont, par conséquent, aucun pouvoir LDAP.
Les BEANs
Les BEANs sont les objets qui vont permettre de mapper les données en provenance de la base de données vers les entrées de l'annuaire. Ce sont eux qui vont garantir les bons types, voir formater les données – exemple : les différences de format de date sont gérées ici.
Il faut respecter une certaine nomenclature dans le nom d'une classe JAVA introduisant un BEAN. Ainsi, à la classe JAVA compte.java est associée une classe JAVA BEAN nommée compteBean.java. La hiérarchie est aussi ici respectée : compteBean.java hérite de InetOrgPersonBean.java, etc… Un BEAN est un objet de type AbstractBean et implémentant l'interface IBean – disponibles au sein du paquetage org.linagora.ldap.lsc.beans du répertoire src/app.
Une telle classe comporte une partie statique, et une partie plus flexible. La classe de l'exemple ci-dessous, nommée compteBean.java, possède une partie statique, à adapter ensuite au nom de la classe d'objet de base :
package org.linagora.ldap.lsc.beans;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import javax.naming.NamingException;import org.linagora.ldap.lsc.jndi.JndiModifications;
import org.linagora.ldap.lsc.objects.compte;
import org.linagora.utils.CharacterUnacceptedException;
import org.linagora.utils.Filters;public class compteBean extends inetOrgPersonBean implements IBean { static {
localMethods = new HashMap<String,Method>();
} public static compteBean getInstance(compte myclass)
throws IllegalArgumentException, IllegalAccessException,
InvocationTargetException, CharacterUnacceptedException,
NamingException {
compteBean bean = new compteBean() ;
AbstractBean.mapper(compteBean.class, localMethods, bean, myclass);
bean.generateDn();
return bean;
} public compteBean() {
super();
}
public JndiModifications[] checkDependenciesWithcompte(
compte soc, JndiModifications jm) {
return new JndiModifications[] {};
}
}Ensuite, au sein de cette classe viennent toutes les méthodes permettant le mapping de données, voir la génération. Deux types de méthodes sont distinguables :
Les méthodes de mapping :
Elle permettent de réaliser des opérations de transformation sur les données. De façon obligatoire et générale, le nom d'une telle méthode est constitué du préfixe « map » puis du nom de l'attribut LDAP, la première lettre étant en majuscule ;
Les méthodes de génération :
Elles permettent de forcer la génération d'une valeur d'un attribut, quand l'attribut pourrait ne rien contenir depuis la mapping. De façon obligatoire et générale, le nom d'une telle méthode est constitué du préfixe « generate » puis du nom de l'attribut LDAP, la première lettre étant en majuscule.
Dans l'exemple ci-dessus, il faut alors rajouter les deux méthodes suivantes pour la terminer parfaitement :
public static void mapStatut(compte soc, IBean doc, String value)
throws NamingException, CharacterUnacceptedException {
if (value != null && value.trim().length() > 0) {
mapString(doc, "statut", Filters.filterNumber(value));
}
} public static void generateStatut(compte soc, IBean doc) throws
NamingException {}Les méthodes de mapping sont susceptibles de soulever des exceptions de type NamingException ou CharacterUnacceptedException. Dans le premier cas, l'exception est généralement due au fait que l'attribut visé – inscrit en dur dans le code ci-dessus, ici statut – n'a aucune signification dans le schéma LDAP pour la classe d'objet utilisée. Quant au second cas, cela provient obligatoirement du filtre appliqué sur la données qui soulève une exception CharacterUnacceptedException.
Comme c'est le cas dans l'exemple ci-dessus, les méthodes de mapping « peuvent » contenir des filtres sur les données. Ceci est recommandé afin de se prémunir au plus tôt des erreurs provenant de l'annuaire. Ici, c'est l'outil qui indique s'il existe des erreurs dans les données où non, et non l'annuaire – cela évite du flux réseau gâché. Le chapitre 5 indique plus de détail concernant les filtres à disposition.
Si les méthodes de mapping et de génération ne sont pas ajoutées à la classe, les valeurs ne seront tout simplement pas ajoutées dans l'annuaire. Aussi, attention à l'héritage de classe qui permet de garder les méthodes de mapping et de génération des classes supérieures.
Pour compléter l'exemple, il faut adapter la classe utilisateurBean.java comme dans l'exemple suivant.
package org.linagora.ldap.lsc.beans;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import javax.naming.NamingException;import org.linagora.ldap.lsc.jndi.JndiModifications;
import org.linagora.ldap.lsc.objects.compte;
import org.linagora.utils.CharacterUnacceptedException;
import org.linagora.utils.Filters;public class utilisateurBean extends compteBean implements IBean { static {
localMethods = new HashMap<String,Method>();
} public static utilisateurBean getInstance(utilisateur myclass)
throws IllegalArgumentException, IllegalAccessException,
InvocationTargetException, CharacterUnacceptedException,
NamingException {
utilisateurBean bean = new utilisateurBean() ;
AbstractBean.mapper(utilisateurBean.class, localMethods,
bean, myclass);
bean.generateDn();
return bean;
} public utilisateurBean() {
super();
} public static void mapCivilite(utilisateur soc, IBean doc,
String value)
throws NamingException, CharacterUnacceptedException {
if (value != null && value.trim().length() > 0) {
mapString(doc, "civilite", Filters.filterNumber(value));
}
} public static void generateCivilite(utilisateur soc, IBean doc)
throws NamingException {} public static void mapDateDebutContrat(utilisateur soc, IBean doc,
String value)
throws NamingException, CharacterUnacceptedException {
if (value != null && value.trim().length() > 0) {
// La donnée extraite de la base est au format
// dd/MM/yyyy-HH:mm:ss
mapString(doc, "dateDebutContrat",
Filters.filterDate(value,"dd/MM/yyyy-HH:mm:ss")) ;
}
} public static void generateDateDebutContrat(utilisateur soc,
IBean doc)
throws NamingException {}
public static void mapDateFinContrat(utilisateur soc, IBean doc,
String value)
throws NamingException, CharacterUnacceptedException {
if (value != null && value.trim().length() > 0) {
// La donnée extraite de la base est au format
// dd/MM/yyyy-HH:mm:ss
mapString(doc, "dateFinContrat",
Filters.filterDate(value,"dd/MM/yyyy-HH:mm:ss")) ;
}
} public static void generateDateFinContrat(utilisateur soc, IBean doc)
throws NamingException {} public JndiModifications[] checkDependenciesWithutilisateur(
lapCompteUtilisateurSI soc, JndiModifications jm) {
return new JndiModifications[] {};
}
}Enfin, concernant les BEANs, la classe salarieBean.java ressemble à ceci :
package org.linagora.ldap.lsc.beans;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import javax.naming.NamingException;import org.linagora.ldap.lsc.Configuration;
import org.linagora.ldap.lsc.jndi.JndiModifications;
import org.linagora.ldap.lsc.objects.salarie;
import org.linagora.utils.CharacterUnacceptedException;
import org.linagora.utils.Filters;public class salarieBean extends utilisateurBean implements IBean { static {
localMethods = new HashMap<String,Method>();
} public static salarieBean getInstance(salarie myclass)
throws IllegalArgumentException, IllegalAccessException,
InvocationTargetException, CharacterUnacceptedException,
NamingException {
salarieBean bean = new salarieBean() ;
AbstractBean.mapper(salarieBean.class, localMethods,
bean, myclass);
bean.generateDn();
return bean;
} public salarieBean() {
super();
} public static void mapAssistant(salarie soc, IBean doc, List values)
throws NamingException, CharacterUnacceptedException {
if (values != null && values.size() > 0) {
Vector<String> v = new Vector<String>() ;
Iterator valuesIter = values.iterator() ;
while (valuesIter.hasNext()) {
String value = (String) valuesIter.next() ;
if (value != null && value.trim().length() > 0) {
v.add( "uid=" + value + ","
+ Configuration.DN_USERS
+ "," + Configuration.DN_REAL_ROOT );
}
}
mapList(doc, "assistant", v) ;
}
}
public static void generateAssistant(salarie soc, IBean doc)
throws NamingException {} public static void mapFonction(salarie soc, IBean doc, List values)
throws NamingException, CharacterUnacceptedException {
if (values != null && values.size() > 0) {
Vector<String> v = new Vector<String>() ;
Iterator valuesIter = values.iterator() ;
while (valuesIter.hasNext()) {
String value = (String) valuesIter.next() ;
if (value != null && value.trim().length() > 0) {
v.add(Filters.filterString(value)) ;
}
}
mapList(doc, "fonction", v) ;
}
} public static void generateFonction(salarie soc, IBean doc)
throws NamingException {} public JndiModifications[] checkDependenciesWithsalarie(salarie soc,
JndiModifications jm) {
return new JndiModifications[] {};
}
}Cette classe JAVA introduit une nouvelle notion : le traitement des valeurs multiples pour un même attribut. Du fait même que la source soit une base de données, un champs en base ne peut pas contenir plusieurs valeurs, suivant la notion introduite par les attributs multi-valués LDAP. Par soucis de clarté, les valeurs multiples sont traitées ici par des List, mais il faut bien voir que ces listes ne contiennent qu'une unique valeur, si aucun traitement n'intervient en amont.
L'exemple de la classe salarieBean.java montre clairement qu'il est tout à fait possible de mapper les données. Ainsi, comme l'attribut assistant de la classe d'objet LDAP salarie attend un DN, la méthode de mapping mapAssistant(...) correspondante permet de fabriquer ce DN en fonction de critères spécifiques.
Les classes du service JNDI
Ces classes permettent d'interroger l'annuaire avec des filtres particuliers. Elles sont disponibles dans le paquetage org.linagora.ldap.lsc.jndi. Il n'est pas nécessaire de toutes les implémenter, juste celles correspondant aux classes d'objets qui seront réellement utilisées. Par exemple, les entrées dans l'annuaire seront de classe structurelle salarie. Ainsi, il s'agira de manipuler ici seulement des salariés, et non globalement des utilisateurs ou des comptes. Malgré que l'héritage de classe entre en jeu dans les paquetages org.linagora.ldap.lsc.objects et org.linagora.ldap.lsc.objects.flat, seule la classe JNDI des salaries sera nécessaire, et n'a aucunement besoin d'un quelconque héritage particulier.
Il faut respecter une certaine nomenclature dans le nom des classes JAVA. Ainsi, à la classe JAVA salarie.java est associée une classe JAVA JNDI nommée SalarieJNDIService.java.
Cette classe est de la forme :
package org.linagora.ldap.lsc.jndi;import javax.naming.NamingException;
import org.linagora.ldap.lsc.Configuration;
import org.linagora.ldap.lsc.beans.salarieBean;
import org.linagora.ldap.lsc.jndi.JndiServices;public class SalarieJNDIService { private static final SalarieJNDIService instance
= new SalarieJNDIService(); public SalarieJNDIService() {} public static SalarieJNDIService getInstance() { return instance; } public salarieBean getSalarie(String id) throws NamingException {
return (salarieBean) salarieBean.getInstance(
JndiServices.getInstance().getEntry(
Configuration.DN_USERS, "uid=" + id
),
Configuration.DN_USERS,
salarieBean.class
);
}
}De façon générale, les noms de méthodes contiennent obligatoirement le nom de la classe source – ici salarie. Ensuite, la casse est très importante, la méthode getSalarie(String id) n'est pas la méthode getsalarie(String id). Ceci est du entièrement au moteur générique, qui fait entre autre de l'introspection de classes en fonction des noms de base des classes d'objet LDAP.
Il y a donc deux méthodes précisément nommées :
<NomClasseObjectLDAP>JNDIService() ;
get<NomClasseObjectLDAP>(String id).
Les prototypes de fonction doivent être identiques au format de l'exemple ci-dessus.
Dans l'exemple ci-dessus, la méthode importante est la méthode getSalarie(String id) qui retourne obligatoirement le BEAN correspondant à la classe d'objet LDAP de base – ici salarieBean.
Cette méthode prend trois arguments :
Une entrée LDAP retournée par le service JNDI du moteur, identifiée par la branche de recherche Configuration.DN_USERS et un filtre particulier « uid=id » ;
La branche LDAP contenant au final l'objet LDAP – Configuration.DN_USERS ;
Le type de l'objet BEAN généré – salarieBean.class.
Les classes du service JDBC
Ces classes vont permettre d'interroger la base de données afin de récupérer les informations concernant les utilisateurs. Comme pour les autres classes, à chaque classe d'objet LDAP correspond une classe JAVA de service JDBC. Celles-ci sont disponibles au sein du paquetage org.linagora.ldap.lsc.service dans le répertoire src/impl. De façon identique aux classes de service JNDI, il n'est pas nécessaire d'implémenter les classes JDBC dont les objets ne seront jamais traités directement. Ainsi, par exemple, seule la classe d'objet SalarieJDBCService.java sera créée, car il n'est pas nécessaire d'avoir recourt à UtilisateurJDBCService.java et autres classes pouvant se calquer sur les classes d'objet LDAP.
Plusieurs possibilités sont envisageables afin de créer un tel service JDBC. Néanmoins, l'outil fournit un moyen simple afin d'y parvenir. Il s'agit de créer une classe s'appuyant sur un fichier XML contenant les requêtes SQL. Le traitement se fait alors via des librairies externes (IBATIS par exemple) pour retourner un résultat facile à travailler.
Il faut respecter une certaine nomenclature dans le nom des classes JAVA de service JDBC. Ainsi, à la classe JAVA salarie.java est associée une classe JAVA JDBC nommée SalarieJDBCService.java. Pour faire simple, voici à quoi ressemble une telle classe auto-générée :
package org.linagora.ldap.lsc.service;import java.sql.SQLException;
import java.util.List;import org.apache.log4j.Logger;
import org.linagora.ldap.lsc.objects.flat.fSalarie;
import org.linagora.ldap.lsc.persistence.DaoConfig;import com.ibatis.sqlmap.client.SqlMapClient;public class SalarieJDBCService { private Logger LOGGER = Logger.getLogger(SalarieJDBCService.class);
private static final SalarieJDBCService INSTANCE
= new SalarieJDBCService();
private SqlMapClient sqlMapper; public SalarieJDBCService() {
sqlMapper = DaoConfig.getSqlMapClient();
} public static SalarieJDBCService getInstance() { return INSTANCE; } public final fSalarie getSalarie(final String id) {
try {
return (fSalarie) sqlMapper.queryForObject(
"getSalarie", id);
} catch (SQLException e) {
LOGGER.warn("Unable to found salarie with id=" + id
+ " (" + e + ")", e);
}
return null;
} public final List getSalarieList() {
try {
return sqlMapper.queryForList("getSalarieList", null);
} catch (SQLException e) {
LOGGER.warn("Unable to return salarie list (" + e
+ ")", e);
}
return null;
}
}Basiquement, une classe de service JDBC comprend deux méthodes essentielles. La première doit retourner, après traitement SQL via les librairies IBATIS grâce à un identifiant, un objet « flat » – associé à la classe d'objet LDAP de base traitée – contenant des données en base correspondant à une entrée – ici un salarié. La seconde méthode permet de retourner, quant à elle, l'ensemble des identifiants des utilisateurs sous forme de liste. Ceux-ci seront utilisés afin de récupérer les données pour chaque utilisateur grâce à la première méthode.
Le nom de la première méthode est obligatoirement constitué du préfixe « get » suivi du nom de la classe d'objet LDAP associée, la première lettre étant en majuscule. Le nom de la seconde est obligatoirement constitué du préfixe « get », suivi du nom de la classe d'objet LDAP associée, la première lettre étant en majuscule, suivi du suffixe « List ».
Néanmoins, pour fonctionner, une telle classe doit être obligatoirement associée à un fichier XML de requêtes SQL, géré par les bibliothèques IBATIS.
Les fichiers XML de requêtes SQL
Le principe reste le même, à un service JDBC correspond un fichier XML de requêtes SQL. Ce fichier XML est automatiquement généré lors de l'auto-génération, comme toutes les classes et fichiers abordés jusqu'à présent. Les fichiers XML sont disponibles au sein du paquetage org.linagora.ldap.lsc.persistence.xml, dans le répertoire src/impl.
Un tel fichier XML est très sensible à la casse. Il est découpé en trois grandes parties :
La définition de l'espace de nom unique et de la classe « flat » associée ;
La définition du mapping des champs SQL vers les attributs LDAP ;
L'ensemble des requêtes SQL possibles et implémenter par le service JDBC associé.
L'espace de nom est obligatoirement le nom de la classe d'objet LDAP sous-jacente à l'entrée à traiter, sachant que la première lettre est en majuscule. Par exemple, pour la classe d'objet LDAP salarie, l'espace de nom XML sera « Salarie ». Cet espace de nom doit être unique dans le paquetage org.linagora.ldap.lsc.persistence.xml. Ensuite, la définition de la classe JAVA « flat » adéquate est positionnée au sein de la balise «
». Cette balise contient deux paramètres : le paramètre « alias » contenant exactement le nom de la classe d'objet LDAP, et le paramètre « type » contenant le nom complet – le nom du paquetage suivi du nom de la classe, séparé par le caractère « . » – de la classe JAVA « flat » associée.
La définition du mapping des champs SQL vers les attributs LDAP est représentée sous forme balisée. La balise XML générale se nomme « resultMap », elle comprend deux paramètres : le paramètre « id » contenant exactement le nom de la classe d'objet LDAP associée suivi du suffixe « Result », et le paramètre « class » contenant exactement le nom de la classe d'objet LDAP associée. Entre la balise ouvrante et la balise fermante se trouvent un ensemble de balises « result » contenant elles-mêmes deux paramètres : le paramètre « property » ayant pour valeur exactement le nom d'un attribut LDAP contenu dans la classe d'objet LDAP courante, et le paramètre « column » contenant le nom de la colonne associée à l'attribut dans la table SQL.
Les requêtes SQL sont définies dans une troisième partie, chaque requête SQL étant balisée par le tag « select ». Celui-ci contient quant à lui trois paramètres distincts : le paramètre « id » contenant exactement le nom de la méthode de la classe de service JDBC qui utilise cette requête, ainsi que deux autres paramètres qui différent en fonction du type de requête SQL effectuée, à savoir requête pour récupérer les données d'un utilisateur, ou requête pour récupérer un ou plusieurs identifiants en base. Dans le premier cas, les deux paramètres sont : le paramètre « resultMap » contenant le nom de la map précédemment définie (cf : définition du mapping des champs SQL vers les attributs LDAP), et le paramètre « parameterClass » fixé à « string » obligatoirement. Dans le second cas, il y a un unique paramètre nommé « resultClass » contenant obligatoirement la valeur « string ».
Pour résumer par l'exemple, voici le contenu du fichier Salarie.xml :
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd"><sqlMap namespace="LapSalarie"> <typeAlias alias="lsalarie"
type="org.linagora.ldap.lsc.objects.flat.fSalarie" /> <resultMap id="salarieResult" class="salarie">
<result property="uid" column="COMPTE_UTILISATEUR_ID"/>
<result property="sn" column="NOM"/>
<result property="cn" column="NOM_COMPLET"/>
<result property="userPassword" column="MOT_DE_PASSE"/>
<result property="statut"
column="STATUT_COMPTE_UTILISATEUR_ID"/>
<result property="givenName" column="PRENOM"/>
<result property="civilite" column="CIVILITE_ID"/>
<result property="dateDebutContrat"
column="DATE_DEBUT_CONTRAT"/>
<result property="dateFinContrat" column="DATE_FIN_CONTRAT"/>
<result property="telephoneNumber"
column="NUMERO_TELEPHONE_BUREAU"/>
<result property="assistant" column="ASSISTANT_ID"/>
<result property="fonction" column="TITRE_RH"/>
</resultMap> <select id="getSalarie" resultMap="salarieResult"
parameterClass="string">
select
C.COMPTE_UTILISATEUR_ID,
C.NOM,
C.NOM_COMPLET,
C.MOT_DE_PASSE,
C.STATUT_COMPTE_UTILISATEUR_ID,
C.PRENOM,
C.CIVILITE_ID,
TO_CHAR(C.DATE_DEBUT_CONTRAT, 'DD/MM/YYYY-HH24:MI:SS')
AS DATE_DEBUT_CONTRAT,
TO_CHAR(C.DATE_FIN_CONTRAT, 'DD/MM/YYYY-HH24:MI:SS')
AS DATE_FIN_CONTRAT,
C.NUMERO_TELEPHONE_BUREAU,
S.ASSISTANT_ID,
S.TITRE_RH
from COMPTE_UTILISATEUR_SI C, SALARIE S
where C.COMPTE_UTILISATEUR_ID = S.COMPTE_UTILISATEUR_ID
and C.COMPTE_UTILISATEUR_ID = #value#
</select> <select id="getSalarieList" resultClass="string">
select C.COMPTE_UTILISATEUR_ID
from COMPTE_UTILISATEUR_SI C, SALARIE S
where C.COMPTE_UTILISATEUR_ID = S.COMPTE_UTILISATEUR_ID
</select>
</sqlMap>
Il est intéressant de souligner que l'identifiant nécessaire pour récupérer les bonnes données pour un utilisateur visé, auprès de la base de données, est annoncé par le tag particulier « #value# », à ne pas modifier.
Pour finir, comme ce sont les librairies IBATIS qui sont utilisées par le moteur générique de l'outil, un fichier d'inclusions globales est présent afin d'inclure ou non un tel fichier XML. Ce fichier est disponible dans le paquetage org.linagora.ldap.lsc.persistence.xml, dans le répertoire src/impl, sous le nom sql-map-config.xml.
Ce fichier ressemble à peu de chose près à ceci :
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd"><sqlMapConfig>
<properties resource="app/database.properties"/>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property value="${driver}" name="JDBC.Driver" />
<property value="${url}" name="JDBC.ConnectionURL" />
<property value="${username}" name="JDBC.Username"/>
<property value="${password}" name="JDBC.Password"/>
<property value="15"
name="Pool.MaximumActiveConnections"/>
<property value="15" name="Pool.MaximumIdleConnections"/>
<property value="1000" name="Pool.MaximumWait"/>
</dataSource>
</transactionManager>
<sqlMap resource="org/linagora/ldap/lsc/persistence/xml/Site.xml"/>
<sqlMap resource="org/linagora/ldap/lsc/persistence/xml/Salarie.xml"/>
</sqlMapConfig>Le point important concernent les balises « sqlMap » incluant les fichiers XML nécessaires.
Classe principale gérant la synchronisation
Pour l'exemple, il s'agit d'une simple classe nommée Synchronize.java contenant quelques lignes de code JAVA permettant d'exécuter une synchronisation. Une telle classe, généralement disponible au sein du paquetage org.linagora.ldap.lsc, dans le répertoire src/impl, est susceptible de ressembler à l'exemple ci-dessous.
package org.linagora.ldap.lsc;import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.linagora.ldap.lsc.beans.*;
import org.linagora.ldap.lsc.jndi.*;
import org.linagora.ldap.lsc.objects.*;
import org.linagora.ldap.lsc.objects.flat.*;
import org.linagora.ldap.lsc.service.*;public final class Synchronize { public static final String LOG4J_CONFIGURATION_FILE
= "app/log4j.properties";
private static Logger LOGGER
= Logger.getLogger(Synchronize.class);
private Synchronize() {
PropertyConfigurator.configure(LOG4J_CONFIGURATION_FILE);
} private void launch(String args[]) {
Iterator idsSalaries =
SalarieJDBCService.getInstance().getSalarieList().iterator();
while (idsSalaries.hasNext()) {
synchronizeSalarie((String) idsSalaries.next()) ;
}
} private static void synchronizeSalarie(final String id) {
try {
fSalarie fSalarie =
SalarieJDBCService.getInstance().getSalarie(id);
salarie salarie = new salarie() ;
salarie.setUpFromFlatObject(fSalarie) ;
salarieBean jdbcObj =
salarieBean.getInstance(salarie);
salarieBean jndiObj =
SalarieJNDIService.getInstance().getSalarie(id); JndiModifications jm =
BeanComparator.calculateModifications(jdbcObj,
jndiObj, true);
if (jm != null) {
JndiServices.getInstance().apply(jm) ;
}
} catch (Exception e) {
LOGGER.error("Synchronization error for salarie with id '"
+ id + "', " + e.toString() + ")", e) ;
}
} public static void main(final String[] args) {
new Synchronize().launch(args);
}
Cas particulier : traitement d'attributs multi-valués en base
Dans certain cas, il se pourrait que des données croisées doivent être intégrées dans l'annuaire. Dans ce cas bien précis, les données en base sont relationnées par une clef unique. L'exemple sur lequel ce chapitre s'appuie fait référence à une table connexe à la table des utilisateurs, nommée « MAIL ». Cette table contient l'ensemble des mails du SI. Chaque mail est relié à un utilisateur par son identifiant unique dans la table des utilisateurs.
Ces nouvelles données ne peuvent pas être prises en compte naturellement par l'outil, et par l'implémentation de base, jusqu'alors décrite. Si le principe est assimilé, la mise en pratique devient relativement simple. Les principales étapes sont alors :
Vérifier que l'attribut LDAP à utiliser est connu ;
Créer une nouvelle requête SQL dans le fichier XML correspondant ;
Adapter le BEAN correspondant à la classe d'objet recevant les nouvelles données ;
S'insérer dans le code développé dans le chapitre 3.6 afin de fixer de nouvelles données, en provenance de la base, sur le BEAN associé à la base.
Dans les exemples qui suivent, il est supposé que l'attribut LDAP est connu. Par simplicité, et pour l'exemple, il s'agit d'utiliser l'attribut mail de la classe d'objet LDAP inetOrgPerson.
Service JDBC et Requête SQL
Le plus simple est de montrer la voie par l'exemple. L'extrait ci-dessous est la définition d'une nouvelle requête SQL permettant de récupérer l'ensemble des adresses électroniques d'un utilisateur précisé par son identifiant unique. Cette requête est introduite dans le fichier salarie.xml – paquetage org.linagora.ldap.lsc.persistance.xml.
<select id="getSalarieMails" resultClass="string">
select EMAIL
from EMAIL_COMPTE_UTILISATEUR
where COMPTE_UTILISATEUR_ID = #value#
</select>
Une fois cette requête correctement créée, il ne reste plus qu'à l'intégrer dans la classe JAVA de service JDBC. Ainsi, la classe SalarieJDBCService.java se voit compléter par le code suivant :
public final List getSalarieMails(final String id) {
try {
return sqlMapper.queryForList("getSalarieMails", id);
} catch (SQLException e) {
LOGGER.warn("Unable to return mails for user id = " + id
+ " (" + e + ")", e);
}
return null;
}
Adaptation au niveau des BEANs
La chose à vérifier est que le BEAN salarieBean.java connaît bien la méthode mapMail(...). Comme ce BEAN est régulé par une succession d'héritage, il hérite de la méthode appropriée grâce à inetOrgPersonBean.java. En effet, bien que cette méthode ne soient pas réellement écrite, le moteur générique, grâce à l'introspection de classe, est capable de fixer les valeurs.
Au contraire, si des modifications ou des vérifications s'avèrent nécessaire au niveau des mails, il suffit alors de surcharger cette méthode dans le BEAN salarieBean.java. Ce qui pourrait donner, par exemple :
public static void mapMail(salarie soc, IBean doc, List values)
throws NamingException, CharacterUnacceptedException {
if (values != null && values.size() > 0) {
Vector<String> v = new Vector<String>() ;
Iterator valuesIter = values.iterator() ;
while (valuesIter.hasNext()) {
String value = (String) valuesIter.next() ;
if (value != null && value.trim().length() > 0) {
v.add(Filters.filterMail(value)) ;
}
}
mapList(doc, "mail", v) ;
}
}Dans cet exemple, il est supposé que le filtre « filterMail » existe. Ce qui n'est pas le cas par défaut.
Compléter la classe principale de synchronisation
Ceci est l'ultime et dernière étape. Il s'agit essentiellement de fixer l'ensemble des mails récupéré dans la base de données pour un utilisateur spécifique dans l'objet JAVA LDAP. Très peu de code à écrire encore une fois, tout se passe dans la classe principale de synchronisation, nommée Synchronize.java du paquetage org.linagora.ldap.lsc dans le répertoire src/impl.
L'exemple précédent s'articulait autour d'une méthode synchronizeSalarie(...), qu'il faut compléter par deux lignes de code supplémentaires.
private static void synchronizeSalarie(final String id) {
try {
fSalarie fSalarie =
SalarieJDBCService.getInstance().getSalarie(id);
salarie salarie = new salarie() ;
salarie.setUpFromFlatObject(fSalarie) ; // Fixe les mails :
List mails =
SalarieJDBCService.getInstance().getSalarieMails(id) ;
salarie.setMail(mails) ; salarieBean jdbcObj =
salarieBean.getInstance(salarie);
salarieBean jndiObj =
SalarieJNDIService.getInstance().getSalarie(id); JndiModifications jm =
BeanComparator.calculateModifications(jdbcObj,
jndiObj, true);
if (jm != null) {
JndiServices.getInstance().apply(jm) ;
}
} catch (Exception e) {
LOGGER.error("Synchronization error for salarie with id '"
+ id + "', " + e.toString() + ")", e) ;
}
}