:: Enseignements :: ESIPE :: E4INFO :: 2025-2026 :: Java Inside ::
[LOGO]

Examen de Java Inside - 2025 - session 1


Exercice 1 - Property Mapper

Le but de ce TP est d'implanter une classe PropertyMapper capable de voir des propriétés de configuration (la classe java.util.Properties) de façon structurée et typée.

En Java, il existe une classe java.util.Properties qui permet de lire et écrire des fichiers de configuration ".properties".
Voici un exemple de fichier de configuration :
      server.port=8080
      server.host=localhost
    

La classe java.util.Properties est une table de hachage qui associe à une clé (une chaine de caractères) une valeur (aussi une chaine de caractère).
Pour lire ou écrire les couples clé/valeur, on utilise les méthodes
  • getProperty(key) pour lire une valeur d'une clé (ou renvoie null)
  • setProperty(key, value) pour écrire la valeur d'une clé

Cette API considère que les valeurs sont des chaines de caractères donc pour obtenir la valeur server.port comme un entier, le développeur qui doit appeler la méthode Integer.parseInt à la main.
On se propose de fournir une API PropertyMapper qui ajoute une couche de typage qui va grouper les propriétés en fonction d'un préfixe (par exemple, "server") et effectuer les conversions de type primitifs vers les chaines de caractères (et inversement).
Voici quelques exemples d'utilisation:
  • Lire des propriétés de même préfixe dans un objet Java Bean.
    La méthode PropertyMapper.create() créé une instance de PropertyMapper à partir d'un texte d'un fichier de configuration.
    La méthode extract(prefix, beanType) remplie un objet Java bean avec les propriétés du fichier de configuration commençant par un même prefixe.
    public class AppBean {
      private String name;
      private String version;
    
      public AppBean() {
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public void setVersion(String version) {
        this.version = version;
      }
    
      public String getName() {
        return name;
      }
    
      public String getVersion() {
        return version;
      }
    }
    ...
    
      var propertyText = """
          app.name=TestApp
          app.version=1.0
          """;
      var mapper = PropertyMapper.create(propertyText);
    
      var bean = mapper.extract("app", AppBean.class);
    
      IO.println(bean.getName());     // TestApp
      IO.println(bean.getVersion());  // 1.0
          

  • Créer un proxy sur des propriétés de configuration ayant un même préfixe
    Contrairement à un objet Java bean où lorsque l'on appelle un setter, seuls les champs de l'objet sont modifié, avec un proxy, appeler un setter change la valeur de l'objet java.util.Properties car le proxy agit comme une vue.
    public interface App {
      String getName();
      String getVersion();
      void setVersion(String version);
    }
    ...
      var propertyText = """
          app.name=TestApp
          app.version=1.0
          """;
      var mapper = PropertyMapper.create(propertyText);
    
      App proxy = mapper.proxy("app", App.class);
      proxy.setVersion("1.1");
    
      IO.println(proxy.getName());    // TestApp
      IO.println(proxy.getVersion()); // 1.1
          

Dans la suite du TP, nous allons implanter la classe PropertyMapper au fur et à mesure, voici une ébauche
import java.util.Properties;
import java.io.IOError;
import java.io.IOException;
import java.io.StringReader;

public final class PropertyMapper {
  private final Properties properties;

  private PropertyMapper(Properties properties) {
    this.properties = properties;
    super();
  }

  public static PropertyMapper create(Properties properties) {
    Objects.requireNonNull(properties);
    return new PropertyMapper(properties);  // no defensive copy
  }

  public static PropertyMapper create(String propertyText) {
    Objects.requireNonNull(propertyText);
    var properties = new Properties();
    try(var reader = new StringReader(propertyText)) {
      properties.load(reader);
    } catch (IOException e) {
      throw new IOError(e);
    }
    return create(properties);
  }

  // TODO
    

La javadoc 25 est https://igm.univ-mlv.fr/~juge/javadoc-25/.
Les trucs et astuces utilisés pour l'implantation COMPANION.pdf
La classe Utils qui gère les exceptions correctement : Utils.java
Les tests unitaires correspondant à l'examen se trouvent dans la classe PropertyMapperTest.java
Note : comme on utilise les tests unitaires JUnit sans Maven, dans la configuration de votre projet, il faut ajouter la librairie JUnit 5, soit à partir du fichier PropertyMapperTest.java, en cliquant sur l'annotation @Test et en sélectionnant le quickfix "Fixup project ...", soit en sélectionnant les "Properties" du projet (avec le bouton droit de la souris sur le projet) puis en ajoutant la librairie JUnit 5 (jupiter) au ClassPath.

  1. On cherche dans un premier temps à implanter la méthode extract(prefix, beanType). qui renvoie un objet Java Bean de la classe prise en paramètre.
    L'algorithme est le suivant, le constructeur sans paramètre du beanType est appelé puis pour chaque property descriptor, la propriété correspondante est cherchée dans l'objet java.util.Properties (avec le préfixe), si la propriété possède une valeur associé alors le setter est appelé avec la valeur de la propriété.
    Pour l'instant, on ne va implanter le support que des propriétés dont les valeurs sont des chaines de caractères.
    Écrire la méthode extract(prefix, beanType).
    Vérifier que les tests unitaires marqués "Q1" passent.

  2. On souhaite que extract fonctionne aussi si le préfixe est "".
    Modifier si nécessaire le code de la méthode extract.
    Vérifier que les tests unitaires marqués "Q2" passent.

  3. On souhaite maintenant écrire la méthode proxy(prefix, beanType).
    L'algorithme est un peu différent. Lors de l'appel à la méthode proxy, le proxy est créé indépendamment du fait que les propriétés sous-jacentes existe ou pas. En effet, on peut créer les propriétés en appelant les setter de l'objet.
    Lorsqu'un getter est appelé, la propriété correspondante (en prenant en compte le préfixe) est recherchée et sa valeur est retournée (null peut être renvoyé si la propriété n'existe pas).
    Lorsqu'un setter est appelé, la propriété correspondante est écrit dans l'objet java.util.Properties.
    Ecrire le code de la méthode proxy(prefix, beanType) sachant que, pour l'instant, on veut uniquement que les getter marche.
    Vérifier que les tests unitaires marqués "Q3" passent.

  4. Modifier votre code pour que les setter du proxy fonctionnent aussi.
    Vérifier que les tests unitaires marqués "Q4" passent.

  5. En fait, on veut maintenant une implantation efficace des getter et setter. Pour cela, on va utiliser un dictionnaire qui associe à l'objet java.lang.reflect.Method, une fonction qui execute le code du getter ou du setter.
    Le code de l'InvocationHandler devrait donc être
    var op = propertyMap.get(method);
    if (op != null) {
      return op.apply(args == null ? null : args[0]);
    }
    throw new UnsupportedOperationException("Method not supported: " + method);
        

    Faites les changements qui s'imposent dans votre code. Les tests unitaires devraient continuer de passer.
    Note: si vous n'y arrivez pas, vous pouvez passer à la question suivante.

  6. On souhaite que si getter renvoie une interface, alors le getter renvoie un nouvel objet proxy qui permet de voir les propriétés qui commence par le même sous préfixe.
    Par exemple, on souhaite que le code suivant fonctionne
    public interface Address {
      String getStreet();
    }
    public interface Person {
      String getName();
      Address getAddress();  // returns an interface
    }
    ...
      var propertyText = """
          user.name=Alice
          user.address.street=32 canyon road
          """;
      var mapper = PropertyMapper.create(propertyText);
      var person = mapper.proxy("user",  Person.class);
      var address = person.getAddress();  // returns a new proxy
    
      IO.println(person.getName());     // Alice
      IO.println(address.getStreet());  // 32 canyon road
        

    Modifier votre code pour que cela fonctionne.
    Vérifier que les tests unitaires marqués "Q6" passent.
    Note: en termes de performance, vous ne devriez créer l'objet proxy correspondant à une adresse que si nécessaire.

  7. On souhaite ajouter une méthode freeze() qui interdit toutes modifications ultérieures des propriétés par les setter des proxies (même si ceux-ci ont été créés avant).
    Écrire la méthode freeze.
    Vérifier que les tests unitaires marqués "Q7" passent.

  8. Enfin, on veut que les Java Bean et les proxies fonctionnent avec des types primitifs.
    Modifier votre implantation en conséquence.
    Vérifier que les tests unitaires marqués "Q8" passent.
    Note: pour le type char, la chaine de caractères correspondante devra avoir un seul caractère !