Implémenter le patron de conception DAO par les tests

Thierry-Leriche-Dessirier

Ce TP vous montre comment écrire et tester un DAO en Java. Vous allez découvrir Maven, tracer le fonctionnement avec Log4j, tester vos méthodes avec JUnit, lire des fichiers CSV avec OpenCsv et CsvEngine, gérer des ressources en Java et bien plus encore… Commentez

- Titre : Implémenter le patron de conception DAO par les tests

- Auteur : Thierry-Leriche-Dessirier

- Parution : 15 décembre 2012

- Mise à jour : 28 décembre 2016

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Thierry Leriche-Dessirier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Versions des logiciels et bibliothèques utilisés

- I-B. Mises à jour

- II. Préparation

- II-A. Télécharger le projet d'exemple

- II-B. Import dans Eclipse (projet Maven)

- II-C. Quelques raccourcis utiles

- II-D. Les objets du domaine

- II-E. DAO pour la lecture

- III. Log4J

- IV. CSV

- IV-A. Découverte du format

- IV-B. Tests génériques avec JUnit

- IV-C. Lecture naïve

- IV-D. Version améliorée

- IV-D-1. Entêtes

- IV-D-2. Recherche

- IV-E. OpenCsv

- IV-F. CSV Engine (avec des annotations)

- V. Conclusion

- VI. Remerciements

- VII. Annexes

- VII-A. Liens

- VII-B. Pour aller plus loin

- VII-C. FAQ

- VII-C-1. M2_REPO

- VII-C-2. Je ne trouve pas le dossier .m2 dans l'explorateur de fichiers

- VII-C-3. Je n'arrive pas à installer mon ordinateur portable

- VII-C-4. Le sommaire prend trop de place

- VII-C-5. J'ai un warning "the import com.foo.truc is never used"

- VII-C-6. J'ai un warning "the serializable class Foo does not declare a static final serialVersionUID field of type long"

- VII-C-7. Comment savoir la taille d'un tableau ou d'une liste en Java ?

- VII-C-8. Comment configurer le proxy de Maven

- VII-C-9. Les travaux sont-ils à rendre ?

- VII-D. Installer un environnement de développement à la maison

- VII-D-1. Installation de Java

- VII-D-2. Installation d'Eclipse

- VII-D-3. Installation de Maven

- VII-D-4. FAQ de Developpez.com

- VII-E. Ancienne procédure de compillation et d'import dans Eclipse

- VII-E-1. Anciène procédure avec Maven

- VII-E-2. Ancienne procédure d'import dans Eclipse

- VII-F. Solution possible

I - Introduction

Ce qui suit est un TP de Génie Logiciel qui utilise Java. Notez qu'on aurait pu mettre en application les concepts de GL, comme les design patterns, les notions OO, l'usine logiciel (etc.) avec un autre langage sans changer beaucoup de choses. Ce TP peut sembler un peu long en première approche (n'oubliez pas que vous devez le finir à la maison si vous n'avez pas fini à l'école).

Vous allez coder et tester un DAO (Data Access Object) en Java sous Eclipse. Vous utiliserez Maven pour construire votre application. Vous écrirez des classes Java, mais aussi des interfaces et des enums. Vous dessinerez des diagrammes UML. Vous allez manipuler des ressources en Java, gérer des exceptions, décoder des lignes, alimenter des listes. Vous tracerez vos méthodes à l'aide de Log4J. Vous utiliserez des bibliothèques comme OpenCsv ou CsvEnine pour lire des données dans des fichiers. Et bien entendu, vous aurez des travaux (identifiés par une icône en forme de calepin) à réaliser lors des étapes-clés et à rendre à la fin de la séance.

Des pistes et/ou des solutions sont proposées en annexe. Essayez de résoudre les problèmes par vous-même avant de regarder les réponses.

Demandez l'aide du professeur ou de son assistant si vous restez bloqué trop longtemps. Pensez tout de même à consulter la FAQ en annexe.

Si vous souhaitez installer Maven et Java sur votre ordinateur portable, vous pouvez consulter les FAQ en annexe. Vous y trouverez les points essentiels.

I-A - Versions des logiciels et bibliothèques utilisés

Pour écrire ce document, j'ai utilisé les versions suivantes :

- Java JDK 1.8 ;

- Eclipse Indigo 3.7 JEE 64b ;

- Maven 3.0.3 ;

- Log4J 1.2.13 ;

- JUnit 4.12 ;

- Open CSV 2.3 ;

- CSV Engine 1.3.5.

I-B - Mises à jour

1er décembre 2012 : Création.

6 mars 2013 : Ajout de questions dans la FAQ. Ajout de liens. Ajout d'un petit manuel d'installation à la maison.

14 avril 2014 : Ajout d'une vidéo pour illustrer les premières étapes.

1er septembre 2014 : Les travaux sont à désormais à rendre : une copie par élève (ou binôme).

22 novembre 2014 : Ajout d'une vidéo pour illustrer les premières étapes avec Eclipse Luna, quand on a le plugin Maven (m2).

1er novembre 2016 : Remplacement de tout le chapitre dédié à l'import dans Eclipse et à la préparation Maven.

II - Préparation

Avant d'entrer dans le vif du sujet, vous devez suivre quelques étapes simples (mais intéressantes) pour préparer le travail.

II-A - Télécharger le projet d'exemple

Durée estimée : 1 minute.

Pour commencer, s'il ne vous a pas été fourni par votre professeur, téléchargez le fichier "tp-chien-dao-01.zip" (15ko), contenant un projet Java-Maven d'exemple.

Décompressez le fichier "tp-chien-dao-01.zip".

Vous devez normalement avoir un dossier avec les éléments suivants :

- le dossier "src" où se trouvent les sources ;

- le fichier "pom.xml" pour Maven ;

- les fichiers "build.bat" et "build.sh" ;

- les fichiers "license.txt" et "README.txt" (ces deux derniers fichiers ne sont pas importants pour la suite).

Voici deux captures d'écran pour vous montrer à quoi cela ressemble sur MON ordinateur portable :

Dossier c:\TP\ avant la décompression

Dossier c:\TP\tp-chien-dao\ après la décompression

II-B - Import dans Eclipse (projet Maven)

Durée estimée : 2 minutes.

Importez le projet directement dans Eclipse car cet IDE sait traiter les projets Maven. Pour cela, lancez donc Eclipse si ce n'est pas déjà fait. Utilisez le menu "File/Import". Dans la popup il faut prendre "Maven/Existing Maven projet". Dans la seconde popup, utilisez le bouton "Browse" pour aller jusqu'au dossier que vous avez dézippé et où se trouve le fichier "pom.xml". Enfin cliquez sur le bouton "Finish". Eclipse devrait mouliner quelques instants.

Le fichier Maven "pom.xml" décrit la structure du projet d'exemple. Maintenant que le projet a été importé dans Eclipse, vous pouvez l'ouvrir directement. Vous auriez pu l'ouvrir avec Ultra Edit, VI, ou tout autre éditeur de code, mais pas avec Internet Explorer ou Word hein !... Voici un résumé des parties importantes à ce stade.

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>

<modelVersion>4.0.0</modelVersion>

<groupId>com.icauda</groupId>
<artifactId>tp-chien-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>TP Chien</name>

<properties>
<!-- Construction du projet -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven-compiler-plugin.version>3.6.0</maven-compiler-plugin.version>

<!-- Lib de test -->
<junit.version>4.12</junit.version>

<!-- Lib de log -->
<log4j.version>1.2.13</log4j.version>

...
</properties>

<dependencies>
<!-- Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>

...
</project>

Le fichier "pom.xml" indique ici une dépendance aux bibliothèques "jUnit" en version "4.12" et "Log4j" en version "1.2.13".

Les versions choisies ("4.12" et "1.2.13") sont tout simplement les dernières qui étaient disponibles quand j'ai écrit (ou mis à jour) ce document.

Dans la fenêtre principale d'Eclipse, vous pouvez distinguer la structure des projets Java-Maven :

- les dossiers "src/test/java" et "src/test/resources" contiennent respectivement des tests écrits en Java et des fichiers de configuration (ressources) pour les tests ;

- les dossiers "src/main/java" et "src/main/resources" contiennent le code du programme et les ressources associées ;

- le bloc "Referenced librairies" contient la liste des dépendances (bibliothèques) utilisées par le projet. Ce sont ces mêmes dépendances qui apparaissent dans le fichier "pom.xml" ;

- le bloc "JRE System Librairy" correspond simplement à la version de Java que vous utilisez ;

- enfin le dossier "target" (déjà présenté plus haut) contient la version compilée par Maven du projet.

Structure du projet

Ouvrez la classe "Launcher" que vous trouverez dans le package "com.icauda.tp.chien". Cette classe contient une méthode "main(..)" et est donc exécutable.

Classe Launcher

Lancez l'exécution de la classe "Launcher". Pour cela, sélectionnez la classe puis cliquez sur l'icône en forme de triangle (cf. capture). Si tout est bon, vous devriez voir apparaître des lignes dans la "console".

Exécution

Vous pouvez aussi lancer l'exécution à l'aide du bouton droit de la souris. Dans le menu contextuel qui apparaît, choisissez "Run as/Java application".

II-C - Quelques raccourcis utiles

Avant d'aller plus loin, voici quelques raccourcis qui devraient vous être utiles pour la suite.

Navigation : [F3] : Dans le programme, positionnez-vous sur une variable, une méthode ou une classe puis cliquez sur [F3] pour aller directement à la déclaration de l'élément.

Par exemple, dans la méthode "main(..)" de la classe "Launcher", cliquez sur "doFindChiensBidon" (ligne 39) puis tapez la touche [F3]. Ça vous emmènera à la déclaration de la méthode "doFindChiensBidon()" à la ligne 45.

Un autre exemple, maintenant que vous êtes sur la méthode "doFindChiensBidon()", cliquez sur "BidonChienDao" (ligne 49) puis tapez sur [F3]. Ça ouvre la classe "BidonChienDao" et ça vous positionne au niveau de la déclaration.

Classe BidonChienDao

Formatage : [Ctrl] + [Shift] + [F] : Vous pouvez/devez formater le code régulièrement. Cette combinaison de touche indente votre programme en fonction des paramètres configurés dans Eclipse.

Des fois, quand vous cherchez une erreur dans votre code, le simple fait de le formater vous aide à la détecter.

Vous devez formater le code avant de demander l'aide du professeur, pour qu'il y voie clair.

Import [Ctrl] + [Shift] + [M] : Cette combinaison vous permet d'importer une classe ou de faire un import statique. Pour cela, il faut cliquer sur l'élément, puis faire la combinaison de touches.

Organize imports [Ctrl] + [Shift] + [O] : Cette combinaison fait le ménage dans tous les imports. En particulier, elle supprime les imports de classe inutiles (i.e. importées, mais pas utilisées).

Pour en finir avec les raccourcis de votre IDE préféré, je vous propose un article plus détaillé, et qui vous permettra notamment de télécharger un petit mémento des raccourcis d'Eclipse.

II-D - Les objets du domaine

Durée estimée : 5 minutes.

Il est temps de découvrir les objets que vous allez utiliser dans ce TP. Sans surprise, vous avez sûrement déjà deviné que ça parle de chiens.

Le cœur du domaine est constitué par l'interface Chien, qui définit quelques méthodes simples :

Chien.java

public interface Chien extends Serializable {

String getNom();

String getNomComplet();

Sexe getSexe();

RaceDeChien getRace();

List<String> getCouleurs();

Double getPoids();

}

La classe SimpleChien est l'implémentation (par défaut) de Chien.

SimpleChien.java

public class SimpleChien implements Chien {

private String nom;
private String nomComplet;
private Sexe sexe;
private RaceDeChien race;
private List<String> couleurs;
private Double poids;

public SimpleChien() {
// rien...
}

public SimpleChien(final String nom) {
this.nom = nom;
}

public SimpleChien(final String nom, final RaceDeChien race, final Sexe sexe) {
this(nom);
this.race = race;
this.sexe = sexe;
}

@Override
public String toString() {
return "SimpleChien [nom=" + nom + "]";
}

// Getters et setters
...

}

Comme vous pouvez le constater, SimpleChien (et Chien) définit des attributs vers les enums Sexe et RaceDeChien, dont l'utilité ne devrait pas nécessiter d'explication.

Sexe.java

public enum Sexe {

FEMALE(2),
MALE(1);

private final int code;

Sexe(final int code) {
this.code = code;
}

public static Sexe valueOfByCode(final int code) {
switch (code) {
case 2:    return FEMALE;
case 1:    return MALE;
default: throw new IllegalArgumentException("Le sexe demande n'existe pas.");
}
}

public boolean isMale() {
return this == MALE;
}

public int getCode() {
return code;
}
}

RaceDeChien.java

public enum RaceDeChien {

BASSET_ALPES("Basset des Alpes", "basset_alp"),
BERGER_ALLEMAND("Berger allemand", "berger_all"),
CANICHE("Caniche", "caniche"),
HARRIER("Harrier", "harrier"),
GOLDEN("Golden retriever", "golden_ret"),
ROTTWEILER("Rottweiler", "rottweiler"),
WESTIE("Westie", "westie");

private final String label;
private final String code;

RaceDeChien(final String label, final String code) {
this.label = label;
this.code = code;
}

public static RaceDeChien valueOfByCode(final String code) {

// On verifie que le code n'est ni nul ni vide.
if (code == null || code.isEmpty()) {
throw new IllegalArgumentException("Le code ne peut pas etre vide.");
}

// Note : La methode "values()" renvoie la liste de toutes les
// "instances" ce cette enum (ie. BASSET_ALPES, CANICHE, HARRIER, etc.)

for (RaceDeChien race : values()) {
if (race.code.equalsIgnoreCase(code)) {
return race;
}
}

// Si on n'a pas trouve alors on lance une exception.
throw new IllegalArgumentException("La race de chien demandee n'existe pas.");
}

public String getLabel() {
return label;
}

public String getCode() {
return code;
}
}

Faites un diagramme de classe UML, incluant les quatre objets du domaine. Ce travail est à faire au stylo et à rendre à la fin de la séance.

II-E - DAO pour la lecture

Durée estimée : 5 minutes.

Le DAO (Data Access Object) ChienDao est également fourni avec le zip. Il définit la méthode "findAllChiens()" sur laquelle vous allez travailler dans la suite.

ChienDao.java

public interface ChienDao {

List<Chien> findAllChiens();
}

Pour commencer, et pour avoir du code à vous mettre sous la dent, vous trouverez BidonChienDao qui est une implémentation "bidon" de ChienDao. C'est d'ailleurs BidonChienDao qui est utilisée par Launcher.

BidonChienDao.java

public class BidonChienDao implements ChienDao {

@Override
public List<Chien> findAllChiens() {

final ArrayList<Chien> chiens = new ArrayList<Chien>();

// TODO : Remplacer RaceDeChien.XXX et Sexe.XXX par des imports static.

final SimpleChien milou = new SimpleChien("Milou", RaceDeChien.CANICHE, Sexe.MALE);
final SimpleChien lassie = new SimpleChien("Lassie", RaceDeChien.BERGER_ALLEMAND, Sexe.FEMALE);
final SimpleChien pluto = new SimpleChien("Pluto", RaceDeChien.GOLDEN, Sexe.MALE);

chiens.add(milou);
chiens.add(lassie);
chiens.add(pluto);

return chiens;
}
}

Comme son nom l'indique, BidonChienDao est une première version vraiment "bidon".

Pour que le code soit plus propre, cliquez sur "RaceDeChien.CANICHE" (sur la ligne correspondant à Milou) puis utilisez le raccourci [Ctrl] + [Shift] + [M] pour générer les imports static dans BidonChienDao. reproduisez l'opération sur les autres RaceDeChien.XXX et Sexe.XXX.

Pour l'instant, il n'y a que BidonChienDao qui implémente ChienDao. Dans la suite, vous allez ajouter des implémentations plus intéressantes.

Ajoutez ChienDao et BidonChienDao à votre diagramme de classe. Ce travail est à faire au stylo et à rendre à la fin de la séance.

III - Log4J

Durée estimée : 5 minutes.

Vous allez ajouter des "logs" dans le programme. Cela peut se faire à l'aide de "System.out.println(..)" comme c'est le cas dans Launcher mais ce n'est pas l'idéal. En effet, cette solution offre très peu d'options.

À la place, vous allez utiliser la bibliothèque Log4J. Pour cela, complétez la classe BidonChienDao pour qu'elle ressemble au code suivant. Il faut ajouter l'import de Logger, définir l'attribut LOGGER au niveau de la classe puis l'utiliser dans findAllChiens() avec le message que vous souhaitez afficher.

BidonChienDao.java

import org.apache.log4j.Logger;
...

public class BidonChienDao implements ChienDao {

private static final Logger LOGGER = Logger.getLogger(BidonChienDao.class);

public List<Chien> findAllChiens() {

LOGGER.debug("findAllChiens : debut");
...
}

Relancez Launcher. Vous devriez voir apparaître une nouvelle ligne dans la console.

Console

TP des chiens : DEBUT

Recherche bidon
0    [main] DEBUG com.icauda.tp.chien.dao.BidonChienDao  - findAllChiens : debut
Nombre de chiens : 3
Liste des chiens
SimpleChien [nom=Milou]
SimpleChien [nom=Lassie]
SimpleChien [nom=Pluto]
TP des chiens : FIN

Ajoutez un logueur dans Launcher puis remplacez les "System.out.println(..)" par des logs Log4J comme vous venez de le faire dans BidonChienDao. Quand vous relancerez Launcher, vous devriez avoir une trace ressemblant à la suivante :

Console

0    [main] DEBUG com.icauda.tp.chien.Launcher  - TP des chiens : DEBUT
1    [main] DEBUG com.icauda.tp.chien.Launcher  - Recherche bidon
2    [main] DEBUG com.icauda.tp.chien.dao.BidonChienDao  - findAllChiens : debut
5    [main] DEBUG com.icauda.tp.chien.Launcher  - Nombre de chiens : 3
5    [main] DEBUG com.icauda.tp.chien.Launcher  - Liste des chiens
5    [main] DEBUG com.icauda.tp.chien.Launcher  - SimpleChien [nom=Milou]
5    [main] DEBUG com.icauda.tp.chien.Launcher  - SimpleChien [nom=Lassie]
5    [main] DEBUG com.icauda.tp.chien.Launcher  - SimpleChien [nom=Pluto]
6    [main] DEBUG com.icauda.tp.chien.Launcher  - TP des chiens : FIN

La bibliothèque Log4J est notamment intéressante, car elle permet de spécifier un format de log mais aussi de dire si on doit logguer (ou non), à l'aide d'un fichier de configuration. Le fichier de configuration de Log4J se nomme "log4j.properties". Par exemple, on va logguer une grosse quantité d'informations durant le développement alors qu'on va limiter les logs sur les serveurs de production.

Déplacez le fichier "log4j.properties", qui se trouve dans "src/test/resources", vers "src/main/resources".

log4j.properties

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1

# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

Cherchez, dans la documentation de Log4J, comment configurer pour que les logs indiquent également les numéros des lignes.

Log4J propose plusieurs niveaux de log : fatal, error, warn, info et debug.

Changez le niveau de log, de DEBUG à INFO, dans le fichier de configuration pour que cela ressemble au code suivant :

log4j.properties

...
log4j.rootLogger=INFO, A1
...

Quand vous relancez Launcher, vous ne voyez plus aucune ligne dans la console.

Modifiez BidonChienDao pour qu'elle ressemble au code suivant. Ici, l'idée est de changer LOGGER.debug(..) en LOGGER.info(..) :

BidonChienDao.java

public class BidonChienDao implements ChienDao {

...
public List<Chien> findAllChiens() {

LOGGER.debug("findAllChiens : debut");

LOGGER.debug("Une info utile en dev.");
LOGGER.info("Une info importante pour la prod.");
...

Quand vous relancez Launcher, vous ne devriez voir que la ligne qui utilise LOGGER.info(..) :

Console

0    [main] INFO  com.icauda.tp.chien.dao.BidonChienDao  - Une info importante pour la prod.

Dans la suite, vous allez surtout développer. Remettez donc le niveau de log à "DEBUG" dans "log4j.properties".

Console

0    [main] DEBUG com.icauda.tp.chien.Launcher  - TP des chiens : DEBUT
1    [main] DEBUG com.icauda.tp.chien.Launcher  - Recherche bidon
2    [main] DEBUG com.icauda.tp.chien.dao.BidonChienDao  - findAllChiens : debut
2    [main] DEBUG com.icauda.tp.chien.dao.BidonChienDao  - Une info utile en dev.
2    [main] INFO  com.icauda.tp.chien.dao.BidonChienDao  - Une info importante pour la prod.
4    [main] DEBUG com.icauda.tp.chien.Launcher  - Nombre de chiens : 3
4    [main] DEBUG com.icauda.tp.chien.Launcher  - Liste des chiens
4    [main] DEBUG com.icauda.tp.chien.Launcher  - SimpleChien [nom=Milou]
4    [main] DEBUG com.icauda.tp.chien.Launcher  - SimpleChien [nom=Lassie]
4    [main] DEBUG com.icauda.tp.chien.Launcher  - SimpleChien [nom=Pluto]
5    [main] DEBUG com.icauda.tp.chien.Launcher  - TP des chiens : FIN

Les niveaux de logs de Log4J fonctionnent comme des poupées russes. Chaque niveau englobe le(s) niveau(x) inférieur(s). Cherchez la hiérarchie des niveaux prédéfinis dans la documentation et représentez la graphiquement. Ce travail est à faire au stylo et à rendre à la fin de la séance.

Il est également possible de configurer les logs à l'aide de fichiers XML, et donc pas seulement à l'aide de fichiers plats.

IV - CSV

Dans cette partie, vous allez utiliser des fichiers CSV. C'est un format d'échange simple et encore très utilisé dans l'industrie.

IV-A - Découverte du format

Durée estimée : 1 minute.

Les fichiers CSV (Comma Separated Values) sont des fichiers texte qui contiennent des données réparties par ligne. Les données de chaque ligne sont séparées par des virgules. L'ensemble forme ainsi un tableau de valeurs en deux dimensions.

Certains pays utilisent d'autres caractères que la virgule comme séparateur. En France on utilise plus volontiers le point-virgule, le trait ou même une tabulation. On préfère en effet réserver la virgule pour les nombres décimaux.

Dans un fichier CSV, les lignes commençant par le caractère dièse sont des commentaires. Les lignes vides ne comptent pas. La première ligne qui n'est ni un commentaire ni vide correspond aux entêtes des colonnes.

Le projet contient plusieurs fichiers CSV, dont "chiens-01.csv" qui liste des chiens et leurs attributs.

chiens-01.csv

# Chiens

Nom;Nom complet;sexe;race;couleurs;poids

Milou;Milou de Belgique;1;caniche;blanc;12,5
Pluto;Pluto le chien Disney;1;golden_ret;jaune;24,0
Lassie;Lassie des alpes;2;berger_all;blanc,noir;32,3
Volt;Volt le super chien;1;caniche;blanc,noir;14,0
Medor;Medor;1;rottweiler;gris;32,0

Dans Eclipse, pour ouvrir un fichier CSV, il faut faire un clic droit sur le nom du fichier puis choisir le menu "Open with/Text editor". Ne double-cliquez pas sur le fichier, sinon ça va l'ouvrir dans Excel.

Ici, on remarque que le séparateur utilisé est le point-virgule, bien que rien ne l'indique dans le fichier. Il y a six colonnes dont les entêtes sont "Nom", "Nom complet", "sexe", "race", "couleurs", et "poids". Enfin il y a cinq lignes de données, correspondant ici aux attributs de cinq chiens : "Milou", "Pluto", "Lassie", "Volt" et "Medor".

Pour mieux voir la structure du fichier, vous pouvez ajouter (dans une copie du fichier) des espaces. Le fichier modifié ressemblera alors au suivant.

chiens-01.csv modifié

# Chiens

Nom;     Nom complet;             sexe;    race;        couleurs;    poids

Milou;   Milou de Belgique;       1;       caniche;     blanc;       12,5
Pluto;   Pluto le chien Disney;   1;       golden_ret;  jaune;       24,0
Lassie;  Lassie des alpes;        2;       berger_all;  blanc,noir;  32,3
Volt;    Volt le super chien;     1;       caniche;     blanc,noir;  14,0
Medor;   Medor;                   1;       rottweiler;  gris;        32,0

IV-B - Tests génériques avec JUnit

Durée estimée : 15 minutes.

Avant même de coder le DAO qui lira le fichier CSV, vous allez écrire des tests permettant de valider que le programme fonctionne. Cette manière de faire s'inscrit dans une démarche de qualité.

Dans les annexes et sur le Web, vous trouverez une liste d'articles qui abordent ce principe, sous les noms TDD (Test Driven Developpement) ou 3T (Tests en Trois Temps).

Pour commencer, créez un package nommé "com.icauda.tp.chien.dao.csv" dans "src/test/java". Créez ensuite une classe nommée "AbstractCsvChienDaoTest" dans ce package en cochant l'option "abstract" dans la popop. Ajoutez ensuite un peu de doc dans cette classe. Pour finir, déclarez un logger comme expliqué plus haut.

AbstractCsvChienDaoTest.java

package com.icauda.tp.chien.dao.csv;

/**
* Classe de test generique pour les DAO de chien en CSV.
*
* @author Thierry Leriche-Dessirier
*
*/
public abstract class AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(AbstractCsvChienDaoTest.class);
}

Si vous avez oublié de cocher la case "abstract", vous pouvez compléter le code directement.

Ajoutez ensuite deux constantes pour indiquer le fichier CSV que vous allez utiliser.

AbstractCsvChienDaoTest.java

public abstract class AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(AbstractCsvChienDaoTest.class);

private final static String RESOURCES_PATH = "src/test/resources/";
private final static String CHIENS_FILE_NAME = "chiens-01.csv";
}

Comme vous voulez tester un DAO, il faut ajouter une variable :

AbstractCsvChienDaoTest.java

import com.icauda.tp.chien.dao.ChienDao;
...

public abstract class AbstractCsvChienDaoTest {
...
protected CsvChienDao dao;
}

Ici, le DAO est ajouté en "protected" car vous allez faire de l'héritage dans la suite.

Vous allez maintenant préparer l'exécution de vos tests. Pour cela, vous allez créer la méthode "doBefore()", annotée "@Before", qui va alimenter votre fichier.

AbstractCsvChienDaoTest.java

import java.io.File;
import org.junit.Before;
...

public abstract class AbstractCsvChienDaoTest {

protected CsvChienDao dao;
...

@Before
public void doBefore() {
LOGGER.debug("doBefore Debut");

final File file = new File(RESOURCES_PATH + CHIENS_FILE_NAME);
dao.init(file);

LOGGER.debug("doBefore Fin");
}
}

Les méthodes annotées avec "@Before" sont lancées avant chaque test.

Vous pouvez maintenant ajouter des tests, correspondant à votre jeu de tests (ex. chiens-01.csv). Comprenez bien que les tests seront spécifiques à ce jeu de tests. Chaque test ne doit tester qu'une seule chose (ou qu'une seule série de choses). Les méthodes de test doivent être annotées avec "@Test" :

AbstractCsvChienDaoTest.java

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
...

public abstract class AbstractCsvChienDaoTest {

...
protected CsvChienDao dao;
...

/**
* Teste le nombre de chiens trouves.
*
* RESULT Nombre chiens : 5
*/
@Test
public void testCinqChiens() {
LOGGER.debug("testCinqChiens... Debut");

// Arrange
final int nombreChiensAttendus = 5;

// Act
final List<Chien> chiens = dao.findAllChiens();

// Assert
Assert.assertEquals(nombreChiensAttendus, chiens.size());

LOGGER.debug("testCinqChiens... Fin");
}

/**
* Teste que le premier chien est bien Milou.
*
* PARAM position : 0
* RESULT nom : Milou
*/
@Test
public void testPremierEstMilou() {
LOGGER.debug("testFirstIsMilou... Debut");

// Arrange
final int position = 0;
final String nomAttendu = "Milou";

// Act
final List<Chien> chiens = dao.findAllChiens();
final Chien chien = chiens.get(position);

// Assert
Assert.assertEquals(nomAttendu, chien.getNom());

LOGGER.debug("testFirstIsMilou... Fin");
}
}

Au passage, vous pouvez constater que c'est le formalisme AAA (Arrange Act Assert) qui a été choisi pour bien présenter/organiser le code des tests.

La méthode "get(..)" d'une List renvoie l'élément qui se trouve à la position indiquée en paramètre. En Java, les positions commencent à zéro.

Ajoutez des tests intéressants. Essayez de tester le plus de choses (différentes) possible. Par exemple, vous pouvez tester l'ordre des chiens, le poids de Lassie, les couleurs de la fourrure de Volt, la race de Pluto, etc. Jouez le jeu en ne regardant pas tout de suite les propositions de réponse en annexe. De manière générale, plus votre code sera testé et plus il sera légitime de penser qu'il sera fiable. N'oubliez pas de documenter votre code. Ce travail n'est pas à rendre par écrit. Appelez le professeur pour qu'il vous valide cette étape.

JUnit propose de nombreuses méthodes pour compléter "assertEquals(..)" :

- assertTrue(..) et assertFalse(..) vérifient qu'une condition est vraie ou fausse ;

- assertNull(..) et assertNotNull(..) vérifient qu'une variable est nulle ou non ;

- assertSame(..) et assertNotSame(..) vérifient des équivalences ;

- assertArrayEquals(..) teste les contenus de tableaux ;

- assertThat(..) dont la prise en main est délicate ;

- etc.

Ajoutez la classe "NaiveCsvChienDaoTest" étendant "AbstractCsvChienDaoTest" dans le package de test "com.icauda.tp.chien.dao.csv". C'est dans le constructeur de "NaiveCsvChienDaoTest" que vous allez spécifier votre DAO cible.

NaiveCsvChienDaoTest.java

package com.icauda.tp.chien.dao.csv;

/**
* Classe de test de NaiveCsvChienDao.
*
* @author Thierry Leriche-Dessirier
*
*/
public class NaiveCsvChienDaoTest extends AbstractCsvChienDaoTest {

public NaiveCsvChienDaoTest() {
dao = new NaiveCsvChienDao();
}

}

Pour finir, lancez l'exécution des tests de NaiveCsvChienDaoTest. Pour cela, sur le nom de la classe, à l'aide d'un clic droit, lancez le menu "Run as/JUnit test".

Run as/JUnit test

Vous constatez que ça lance bien tous vos tests et qu'ils sont tous en rouge pour signaler un échec. À ce stade, c'est normal ; c'est justement ce que l'on veut.

Tests en erreur

Utilisez le raccourci [Ctrl] + [Shift] + M sur "Assert.assertEquals" pour ajouter l'import statique de "assertEquals" et alléger votre code. Profitez-en pour formater le code à l'aide de [Ctrl] + [Shift] + F si ce n'est pas déjà fait.

En vous inspirant de ce que vous venez de coder (AbstractCsvChienDaoTest, NaiveCsvChienDaoTest, etc.) pour tester la (future) lecture de "chiens-01.csv", codez de nouvelles classes pour tester le jeu de tests "chiens-02.csv". N'oubliez pas de compléter "RaceDeChien" avec les valeurs manquantes. Ce travail n'est pas à rendre par écrit. Appelez le professeur pour qu'il vous valide cette étape.

IV-C - Lecture naïve

Durée estimée : 15 minutes.

Dans un premier temps, vous allez coder une lecture du fichier CSV sans chercher à optimiser le code. Vous allez surtout bien séparer les différentes étapes, car il y a beaucoup de choses à comprendre.

Pour lire naïvement un fichier CSV, il a principalement trois étapes :

- charger les lignes du fichier en les stockant dans une liste ou un tableau ;

- décoder chaque ligne ;

- transformer les champs des lignes en objet Chien.

Cela se traduit par le code suivant, encore très incomplet comme vous pouvez le constater :

NaiveCsvChienDao.java

import java.util.ArrayList;
...

public class NaiveCsvChienDao implements CsvChienDao {

...

private List<String> getLignesFromFile() throws Exception {
throw new UnsupportedOperationException("Cette methode n'est pas encore programmee.");
}

private Chien transformLigneToChien(final String ligne) throws Exception {
throw new UnsupportedOperationException("Cette methode n'est pas encore programmee.");
}

@Override
public List<Chien> findAllChiens() {

LOGGER.debug("Chargement des chiens.");

try {
final List<String> lignes = getLignesFromFile();

final List<Chien> chiens = new ArrayList<Chien>();
for (String ligne : lignes) {
final Chien chien = transformLigneToChien(ligne);
chiens.add(chien);
}

return chiens;

} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
return nul;
}

}

La méthode "findAllChiens()" vous permet simplement de voir l'algorithme général, dans ses grandes lignes, sans jeu de mots…

Relancez vos tests. Vous constatez que ce n'est plus "findAllChiens()" qui les fait échouer, mais "getLignesFromFile()". Vous allez maintenant vous occuper de "getLignesFromFile()" et de "transformLigneToChien(..)" en même temps et voir comment les résultats de vos tests évoluent au fur et à mesure.

NaiveCsvChienDao.java

public class NaiveCsvChienDao implements CsvChienDao {

...

private List<String> getLignesFromFile() throws Exception {

LOGGER.debug("getLignesFromFile");

final List<String> lignes = new ArrayList<String>();

// TODO ...

return lignes;

}

private Chien transformLigneToChien(final String ligne) throws Exception {

final SimpleChien chien = new SimpleChien();

// TODO...

return chien;
}

Relancez les tests. Ils échouent toujours, mais plus pour la même raison. Cette fois ça dit "expected:5 but was:0", ce qui est relativement cohérent avec le code.

Détails de la log JUnit

java.lang.AssertionError: expected:5 but was:0
at org.junit.Assert.fail(Assert.java:91)
at org.junit.Assert.failNotEquals(Assert.java:645)
at org.junit.Assert.assertEquals(Assert.java:126)
at org.junit.Assert.assertEquals(Assert.java:470)
at org.junit.Assert.assertEquals(Assert.java:454)
at com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest.testCinqChiens(AbstractCsvChienDaoTest.java:54)
...

Les numéros de ligne indiqués dans la trace peuvent varier en fonction du formatage utilisé dans le code.

Il existe plusieurs manières de lire un fichier texte. La plus courante est d'utiliser une combinaison des classes "FileReader" et "BufferedReader". La première vous permet de lire le contenu d'un fichier (sous la forme de flux) et la seconde vous permet de manipuler des lignes.

NaiveCsvChienDao.java

private List<String> getLignesFromFile() throws Exception {

LOGGER.debug("getLignesFromFile");

final List<String> lignes = new ArrayList<String>();

final FileReader fr = new FileReader(file);
final BufferedReader br = new BufferedReader(fr);

// TODO ...

return lignes;

}

Ce qu'il ne faut pas oublier, c'est de fermer ces "ressources" quand vous n'en avez plus besoin. Évidemment, il faut les fermer dans l'ordre inverse de leur ouverture…

NaiveCsvChienDao.java

private List<String> getLignesFromFile() throws Exception {
...

final FileReader fr = new FileReader(file);
final BufferedReader br = new BufferedReader(fr);

// TODO ...

br.close();
fr.close();
}

Il ne reste plus qu'à parcourir le fichier :

NaiveCsvChienDao.java

private List<String> getLignesFromFile() throws Exception {
...

for (String ligne = br.readLine(); ligne != null; ligne = br.readLine()) {
lignes.add(ligne);
}

...
}

N'ayez pas peur. C'est une simple boucle "for" en trois parties : initialisation, condition d'arrêt et incrémentation.

Il faut maintenant filtrer les lignes vides ou de commentaire :

NaiveCsvChienDao.java

private List<String> getLignesFromFile() throws Exception {
...

for (String ligne = br.readLine(); ligne != null; ligne = br.readLine()) {

// Suppression des espaces en trop
ligne = ligne.trim();

// Filtre des lignes vides
if(ligne.isEmpty()) {
continue;
}

// Filtre des lignes de commentaire
if(ligne.startsWith("#")) {
continue;
}

lignes.add(ligne);
}

...
}

Relancez les tests. Vous constatez que "testCinqChiens" échoue, car il attend cinq chiens, mais en trouve six. C'est parce que vous n'avez pas encore filtré la première ligne, correspondant aux titres.

NaiveCsvChienDao.java

public List<Chien> findAllChiens() {

...

try {
final List<String> lignes = getLignesFromFile();

final String ligneEntete = lignes.remove(0);
LOGGER.debug("Entetes : " + ligneEntete);
...

Relancez encore les tests. Cette fois "testCinqChiens" devrait passer au vert, ce qui signifie qu'on en a "fini" avec la méthode "getLignesFromFile()".

Le plus dur est fait. Il ne reste plus qu'à décoder les lignes. Pour commencer, il faut séparer les colonnes.

NaiveCsvChienDao.java

private Chien transformLigneToChien(final String ligne) throws Exception {

final SimpleChien chien = new SimpleChien();

final String separator = ";";
final String[] values = ligne.split(separator);

// TODO...

return chien;
}

Regardez la doc Java de la méthode "split(..)" pour bien comprendre comment elle fonctionne.

Vous pouvez maintenant commencer à remplir l'objet Chien.

NaiveCsvChienDao.java

private Chien transformLigneToChien(final String ligne) throws Exception {

final SimpleChien chien = new SimpleChien();

final String separator = ";";
final String[] values = ligne.split(separator);

chien.setNom(values[0]);
// TODO...

return chien;
}

Relancez les tests. À ce stade, le test "testPremierEstMilou" devrait lui aussi passer au vert. On approche de la solution.

La suite du document suppose que vous ayez ajouté d'autres tests. À titre d'illustration (cf. annexes), j'ai ajouté "testPoidsLassie", "testCouleursVolt" et "testRacePluto".

Vous pouvez passer aux autres attributs :

NaiveCsvChienDao.java

private Chien transformLigneToChien(final String ligne) throws Exception {

final SimpleChien chien = new SimpleChien();

final String separator = ";";
final String[] values = ligne.split(separator);

chien.setNom(values[0]);
chien.setNomComplet(values[1]);

final String tempSexe = values[2];
final Sexe sexe = Sexe.valueOfByCode(new Integer(tempSexe));
chien.setSexe(sexe);

final String tempRace = values[3];
final RaceDeChien race = RaceDeChien.valueOfByCode(tempRace);
chien.setRace(race);

// TODO...

return chien;
}

Quand vous relancez les tests, "testRacePluto" devrait passer au vert.

Complétez la méthode de transformation pour les attributs couleurs et poids. Pour les couleurs, vous pouvez à nouveau faire appel à "split(..)". Pour le poids, n'oubliez pas que les nombres utilisent des points et non de virgules en Java.

À ce stade, tous vos tests (y compris ceux que vous avez inventés vous-même) devraient passer au vert. Vous êtes donc légitimement en droit de penser que le travail est fini.

Tous les tests sont verts

Prenez quelques instants pour analyser comment les logs s'enchaînent dans la console. Vérifiez surtout que les traces de "doBefore()" apparaissent bien avant chaque test.

Vous allez maintenant utiliser ce nouveau DAO dans votre Launcher. Pour cela, inspirez-vous du code suivant :

Launcher.java

public class Launcher {

public static void main(String[] args) {
LOGGER.debug("TP des chiens : DEBUT");

doFindChiensBidon();
doFindChiensNaif();

...

private static void doFindChiensNaif() {
LOGGER.debug("Recherche naïve");

final String fileName = "src/main/resources/chiens.csv";
final File file = new File(fileName);

final CsvChienDao dao = new NaiveCsvChienDao();
dao.init(file);

final List<Chien> chiens = dao.findAllChiens();

final int nombreDeChiens = chiens.size();
LOGGER.debug("Nombre de chiens : " + nombreDeChiens);

printChiens(chiens);
}

...

Ajoutez quelques lignes, à votre convenance, dans "src/main/resources/chiens.csv". Vous pouvez copier les lignes de "chiens-01.csv" si vous n'avez pas d'idée.

Vous pouvez relancer Launcher et vérifier les logs produits.

Faites un diagramme de séquence UML, ilustrant le fonctionnement de la méthode "findAllChiens" en incluant les appels aux autres classes/méthodes utilisées (hors getter/setter). Ce travail est à faire au stylo et à rendre à la fin de la séance.

IV-D - Version améliorée

Durée estimée : 30 minutes.

La version naïve du DAO peut très clairement être améliorée. Ce qui ne va pas changer (ou presque, cf. entêtes), ce sont les tests.

Créez la classe AdvancedCsvChienDao et la classe de test AdvanceCsvChienDaoTest, ressemblant aux classes déjà écrites, en vous inspirant du code suivant :

AdvancedCsvChienDao.java

public class AdvancedCsvChienDao implements CsvChienDao {
private static final Logger LOGGER = Logger.getLogger(AdvancedCsvChienDao.class);

private File file;

@Override
public void init(File file) {
LOGGER.debug("init");
this.file = file;
}

@Override
public List<Chien> findAllChiens() {
LOGGER.debug("findAllChiens");
throw new UnsupportedOperationException("pas encore fait");
}

@Override
public File getFile() {
return file;
}
}

AdvanceCsvChienDaoTest.java

public class AdvanceCsvChienDaoTest extends AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(AdvanceCsvChienDaoTest.class);

public AdvanceCsvChienDaoTest() {
LOGGER.debug("Constructeur...");

dao = new AdvancedCsvChienDao();
}
}

Lancez la nouvelle classe de test. Sans surprise, tout est rouge. Vous allez maintenant refaire le même travail qu'un peu plus tôt, mais de manière légèrement différente.

D'abord, faites en sorte que la lecture du fichier ne soit réalisée qu'à l'initialisation (appel de "init") et non à chaque fois qu'on appelle "findAllChiens()" comme c'était le cas dans la version naïve, en vous inspirant du code suivant :

AdvancedCsvChienDao.java

public class AdvancedCsvChienDao implements CsvChienDao {

private static final Logger LOGGER = Logger.getLogger(AdvancedCsvChienDao.class);

private File file;
private List<Chien> chiens;

@Override
public void init(File file) {
LOGGER.debug("init");
this.file = file;

// On relance une lecture a chaque initialisation, ce qui permet de
// changer de fichier, ou de recharger ledit fichier.
reloadChiens();
}

/**
* Chargement des chiens.
*/
private void reloadChiens() {
LOGGER.debug("findAllChiens");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

throw new UnsupportedOperationException("Bientot...");

}

@Override
public List<Chien> findAllChiens() {
LOGGER.debug("findAllChiens");

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chiens;
}

...

Pour la suite, il suffit de reprendre ce que vous aviez déjà fait dans la version naïve… Commencez par ajouter, avec un simple copier-coller, les méthodes "getLignesFromFile()" et "transformLigneToChien(..)" puis modifiez "reloadChiens()" en vous inspirant du code suivant :

AdvancedCsvChienDao.java

private void reloadChiens() {
LOGGER.debug("findAllChiens");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

try {
final List<String> lignes = getLignesFromFile();

final String ligneEntete = lignes.remove(0);
LOGGER.debug("Entetes : " + ligneEntete);

chiens = new ArrayList<Chien>(lignes.size());
for (String ligne : lignes) {
final Chien chien = transformLigneToChien(ligne);
chiens.add(chien);
}

} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}

}

Notez qu'on initialise directement la liste des chiens avec le bon nombre d'items :

AdvancedCsvChienDao.java

chiens = new ArrayList<Chien>(lignes.size());

Relancez les tests. Tout doit être vert. Par acquit de conscience, relancez également les tests de NaiveCsvChienDaoTest. Ils devraient rester verts, sauf si vous avez fait une mauvaise manipulation lors du copier-coller.

Pour la suite, vous allez coder une meilleure gestion des ressources. En effet, que se passe-t-il (dans la version naïve) lorsqu'une exception survient durant la boucle "for" ? Ça sort tout simplement de la méthode en lançant une exception et vos appels aux méthodes "close()" ne sont pas réalisés… Vous devez obligatoirement permettre ces appels en vous inspirant du code suivant :

AdvancedCsvChienDao.java

private List<String> getLignesFromFile() {

LOGGER.debug("getLignesFromFile");

final List<String> lignes = new ArrayList<String>();

FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader(file);
br = new BufferedReader(fr);

for (String ligne = br.readLine(); ligne != null; ligne = br.readLine()) {

// Suppression des espaces en trop
ligne = ligne.trim();

// Filtre des lignes vides
if (ligne.isEmpty()) {
continue;
}

// Filtre des lignes de commentaire
if (ligne.startsWith("#")) {
continue;
}

lignes.add(ligne);
}
} catch (IOException e) {
LOGGER.error("Lecture impossible", e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
LOGGER.error("Fermeture impossible", e);
}
}
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
LOGGER.error("Fermeture impossible", e);
}
}
}

return lignes;
}

Vous devez connaître ce code par cœur. En Java 7, il existe les "try-with-resources" ("try améliorés"), mais encore peu d'entreprises utilisent Java 7 pour l'instant.

IV-D-1 - Entêtes

Dans la version naïve, on sautait la ligne des entêtes et on perdait de l'information. Vous allez maintenant rectifier cela en vous inspirant du code suivant :

AdvancedCsvChienDao.java

public class AdvancedCsvChienDao implements CsvChienDao {

private final static String SEPARATOR = ";";
private List<String> entetes;
...

private void transformEntetes(final String ligneEntete) {
LOGGER.debug("transformEntetes");

final String[] tabEntetes = ligneEntete.split(SEPARATOR);

entetes = new ArrayList<String>(tabEntetes.length);

for (String entete : tabEntetes) {
entetes.add(entete);
}
}

private void reloadChiens() {
...
try {
final List<String> lignes = getLignesFromFile();

final String ligneEntete = lignes.remove(0);
LOGGER.debug("Entetes : " + ligneEntete);
transformEntetes(ligneEntete);

chiens = new ArrayList<Chien>(lignes.size());
...

public List<String> getEntetes() {
return entetes;
}
...

Relancez les tests pour vérifier que tout est toujours bon.

Au point où vous en êtes, vous vous dites que les entêtes sont importants et que vous allez carrément les imposer à tous les DAO en le spécifiant dans l'interface :

AdvancedCsvChienDao.java

public interface CsvChienDao extends ChienDao {

public void init(File file);

public File getFile();

public List<String> getEntetes();
}

Du coup, vous pouvez maintenant annoter "getEntetes()" avec "@Override" :

AdvancedCsvChienDao.java

@Override
public List<String> getEntetes() {
return entetes;
}

Et surtout, vous devez compléter NaiveCsvChienDao pour que ça continue de compiler. Comme vous ne souhaitez pas investir de temps supplémentaire sur cette (mauvaise) implémentation, vous allez simplement renvoyer une exception.

AdvancedCsvChienDao.java

public class NaiveCsvChienDao implements CsvChienDao {
...

@Override
public List<String> getEntetes() {
throw new UnsupportedOperationException("Fonction non disponible, et ne le sera jamais...");
}

Relancez tous les tests pour vérifier que ça fonctionne toujours.

Sans surprise, vous allez maintenant ajouter des tests pour valider cette nouvelle fonctionnalité. Notez que vous auriez même dû le faire avant d'écrire la fonctionnalité. Mais disons que vous n'y aviez pas pensé (n'est-ce pas ?) dans le feu de l'action…

AbstractCsvChienDaoTest.java

public class AbstractCsvChienDaoTest implements CsvChienDao {
...

@Test
public void testTailleEntetes() {
LOGGER.debug("testTailleEntetes... Debut");

// Arrange
final int tailleAttendue = 6;

// Act
final List<String> entetes = dao.getEntetes();

// Assert
Assert.assertEquals(tailleAttendue, entetes.size());

LOGGER.debug("testTailleEntetes... Fin");
}

Relancez tous les tests. Vous constatez que les tests de AdvanceCsvChienDaoTest passent au vert, mais qu'il y a un échec sur NaiveCsvChienDaoTest. C'est normal puisque NaiveCsvChienDaoTest renvoie une exception directement. Ici, puisque vous ne souhaitez plus investir de temps sur la version naïve, vous avez deux solutions : soit vous supprimez purement et simplement cette version, soit vous gérez un cas particulier dans les tests. Pour vous montrer la gestion des exceptions attendues dans les tests, c'est la seconde solution que vous allez coder en surchargeant la méthode "testTailleEntetes()" :

NaiveCsvChienDaoTest.java

public class NaiveCsvChienDaoTest extends AbstractCsvChienDaoTest {

...

@Test(expected = UnsupportedOperationException.class)
@Override
public void testTailleEntetes() {
LOGGER.debug("testTailleEntetes... Debut");

// Arrange

// Act
final List<String> entetes = dao.getEntetes();

// Assert

LOGGER.debug("testTailleEntetes... Fin");
}

Relancez tous les tests pour vérifier que tout remarche.

Ajoutez d'autres tests des entêtes, par exemple pour en vérifier l'ordre.

Faites un diagramme de séquence UML, incluant les quatre objets du domaine. Ce travail est à faire au stylo et à rendre à la fin de la séance.

IV-D-2 - Recherche

Vous n'allez tout de même pas vous arrêter en si bon chemin ? Maintenant que vous savez charger la liste des chiens, vous avez (très) envie de faire des recherches dans cette liste, par exemple en cherchant "Lassie".

Modifiez donc l'interface ChienDao en vous inspirant du code suivant :

NaiveCsvChienDaoTest.java

public interface ChienDao {
...

Chien findChienByNom(final String nom);

Complétez BidonChienDao, NaiveCsvChienDao et AdvancedCsvChienDao pour que ces classes compilent. Dans la suite, on ne s'intéressera qu'à AdvancedCsvChienDao…

AdvancedCsvChienDao.java

...

@Override
public Chien findChienByNom(String nom) {
throw new UnsupportedOperationException("Bientot dispo");
}

Cette fois, vous n'allez pas vous laisser emporter par la musique. Vous allez écrire les tests avant de coder cette fonction de recherche :

AbstractCsvChienDaoTest.java

public abstract class AbstractCsvChienDaoTest {

/**
* Teste la recherche d'un chien.
*
* PARAM nom : Lassie <br/>
* RESULT poids : 32.3
*/
@Test
public void testRechercheLassie() {
LOGGER.debug("testRechercheLassie... Debut");

// Arrange
final String nom = "Lassie";
final Double poidsAttendu = 32.3;

// Act
final Chien chien = dao.findChienByNom(nom);

// Assert
Assert.assertNotNull(chien);
Assert.assertEquals(nom, chien.getNom());
Assert.assertEquals(poidsAttendu, chien.getPoids());

LOGGER.debug("testRechercheLassie... Fin");
}

/**
* Teste la recherche d'un chien qui n'est pas dans la liste.
*
* PARAM nom : Idefix <br/>
* RESULT null
*/
@Test
public void testRechercheIdefix() {
LOGGER.debug("testRechercheIdefix... Debut");

// Arrange
final String nom = "Idefix";

// Act
final Chien chien = dao.findChienByNom(nom);

// Assert
Assert.assertNull(chien);

LOGGER.debug("testRechercheIdefix... Fin");
}

C'est le moment de lancer les tests. Les deux nouveaux tests doivent être en échec.

Comme vous l'avez fait précédemment, vous pouvez maintenant coder la fonctionnalité de recherche en lançant les tests au fur et à mesure. Voici une première version naïve :

AdvancedCsvChienDao.java

public class AdvancedCsvChienDao implements CsvChienDao {

...

@Override
public Chien findChienByNom(final String nom) {

if(nom == null || nom.isEmpty()) {
throw new IllegalArgumentException("Le nom ne peut pas etre vide.");
}

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

for(Chien chien : chiens) {
if(nom.trim().equalsIgnoreCase(chien.getNom())) {
return chien;
}
}

// Si pas trouve...
return nul;
}

Le truc moche, avec cette première version, c'est qu'il faut parcourir la liste à chaque fois qu'une recherche est lancée. Ce n'est donc pas très performant. On aimerait donc bien mettre en place un système d'indexation (cache) pour améliorer les performances.

Regardez la documentation de HashMap pour comprendre comment elle fonctionne. "HashMap" est l'implémentation de "Map" la plus utilisée. Ses deux méthodes les plus importantes sont "put(..)" et "get(..)". Indexez les données à l'aide d'une Map.

Le code devrait (plusieurs solutions possibles) maintenant ressembler au suivant, avec votre solution à la place des "XXX" :

AdvancedCsvChienDao.java

public class AdvancedCsvChienDao implements CsvChienDao {

private Map<String, Chien> chienMapByNom;
...

@Override
public Chien findChienByNom(final String nom) {

private void reloadChiens() {
...

chiens = new ArrayList<Chien>(lignes.size());
chienMapByNom = new HashMap<String, Chien>(lignes.size());
for (String ligne : lignes) {
final Chien chien = transformLigneToChien(ligne);
chiens.add(chien);

XXX
}
...

@Override
public Chien findChienByNom(final String nom) {

if(nom == null || nom.isEmpty()) {
throw new IllegalArgumentException("Le nom ne peut pas etre vide.");
}

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return XXX
}

...

C'est le nom du chien qui sert de clé. Que se passe-t-il si deux chiens (ou plus) ont le même nom ? La réponse à cette question est aussi à rendre à la fin de la séance. N'hésitez pas à modifier le code pour vous aider à répondre. Ce travail est à faire au stylo et à rendre à la fin de la séance.

Comme toujours, pensez à lancer les tests pour vérifier que tout est toujours vert.

Vous pourriez encore ajouter de nombreuses fonctionnalités (tris, comptages, recherches croisées, etc.), mais vous pouvez en rester là pour ce TP.

Quand vous aurez le temps, je vous invite à lire l'article Les fichiers CSV avec Java qui détaille les contraintes liées à la lecture d'un fichier CSV.

IV-E - OpenCsv

Durée estimée : 15 minutes.

Comme vous venez de le constater, c'est relativement simple de lire des fichiers CSV en Java. Toutefois, c'est sans compter avec les optimisations plus complexes, la lecture des (très) gros fichiers, avec une forte volumétrie, etc. Et puis, surtout, vous n'avez pas envie de consacrer du temps à redévelopper des fonctionnalités que plusieurs bibliothèques gratuites réalisent déjà très bien.

C'est important de l'avoir fait au moins une fois, pour savoir de quoi il s'agit, ne serait-ce que pour les points essentiels.

Vous allez maintenant utiliser la bibliothèque "Open CSV" qui s'impose comme l'une des meilleures du marché. Pour cela, vous devez modifier votre configuration Maven (ex. pom.xml) comme suit :

pom.xml

...

<properties>
...

<!-- Lib CSV -->
<opencsv.version>2.3</opencsv.version>

...
</properties>

<dependencies>
...

<!-- Lib CSV -->
<dependency>
<groupId>net.sf.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>${opencsv.version}</version>
</dependency>

...
</dependencies>

...

Relancez une compilation Maven à l'aide de la commande "build" comme vous l'avez fait au début de ce TP. En fonction des éléments déjà présents sur votre machine, le résultat devrait ressemble à la trace suivante.

build

C:\TP\tp-chien-dao>build

C:\TP\tp-chien-dao>echo Build du TP Chien DAO
Build du TP Chien DAO

C:\TP\tp-chien-dao>mvn clean install eclipse:eclipse

C:\TP\tp-chien-dao>set JAVA_HOME=C:\dev\javas\jdk1.6.0_24
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building TP Chien 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ tp-chien-dao ---
[INFO] Deleting C:\TP\tp-chien-dao\target
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ tp-chien-dao ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:compile (default-compile) @ tp-chien-dao ---
[INFO] Compiling 10 source files to C:\TP\tp-chien-dao\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ tp-chien-dao ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:testCompile (default-testCompile) @ tp-chien-dao ---
[INFO] Compiling 3 source files to C:\TP\tp-chien-dao\target\test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.9:test (default-test) @ tp-chien-dao ---
[INFO] Surefire report directory: C:\TP\tp-chien-dao\target\surefire-reports

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.icauda.tp.chien.dao.csv.AdvanceCsvChienDaoTest
0    [main] DEBUG com.icauda.tp.chien.dao.csv.AdvanceCsvChienDaoTest  - Constructeur...
4    [main] DEBUG com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest  - doBefore Debut
4    [main] DEBUG com.icauda.tp.chien.dao.csv.AdvancedCsvChienDao  - init
4    [main] DEBUG com.icauda.tp.chien.dao.csv.AdvancedCsvChienDao  - findAllChiens
4    [main] DEBUG com.icauda.tp.chien.dao.csv.AdvancedCsvChienDao  - getLignesFromFile
5    [main] DEBUG com.icauda.tp.chien.dao.csv.AdvancedCsvChienDao  - Entetes : Nom;Nom complet;sexe;race;couleurs;poids
5    [main] DEBUG com.icauda.tp.chien.dao.csv.AdvancedCsvChienDao  - transformEntetes
9    [main] DEBUG com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest  - doBefore Fin
...
59   [main] DEBUG com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest  - testRechercheIdefix... Fin
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.303 sec
Running com.icauda.tp.chien.dao.csv.NaiveCsvChienDaoTest
70   [main] DEBUG com.icauda.tp.chien.dao.csv.NaiveCsvChienDaoTest  - Constructeur...
71   [main] DEBUG com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest  - doBefore Debut
...
93   [main] DEBUG com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest  - testRacePluto... Fin
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.039 sec

Results :

Tests run: 18, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- maven-jar-plugin:2.3.1:jar (default-jar) @ tp-chien-dao ---
[INFO] Building jar: C:\TP\tp-chien-dao\target\TP chien.jar
[INFO]
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ tp-chien-dao ---
[INFO] Installing C:\TP\tp-chien-dao\target\TP chien.jar to C:\dev\mavens\repository\com\icauda\tp-chien-dao\1.0-SNAPSHOT\tp-chien-dao-1.0-SNAPSHOT.jar
[INFO] Installing C:\TP\tp-chien-dao\pom.xml to C:\dev\mavens\repository\com\icauda\tp-chien-dao\1.0-SNAPSHOT\tp-chien-dao-1.0-SNAPSHOT.pom
[INFO]
[INFO]  maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao
[INFO]
[INFO]  maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao
[INFO]
[INFO] --- maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao ---
[INFO] Using Eclipse Workspace: null
[INFO] Adding default classpath container: org.eclipse.jdt.launching.JRE_CONTAINER
[INFO] File C:\TP\tp-chien-dao\.project already exists.
Additional settings will be preserved, run mvn eclipse:clean if you want old settings to be removed.
[INFO] Wrote Eclipse project for "tp-chien-dao" to C:\TP\tp-chien-dao.
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.382s
[INFO] Finished at: Tue Nov 13 20:48:30 CET 2012
[INFO] Final Memory: 14M/61M
[INFO] ------------------------------------------------------------------------

Maven reconstruit le projet en ajoutant la nouvelle dépendance. Au passage vous remarquez que Maven a rejoué tous vos tests. Cet aspect est assez intéressant dans une démarche d'intégration continue.

Faites ensuite un "Refresh" (touche [F5] sur "tp-chien-dao") de votre projet dans Eclipse.

Open-csv dans Eclipse

Avant de programmer cette nouvelle version du DAO, vous allez écrire les tests associés. Ici il va suffire de recopier AdvanceCsvChienDaoTest et de créer OpenCsvChienDao en vous inspirant des codes suivants :

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {

private static final Logger LOGGER = Logger.getLogger(OpenCsvChienDao.class);

private final static char SEPARATOR = ';';

private File file;

@Override
public List<Chien> findAllChiens() {
throw new UnsupportedOperationException("Bientot");
}

@Override
public Chien findChienByNom(String nom) {
throw new UnsupportedOperationException("Bientot");
}

@Override
public void init(File file) {
LOGGER.debug("init");
this.file = file;
}

@Override
public File getFile() {
return file;
}

@Override
public List<String> getEntetes() {
throw new UnsupportedOperationException("Bientot");
}

}

OpenCsvChienDaoTest.java

public class OpenCsvChienDaoTest extends AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(OpenCsvChienDaoTest.class);

public OpenCsvChienDaoTest() {
LOGGER.debug("Constructeur...");

dao = new OpenCsvChienDao();
}
}

Dans la foulée, vous pouvez lancer les tests de OpenCsvChienDaoTest et vérifier qu'ils sont bien tous en échec.

Ajoutez des attributs de classe comme vous l'aviez fait plus tôt, en vous inspirant du code suivant :

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {
...

private File file;
private List<Chien> chiens;
private Map<String, Chien> chienMapByNom;
private List<String> entetes;

...

@Override
public List<String> getEntetes() {
return entetes;
}

Vous pouvez également reprendre quelques méthodes simples :

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {
...

private void reloadChiens() {
throw new UnsupportedOperationException("Bientot");
}

@Override
public List<Chien> findAllChiens() {
LOGGER.debug("findAllChiens");

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chiens;
}

@Override
public Chien findChienByNom(final String nom) {

if(nom == null || nom.isEmpty()) {
throw new IllegalArgumentException("Le nom ne peut pas etre vide.");
}

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chienMapByNom.get(nom);
}

@Override
public void init(File file) {
LOGGER.debug("init");
this.file = file;

reloadChiens();
}

...

Vous pouvez enfin passer à la lecture à l'aide d'Open CSV. Pour cela, commencez par reprendre la méthode "getLignesFromFile()" :

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {
...

private void reloadChiens() {
LOGGER.debug("reloadChiens");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

final List<String[] > lignes = getLignesFromFile();
}

private List<String[] > getLignesFromFile() {
LOGGER.debug("getLignesFromFile");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

throw new UnsupportedOperationException("Bientot");
}

Dans cette version, la méthode "getLignesFromFile()" renvoie une liste de tableaux de Strings et non plus simplement une liste de Strings.

Le cœur d'Open CSV est la classe CSVReader :

OpenCsvChienDao.java

import java.io.FileReader;
import au.com.bytecode.opencsv.CSVReader;
...

public class OpenCsvChienDao implements CsvChienDao {
...

private List<String[] > getLignesFromFile() {
LOGGER.debug("getLignesFromFile");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

try {
final FileReader fr = new FileReader(file);
final CSVReader csvReader = new CSVReader(fr, SEPARATOR);

} catch (Exception e) {
LOGGER.error("aie aie aie", e);
}

throw new UnsupportedOperationException("Bientot");
}

Vous pouvez maintenant charger et filtrer les lignes à l'aide d'Open CSV :

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {
...

private List<String[] > getLignesFromFile() {
LOGGER.debug("getLignesFromFile");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

final List<String[] > lignes = new ArrayList<String[] >();

try {
final FileReader fr = new FileReader(file);
final CSVReader csvReader = new CSVReader(fr, SEPARATOR);

String[] nextLine = null;
while ((nextLine = csvReader.readNext()) != null) {
int size = nextLine.length;

// ligne vide
if (size == 0) {
continue;
}
String debut = nextLine[0].trim();
if (debut.isEmpty() && size == 1) {
continue;
}

// ligne de commentaire
if (debut.startsWith("#")) {
continue;
}
lignes.add(nextLine);
}

} catch (Exception e) {
LOGGER.error("aie aie aie", e);
}

return lignes;
}

Pour la conversion, il suffit de reprendre ce qui avait déjà été fait. Cette fois vous pouvez directement passer un tableau.

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {
...

private Chien transformLigneToChien(final String[] values) throws Exception {

final SimpleChien chien = new SimpleChien();

chien.setNom(values[0]);
...

Et pour finir avec la méthode de rechargement :

OpenCsvChienDao.java

public class OpenCsvChienDao implements CsvChienDao {
...

private void reloadChiens() {
LOGGER.debug("reloadChiens");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

try {
final List<String[] > lignes = getLignesFromFile();

final String[] ligneEntete = lignes.remove(0);
transformEntetes(ligneEntete);

chiens = new ArrayList<Chien>(lignes.size());
chienMapByNom = new HashMap<String, Chien>(lignes.size());
for (String[] ligne : lignes) {
final Chien chien = transformLigneToChien(ligne);
chiens.add(chien);

chienMapByNom.put(chien.getNom(), chien);
}
} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}
}

...

N'oubliez pas de programmer la méthode "transformEntetes(..)" en faisant les évolutions qui s'imposent.

Relancez les tests, qui devraient donc être tous verts.

Comme vous le constatez, vous n'avez pas gagné tant que ça au niveau du nombre de lignes et/ou de la quantité de travail à fournir. L'intérêt d'Open CSV réside surtout dans le fait que vous n'avez pas eu à décoder vous-même les lignes. Par exemple, vous n'avez pas traité les valeurs sur plusieurs lignes ou les caractères interdits ; Open CSV l'a fait pour vous de manière transparente.

Quand vous aurez le temps, je vous invite à lire l'article Charger des données depuis un fichier CSV simple en 5 minutes qui propose une autre explication/démo de la lecture d'un fichier CSV à l'aide d'Open CSV.

Dessinez un diagramme de séquence UML, correspondant au mécanisme de chargement de OpenCsvChienDao. Ce travail est à faire au stylo et à rendre à la fin de la séance.

IV-F - CSV Engine (avec des annotations)

Durée estimée : 10 minutes.

Vous allez maintenant utiliser la bibliothèque "CSV Engine", qui s'appuie sur Open CSV et qui va radicalement simplifier votre travail.

Commencez en ajoutant une nouvelle dépendance dans le fichier "pom.xml" :

pom.xml

...

<properties>
...

<!-- Lib CSV -->
<!--<opencsv.version>2.3</opencsv.version>-->
<csvengine.version>1.3.5</csvengine.version>

...
</properties>

<dependencies>
...

<!-- Lib CSV -->
<!--
<dependency>
<groupId>net.sf.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>${opencsv.version}</version>
</dependency>
-->

<dependency>
<groupId>fr.ybonnel</groupId>
<artifactId>csvengine</artifactId>
<version>${csvengine.version}</version>
</dependency>

...
</dependencies>

<repositories>
<repository>
<id>ybonnel-release</id>
<url>https://repository-ybonnel.forge.cloudbees.com/release/</url>
</repository>
</repositories>
...

Vous remarquez que la dépendance à Open CSV a été mise en commentaire. Elle n'est plus nécessaire dans le fichier "pom.xml" car CSV Engine déclare déjà une dépendance vers Open CSV. Dans Eclipse, vous aurez donc toujours "opencsv-2.3.jar" car c'est CSV Engine qui va "tirer" cette bibliothèque. On parle de "dépendance transitive".

Relancez une compilation Maven. En fonction des éléments déjà présents sur votre machine, la réponse de Maven devrait ressembler à la trace suivante :

build

C:\TP\tp-chien-dao>build

C:\TP\tp-chien-dao>echo Build du TP Chien DAO
Build du TP Chien DAO

C:\TP\tp-chien-dao>mvn clean install eclipse:eclipse

C:\TP\tp-chien-dao>set JAVA_HOME=C:\dev\javas\jdk1.6.0_24
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building TP Chien 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ tp-chien-dao ---
[INFO] Deleting C:\TP\tp-chien-dao\target
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ tp-chien-dao ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:compile (default-compile) @ tp-chien-dao ---
[INFO] Compiling 11 source files to C:\TP\tp-chien-dao\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ tp-chien-dao ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:testCompile (default-testCompile) @ tp-chien-dao ---
[INFO] Compiling 4 source files to C:\TP\tp-chien-dao\target\test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.9:test (default-test) @ tp-chien-dao ---
[INFO] Surefire report directory: C:\TP\tp-chien-dao\target\surefire-reports

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.icauda.tp.chien.dao.csv.AdvanceCsvChienDaoTest
...
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.223 sec
Running com.icauda.tp.chien.dao.csv.NaiveCsvChienDaoTest
...
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.08 sec
Running com.icauda.tp.chien.dao.csv.OpenCsvChienDaoTest
...
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.057 sec

Results :

Tests run: 27, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- maven-jar-plugin:2.3.1:jar (default-jar) @ tp-chien-dao ---
[INFO] Building jar: C:\TP\tp-chien-dao\target\TP chien.jar
[INFO]
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ tp-chien-dao ---
[INFO] Installing C:\TP\tp-chien-dao\target\TP chien.jar to C:\dev\mavens\repository\com\icauda\tp-chien-dao\1.0-SNAPSHOT\tp-chien-dao-1.0-SNAPSHOT.ja
r
[INFO] Installing C:\TP\tp-chien-dao\pom.xml to C:\dev\mavens\repository\com\icauda\tp-chien-dao\1.0-SNAPSHOT\tp-chien-dao-1.0-SNAPSHOT.pom
[INFO]
[INFO]  maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao
[INFO]
[INFO]  maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao
[INFO]
[INFO] --- maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao ---
[INFO] Using Eclipse Workspace: null
[INFO] Adding default classpath container: org.eclipse.jdt.launching.JRE_CONTAINER
[INFO] File C:\TP\tp-chien-dao\.project already exists.
Additional settings will be preserved, run mvn eclipse:clean if you want old settings to be removed.
[INFO] Wrote Eclipse project for "tp-chien-dao" to C:\TP\tp-chien-dao.
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.745s
[INFO] Finished at: Tue Nov 13 21:01:28 CET 2012
[INFO] Final Memory: 14M/53M
[INFO] ------------------------------------------------------------------------

Bien entendu, faites un "refresh" dans Eclipse. Vérifiez que "csvengine-1.3.5.jar" est bien apparu dans la liste de vos dépendances.

Sans surprise, vous devez d'abord préparer un nouveau DAO et une nouvelle série de tests associée :

EngineCsvChienDao.java

public class EngineCsvChienDao implements CsvChienDao {

private static final Logger LOGGER = Logger.getLogger(EngineCsvChienDao.class);

private File file;
private List<Chien> chiens;
private Map<String, Chien> chienMapByNom;
private List<String> entetes;

@Override
public List<Chien> findAllChiens() {
throw new UnsupportedOperationException("Bientot");
}

@Override
public Chien findChienByNom(String nom) {
throw new UnsupportedOperationException("Bientot");
}

private void reloadChiens() {
throw new UnsupportedOperationException("Bientot");
}

@Override
public void init(File file) {
LOGGER.debug("init");
this.file = file;

reloadChiens();
}

@Override
public File getFile() {
return file;
}

@Override
public List<String> getEntetes() {
return entetes;
}
}

Comme ça commence à être du connu, je vais un peu plus vite…

EngineCsvChienDaoTest.java

public class EngineCsvChienDaoTest extends AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(EngineCsvChienDaoTest.class);

public EngineCsvChienDaoTest() {
LOGGER.debug("Constructeur...");

dao = new EngineCsvChienDao();
}
}

Profitez-en pour lancer les nouveaux tests, qui doivent être rouges.

Ne soyez pas choqué, vous allez maintenant faire évoluer le modèle :

SimpleChien.java

import fr.ybonnel.csvengine.annotation.CsvColumn;
import fr.ybonnel.csvengine.annotation.CsvFile;
...

@CsvFile(separator=";")
public class SimpleChien implements Chien {

@CsvColumn("Nom")
private String nom;

...

Le cœur de Engine CSV est la classe CsvEngine :

EngineCsvChienDao.java

import fr.ybonnel.csvengine.CsvEngine;
...

public class EngineCsvChienDao implements CsvChienDao {
...

private void reloadChiens() {
LOGGER.debug("reloadChiens");

final CsvEngine engine = new CsvEngine(SimpleChien.class);

throw new UnsupportedOperationException("Bientot");
}
...

Vous pouvez maintenant faire le traitement directement :

EngineCsvChienDao.java

public class EngineCsvChienDao implements CsvChienDao {
...

private void reloadChiens() {
LOGGER.debug("reloadChiens");

try {
final CsvEngine engine = new CsvEngine(SimpleChien.class);
final FileInputStream fis = new FileInputStream(file);

List<SimpleChien> dogs = engine.parseInputStream(fis, SimpleChien.class).getObjects();

} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}
}

...

Vous êtes obligés de passer par SimpleChien et une variable temporaire, puis la "caster" dans le bon type. On parle de "type erasure".

EngineCsvChienDao.java

...

private void reloadChiens() {
LOGGER.debug("reloadChiens");

try {
final CsvEngine engine = new CsvEngine(SimpleChien.class);
final FileInputStream fis = new FileInputStream(file);

final List<? extends Chien> dogs = engine.parseInputStream(fis, SimpleChien.class).getObjects();
chiens = (List<Chien>) dogs;
chienMapByNom = new HashMap<String, Chien>(dogs.size());
for (Chien chien : chiens) {
LOGGER.debug("[chien] " + chien);
chienMapByNom.put(chien.getNom(), chien);
}

} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}
}

Pour bien faire, complétez les autres méthodes simples :

EngineCsvChienDao.java

...

@Override
public List<Chien> findAllChiens() {
LOGGER.debug("findAllChiens");

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chiens;
}

@Override
public Chien findChienByNom(final String nom) {

if (nom == null || nom.isEmpty()) {
throw new IllegalArgumentException("Le nom ne peut pas etre vide.");
}

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chienMapByNom.get(nom);
}

CSV Engine ne gère pas bien les commentaires dans les fichiers CSV. Vous devez donc les enlever de "chiens-01.csv", faute de quoi la bibliothèque vous renverra une exception… Attention aussi aux lignes "blanches" qui contiennent des espaces, et ne sont donc pas tout à fait vides…

Si toutefois vous ne souhaitez pas supprimer les lignes de commentaire du fichier, vous pouvez utiliser la solution suivante, qui a été proposée par le créateur de CSV Engine :

EngineCsvChienDao.java

import fr.ybonnel.csvengine.CsvEngine;
import fr.ybonnel.csvengine.adapter.AdapterCsv;
import fr.ybonnel.csvengine.factory.AbstractCsvReader;
import fr.ybonnel.csvengine.factory.DefaultCsvManagerFactory;
import fr.ybonnel.csvengine.factory.OpenCsvReader;
...

public class EngineCsvChienDao implements CsvChienDao {
...

private void reloadChiens() {
LOGGER.debug("reloadChiens");

try {
final CsvEngine engine = new CsvEngine(SimpleChien.class);
setEngineFactory(engine);
...

private void setEngineFactory(final CsvEngine engine) {
engine.setFactory(new DefaultCsvManagerFactory() {
@Override
public AbstractCsvReader createReaderCsv(Reader reader, char separator) {
return new OpenCsvReader(reader, separator) {
@Override
public String[] readLine() throws IOException {
String[] nextLine = super.readLine();
if (isLineAComment(nextLine)) {
nextLine = readLine();
}
return nextLine;
}

private boolean isLineAComment(String[] line) {
return line != null && line.length > 0 && line[0].startsWith("#");
}
};
}
});
}

...

Relancez les tests. Pour le moment, seuls "testCinqChiens" et "testPremierEstMilou" devraient être verts. C'est normal puisque vous n'avez pas encore annoté les autres attributs.

SimpleChien.java

@CsvFile(separator=";")
public class SimpleChien implements Chien {

@CsvColumn("Nom")
private String nom;

@CsvColumn("Nom complet")
private String nomComplet;

...

@CsvColumn(value = "race", adapter = AdapterRace.class)
private RaceDeChien race;

...

RaceDeChien.java

import fr.ybonnel.csvengine.adapter.AdapterCsv;
...

public enum RaceDeChien {
...

public static class AdapterRace extends AdapterCsv<RaceDeChien> {

@Override
public RaceDeChien parse(String chaine) {
return RaceDeChien.valueOfByCode(chaine);
}

@Override
public String toString(RaceDeChien race) {
return race.getCode();
}
}

...

Relancez les tests et vérifiez que "testRacePluto" passe au vert.

Programmez l'"adapter" nécessaires au fonctionnement de l'attribut "sexe".

Vous ne pouvez pas modifier la classe "Double" pour y ajouter un "adapter" pour l'attribut "poids". Vous devez ajouter cet "adapter" dans le DAO directement. CSV Engine propose bien un "adapter" dédié, mais il n'accepte pas le format français (avec une virgule).

EngineCsvChienDao.java

public class EngineCsvChienDao implements CsvChienDao {
...

public static class AdapterPoids extends AdapterCsv<Double> {

@Override
public Double parse(String chaine) {

chaine = chaine.replace(',', '.');
return new Double(chaine);
}

@Override
public String toString(Double value) {
return value.toString();
}
}

...

SimpleChien.java

public class SimpleChien implements Chien {
...

@CsvColumn("Nom")
private String nom;

@CsvColumn("Nom complet")
private String nomComplet;

@CsvColumn(value = "sexe", adapter = AdapterSexe.class)
private Sexe sexe;

@CsvColumn(value = "race", adapter = AdapterRace.class)
private RaceDeChien race;

@CsvColumn(value = "couleurs", adapter = AdapterCouleurs.class)
private List<String> couleurs;

@CsvColumn(value = "poids", adapter = AdapterPoids.class)
private Double poids;

...

Relancez les tests et vérifiez que les tests basés sur le poids passent au vert.

Programmez "AdapterCouleurs" nécessaire au fonctionnement de l'attribut "couleurs".

Normalement, seuls les tests liés aux titres doivent rester en échec.

Un point (très) gênant est que cette façon de faire a introduit une dépendance de votre domaine vers le DAO et c'est moche… Pour y remédier, il suffit de créer des "vraies" classes :

AdapterPoids.java

package com.icauda.tp.chien.domain.adapter;

import fr.ybonnel.csvengine.adapter.AdapterCsv;

public class AdapterPoids extends AdapterCsv<Double> {
@Override
public Double parse(String chaine) {

chaine = chaine.replace(',', '.');
return new Double(chaine);
}

@Override
public String toString(Double value) {
return value.toString();
}
}

SimpleChien.java

import com.icauda.tp.chien.domain.RaceDeChien.AdapterRace;
import com.icauda.tp.chien.domain.Sexe.AdapterSexe;
import com.icauda.tp.chien.domain.adapter.AdapterCouleurs;
import com.icauda.tp.chien.domain.adapter.AdapterPoids;
...

@CsvFile(separator = ";")
public class SimpleChien implements Chien {
...

Il ne manque plus que la liste des titres que vous devez programmer pour finir la partie sur les fichiers CSV. Cette partie est relativement simple, mais va vous demander de consulter la documentation de CSV Engine. Si vous n'y arrivez pas rapidement, vous avez (exceptionnellement) le droit de regarder la solution dans les annexes.

N'oubliez pas de lancer les tests, de formater votre code, d'organiser les imports, de documenter…

Le code du programme, à ce stade, est disponible dans le fichier Zip tp-chien-dao-02.zip.

Vous venez de programmer quatre versions différentes d'un DAO dédié à la lecture d'un fichier CSV. Si on laisse de côté la version naïve, vous avez donc trois DAO qui se ressemblent beaucoup. Et pour cause puisque vous avez usé et abusé du copier-coller. La conséquence qui saute aux yeux, c'est que vous avez une grosse quantité de code dupliqué, qui diminue d'autant le niveau de qualité de votre application.

À l'aide d'héritage et/ou de classes utilitaires, vous devez réorganiser (on dira "refactorer") le code des trois DAO (la version naïve va finir à la poubelle) pour limiter les duplications. Je vous propose une solution "rapide" en annexe et dans le fichier Zip tp-chien-dao-03.zip.

Dessinez un diagramme de classe UML, incluant les trois DAO "refactorés" et leurs liens. Ce travail est à faire au stylo et à rendre à la fin de la séance.

V - Conclusion

Ce TD est relativement complet, bien qu'on puisse aller encore plus loin. Il vous fait découvrir de nombreuses technologies, qui vous seront utiles au quotidien et dont vous devez maîtriser les grands principes : logs, gestion des ressources, tests, etc.

Pour aller plus loin, vous pouvez refaire ce travail, non plus avec des fichiers CSV mais avec une base de données. Je vous conseille les bases MySql (base classique) et HSQL DB (base en mémoire), ainsi que les bibliothèques de tests spécifiques pour les bases DbUnit et Unitils. Vous pouvez, par exemple, créer une version JDBC et une version JPA.

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : Commentez

Ce TP est utilisé par de nombreux étudiants. N'hésitez donc pas à m'envoyer directement des idées d'amélioration, des propositions d'exercices supplémentaires, des pistes intéressantes, etc.

VI - Remerciements

Je tiens à remercier, en tant qu'auteur de TP, toutes les personnes qui m'ont aidé et soutenu. Je pense tout d'abord à mes collègues qui subissent mes questions au quotidien, mais aussi à mes contacts et amis du Web, dans le domaine de l'informatique ou non, qui m'ont fait part de leurs remarques et critiques. Bien entendu, je n'oublie pas l'équipe de Developpez.com qui m'a guidé dans la rédaction de cet article et m'a aidé à le corriger et le faire évoluer, principalement sur le forum.

Plus particulièrement j'adresse mes remerciements à Yan Bonnel (@ybonnel créateur français de CSV Engine), Mickael BARON (keulkeul), djibril, Sébastien Germez (FirePrawn), f-leb, Maxime Gault (_Max_), Philippe Leménager (CinePhil), Nemek et Claude Leloup.

VII - Annexes

VII-A - Liens

Maven : http://maven.apache.org/

JUnit : http://junit.org/

Eclipse : http://eclipse.org/

Log4j : http://logging.apache.org/log4j

Open CSV : http://opencsv.sourceforge.net/

CSV Engine : https://github.com/ybonnel/CsvEngine/wiki

Unitils : http://www.unitils.org/

DB Unit : http://www.dbunit.org/

JDBC : http://www.oracle.com/technetwork/java/javase/jdbc/index.html

JPA : http://www.oracle.com/technetwork/java/javaee/tech/persistence-jsp-140049.html

VII-B - Pour aller plus loin

Article "Importer un projet Maven dans Eclipse en 5 minutes" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/importer-projet-maven-dans-eclipse-5-min/

Article "Utiliser Maven 2" :
https://matthieu-lux.developpez.com/tutoriels/java/maven/

Article "Charger des données depuis un fichier CSV simple en 5 minutes" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/charger-donnees-fichier-csv-5-min/

Article "Les fichiers CSV avec Java" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/csv-avec-java/

Article "Charger des données depuis une base MySQL avec JDBC en 5 minutes" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/charger-donnees-mysql-5-min/

Article "Introduction à JPA, application au chargement de données depuis une base MySQL" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/charger-donnees-mysql-jpa-intro/

Article "3T en pratique, application au calcul de la suite de Fibonnaci, en 5 minutes" :
https://thierry-leriche-dessirier.developpez.com/tutoriels/java/fibonacci-3t-5-min/

Cours "JPA (Java Persistence API)" :
https://jmdoudoux.developpez.com/cours/developpons/java/chap-jpa.php

Même si j'espère que vous n'en avez pas besoin, voici un très bon article de Jean-Michel DOUDOUX, pour "apprendre" Java :
https://jmdoudoux.developpez.com/cours/developpons/java/

Voici aussi une série d'articles (en plusieurs chapitres), dédiée à la préparation de la certification Java, grâce à laquelle vous en apprendrez certainement plus qu'avec n'importe quel autre support :
https://armel-ndjobo.developpez.com/memocertifjava6/

Retrouvez ma page et mes autres articles sur developpez.com à l'adresse
https://thierry-leriche-dessirier.developpez.com/#page_articles

QR Code vers mes articles

Ajoutez-moi à vos contacts à l'aide du QR Code suivant :

QR Code contenant ma vCard

Suivez-moi sur Twitter : @thierryleriche

@thierryleriche

VII-C - FAQ

Cette section traite des questions qu'on me pose souvent. Merci de la lire avant de demander l'aide du professeur.

VII-C-1 - M2_REPO

S'il vous manque les bibliothèques (cf. capture) ou qu'Elipse ne trouve pas le "Repositoty" Maven, c'est sans doute qu'il n'est pas configuré.

Lib manquante

Dans ce cas, il faut aller dans les préférences à l'aide du menu "Window/Préférences". La suite dépend de la version d'Eclipse que vous utilisez.

Si vous n'avez pas de section "Maven" dans les préférences, alors allez dans "Java/Build Path/Classpath Variables". Ajoutez une variable à l'aide du bouton "New". Dans la popop qui s'ouvre, précisez "M2_REPO" en nom de variable. En valeur, vous devez indiquer le dossier où se trouve le "repository" de Maven. Par défaut, Maven enregistre les bibliothèques qu'il télécharge dans le dossier "[votre home]/.m2/repository". C'est ce dossier que doit référencer la variable M2_REPO.

Il est possible de changer le dossier du "repository" Maven. Pour cela, il faut modifier le fichier "settings.xml" qui se trouve dans le dossier "conf" dans le dossier où est installé Maven sur le disque. Personnellement, je préfère avoir un seul repository sur mon disque dur, et non un par utilisateur (comme c'est donc le cas par défaut), pour éviter de télécharger plusieurs fois les mêmes bibliothèques, ce qui sauve un peu (beaucoup) d'espace sur le disque dur. Voici un extrait de ma configuration :

settings.xml

<settings ..>
...

<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ~/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<localRepository>C:\dev\mavens\repository</localRepository>

Si vous avez une section "Maven" dans les préférences, alors allez dans "Maven/User settings" où vous pouvez indiquer l'emplacement, sur le disque, où se trouve le Maven que vous utilisez, et plus spécifiquement le fichier de configuration "settings.xml".

Config Maven

En effet, dans les nouvelles versions d'Eclipse, il y a un plugin pour Maven et on ne peut pas modifier la variable M2_REPO (marquée "non modifiable") soi-même. En revanche, le plugin configure la variable M2_REPO en fonction de l'emplacement de Maven qu'on aura indiqué.

VII-C-2 - Je ne trouve pas le dossier .m2 dans l'explorateur de fichiers

Le dossier ".m2/repository" est l'endroit où Maven stocke toutes les bibliothèques qu'il a téléchargées. Ce dossier est généralement placé dans le "HOME" de l'utilisateur (à la racine de votre compte sous Linux, ou dans "Mes Documents" sous Windows). Le "HOME" est référencé par les variables d'environnement "$HOME" sous X et "%USERPROFILE%" sous Windows. Pour que ce dossier existe, il faut avoir lancé une commande Maven, telle que "mvn clean install", au moins une fois.

Sous Linux, les dossiers commençant par un point sont des dossiers cachés. Vous pouvez les voir dans l'explorateur de fichier, quand vous configurez "M2_REPO" dans Eclipse par exemple, à l'aide de la combinaison de touches [Ctrl] + [H]. Note : J'ai testé ce raccourci sous Nautilus. Il est possible qu'il ne fonctionne pas sous Konqueror.

Si vous voulez définir un dossier spécifique et non caché, contrairement à ".m2", il faut configurer le fichier "settings.xml" dans Maven, et plus spécifiquement la clé "localRepository". Voici à quoi ça ressemble chez moi :

settings.xml

<settings ...>
<localRepository>C:\dev\mavens\repository</localRepository>
...

VII-C-3 - Je n'arrive pas à installer mon ordinateur portable

Consultez le chapitre "Installer un environnement de développement à la maison".

VII-C-4 - Le sommaire prend trop de place

Vous pouvez réduire la table des matières (sommaire) dans la version HTML en ligne, si elle prend trop de place, à l'aide de la petite flèche.

VII-C-5 - J'ai un warning "the import com.foo.truc is never used"

Si vous avez un warning vous indiquant qu'un package, que vous avez importé, n'est pas utilisé, ce n'est pas grave. Toutefois, je vous conseille de faire le ménage dans les imports qui ne servent à rien. Dans Eclipse, le plus simple est d'utiliser la combinaison [Ctrl] + [Maj] + [O]. C'est "O" comme "Organize" et pas comme "zéro".

VII-C-6 - J'ai un warning "the serializable class Foo does not declare a static final serialVersionUID field of type long"

C'est un warning. Ce n'est pas grave. Cliquez sur le triangle jaune (avec le bouton de gauche) et prenez l'option "Add generated version id" dans le menu contextuel qui apparaît. Ça doit vous ajouter une variable ressemblant au code suivant : :

serialVersionUID

public class Foo ... {

private static final long serialVersionUID = -8600160150321329182L;
...

VII-C-7 - Comment savoir la taille d'un tableau ou d'une liste en Java ?

En Java, il suffit d'utiliser la propriété "lenght" pour savoir la taille d'un tableau :

Taille d'un tableau

String[] tab = ...
int taille = tab.length;

Pour savoir la taille d'une liste, il faut utiliser la méthode "size()" :

Taille d'une liste

List<String> list = ...
int taille = list.size();

"length" et "size()" vous donnent la taille d'un tableau et d'une liste, c'est-à-dire le nombre de "cases" ou d'items qu'ils contiennent. Ça ne vous dit pas si ces cases sont remplies (ie. si le contenu est différent de "null").

VII-C-8 - Comment configurer le proxy de Maven

Les sociétés et les écoles protègent souvent le réseau (et l'accès Internet) à l'aide d'un proxy. Vous devez donc configurer Maven pour qu'il utilise ce proxy, faute de quoi Maven sera incapable de télécharger les bibliothèques nécessaires.

Voici un exemple avec la configuration de l'ESIEA. Vous devez renseigner les éléments suivants dans le fichier settings.xml :

settings.xml

<settings ...>
...

<proxies>
<proxy>
<id>ESIEA</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy2.esiea.fr</host>
<port>8080</port>
</proxy>
...

</proxies>

VII-C-9 - Les travaux sont-ils à rendre ?

Oui. Chaque élève (ou binôme) doit rendre une copie. Les travaux demandés doivent être réalisés au stylo sur papier. Merci d'indiquer vos NOM, Prénom et classe en haut à gauche.

VII-D - Installer un environnement de développement à la maison

L'école vous fournit des machines déjà configurées. La plupart du temps, à part la variable "M2_REPO" dans Eclipse, il n'y a rien à faire de plus. C'est toutefois une bonne idée d'installer votre ordinateur pour pouvoir travailler à la maison. Je précise que les séances de TP ne sont pas dédiées à l'installation de votre machine. Si vous venez avec votre propre ordinateur à l'école, il doit être installé et configuré avant le début de la séance de TP.

Notez bien que votre professeur n'assure pas de support pour vos propres installations.

Pour commencer, je vous conseille de ne jamais installer vos logiciels de développement dans des dossiers contenant des caractères spéciaux ou des espaces. Ce premier conseil a son importance, car Windows vous propose généralement d'installer vos logiciels dans "Program Files". Dans la suite, je vais vous proposer d'installer vos logiciels dans le dossier "C:/dev". C'est comme ça que j'ai installé sur MON ordinateur portable.

VII-D-1 - Installation de Java

Comme il est possible d'avoir plusieurs versions de Java installées en même temps sur un ordinateur, je vous conseille de les installer dans le dossier "C/dev/javas" où le "s" à la fin de "javas" prend tout son sens. Pensez à ne pas installer Java dans "Program Files".

Windows propose d'installer dans Program Files

Perso j'installe Java dans c:/dev/javas

Pour développer vous avez besoin d'une version "JDK" et non d'une simple version "JRE". Je vous conseille d'avoir une version de Java assez proche de celle utilisée pour ce TP (ie. 1.6.x, donc ni 1.5.X ni 1.7.x). Souvenez-vous que vous pouvez avoir plusieurs versions, tant qu'une seule n'est utilisée à la fois. Il faut commencer les installations par les versions les plus vieilles. Par exemple, sur MON PC portable, j'ai les versions :

- "1.4.2.19" installée dans "C:/dev/javas/j2sdk1.4.2_19" ;

- "1.5.0.20" installée dans "C:/dev/javas/jdk1.5.0_20" ;

- "1.6.0.24" installée dans "C:/dev/javas/jdk1.6.0_24" ;

- "1.6.0.24" en "32 bits" installée dans "C:/dev/javas/jdk1.6.0_24_32bits" ;

- "1.7.0.03" installée dans "C:/dev/javas/jdk1.7.0_03" ;

- …

Au moment où j'écris cet article, la page de téléchargement de Java chez Oracle (ne jamais la prendre ailleurs) est :
http://www.oracle.com/technetwork/java/javase/downloads/index.html

Notez bien que Java est disponible en "32 bits" et en "64 bits". Si vous avez un ordinateur récent, je vous conseille la version "64 bits".

Après avoir installé chaque version, ouvrez une nouvelle console et vérifiez qu'elle est prise en compte :

cmd

C:\>java -version

java version "1.7.0_03"
Java(TM) SE Runtime Environment (build 1.7.0_03-b05)
Java HotSpot(TM) 64-Bit Server VM (build 22.1-b02, mixed mode)

VII-D-2 - Installation d'Eclipse

Eclipse se présente sous la forme d'une archive Zip qu'il suffit de décompresser au bon endroit sur votre disque dur. Dans chaque version d'Eclipse, il y a plusieurs déclinaisons. Je vous conseille de télécharger la "Eclipse IDE for Java EE Developers" ou la "Eclipse IDE for Java Developers". Notez bien qu'Eclipse est disponible en "32 bits" et en "64 bits". Si vous avez un ordinateur récent, je vous conseille la version "64 bits". Cette dernière remarque est valable pour le JDK.

Au moment où j'écris cet article, la page de téléchargement d'Eclipse sur le site officiel (ne jamais la prendre ailleurs) est :
http://www.eclipse.org/downloads/

Comme il est possible d'avoir plusieurs versions de Maven en même temps sur un ordinateur, je vous conseille de les décompresser dans le dossier "C/dev/eclipses" où le "s" à la fin de "eclipses" prend tout son sens. Pensez juste à ne pas installer Java dans "Program Files".

À titre d'exemple, le Eclipse que j'utilise le plus souvent est installé dans le dossier "C:/dev/eclipses/eclipse-jee-indigo-win32-x86_64" sur MON ordinateur portable.

Pour lancer Eclipse, il suffit de double-cliquer sur "eclipse.exe". Je vous conseille de vous faire un raccourci.

Au lancement d'Eclipse, il vous demande où vous souhaitez mettre votre "workspace" sur le disque. Sur MON portable, j'ai plusieurs "workspaces". Depuis Eclipse, on peut passer très facilement d'un "workspace" à l'autre. À titre d'exemple, le "workspace" que j'utilise pour écrire le code de mes articles sur Developpez.com est "D:/workspaces/workspace-article-dvp-03".

Il est important de comprendre que le "workspace" correspond à la configuration de votre Eclipse. Avoir plusieurs "workspaces" permet de changer de configuration facilement. Cette configuration inclut par exemple la liste des projets ouverts dans Eclipse.

Il est aussi très important de comprendre que l'emplacement du "workspace" et l'emplacement de vos projets sur le disque sont deux choses différentes. Votre "workspace" ne doit pas inclure vos sources. Une bonne raison à cela est qu'un même projet peut être ouvert dans plusieurs "workspaces". Ainsi, sur MON portable, mes "workspaces" sont dans "D:/workspaces" et mes projets Java sont dans "D:/javadev".

Sur MON ordinateur portable, j'installe les logiciels sur le disque "C" et je place les données sur le disque "D". Quand je réinstalle mon Windows, je ne formate que le disque "C". Du coup mes données sont toujours présentes sur "D". Je précise que je n'ai en réalité d'un seul disque dur (physiquement) dans MON portable. Les disques "C" et "D" sont en fait des partitions.

VII-D-3 - Installation de Maven

Maven se présente sous la forme d'une archive Zip qu'il suffit de décompresser au bon endroit sur votre disque dur.

Au moment où j'écris cet article, la page de téléchargement de Maven chez Apache (ne jamais la prendre ailleurs) est :
http://maven.apache.org/download.cgi

Comme il est possible d'avoir plusieurs versions de Maven en même temps sur un ordinateur, je vous conseille de les décompresser dans le dossier "C/dev/mavens" où le "s" à la fin de "mavens" prend tout son sens. Pensez juste à ne pas installer Java dans "Program Files".

Vous devez ensuite configurer votre variable d'environnement "PATH" en incluant le dossier "bin" de votre Maven. Par exemple, sur MON ordinateur, Maven est installé dans le dossier "C:/dev/mavens/apache-maven-3.0.3". J'ai donc ajouté le chemin "C:/dev/mavens/apache-maven-3.0.3/bin" dans la variable d'environnement "Path".

Pour modifier les variables d'environnement sous Windows, vous pouvez utiliser le raccourci [Windows] + [Pause]. Attention, sur les portables, il faut souvent compléter avec la touche bleue [Fn]. Dans la popup qui s'ouvre, choisissez "Paramètres système avancés" (à gauche sous Windows 7). Dans la nouvelle popup, cliquez sur le bouton "Variables d'environnement". Dans la popup il faut alors chercher "Path" dans la zone du bas…

Ouvrez une nouvelle console et tapez "mvn --version" pour vérifier que c'est bien pris en compte.

Maven

C:\>mvn --version

Apache Maven 3.0.3 (r1075438; 2011-02-28 18:31:09+0100)
Maven home: C:\dev\mavens\apache-maven-3.0.3\bin\..
Java version: 1.6.0_24, vendor: Sun Microsystems Inc.
Java home: C:\dev\javas\jdk1.6.0_24\jre
Default locale: fr_FR, platform encoding: Cp1252
OS name: "windows 7", version: "6.1", arch: "amd64", family: "windows"

Si vous faites attention, vous remarquez que ma version de Maven n'utilise pas ma version de Java par défaut "1.7.0_03" (cf. plus haut), mais la version "1.6.0_24". Pour faire ça, j'ai ajouté une instruction en haut du fichier "mvn.bat" (dans le dossier "bin") de Maven :

mvn.bat

echo Maven 3 de Thierry
set JAVA_HOME=C:\dev\javas\jdk1.6.0_24

Pour indiquer un emplacement spécifique pour le repository (là où Maven stocke les bibliothèques téléchargées), il faut configurer le fichier "settings.xml" :

settings.xml

<settings ...>
<localRepository>C:\dev\mavens\repository</localRepository>
...

VII-D-4 - FAQ de Developpez.com

Je vous encourage vivement à consulter les FAQ de Developpez.com : https://www.developpez.com/faq/

VII-E - Ancienne procédure de compillation et d'import dans Eclipse

VII-E-1 - Anciène procédure avec Maven

Durée estimée : 2 minutes.

Nous allons utiliser Maven en ligne de commande pour compiler le projet d'exemple et créer les fichiers Eclipse dont nous aurons besoin dans le chapitre suivant.

Ouvrez une console (terminal). Pour cela, sous Windows, dans le menu "Démarrer", utilisez la commande "cmd". Ça doit vous ouvrir une popup noire, qui vous place dans votre dossier personnel. Allez dans le dossier "C:\TP\tp-chien-dao" qu'on a créé plus tôt.

Dossier C:\TP\tp-chien-dao

Le fichier Maven "pom.xml" décrit la structure du projet d'exemple. Ouvrez-le dans un éditeur de texte comme Ultra Edit (ou Notepad++ ou PsPad mais surtout pas Word ou Bloc-notes) pour découvrir comment il est fait. Voici un résumé des parties importantes à ce stade.

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>

<modelVersion>4.0.0</modelVersion>

<groupId>com.icauda</groupId>
<artifactId>tp-chien-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>TP Chien</name>

<properties>
<!-- Construction du projet -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version>
<maven-compiler-plugin.version>2.3.1</maven-compiler-plugin.version>

<!-- Lib de test -->
<junit.version>4.8.2</junit.version>

<!-- Lib de log -->
<log4j.version>1.2.13</log4j.version>

...
</properties>

<dependencies>
<!-- Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>

...
</project>

Le fichier "pom.xml" indique ici une dépendance aux bibliothèques "jUnit" en version "4.8.2" et "Log4j" en version "1.2.13".

Les versions choisies ("4.8.2" et "1.2.13") sont tout simplement les dernières qui étaient disponibles quand j'ai écrit (ou mis à jour) ce document.

Compilez maintenant le projet à l'aide de la commande Maven suivante :

mvn clean install

L'option "clean" ne sert à rien à ce stade, mais prenez l'habitude de l'utiliser.

En fonction des éléments déjà présents sur l'ordinateur, l'exécution de la commande devrait ressembler à la trace suivante :

C:\TP\tp-chien-dao>mvn clean install

...
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building TP Chien 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ tp-chien-dao ---
[INFO] Deleting C:\TP\tp-chien-dao\target
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ tp-chien-dao ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:compile (default-compile) @ tp-chien-dao ---
[INFO] Compiling 8 source files to C:\TP\tp-chien-dao\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ tp-chien-dao ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.1:testCompile (default-testCompile) @ tp-chien-dao ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.9:test (default-test) @ tp-chien-dao ---
[INFO] Surefire report directory: C:\TP\tp-chien-dao\target\surefire-reports

-------------------------------------------------------
T E S T S
-------------------------------------------------------

Results :

Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.000s
[INFO] Finished at: Tue Oct 23 13:34:52 CEST 2012
[INFO] Final Memory: 8M/105M
[INFO] ------------------------------------------------------------------------
C:\TP\tp-chien-dao>

La trace "Final Memory: 8M/105M" indique simplement la mémoire utilisée sur MON ordinateur. La valeur dépend de nombreux facteurs, comme les autres logiciels (Word, Photoshop, etc.) lancés en même temps. En toute logique, la consommation mémoire sera donc différente d'un ordinateur à l'autre.

Voici une capture d'écran de ma console à ce stade :

Après la commande mvn clean install

Au passage, vous pouvez constater que le dossier "target" a été créé. Il contient la version compilée du projet.

Dossier target

Créez maintenant les fichiers Eclipse à l'aide de la commande Maven suivante :

mvn eclipse:eclipse

En fonction des éléments déjà présents sur l'ordinateur, l'exécution de la commande devrait ressembler à la trace suivante :

C:\TP\tp-chien-dao> mvn eclipse:eclipse

...
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building TP Chien 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO]  maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao
[INFO]
[INFO]  maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao
[INFO]
[INFO] --- maven-eclipse-plugin:2.8:eclipse (default-cli) @ tp-chien-dao ---
[INFO] Using Eclipse Workspace: null
[INFO] Adding default classpath container: org.eclipse.jdt.launching.JRE_CONTAINER
[INFO] Wrote settings to C:\TP\tp-chien-dao\.settings\org.eclipse.jdt.core.prefs
[INFO] Wrote Eclipse project for "tp-chien-dao" to C:\TP\tp-chien-dao.
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.604s
[INFO] Finished at: Tue Oct 23 13:35:34 CEST 2012
[INFO] Final Memory: 6M/76M
[INFO] ------------------------------------------------------------------------
C:\TP\tp-chien-dao>

Voici une capture d'écran de ma console à ce stade :

Après la commande mvn eclipse:eclipse

Déterminez à quoi sert Maven. Vous pouvez attendre la fin du TP pour répondre à cette question si c'est plus facile pour vous.

On aurait pu lancer les deux commandes "mvn clean install" et "mvn eclipse:eclipse" en une seule fois, pour gagner du temps :

mvn clean install eclipse:eclipse

Les fichiers "build.bat" et "build.sh" servent simplement de raccourcis pour lancer toutes les commandes Maven sans avoir à tout taper.

VII-E-2 - Ancienne procédure d'import dans Eclipse

Durée estimée : 1 minute.

Lancez Eclipse si ce n'est pas déjà fait. Eclipse devrait ressembler à la capture d'écran suivante :

Eclipse

Les zones (package, outline, console, etc.) qui composent le logiciel peuvent être disposées à votre convenance.

Importez le projet dans Eclipse. Pour cela, utilisez le menu "File/Import", qui ouvre la popup suivante :

Popup d'import

Choisissez l'option "General/Existing projects into workspace". Dans la suite, sélectionnez simplement le projet sur le disque.

De retour sur la fenêtre principale d'Eclipse, vous pouvez distinguer la structure des projets Java-Maven :

- les dossiers "src/test/java" et "src/test/resources" contiennent respectivement des tests écrits en Java et des fichiers de configuration (ressources) pour les tests ;

- les dossiers "src/main/java" et "src/main/resources" contiennent le code du programme et les ressources associées ;

- le bloc "Referenced librairies" contient la liste des dépendances (bibliothèques) utilisées par le projet. Ce sont ces mêmes dépendances qui apparaissent dans le fichier "pom.xml" ;

- le bloc "JRE System Librairy" correspond simplement à la version de Java que vous utilisez ;

- enfin le dossier "target" (déjà présenté plus haut) contient la version compilée par Maven du projet.

Structure du projet

Si les dépendances apparaissent en erreur, si le bloc est absent ou si vous avez des erreurs indiquant qu'il manque la variable « M2_REPO » (cf. capture d'écran et vidéo ci-dessous), c'est signe que tout n'est pas bien configuré. Consultez alors la FAQ (dans les annexes) et plus particulièrement les questions dédiées à Maven et "M2_REPO". Il est possible que la variable "M2_REPO" soit déjà présente mais non modifiable car est est bloquée par le plugin Maven (si installé). Dans ce cas, il faudra spécifier le bon fichier de setting au plugin Maven.

3 erreurs dans Eclipse qui indiquent un problème de config de M2_REPO

Vidéo de démo des étapes précédentes dans Eclipse Kepler, sans le plugin Maven (m2) :

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/v/yezOahmxZfA

Vidéo de démo des étapes précédentes dans Eclipse Luna, avec le plugin Maven (m2) :

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/v/D86OkJpNNUA

Merci de tenir compte de l'avertissement précédent, à propos de "M2_REPO".

Merci de vraiment tenir compte de l'avertissement à propos de la variable "M2_REPO" dans Eclipse.

Cette procédure (Maven + Eclipse) a aussi été expliquée, avec d'autres mots, dans l'article "Importer un projet Maven dans Eclipse en 5 minutes", disponible à l'adresse https://thierry-leriche-dessirier.developpez.com/tutoriels/java/importer-projet-maven-dans-eclipse-5-min/ Vous pouvez donc consulter cet article si vous avez besoin d'une seconde explication.

Ouvrez la classe "Launcher" que vous trouverez dans le package "com.icauda.tp.chien". Cette classe contient une méthode "main(..)" et est donc exécutable.

Classe Launcher

Lancez l'exécution de la classe "Launcher". Pour cela, sélectionnez la classe puis cliquez sur l'icône en forme de triangle (cf. capture). Si tout est bon, vous devriez voir apparaître des lignes dans la "console".

Exécution

Vous pouvez aussi lancer l'exécution à l'aide du bouton droit de la souris. Dans le menu contextuel qui apparaît, choisissez "Run as/Java application".

VII-F - Solution possible

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.icauda</groupId>
<artifactId>tp-chien-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>TP Chien</name>
<description>TP avec des chiens.</description>
<url>http://www.icauda.com</url>

<licenses>
<license>
<name>Copyright ©1995-2013 icauda.com et Copyright ©2012 Developpez.com</name>
<comments>Les sources présentés sur cette page sont libres de droits, et vous pouvez les utiliser à votre convenance.</comments>
</license>
</licenses>

<developers>
<!-- Thierry -->
<developer>
<name>Thierry Leriche-Dessirier</name>
<roles>
<role>Developper</role>
<role>Professeur</role>
</roles>
<organization>ICAUDA</organization>
</developer>

<!-- TODO Mettre votre nom ici, pour chaque membre de l'équipe. -->
<developer>
<name>Prénom Nom</name>
<roles>
<role>Etudiant</role>
</roles>
<organization>Mon école</organization>
</developer>
</developers>

<properties>

<!-- Construction du projet -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.6</java.version>
<maven-compiler-plugin.version>2.3.1</maven-compiler-plugin.version>

<!-- Lib de test -->
<junit.version>4.8.2</junit.version>
<unitils-dbunit.version>3.3</unitils-dbunit.version>

<!-- DB -->
<mysql-connector-java.version>5.1.18</mysql-connector-java.version>
<hsqldb.version>2.2.8</hsqldb.version>

<!-- Lib de log -->
<log4j.version>1.2.13</log4j.version>

<!-- Lib CSV -->
<opencsv.version>2.3</opencsv.version>
<csvengine.version>1.3.5</csvengine.version>

<!-- Libs pour les rapports -->
<maven-site-plugin.version>3.0-beta-3</maven-site-plugin.version>
<maven-javadoc-plugin.version>2.8</maven-javadoc-plugin.version>
<maven-jxr-plugin.version>2.3</maven-jxr-plugin.version>
<taglist-maven-plugin.version>2.4</taglist-maven-plugin.version>
<maven-surefire-plugin.version>2.9</maven-surefire-plugin.version>
<maven-surefire-report-plugin>2.9</maven-surefire-report-plugin>
</properties>

<dependencies>
<!-- Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>

<!-- Lib CSV -->
<dependency>
<groupId>net.sf.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>${opencsv.version}</version>
</dependency>

<dependency>
<groupId>fr.ybonnel</groupId>
<artifactId>csvengine</artifactId>
<version>${csvengine.version}</version>
</dependency>

<!-- Ci dessous des lib dont vous aurez besoin pour aller plus loin. -->

<!-- MySql et HSQLDB -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
<scope>test</scope>
</dependency>

<!-- Unitils (avec DBUnit) -->
<dependency>
<groupId>org.unitils</groupId>
<artifactId>unitils-dbunit</artifactId>
<version>${unitils-dbunit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<finalName>TP chien</finalName>

<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>

<!-- Doc generee par Maven -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>${maven-site-plugin.version}</version>
</plugin>

<!-- Surefire (rapport de test) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
</plugins>
</build>

<reporting>
<plugins>
<!-- Javadoc -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven-javadoc-plugin.version}</version>
</plugin>

<!-- JXR : pour lier les sources -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jxr-plugin</artifactId>
<version>${maven-jxr-plugin.version}</version>
</plugin>

<!-- Taglist : pour voir les TODO restant dans le code -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>taglist-maven-plugin</artifactId>
<version>${taglist-maven-plugin.version}</version>
<configuration>
<tagListOptions>
<tagClasses>
<tagClass>
<displayName>Todo Work</displayName>
<tags>
<tag>
<matchString>TODO</matchString>
<matchType>ignoreCase</matchType>
</tag>
<tag>
<matchString>FIXME</matchString>
<matchType>exact</matchType>
</tag>
</tags>
</tagClass>
<tagClass>
<displayName>Regles</displayName>
<tags>
<tag>
<matchString>REGLE</matchString>
<matchType>ignoreCase</matchType>
</tag>
</tags>
</tagClass>
</tagClasses>
</tagListOptions>
</configuration>
</plugin>

<!-- Surefire (tests JUnit) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>${maven-surefire-report-plugin}</version>
</plugin>
</plugins>
</reporting>

<repositories>
<repository>
<id>ybonnel-release</id>
<url>https://repository-ybonnel.forge.cloudbees.com/release/</url>
</repository>
<repository>
<id>ybonnel-snapshot</id>
<url>https://repository-ybonnel.forge.cloudbees.com/snapshot/</url>
</repository>
</repositories>

</project>

Pensez à utiliser la commande "mvn site" pour générer la doc du projet.

ChienDao.java (sans les commentaires)

package com.icauda.tp.chien.dao;

import java.util.List;

import com.icauda.tp.chien.domain.Chien;

public interface ChienDao {

List<Chien> findAllChiens();

Chien findChienByNom(final String nom);
}

CsvChienDao.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv;

import java.io.File;
import java.util.List;

import com.icauda.tp.chien.dao.ChienDao;

public interface CsvChienDao extends ChienDao {

public void init(File file);

public File getFile();

public List<String> getEntetes();
}

AbstractCsvChienDao.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv;

import java.io.File;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.domain.Chien;

public abstract class AbstractCsvChienDao implements CsvChienDao {

private static final Logger LOGGER = Logger.getLogger(AbstractCsvChienDao.class);

protected File file;
protected List<Chien> chiens;
protected Map<String, Chien> chienMapByNom;
protected List<String> entetes;

protected abstract void reloadChiens();;

@Override
public void init(File file) {
LOGGER.debug("init");
this.file = file;

reloadChiens();
}

@Override
public List<Chien> findAllChiens() {
LOGGER.debug("findAllChiens");

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chiens;
}

@Override
public Chien findChienByNom(final String nom) {

if (nom == null || nom.isEmpty()) {
throw new IllegalArgumentException("Le nom ne peut pas etre vide.");
}

if (chiens == null) {
throw new IllegalStateException("La liste n'a pas encore ete initialisee...");
}

return chienMapByNom.get(nom);
}

@Override
public File getFile() {
return file;
}

@Override
public List<String> getEntetes() {
return entetes;
}
}

AdvancedCsvChienDao.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.domain.Chien;
import com.icauda.tp.chien.domain.RaceDeChien;
import com.icauda.tp.chien.domain.Sexe;
import com.icauda.tp.chien.domain.SimpleChien;

public class AdvancedCsvChienDao extends AbstractCsvChienDao {

private static final Logger LOGGER = Logger.getLogger(AdvancedCsvChienDao.class);

private final static String SEPARATOR = ";";

private List<String> getLignesFromFile() {

LOGGER.debug("getLignesFromFile");

final List<String> lignes = new ArrayList<String>();

FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader(file);
br = new BufferedReader(fr);

for (String ligne = br.readLine(); ligne != null; ligne = br.readLine()) {

// Suppression des espaces en trop
ligne = ligne.trim();

// Filtre des lignes vides
if (ligne.isEmpty()) {
continue;
}

// Filtre des lignes de commentaire
if (ligne.startsWith("#")) {
continue;
}

lignes.add(ligne);
}
} catch (IOException e) {
LOGGER.error("Lecture impossible", e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
LOGGER.error("Fermeture impossible", e);
}
}
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
LOGGER.error("Fermeture impossible", e);
}
}
}

return lignes;

}

protected Chien transformLigneToChien(final String[] values) throws Exception {
LOGGER.debug("transformLigneToChien");

final SimpleChien chien = new SimpleChien();

chien.setNom(values[0]);
chien.setNomComplet(values[1]);

final String tempSexe = values[2];
final Sexe sexe = Sexe.valueOfByCode(new Integer(tempSexe));
chien.setSexe(sexe);

final String tempRace = values[3];
final RaceDeChien race = RaceDeChien.valueOfByCode(tempRace);
chien.setRace(race);

final String tempCouleurs = values[4];
final List<String> couleurs = new ArrayList<String>();
final String[] tempCouleurs2 = tempCouleurs.split(",");
for (String couleur : tempCouleurs2) {
couleurs.add(couleur);
}
chien.setCouleurs(couleurs);

final String tempPoids = values[5];
Double poids = new Double(tempPoids.replace(',', '.'));
chien.setPoids(poids);

return chien;
}

private Chien transformLigneToChien(final String ligne) throws Exception {
LOGGER.debug("transformLigneToChien");

final String[] values = ligne.split(SEPARATOR);

return transformLigneToChien(values);
}

protected void transformEntetes(final String[] tabEntetes) {
LOGGER.debug("transformEntetes");

entetes = new ArrayList<String>(tabEntetes.length);

for (String entete : tabEntetes) {
entetes.add(entete);
}
}

private void transformEntetes(final String ligneEntete) {
LOGGER.debug("transformEntetes");

final String[] tabEntetes = ligneEntete.split(SEPARATOR);

transformEntetes(tabEntetes);
}

/**
* Chargement des chiens.
*/
@Override
protected void reloadChiens() {
LOGGER.debug("reloadChiens");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

try {
final List<String> lignes = getLignesFromFile();

final String ligneEntete = lignes.remove(0);
LOGGER.debug("Entetes : " + ligneEntete);
transformEntetes(ligneEntete);

chiens = new ArrayList<Chien>(lignes.size());
chienMapByNom = new HashMap<String, Chien>(lignes.size());
for (String ligne : lignes) {
final Chien chien = transformLigneToChien(ligne);
chiens.add(chien);

chienMapByNom.put(chien.getNom(), chien);
}

} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}
}
}

OpenCsvChienDao.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv;

import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.log4j.Logger;

import au.com.bytecode.opencsv.CSVReader;

import com.icauda.tp.chien.domain.Chien;

public class OpenCsvChienDao extends AdvancedCsvChienDao {

private static final Logger LOGGER = Logger.getLogger(OpenCsvChienDao.class);

private final static char SEPARATOR = ';';

@Override
protected void reloadChiens() {
LOGGER.debug("reloadChiens");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

try {
final List<String[] > lignes = getLignesFromFile();

final String[] ligneEntete = lignes.remove(0);
transformEntetes(ligneEntete);

chiens = new ArrayList<Chien>(lignes.size());
chienMapByNom = new HashMap<String, Chien>(lignes.size());
for (String[] ligne : lignes) {
final Chien chien = transformLigneToChien(ligne);
chiens.add(chien);

chienMapByNom.put(chien.getNom(), chien);
}
} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}

}

private List<String[] > getLignesFromFile() {
LOGGER.debug("getLignesFromFile");

if (file == null) {
throw new IllegalStateException("Le fichier est nul...");
}

final List<String[] > lignes = new ArrayList<String[] >();

try {
final FileReader fr = new FileReader(file);
final CSVReader csvReader = new CSVReader(fr, SEPARATOR);

String[] nextLine = null;
while ((nextLine = csvReader.readNext()) != null) {
int size = nextLine.length;

// ligne vide
if (size == 0) {
continue;
}
String debut = nextLine[0].trim();
if (debut.isEmpty() && size == 1) {
continue;
}

// ligne de commentaire
if (debut.startsWith("#")) {
continue;
}
lignes.add(nextLine);
}

} catch (Exception e) {
LOGGER.error("aie aie aie", e);
}

return lignes;
}
}

EngineCsvChienDao.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.domain.Chien;
import com.icauda.tp.chien.domain.SimpleChien;

import fr.ybonnel.csvengine.CsvEngine;
import fr.ybonnel.csvengine.factory.AbstractCsvReader;
import fr.ybonnel.csvengine.factory.DefaultCsvManagerFactory;
import fr.ybonnel.csvengine.factory.OpenCsvReader;
import fr.ybonnel.csvengine.model.Error;
import fr.ybonnel.csvengine.model.Result;

public class EngineCsvChienDao extends AbstractCsvChienDao {

private static final Logger LOGGER = Logger.getLogger(EngineCsvChienDao.class);

private void setEngineFactory(final CsvEngine engine) {
engine.setFactory(new DefaultCsvManagerFactory() {
@Override
public AbstractCsvReader createReaderCsv(Reader reader, char separator) {
return new OpenCsvReader(reader, separator) {
@Override
public String[] readLine() throws IOException {
String[] nextLine = super.readLine();
if (isLineAComment(nextLine)) {
nextLine = readLine();
}
return nextLine;
}

private boolean isLineAComment(String[] line) {
return line != null && line.length > 0 && line[0].startsWith("#");
}
};
}
});
}

@Override
protected void reloadChiens() {
LOGGER.debug("reloadChiens");

try {
final CsvEngine engine = new CsvEngine(SimpleChien.class);
setEngineFactory(engine);

final FileInputStream fis = new FileInputStream(file);

final Result<SimpleChien> resultat = engine.parseInputStream(fis, SimpleChien.class);
final List<SimpleChien> dogs = resultat.getObjects();
chiens = new ArrayList<Chien>(dogs.size());
chienMapByNom = new HashMap<String, Chien>(dogs.size());
for (Chien chien : dogs) {
LOGGER.debug("[chien] " + chien);
chiens.add(chien);
chienMapByNom.put(chien.getNom(), chien);
}

List<Error> errors = resultat.getErrors();
LOGGER.debug(errors);

entetes = engine.getColumnNames(SimpleChien.class);

LOGGER.debug("[entetes] " + entetes);

} catch (Exception e) {
LOGGER.error("Une erreur s'est produite...", e);
}
}

}

SimpleChien.java (sans les commentaires)

package com.icauda.tp.chien.domain;

import java.util.List;

import com.icauda.tp.chien.domain.RaceDeChien.AdapterRace;
import com.icauda.tp.chien.domain.Sexe.AdapterSexe;
import com.icauda.tp.chien.domain.adapter.AdapterCouleurs;
import com.icauda.tp.chien.domain.adapter.AdapterPoids;

import fr.ybonnel.csvengine.annotation.CsvColumn;
import fr.ybonnel.csvengine.annotation.CsvFile;

@CsvFile(separator = ";")
public class SimpleChien implements Chien {

private static final long serialVersionUID = -1225454238084424608L;

@CsvColumn(value = "Nom", order = 0)
private String nom;

@CsvColumn(value = "Nom complet", order = 1)
private String nomComplet;

@CsvColumn(value = "sexe", adapter = AdapterSexe.class, order = 2)
private Sexe sexe;

@CsvColumn(value = "race", adapter = AdapterRace.class, order = 3)
private RaceDeChien race;

@CsvColumn(value = "couleurs", adapter = AdapterCouleurs.class, order = 4)
private List<String> couleurs;

@CsvColumn(value = "poids", adapter = AdapterPoids.class, order = 5)
private Double poids;

public SimpleChien() {
// rien...
}

public SimpleChien(final String nom) {
this.nom = nom;
}

public SimpleChien(final String nom, final RaceDeChien race, final Sexe sexe) {
this(nom);
this.race = race;
this.sexe = sexe;
}

@Override
public String toString() {
return "SimpleChien [nom=" + nom + "]";
}

// + getters et setters

}

AbstractCsvChienDaoTest.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv;

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

import java.io.File;
import java.util.List;

import org.apache.log4j.Logger;
import org.junit.Assert;

import com.icauda.tp.chien.domain.Chien;
import com.icauda.tp.chien.domain.RaceDeChien;

public abstract class AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(AbstractCsvChienDaoTest.class);

public final static String RESOURCES_PATH = "src/test/resources/";

protected CsvChienDao dao;

protected void prepare(final String fileName) {
LOGGER.debug("prepare");

final File file = new File(RESOURCES_PATH + fileName);
dao.init(file);

}

protected void doTestNbChiens(final int nombreChiensAttendus) {
// Act
final List<Chien> chiens = dao.findAllChiens();

// Assert
assertEquals(nombreChiensAttendus, chiens.size());

}

protected void doTestNbNom(final int position, final String nomAttendu) {
// Act
final List<Chien> chiens = dao.findAllChiens();
final Chien chien = chiens.get(position);

// Assert
assertEquals(nomAttendu, chien.getNom());
}

protected void doTestNbPoids(final int position, final Double poidsAttendu) {
// Act
final List<Chien> chiens = dao.findAllChiens();
final Chien chien = chiens.get(position);

// Assert
assertEquals(poidsAttendu, chien.getPoids());
}

protected void doTestNbCouleurs(final int position, final String[] couleursAttendues) {
final List<Chien> chiens = dao.findAllChiens();
final Chien chien = chiens.get(position);

// Assert
final List<String> couleurs = chien.getCouleurs();
final int size = couleurs.size();
LOGGER.debug("[size] " + size);
assertEquals(couleursAttendues.length, size);

// Test dans ordre
for (String couleurAttendue : couleursAttendues) {
assertTrue(couleurs.contains(couleurAttendue));
}

}

protected void doTestNbRace(final int position, final RaceDeChien raceAttendue) {
// Act
final List<Chien> chiens = dao.findAllChiens();
final Chien chien = chiens.get(position);

// Assert
assertEquals(raceAttendue, chien.getRace());
}

protected void doTestTailleEntetes(final int tailleAttendue) {
// Act
final List<String> entetes = dao.getEntetes();

// Assert
assertEquals(tailleAttendue, entetes.size());
}

protected void doTestNbEntete(final String enteteAttendu, final int position) {
// Act
final List<String> entetes = dao.getEntetes();
final String entete = entetes.get(position);

// Assert
assertEquals(enteteAttendu, entete);

}

protected void doTestAllEntetes(final String[] entetesAttendus) {
final List<String> entetes = dao.getEntetes();

// Assert
// Ici je teste la taille, non pas pour verifier la taille, mais pour
// verifier que les deux listes
// contiennent le meme nombre d'elements. Ca m'evite de lancer deux
// boucles for.
Assert.assertEquals(entetesAttendus.length, entetes.size());
for (int i = 0; i < entetesAttendus.length; i++) {
assertEquals(entetesAttendus[i], entetes.get(i));
}
}

protected void doTestRechercheChienByNom(final String nom, final Double poidsAttendu) {
// Act
final Chien chien = dao.findChienByNom(nom);

// Assert
assertNotNull(chien);
assertEquals(nom, chien.getNom());
assertEquals(poidsAttendu, chien.getPoids());

}

protected void doTestRechercheFailed(final String nom) {
// Act
final Chien chien = dao.findChienByNom(nom);

// Assert
assertNull(chien);
}
}

AbstractCsvChienDaoTest01.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv.csv01;

import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import com.icauda.tp.chien.dao.csv.AbstractCsvChienDaoTest;
import com.icauda.tp.chien.domain.RaceDeChien;

public abstract class AbstractCsvChienDaoTest01 extends AbstractCsvChienDaoTest {
private static final Logger LOGGER = Logger.getLogger(AbstractCsvChienDaoTest01.class);

public final static String CHIENS_FILE_NAME = "chiens-01.csv";

@Before
public void doBefore() {
LOGGER.debug("doBefore Debut");

prepare(CHIENS_FILE_NAME);

LOGGER.debug("doBefore Fin");
}

@Test
public void testCinqChiens() {
LOGGER.debug("testCinqChiens... Debut");

// Arrange
final int nombreChiensAttendus = 5;

// Act and assert
doTestNbChiens(nombreChiensAttendus);

LOGGER.debug("testCinqChiens... Fin");
}

@Test
public void testPremierEstMilou() {
LOGGER.debug("testPremierEstMilou... Debut");

// Arrange
final int position = 0;
final String nomAttendu = "Milou";

// Act and assert
doTestNbNom(position, nomAttendu);

LOGGER.debug("testPremierEstMilou... Fin");
}

@Test
public void testPoidsLassie() {
LOGGER.debug("testPoidsLassie... Debut");

// Arrange
final int position = 2;
final Double poidsAttendu = 32.3;

// Act and assert
doTestNbPoids(position, poidsAttendu);

LOGGER.debug("testPoidsLassie... Fin");
}

@Test
public void testCouleursVolt() {
LOGGER.debug("testCouleursVolt... Debut");

// Arrange
final int position = 3;
final String[] couleursAttendues = { "blanc", "noir" };

// Act and assert
doTestNbCouleurs(position, couleursAttendues);

LOGGER.debug("testCouleursVolt... Fin");
}

@Test
public void testRacePluto() {
LOGGER.debug("testRacePluto... Debut");

// Arrange
final int position = 1;
final RaceDeChien raceAttendue = RaceDeChien.GOLDEN;

// Act and assert
doTestNbRace(position, raceAttendue);

LOGGER.debug("testRacePluto... Fin");
}

@Test
public void testTailleEntetes() {
LOGGER.debug("testTailleEntetes... Debut");

// Arrange
final int tailleAttendue = 6;

// Act and assert
doTestTailleEntetes(tailleAttendue);

LOGGER.debug("testTailleEntetes... Fin");
}

@Test
public void testPremierEntetes() {
LOGGER.debug("testPremierEntetes... Debut");

// Arrange
final String enteteAttendu = "Nom";
final int position = 0;

// Act and assert
doTestNbEntete(enteteAttendu, position);

LOGGER.debug("testTailleEntetes... Fin");
}

@Test
public void testAllEntetes() {
LOGGER.debug("testAllEntetes... Debut");

// Arrange
final String[] entetesAttendus = { "Nom", "Nom complet", "sexe", "race", "couleurs", "poids" };

// Act and assert
doTestAllEntetes(entetesAttendus);

LOGGER.debug("testAllEntetes... Fin");
}

@Test
public void testRechercheLassie() {
LOGGER.debug("testRechercheLassie... Debut");

// Arrange
final String nom = "Lassie";
final Double poidsAttendu = 32.3;

// Act and assert
doTestRechercheChienByNom(nom, poidsAttendu);

LOGGER.debug("testRechercheLassie... Fin");
}

@Test
public void testRechercheIdefix() {
LOGGER.debug("testRechercheIdefix... Debut");

// Arrange
final String nom = "Idefix";

// Act and assert
doTestRechercheFailed(nom);

LOGGER.debug("testRechercheIdefix... Fin");
}
}

EngineCsvChienDaoTest01.java (sans les commentaires)

package com.icauda.tp.chien.dao.csv.csv01;

import org.apache.log4j.Logger;

import com.icauda.tp.chien.dao.csv.EngineCsvChienDao;

public class EngineCsvChienDaoTest01 extends AbstractCsvChienDaoTest01 {
private static final Logger LOGGER = Logger.getLogger(EngineCsvChienDaoTest01.class);

public EngineCsvChienDaoTest01() {
LOGGER.debug("Constructeur...");

dao = new EngineCsvChienDao();
}

}