Spring-Batch (Part III) : Comment lire ou parser efficacement divers fichiers CSV avec RecordSeparatorPolicy?

 

L'objet de ce billet est de vous présenter les "best-practices" afin de lire les divers de formats de fichiers csv du simple à l'exotique.

Le reader de spring-batch doit être configuré (et uniquement si possible 🙂 ) pour gérer des formats particuliers de fichiers csv.

La démarche reste identique pour les autres formats de fichiers.

Comme il est difficile de traiter tous les cas pour un blog, la démo ci-après se focalise sur ce fichier csv:

id, rs, dateCreation
EOF_RECORD
"1", 
"rs, RAISONSOCIALE,FLEURS ORANGERS",
"2017-05-12"
EOF_RECORD
"2",,"2017-05-23"
EOF_RECORD
csv

A nommer ce fichier entreprises.csv sous src/main/resources.

D'où, en dehors de l'entête ignorée par le reader FlatFileItemReader, chaque bloc (ligne ou +) est préfixé avec le mot EOF_RECORD.

Merci de noter :

  1. le séparateur du fichier csv est la virgule,
  2. le bloc de données peut s'étendre sur plusieurs lignes ( c'est le cas du record avec id=1),
  3. les valeurs sont encadrées (quotées) avec le caractère guillement '"',
  4. la présence de la virgule dans certaines valeurs du champ nommé rs (pour raison sociale) par ex. ligne(s) ayant id=1,
  5. Enfin, l'avant dernière ligne ayant l'id=2 ne fournit pas de raison sociale!

 

Ces lignes vont être stockées en base de données en passant par la création d'une liste d'entités java de type Entreprise:

public class Entreprise {

	private String id;	
	private String rs;
	private String dateCreation;

// cstructor & g(s)etters omis

}

 

Ingrédients du blog: Spring 4, Spring-Batch 3, java 7+, JUnit

Les mots clés sont ceux de spring: RecordSeparatorPolicy, SuffixRecordSeparatorPolicy.

 

Voici les étapes de la démo :

Étape 1. Configurer le projet maven,

Étape 2. Configurer spring (java config minimale: seul le reader),

Étape 3. Configurer un policy (suffix policy) pour le reader,

Étape 4. Écrire un test unitaire du reader.

 

L'intérêt de tester unitairement le(s) reader(s) se confirme lorsque le métier présente des fichiers csv assez particuliers ou encore lorsqu'il exige un traitement spécifique au moment de les lire.
Il est bien évidemment possible de tester unitairement les tokenizer, lineMapper, beanWrapperFieldSetMapper.. mais là ça fera beaucoup de chose à tester séparément!!

Une fois le test TU du reader écrit, le(s) refactoring(s) et les évolutions (agilité oblige) deviennent des opérations plus sûres.

 

ETAPE 1. Créer le projet maven

 

Partons d'un projet spring-boot créé, from scratch, depuis la page initilzr en ayant sélectionné spring-batch & h2.

Note. Pour le test junit, la dépendance h2 n'est pas nécessaire mais comme rien n'est à configurer, laissons-la.

ETAPE 2. Configurer Spring

Cette étape est réduite à la création du reader et ce qui est nécessaire au lancement du test Junit ci-après.

Voici le code utile:

public class ReaderUtils {
	public static FlatFileItemReader<Entreprise> configureFtaFileReaderBySuffixPolicy(String nameFileCsv, String[] names) {
BeanWrapperFieldSetMapper<Entreprise> beanWrapFS = new BeanWrapperFieldSetMapper<>();	
beanWrapFS.setTargetType(Entreprise.class);
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
tokenizer.setNames(names);
DefaultLineMapper<Entreprise> defLineMapper = new DefaultLineMapper<>();
defLineMapper.setLineTokenizer(tokenizer);
defLineMapper.setFieldSetMapper(beanWrapFS);
FlatFileItemReader<Entreprise> reader = new FlatFileItemReader<>();
reader.setResource(new ClassPathResource(nameFileCsv));
reader.setLineMapper(defLineMapper);
reader.setLinesToSkip(1);
}
		return reader;
	}
}

 

L'entête des imports est comme suit:

package fr.netapsys.abdou;
import o.s.b.item.file.FlatFileItemReader;
import o.s.b.item.file.mapping.*;
import o.s.b.item.file.separator.SuffixRecordSeparatorPolicy;
import o.s.b.item.file.transform.DelimitedLineTokenizer;
import o.s.core.io.ClassPathResource;
import fr.netapsys.abdou.model.Entreprise;

NOTE. Penser à remplacer les o.s.b. par org.springframework.batch.

ETAPE 3. Configurer SuffixRecordPolicy

 

A ce code, nous devons rajouter la configuration d'un SuffixRecordSeparatorPolicy pour le reader.

Cela permettra de lire facilement le format csv présenté ci-dessus ou les blocs des enregistrements (records) sont préfixés avec le mot 'EOF_RECORD'.

Voici exactement ce qu'il faudrait rajouter, à la fin de la méthode avant le return reader:

final String suffix="EOF_RECORD";
SuffixRecordSeparatorPolicy policy = new SuffixRecordSeparatorPolicy();
			
policy.setSuffix(suffix);
	reader.setRecordSeparatorPolicy(policy);
	
return reader;

}

 

ETAPE 4. Test unitaire Junit

 

Maintenant que notre reader est configuré et le suffixRecordPolicy est défini, testons unitairement ce reader.

Voici le code simple du test JUnit:

import org.junit.Test;
import o.s.b.item.file.FlatFileItemReader;
import o.s.b.test.MetaDataInstanceFactory;
import fr.netapsys.abdou.model.Entreprise;
public class TUReaderWithSuffixPOlicyTest {
	@Test 
	public void testTUReader() throws Exception{
	final String EOF_RECORD = "EOF_RECORD";

	FlatFileItemReader<Entreprise> reader =ReaderUtils.configureFtaFileReaderBySuffixPolicy("entreprises.csv",AppBatchConfigReaderWithPolicy.NAMES_COLUMNS);		
//open reader
  reader.open(MetaDataInstanceFactory.createStepExecution().getExecutionContext());
//assertions
 CommonTestUtils.assertEntreprisesReadden(reader);	
	}
}

 

La dernière ligne appelle la méthode qui effectue certaines assertions sur l'opération read du FlatFileItemReader:

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.springframework.batch.item.ItemReader;

import fr.netapsys.abdou.model.Entreprise;

public class CommonTestUtils {
	public static void assertEntreprisesReadden(ItemReader<Entreprise> reader) throws Exception {
		int k=1;
		Entreprise entr = reader.read()  ;
		while( entr!=null  ){
			assertNotNull(entr);
			assertEquals(""+k,entr.getId());
			if(k==1) assertEquals("rs, RAISONSOCIALE,FLEURS ORANGERS", entr.getRs());
			if(k==2) assertTrue(entr.getRs().isEmpty());
			entr = reader.read();
			k++;
		}
	}
}

 

L'exécution du test unitaire JUnit confirme que chaque bloc (et non pas ligne) lu correspond à un objet java de type Entreprise et que ses propriétés sont conformes et sont celles attendues.

 

CONCLUSION

 

L'intérêt de l'approche de définir un SuffixRecordSeparatorPolicy est triple:

  • Suivre les best-practices tel le principe SOC (separation of concerns),
  • Éliminer l'écriture de nouveau code inutile (code de plomberie) et ses conséquences (bugs!) sur la recette,
  • Centrer dans un composant les adaptations nécessaires à la configuration de chaque reader.

 

Le SuffixRecordSeparatorPolicy permet aussi, via juste quelques adaptations des assertions junit, de gérer le cas suivant où certains enregistrements(records) sont répartis sur plusieurs lignes comme illustré ci-après:

id, rs, dateCreation
EOF_RECORD
"1", 
"rs, RAISONSOCIALE,
FLEURS ORANGERS",
"2017-05-12"
EOF_RECORD
"2",,"2017-05
-23"
EOF_RECORD

Vous remarquez que les champs rs et date sont répartis sur deux lignes.

J'espère que cet article vous sera utile.

2 commentaires

  1. Hello Abderrazek,
    Merci cet excellent blog , je m’en sert énormément durant mon stage ! pourriez-vous svp mettre partager le code source finale svp.
    Je vous remercie d’avance.
    Nacim

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *