Stratégies d’héritage composées dans Hibernate

Baptiste Autin, le 27 septembre 2011

Le concept d’héritage n’existe pas dans le modèle entité-relation en tant que tel.
Dans une base de données relationnelle, on peut donc représenter une relation d’héritage :

  • soit avec deux tables et une association intermédiaire (joined strategy en JPA)
  • soit avec une seule table dans laquelle on regroupe les deux entités, et à laquelle on adjoint une colonne pour identifier le nom de la classe (single table strategy)

Une variante de la seconde stratégie, dite table per class, existe aussi, mais je l’ignore pour la simplicité de mon exposé (elle consiste à définir une table par classe concrète, en regroupant les attributs de la classe concrète et ceux de toutes ses classes parentes).

Chacune de ces deux techniques présente des avantages et des inconvénients, en terme de performance, de simplicité d’utilisation, d’évolution, de respect/non-respect des formes normales, etc.

Hibernate prend en charge les deux techniques, et permet même de les mixer.
C’est cette technique de mixage des stratégies d’héritage que je voudrais vous présenter maintenant.

Cas d’utilisation

Je me suis librement inspiré de cet article de bioinformatique.
L’auteur y propose une ontologie des termes cliniques associés aux néoplasmes.
Je ne m’étendrai ni sur les ontologies, ni sur les néoplasmes, il s’agit juste pour moi de partir d’un cas concret.

Du schéma de classification générale fourni par l’auteur de ce blog, j’en ai tiré le diagramme de classe suivant :

Nous supposerons que ces classes possèdent des attributs et des méthodes, que je n’ai pas indiqués.

Nous supposerons aussi que les classes de niveau 1 (Neoplasm) et 2 (NeuralCrest, GermCell, Mesoderm, Trophectoderm, Neuroectoderm et EndodermEctoderm) sont abstraites.
Et les autres concrètes.

Imaginons maintenant que nous ayons besoin d’enregistrer des instances de classes concrètes dans une base de données relationnelle.
Comment allons-nous faire ?
Et d’abord, comment allons-nous mapper ce modèle objet vers un modèle physique de tables ?

Pour commencer, il est probable que certaines de ces classes partageront des attributs communs (un nom latin, une famille, une catégorie biologique, un ensemble de vocabulaires associés, etc.)
D’autres attributs seront au contraire spécifiques aux classes de bas niveau.

Par exemple :

Créer une seule table Néoplasm regroupant tous les attributs de toutes les classes, accompagnée d’un champ “type” pour identifier le type de la classe n’est pas très élégant… même si cette solution serait vraisemblablement gagnante en terme de performances (pas de jointure à effectuer). Je dis vraisemblablement car tout dépendra de la manière dont la base de données gérera l’accroissement du nombre de colonnes avec des valeurs à NULL. L’ajout d’index pourrait également avoir un impact négatif en ce que cette indexation portera sur toute la table, et donc, potentiellement, sur des enregistrements qui ne seraient pas concernés.

Créer une table par classe, avec une association par relation d’héritage, est une solution classique… Mais avec 26 classes, il faudra prévoir 26 tables.
De plus, si les classes de bas niveau ont peu d’attributs propres, est-il vraiment nécessaire de leur consacrer une table ?
Ne pourrions-nous pas limiter un peu le nombre de tables en regroupant localement certaines classes ?

C’est là que le mixage des solutions de persistance d’Hibernate vient à notre secours.

Je vous propose la configuration suivante :
- les classes de niveau 1 et 2 seront chacune mappées vers une table individuelle
- les classes de niveau 3 sont regroupées dans la classe de niveau 2 dont elles héritent

Par exemple, les classes Molar et Tropoblast seront regroupées dans la table Tropectoderm, les classes NeuroectodermPrimitive et NeuralTube dans la table Neuroectoderm, etc.

La définition de la table Trophectoderm devient alors la suivante : Trophectoderm(id, a, b, c, d, e, f, type)
Le champ type est l’identifiant de la classe (il le faut bien, puisque nous avons regroupé certaines classes). Hibernate renseignera automatiquement ce champ avec le nom simple de la classe Java.

Mapping

Le mapping Hibernate consiste à définir au niveau de la classe Neoplasm une stratégie de “table unique” :

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

Chaque classe de niveau 2 déclare une table complémentaire :

@SecondaryTable(name="NeuralCrest", pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))

Les classes de niveau 3 utilisent à la fois la table secondaire ET la table globale (celle de Neoplasm).

Notez au passage que les annotations JPA ne sont pas automatiquement héritées, et qu’il faut donc explicitement annoter les classes de bas niveau.
Notez également qu’il est indispensable de repréciser le nom de la table secondaire dans toutes les annotations @Column qui se rapportent à une table secondaire :

@Column(table = TABLE_NAME)
private String d;

C’est pour cette raison que mon code fait usage d’une constante publique TABLE_NAME, pour concentrer en une seul endroit le nom de la table.

On prendra soin de créer une contrainte d’intégrité référentielle sur la clef primaire de chaque table vers la clef primaire de Neoplasm (autrement dit Trophectoderm.id -> Neoplasm.id, NeuralCrest.id -> Neoplasm.id, etc.). On s’assurera ainsi qu’on ne pourra supprimer un enregistrement d’une des tables secondaires sans supprimer l’enregistrement en assocation dans la table Neoplasm).

Utilisation

On peut ensuite manipuler des sous-classes de Neoplasm. Par exemple :

NeuralCrestPrimitive ncp = new NeuralCrestPrimitive();
ncp.setLatinName("Pia mater");
ncp.setA("Example #1");
ncp.setB("Example #2");
session.persist(ncp);

Hibernate exécutera alors les 2 opérations suivantes :
insert into neoplasm (latinName, type) values ('Pia mater', 'NeuralCrestPrimitive')
insert into neural_crest (a, b, id) values (Example #1, Example #2, [last inserted id])

Conclusion
Nous avons mappé notre modèle vers une base contenant 7 tables, au lieu de 26 tables avec une stratégie de mapping joined.
Une opération de lecture d’une instance de bas niveau ne nécessite qu’une jointure, une opération d’écriture 2 INSERTS/UPDATES.
C’est une solution moyenne, qui est d’autant plus intéressante :
- que la hauteur de l’arbre hiérarchique est grande (au moins 3 niveaux)
- que la structure des classes de bas-niveau diffère peu d’une classe à l’autre au sein d’une même branche
- que la structure des classes diffère beaucoup d’une branche à l’autre

Classes Java

// *********
// Top class
// *********

@Entity
@Table(name = Neoplasm.TABLE_NAME)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
abstract public class Neoplasm {

	public static final String TABLE_NAME = "neoplasm";

	@Id @GeneratedValue
	private int id;

	@Column(name = "latinName")
	private String latinName;

	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getLatinName() {
		return latinName;
	}
	public void setLatinName(String latinName) {
		this.latinName = latinName;
	}
}

// **************
// Middle classes
// **************

@Entity
@SecondaryTable(name=NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class NeuralCrest extends Neoplasm {

	public static final String TABLE_NAME = "neural_crest";

	@Column(table=TABLE_NAME)
	private String a;

	@Column(table=TABLE_NAME)
	private String b;

	public String getA() {
		return a;
	}
	public void setA(String a) {
		this.a = a;
	}
	public String getB() {
		return b;
	}
	public void setB(String b) {
		this.b = b;
	}
}

@Entity
@SecondaryTable(name = GermCell.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class GermCell extends Neoplasm {

	public static final String TABLE_NAME = "germ_cell";
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class Mesoderm extends Neoplasm {

	public static final String TABLE_NAME = "mesoderm";
}

@Entity
@SecondaryTable(name = Neuroectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class Neuroectoderm extends Neoplasm {

	public static final String TABLE_NAME = "neuroectoderm";
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class EndodermEctoderm extends Neoplasm {

	public static final String TABLE_NAME = "endoderm_ectoderm";
}

@Entity
@SecondaryTable(name = Trophectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class Trophectoderm extends Neoplasm {

	public static final String TABLE_NAME = "trophectoderm";
}

// **************
// Bottom classes
// **************

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Ectomesenchymal extends NeuralCrest {

	@Column(table=TABLE_NAME)
	private String d;

	public void setD(String d) {
		this.d = d;
	}

	public String getD() {
		return d;
	}
}

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuralCrestEndocrine extends NeuralCrest {

	@Column(table=TABLE_NAME)
	private String d;

	@Column(table=TABLE_NAME)
	private String e;

	public void setD(String d) {
		this.d = d;
	}
	public String getD() {
		return d;
	}
	public void setE(String e) {
		this.e = e;
	}
	public String getE() {
		return e;
	}
}

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuralCrestPrimitive extends NeuralCrest {

	@Column(table = TABLE_NAME)
	private String d;

	public void setD(String d) {
		this.d = d;
	}

	public String getD() {
		return d;
	}
}

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class PeripheralNervousSystem extends NeuralCrest {

	@Column(table=TABLE_NAME)
	private String c;

	@Column(table=TABLE_NAME)
	private String f;

	@Column(table=TABLE_NAME)
	private String g;

	public String getC() {
		return c;
	}
	public void setC(String c) {
		this.c = c;
	}
	public String getF() {
		return f;
	}
	public void setF(String f) {
		this.f = f;
	}
	public String getG() {
		return g;
	}
	public void setG(String g) {
		this.g = g;
	}
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Coelomic extends Mesoderm {
}

@Entity
@SecondaryTable(name = GermCell.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Differentiated extends GermCell {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class EndodermEctodermEndocrine extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class EndodermEctodermPrimitive extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Mesenchymal extends Mesoderm {
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class MesodermPrimitive extends Mesoderm {
}

@Entity
@SecondaryTable(name = Trophectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Molar extends Trophectoderm {
}

@Entity
@SecondaryTable(name = Neuroectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuralTube extends Neuroectoderm {
}

@Entity
@SecondaryTable(name = Neuroectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuroectodermPrimitive extends Neuroectoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Odontogenic extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Parenchymal extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = GermCell.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Primordial extends GermCell {
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Subelomic extends Mesoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Surface extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = Trophectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Trophoblast extends Trophectoderm {
}

Variante
Plutôt que de laisser Hibernate enregistrer textuellement le nom de la classe dans le champ type de la table Neoplasm, nous pourrions définir une entité NeoplasmType(id, name), et insérer dans Neoplasm une clef étrangère vers cette entité.
Il faudra alors penser à indiquer l’identifiant numérique correspondant dans chaque classe concrète à l’aide de l’annotation @DiscriminatorValue.

Le mapping de la classe Trophectoderm deviendrait par exemple :

@Entity
@SecondaryTable(name="trophectoderm", pkJoinColumns=@PrimaryKeyJoinColumn(name="id"))
@DiscriminatorValue("3")	// si "3" est l'identifiant numérique correspondant à "Trophectoderm" dans la table NeoplasmType
abstract class Trophectoderm extends Neoplasm {
	(...)
}

Il faudrait aussi préciser à Hibernate qu’il ne doit pas se baser sur le nom de la classe pour identifier le type, mais sur un identifiant numérique et une table annexe :

@Entity
@Table(name = Neoplasm.TABLE_NAME)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "id_type", discriminatorType = DiscriminatorType.INTEGER)
abstract public class Neoplasm {
	(...)
	@ManyToOne
	@JoinColumn(name="id_type", nullable=false, insertable=false, updatable=false)
	private NeoplasmType type;
	(...)
}

EasyMock et Autowiring

Baptiste Autin, le 27 juillet 2011

Spring Test, JUnit et EasyMock forment un trio de choix pour réaliser des tests unitaires.
Malheureusement, tester des classes qui injectent leurs collaborateurs par autowiring peut poser des difficultés lorsque les collaborateurs doivent être mockés (cas des DAO par exemple).

Normalement, injecter des simulacres en environnement Spring passe par l’utilisation de l’attribut factory-method :

<bean class="org.easymock.EasyMock" factory-method="createMock" id="ClientDAO">
	<constructor-arg value="com.example.dao.IClientDAO" />
</bean>

Supposons maintenant que la classe à tester injecte ainsi son DAO :

@Autowired
protected IClientDAO cDao;

Le démarrage du contexte risque alors d’échouer :

org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.example.dao.IClientDAO] is defined: Unsatisfied dependency of type [interface com.example.dao.IClientDAO]: expected at least 1 matching bean

En effet, Spring détermine le type d’un bean défini via une factory-method par réflexion. Par conséquent si la méthode en question déclare retourner un type Object ou un type paramétré, comme c’est le cas de la méthode statique EasyMock.createMock(), l’autowiring par type ne fonctionnera pas (même accompagné d’un @Qualifier).

Solutions:

  • Use the annotation @Resource
    @Resource(name="ClientDAO")
    protected IClientDAO cDao;

    Mais il n’est pas toujours possible/souhaitable d’utiliser cette annotation, ou de faire une référence explicite à un nom de bean.

  • Utiliser une classe adaptatrice, qui indiquera explicitement le bon type de retour (ici, IClientDAO) :
    public class MocksFactory {
    
    	public IClientDAO getClientDAO() {
    		return EasyMock.createMock(IClientDAO.class);
    	}
    }

    Malheureusement, il faudra définir autant de méthodes que d’objets à mocker, ce qui peut s’avérer fastidieux.

    Si aucune des deux précédentes solutions n’est envisageable, il reste une troisième possibilité : définir une version non-mockée du bean (ce qui permettra à l’autowiring de se faire correctement), et recourir ensuite à l’interface BeanFactoryPostProcessor pour remplacer dans le registre Spring ce bean non-mocké par la version mockée.
    Deux classes suffisent à faire cela :
    - une classe MocksFactory pour générer des mocks objects (grâce à l’interface FactoryBean)
    - une classe MocksFactoryPostProcessor à qui on transmet la liste des noms de beans devant être redéfinis

import org.easymock.classextension.EasyMock;
import org.springframework.beans.factory.FactoryBean;

public class MocksFactory implements FactoryBean {

	private Class type;

	public void setType(final Class type) {
		this.type = type;
	}

	@Override
	public Object getObject() throws Exception {
		return EasyMock.createMock(type);
	}

	@Override
	public Class getObjectType() {
		return type;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}
}
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;

public class MocksFactoryPostProcessor implements BeanFactoryPostProcessor {

	private static final Class factoryClass = MocksFactory.class;

	private String[] beanNames;

	@Override
	public void postProcessBeanFactory(final ConfigurableListableBeanFactory context) throws BeansException {

		BeanDefinitionRegistry registry = (BeanDefinitionRegistry) context;

		for (String beanName : beanNames) {

			BeanDefinition bd = registry.getBeanDefinition(beanName);

			MutablePropertyValues values = new MutablePropertyValues();
			values.addPropertyValue(new PropertyValue("type", bd.getBeanClassName()));

			RootBeanDefinition definition = new RootBeanDefinition(factoryClass, values);
			registry.registerBeanDefinition(beanName, definition);
		}
	}

	public void setBeanNames(String[] beans) {
		this.beanNames = beans;
	}
}

Enfin, dans l’applicationContext.xml, on passe la liste des noms de beans à redéfinir via la propriété beanNames, en les séparant par une virgule :

<bean id="ClientDAO" class="com.example.dao.ClientDAO"/>
<bean id="mocksFactoryPostProcessor" class="com.example.MocksFactoryPostProcessor">
	<property name="beanNames" value="ClientDAO,ProductDAO,ContractDAO"/>
</bean>

Notez que cette dernière solution impose le recours à classextension.EasyMock (qui permet de créer un mock object à partir d’une instance concrète, et non d’une interface).

Remarque : plutôt que de passer une liste exhaustive de beans comme nous le faisons, nous pourrions parcourir tous les beans du registre Spring et redéfinir tous ceux qui sont injectés par @Autowired

Sérialiser des dates avec Castor XML

Baptiste Autin, le 4 avril 2010

Castor est un outil léger et efficace pour faire du data binding Java / XML.
L’une des principales embûches sur laquelle on risque de tomber est la conversion des dates.
Castor propose un mécanisme permettant de personnaliser les opérations de sérialisation/désérialisation via une classe gestionnaire (handler). La solution la plus pratique que j’ai expérimentée consiste à étendre la classe GeneralizedFieldHandler, comme dans l’exemple ci-dessous.

Voici notre Java bean d’exemple :

package articles;

import java.util.Date;

public class Version {

	private String name;
	private Date releaseDate;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Date getReleaseDate() {
		return releaseDate;
	}
	public void setReleaseDate(Date date) {
		this.releaseDate = date;
	}
}

Le fichier de mapping Castor /articles/mapping.xml:

<?xml version="1.0"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "http://castor.org/mapping.dtd">
<mapping>
	<description></description>
	<class name="articles.Version">
		<map-to xml="version" />
		<field name="releaseDate" type="string" handler="articles.DateHandler">
			<bind-xml name="releaseDate" />
		</field>
		<field name="name" type="string">
			<bind-xml name="name" />
		</field>
	</class>
</mapping>

Le fichier de données XML d’exemple (utilisé pour l’opération d’unmarshalling) /articles/history.xml:

<?xml version="1.0" standalone="yes"?>
<versions>
	<version>
		<name>JDK 1.1.4</name>
		<releaseDate>12-09-1997</releaseDate>
	</version>
	<version>
		<name>JDK 1.1.5</name>
		<releaseDate>03-12-1997</releaseDate>
	</version>
	<version>
		<name>JDK 1.1.6</name>
		<releaseDate>typo</releaseDate>
	</version>
</versions>

La classe handler proprement dite :

package articles; 

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.mapping.GeneralizedFieldHandler;

public class DateHandler extends GeneralizedFieldHandler {

	private static final Log logger = LogFactory.getLog(DateHandler.class);

	private static final String FORMAT = "dd-MM-yyyy";

	private SimpleDateFormat formatter = new SimpleDateFormat(FORMAT);

	public Object convertUponGet(Object value) {
		if (value == null) {
			return "13-07-1974";	// default value if null date
		}
		Date date = (Date) value;
		return formatter.format(date);
	}

	public Object convertUponSet(Object value) {
		Date date = null;
		try {
			date = formatter.parse((String) value);
		}
		catch (ParseException px) {
			logger.error("Parse Exception (bad date format) : " + (String) value);
			return null;  // default value for empty/incorrect date
		}
		return date;
	}

	public Class<?> getFieldType() {
		return Date.class;
	}

	public Object newInstance(Object parent) throws IllegalStateException {
		return null;
	}
}

Et enfin le fichier exécutable de test :

package articles;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;

import org.apache.log4j.Logger;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.ValidationException;
import org.xml.sax.InputSource;

public class CastorTest {

	private static Logger logger = Logger.getLogger(CastorTest.class);
	private static Mapping mapping = getMapping();

	final private static String MAPPING_FILE = "/articles/mapping.xml";

	public static void main(String[] args) throws MappingException, MarshalException, ValidationException, IOException {

		/**
		 * Unmarshalling (XML -> Java)
		 */

		Unmarshaller unmarshaller = new Unmarshaller(ArrayList.class);
		unmarshaller.setIgnoreExtraElements(true);
		unmarshaller.setMapping(mapping);

		String dataFile = CastorTest.class.getResource("/articles/history.xml").getPath();
		InputSource source = new InputSource(dataFile);
		ArrayList<Version> list = (ArrayList<Version>) unmarshaller.unmarshal(source);

		logger.debug("Unmarshalling :");
		for (Version item : list) {
			logger.debug("Name = " + item.getName());
			logger.debug("Date = " + item.getReleaseDate());
		}

		/**
		 * Marshalling (Java -> XML)
		 */

		Marshaller marshaller = new Marshaller();
		Writer writer = new StringWriter();

		marshaller.setWriter(writer);
		marshaller.setMapping(mapping);

		Version v = new Version();
		v.setName("New name");
		v.setReleaseDate(new Date());

		marshaller.marshal(v);

		logger.debug("Marshalling :");
		logger.debug(writer.toString());
	}

	static protected Mapping getMapping() {

		String mapFile = CastorTest.class.getResource(MAPPING_FILE).getPath();
		InputSource is = new InputSource(mapFile);

		Mapping mapping = new Mapping();
		mapping.loadMapping(is);

		return mapping;
	}
}

Les traces attendues lors à l’exécution de CastorTest :

DEBUG [main] (CastorTest.java:39) [] - Unmarshalling :
DEBUG [main] (CastorTest.java:41) [] - Name = JDK 1.1.4
DEBUG [main] (CastorTest.java:42) [] - Date = Fri Sep 12 00:00:00 CEST 1997
DEBUG [main] (CastorTest.java:41) [] - Name = JDK 1.1.5
DEBUG [main] (CastorTest.java:42) [] - Date = Wed Dec 03 00:00:00 CET 1997
DEBUG [main] (CastorTest.java:41) [] - Name = JDK 1.1.6
DEBUG [main] (CastorTest.java:42) [] - Date = null
(...)
DEBUG [main] (CastorTest.java:61) [] - Marshalling :
DEBUG [main] (CastorTest.java:62) [] - <?xml version="1.0" encoding="UTF-8"?>
<version><releaseDate>04-04-2011</releaseDate><name>New name</name></version>

On remarque que la date volontairement erronée du troisième élément <version> de la liste <versions> a bien été prise en charge, et qu’on récupère un null à la place, comme prévu.
L’avantage d’étendre GeneralizedFieldHandler comme nous le faisons est que nous pouvons réutiliser notre classe DateHandler avec d’autres champs de type date. En revanche, si des formats de date différents sont attendus, il faudra implémenter ConfigurableFieldHandler.

Plus d’information :

http://www.castor.org/xml-fieldhandlers.html

Mes travaux d’études

Baptiste Autin, le 23 mars 2009

Le 14 novembre 2008, j’ai soutenu et obtenu (avec la mention TB !) mon diplôme d’ingénieur CNAM en Informatique. Cet examen a sanctionné 5 années d’études menées en parallèle de mon activité professionnelle.

Mon cursus au CNAM est marqué par la diversité.
Il a débuté par un premier bloc d’enseignements intitulé Système de Conduite (architecture de systèmes, gestion de bases de données, génie logiciel, méthodologie de programmation des systèmes (en C), systèmes et applications répartis, réseaux et communications).

J’ai complété cette formation par l’UV de spécialisation Bases de données avancées, un enseignement axé sur les bases de données spatiales, l’indexation dans les grandes bases multimédia, ainsi que sur la recherche de documents sur le web, avec l’étude des standards XML.
Ces cours de spécialisation, particulièrement intéressants, m’ont été dispensés par des enseignants du laboratoire du Cédric (EA 1395).

Durant ce cursus, j’ai également eu à rédiger un rapport sur les métaheuristiques, qui sont des techniques algorithmiques de résolution de problèmes combinatoires complexes :

Mon mémoire de fin d’étude, quant à lui, porte sur mon action professionnelle à l’Institut Curie pendant 18 mois (un CDD financé par l’ANR). J’y décris le processus de développement d’une application de gestion de projets de recherche, destinée à intégrer des données biomédicales issues de l’exploitation d’une banque de prélèvements biologiques.

Cet enseignement du CNAM, ajouté à ma formation initiale d’analyste-programmeur (voir mon CV), me permet aujourd’hui d’aborder de nombreux problèmes qui peuvent se poser en informatique.

Une architecture Web-XML

Baptiste Autin, le 10 février 2009

Un système de production documentaire basé sur XMLLa page CV de ce site est générée à l’aide de documents XML, XSL et CSS.

Le principe est le suivant : les données (compétences, formation professionnelle, expérience, etc.) sont enregistrées dans des fichiers XML, que des processus XSLT transforment en pages web, ou en documents PDF.
Si des modifications doivent être apportées à mon CV, je modifie uniquement les fichiers XML, et toutes les sorties, web comme papier, reflètent instantanément ces modifications.

L’idée semble naturelle, et pourtant il a fallu des années d’évolution des standards et des API de développement XML pour que ce modèle devienne techniquement possible.

Le dynamisme des technologies XML

Contrastant avec la lourdeur de son ancêtre (le SGML), XML constitue une technologie très intéressante, et de plus en plus incontournable :

  • D’abord, XML permet de définir, avec une syntaxe simple, des structures de données arborescentes, riches et complexes (c’est tout l’intérêt d’un document XML par rapport à des lignes d’enregistrements de BD)
  • Modèle de données et présentation peuvent être facilement séparés, ce qui facilite la maintenance d’un site Internet, et la réutilisation des données sous d’autres formes (mobile, PDF, etc.)
  • Que ce soit via les microformats, ou via RDF, le nombre de vocabulaires XML disponibles va s’accroissant
  • La modularité est totale : un document XSL peut faire appel à d’autres documents XSL, et il est ainsi possible de créer des librairies de templates XSL
  • L’environnement technique requis est léger : il suffit d’une bonne implémentation XSLT, comme LibXML ou Saxon

XSL n’est pas le seul moyen pour exploiter un document XML.
On peut le remplacer par un script (écrit dans un langage web traditionnel) qui extrait du document XML les éléments nécessaires à la construction de la page web, à l’aide de l’API DOM, dont les classes permettent d’explorer un document XML, en balayant sa structure grâce au langage de requêtage XPath (d’ailleurs également utilisé avec XSLT).

Par rapport à XSL, cette façon de procéder s’avère utile surtout si des opérations spéciales (connexion avec une base SQL, exécution de programmes externes, etc.) ou des calculs complexes, doivent être menés avec les données, et que seuls un langage de programmation classique, dans son framework, sera à même de réaliser.
Cependant, avec le développement des web services, qui reposent sur une syntaxe XML, et l’introduction de fonctionnalités d’import/export XML dans un nombre toujours plus grand d’applications, on peut penser que le recours à un langage de script traditionnel sera de moins en moins nécessaire sur le web.

et CSS dans tout ça ?

Le site Internet que vous consultez actuellement repose aussi sur CSS2,
un système astucieux qui permet de définir et redéfinir ses styles de présentation à volonté, auxquels le code HTML des pages fait référence.
Aujourd’hui, pour monter une page web, il n’est donc plus nécessaire d’avoir recours à l’imbrication invraisemblable de tableaux HTML et de pixels invisibles (qui servaient à faire “tenir” les tableaux) comme cela se pratiquait encore dans les agences web il y a quelques années (et comme j’ai eu à le faire à l’époque !).
La généralisation de CSS comme technique globale de montage des pages n’est possible que depuis la disparition des “vieux navigateurs” (tel Netscape 4), l’amélioration d’IE, et l’émergence de Mozilla/Firefox, un browser qui colle de très près aux standards du web.

CSS n’est d’ailleurs pas sans lien avec XML : d’abord parce que c’est le consortium W3C qui en assure la direction, comme pour XML et XSL.
Ensuite parce que ce système tend à séparer le contenu de la forme : en gros, vous placez vos données dans des balises DIV (des conteneurs), et vous précisez dans votre fichier CSS où dans l’écran, et de quelle manière, vos données vont s’afficher.

Corollaire : pourquoi ne pas afficher alors un document XML avec une simple feuille CSS ? Cela se pratique, mais ce n’est envisageable que si la structure XML des données reste simple et linéaire, autrement, si le gabarit de la page est un peu complexe, il faut avoir recours à XSL, ou au DOM.

XHTML et le web sémantique

Vu que leurs effets visuels peuvent se redéfinir entièrement avec une feuille de style, les balises HTML deviennent presque interchangeables, au point que l’on peut se demander si l’on a encore besoin de HTML… D’ailleurs, avec le standard XHTML, la tendance est à la restriction du vocabulaire utilisable.
En fait, derrière cet appauvrissement apparent, fort éloigné de ce qui se passait à la fin des années 90 (époque marquée par une guerre sauvage entre browsers, qui rivalisaient d’innovations techniques et de balises originales), l’idée est d’inciter les webmasters et les gestionnaires de contenu à utiliser les balises davantage pour leur sens, et non plus pour leur effet visuel.
Ainsi, en XHTML Strict, l’usage de la traditionnelle balise <u>, qui en HTML standard sert à souligner un mot, est-il déprécié, car le nom même de cette balise (u comme underline) renvoie à une caractéristique purement graphique, et non sémantique, ou logique.

A contrario, recourir à la balise <blockquote>, dans un document XHTML, plutôt qu’à un classique <div>, permet de signifier que ce qui va suivre est une citation. On facilite ainsi le travail des moteurs de recherche, robots d’indexation et autres agrégateurs de contenu, en leur donnant une information sur la nature du texte qui suit.
Le web tend en effet à être exploité par des machines, de façon automatique, et plus seulement par des êtres humains avec un navigateur. Or, pour que ces machines interprètent correctement ce qu’elles lisent, il faut leur parler avec un vocabulaire précis, faisant référence à des concepts connus.

Sur le web aujourd’hui, la tendance est donc à structurer le contenu des pages en fonction du sens, et non plus en fonction du rendu visuel souhaité à un moment donné. C’est une évolution importante vers ce que les spécialistes appellent le web sémantique.