Présentation

La classe ApplicationConfig a pour but de gérer les options et actions disponibles au sein d'une application. Elle gère aussi bien :

  • la lecture de fichier de configuration ;
  • le parsing de la ligne de commande ;
  • l'execution des actions ;
  • la sauvegarde de la configuration.

Lecture/écriture

Lecture des fichiers de configuration

La lecture des fichiers de configuration est effectuée lors de l'appel à la méthode parse(String...) en utilisant la valeur de getConfigFileName() pour trouver les fichiers à lire.

La sauvegarde

La sauvegarde des options se fait via une des trois méthodes disponibles :

  • save : sauvegarde dans un fichier spécifique ;
  • saveForSystem : sauvegarde les données dans /etc ;
  • saveForUser : sauvegarde les données dans $HOME.

Seules les options qui ont été modifiées par l'application (par la méthode setOption()) seront sauvegardées. Les variables d'environnement, les arguments de la ligne de commandes(etc...) ne seront pas sauvegardés.

Configuration multi instance

Il est possible d'associer un nom de contexte à une configuration via la méthode setAppName("azerty"). Ainsi, les fichiers seront cherchés dans le dossier défini par l'option azerty.config.path si elle existe (sinon, dans le dossier par défaut) et le nom du fichier cherché défini par l'option azerty.config.file.

Cette option est utilisée par exemple pour installer plusieurs instances d'application dans un serveur web et que chaque instance aille chercher ses fichiers de configuration à son propre endroit.

Fonctionnalités

Les options de configuration

L'ordre de prise en compte des options est le suivant :

  • Option renseignée par programmation ;
  • Ligne de commande ;
  • Propriétés système (System.getProperties()) ;
  • Propriétés d'environnement (System.getenv()) ;
  • Fichier du dossier courant ( ./ + nom du fichier) ;
  • Fichier de configuration globale ( /etc/ + nom du fichier) ;
  • Fichier dans le classpath ( / + nom du fichier) ;
  • Valeur par défaut (renseignée à l'init).

Cela signifie par exemple que si une option a une valeur par défaut et qu'elle est renseignée dans le fichier /etc et sur la ligne de commande, c'est la valeur présente sur la ligne de commande qui sera prise en compte.

Les actions

Les actions ne peuvent être renseignées que sur la ligne de commande. Exemple :

--org.nuiton.test.Test#doLogin user password true

Une action est donc définie par le chemin complet de la méthode qui traitera l'action. Si la méthode est statique, elle sera appelée directement. Dans le cas contraire, la classe contenant la méthode sera instanciée à partir d'un constructeur prenant en paramètre seulement la configuration, ou, s'il n'est pas disponible, le constructeur par défaut. La méthode sera ensuite appelée sur cette instance. Les diverses instances sont conservées pour effectuer plusieurs actions.

Les arguments de la méthode utilisés lors de l'appel sont convertis dans le bon type. Si la méthode a des arguments de taille variante (...) tous les arguments jusqu'à la prochaine option ou à la fin de la ligne seront utilisés.

Si vous avez des paramètres optionnels, le seul moyen est d'utiliser des arguments variants.

Par exemple, la ligne de commande précédente appellera la méthode :

public class Test {
    public doLogin(String login, String password, boolean dryRun) {
       [...]
    }
}

Les actions ne sont pas exécutées, mais seulement parsées. Cela signifie qu'elles seront exécutées seulement lorsque l'application appellera la méthode doAction(int). Par défaut, toutes les actions sont de niveau 0 et sont exécutées dans leur ordre d'apparition sur la ligne de commande. Il est possible de différencier les différentes actions en utilisant l'annotation @Step

doAction(0);
... do something ...
doAction(1);

Dans cet exemple, les actions 0 et 1 ne sont pas effectuées au même moment. C'est très utile par exemple pour exécuter certaines actions avant le démarrage de l'UI par exemple, et d'autres après...

Les arguments non parsés

La configuration 'consomme' les arguments de la ligne de commande qu'elle a réussie à traiter. Pour récupérer les autres arguments propres à l'application il est possible de les obtenir grace à la méthode getUnparsed(). Si l'on souhaite forcer la fin du parsing de la ligne de commande il est possible de mettre --.

Par exemple, la ligne suivante :

monApplication "mon arg" --option k1 v1 -- --option k2 v2 -- autre

Renverra la liste suivante via getUnparsed() :

"mon arg", "--option", "k2", "v2", "--", "autre"

Les alias

Il est possible d'utiliser des alias pour définir les options et les actions. Ces alias doivent être renseignés par la méthode addAlias(String, String:

addAlias("-v", "--option", "verbose", "true");
addAlias("-o", "--option", "outputfile");
addAlias("-i", "--mon.package.MaClass#MaMethode", "import");

Dans le premier exemple on simplifie une option de flags l'option -v n'attend donc plus d'argument. Dans le second exemple on simplifie une option qui attend encore un argument de type File. Enfin dans le troisième exemple on simplifie la syntaxe d'une action et on force le premier argument de l'action à être "import".

Lors du parsing de la ligne de commande, tous les alias sont remplacés par leur correspondance. Il est donc possible d'utiliser ce mécanisme pour autre chose :

addAlias("cl", "Code Lutin");

Conversion de type

Pour convertir les types des options et arguments de méthodes, commons-beanutils est utilisé.

Les types actuellement supporté sont :

  • java.lang.String ;
  • java.io.File ;
  • java.net.URL ;
  • java.lang.Class ;
  • java.sql.Date ;
  • java.sql.Time ;
  • java.sql.Timestamp ;
  • Les tableaux d'un type primitif ou @link String. Chaque élément doit être séparé par une virgule.

Pour utiliser d'autres types, il suffit de les enregistrer dans beanutils via la méthode ConvertUtils.register(Converter, Class)

Les substitutions de variable

La configuration de variable supporte la substitution par d'autres variables via la syntaxe ${xxx}xxx est une autre variable de la configuration.

Par exemple (fichier de configuration) :

application.name = Mon Appli
application.version = 1.2.3
application.info = ${application.name} ${application.version} (${java.version})

L'appel de l'option application.info via la méthode getOption() retournera une chaîne de la forme :

Mon Appli 1.2.3 (1.6.0_18)

À noter que les substitutions ne sont remplacées qu'a leur lecture, la sauvegarde de l'option application.info se fera sans remplacement.

Mise en oeuvre

Définition

Voici l'ensemble des tâches à effectuer pour définir une configuration d'application :

  • Creation d'une sous classe d'ApplicationConfig ;
  • Ajout des options par défaut ;
  • Création des classes et méthodes d'actions ;
  • Déclaration des alias des options et actions.

Exemple :

public class MyConfig extends ApplicationConfig {

    public static final int AFTER_LOGIN = 1;

    public MyConfig () {
        // options par défaut
        setDefaultOption("user", "anonymous");
        setDefaultOption("password", "");
        // ajout des alias
        addAlias("-u", "--user");
        addAlias("-p", "--password");
        addActionAlias("--login", MyConfig.class.getName + "#" + "doLogin");
    }

    public void setUser(String user) {
        setOption("user", user);
    }

    public void setUser(String user) {
        setOption("user", user);
    }

    public void doLogin(String user, String password) {
        [...]
    }

    @Step(AFTER_LOGIN)
    public void doSomething() {
        [...]
    }
}

Usage

La configuration doit principalement être initialisée grâce à la méthode parse(String[]) avant d'être utilisée.

public static void main(String[] args) {
    MyConfig config = new MyConfig();
    config.setConfigFileName("myconfig.conf");
    config.parse(args);

    System.out.println("Connecting with " : + config.getOption("user"));
    config.doAction(0);
    System.out.println("Connected, do something...");
    config.doAction(MyConfig.AFTER_LOGIN);
}

Utilisation du ApplicationConfigProvider

Ce contrat ajouté en version 2.4.8 permet de spécifier qu'une librairie ou une application offre des options.

Il suffit d'implanter ce contrat et de le rendre disponible via le mécanisme de ServiceLoader.

Exemple

public class PollenApplicationConfigProvider implements ApplicationConfigProvider {

    @Override
    public String getName() {
        return "pollen";
    }

    @Override
    public String getDescription(Locale locale) {
        return l_(locale, "pollen.application.config");
    }

    @Override
    public ConfigOptionDef[] getOptions() {
        return PollenConfigurationOption.values();
    }

    @Override
    public ActionOptionDef[] getActions() {
        return new ActionOptionDef[0];
    }
}

Puis ajouter le fichier META-INF/services/org.nuiton.config.ApplicationConfigProvider dans les resources du projet :

org.chorem.pollen.PollenApplicationConfigProvider

Cela permet ensuite, par exemple, de générer un rapport contenant toutes les options disponibles dans l'application.

Lire/Écrire des fichiers au format ini

Depuis la version 3.1, il est possible de lire/écrire les configuration au format ini.

Pour ce faire, il faut indiquer à ApplicationConfig via son objet d'initialisation qu'on veut utiliser ce format :

ApplicationConfig applicationConfig = new ApplicationConfig(ApplicationConfigInit.forAllScopes().useIniFormat());

et ajouter la dépendance suivante dans votre projet :

    <dependency>
      <groupId>org.nuiton</groupId>
      <artifactId>nuiton-config-io-ini</artifactId>
      <version>3.5-SNAPSHOT</version>
    </dependency>