Formes, chenilles, etc .... L'épisode final !

Objectifs : L'objectif de ce TP est de vous faire expérimenter avec la gestion des exceptions en Java. En réalisant les exercices proposés vous serez conduits à écrire du code gérant ainsi que du code provoquant des exceptions.

Dans le TP précédent a été développée une application permettant d'afficher et d'animer dans une même fenêtre toutes sortes d'objets: des formes géométriques, des "visages", des "chenilles".

La séance d'aujourd'hui va consister à étendre cette application afin de pouvoir créer les objets à afficher et éventuellement à animer à partir d'un fichier de description textuel chargé au lancement du programme.

Exercice 1: Utiliser les WorkSpaces de VS Code

Dans ce premier exercice vous allez apprendre à utiliser les WorkSpaces de Visual Studio Code pour pouvoir travailler en même temps sur plusieurs projets reliés entre eux.

Pour en savoir plus sur les WorkSpaces de Visual Studio Code, vous pouvez consulter la page suivante: Multi-root Workspaces

  1. Créez un répertoire TP10 dans votre répertoire de travail PLAI/Java

  2. Dans ce répertoire téléchargez :

    • sur gitlab, la Release v7.2 du projet Formes-Animées qui contient un projet Java maven correspondant la correction complète du TP Formes Animées.

    • le fichier fichiers-animation.zip qui contient un projet Java maven d'application qui nous servira de point de départ pour la réalisation de ce TP.

    Le contenu du répertoire TP10 est alors le suivant

  3. Dans le répertoire TP10, décompressez puis supprimez les archives téléchargées (fichiers .zip ou .tar.gz).

  4. Ouvrez Visual Studio Code avec la commande code, puis ajoutez à votre espace de travail (Workspace) le projet maven formes-animees-v7.2.

  5. Une fois le projet chargé, vérifiez que l'application AppliNChenilles peut être compilée et exécutée

  6. En procédant comme à l'étape 4, ajoutez à votre espace de travail (Workspace) le projet maven fichiers-animation. Une fois le projet chargé, l'explorateur de VScode doit avoir le contenu suivant :

  7. Vous pouvez constater que le code de fichiers-animation ne compile pas. Pouvez-vous expliquer pourquoi ? Sauriez-vous comment remédier à ce problème ?

  8. Ce nouveau projet (fichiers-animation) contient du code java qui fait référence au code java des classes et interfaces développées dans le TP précédent et donc présentes dans le projet formes-animees-v7.2. Pour que le projet fichiers-animation puisse être compilé il faut donc indiquer où trouver le code des classes projet formes-animees-v7.2. Plutôt que de recopier (et dupliquer) le code de celles-ci dans ce nouveau projet, il vaut mieux créer une dépendance entre projets. Ces projets étant gérés via l'outil de construction maven, cette dépendance est définie dans le fichier pom.xml du projet fichiers-animation.

    La dépendance est définie en indiquant le groupId, l'artifcatId et n° de version du projet formes-animees-v7.2. Le code XML à insérer est le suivant.

        <dependencies>
            <dependency>
                <groupId>fr.im2ag.m2cci</groupId>
                <artifactId>Formes-Animees</artifactId>
                <version>7.2</version>
            </dependency>
        </dependencies>

    Au moment d'enregister le fichier pom.xml, pensez à demander la synchronisation du classpath de votre application afin que votre code puisse être recompilé correctement.

    De plus les deux projets étant dans le même Workspace, VScode ira ainsi automatiquement chercher les classes dans les répertoires du projet formes-animees-v7.2 et toute modification de ce dernier projet sera immédiatement répercutée sur le projet fichiers-animation.

  9. Mainteant que le projet fichiers-animation ne comporte plus d'erreurs,

    Verifiez que le projet fichiers-animation fonctionne correctement en exécutant le programme TestDessinFileReader situé dans le package fr.im2ag.m2ccci.applis.

    Le programme principal (main) situé dans la classe TestDessinFileReader crée un dessin à partir d'une description textuelle de formes contenue dans un fichier. Lorsque vous exécutez cette classe, un nom de fichier vous est demandé sur la console, tappez data/polygones1.txt. Vous devez obtenir un affichage identique à la figure suivante.

    trois polygones réguliers
    Figure 2: affichage produit par l'exécution de TPexceptions avec le fichier data/polygones1.txt

    Le fichier polgyones1.txt situé dans le répertoire data à la racine du projet fichiers-animation contient les descriptions suivantes (pour le moment les seules supportées par l'application) :

    fichier de données
    Accès au fichier de données polygones1.txt
  10. Maintenant que tout fonctionne, vous pouvez sauvegarder votre espace de travail afin de pouvoir le réutiliser ultérieurement.

    fichier de données
    Sauvegarde de l'espace de travail (Workspace)
  11. Fermer VScode et réouvrez le en lançant la commande code TP10Exceptions.code-workspace & en vous plçant dans le répertoire TP10

    fichier de données
    Ouverture de VSCode sur une espace de travail (Workspace)

Exercice 2: Attraper des exceptions

Exercice 2.1:

  1. Que se passe-t-il lorsque le nom de fichier donné par l'utilisateur au lancement de l'application ne correspond pas à un fichier existant ? Par exemple, observez ce que donne l'exécution du programme avec la data/polygone1.txt comme nom de fichier. A quel endroit du programme l'exécution s'est-elle arrêtée ?

    Essayez de trouvez la réponse par vous-même. Vous pouvez ensuite comparer avec la bonne réponse en cliquant sur ce bouton:

    une exception de type FileNotFoundException est levée, ce qui interrompt l'exécution normale du programme.

    FNF exception
    Figure 1: levée d'une exception FileNotFoundException


  2. Modifiez le programme pour que, en cas d'erreur sur le nom du fichier, l'utilisateur puisse proposer un nouveau nom.

    Essayez de trouvez une solution par vous-même.Vous pouvez ensuite comparer avec une solution en cliquant sur ce bouton:

    Pour cela il faut attraper l'exception et recommencer la saisie à l'aide d'une boucle do while. Cela entraine donc les modifications suivantes dans le programme principal:

    ...
    // chargement des données à partir d'un fichier
    Scanner sc = new Scanner(System.in);
    boolean chargementOK = false;
    do {
         try {
            System.out.println("Entrez le chemin relatif pour accéder au fichier de données : ");
            String cheminRelatif = sc.nextLine();
            String cheminAbsolu = System.getProperty("user.dir") + "/" + cheminRelatif;
            DessinFileReader.chargerDessinables(cheminAbsolu, d);
            // le fichier a été correctement chargé
            chargementOK = true; // en cas de problème une exception est levée et 
                                 // cette affectation n'est pas effectuée, on 
                                 // passe directement dans la clause catch
         } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
         }
    } while (! chargementOK);
    ...

Le format général des fichiers de descriptions d'objets est le suivant:

  • Toute ligne débutant par un # est un commentaire qui est ignoré lorsque le fichier est lu.

  • Les lignes blanches ne sont pas significatives et sont également ignorées à la lecture.

  • Une ligne de description permet de définir les caractéristique de l'objet à créer et à afficher:

    • la ligne débute un code qui définit le type d'objet décrit, ici la lettre 'F' indique qu'il s'agit d'une forme géométrique

    • cette lettre est suivie d'un code (une chaîne de caractères sans espaces) indiquant le type de la forme,

    • ce code est suivi des paramètres permettant de spécifier la forme,

    • les différents éléments sont séparés par un ou plusieurs espaces qui ne sont pas significatifs.

    Par exemple pour un polygone regulier on aura :

    explications pour le type POLYR
    Figure 3: description d'un polygone regulier

Exercice 2.2:

  1. Que se passe-t-il lorsque l'utilisateur propose de charger le fichier polygones2.txt situé dans le répertoire data ?

    Essayez de trouvez la réponse par vous-même. Vous pouvez ensuite comparer avec la bonne réponse en cliquant sur ce bouton:

    Une exception de type NombreArgumentsIncorrect est levée par la méthode lirePolygoneRegulier. Cette exception n'est pas traitée par le code (pas de clause catch l'attrapant), elle remonte donc jusqu'au programme principal, l'exécution est interrompue et la pile des appels est affichée sur la console.

    FNF exception
    Figure 2: levée d'une exception NombreArgumentsIncorrect

    Cette exception est une exception contrôlée définie dans l'application d'animation des formes. C'est une sous classe de la classe DessinFileReaderException qui regroupe plusieurs type d'exceptions:

    hiérarchie des exceptions
    Figure 3: hiérarchie des exceptions de l'application
    • UnkownObjectException la ligne de description d'un objet lue dans le fichier texte débute par un code qui ne correspond pas à un type objet reconnu par le "reader"
    • UnkownFormeException si dans le fichier texte la ligne de description lue dans le fichier texte contient un code de forme qui n'est pas reconnu par le "reader"
    • NombreArgumentsIncorrect si pour une forme ou un animateur il manque ou il y trop d'arguments dans la ligne de description lue dans le fichier texte.


  2. Sans modifiez le fichier polygones2.txt, corrigez le programme pour que, en cas d'erreur sur une description d'objet, l'exécution du programme ne soit pas arrêtée mais que la ligne soit simplement ignorée.

    Vérifiez que votre correction est bonne en rechargeant le fichier polygones2.txt. Vous ne devez alors obtenir l'affichage que des formes dont la description est correcte, à savoir, comme dans l'exercice 1, un pentagone rouge, un octogone vert et un hexagone bleu (voir figure 2).

    Essayez de trouvez une solution par vous-même. Cependant, si vous n'avez pas d'idées sur la manière de procéder et si vous souhaitez être guidé vers la solution, cliquez sur le bouton ci-dessous

    Si l'on observe le parcours de l'exception NombreArgumentsIncorrect, celle-ci est lancée (levée) dans la méthode lirePolyRegulier qui la déclare dans sa signature (clause throws).

    private static PolygoneRegulier lirePolyRegulier(String[] params) throws NombreArgumentsIncorrect {
       if (params.length != 7) {
          throw new NombreArgumentsIncorrect("POLYR", params.length, 7);
       }
       ...

    lirePolyRegulier est invoquée par la méthode lireForme qui non seulement ne traite pas l'exception NombreArgumentsIncorrect mais en plus peut lancer une exception UnknownFormeException au cas où le type de forme passé en paramètre est inconnu.

    private static IForme lireForme(String typeForme, String[] paramsForme) throws
                UnknownFormeException, NombreArgumentsIncorrect {
      switch (typeForme) {
        case "POLYR":
            return lirePolyRegulier(paramsForme);
        default:
            throw new UnknownFormeException(typeForme);
       }
    }

    lireForme est elle même invoquée par la méthode chargerDessinables qui elle n'ont plus ne traite pas ces exceptions et les transmet au programme appelant (dans le cas de notre application le programme principal, main de la classe TPExceptions).

    public static void chargerDessinables(String fileName, Dessin dessin)
                throws FileNotFoundException, IOException, UnknownObjectException,
                       UnknownFormeException, NombreArgumentsIncorrect

    La figure 3 illustre cette remontée.

    lancement et remontée d'une exception NombreArgumentsIncorrect
    Figure 3: lancement et remontée d'une exception NombreArgumentsIncorrect

    La question qui se pose est donc la suivante:

    A quel niveau l'exception NombreArgumentsIncorrect doit-elle être attrapée et traitée, afin de ne pas interrompre la lecture d'un fichier de description lorsqu'elle est levée ?

    Il est clair qu'attraper l'exception au niveau du programme principal comme nous l'avons fait pour l'exception FileNotFoundException interviendrait trop tard. La lecture du fichier de description ne pourrait être poursuivie.

    Attraper l'exception au niveau de lireForme interviendrait trop tôt. En effet du coup l'exécution de chargerDessinable pourrait alors se poursuivre normalement, alors que la forme n'a pu être créée, ce qui n'a pas de sens (voir figure 4).

    attraper l'exception NombreArgumentsIncorrect dans lireForme
    Figure 4: attraper l'exception NombreArgumentsIncorrect dans lireForme

    L'exception doit donc être attrapée au niveau de la méthode chargerDessinables. La question est maintenant : dans quel bloc ?

    Il est clair que l'exception doit être attrapée dans le bloc de la boucle while puisque l'objectif est justement de ne pas interrompre celle-ci. La figure 5 nous montre les principales options qu'il nous reste.

    où attraper l'exception NombreArgumentsIncorrect
    Figure 5: où attraper l'exception NombreArgumentsIncorrect dans chargerDessinables ?

    Examinons chacune de ces possibilités

    • (3) mettre le try ... catch dans le case : sachant que ce type d'exception (NombreArgumentsIncorrect) peut aussi intervenir dans les appels de fonction effectués dans les autres case, faire le traitement à ce niveau là impliquerait de faire la même chose dans chacun des case. Nous rejetons donc cette solution.
    • (2) englober tout le switch par le bloc try ... catch : si cela permettrait d'avoir un seul bloc try ... catch pour attraper toutes les exceptions pouvant intervenir dans les différents case et fonctionnerait dans le cas présent cette solution a néanmoins un inconvenient. Le point de reprise après le bloc catch n'est pas directement l'instruction de passage à la ligne suivante. Si jamais, plus tard, le code est modifié et que de nouvelles instructions sont ajoutées après le catch avant l'instruction de lecture de la ligne suivante il y a un risque potentiel d'erreur.
    • (1) englober tout le contenu de la boucle while, excepté l'instruction de passage à la ligne suivante, par le bloc try ... catch : c'est cette solution que nous adopterons. En effet, contrairement à la précédente, elle indique sans ambiguité où est le point de reprise normale de l'exécution.

    Le traitement de l'exception consiste à simplement écrire sur un message signalant que la ligne a été ignorée en indiquant la raison. On obtient donc le code suivant pour la méthode chargerDessinables:

    public static void chargerDessinables(String fileName, Dessin dessin)
                throws FileNotFoundException, IOException, UnknownObjectException, UnknownFormeException {
       try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
             // try avec ressources, qui permet de fermer automatiquement le reader
    
          String ligne; // chaîne contenant la ligne courante.
          int noLigne = 0;    // numéro de la ligne en cours d'analyse.
          IForme forme; // la dernière forme créée
          IFormeAnimator animator = null; // le dernier animateur lu
          ligne = reader.readLine();
          while (ligne != null) {
             noLigne++;
             System.out.println(ligne);
             try {
                 if (!"".equals(ligne) && !ligne.startsWith("#")) {
                     // ligne non vide et non commentaire
                     // recupération dans un tableau de chaînes des différents élements de la ligne
                     String[] tokens = ligne.toUpperCase().split("\\s+");
                     switch (tokens[0]) {
                          case "F":
                               forme = lireForme(tokens[1], Arrays.copyOfRange(tokens, 2, tokens.length));
                               System.out.println("--> forme créée");
                               if (animator != null) {
                                   dessin.ajouterObjet(new FormeAnimee(forme, animator));
                                   animator = null;
                               } else {
                                   dessin.ajouterObjet(forme);
                               }
                                break;
                          case "A":
                               System.out.println("Animateur");
                               animator = lireAnimator(tokens);
                                break;
                          case "C":
                               System.out.println("Chenille");
                               dessin.ajouterObjet(lireChenille(tokens));
                               break;
                          default:
                               throw new UnknownObjectException(tokens[0]);
                   } // fin du switch 
                } // fin du if (! ligne.equals(""))
             }  // fin du bloc try
             catch (NombreArgumentsIncorrect  ex) {
                        System.out.println("Ligne no " + noLigne + " ignorée");
                        System.out.println(ex.getMessage());
             }
             // passage à la ligne suivante
             ligne = reader.readLine();
         } // fin du while
       } // fin du try avec resources
    }

    Les exceptions de type NombreArgumentsIncorrect étant traitées au niveau de la méthode chargerDessinables, elles ne sont plus transmises au programme appelant et sont donc retirées dae la clause throws de la signature de la méthode.

Exercice 2.3: Même exercice que le précédent, mais avec le fichier data/polygones3.txt.

Le fichier de données contient deux erreurs qui soulèvent respectivement des exceptions de type UnknownFormeException et UnknownObjectException.

Le traitement de ces exceptions est similaire à celui des exceptions NombreArgumentsIncorrect. Elles sont déclenchées dans le même bloc try. Le traitement par un bloc catch consiste à simplement écrire sur un message signalant que la ligne a été ignorée en indiquant la raison (fournie par le message de l'exception). Il y a plusieurs manières de programmer cela.

  1. En écrivant un bloc catch pour chaque type d'exception.

    try {
       ...
    }
    catch (NombreArgumentsIncorrect ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }
    catch (UnknownFormeException ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }
    catch (UnknownObjectException ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }
  2. On voit bien que le traitement est le même pour chaque type d'exception. On peut simplifier le code sachant que NombreArgumentsIncorrect, UnknownFormeException et UnknownObjectException sont sous classes de DessinFileReaderException. Elles peuvent donc toutes être traitées par une seule clause catch. Ce qui donne donc le code suivant :

    try {
       ...
    }
    catch (DessinFileReaderException ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }

Ces exceptions étant traitées dans la méthode chargerDessinables il faut les retirer de la clause throws dans sa signature qui devient

public static void chargerDessinables(String fileName, Dessin dessin) throws FileNotFoundException, IOException 

Exercice 2.4: Même exercice que le précédent, mais avec le fichier data/polygones4.txt.

la lecture du fichier data/polygones4.txt provoque la levée d'une exception de type NumberFormatException qui peut être levée lorsqu'un argument associé à une valeur numérique ne correspond pas au type attendu (par exemple 8.0 au lieu de 8 pour un entier, ou 12a4 ...).

remarque: cette exception étant de type RuntimeException il n'y a pas obligation de la déclarer dans la clause throws des méthodes pouvant la lever. Ce qui ne nous empêche pas de la traiter ici.

Le traitement de ces exceptions est similaire à celui des exceptions DessinFileReaderExceptionconsidérées dans les exercices précédents. Il consiste à simplement écrire sur un message signalant que la ligne a été ignorée en indiquant la raison et est localisée au même endroit dans la boucle while de la méthode chargerDessinables. Il y a plusieurs manières de programmer cela :

  1. En écrivant un bloc catch spécifique.

    try {
       ...
    }
    catch (DessinFileReaderException ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }
    catch (NumberFormatException ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }
  2. Depuis, la version 7 de Java, il est même possible de regrouper plusieurs types d'exceptions dans une même clause catch. Le code précédent peut donc être encore simplifié.

    try {
       ...
    }
    catch (DessinFileReaderException | NumberFormatException ex) {
        System.out.println("ligne ignorée : " + ex.getMessage()); 
    }

Exercice 3: Prise en charge de nouveaux types de formes : les étoiles et disques

Modifiez l'application pour pouvoir prendre en compte d'autres types de formes que les polygones réguliers : les étoiles et les disques.

  1. Définissez un format textuel pour ces deux type de formes.

  2. Ajoutez au fichier polygones1.txt une description pour une étoile de couleur orange et un disque de couleur jaune.

  3. Modifiez l'application pour qu'elle puisse les afficher et vérifiez que vous obtenez bien l'affichage escompté

Essayez de trouvez une solution par vous-même. Cependant, si vous n'avez pas d'idées sur la manière de procéder et si vous souhaitez avoir quelques indications, cliquez sur ce bouton:

Il s'agit ici de simplement produire un code relativement proche de celui de lirePolyRegulier et de modifier en conséquence la méthode lireForme en ajoutant les différents nouveaux cas de formes.

La description d'une étoile ou d'un disque sont très similaires :

format de description d'une étoile
Figure 6: format de description d'une étoile

Seul diffère le type de forme avec par exemple le ETOILE pour les étoiles et le code DISQUE pour les disques. Aussi pouvons nous écrire une seule méthode pour lire ces deux types de formes :

    /**
     * Construit une forme décrite à partir des paramètres lus dans un fichier
     * de descriptions, ces paramètres étant la description d'un cercle et d'une
     * couleur. La forme peut être un disque ou une étoile.
     * 
     * @param noLigne numéro de la ligne dans le fichier
     * @param params  les paramètre décrivant la forme (x, y, r, r, v, b)
     * @return la forme (Etoile ou Disque)
     * @throws NombreArgumentsIncorrect si le nombre de paramètres de
     *                                  description ne correspond pas au nombre
     *                                  attendu.
     */
    private static FormeCirculaire lireFormeCirculaire(int noLigne, String typeForme, String[] params)
            throws NombreArgumentsIncorrect, UnknownFormeException {
        if (params.length != 6) {
            throw new NombreArgumentsIncorrect(noLigne, typeForme, params.length, 6);
        }
        int x = Integer.parseInt(params[0]);
        int y = Integer.parseInt(params[1]);
        int r = Integer.parseInt(params[2]);
        Color c = new Color(Integer.parseInt(params[3]), Integer.parseInt(params[4]),
                Integer.parseInt(params[5]));
        switch (typeForme.toUpperCase()) {
            case "ETOILE":
                return new Etoile(x, y, r, 1.0f, c, c);
            case "DISQUE":
                return new Disque(x, y, r, 1.0f, c, c);
            default:
                throw new UnknownFormeException(noLigne, typeForme);
        }
    }

Le code de lireForme est alors modifié comme suit:

    /**
     * @param noLigne     numéro de la ligne dans le fichier
     * @param typeForme   le type de la forme
     * @param paramsForme le tableau des paramètres de la forme
     *
     * @return la référence d'un objet forme correspondant à la description
     *
     * @throws UnknownFormeException    si le type de forme n'est pas reconnu.
     *
     * @throws NombreArgumentsIncorrect si le nombre de paramètres de
     *                                  description n'est pas le nombre attendu.
     */
    private static IForme lireForme(int noLigne, String typeForme, String[] paramsForme)
            throws UnknownFormeException, NombreArgumentsIncorrect {
        switch (typeForme) {
            case "POLYR":
                return lirePolyRegulier(noLigne, paramsForme);
            case "ETOILE":
            case "DISQUE":
                return lireFormeCirculaire(noLigne, typeForme, paramsForme);
            default:
                throw new UnknownFormeException(noLigne, typeForme);
        }
    }
une image de l'affichage produit
Figure 4: affichage attendu

Exercice 4: Animer les formes

On souhaite rajouter dans les fichiers texte, la possibilité de décrire des animateurs pour les formes. Comme pour une forme, un Animateur sera décrit par une ligne dans le fichier texte. L'animateur s'applique à la forme qui le suit dans le fichier.

Une ligne de description d'un Animateur a la structure suivante:

  • la ligne débute par la lettre 'A',

  • cette lettre est suivie d'un code (une chaîne de caractères sans espaces) indiquant le type de l'animateur,

  • ce code est suivi des paramètres permettant de spécifier l'animateur,

  • les différents éléments sont séparés par un ou plusieurs espaces qui ne sont pas significatifs.

Par exemple pour un animateur de cap on aura :

explications pour le type POLYR
Figure 5: description d'un animateur selon un cap.

Exercice 4.1: Modifiez le fichier de données et l'application de manière à ce que l'étoile jaune ait un mouvement aléatoire (animateur de cap) et le pentagone rouge ait un mouvement circulaire.

une image de l'affichage produit
Figure 6: animation des formes.

Essayez de trouvez une solution par vous-même. Cependant, si vous n'avez pas d'idées sur la manière de procéder et si vous souhaitez avoir quelques indications, cliquez sur ce bouton:

La aussi le code est très similaire. On peut néanmoins introduire un nouveau type d'exception UnkownAnimatorException pour signaler un type d'animateur non reconnu. On peut à cette occasion restructurer un peu nos type d'exceptions (voir figure 7) en plaçant les types UnkownObjectException, UnkownFormeException et UnkownAnimatorException sous un nouveau type plus génrique UnkownTypeException

nouvelle hiéarchie d'exceptions
Figure 7: nouvelle hiéarchie d'exceptions

La classe d'exception UnkownTypeException est définie comme suit:

public class UnknownTypeException extends DessinFileReaderException {

    public UnknownTypeException(int noLigne, String codeType) {
        super(noLigne,codeType + " est un type non connu");
    }
}

La classe d'exception UnkownAnimatorException est définie comme suit:

public class UnknownAnimatorException extends UnknownTypeException {

    public UnknownAnimatorException(int noLigne, String animatorType) {
        super(noLigne, animatorType + " est un type d'animateur non connu");
    }
}

et les classes d'exception UnkownObjectException et UnkownFormeException sont modifiées en changeant leur super classe

public class UnkownObjectException extends UnknownTypeException 

public class UnkownFormeException extends UnknownTypeException

Dans le code de chargerDessinables le bloc catch(DessinFileReaderException | NumberFormatException ex) n'a pas besoin d'être modifié, il attrapera ce nouveau type d'exception qui est un sous type de DessinFileReaderException:

noLigne++;
try {

     ...
} catch (DessinFileReaderException  | NumberFormatException ex) {
     System.out.println("Ligne ignorée");
    System.out.println(ex.getMessage());
}
// passage à la ligne suivante
ligne = reader.readLine(); 

Le code pour lire la description d'un animateur de type CapAnimator

    /**
     * construit une animateur de type CapAnimator à partir des paramètres
     * définis dans un fichier de description textuel
     *
     * @param noLigne        numéro de la ligne dans le fichier
     * @param paramsAnimator les paramètre décrivant l'animateur
     * @param d              le dessin dans lequel l'animateur travaille
     * 
     * @return l'animateur
     * @throws NombreArgumentsIncorrect si le nomnre de paramètres de
     *                                  description ne correspond pas au nombre
     *                                  attendu.
     */
    private static IAnimateur lireCapAnimator(int noLigne, String[] paramsAnimator, Dessin d)
            throws NombreArgumentsIncorrect {
        if (paramsAnimator.length != 2) {
            throw new NombreArgumentsIncorrect(noLigne, "CAP", paramsAnimator.length, 2);
        }
        return new AnimateurCap(d, Integer.parseInt(paramsAnimator[0]), Double.parseDouble(paramsAnimator[1]));
    }

Le code de la méthode lireAnimator

    /**
     * @param noLigne        numéro de la ligne dans le fichier
     * @param typeAnimator   le type de l'animateur
     * @param paramsAnimator le tableau des paramètres de l'animateur
     *
     * @return la référence d'un objet animateur correspondant à la description
     *
     * @throws UnknownAnimatorException si le type d'animateur n'est pas
     *                                  reconnu.
     *
     * @throws NombreArgumentsIncorrect si le nombre de paramètres de
     *                                  description n'est pas le nombre attendu.
     */
    private static IAnimateur lireAnimator(int noLigne, String typeAnimator, String[] paramsAnimator, Dessin d)
            throws UnknownAnimatorException, NombreArgumentsIncorrect {
        switch (typeAnimator.toUpperCase()) {
            case "CAP":
                return lireCapAnimator(noLigne, paramsAnimator, d);
            case "REB":
                return lireRebondAnimator(noLigne, paramsAnimator, d);
            case "CIRC":
                return lireAnimateurCercle(noLigne, paramsAnimator);
            default:
                throw new UnknownAnimatorException(noLigne, typeAnimator);
        }
    }

et il ne faudra pas oublier de modifier dans la méthode chargerDessinables le case correspondant aux animateurs pour ajuster les paramètres de l'appel à la méthode lireAnimator

case "A":
    System.out.println("Animateur");
    animator = lireAnimator(noLigne, tokens[1], Arrays.copyOfRange(tokens, 2, tokens.length), dessin);
    break;


Exercice 4.2: Rajoutez le support pour les Chenilles et puis ensuite, si vous avez le temps, enrichissez la description des formes pour prendre en compte la couleur et l'épaisseur du trait.

Essayez de trouvez une solution par vous-même. Cependant, si vous n'avez pas d'idées sur la manière de procéder et si vous souhaitez avoir quelques indications, cliquez sur ce bouton:

il n'y a aucune difficulté particulière, le seul point est de définir la description de la chenille dans le fichier texte. On peut par exemple choisir le format suivant :

format de description d'une chenille
Figure 8: format de description d'une chenille

Le code pour créer une chenille est alors:

/**
     * construit une chenille à partir des paramètres définis dans un fichier de
     * description textuel
     * 
     * @param noLigne      numéro de la ligne dans le fichier
     * @param typeChenille le type de chenille à créer
     * @param params       les paramètres décrivant la chenille
     * @param animator     l'animateur de la chenille, si l'animateur est null la
     *                     chenille aura un AnimateurCap par défaut.
     * @param d            le dessin ou se trouve la chenille
     *
     * @return la chenille
     *
     * @throws UnknowChenilleException  si le type de chenille n'est pas reconnu
     *
     * @throws NombreArgumentsIncorrect si le nombre de paramètres de
     *                                  description ne correspond pas au nombre
     *                                  attendu.
     *
     * @throws IOException              si la l'image pour une chenille image n'a pu
     *                                  ête lue.
     *
     */
    private static Chenille lireChenille(int noLigne, String typeChenille, String[] params, IAnimateur animator,
            Dessin d)
            throws NombreArgumentsIncorrect, UnknownChenilleException, IOException {
        switch (typeChenille.toUpperCase()) {
            case "COUL":
                return lireChenilleCouleur(noLigne, params, animator, d);
            case "IMG":
                return lireChenilleImage(noLigne, params, animator, d);
            default:
                throw new UnknownChenilleException(noLigne, typeChenille);
        }
    }

    /**
     * construit une chenille couleur à partir des paramètres définis dans un
     * fichier de
     * description textuel
     * 
     * @param noLigne  numéro de la ligne dans le fichier
     * @param params   les paramètres décrivant la chenille
     * @param animator l'animateur de la chenille, si l'animateur est null la
     *                 chenille aura un AnimateurCap par défaut.
     * @param d        le dessin ou se trouve la chenille
     * 
     * @return la chenille
     * @throws NombreArgumentsIncorrect si le nomnre de paramètres de
     *                                  description ne correspond pas au nombre
     *                                  attendu.
     */
    private static Chenille lireChenilleCouleur(int noLigne, String[] params, IAnimateur animator, Dessin d)
            throws NombreArgumentsIncorrect, UnknownChenilleException, IOException {
        if (params.length != 5) {
            throw new NombreArgumentsIncorrect(noLigne, "CHENILLE COULEUR", params.length, 5);
        }

        int r = Integer.parseInt(params[0]); // rayon
        int nbA = Integer.parseInt(params[1]); // nombre d'anneaux
        Color coul = new Color(Integer.parseInt(params[2]), Integer.parseInt(params[3]),
                Integer.parseInt(params[4]));
        if (animator == null) {
            return new ChenilleCouleur(d, r, nbA, coul);
        } else {
            return new ChenilleCouleur(d, r, nbA, coul, animator);
        }
    }

    /**
     * construit une chenille image à partir des paramètres définis dans un
     * fichier de description textuel
     * 
     * @param noLigne  numéro de la ligne dans le fichier
     * @param params   les paramètres décrivant la chenille
     * @param animator l'animateur de la chenille, si l'animateur est null la
     *                 chenille aura un AnimateurCap par défaut.
     * @param d        le dessin ou se trouve la chenille
     * 
     * @return la chenille
     * @throws NombreArgumentsIncorrect si le nombre de paramètres de description ne
     *                                  correspond pas au nombre attendu.
     */
    private static Chenille lireChenilleImage(int noLigne, String[] params, IAnimateur animator, Dessin d)
            throws NombreArgumentsIncorrect, UnknownChenilleException, IOException {

        if (params.length != 3) {
            throw new NombreArgumentsIncorrect(noLigne, "CHENILLE IMAGE", params.length, 2);
        }
        
        int rayon = Integer.parseInt(params[0]); // rayon des anneaux
        int nbA = Integer.parseInt(params[1]); // nombre d'anneaux
        if (animator == null) {
            return new ChenilleImage(d, nbA, rayon, ImageIO.read(new File(params[2])));
        } else {
            return new ChenilleImage(d, nbA, rayon, ImageIO.read(new File(params[2])), animator);
        }
    }

L'application est maintenant terminée, son exécution avec le fichier de description data/animation.txt ci dessous:

# un pentagone rouge fixe
F POLYR 100 100 40 5 255 0 0
# un octogone vert animé d'un mouvement avec rebonds
A REB 5 -5
F POLYR 200 190 40 8 0 255 0
# un octogone animé d'un mouvement circulaire
A CIRC 100 190 100 0 5
F POLYR 200 190 35 8 0 255 255
# un etoile jaune animée se déplaçant aléatoirement
  A CAP 120 7
F ETOILE 230 290 35 255 255 0
#un disque magenta animé se déplaçant aléatoirement
A CAP 190 4 
F DISQUE 410 300 20 255 0 255
# un cercle bleu
F CERCLE 300 270 30 0 0 255
# une chenille noire à 10 anneaux de rayon 10
C COUL 10 10 0 0 0
#une chenille verte à 20 anneaux de rayon 8
C COUL 8 20 0 255 0
#une chenille rouge à 14 anneaux de rayon 7 qui tourne en rond
A CIRC 120 240 240  0 -5
C COUL 7 14 255 0 0
#une chenille image (DarthVador) 14 anneaux de rayon 20 qui tourne en rond
A CIRC 100 400 300  0 5
C IMG 20 14 images/darthVador.png

doit produire l'affichage suivant:

affichage produit par chargement du fichier animation.txt
Figure 9: affichage produit par le chargement du fichier data/animation.txt