lundi 23 avril 2018

L'accès à la propriété pour tous

Toute application d'une certaine envergure expose un certain nombre des variétés de son exécution sous forme de paramètres, chacun composé d'un nom associé à une valeur. Il est commun que ces paramètres soient regroupés dans un fichier, dit "de configuration" que l'application lira au démarrage. On peut y trouver de tout : la couleur du fond d'écran, l'URL de connexion à une base de donnée (et son user/password), la taille du cache, le nom d'une classe concrète à utiliser pour rendre un service donné, l'âge du capitaine, etc.

Pour une application Java, ce fichier de configuration sera le moins souvent un fichier XML, et le plus souvent un fichier plat, les fameux fichiers .properties. Pour faciliter son traitement, Java offre la classe Properties éponyme avec les moyens de lire (ou de sauvegarder après modification) ces propriétés.

Oui mais voilà, ces propriétés sont accessibles par une clé qui n'est qu'une simple String, et disponibles sous la forme d'une valeur qui, également, n'est qu'une simple String. Ne pourrait-on pas faire mieux, tout en restant simple ?

Si, bien sûr...

Une propriété bien peu pratique d'accès

Un exemple



Voici un exemple typique de fichier de propriété, le fichier "conf.properties" :
# Properties definining the GUI
gui.window.width = 500
gui.window.height = 300
gui.colors.background = #FFFFFF
gui.colors.foreground = #000080

# Properties definining datasources
service.url = https://some-services.example.org/someService
service.uuid = 5da66c77-7062-4b30-97fc-e747eb64570a

db.driver = com.fakesql.jdbc.Driver
db.url = jdbc:fakesql://localhost:3306/localdb?autoReconnect=true
db.account.login = theLogin
db.account.password = thePassword

Et bien moi, ce que je voudrais, c'est pouvoir charger ce fichier dans un objet qui reflète son contenu : les membres de cet objet seraient les noms des propriétés, et les valeurs seraient des objets typés. Par exemple, si je chargeais ce fichier dans une instance "conf" comme ceci :
// unmarshall the property file to an org.example.Conf object
Conf conf = Conf.unmarshall(new FileInputStream("conf.properties"));

...je voudrais ensuite pouvoir y accéder comme cela :
// look, we have an int :
int width = Math.min(conf.gui.window.width, 400);
// we can also use typed data in place :
Dimension size = new Dimension(width, conf.gui.window.height);

// look, we have an URI :
URLConnection extService = conf.service.url.toURL().openConnection();

// we can also supply an adapter for other types
// such as java.awt.Color
Color darkBg = conf.gui.colors.background.darker(); // darker() is a method of java.awt.Color

// you can handle groups of fields at once
Connection dbConnection = getConnection(conf.db);
// in fact, the class org.example.Conf wraps the class org.example.Conf.Db and others

Ci-dessus, une fois l'objet conf obtenu, l'accès à la propriété dans le code Java utilise le même chemin que le nom de la propriété dans le fichier, tel que conf.gui.window.height, et son type est celui attendu, à savoir une valeur numérique.

Génération de la classe Conf



Comme je considère que le fichier de configuration ci-dessus se suffit (presque) à lui-même, un petit générateur de notre classe cible Conf devrait être facile à réaliser. Je l'ai donc fait.

Voyons comment l'utiliser.

Bien qu'il soit possible de se passer de Maven, le plus simple est d'utiliser le générateur prévu à cet effet. Dans votre projet, ajoutez la dépendance suivante :
<dependency>
    <groupId>ml.alternet</groupId>
    <artifactId>alternet-tools</artifactId>
    <version>1.0</version>
</dependency>

...qui vous permettra de charger le fichier de configuration, et le plugin suivant :
<plugin>
    <groupId>ml.alternet</groupId>
    <artifactId>prop-bind-maven-plugin</artifactId>
    <version>1.0</version>
    <executions>
        <execution>
            <?m2e execute?>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

...qui vous permettra de générer la classe cible.

A cette fin, copiez votre fichier de propriétés "conf.properties" dans "${basedir}/src/main/properties/conf.template.properties", ce qui aura pour effet de produire le code source de la classe Conf dans “${project.build.directory}/generated-sources/prop-bind” lors de la compilation (pour être précis, lors de la phase generate-sources de Maven).

Si vous utilisez un IDE, faites en sorte que ce dernier répertoire fasse partie des sources à compiler, de sorte qu'il vous soit possible d'utiliser immédiatement cette classe dans votre projet.

Modifications du template



Pour que cela fonctionne correctement, il faut tout de même opérer quelques modifications : à minima, les mots de passe ne devraient pas être conservés dans le template, mais remplacés par des "*****".

Il faut aussi préciser le nom qualifié de la classe à générer ; cela se fait en ajoutant une propriété spéciale au début du fichier :
. = #org.example.Conf

"." est une clé spéciale du fichier de configuration utilisée pour piloter la génération du code avec quelques directives.

Pour le reste du fichier, le générateur va créer les propriétés avec le type approprié en fonction de ce qui est mis dans le template. Typiquement, si une valeur du template contient un entier, le type généré sera int, si une valeur contient true ou false, le type généré sera boolean. Et si plusieurs valeurs sont séparées par des virgules, vous obtiendrez java.util.List<java.lang.Boolean>

Dans notre template, nous avons gui.window.width = 500, donc nous aurons un short.
Si vous voulez forcer le type, vous pouvez toujours modifier le template comme ceci : gui.window.width = $int 500.

Avez-vous remarqué l'usage de $, contrairement au # vu précédemment ? En règle générale, $ sert à marquer les types existants (y compris les primitives comme int) et # sert à marquer les types à générer.

Par exemple, nous pouvons associer une propriété à une classe donnée :
gui.colors.background = $java.awt.Color #FFFFFF

Dans ce template, #FFFFFF n'est plus qu'une valeur indicative, un exemple de valeur permise dans le fichier de configuration.

Pour autant, pour que la propriété ait la valeur appropriée, il faut indiquer comment la produire à partir d'une chaîne. Cela est pris en charge en ajoutant un "adapteur", qui n'est autre qu'une fonction qui prend en paramètre java.lang.String pour créer l'instance du type attendu. En pur Java, nous écririons ceci à l'aide de l'utilitaire Adapter fourni :
Adapter.map(Color.class, Color::decode)
// when a Color type is expected,
//parse the text with Color.decode()

Comme cette déclaration doit être faite dans le fichier de template, il existe une directive appropriée pour cela :
. = @Adapter.map(Color.class, Color::decode)


Pour les autres de types de données, le générateur reconnaît et utilisera naturellement les types URI, classes, fichiers, les dates et heures, et également les types énumérés en séparant les valeurs par un "|" :
service.status = PENDING | ACTIVE | INACTIVE | DELETED


Et si le comportement par défaut ne vous plaît pas, vous pouvez toujours mettre la valeur entre double quote pour forcer le type chaîne de caractère.

Le fichier template final



Le fichier conf.template.properties final ressemble à ceci :
# Target class name
. = #org.example.Conf
# Required adapters
. = @Adapter.map(Color.class, Color::decode)
. = @Adapter.map(UUID.class, UUID::fromString)

# Properties definining the GUI
gui.window.width = $int 500
gui.window.height = 300
gui.colors.background = $java.awt.Color #FFFFFF
gui.colors.foreground = $java.awt.Color #000080

# Properties definining datasources
service.url = https://some-services.example.org/someService
service.uuid = $java.util.UUID 5da66c77-7062-4b30-97fc-e747eb64570a

db.driver = java:java.sql.Driver
db.url = "jdbc:fakesql://localhost:3306/localdb?autoReconnect=true"
db.account.login = theLogin
db.account.password = *****

Autres fonctionnalités





Il existe quelques autres fonctionnalités dans le template :
  • génération de type séparé (par défaut, tous les types générés sont imbriquées) :
    gui.window. = #org.example.Gui
  • support des types portant à la fois valeur et sous types :
    gui.window = Sample application
    gui.window.width = $int 500
  • support des valeurs dont la clé n'est pas connue à l'avance :
    map.*.geo = $org.example.Geo 48.864716, 2.349014
  • import explicite en cas de besoin :
    . = !java.awt.Color
    (dans notre example, cet import est inutile car le nom qualifié de la classe est mentionné par ailleurs dans le template)
  • génération de noms de propriétés compatibles avec Java
...et d'autres dans le fichier de propriété :
  • fusion de fichiers de configuration multiples :
    db = file:datasource/db.properties
  • interpolation de variables :
    gui.window.width = ${gui.window.height + 200}
  • initialisation avec des valeurs par défaut

Voilà : ce générateur / chargeur de classe à partir d'un fichier de propriétés est un petit outil commode et facile à prendre en main :





Semantic Mismatch
ERR-19 : IllegalArgumentException
Unable to set CORDE[] to ARC
A single CORDE was expected
-- In "Avoir plusieurs cordes à son arc"
-- See log for details



Aucun commentaire:

Enregistrer un commentaire