Un tableau avec grands totaux grâce à Google Charts

Baptiste Autin, le 8 mai 2015

La Datatable est la structure de base de la puissante API Google Visualization. Comme son nom l’indique, une Datatable permet de construire des tableaux de données, via un objet google.visualization.Table, mais elle peut également servir à générer des graphiques (avec Google Charts).

Malheureusement, par défaut, un objet google.visualization.Table ne permet pas d’afficher automatiquement des totaux, en ligne et en colonne, et je vous propose donc un script complet pour pallier cette absence.

Notre point de départ, ce seront de simples objets Javascript :

	var data = new google.visualization.DataTable();
	data.addColumn('string', 'Nom');
	data.addColumn('number', 'Janvier');
	data.addColumn('number', 'Février');
	data.addColumn('number', 'Mars');
	data.addColumn('number', 'Avril');
	data.addColumn({type: 'number', label: 'Total'});

	data.addRow(['Jean', 5, 12, 18, 25, 60]);
	data.addRow(['Robert', 0, 1, 20, 2, 23]);
	data.addRow(['Maud', 11, 5, 7, 9, 32]);
	data.addRow(['Tom', 3, 1, 1, 4, 9]);

Après quelques opérations sur la Datatable data, on créé un objet google.visualization.Table, on appelle sa méthode draw() et voici ce que l’on obtient :


Pas mal, non ?

On trouvera ici le code source complet de cet exemple.

S’il est assez facile d’obtenir le total vertical (par une simple colonne de regroupement de type google.visualization.data.sum), le total horizontal est plus difficile à réaliser (il faut créer x colonnes d’agrégation, filtrer, et ajouter la ligne).

Notez que les colonnes sont triables, c’est une fonctionnalité élégante qu’offre la Datable par défaut.
Remarquez également que le tri sur la colonne Total doit être corrigé. En effet, il faut exclure du tri le total horizontal final, sous peine de voir ce grand total remonter en première ligne une fois sur deux : c’est l’objet du listener sur l’événement ‘sort’, un petit hack qui nous permet de rejeter systématiquement en bas du tableau le grand total.

Histogrammes dérivés

En guise de cadeau final, voici deux graphiques HTML5 obtenus très facilement à partir de la Datatable précédemment construite, toujours grâce à l’API Google Charts.
Le premier graphique s’obtient en instanciant un objet ColumnChart, puis en appelant sa méthode draw(), et en lui passant directement notre Datatable :

	var chart = new	google.visualization.ColumnChart(document.getElementById("chart_div"));
	data.removeColumn(data.getNumberOfColumns()-1);		// On enlève la colonne total !
	chart.draw(data, options);

Pour générer le second graphique (où lignes et colonnes ont permuté), il suffit de construire préalablement une Datatable inversée de la première en pivotant ses données.
Le code de cette petite opération de permutation est à retrouver dans le code source complet de notre exemple.

Insert where… not exists

Baptiste Autin, le 19 janvier 2015

Petit problème du jour :

Soit la TABLE bibelot (nom VARCHAR(200));
En MySQL, écrire la requête qui insère la donnée ‘x’ dans la colonne ‘nom’, à condition qu’un enregistrement contenant cette même donnée n’y soit pas déjà présent (autrement, la requête ne doit rien faire dans ce cas).

Rappel : INSERT INTO VALUES ... WHERE ... n’existe pas en MySQL.


Réponse :
INSERT INTO bibelot (nom) SELECT 'x' FROM DUAL WHERE NOT EXISTS( SELECT 1 FROM bibelot WHERE nom = 'x');

De l’importance de bien comprendre les contextes Spring

Baptiste Autin, le 31 août 2013

Récemment, j’ai eu à intervenir sur une application web qui posait problème : toutes les annotations @Transactional, définies au niveau des classes métier, semblaient inopérantes, malgré la définition correcte d’un gestionnaire transactionnel dans le contexte Spring principal.

Je précise que les méthodes annotées étaient bien publiques (rappelons que c’est l’une des conditions du fonctionnement de l’annotation @Transactional avec les proxys JDK)

Que diable se passait-il ?

 

Dans le fichier applicationContext.xml (= contexte Web), j’ai bien trouvé la déclaration du manager JPA :

    
    
        
    

Ainsi que les beans de service de l’application, détectés par scan :

                               
       

Je m’attendais donc à ce que le fichier myapplication-servlet.xml (= contexte Spring MVC), quant à lui, définisse les contrôleurs de l’application.
Mais voici ce que je vis :

    

Au lieu de se contenter de scanner le paquetage où étaient rangés les controleurs MVC (dans foo.myapplication.controllers), on scannait tout le paquetage de l’application !
Du coup, on scannait aussi une deuxième fois les beans de la couche métier (notez au passage que cela ne pose pas de problème à Spring).

Or, comme beaucoup d’autres choses dans le framework Spring, le support déclaratif des transactions repose sur des proxys AOP, et les proxys AOP ne sont valides que dans le contexte Spring où ils sont définis, par conséquent la déclaration <tx:annotation-driven />, pourtant bien présente dans le contexte principal, devenait inopérante dans le contexte MVC.
Du coup, les annotations @Transactional restaient sans effet pour tous les beans de service qui étaient injectés dans des beans du contexte MVC (comme les beans @Controller).

La solution à mon problème a tout simplement consisté à limiter, dans le fichier myapplication-servlet.xml, le scan aux seules classes contrôleurs :

    

Les beans métiers et DAO sont maintenant uniquement définis dans le contexte principal, c’est à dire là où se trouve également défini le gestionnaire JPA.

 

Moralité
Le contexte Spring MVC ne devrait contenir que la définition des composants relatifs au framework MVC utilisé, et surtout pas de logique métier ou d’accès aux données.
Ne pas respecter ce principe, c’est non seulement problématique du point de vue architectural, mais c’est aussi s’exposer à des erreurs qui peuvent rester longtemps inaperçues.

Conversion Word vers HTML avec JOD et OpenOffice

Baptiste Autin, le 30 avril 2013

En combinant OpenOffice et la librairie JODConverter, on peut facilement intégrer dans une application J2EE/Spring un service de conversion de documents Office vers HTML (ou vers PDF).

Voici la marche à suivre :

1. Installez OpenOffice sur votre serveur
2. Recopiez ma classe d’exemple ConvertorJod (ci-dessous) dans votre webapp, et modifiez votre fichier applicationContext en conséquence
3. Modifiez en particulier le chemin menant à votre répertoire d’installation d’Office, ainsi que celui menant au répertoire de profil à utiliser
4. Lancez votre webapp. Un processus soffice.bin devrait apparaître dans la liste des processus de votre serveur, et vos logs devraient ressembler à quelque chose comme :

	DEBUG - Starting LibreOffice server...
	org.artofsolving.jodconverter.office.ProcessPoolOfficeManager
	INFO: ProcessManager implementation is PureJavaProcessManager
	org.artofsolving.jodconverter.office.OfficeProcess prepareInstanceProfileDir
	org.artofsolving.jodconverter.office.OfficeProcess start
	INFO: starting process with acceptString 'socket,host=127.0.0.1,port=8100,tcpNoDelay=1' and profileDir (...)
	org.artofsolving.jodconverter.office.OfficeProcess start
	INFO: started process
	org.artofsolving.jodconverter.office.OfficeConnection connect
	INFO: connected: 'socket,host=127.0.0.1,port=8100,tcpNoDelay=1'
	DEBUG [localhost-startStop-1] (ConvertorJod.java:48) [] - LibreOffice server started...

Votre application ayant correctement démarré, votre service de conversion est maintenant disponible pour vos autres beans. Le démarrage du processus de conversion ayant été réalisé une seule fois (au démarrage de l’ApplicationContext), les temps de conversion sont plutôt bons (de l’ordre d’une à deux secondes pour un document de taille moyenne).

Remarques importantes
Si vous ne précisez pas de répertoire de template, c’est un répertoire par défaut qui sera choisi, et si celui-ci n’existe pas, le démon soffice.bin ne se lancera pas.
Ceci est particulièrement important dans le cas d’une webapp, car votre serveur d’application ne s’exécute probablement pas sous un compte utilisateur disposant d’un répertoire de template OpenOffice.
Éventuellement, faites un copier-coller d’un répertoire correspondant à un utilisateur existant (sous Windows, vous le trouverez sous C:\Users\\AppData\Roaming\OpenOffice.org\)
Il faut également que des droits d’écriture soient positionnés sur le répertoire de template. Autrement, vous aurez une erreur :

java.net.ConnectException: connection failed: 'socket,host=127.0.0.1,port=8100,tcpNoDelay=1'; java.net.ConnectException: Connection refused: connect

package test.convertor;

import java.io.File;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.log4j.Logger;
import org.artofsolving.jodconverter.OfficeDocumentConverter;
import org.artofsolving.jodconverter.document.DocumentFormat;
import org.artofsolving.jodconverter.office.DefaultOfficeManagerConfiguration;
import org.artofsolving.jodconverter.office.OfficeConnectionProtocol;
import org.artofsolving.jodconverter.office.OfficeException;
import org.artofsolving.jodconverter.office.OfficeManager;
import org.springframework.stereotype.Service;

@Service
public class ConvertorJod implements AbstractFileConvertor {

	protected final Logger logger = Logger.getLogger(getClass());

	private OfficeManager officeManager = null;
	private OfficeDocumentConverter converter = null;

	@PostConstruct
	protected void initOfficeManager() {
		logger.debug("Starting conversion service...");

		DefaultOfficeManagerConfiguration configuration = new DefaultOfficeManagerConfiguration();

		configuration.setPortNumber(8100);
		configuration.setConnectionProtocol(OfficeConnectionProtocol.SOCKET);

		configuration.setTemplateProfileDir(new File("D:\\openoffice\\3"));
		configuration.setOfficeHome(new File("C:\\Program Files (x86)\\OpenOffice.org 3"));

		configuration.setTaskExecutionTimeout(30000L);

		officeManager = configuration.buildOfficeManager();
		converter = new OfficeDocumentConverter(officeManager);

		officeManager.start();

		logger.debug("Conversion service started");
	}

	@PreDestroy
	protected void preDrestroy() {
		logger.debug("Stopping conversion service...");
		officeManager.stop();
		logger.debug("Conversion service stopped");
	}

	@Override
	public void convertToHtml(final File source, final File destination) throws OfficeException {

		DocumentFormat outputFormat = converter.getFormatRegistry().getFormatByExtension("html");    // "html" ou "pdf"

		logger.debug("Converting " + source.getName());

		converter.convert(source, destination, outputFormat);
	}
}

Et voici l’interface Java du service (à injecter dans tous vos beans métier ayant besoin de réaliser des conversions de documents) :

package test.convertor;

import java.io.File;

import org.artofsolving.jodconverter.office.OfficeException;

public interface AbstractFileConvertor {

	void convertToHtml(File source, File destination) throws OfficeException;

}

Retourner des pièces jointes avec Spring Web Service 1.5.9

Baptiste Autin, le 30 juillet 2012

Depuis la version 2.0 de Spring Web Service, il est facile d’accéder à l’objet MessageContext, et donc de récupérer ou de retourner des attachments dans des messages SOAP.

Mais avec la version précédente (1.5.9), celle que je suis encore contraint d’utiliser à mon travail, c’est plus délicat.
Je suis donc parti d’une astuce donnée par un autre blogueur, Osvaldas Grigas, consistant à intercepter les appels SOAP, et à mémoriser dans un ThreadLocal une
référence à l’objet MessageContext (exposé par l’interface EndpointInterceptor).

Dans son post, l’auteur de cette astuce montre comment récupérer un fichier transmis par le client vers le service web.
Je vais vous montrer le chemin inverse, c’est à dire comment intégrer un attachment dans la réponse du web service vers le client :

@Endpoint
@Transactional
public class MySoapServerImpl implements MySoapServer {

    @Autowired
    private SaajSoapMessageFactory saajMessageFactory;
    
    @Autowired
    private ApplicationContext ctx;
    
    @Override
    @PayloadRoot(namespace = "some namespace", localPart = "FindSomethingRequest")
    public FindSomethingResponse FindSomething(FindSomethingRequest request) throws InvalidParameterException, IOException {
        
        final FindSomethingResponse response = new FindSomethingResponse();
        
        WebServiceMessage message = saajMessageFactory.createWebServiceMessage();
        SoapMessage soapMessage = (SoapMessage) message;
        
        File file = ctx.getResource("foo.jpg").getFile();    // here is the file we want to send back
                    
        soapMessage.addAttachment(file.getName(), file);
        
        MessageContextHolder.getMessageContext().setResponse(soapMessage);
    
        return response;
    }
}

Et pour rappel, voici comment intercepter et enregistrer un message SOAP dans un ThreadLocal :



    
    
        
            
        
    

@Component("MsgCtxInterceptorAdapter")
public class MsgCtxInterceptorAdapter extends EndpointInterceptorAdapter {
       
    @Override
    public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception {

        MessageContextHolder.setMessageContext(messageContext);
        //messageContext.getRequest().writeTo(System.out);
        return super.handleRequest(messageContext, endpoint);
    }
   
    @Override
    public boolean handleResponse(MessageContext messageContext, Object endpoint) throws Exception {
       
        MessageContextHolder.removeMessageContext();
        //messageContext.getResponse().writeTo(System.out);
        return super.handleResponse(messageContext, endpoint);
    }
   
    public boolean handleFault(MessageContext messageContext, Object endpoint) {
       
        MessageContextHolder.removeMessageContext();
        return super.handleFault(messageContext, endpoint);
    }
}
public final class MessageContextHolder {
    private static ThreadLocal<MessageContext> threadLocal = new ThreadLocal<MessageContext>() {
        @Override
        protected MessageContext initialValue() {
            return null;
        }
    };

    private MessageContextHolder() {
    }

    public static MessageContext getMessageContext() {
        return threadLocal.get();
    }

    public static void setMessageContext(MessageContext context) {
        threadLocal.set(context);
    }

    public static void removeMessageContext() {
        threadLocal.remove();
    }
}

Spring Batch : enregistrer des données dans l’Execution Context d’un Step

Baptiste Autin, le 3 avril 2012

Dans la méthode execute() d’une Tasklet, si vous voulez enregistrer des données dans l’ExecutionContext du Step courant, n’allez pas trop vite.
N’écrivez pas :

chunkContext.getStepContext().getStepExecutionContext().put("myKey", "myValue");

Écrivez plutôt :

chunkContext.getStepContext().getStepExecution().getExecutionContext().put("mode", "test");

En effet, dans le premier cas, getStepExecutionContext() retourne une collection non modifiable. Vous êtes donc bon pour une java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1285)
si vous tentez de modifier le contenu qu’elle renvoie.



Remarque : avec un ItemWriter, on ne peut pas se tromper, puisqu’il faut ajouter explicitement un écouteur à sa classe avec l’annotation @BeforeStep :

private StepExecution stepExecution;

@BeforeStep
public void saveStepExecution(StepExecution stepExecution) {
    this.stepExecution = stepExecution;
}

(...)

this.stepExecution.getExecutionContext().put("someKey", "myValue");

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 quelques 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:

  • Utiliser l’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.

« Older Entries