Aller au contenu

TP – CobolPipeline : Reformatage de fichiers pour applications COBOL


Objectifs

À la fin de ce TP, vous serez capable de :


Contexte métier

Vous êtes développeur.euse Java au sein du service informatique d’une grande entreprise. Le département mainframe utilise une application COBOL de gestion des ressources humaines. Chaque nuit, votre application Java doit :

  1. Lire le fichier des employés exporté par le système RH (format CSV, séparateur ;)
  2. Valider et nettoyer les données
  3. Reformater chaque champ selon le schéma COBOL strict
  4. Produire un fichier à longueur fixe prêt à être consommé par l’application COBOL
  5. Générer un rapport de traitement

Fichier d’entrée

Format du fichier CSV

Le fichier s’appelle employes.csv et utilise ; comme séparateur.

En-tête (ligne 1) :

matricule;nom;prenom;service;poste;salaire;dateEmbauche;codeAgence;actif

Exemples de lignes :

EMP00001;Dupont;Jean;INFORMATIQUE;Développeur;45000.00;2018-03-15;AG001;O
EMP00002;Martin;Marie;COMPTABILITE;Comptable;38500.50;2020-07-01;AG002;O
EMP00003;Schmidt;Hans;MARKETING;Chef de projet;52000.00;2015-11-20;AG001;N
EMP00004;;Pierre;RH;Chargé RH;31000.00;2022-01-10;AG003;O
EMP00005;Leroy;Isabelle;INFORMATIQUE;Architecte;68000.00;2010-05-05;AG001;O

Note : Le fichier de test employes.csv fourni contient 200 lignes dont quelques lignes invalides (nom vide, matricule manquant, salaire non numérique).

Règles de validation

Un enregistrement est invalide (à ignorer avec Skip) si :

Un enregistrement avec actif = N doit être filtré (retourner null dans le processor) → il n’est pas écrit dans le fichier COBOL mais n’est pas compté comme une erreur.


Schéma COBOL de sortie

Le fichier de sortie s’appelle employes_cobol.dat.

+----------+---------+----------+---------+--------+---------+--------+-----------+------+
| Champ    | Longueur| Type COBOL| Position| Règles                                      |
+----------+---------+----------+---------+---------------------------------------------+
| matricule|    8    | PIC X(8)  |  1-8    | Compléter d'espaces à droite. Tronquer si > 8  |
| nom      |   25    | PIC X(25) |  9-33   | MAJUSCULES. Espaces à droite si < 25.          |
| prenom   |   25    | PIC X(25) | 34-58   | 1ère lettre maj, reste minuscule. Espaces à droite. |
| service  |   20    | PIC X(20) | 59-78   | MAJUSCULES. Espaces à droite.                  |
| poste    |   30    | PIC X(30) | 79-108  | Conserver la casse. Espaces à droite.           |
| salaire  |   10    | PIC 9(8)V99|109-118 | Entier sur 8 ch + centimes sur 2 ch, SANS virgule. Ex: 45000.00 → 0004500000 |
| dateEmb  |    8    | PIC X(8)  |119-126  | Format AAAAMMJJ. Ex: 2018-03-15 → 20180315     |
| codeAgence|   5    | PIC X(5)  |127-131  | Espaces à droite.                              |
+----------+---------+----------+---------+---------------------------------------------+
TOTAL : 131 caractères par enregistrement (pas de retour à la ligne)

Exemple d’enregistrement de sortie :

EMP00001 Dupont                   Jean                     INFORMATIQUE        Développeur                   0004500000201803150AG001 

(131 caractères exactement)


Partie 1 – Mise en place du projet

1.1 Création du projet

Créez un projet Spring Boot avec Spring Initializr (start.spring.io) :

1.2 Configuration application.properties

# JobRepository en mémoire (H2)
spring.datasource.url=jdbc:h2:mem:batchdb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.batch.jdbc.initialize-schema=always
spring.batch.job.enabled=false

# Chemins des fichiers (modifiables via JobParameters)
batch.input.file=classpath:input/employes.csv
batch.output.file=file:output/employes_cobol.dat
batch.chunk.size=500
batch.skip.limit=50

logging.level.com.formation=DEBUG
logging.level.org.springframework.batch=INFO

1.3 Créer le fichier CSV de test

Créez src/main/resources/input/employes.csv avec au moins 200 lignes dont :


Partie 2 – Modèle de données

2.1 Créer la classe EmployeCsv

Classe représentant une ligne du fichier CSV d’entrée. Tous les champs sont des String (le processor se chargera des conversions) sauf que vous pouvez ajouter des getters/setters.

public class EmployeCsv {
    private String matricule;
    private String nom;
    private String prenom;
    private String service;
    private String poste;
    private String salaire;        // String → sera converti en BigDecimal dans le processor
    private String dateEmbauche;   // String → sera converti en LocalDate
    private String codeAgence;
    private String actif;          // "O" ou "N"
    // getters, setters, toString...
}

2.2 Créer la classe EmployeCobol

Classe représentant un enregistrement COBOL. Tous les champs sont des String avec la longueur exacte.

public class EmployeCobol {
    private String matricule;     // 8 chars
    private String nom;           // 25 chars
    private String prenom;        // 25 chars
    private String service;       // 20 chars
    private String poste;         // 30 chars
    private String salaire;       // 10 chars
    private String dateEmbauche;  // 8 chars
    private String codeAgence;    // 5 chars

    /**
     * Construit et retourne la ligne COBOL de 131 caractères.
     * Lance une IllegalStateException si la longueur n'est pas correcte.
     */
    public String toLigneCobol() {
        String ligne = matricule + nom + prenom + service + poste
                     + salaire + dateEmbauche + codeAgence;
        if (ligne.length() != 131) {
            throw new IllegalStateException(
                "Longueur incorrecte : " + ligne.length() + " (attendu 131) pour " + matricule
            );
        }
        return ligne;
    }
}

2.3 Créer la classe utilitaire CobolFormatter

Créez CobolFormatter avec les méthodes statiques suivantes :

public final class CobolFormatter {

    // PIC X(n) : alphanumérique, complété d'espaces à droite
    public static String picX(String valeur, int longueur) { ... }

    // PIC 9(n) : numérique entier, complété de zéros à gauche
    public static String pic9(long valeur, int longueur) { ... }

    // PIC 9(n)V9(d) : numérique décimal, sans virgule
    // Ex: 45000.00, nEntier=8, nDec=2 → "0004500000"
    public static String pic9V(BigDecimal valeur, int nEntier, int nDecimales) { ... }

    // Date au format AAAAMMJJ
    public static String dateAaaaMmJj(LocalDate date) { ... }
}

Partie 3 – ItemReader

3.1 Configurer le FlatFileItemReader

Dans votre classe BatchConfig, créez un bean FlatFileItemReader<EmployeCsv> :

Indice : Utilisez FlatFileItemReaderBuilder avec .delimited().delimiter(";").


Partie 4 – ItemProcessor

4.1 Créer EmployeValidationProcessor

Ce processor doit :

  1. Filtrer (retourner null) les employés avec actif = "N"
  2. Lancer une exception (qui sera catchée par Skip) si :
    • matricule est vide ou null → lancer ValidationException
    • nom est vide ou null → lancer ValidationException
    • salaire ne peut pas être converti en BigDecimal → lancer ValidationException
    • dateEmbauche ne peut pas être parsée en LocalDate (format yyyy-MM-dd) → lancer ValidationException
  3. Normaliser les données valides :
    • nom en MAJUSCULES
    • prenom : première lettre majuscule, reste minuscule
    • service en MAJUSCULES
    • Supprimer les espaces en début/fin de chaque champ
// Créez votre propre exception de validation
public class ValidationException extends RuntimeException {
    public ValidationException(String message) { super(message); }
}

4.2 Créer EmployeToCoblProcessor

Ce processor convertit un EmployeCsv (déjà validé et nettoyé) en EmployeCobol :

4.3 Chaîner les deux processors

Utilisez CompositeItemProcessor pour chaîner :

  1. EmployeValidationProcessor
  2. EmployeToCoblProcessor

Partie 5 – ItemWriter

5.1 Créer le FlatFileItemWriter

Créez un bean FlatFileItemWriter<EmployeCobol> :


Partie 6 – Configuration du Job

6.1 Créer les Steps

Dans BatchConfig, créez deux Steps :

Step 1 : etapeVerificationFichier (Tasklet)

Step 2 : etapeTransformation (Chunk)

6.2 Créer le Job

@Bean
public Job cobolPipelineJob(Step etapeVerification, Step etapeTransformation,
                             JobRapportListener rapportListener) {
    return new JobBuilder("cobolPipelineJob", jobRepository)
        .listener(rapportListener)
        .start(etapeVerification)
        .next(etapeTransformation)
        .build();
}

6.3 Créer le SkipListener

Créez EmployeSkipListener qui :


Partie 7 – Rapport d’exécution

7.1 Créer JobRapportListener

Créez un JobExecutionListener qui affiche dans les logs à la fin du Job :

╔══════════════════════════════════════════════════════╗
║           RAPPORT D'EXÉCUTION - CobolPipeline        ║
╠══════════════════════════════════════════════════════╣
║ Job         : cobolPipelineJob                       ║
║ Statut      : COMPLETED                              ║
║ Durée       : 12.345 secondes                        ║
╠══════════════════════════════════════════════════════╣
║ STEP : etapeTransformation                           ║
║   Lignes lues      : 200                             ║
║   Lignes écrites   : 183                             ║
║   Lignes filtrées  : 7  (actif=N)                    ║
║   Lignes ignorées  : 10 (données invalides)          ║
╚══════════════════════════════════════════════════════╝

Indice : Dans afterJob(), itérez sur jobExecution.getStepExecutions() pour récupérer readCount, writeCount, skipCount de chaque step.


Partie 8 – Lancement et test

8.1 Créer le point d’entrée

Créez une classe CobolPipelineRunner qui implémente CommandLineRunner et lance le Job :

@Component
public class CobolPipelineRunner implements CommandLineRunner {

    @Autowired private JobLauncher jobLauncher;
    @Autowired private Job cobolPipelineJob;

    @Value("${batch.input.file}")
    private String fichierEntree;

    @Value("${batch.output.file}")
    private String fichierSortie;

    @Override
    public void run(String... args) throws Exception {
        JobParameters params = new JobParametersBuilder()
            .addString("fichierEntree", fichierEntree)
            .addString("fichierSortie", fichierSortie)
            .addLong("timestamp", System.currentTimeMillis())
            .toJobParameters();

        JobExecution execution = jobLauncher.run(cobolPipelineJob, params);
        System.exit(execution.getStatus() == BatchStatus.COMPLETED ? 0 : 1);
    }
}

8.2 Tests à effectuer

Lancez l’application et vérifiez :

  1. Le fichier employes_cobol.dat est bien créé dans le répertoire output/
  2. Chaque ligne du fichier de sortie fait exactement 131 caractères
  3. Les lignes avec actif=N ne sont pas dans le fichier de sortie
  4. Les lignes invalides ne sont pas dans le fichier de sortie
  5. Le rapport d’exécution s’affiche dans les logs
  6. Comptez manuellement les lignes écrites et comparez avec le rapport

Commande pour vérifier la longueur des lignes (Linux/Mac) :

awk 'length != 131 {print NR": longueur="length" → "$0}' output/employes_cobol.dat
# Ne doit rien afficher si toutes les lignes font 131 chars

Partie 9 – Bonus (pour aller plus loin)

Bonus 1 – Multi-threading

Activez le traitement multi-threadé sur le Step etapeTransformation avec 4 threads. N’oubliez pas de rendre le reader thread-safe avec SynchronizedItemStreamReader.

Comparez le temps d’exécution avec et sans multi-threading sur le fichier de 200 lignes. (L’effet sera plus visible avec un fichier de 100 000 lignes.)

Bonus 2 – Lancer depuis la ligne de commande

Modifiez CobolPipelineRunner pour lire le chemin du fichier d’entrée depuis les arguments de la ligne de commande :

java -jar cobol-pipeline.jar --fichier=/data/employes_production.csv

Bonus 3 – Second Step : rapport CSV

Ajoutez un troisième Step qui lit le fichier COBOL produit et génère un fichier rapport.csv récapitulatif :

matricule,nom,prenom,service,salaire_calcule
EMP00001,DUPONT,Jean,INFORMATIQUE,45000.00
...

Bonus 4 – Gestion des doublons

Ajoutez dans le processor une vérification des doublons de matricule (en maintenant un Set<String> en mémoire). Si un matricule apparaît deux fois, ignorer la deuxième occurrence et la logger.


Aide-mémoire

Commandes utiles

# Compter les lignes du fichier CSV (sans l'en-tête)
wc -l output/employes_cobol.dat

# Afficher les 5 premières lignes
head -5 output/employes_cobol.dat

# Afficher la longueur de chaque ligne
awk '{print length, $0}' output/employes_cobol.dat | head -10

# Vérifier que toutes les lignes font 131 chars
awk 'length != 131 {count++} END {print count+0 " lignes incorrectes"}' output/employes_cobol.dat

Pièges courants

  1. @StepScope oublié → Le reader est créé sans les JobParameters → NullPointerException
  2. spring.batch.job.enabled=false oublié → Le Job se lance au démarrage sans paramètres
  3. Longueur de ligne incorrecte → Vérifiez chaque picX(valeur, n) dans le processor
  4. Encodage → Utiliser ISO-8859-1 pour les fichiers mainframe (pas UTF-8)
  5. shouldDeleteIfExists(true) → Sans ça, le writer append au fichier existant lors des relances
  6. Skip et transactions → En cas de Skip en écriture, tout le chunk est rollbacké et les enregistrements sont écrits un par un pour isoler le mauvais

Bon courage et bons batchs !