:: Enseignements :: ESIPE :: E4INFO :: 2021-2022 :: Java Inside ::
[LOGO]

Examen de Java Inside - 2021 - session 1


Exercice 1 - DictionaryDecoder

Le but de cette examen est d'écrire une classe DictionaryDecoder qui permet de décoder des dictionnaires, c'est à dire des objets définis par des chaîne de caractères organisées en couples clé/valeur séparés par des virgules.
Par exemple, un dictionnaire représentant un Point peut être défini par la chaîne de caractères suivante
     x: 12, y: 67
   
Le décodeur permet de décoder des dictionnaires
  1. soit sous forme d'un objet Java utilisant la représentation Java beans
  2. soit sous la forme d'un record.

Un DictionaryDecoder possède deux méthodes
  • register(class) qui permet d'enregistrer une classe qui sera utilisée pour décoder un dictionnaire
  • decode(dictionary) qui demande de décoder un dictionnaire et renvoyée une instance initialisée avec les valeurs du dictionnaire.

Par exemple, avec une classe Point définie comme un Java bean
    class Point {
      private int x;
      private int y;

      public Point() {
      }

      public int getX() {
        return x;
      }
      public void setX(int x) {
        this.x = x;
      }
      public int getY() {
        return y;
      }
      public void setY(int y) {
        this.y = y;
      }
    }
   
La classe DictionaryDecoder permet de décoder un dictionnaire avec le code suivant.
 var dictionary = Dictionary.parseLine("x: 12, y: 67");

 var decoder = new DictionaryDecoder();
 decoder.register(Point.class);
 var point = decoder.decode(dictionary);
 System.out.println(point); // Point(x: 12, y: 67)
   

L'implantation fonctionne de cette façon : la méthode register() va trouver l'ensemble des propriétés (PropertyDescriptor) associées à la classe prise en paramètre et enregistre que cette classe est associée à l'ensemble des noms des propriétés.
Dans notre exemple, Point.class a les propriétés x et y donc Point est associé à l'ensemble (x, y).
Lorsque l'on appelle la méthode decode, celle-ci calcule l'ensemble des clés du dictionnaire pris en paramètre, trouve la classe correspondant à l'ensemble, appelle le constructeur sans paramètre de la classe trouvée afin d'en créer une instance et enfin appelle les setters pour initialiser l'instance avec les valeurs.
Dans notre exemple, le constructeur Point() est appelé, puis les méthodes setX et setY sont appelées avec respectivement les valeurs 12 et 67.

Pour me faciliter la correction, vous écrirez le code dans le fichier DictionaryDecoder sachant que le code pour transformer une chaîne de caractères en un dictionnaire (la méthode Dictionary.parseLine) est déjà écrite.
public class DictionaryDecoder {
  private static final Pattern PATTERN = Pattern.compile("([^ \t:]+)[ \t]*:[ \t]*([^ \t,]+)");
  private static final Pattern NUMBER = Pattern.compile("[0-9]*\\.[0-9]+");
  private static final Pattern INTEGER = Pattern.compile("[0-9]+");

  public record Data(String key, Object value) {
    public Data {
      Objects.requireNonNull(key);
      Objects.requireNonNull(value);
    }
  }

  private static Object decodeAsObject(String text) {
    return switch (text) {
      case "true", "false" -> Boolean.parseBoolean(text);
      case String s -> NUMBER.matcher(text).matches() ? Double.parseDouble(text) :
          (INTEGER.matcher(text).matches() ? Integer.parseInt(text) : text);
    };
  }

  public record Dictionary(List<Data> dataList) {
    public static Dictionary parseLine(String line) {
      return new Dictionary(PATTERN.matcher(line)
          .results()
          .map(result -> new Data(result.group(1), decodeAsObject(result.group(2))))
          .toList());
    }
  }

  // --- don't change the code above that line

  // TODO write your code here
}
   

La javadoc 17 est https://igm.univ-mlv.fr/~juge/javadoc-17/api/index.html.
Les trucs et astuces utiliser pour l'implantation COMPANION.pdf
La classe utilitaire gérant les exceptions Utils.java
Les tests unitaires correspondant à l'examen se trouve dans la classe DictionaryDecoderTest.java

  1. On va procéder par étapes. Dans un premier temps, nous allons implanter une méthode findKeys(dictionnary) (non publique) qui prend en paramètre un dictionnaire et renvoie l'ensemble des clés de celui-ci.
    Dans un second temps, nous allons écrire une méthode register(type) qui prend en paramètre une classe et enregistre pour l'ensemble des noms des propriétés de cette classe vue comme un Java Bean, la classe elle même. Par exemple, pour la classe Point ci-dessus, nous allons associer à l'ensemble ("x", "y") la classe Point.class.
    De plus, nous allons implanter une méthode canDecode(keys) (non publique) qui prend un ensemble de clés en paramètre et renvoie vrai si la classe correspondante existe ou faux sinon.
    Par exemple, si on register la classe Point, canDecode(Set.of("x")) va renvoyer faux, canDecode(Set.of("x", "y")) va renvoyer vrai, canDecode(Set.of("x", "y", "z")) va renvoyer faux.
    Implanter les méthodes findKeys, register et canDecode.
    Vérifier que les test marqués "Q1" passent.
    Note : en Java, un ensemble est représenté par l'interface Set.
    Note 2 : pour associer une classe à un ensemble de clés, il faut utiliser une ...
  2. On souhaite modifier la méthode register pour valider que le bean est correctement défini, c'est à dire qu'il doit avoir un getter et un setter pour chaque property. De plus, il ne doit pas être possible de faire deux appels à register avec deux classes ayant le même ensemble de noms de propriétés. Par exemple, Point correspond à l'ensemble de propriétés (x, y), il ne doit pas être possible d'ajouter une autre classe ayant les propriétés (x, y).
    Modifier le code de la méthode register.
    Vérifier que les tests marqués "Q2" passent.
  3. On souhaite ajouter une méthode decode(dictionnary) qui prend un dictionnaire en paramètre, calcule l'ensemble des clés, va chercher la classe correspondante, créer un objet de cette classe et initialise l'objet en appelant les setters correspondant à chaque clé avec la valeur du dictionnaire.
    Écrire la méthode decode.
    Vérifier que les tests marqués "Q3" passent.
    Note : si on veut être efficace, il ne faut pas uniquement enregistrer la classe correspondant à l'ensemble des clés mais aussi comment construire l'objet et comment initialiser chaque champ.
  4. Déclarer une annotation @Property définie à l'intérieur de la classe DictionaryDecoder ayant une valeur de type String. Faire en sorte qu'il soit possible de déclarer cette annotation sur les getters et quelle soit accessible à l'exécution par l'API de réflexion.
    L'idée est que si l'annotation @Property est définie sur un getter, alors au lieu d'utiliser le nom de la property, il faut utiliser la valeur de l'annotation pour décoder le dictionnaire.
    Modifier le code de la classe DictionaryDecoder pour prendre en compte l'annotation Property.
    Vérifier que les tests marqués "Q4" passent.
  5. On souhaite maintenant pouvoir gérer les records en plus des classes Java bean.
    Pour un record, l'algorithme de décodage est le suivant : dans register, on récupère les RecordComponents ainsi que le constructeur canonique. On calcul l'ensemble des noms des composants des records et on stocke dans une structure de données l'association entre le nom d'un composant et l'index de celui-ci dans l'ordre dans lequel les composants sont déclarés. Lorsque l'on décode, on créé un tableau qui va correspondre aux arguments du constructeur canonique ; pour chaque couple clé/valeur (Data), à partir de la clé, on trouve l'index dans le tableau et on stocke la valeur à cet index. Enfin, on appelle le constructeur canonique avec le tableau des valeurs.
    Dans un premier temps, réfléchissez aux différents endroits où il faut faire des modifications pour prendre en compte les records. Puis essayer de trouver un façon simple de choisir pour ces endroits quelle implantation doit être utilisée.
    Modifier le code de la classe DictionaryDecoder pour prendre en compte les records.
    Vérifier que les tests marqués "Q5" passent.