BeanIO - Lire un fichier plat en Java n'a jamais été aussi simple

Introduction

Récemment sur un projet, nous avons été confrontés à un format de fichier que je n'avais jamais rencontré avant : les fichiers texte à longueur fixe. C'est un format de fichier texte dans lequel les valeurs sont placées à des positions et avec une longueur prédéfinies. En voici un exemple :

AF3456D8TJoe                 Smith               43  Avenue de la Grande Armée               75116PARIS                         
3   article1       4.50 
10  article2       3.50 
2   article4       45.00   
BF7654E21John                Doe                 3   Allée Susan Brownell-Anthony            44200NANTES                          
3   article1       4.50 
5   article4       3.50 
2   article8       45.00

En voyant ce genre de fichier, nous nous sommes demandés comment le lire. Nous pourrions tout simplement utiliser java.io avec les méthodes de la classe String et nous débrouiller pour faire un traitement qui lira ce fichier. Cependant, la solution ne nous a pas plu. En effet, cela est peu maintenable et nous avions des dizaines de fichiers de ce type avec des formats différents à lire.

Après une étude sur les différentes librairies existantes, nous avons trouvé notre bonheur avec BeanIO (BeanIO 2.1 Reference Guide).

Cette librairie consiste à décrire le fichier à lire et à partir de cette description le charger dans un POJO (Plain Old Java Object). BeanIo nous offre trois manières de faire cette description: par annotation, XML ou avec une Stream API. BeanIO permet le chargement en mémoire (unmarshalling/désérialisation) ou l’écriture de fichiers (marshalling/sérialisation) à partir et vers des fichiers au format texte, CSV, texte avec délimiteur, texte à longueur fixe et XML.

Dans cet article, je vais vous présenter le mapping en XML qui permet de bien comprendre la logique du mapping de BeanIO.

Import Maven

Un seul import est nécessaire pour utiliser la librairie.

<dependency>
   <groupId>org.beanio</groupId>
   <artifactId>beanio</artifactId>
   <version>2.1.0</version>
</dependency>

Le mapping

Nous allons utiliser comme exemple le texte présenté dans l'introduction.

POJO

Ce texte contient des commandes que des clients ont passées.

Voici le POJO associé :


Mapping XML

<beanio xmlns="http://www.beanio.org/2012/03">
   <stream name="commandes" format="fixedlength">
       <group name="commande" class="fr.demo.beanio.model.Commande" occurs="1+">
           <record name="client" class="fr.demo.beanio.model.Client" occurs="1">
               <field name="reference" rid="true" regex="^[A-Z0-9]{9}$" position="0" length="9"/>
               <field name="prenom" position="9" length="20"/>
               <field name="nom" position="29" length="20"/>
               <field name="numeroRue" position="49" length="4"/>
               <field name="libelleAdresse" position="53" length="40"/>
               <field name="codePostal" position="93" length="5"/>
               <field name="ville" position="98" length="25"/>
           </record>
           <record name="articles" class="fr.demo.beanio.model.Article" collection="list" occurs="1+">
               <field name="quantite" rid="true" regex="^[\d]{1,4}[\s]{0,3}$" position="0" length="4"/>
               <field name="libelle" position="4" length="15"/>
               <field name="montant" position="19" length="6"/>
           </record>
       </group>
   </stream>
</beanio>

Stream

<stream name="commandes" format="fixedlength">

Dans un fichier, il est possible de déclarer plusieurs flux de données (streams). Cela permet de n’avoir qu’un seul fichier de mapping pour plusieurs types de fichiers texte à sérialiser ou désérialiser. À l'exécution on donnera le nom du stream à utiliser. C'est ici qu'on paramètre le type de fichier en entrée.

Group

<group name="commande" class="fr.demo.beanio.model.Commande" occurs="1+">

Cela permet de regrouper des "Record" dans un seul objet. Notez que quand le nombre d'occurrences est supérieur à 1 il s’agit d'une collection.

Record

<record name="client" class="fr.demo.beanio.model.Client" occurs="1">

Une balise “record” permet de mapper une ligne de notre fichier, nous allons les décrire avec des balises "field". Chaque ligne doit être associée à un objet java.

Field

<field name="prenom" position="9" length="20"/>

Ce sont les attributs de nos lignes. Pour les fichiers texte à longueur fixe, il faut spécifier avec les attributs “position” et “length” l’emplacement de notre attribut.

Pour ce genre de fichier, il faut donner le moyen à BeanIO d’identifier le “record”. Cela se fait par l'attribut xml "rid" (pour record identifier) associé à une "regex" ou un "literal" sur l'un des fields. Il faut trouver dans la ligne quelque chose qui ne se trouve pas dans d'autres lignes afin de l’identifier.

Dans l’exemple actuel, j’utilise la référence client pour identifier le “record” client et la quantité pour identifier l’article :

AF3456D8TJoe                 Smith               43  Avenue de la Grande Armée               75116PARIS
3   article1       4.50 
<field name="reference" rid="true" regex="^[A-Z0-9]{9}$" position="0" length="9"/>
La référence sera toujours en position 0 et sera formée de chiffres et de lettres sur 9 caractères.
<field name="quantite" rid="true" regex="^[\d]{1,4}[\s]{0,3}$" position="0" length="4"/>
La quantité sera toujours en position 0 et sera formée de chiffres suivis d’espaces sur 4 caractères.

Généralement, les logiciels qui génèrent ce genre de fichier mettent un numéro qui permet d'identifier le type de ligne en début de ligne.

Notez que l’attribut “type” est facultatif. S’il n’est pas présent, BeanIO se chargera de le déduire à partir du POJO.

Unmarshalling du texte

Voici comment utiliser BeanIO dans votre application, le code est assez simple et parle de lui-même :

// BeanIO fournit une factory pour les streams
StreamFactory factory = StreamFactory.newInstance();
// Chargement du fichier de mapping
factory.load(Main.class.getClassLoader().getResourceAsStream("commande-mapping.xml"));
// On crée un BeanReader pour lire le fichier "input.txt"
BeanReader in =
	factory.createReader(
    	"commandes", new
         File(Main.class.getClassLoader().getResource("input.txt").toURI()));

Commande commande = null;
// On parcourt le stream qui contient l'objet
while ((commande = (Commande) in.read()) != null) {
	// traitement de la commande
  	System.out.println(commande);
}
in.close();

Conclusion

BeanIO est très pratique, il peut être intégré sans problème à Camel ou Spring Batch. Il est possible de gérer tous les types en utilisant des “Custom Type Handlers”. Je vous le recommande fortement pour gérer le marshalling ou l’unmarshalling de fichiers texte.