annotated tag v3.0-alpha-1 created (now 27d5d8e)
This is an automated email from the git hooks/post-receive script. New change to annotated tag v3.0-alpha-1 in repository nuiton-config. See https://gitlab.nuiton.org/nuiton/nuiton-config.git at 27d5d8e (tag) tagging 4eed3dadba29616cc1d0f1c2ebdd356d59e5ca09 (commit) tagged by Maven Release Manager on Tue Jul 23 15:34:35 2013 +0000 - Log ----------------------------------------------------------------- [maven-release-plugin] copy for tag nuiton-config-3.0-alpha-1 ----------------------------------------------------------------------- This annotated tag includes the following new commits: new 4eed3da [maven-release-plugin] copy for tag nuiton-config-3.0-alpha-1 The 1 revisions listed above as "new" are entirely new to this repository and will be described in separate emails. The revisions listed as "adds" were already present in the repository and have only been added to this reference. Detailed log of new commits: commit 4eed3dadba29616cc1d0f1c2ebdd356d59e5ca09 Author: Maven Release Manager <???> Date: Tue Jul 23 15:34:35 2013 +0000 [maven-release-plugin] copy for tag nuiton-config-3.0-alpha-1 -- To stop receiving notification emails like this one, please contact nuiton.org SCM administrator <admin+scm@nuiton.org>.
This is an automated email from the git hooks/post-receive script. New commit to annotated tag v3.0-alpha-1 in repository nuiton-config. See https://gitlab.nuiton.org/nuiton/nuiton-config.git commit 4eed3dadba29616cc1d0f1c2ebdd356d59e5ca09 Author: Maven Release Manager <???> Date: Tue Jul 23 15:34:35 2013 +0000 [maven-release-plugin] copy for tag nuiton-config-3.0-alpha-1 --- LICENSE.txt | 166 ++ README.txt | 0 pom.xml | 202 ++ src/license/THIRD-PARTY.properties | 19 + .../java/org/nuiton/config/ApplicationConfig.java | 2596 ++++++++++++++++++++ .../org/nuiton/config/ApplicationConfigHelper.java | 243 ++ .../nuiton/config/ApplicationConfigProvider.java | 79 + .../config/ApplicationConfigSaveException.java | 39 + .../nuiton/config/ArgumentsParserException.java | 46 + .../java/org/nuiton/config/ConfigActionDef.java | 78 + .../java/org/nuiton/config/ConfigOptionDef.java | 163 ++ src/site/apt/index.apt | 395 +++ src/site/apt/versions.apt | 34 + src/site/site_fr.xml | 109 + .../org/nuiton/config/ApplicationConfigTest.java | 660 +++++ src/test/resources/log4j.properties | 33 + 16 files changed, 4862 insertions(+) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b1a93ee --- /dev/null +++ b/pom.xml @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.nuiton</groupId> + <artifactId>mavenpom4redmineAndCentral</artifactId> + <version>3.4.13</version> + </parent> + + <artifactId>nuiton-config</artifactId> + <version>3.0-alpha-1</version> + + <name>Nuiton Config</name> + <description>Simple Application config API</description> + <url>http://maven-site.nuiton.org/nuiton-config</url> + <inceptionYear>2013</inceptionYear> + + <developers> + + <developer> + <name>Benjamin Poussin</name> + <id>bleny</id> + <email>poussin at codelutin dot com</email> + <organization>CodeLutin</organization> + <organizationUrl>http://www.codelutin.com/</organizationUrl> + <timezone>Europe/Paris</timezone> + <roles> + <role>developer</role> + </roles> + </developer> + + <developer> + <name>Tony Chemit</name> + <id>tchemit</id> + <email>chemit at codelutin dot com</email> + <organization>CodeLutin</organization> + <organizationUrl>http://www.codelutin.com/</organizationUrl> + <timezone>Europe/Paris</timezone> + <roles> + <role>developer</role> + </roles> + </developer> + + </developers> + + <scm> + <connection> + scm:svn:http://svn.nuiton.org/svn/nuiton-config/tags/nuiton-config-3.0-alpha-1 + </connection> + <developerConnection> + scm:svn:http://svn.nuiton.org/svn/nuiton-config/tags/nuiton-config-3.0-alpha-1 + </developerConnection> + <url>http://nuiton.org/projects/nuiton-config/repository/show/tags/nuiton-config-3.0-alpha-1</url> + </scm> + <distributionManagement> + <site> + <id>${platform}</id> + <url>${our.site.repository}/${projectId}</url> + </site> + </distributionManagement> + + <properties> + + <projectId>nuiton-config</projectId> + + <!-- Documentation is in apt format --> + <siteSourcesType>apt</siteSourcesType> + + </properties> + + <dependencies> + + <dependency> + <groupId>org.nuiton</groupId> + <artifactId>nuiton-utils</artifactId> + <version>2.6.12</version> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + + <dependency> + <groupId>commons-logging</groupId> + <artifactId>commons-logging</artifactId> + </dependency> + + <dependency> + <groupId>commons-collections</groupId> + <artifactId>commons-collections</artifactId> + </dependency> + + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + <scope>provided</scope> + </dependency> + + </dependencies> + + <profiles> + + <profile> + <id>reporting</id> + <activation> + <property> + <name>performRelease</name> + <value>true</value> + </property> + </activation> + + <reporting> + <plugins> + + <plugin> + <artifactId>maven-project-info-reports-plugin</artifactId> + <version>${projectInfoReportsPluginVersion}</version> + <reportSets> + <reportSet> + <reports> + <report>project-team</report> + <report>mailing-list</report> + <report>cim</report> + <report>issue-tracking</report> + <report>license</report> + <report>scm</report> + <report>dependency-info</report> + <report>dependencies</report> + <report>dependency-convergence</report> + <report>plugin-management</report> + <report>plugins</report> + <report>dependency-management</report> + <report>summary</report> + </reports> + </reportSet> + </reportSets> + </plugin> + + </plugins> + </reporting> + + </profile> + + <!-- create assemblies at release time --> + <profile> + <id>assembly-profile</id> + <activation> + <property> + <name>performRelease</name> + <value>true</value> + </property> + </activation> + <build> + <defaultGoal>package</defaultGoal> + <plugins> + + <!-- launch in a release the assembly automaticly --> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <id>create-assemblies</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + <configuration> + <attach>false</attach> + <descriptorRefs> + <descriptorRef>deps</descriptorRef> + <descriptorRef>full</descriptorRef> + </descriptorRefs> + </configuration> + </plugin> + + </plugins> + + </build> + </profile> + + </profiles> +</project> diff --git a/src/license/THIRD-PARTY.properties b/src/license/THIRD-PARTY.properties new file mode 100644 index 0000000..f8d4994 --- /dev/null +++ b/src/license/THIRD-PARTY.properties @@ -0,0 +1,19 @@ +# Generated by org.codehaus.mojo.license.AddThirdPartyMojo +#------------------------------------------------------------------------------- +# Already used licenses in project : +# - BSD License +# - COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 +# - Common Public License Version 1.0 +# - Indiana University Extreme! Lab Software License, vesion 1.1.1 +# - Lesser General Public License (LGPL) v 3.0 +# - Lesser General Public License (LPGL) +# - Lesser General Public License (LPGL) v 2.1 +# - MIT License +# - New BSD License +# - The Apache Software License, Version 2.0 +#------------------------------------------------------------------------------- +# Please fill the missing licenses for dependencies : +# +# +#Sat Jul 20 15:32:25 CEST 2013 +commons-primitives--commons-primitives--1.0=The Apache Software License, Version 2.0 diff --git a/src/main/java/org/nuiton/config/ApplicationConfig.java b/src/main/java/org/nuiton/config/ApplicationConfig.java new file mode 100644 index 0000000..b5bfd7f --- /dev/null +++ b/src/main/java/org/nuiton/config/ApplicationConfig.java @@ -0,0 +1,2596 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin, Tony Chemit + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +import org.apache.commons.beanutils.ConstructorUtils; +import org.apache.commons.collections.EnumerationUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.nuiton.util.ObjectUtil; +import org.nuiton.util.RecursiveProperties; +import org.nuiton.util.SortedProperties; +import org.nuiton.util.Version; +import org.nuiton.util.converter.ConverterUtil; + +import javax.swing.KeyStroke; +import java.awt.Color; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URL; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Application configuration. + * <p/> + * <h3>A finir...</h3> + * <ul> + * <li>Ajout d'annotations sur les methodes + * pour preciser plus de chose pour les options (pattern, min/max, alias, + * description, ...) + * <li>Trouver un moyen de document les options et actions pour automatiquement + * generer l'aide en ligne. Pour eviter de devoir maintenir une methode + * dans lequel est ecrit l'aide en plus des options. + * <li>Prise en compte du flag {@link #useOnlyAliases} + * <li>Vu qu'en java on ne peut pas pointer une methode mais seulement une classe + * il y a un bout des actions qui sont des chaines (nom de la methode). Il faudrait + * faire un plugin maven qui check que l'action existe bien durant la compilation. + * Il est simple de le faire a l'execution mais c trop tard :( + * <li>Ajouter de la documentation pour {@link #getOptionAsList(String)} + * </ul> + * <p/> + * <h3>Bonnes pratiques</h3> + * <p/> + * Vous devez créer une factory pour créer les instances d'{@link ApplicationConfig} qui contiendra par exemple une méthode : + * <p/> + * <pre> + * + * public static ApplicationConfig getConfig( + * Properties props, String configFilename, String ... args) { + * + * ApplicationConfig conf = new ApplicationConfig( + * MyAppConfigOption.class, MyAppConfigAction.class, + * props, configFilename); + * + * try { + * conf.parse(args); + * } catch (ArgumentsParserException eee) { + * if (log.isErrorEnabled()) { + * log.error("Can't load app configuration", eee); + * } + * } + * return conf; + * } + * + * </pre> + * <p/> + * <ul> + * <li>MyAppConfigOption doit étendre {@link ConfigOptionDef} et décrir la configuration de l'application. + * <li>MyAppConfigAction doit étendre {@link ConfigActionDef} et décrir la liste des options + * et de leur alias disponible pour l'application. + * </ul> + * <p/> + * <h3>Lecture des fichiers de configuration</h3> + * <p/> + * La lecture des fichiers de configuration se fait durant l'appel de la methode + * {@link #parse(String...)} en utilisant la valeur de qui doit être définit + * dans les options avec pour clef {@link ApplicationConfig#CONFIG_FILE_NAME} pour + * trouver les fichiers (voir Les options de configuration pour l'ordre de + * chargement des fichiers) + * <p/> + * <h3>La sauvegarde</h3> + * La sauvegarde des options se fait via une des trois methodes disponibles : + * <ul> + * <li> {@link #save(File, boolean, String...)} sauve les données dans le fichier demandé + * <li> {@link #saveForSystem(String...)} sauvegarde les donnees dans /etc + * <li> {@link #saveForUser(String...)} sauvegarde les donnees dans $HOME + * </ul> + * <p/> + * Lors de l'utilisation de la methode {@link #saveForSystem(String...)} ou + * {@link #saveForUser(String...)} seules les options lues dans un fichier ou modifiées par + * programmation ({@link #setOption(String, String)} seront sauvegardées. Par exemple les + * options passees sur la ligne de commande ne seront pas sauvees. + * <p/> + * <h3>Les options de configuration</h3> + * <p/> + * Cette classe permet de lire les fichiers de configuration, utiliser les + * variable d'environnement et de parser la ligne de commande. L'ordre de prise + * en compte des informations trouvées est le suivant (le premier le plus + * important) : + * <ul> + * <li>options ajoutees par programmation: {@link #setOption(String, String)}</li> + * <li>ligne de commande</li> + * <li>variable d'environnement de la JVM: java -Dkey=value</li> + * <li>variable d'environnement; export key=value</li> + * <li>fichier de configuration du repertoire courant: $user.dir/filename</li> + * <li>fichier de configuration du repertoire home de l'utilisateur: $user.home/.filename</li> + * <li>fichier de configuration du repertoire /etc: /etc/filename</li> + * <li>fichier de configuration trouve dans le classpath: $CLASSPATH/filename</li> + * <li>options ajoutees par programmation: {@link #defaults}.put(key, value)</li> + * </ul> + * <p/> + * <p/> + * Les options sur la ligne de commande sont de la forme: + * <pre> + * --option key value + * --monOption key value1 value2 + * </pre> + * <p/> + * <ul> + * <li>--option key value: est la syntaxe par defaut + * <li>--monOption key value1 value2: est la syntaxe si vous avez ajouter une + * methode setMonOption(key, value1, value2) sur votre classe de configuration + * qui herite de {@link ApplicationConfig}. Dans ce cas vous pouvez mettre les + * arguments que vous souhaitez du moment qu'ils soient convertibles de la + * representation String vers le type que vous avez mis. + * </ul> + * <p/> + * <h3>Les actions</h3> + * <p/> + * Les actions ne peuvent etre que sur la ligne de commande. Elles sont de la + * forme: + * <pre> + * --le.package.LaClass#laMethode arg1 arg2 arg3 ... argN + * </pre> + * <p/> + * Une action est donc defini par le chemin complet vers la methode qui traitera + * l'action. Cette methode peut-etre une methode static ou non. Si la methode + * n'est pas static lors de l'instanciation de l'objet on essaie de passer en + * parametre du constructeur la classe de configuration utilisee pour permettre + * a l'action d'avoir a sa disposition les options de configuration. Si aucun + * constructeur avec comme seul parametre une classe heritant de + * {@link ApplicationConfig} n'existe alors le constructeur par defaut est + * utilise (il doit etre accessible). Toutes methodes d'actions faisant + * parties d'un meme objet utiliseront la meme instance de cette objet lors + * de leur execution. + * <p/> + * Si la methode utilise les arguments variants alors tous les arguments + * jusqu'au prochain -- ou la fin de la ligne de commande sont utilises. Sinon + * Le nombre exact d'argument necessaire a la methode sont utilises. + * <p/> + * Les arguments sont automatiquement converti dans le bon type reclame par la + * methode. + * <p/> + * Si l'on veut des arguments optionnels le seul moyen actuellement est + * d'utiliser une methode avec des arguments variants + * <p/> + * Les actions ne sont pas execute mais seulement parsees. Pour les executer + * il faut utiliser la méthode {@link #doAction(int)} qui prend en argument un numero + * de 'step' ou {@link #doAllAction()} qui fait les actions dans l'ordre de leur step. + * Par defaut toutes les actions sont de niveau 0 et sont executee + * dans l'ordre d'apparition sur la ligne de commande. Si l'on souhaite + * distinguer les actions il est possible d'utiliser l'annotation + * {@link ApplicationConfig.Action.Step} sur la methode qui fera l'action en + * precisant une autre valeur que 0. + * <pre> + * doAction(0); + * ... do something ... + * doAction(1); + * </pre> + * dans cette exemple on fait un traitement entre l'execution des actions + * de niveau 0 et les actions de niveau 1. + * <p/> + * <h3>Les arguments non parsées</h3> + * Tout ce qui n'est pas option ou action est considere comme non parse et peut + * etre recupere par la methode {@link #getUnparsed}. Si l'on souhaite forcer + * la fin du parsing de la ligne de commande il est possible de mettre --. + * Par exemple: + * <pre> + * monProg "mon arg" --option k1 v1 -- --option k2 v2 -- autre + * </pre> + * Dans cet exemple seule la premiere option sera considere comme une option. + * On retrouvera dans {@code unparsed}: "mon arg", "--option", "k2", "v2", "--", + * "autre" + * <p/> + * <h3>Les alias</h3> + * On voit qu'aussi bien pour les actions que pour les options, le nom de la + * methode doit etre utilise. Pour eviter ceci il est possible de definir + * des alias ce qui permet de creer des options courtes par exemple. Pour cela, + * on utilise la methode {@link #addAlias(String, String...)}. + * <pre> + * addAlias("-v", "--option", "verbose", "true"); + * addAlias("-o", "--option", "outputfile"); + * addAlias("-i", "--mon.package.MaClass#MaMethode", "import"); + * </pre> + * En faite avant le parsing de la ligne de commande tous les alias trouves sont + * automatiquement remplacer par leur correspondance. Il est donc possible + * d'utiliser ce mecanisme pour autre chose par exemple: + * <pre> + * addAlias("cl", "Code Lutin"); + * addAlias("bp", "Benjamin POUSSIN); + * </pre> + * 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 argment de type File. Enfin dans le troisieme exemple + * on simplifie la syntaxe d'une action et on force le premier argument de + * l'action a etre "import". + * <p/> + * <h3>Conversion de type</h3> + * Pour la conversion de type nous utilisons common-beans. Les types supportes + * sont: + * <ul> + * <li> les primitif (byte, short, int, long, float, double, char, boolean) + * <li> {@link String} + * <li> {@link File} + * <li> {@link URL} + * <li> {@link Class} + * <li> Sql{@link Date} + * <li> Sql{@link Time} + * <li> Sql{@link Timestamp} + * <li> les tableaux d'un type primitif ou {@link String}. Chaque element doit + * etre separe par une virgule. + * </ul> + * <p/> + * Pour suporter d'autre type, il vous suffit d'enregistrer de nouveau + * converter dans commons-beans. + * <p/> + * <h3>Les substitutions de variable</h3> + * {@link ApplicationConfig} supporte les substition de variables de la forme + * <tt>${xxx}</tt> où {@code xxx} est une autre variable de la configuration. + * <p/> + * Exemple (dans un fichier de configuration): + * <pre> + * firstname = John + * lastname = Doe + * fullname = ${firstname} ${lastname} + * </pre> + * <tt>getOption("fullname")</tt> retournera <tt>"John Doe"</tt>. + * + * @author Benjamin Poussin <poussin@codelutin.com> + * @author tchemit <chemit@codelutin.com> + * @since 0.30 + */ +public class ApplicationConfig { + + /** Logger. */ + private static final Log log = LogFactory.getLog(ApplicationConfig.class); + + public static final String LIST_SEPARATOR = ","; + + /** Configuration file key option. */ + public static final String CONFIG_FILE_NAME = "config.file"; + + /** Configuration encoding key option. */ + public static final String CONFIG_ENCODING = "config.encoding"; + + /** Permet d'associer un nom de contexte pour prefixer les options {@link #CONFIG_PATH} et {@link #CONFIG_FILE_NAME}. */ + public static final String APP_NAME = "app.name"; + + /** + * Property name of {@link #adjusting} internal state. + * + * @since 1.3 + */ + public static final String ADJUSTING_PROPERTY = "adjusting"; + + /** + * Configuration directory where config path in located. + * <p/> + * Use default system configuration if nothing is defined: + * <ul> + * <li>Linux : /etc/xxx.properties + * <li>Windows : C:\\Windows\\System32\\xxx.properties + * <li>Mac OS : /etc/ + * </ul> + */ + public static final String CONFIG_PATH = "config.path"; + + /** System os name. (windows, linux, max os x) */ + protected String osName; + + /** TODO */ + protected boolean useOnlyAliases; + + /** vrai si on est en train de parser les options de la ligne de commande. */ + protected boolean inParseOptionPhase; + + /** TODO */ + protected Properties defaults = new Properties(); + + /** TODO */ + protected Properties classpath = new Properties(defaults); + + /** TODO */ + protected Properties etcfile = new Properties(classpath); + + /** TODO */ + protected Properties homefile = new Properties(etcfile); + + /** TODO */ + protected Properties curfile = new Properties(homefile); + + /** TODO */ + protected Properties env = new Properties(curfile) { + + private static final long serialVersionUID = 1L; + + /** + * Environnement variables can't contains dot (bash, csh, ...). Dots are + * replaced by underscore (_) to find property if property is not find + * with dot + */ + @Override + public synchronized Object get(Object key) { + Object result = super.get(key); + if (result == null && key instanceof String) { + String skey = (String) key; + skey = skey.replace(".", "_"); + result = super.get(skey); + } + return result; + } + + /** + * override to use get(key) and not super.get(key) as in initial implementation :( + */ + @Override + public String getProperty(String key) { + Object oval = get(key); + String sval = (oval instanceof String) ? (String) oval : null; + //TODO TC-2013-02-26 Make sure what we want :using Applicationconfig#defaults or super.defaults ? + return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval; + + } + + }; + + /** TODO */ + protected Properties jvm = new Properties(env); + + /** TODO */ + protected Properties line = new Properties(jvm); + + /** TODO */ + protected Properties options = new Properties(line); + + /** TODO */ + protected Map<String, CacheItem<?>> cacheOption = new HashMap<String, CacheItem<?>>(); + + /** TODO */ + protected Map<Class<?>, Object> cacheAction = new HashMap<Class<?>, Object>(); + + /** contient apres l'appel de parse, la liste des arguments non utilises */ + protected List<String> unparsed = new ArrayList<String>(); + + /** TODO */ + protected Map<String, List<String>> aliases = new HashMap<String, List<String>>(); + + /** TODO */ + protected Map<Integer, List<Action>> actions = new HashMap<Integer, List<Action>>(); + + /** + * Internal state to manage with masse operations on option and control + * listeners. + * <p/> + * for example, if you want to save options, using javaBeans technology, + * can add a listener to save each time the property is modified. + * <p/> + * Says now you have an algorithm to set new values in configuration using + * setters but you do NOt want to save each time, add in your saving action + * a test to detect if model is adjusting. + * + * @see #saveUserAction + * @since 1.3 + */ + private boolean adjusting; + + /** suport of config modification. */ + protected PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + /** permet de conserver des objets associe avec ce ApplicationConfig */ + protected Map<String, Object> context = new HashMap<String, Object>(); + + /** + * Init ApplicationConfig with current simple class name as config file. + * <p/> + * Also init converters. + * + * @see ConverterUtil#initConverters() + */ + public ApplicationConfig() { + this(null, null); + } + + /** + * Create configuration for a particular configuration filename + * + * @param configFilename name of config to use + */ + public ApplicationConfig(String configFilename) { + this(null, configFilename); + } + + /** + * Init ApplicationConfig with current simple class name as config file + * and use Properties parameter as defaults + * <p/> + * Also init converters. + * + * @param defaults properties + * @see ConverterUtil#initConverters() + */ + public ApplicationConfig(Properties defaults) { + this(defaults, null); + } + + /** + * All in one, this constructor allow to pass all necessary argument to + * initialise ApplicationConfig and parse command line + * + * @param defaults properties that override default value of optionClass, can be null + * @param configFilename override default config filename, can be null + * @since 2.4.8 + */ + public ApplicationConfig(Properties defaults, String configFilename) { + init(defaults, configFilename); + } + + /** + * On separt l'init du corps du constructeur, car les sous classes ne doivent + * pas l'executer. + * + * @param defaults properties that override default value of optionClass, can be null + * @param configFilename override default config filename, can be null + * @since 2.4.9 + */ + protected void init(Properties defaults, String configFilename) { + if (defaults != null) { + // iterate with Properties method and not with Hashtable method to + // prevent missed value with chained Properties object + for (String key : defaults.stringPropertyNames()) { + setDefaultOption(key, defaults.getProperty(key)); + } + } + + setEncoding("UTF-8"); + + if (configFilename == null) { + + setConfigFileName(getClass().getSimpleName()); + } else { + setDefaultOption(CONFIG_FILE_NAME, configFilename); + } + + // init extra-converters + ConverterUtil.initConverters(); + + // get system os name + osName = System.getProperty("os.name"); + + } + + /** + * All in one, this constructor allow to pass all necessary argument to + * initialise ApplicationConfig and parse command line + * + * @param optionClass class that describe option, can be null + * @param actionClass class that describe action, can be null + * @param defaults properties that override default value of optionClass, can be null + * @param configFilename override default config filename, can be null + * @deprecated since 2.4.8, prefer use {@link #ApplicationConfig(Properties, String)} + */ + @Deprecated + public <O extends ConfigOptionDef, A extends ConfigActionDef> ApplicationConfig( + Class<O> optionClass, Class<A> actionClass, + Properties defaults, String configFilename) { + this(defaults, configFilename); + if (optionClass != null) { + loadDefaultOptions(optionClass); + } + if (actionClass != null) { + loadActions(actionClass); + } + } + + /** + * Get user home directory (system property {@code user.home}). + * + * @return user home directory + */ + public static String getUserHome() { + String result = System.getProperty("user.home"); + return result; + } + + /** + * Get user name (system property {@code user.name}). + * + * @return user name + */ + public String getUsername() { + String result = getOption("user.name"); + return result; + } + + /** + * Get os name (system property {@code os.name}). + * + * @return os name + * @since 2.6.6 + */ + public String getOsName() { + String result = getOption("os.name"); + return result; + } + + /** + * Get os arch (system property {@code os.arch}). + * + * @return os arch + * @since 2.6.6 + */ + public String getOsArch() { + String result = getOption("os.arch"); + return result; + } + + /** + * Load default options of enum pass in param (enum must extend {@link ConfigOptionDef}) + * + * @param optionClass to load + * @param <O> type of enum extend {@link ConfigOptionDef} + * @deprecated since 2.4.8, prefer use now {@link #loadDefaultOptions(ConfigOptionDef[])} + */ + @Deprecated + public <O extends ConfigOptionDef> void loadDefaultOptions(Class<O> optionClass) { + + loadDefaultOptions(optionClass.getEnumConstants()); + } + + /** + * Load default given options. + * + * @param options options to load + * @param <O> type of enum extend {@link ConfigOptionDef} + * @since 2.4.8 + */ + public <O extends ConfigOptionDef> void loadDefaultOptions(O[] options) { + + // load default option (included configuration file name : important) + for (ConfigOptionDef o : options) { + if (o.getDefaultValue() != null) { + setDefaultOption(o.getKey(), o.getDefaultValue()); + } + } + } + + /** + * Load actions of enum pass in param (enum must extend {@link ConfigActionDef}) + * + * @param actionClass to load + * @param <A> type of enum extend {@link ConfigActionDef} + * @deprecated since 2.4.8, prefer use now {@link #loadActions(ConfigActionDef[])} + */ + @Deprecated + public <A extends ConfigActionDef> void loadActions(Class<A> actionClass) { + + loadActions(actionClass.getEnumConstants()); + } + + /** + * Load given actions. + * + * @param actions actions to load + * @param <A> type of enum extend {@link ConfigActionDef} + * @since 2.4.8 + */ + public <A extends ConfigActionDef> void loadActions(A[] actions) { + + // load actions + for (A a : actions) { + for (String alias : a.getAliases()) { + addActionAlias(alias, a.getAction()); + } + } + } + + /** + * Used to put default configuration option in config option. Those options + * are used as fallback value. + * + * @param key default property key + * @param value default property value + */ + public void setDefaultOption(String key, String value) { + defaults.setProperty(key, value); + } + + /** + * Save configuration, in specified file. + * + * @param file file where config will be writen + * @param forceAll if true save all config option + * (with defaults, classpath, env, command line) + * @param excludeKeys optional list of keys to exclude from + * @throws IOException if IO pb + */ + public void save(File file, + boolean forceAll, + String... excludeKeys) throws IOException { + + // store sorted in file + Properties prop = new SortedProperties(); + + if (forceAll) { + prop.putAll(defaults); + prop.putAll(classpath); + } + prop.putAll(etcfile); + prop.putAll(homefile); + prop.putAll(curfile); + if (forceAll) { + prop.putAll(jvm); + prop.putAll(env); + prop.putAll(line); + } + prop.putAll(options); + + for (String excludeKey : excludeKeys) { + prop.remove(excludeKey); + } + + // Ano #687 : create parentFile before using it in FileWriter + FileUtils.forceMkdir(file.getParentFile()); + if (log.isDebugEnabled()) { + log.debug("Creation of config directory " + file.getParent()); + } + saveResource(file, prop, "Last saved " + new Date()); + } + + /** + * Save configuration, in system directory (/etc/) using the + * {@link #getConfigFileName}. Default, env and commande line note saved. + * + * @param excludeKeys optional list of keys to exclude from + */ + public void saveForSystem(String... excludeKeys) throws ApplicationConfigSaveException { + File file = getSystemConfigFile(); + if (log.isDebugEnabled()) { + log.debug("will save system configuration in " + file); + } + try { + save(file, false, excludeKeys); + } catch (IOException eee) { + throw new ApplicationConfigSaveException(eee); + } + } + + /** + * Save configuration, in user home directory using the + * {@link #getConfigFileName}. Default, env and commande line note saved + * + * @param excludeKeys optional list of keys to exclude from + */ + public void saveForUser(String... excludeKeys) throws ApplicationConfigSaveException { + File file = getUserConfigFile(); + if (log.isDebugEnabled()) { + log.debug("will save user configuration in " + file); + } + try { + save(file, false, excludeKeys); + } catch (IOException eee) { + throw new ApplicationConfigSaveException(eee); + } + } + + /** + * Clean the user configuration file (The one in user home) and save it + * in user config file. + * <p/> + * All options with an empty value will be removed from this file. + * <p/> + * Moreover, like {@link #saveForUser(String...)} the given + * {@code excludeKeys} will never be saved. + * <p/> + * This method can be useful when migrating some configuration from a + * version to another one with deprecated options (otherwise they will stay + * for ever in the configuration file with an empty value which is not + * acceptable). + * <p/> + * <strong>Important note:</strong> Using this method can have some strange + * side effects, since it could then allow to reuse default configurations + * from other level (default, env, jvm,...). Use with care only! + * + * @param excludeKeys optional list of key to not treat in cleaning process, + * nor save in user user config file. + * @since 2.6.6 + */ + public void cleanUserConfig(String... excludeKeys) throws ApplicationConfigSaveException { + + Set<String> keys = new HashSet<String>(homefile.stringPropertyNames()); + + List<String> toExclude = Arrays.asList(excludeKeys); + + for (String key : keys) { + if (!toExclude.contains(key)) { + String property = homefile.getProperty(key); + if (StringUtils.isBlank(property)) { + if (log.isInfoEnabled()) { + log.info("Remove blank property: " + key); + } + homefile.remove(key); + } + } + } + + // can now save cleaned user config + saveForUser(excludeKeys); + } + + /** + * Obtain the system config file location. + * + * @return the system config file location + */ + public File getSystemConfigFile() { + File file = new File(getConfigPath(), getConfigFileName()); + return file; + } + + /** + * Obtain the user config file location. + * + * @return the user config file location + */ + public File getUserConfigFile() { + return new File(getUserConfigDirectory(), getConfigFileName()); + } + + /** + * Return list of unparsed command line argument + * + * @return list of unparsed arguments + */ + public List<String> getUnparsed() { + return unparsed; + } + + /** + * Add action to list of action to do. + * + * @param action action to add, can be null. + */ + public void addAction(Action action) { + if (action != null) { + Integer step = action.step; + List<Action> list = actions.get(step); + if (list == null) { + list = new LinkedList<Action>(); + actions.put(step, list); + } + list.add(action); + } + } + + /** + * Return ordered action step number. + * example: 0,1,5,6 + * + * @return ordered action step number + * @since 2.4 + */ + public List<Integer> getActionStep() { + List<Integer> result = new ArrayList<Integer>(actions.keySet()); + Collections.sort(result); + return result; + } + + /** + * Do all action in specified order step (first 0). + * + * @throws IllegalAccessException if action invocation failed + * @throws IllegalArgumentException if action invocation failed + * @throws InvocationTargetException if action invocation failed + * @throws InstantiationException if action invocation failed + * @see Action.Step + * @since 2.4 + */ + public void doAllAction() throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException, + InstantiationException { + for (int step : getActionStep()) { + doAction(step); + } + } + + /** + * Do action in specified step. + * + * @param step do action only defined in this step + * @throws IllegalAccessException if action invocation failed + * @throws IllegalArgumentException if action invocation failed + * @throws InvocationTargetException if action invocation failed + * @throws InstantiationException if action invocation failed + * @see Action.Step + */ + public void doAction(int step) throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException, + InstantiationException { + List<Action> list = actions.get(step); + if (list != null) { + for (Action a : list) { + a.doAction(); + } + } + } + + public void setUseOnlyAliases(boolean useOnlyAliases) { + this.useOnlyAliases = useOnlyAliases; + } + + public boolean isUseOnlyAliases() { + return useOnlyAliases; + } + + /** + * Get the encoding used to read/write resources. + * <p/> + * This value is stored as an option using the + * {@link #getEncodingOption()} key. + * + * @return the encoding used to read/write resources. + * @since 2.3 + */ + public String getEncoding() { + return getOption(getEncodingOption()); + } + + /** + * Set the new encoding option. + * + * @param encoding the new value of the option encoding + * @since 2.3 + */ + public void setEncoding(String encoding) { + setDefaultOption(getEncodingOption(), encoding); + } + + /** + * All argument in aliases as key is substitued by target. + * + * @param alias alias string as '-v' + * @param target substitution as '--option verbose true' + */ + public void addAlias(String alias, String... target) { + aliases.put(alias, Arrays.asList(target)); + } + + /** + * Add alias for action. This method put just -- front the actionMethod and + * call {@link #addAlias(String, String...)}. + * + * @param alias the alias to add for the given method action + * @param actionMethod must be fully qualified method path: + * package.Class#method + */ + public void addActionAlias(String alias, String actionMethod) { + addAlias(alias, "--" + actionMethod); + } + + /** + * Set name of file where options are read (in /etc, $HOME, $CURDIR) + * This set used {@link #setDefaultOption(String, String)}. + * + * @param name file name + */ + public void setConfigFileName(String name) { + // put in defaults, this permit user to overwrite it on commande line + setDefaultOption(getConfigFileNameOption(), name); + } + + /** + * Get name of file where options are read (in /etc, $HOME, $CURDIR). + * + * @return name of file + */ + public String getConfigFileName() { + String result = getOption(getConfigFileNameOption()); + return result; + } + + public boolean isAdjusting() { + return adjusting; + } + + public void setAdjusting(boolean adjusting) { + boolean oldvalue = this.adjusting; + this.adjusting = adjusting; + firePropertyChange(ADJUSTING_PROPERTY, oldvalue, adjusting); + } + + protected String getConfigFileNameOption() { + String optionName = CONFIG_FILE_NAME; + if (getOption(APP_NAME) != null) { + optionName = getOption(APP_NAME) + "." + optionName; + } + return optionName; + } + + /** + * Obtains the key used to store the option encoding. + * + * @return the encoding option'key + * @since 2.3 + */ + protected String getEncodingOption() { + String optionName = CONFIG_ENCODING; + if (getOption(APP_NAME) != null) { + optionName = getOption(APP_NAME) + "." + optionName; + } + return optionName; + } + + /** + * Use appName to add a context in config.file and config.path options. + * <p/> + * Ex for an application named 'pollen' : {@code config.file} option becomes + * {@code pollen.config.file} and {@code config.path} becomes + * {@code pollen.config.path} + * + * @param appName to use as application context + * @since 1.2.1 + */ + public void setAppName(String appName) { + setDefaultOption(APP_NAME, appName); + } + + /** + * Get configuration file path to use. + * <p/> + * Use (in order) one of the following definition: + * <ul> + * <li>{@link #CONFIG_PATH} option + * <li>system dependant path + * </ul> + * + * @return path to use with endind {@link File#separator} + * @since 1.2.1 + */ + public String getConfigPath() { + // Concat appName to configPath option to specify context for + // application deployment + String appName = getOption(APP_NAME) != null ? + getOption(APP_NAME) + "." : ""; + + String result = getOption(appName + CONFIG_PATH); + + if (result == null) { + result = getSystemConfigurationPath(); + } + if (log.isDebugEnabled()) { + log.debug("Configuration path used : " + result); + } + return result; + } + + /** + * Get system configuration path. + * <p/> + * Currently supported: + * <ul> + * <li>Windows : C:\Windows\System32 + * <li>Unix : /etc/ + * </ul> + * + * @return the system path + * @since 1.2.1 + */ + protected String getSystemConfigurationPath() { + + String systemPath = null; + + // Windows + if (osName.toLowerCase().contains("windows")) { + + // try 1 : %SystemDirectory% + try { + String systemDirectory = System.getenv("SystemDirectory"); + if (systemDirectory != null && systemDirectory.length() > 0) { + systemPath = systemDirectory; + } + } catch (SecurityException eee) { + if (log.isErrorEnabled()) { + log.error("Can't read env property", eee); + } + } + + // try 2 : %SystemRoot% + if (systemPath != null) { + try { + String systemRoot = System.getenv("SystemRoot"); + if (systemRoot != null && systemRoot.length() > 0) { + systemPath = systemRoot + "\\System32"; + } + } catch (SecurityException eee) { + if (log.isErrorEnabled()) { + log.error("Can't read env property", eee); + } + } + } else { + // default value + systemPath = "C:\\Windows\\System32"; + } + + // %SystemDrive% exists too : C: + } else { + // All others are unix like + // look for in /etc/ + systemPath = File.separator + "etc" + File.separator; + } + if (log.isDebugEnabled()) { + log.debug(systemPath); + } + return systemPath; + } + + /** + * Get user configuration path. + * <p/> + * Currently supported: + * <ul> + * <li>Windows : ${user.home}\\Application Data\\ + * <li>Max os x : ${user.home}/Library/Application Support + * <li>Unix : ${user.home}/.config + * </ul> + * <p/> + * Unix norm is based on freedesktop concept explained here : + * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + * + * @return the user configuration path + * @since 1.2.1 + */ + public String getUserConfigDirectory() { + + String userPath = null; + + String userHome = null; + try { + userHome = getUserHome(); + } catch (SecurityException ignore) { + } + + if (userHome != null) { + // windows + if (osName.toLowerCase().contains("windows")) { + try { + String appDataEV = System.getenv("APPDATA"); + if (appDataEV != null && appDataEV.length() > 0) { + userPath = appDataEV; + } + } catch (SecurityException ignore) { + } + + if (userPath == null || userPath.isEmpty()) { + // ${userHome}\Application Data\ + userPath = userHome + File.separator + "Application Data"; + } + } else if (osName.toLowerCase().contains("mac os x")) { + // ${userHome}/Library/Application Support/${applicationId} + userPath = userHome + File.separator + + "/Library/Application Support"; + } else { + // ${userHome}/.config/ + userPath = userHome + "/.config"; + } + } + + // what if null ? + + return userPath; + } + + /** + * Teste si un option existe ou non. + * + * @param key la clef de l'option à tester + * @return {@code true} si l'option existe, {@code false} sinon. + */ + public boolean hasOption(String key) { + // on est oblige de faire un get, car le containsKey n'est pas recursif + // sur tous les properties + boolean result = options.getProperty(key) != null; + return result; + } + + /** + * Teste si un option existe ou non + * + * @param key la clef de l'option à tester + * @return {@code true} si 'loption existe, {@code false} sinon. + */ + public boolean hasOption(ConfigOptionDef key) { + boolean result = hasOption(key.getKey()); + return result; + } + + /** + * ajoute un objet dans le context, la classe de l'objet est utilise comme cle + * + * @since 2.4.2 + */ + public void putObject(Object o) { + putObject(o.getClass().getName(), o); + } + + /** + * ajoute un objet dans le context, 'name' est utilise comme cle + * + * @since 2.4.2 + */ + public void putObject(String name, Object o) { + context.put(name, o); + } + + /** + * recupere un objet de la class<E>, s'il n'existe pas encore, il est cree + * (il faut donc que class<E> soit instanciable + * <p/> + * E peut prendre en argument du contruteur un objet de type ApplicationConfig + * + * @since 2.4.2 + */ + public <E> E getObject(Class<E> clazz) { + E result = getObject(clazz, clazz.getName()); + return result; + } + + /** + * recupere un objet ayant le nom 'name', s'il n'existe pas encore, il est + * cree en utilisant la class<E>, sinon il est simplement caster vers cette + * classe. + * <p/> + * E peut prendre en argument du contruteur un objet de type ApplicationConfig + * + * @since 2.4.2 + */ + public <E> E getObject(Class<E> clazz, String name) { + E result = clazz.cast(context.get(name)); + if (result == null) { + result = ObjectUtil.newInstance( + clazz, Collections.singleton(this), true); + putObject(name, result); + } + return result; + } + + /** + * retourne une nouvelle instance d'un objet dont on recupere la la class + * dans la configuration via la cle 'key'. Retourne null si la cle n'est pas + * retrouve + * + * @since 2.4.2 + */ + public Object getOptionAsObject(String key) { + Object result = null; + if (hasOption(key)) { + Class<?> clazz = getOptionAsClass(key); + result = ObjectUtil.newInstance( + clazz, Collections.singleton(this), true); + } + return result; + } + + /** + * retourne une nouvelle instance d'un objet dont on recupere la la class + * dans la configuration via la cle 'key' et le cast en E. Retourne null + * si la cle n'est pas retrouve + * <p/> + * E peut prendre en argument du contruteur un objet de type ApplicationConfig + * + * @since 2.4.2 + */ + public <E> E getOptionAsObject(Class<E> clazz, String key) { + E result = clazz.cast(getOptionAsObject(key)); + return result; + } + + /** + * retourne l'objet instancier via la classe recupere dans la configuration + * via la cle 'key'. Une fois instancie, le meme objet est toujours retourne. + * On null si key n'est pas retrouve. + * <p/> + * La classe peut avoir un constructeur prenant un ApplicationConfig + * + * @since 2.4.2 + */ + public Object getOptionAsSingleton(String key) { + Object result = context.get(key); + if (result == null) { + result = getOptionAsObject(key); + putObject(key, result); + } + return result; + } + + /** + * retourne l'objet caster en 'E', instancier via la classe recupere dans la + * configuration via la cle 'key'. Une fois instancie, le meme objet est + * toujours retourne. On null si key n'est pas retrouve + * <p/> + * La classe peut avoir un constructeur prenant un ApplicationConfig + * + * @since 2.4.2 + */ + public <E> E getOptionAsSingleton(Class<E> clazz, String key) { + E result = clazz.cast(getOptionAsSingleton(key)); + return result; + } + + /** + * Set option value. + * + * @param key property key + * @param value property value + */ + public void setOption(String key, String value) { + if (inParseOptionPhase) { + line.setProperty(key, value); + } else { + options.setProperty(key, value); + } + } + + /** + * get option value as string. + * <p/> + * Replace inner ${xxx} value. + * + * @param key the option's key + * @return String representation value + */ + public String getOption(String key) { + String value = options.getProperty(key); + // replace ${xxx} + value = replaceRecursiveOptions(value); + return value; + } + + /** + * Replace included ${xxx} suboptions by their values. + * + * @param option option to replace into + * @return replaced option + * @since 1.1.3 + */ + protected String replaceRecursiveOptions(String option) { + + // TODO do a common code with RecursiveProperties code + // TODO but can't overwrite getProperty() method + + String result = option; + + if (result == null) { + return null; + } + + //Ex : result="My name is ${myName}." + int pos = result.indexOf("${", 0); + //Ex : pos=11 + while (pos != -1) { + int posEnd = result.indexOf("}", pos + 1); + //Ex : posEnd=19 + if (posEnd != -1) { + String value = getOption(result.substring(pos + 2, posEnd)); + // Ex : getProperty("myName"); + if (value != null) { + // Ex : value="Thimel" + result = result.substring(0, pos) + value + + result.substring(posEnd + 1); + // Ex : result="My name is " + "Thimel" + "." + pos = result.indexOf("${", pos + value.length()); + // Ex : pos=-1 + } else { + // Ex : value=null + pos = result.indexOf("${", posEnd + 1); + // Ex : pos=-1 + } + // Ex : pos=-1 + } + } + + return result; + } + + /** + * Returns a sub config that encapsulate this ApplicationConfig. + * + * @param prefix prefix to put automaticaly at beginning of all key + * @return sub config that encapsulate this ApplicationConfig + * @since 2.4.9 + */ + public SubApplicationConfig getSubConfig(String prefix) { + SubApplicationConfig result = new SubApplicationConfig(this, prefix); + return result; + } + + /** + * Permet de recuperer l'ensemble des options commencant par une certaine + * chaine. + * + * @param prefix debut de cle a recuperer + * @return la liste des options filtrées + */ + public Properties getOptionStartsWith(String prefix) { + Properties result = new Properties(); + + for (String key : options.stringPropertyNames()) { + if (key.startsWith(prefix)) { + result.setProperty(key, options.getProperty(key)); + } + } + + return result; + } + + /** + * Get option value from a option definition. + * + * @param key the definition of the option + * @return the value for the given option + */ + public Object getOption(ConfigOptionDef key) { + Object result = getOption(key.getType(), key.getKey()); + return result; + } + + /** + * Get option value as typed value. + * + * @param <T> type of the object wanted as return type + * @param clazz type of object wanted as return type + * @param key the option's key + * @return typed value + */ + public <T> T getOption(Class<T> clazz, String key) { + String value = getOption(key); + T result = null; + if (value != null) { + result = (T) convertOption(clazz, key, value, false); + } + + return result; + } + + /** + * Convert value in instance of clazz or List if asList is true + * <p/> + * example: + * <li> convertOption(Boolean.class, "toto", "true,true", false) => false + * <li> convertOption(Boolean.class, "toto", null, false) => ? ConverterUtil dependant + * <li> convertOption(Boolean.class, "toto", "true,true", true) => [true, true] + * <li> convertOption(Boolean.class, "toto", null, true) => [] + * + * @param clazz result type expected + * @param key option key + * @param value value to convert + * @param asList value is string that represente a list + * @return the converted option in the required type + */ + protected <T> Object convertOption(Class<T> clazz, + String key, + String value, + boolean asList) { + String cacheKey = key + "-" + asList + "-" + clazz.getName(); + + int hash = 0; + if (value != null) { + hash = value.hashCode(); + } + + CacheItem<?> cacheItem = cacheOption.get(cacheKey); + // compute value if value don't exist in cacheOption or + // if it's modified since last computation + if (cacheItem == null || cacheItem.hash != hash) { + if (asList) { + List<T> list = new ArrayList<T>(); + if (value != null) { + String[] values = StringUtils.split(value, LIST_SEPARATOR); + for (String valueString : values) { + // prefer use our convertert method (auto-register more converters) + T v = ConverterUtil.convert(clazz, valueString); + list.add(v); + } + } + cacheItem = new CacheItem<List<T>>(list, hash); + } else { + // prefer use our converter method (auto-register more converters) + T v = ConverterUtil.convert(clazz, value); + cacheItem = new CacheItem<T>(v, hash); + } + // add new item to the cache + cacheOption.put(cacheKey, cacheItem); + } + + // take result in item + Object result = cacheItem.item; + + return result; + } + + /** + * Help to convert value to list of object. If no option for this key + * empty List is returned finaly + * + * @param key the key of searched option + * @return value of option list + */ + public OptionList getOptionAsList(String key) { + String value = getOption(key); + OptionList result = new OptionList(this, key, value); + return result; + } + + /** + * Get option value as {@link File}. + * + * @param key the option's key + * @return value as file + */ + public File getOptionAsFile(String key) { + File result = getOption(File.class, key); + if (result != null) { + result = result.getAbsoluteFile(); + } + return result; + } + + /** + * Get option value as {@link Color}. + * + * @param key the option's key + * @return value as color + */ + public Color getOptionAsColor(String key) { + Color color = getOption(Color.class, key); + return color; + } + + /** + * Get option value as {@link Properties}, this property must be a filepath + * and file must be a properties. + * <p/> + * Returned Properties is {@link RecursiveProperties}. + * + * @param key the option's key + * @return Properties object loaded with value pointed by file + * @throws IOException if exception occured on read file + */ + public Properties getOptionAsProperties(String key) throws IOException { + File file = getOptionAsFile(key); + Properties prop = new RecursiveProperties(); + FileReader reader = new FileReader(file); + try { + prop.load(reader); + } finally { + reader.close(); + } + return prop; + } + + /** + * Get option value as {@link URL}. + * + * @param key the option's key + * @return value as URL + */ + public URL getOptionAsURL(String key) { + URL result = getOption(URL.class, key); + return result; + } + + /** + * Get option value as {@link Class}. + * + * @param key the option's key + * @return value as Class + */ + public Class<?> getOptionAsClass(String key) { + Class<?> result = getOption(Class.class, key); + return result; + } + + /** + * Get option value as {@link Date}. + * + * @param key the option's key + * @return value as Date + */ + public Date getOptionAsDate(String key) { + Date result = getOption(Date.class, key); + return result; + } + + /** + * Get option value as {@link Time}. + * + * @param key the option's key + * @return value as Time + */ + public Time getOptionAsTime(String key) { + Time result = getOption(Time.class, key); + return result; + } + + /** + * Get option value as {@link Timestamp}. + * + * @param key the option's key + * @return value as Timestamp + */ + public Timestamp getOptionAsTimestamp(String key) { + Timestamp result = getOption(Timestamp.class, key); + return result; + } + + /** + * Get option value as {@code int}. + * + * @param key the option's key + * @return value as {@code int} + */ + public int getOptionAsInt(String key) { + Integer result = getOption(Integer.class, key); + if (result == null) { + // primitive value can not be null + result = 0; + } + return result; + } + + /** + * Get option value as {@code long}. + * + * @param key the option's key + * @return value as {@code long} + */ + public long getOptionAsLong(String key) { + Long result = getOption(Long.class, key); + if (result == null) { + // primitive value can not be null + result = 0L; + } + return result; + } + + /** + * Get option value as {@code float}. + * + * @param key the option's key + * @return value as {@code float} + * @since 2.2 + */ + public float getOptionAsFloat(String key) { + Float result = getOption(Float.class, key); + if (result == null) { + // primitive value can not be null + result = 0f; + } + return result; + } + + /** + * Get option value as {@code double}. + * + * @param key the option's key + * @return value as {@code double} + */ + public double getOptionAsDouble(String key) { + Double result = getOption(Double.class, key); + if (result == null) { + // primitive value can not be null + result = 0d; + } + return result; + } + + /** + * Get option value as {@code boolean}. + * + * @param key the option's key + * @return value as {@code boolean}. + */ + public boolean getOptionAsBoolean(String key) { + Boolean result = getOption(Boolean.class, key); + if (result == null) { + // primitive value can not be null + result = false; + } + return result; + } + + /** + * Get option value as {@link Locale}. + * + * @param key the option's key + * @return value as {@link Locale}. + * @since 2.0 + */ + public Locale getOptionAsLocale(String key) { + Locale result = getOption(Locale.class, key); + return result; + } + + /** + * Get option value as {@link Version}. + * + * @param key the option's key + * @return value as {@link Version}. + * @since 2.0 + */ + public Version getOptionAsVersion(String key) { + Version result = getOption(Version.class, key); + return result; + } + + /** + * Get option value as {@link KeyStroke}. + * + * @param key the option's key + * @return value as {@link KeyStroke}. + * @since 2.5.1 + */ + public KeyStroke getOptionAsKeyStroke(String key) { + KeyStroke result = getOption(KeyStroke.class, key); + return result; + } + + + /** + * Get all options from configuration. + * + * @return Properties which contains all options + */ + public Properties getOptions() { + return options; + } + + /** + * Set manually options when you don't want to use parse method to check + * properties file configured by {@link #setConfigFileName(String)}. + * + * @param options Properties which contains all options to set + */ + public void setOptions(Properties options) { + this.options = options; + } + + /** + * Get all options as flat {@link Properties} object (replace inner options). + * + * @return flat Properties object + * @since 1.2.2 + */ + public Properties getFlatOptions() { + return getFlatOptions(true); + } + + /** + * Get all options as flat {@link Properties} object. + * + * @param replaceInner if {@code true} replace imbricated options by theirs values + * @return flat Properties object + * @since 1.2.2 + */ + public Properties getFlatOptions(boolean replaceInner) { + Properties props = new Properties(); + for (String propertyKey : options.stringPropertyNames()) { + String propertyValue; + if (replaceInner) { + // replace ${xxx} option + propertyValue = getOption(propertyKey); + } else { + // do not replace ${xxx} option + propertyValue = options.getProperty(propertyKey); + } + props.setProperty(propertyKey, propertyValue); + } + return props; + } + + /** + * Install the {@link #saveUserAction} on givne {@code properties}. + * + * @param properties properties on which insalls the saveUserAction + */ + protected void installSaveUserAction(String... properties) { + + // pass in adjusting state + setAdjusting(true); + + try { + // ajout de tous les listeners pour sauver la configuration + // lors de la modification des options de la configuration + for (String propertyKey : properties) { + // add a listener + if (log.isDebugEnabled()) { + log.debug("register saveUserAction on property [" + + propertyKey + ']'); + } + addPropertyChangeListener(propertyKey, saveUserAction); + } + } finally { + + // ok back to normal adjusting state + setAdjusting(false); + } + } + + /** + * Get all set method on this object or super object. + * + * @return map with method name without set and in lower case as key, and + * method as value + */ + protected Map<String, Method> getMethods() { + // looking for all methods set on ApplicationConfig + Method[] allMethods = getClass().getMethods(); + Map<String, Method> methods = new HashMap<String, Method>(); + for (Method m : allMethods) { + String methodName = m.getName(); + if (methodName.startsWith("set")) { + methodName = methodName.substring(3).toLowerCase(); + methods.put(methodName, m); + } + } + return methods; + } + + /** + * Take required argument for method in args. Argument used is removed from + * args. If method has varArgs, we take all argument to next '--' + * + * @param m the method to call + * @param args iterator with many argument (equals or more than necessary + * @return the arguments found for the given method + */ + protected String[] getParams(Method m, ListIterator<String> args) { + List<String> result = new ArrayList<String>(); + if (m.isVarArgs()) { + while (args.hasNext()) { + String p = args.next(); + if (p.startsWith("--")) { + // stop search + args.previous(); + break; + } else { + result.add(p); + args.remove(); + } + } + } else { + int paramLenght = m.getParameterTypes().length; + for (int i = 0; i < paramLenght; i++) { + String p = args.next(); + args.remove(); // remove this arg because is used now + result.add(p); + } + } + return result.toArray(new String[result.size()]); + } + + /** + * Create action from string, string must be [package.][class][#][method] + * if package, class or method missing, default is used + * + * @param name name of the action + * @param args arguments for action invocation + * @return the created action + * @throws ArgumentsParserException if parsing failed + * @throws IllegalAccessException if could not create action + * @throws IllegalArgumentException if could not create action + * @throws InstantiationException if could not create action + * @throws InvocationTargetException if could not create action + */ + protected Action createAction(String name, + ListIterator<String> args) + throws ArgumentsParserException, + InstantiationException, + IllegalAccessException, + IllegalArgumentException, + InvocationTargetException { + Action result = null; + + List<Method> methods = ObjectUtil.getMethod(name, true); + + Class clazz = null; + Method method = null; + if (methods.size() > 0) { + if (methods.size() > 1) { + log.warn(String.format( + "More than one method found, used the first: %s", + methods)); + } + method = methods.get(0); + clazz = method.getDeclaringClass(); + } + + if (method != null) { + // remove option from command line, because is used now + args.remove(); + + // creation de l'object sur lequel on fera l'appel + Object o = cacheAction.get(clazz); + if (o == null && !Modifier.isStatic(method.getModifiers())) { + try { + o = ConstructorUtils.invokeConstructor(clazz, this); + } catch (NoSuchMethodException eee) { + log.debug(String.format( + "Use default constructor, because no constructor" + + " with Config parameter on class %s", + clazz.getName())); + o = clazz.newInstance(); + } + cacheAction.put(clazz, o); + } + + // recherche du step de l'action + int step = 0; + Action.Step annotation = method.getAnnotation(Action.Step.class); + if (annotation != null) { + step = annotation.value(); + } + + String[] params = getParams(method, args); + result = new Action(step, o, method, params); + } + + return result; + } + + /** + * Parse option and call set necessary method, read jvm, env variable, + * Load configuration file and prepare Action. + * + * @param args argument as main(String[] args) + * @return ApplicationConfig instance + * @throws ArgumentsParserException if parsing failed + */ + public ApplicationConfig parse(String... args) throws ArgumentsParserException { + if (args == null) { + args = ArrayUtils.EMPTY_STRING_ARRAY; + } + try { + Map<String, Method> methods = getMethods(); + + List<String> arguments = new ArrayList<String>(args.length); + for (String arg : args) { + if (aliases.containsKey(arg)) { + arguments.addAll(aliases.get(arg)); + } else { + arguments.add(arg); + } + } + + // first parse option + inParseOptionPhase = true; + for (ListIterator<String> i = arguments.listIterator(); + i.hasNext(); ) { + String arg = i.next(); + if (arg.equals("--")) { + // stop parsing + break; + } + if (arg.startsWith("--")) { + String optionName = arg.substring(2); + if (methods.containsKey(optionName)) { + i.remove(); // remove this arg because is used now + Method m = methods.get(optionName); + String[] params = getParams(m, i); + if (log.isDebugEnabled()) { + log.debug(String.format( + "Set option '%s' with method '%s %s'", + optionName, m, Arrays.toString(params))); + } + ObjectUtil.call(this, m, params); + } + } + } + inParseOptionPhase = false; + + // + // second load options from all sources + // + // JVM + jvm.putAll(System.getProperties()); + // ENV + env.putAll(System.getenv()); + + // classpath + String filename = getConfigFileName(); + Enumeration<URL> enumInClasspath = ClassLoader.getSystemClassLoader().getResources(filename); + Set<URL> urlsInClasspath = new HashSet<URL>(EnumerationUtils.toList(enumInClasspath)); + + enumInClasspath = ApplicationConfig.class.getClassLoader().getResources(filename); + urlsInClasspath.addAll(EnumerationUtils.toList(enumInClasspath)); + + if (log.isDebugEnabled() && urlsInClasspath.isEmpty()) { + log.debug("No configuration file found in classpath : /" + filename); + } + + for (URL inClasspath : urlsInClasspath) { + if (log.isInfoEnabled()) { + log.info("Loading configuration file (classpath) : " + + inClasspath); + } + loadResource(inClasspath.toURI(), classpath); + } + + // system directory + File etcConfig = getSystemConfigFile(); + if (etcConfig.exists()) { + if (log.isInfoEnabled()) { + log.info("Loading configuration file (etc) : " + etcConfig); + } + loadResource(etcConfig.toURI(), etcfile); + } else { + if (log.isDebugEnabled()) { + log.debug("No configuration file found in system : " + + etcConfig.getAbsolutePath()); + } + } + + // user home directory + File homeConfig = getUserConfigFile(); + if (log.isDebugEnabled()) { + log.debug("User configuration file : " + homeConfig); + } + + if (homeConfig.exists()) { + if (log.isInfoEnabled()) { + log.info("Loading configuration file (home) : " + + homeConfig); + } + loadResource(homeConfig.toURI(), homefile); + } else { + if (log.isDebugEnabled()) { + log.debug("No configuration file found in user home : " + + homeConfig.getAbsolutePath()); + } + } + + // file $CURDIR/filename + File config = new File(filename); + if (config.exists()) { + if (log.isInfoEnabled()) { + log.info("Loading configuration file (curr) : " + config); + } + loadResource(config.toURI(), curfile); + } else { + if (log.isDebugEnabled()) { + log.debug("No configuration file found in current" + + " directory : " + config.getAbsolutePath()); + } + } + + // + // third parse action and do action + // + for (ListIterator<String> i = arguments.listIterator(); + i.hasNext(); ) { + String arg = i.next(); + if (arg.equals("--")) { + // stop parsing + break; + } + if (arg.startsWith("--")) { + String optionName = arg.substring(2); + Action action = createAction(optionName, i); + addAction(action); + } + } + + // + // not used args added to unparsed + // + arguments.remove("--"); + unparsed.addAll(arguments); + + } catch (Exception eee) { + if (log.isErrorEnabled()) { + log.error(eee); + } + throw new ArgumentsParserException("Can't parse argument", eee); + } + return this; + } + + /** + * Move old user configuration file {@code oldHomeConfig} to {@code + * homeConfig}. + * + * @param oldHomeConfig old configuration file path + * @param homeConfig new configuration file path + * @throws IOException if could not move configuration file + */ + protected void migrateUserConfigurationFile(File oldHomeConfig, + File homeConfig) + throws IOException { + if (log.isInfoEnabled()) { + log.info(String.format("Moving old configuration file from %s to %s", + oldHomeConfig.getPath(), homeConfig.getPath())); + } + + boolean b = oldHomeConfig.renameTo(homeConfig); + if (!b) { + // could not move... + String message = String.format( + "could not move old configuration file %s to %s", + oldHomeConfig, + homeConfig + ); + throw new IOException(message); + } + } + + /** + * Load a resources given by his {@code uri} to the given + * {@code properties} argument. + * + * @param uri the uri to load + * @param properties the properties file to load + * @throws IOException if something occurs bad while loading resource + * @see Properties#load(Reader) + * @since 2.3 + */ + protected void loadResource(URI uri, Properties properties) throws IOException { + InputStreamReader reader = + new InputStreamReader(uri.toURL().openStream(), getEncoding()); + try { + properties.load(reader); + } finally { + reader.close(); + } + } + + /** + * Save the given {@code properties} into the given {@code file} with + * the given {@code comment}. + * + * @param file the location where to store the properties + * @param properties the properties file to save + * @param comment the comment to add in the saved file + * @throws IOException if something occurs bad while saving resource + * @see Properties#store(Writer, String) + * @since 2.3 + */ + protected void saveResource(File file, + Properties properties, + String comment) throws IOException { + Writer reader = + new OutputStreamWriter(new FileOutputStream(file), getEncoding()); + try { + properties.store(reader, comment); + } finally { + reader.close(); + } + } + + /** For debugging. */ + public void printConfig() { + System.out.println("-------------------Value-------------------------"); + printConfig(System.out); + System.out.println("-------------------------------------------------"); + } + + /** + * Print out current configuration in specified output. + * + * @param output output to write config to + * @since 1.1.4 + */ + public void printConfig(PrintStream output) { + output.println("defaults " + defaults); + output.println("classpath " + classpath); + output.println("etcfile " + etcfile); + output.println("homefile " + homefile); + output.println("curfile " + curfile); + output.println("env " + env); + output.println("jvm " + jvm); + output.println("line " + line); + output.println("options " + options); + } + + /** + * Return all configuration used with value, that respect includePattern + * + * @param includePattern null for all value, or config key pattern (ex: "wikitty.*") + * @param padding for better presentation, you can use padding to align '=' sign + * @return string that represent config + * @since 1.5.2 + */ + public String getPrintableConfig(String includePattern, int padding) { + String msg = "Configuration:\n"; + for (String key : getFlatOptions().stringPropertyNames()) { + if (includePattern == null || "".equals(includePattern) + || key.matches(includePattern)) { + String value = getOption(key); + msg += String.format("\t%" + padding + "s = %s\n", key, value); + } + } + return msg; + } + + protected void firePropertyChange(String propertyName, + Object oldValue, Object newValue) { + pcs.firePropertyChange(propertyName, oldValue, newValue); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + public void addPropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + pcs.addPropertyChangeListener(propertyName, listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + public void removePropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + pcs.removePropertyChangeListener(propertyName, listener); + } + + public boolean hasListeners(String propertyName) { + return pcs.hasListeners(propertyName); + } + + public PropertyChangeListener[] getPropertyChangeListeners( + String propertyName) { + return pcs.getPropertyChangeListeners(propertyName); + } + + public PropertyChangeListener[] getPropertyChangeListeners() { + return pcs.getPropertyChangeListeners(); + } + + /////////////////////////////////////////////////////////////////////////// + // + // C L A S S E S D E C L A R A T I O N + // + /////////////////////////////////////////////////////////////////////////// + + + /** + * Action to save user configuration. + * <p/> + * Add it as a listener of the configuration for a given property. + * <p/> + * <b>Note:</b> Will not save if {@link #isAdjusting()} is {@code true}. + * + * @since 1.3 + */ + private final PropertyChangeListener saveUserAction = + new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (isAdjusting()) { + if (log.isDebugEnabled()) { + log.debug("Skip save while adjusting"); + } + return; + } + if (log.isDebugEnabled()) { + log.debug("Saving configuration fired by property [" + + evt.getPropertyName() + "] at " + + new Date()); + } + saveForUser(); + } + }; + + /** + * Permet de masquer un prefix. Il est possible d'avoir des valeurs par + * defaut. Par exemple: + * <pre> + * monOption=toto + * monPrefix.monOption=titi + * </pre> + * <p/> + * <li>Si on cree le subApp avec le prefix "monPrefix." et qu'on demande la valeur + * de "monOption", la valeur retournee est "titi". + * <li>Si on cree le subApp avec le prefix "monAutrePrefix." et qu'on demande la valeur + * de "monOption", la valeur retournee est "toto" (valeur par defaut de monOption. + * <p/> + * Certaines methodes retournees ne sont pas + * surchargee et ne masque pas le prefix: + * <li>getOptions() + * + * @since 2.4.9 + */ + public static class SubApplicationConfig extends ApplicationConfig { + + protected ApplicationConfig parent; + + protected String prefix; + + public SubApplicationConfig(ApplicationConfig parent, String prefix) { + this.parent = parent; + this.prefix = prefix; + } + + @Override + protected void init(Properties defaults, String configFilename) { + // do nothing + } + + public ApplicationConfig getParent() { + return parent; + } + + public String getPrefix() { + return prefix; + } + + @Override + public Properties getOptions() { + return getParent().getOptions(); + } + + @Override + public void setDefaultOption(String key, String value) { + getParent().setDefaultOption(getPrefix() + key, value); + } + + @Override + public boolean hasOption(String key) { + boolean result = getOption(key) != null; + return result; + } + + @Override + public void setOption(String key, String value) { + getParent().setOption(getPrefix() + key, value); + } + + /** + * Surcharge pour recherche la cle avec le prefix. Si on ne la retrouve + * pas, on recherche sans le prefix pour permettre d'avoir des valeurs + * par defaut. + * + * @param key La cle de l'option + * @return l'option trouvé avec le prefix ou sinon celle sans le prefix + * si pas trouvé. + */ + @Override + public String getOption(String key) { + String result = getParent().getOption(getPrefix() + key); + if (result == null) { + result = getParent().getOption(key); + } + return result; + } + + /** + * Surcharge de la methode pour que les options commencant par le prefix + * soit modifiee pour qu'elle est la meme cle sans le prefix. Le but + * est de garder les autres options et si une option avait le meme nom + * qu'elle soit effacee par celle dont on a supprime le prefix + * + * @param replaceInner le prefix à remplacer + * @return les options commencant par le prefix + * soit modifiee pour qu'elle est la meme cle sans le prefix. Le but + * est de garder les autres options et si une option avait le meme nom + * qu'elle soit effacee par celle dont on a supprime le prefix + */ + @Override + public Properties getFlatOptions(boolean replaceInner) { + Properties result = getParent().getFlatOptions(replaceInner); + Properties tmp = new Properties(); + int lenght = getPrefix().length(); + for (Map.Entry e : result.entrySet()) { + String k = (String) e.getKey(); + if (k.startsWith(getPrefix())) { + k = k.substring(lenght); + String v = (String) e.getValue(); + tmp.setProperty(k, v); + } + } + result.putAll(tmp); + return result; + } + + /** + * Surcharge pour recupere les valeurs commencant par le prefix demande + * en plus du prefix 'sub'. Les options sont ensuite fusionnee pour + * permettre aussi les valeurs par defaut + * + * @param prefix prefix to use + * @return les valeurs commençant par le prefix demandé en plus du + * prefix 'sub'. + */ + @Override + public Properties getOptionStartsWith(String prefix) { + Properties result = getParent().getOptionStartsWith(prefix); + Properties tmp = getParent().getOptionStartsWith(getPrefix() + prefix); + int lenght = getPrefix().length(); + for (Map.Entry e : tmp.entrySet()) { + String k = (String) e.getKey(); + k = k.substring(lenght); + String v = (String) e.getValue(); + // on ajout/ecrase les valeurs de result + result.setProperty(k, v); + } + return result; + } + + @Override + protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + if (propertyName.startsWith(getPrefix())) { + propertyName = propertyName.substring(getPrefix().length()); + getParent().firePropertyChange(propertyName, oldValue, newValue); + } // else not fire event + } + + @Override + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + getParent().addPropertyChangeListener(getPrefix() + propertyName, listener); + } + + @Override + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + getParent().removePropertyChangeListener(getPrefix() + propertyName, listener); + } + + @Override + public boolean hasListeners(String propertyName) { + return getParent().hasListeners(getPrefix() + propertyName); + } + + // methode interdite dans le sub + + @Override + public ApplicationConfig parse(String... args) throws ArgumentsParserException { + throw new UnsupportedOperationException("This method is not supported in SubApplicationConfig"); + } + + + } + + /** + * Defines a runtime action to be launched via the {@link #doAction()} + * method. + * + * @author poussin + */ + public static class Action { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Step { + + int value() default 0; + } + + protected int step; + + protected Object o; + + protected Method m; + + protected String[] params; + + public Action(int step, Object o, Method m, String... params) { + this.step = step; + this.o = o; + this.m = m; + this.params = params; + } + + public void doAction() throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException, + InstantiationException { + ObjectUtil.call(o, m, params); + } + } + + /** + * Item used for cacheOption + * + * @param <T> + */ + protected static class CacheItem<T> { + + /** typed option value */ + public T item; + + /** hash of string representation */ + public int hash; + + public CacheItem(T item, int hash) { + this.item = item; + this.hash = hash; + } + } + + public static class OptionList { + + protected ApplicationConfig config; + + protected String key; + + protected String value; + + public OptionList(ApplicationConfig config, String key, String value) { + this.config = config; + this.key = key; + this.value = value; + } + + protected <T> List<T> convertListOption(Class<T> type) { + List<T> result = (List<T>) config.convertOption(type, key, + value, + true + ); + return result; + } + + /** + * Get option value as {@link String}. + * + * @return value as String + */ + public List<String> getOption() { + List<String> result = convertListOption(String.class); + return result; + } + + /** + * Get option value as {@link File}. + * + * @return value as file + */ + public List<File> getOptionAsFile() { + List<File> tmp = convertListOption(File.class); + List<File> result = new ArrayList<File>(tmp.size()); + for (File file : tmp) { + result.add(file.getAbsoluteFile()); + } + return result; + } + + /** + * Get option value as {@link URL}. + * + * @return value as URL + */ + public List<URL> getOptionAsURL() { + List<URL> result = convertListOption(URL.class); + return result; + } + + /** + * Get option value as {@link Class}. + * + * @return value as Class + */ + public List<Class> getOptionAsClass() { + List<Class> result = convertListOption(Class.class); + return result; + } + + /** + * Get option value as {@link Date}. + * + * @return value as Date + */ + public List<Date> getOptionAsDate() { + List<Date> result = convertListOption(Date.class); + return result; + } + + /** + * Get option value as {@link Time}. + * + * @return value as Time + */ + public List<Time> getOptionAsTime() { + List<Time> result = convertListOption(Time.class); + return result; + } + + /** + * Get option value as {@link Timestamp}. + * + * @return value as Timestamp + */ + public List<Timestamp> getOptionAsTimestamp() { + List<Timestamp> result = convertListOption(Timestamp.class); + return result; + } + + /** + * Get option value as {@code int}. + * + * @return value as {@code int} + */ + public List<Integer> getOptionAsInt() { + List<Integer> result = convertListOption(Integer.class); + return result; + } + + /** + * Get option value as {@code double}. + * + * @return value as {@code double} + */ + public List<Double> getOptionAsDouble() { + List<Double> result = convertListOption(Double.class); + return result; + } + + /** + * Get option value as {@code boolean}. + * + * @return value as {@code boolean}. + */ + public List<Boolean> getOptionAsBoolean() { + List<Boolean> result = convertListOption(Boolean.class); + return result; + } + } + +} diff --git a/src/main/java/org/nuiton/config/ApplicationConfigHelper.java b/src/main/java/org/nuiton/config/ApplicationConfigHelper.java new file mode 100644 index 0000000..90d3a13 --- /dev/null +++ b/src/main/java/org/nuiton/config/ApplicationConfigHelper.java @@ -0,0 +1,243 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.HashSet; +import java.util.ServiceLoader; +import java.util.Set; + +/** + * Helper about {@link ApplicationConfig}. + * + * @author tchemit <chemit@codelutin.com> + * @since 2.4.8 + */ +public class ApplicationConfigHelper { + + /** Logger. */ + private static final Log log = + LogFactory.getLog(ApplicationConfigHelper.class); + + protected ApplicationConfigHelper() { + // helper with no instance + } + + /** + * Obtain all providers on class-path. + * + * @param classLoader optional classLoader used to seek for providers + * @param includes optional includes providers to use (if none then accept all providers) + * @param excludes optional excludes providers (if none the no reject) + * @param verbose verbose flag + * @return sets of providers + */ + public static Set<ApplicationConfigProvider> getProviders(ClassLoader classLoader, + Set<String> includes, + Set<String> excludes, + boolean verbose) { + ServiceLoader<ApplicationConfigProvider> loader; + if (classLoader == null) { + loader = ServiceLoader.load(ApplicationConfigProvider.class); + + } else { + loader = ServiceLoader.load(ApplicationConfigProvider.class, + classLoader); + } + + Set<ApplicationConfigProvider> result = + new HashSet<ApplicationConfigProvider>(); + + for (ApplicationConfigProvider configProvider : loader) { + String name = configProvider.getName(); + if (includes != null && !includes.contains(name)) { + + // reject by include + if (verbose) { + log.info("configuration named '" + name + + "' is rejected by includes."); + } + continue; + } + if (excludes != null && excludes.contains(name)) { + + // reject by exclude + if (verbose) { + log.info("configuration named '" + name + + "' is rejected by excludes."); + } + continue; + } + if (verbose) { + log.info("configuration named '" + name + + "' will be generated."); + } + result.add(configProvider); + } + return result; + } + + public static ApplicationConfigProvider getProvider(ClassLoader classLoader, + String name) { + Set<ApplicationConfigProvider> providers = getProviders( + classLoader, null, null, false); + ApplicationConfigProvider result = null; + for (ApplicationConfigProvider provider : providers) { + if (name.equals(provider.getName())) { + result = provider; + break; + } + } + return result; + } + + /** + * Load default options from all given config providers. + * + * @param config config where to add default options. + * @param providers providers to use + * @since 2.6.7 + */ + public static void loadAllDefaultOption(ApplicationConfig config, + Set<ApplicationConfigProvider> providers) { + + for (ApplicationConfigProvider provider : providers) { + if (log.isInfoEnabled()) { + log.info("Load default options from configuration: " + + provider.getName()); + } + if (log.isInfoEnabled()) { + for (ConfigOptionDef optionDef : provider.getOptions()) { + log.info(" " + optionDef.getKey() + + " (" + optionDef.getDefaultValue() + ')'); + } + } + config.loadDefaultOptions(provider.getOptions()); + } + } + + /** + * Gets all transient options from the given providers. + * + * @param providers providers to inspect + * @return the set of all options that are transient + * @see ConfigOptionDef#isTransient() + * @since 2.6.7 + */ + public static Set<ConfigOptionDef> getTransientOptions(Set<ApplicationConfigProvider> providers) { + Set<ConfigOptionDef> result = new HashSet<ConfigOptionDef>(); + for (ApplicationConfigProvider provider : providers) { + for (ConfigOptionDef def : provider.getOptions()) { + if (def.isTransient()) { + result.add(def); + } + } + } + return result; + } + + + /** + * Gets all final options from the given providers. + * + * @param providers providers to inspect + * @return the set of all options that are final + * @see ConfigOptionDef#isFinal() + * @since 2.6.7 + */ + public static Set<ConfigOptionDef> getFinalOptions(Set<ApplicationConfigProvider> providers) { + Set<ConfigOptionDef> result = new HashSet<ConfigOptionDef>(); + for (ApplicationConfigProvider provider : providers) { + for (ConfigOptionDef def : provider.getOptions()) { + if (def.isFinal()) { + result.add(def); + } + } + } + return result; + } + + /** + * Get all option keys that should not be saved in the user config file + * from the given options providers. + * <p/> + * Such options are {@code transient} or {@code final}. + * + * @param providers providers to inspect + * @return the set of options key not to store in the config file + * @see ConfigOptionDef#isFinal() + * @see ConfigOptionDef#isTransient() + * @since 2.6.11 + */ + public static Set<String> getTransientOrFinalOptionKey(Set<ApplicationConfigProvider> providers) { + Set<String> result = new HashSet<String>(); + result.addAll(getTransientOptionKeys(providers)); + result.addAll(getFinalOptionKeys(providers)); + return result; + } + + /** + * Gets all transient options keys from the given providers. + * + * @param providers providers to inspect + * @return the set of all options key that are transient + * @see ConfigOptionDef#isTransient() + * @since 2.6.11 + */ + public static Set<String> getTransientOptionKeys(Set<ApplicationConfigProvider> providers) { + Set<String> result = new HashSet<String>(); + for (ApplicationConfigProvider provider : providers) { + for (ConfigOptionDef def : provider.getOptions()) { + if (def.isTransient()) { + result.add(def.getKey()); + } + } + } + return result; + } + + /** + * Gets all final options keys from the given providers. + * + * @param providers providers to inspect + * @return the set of all options keys that are final + * @see ConfigOptionDef#isTransient() + * @since 2.6.7 + */ + public static Set<String> getFinalOptionKeys(Set<ApplicationConfigProvider> providers) { + Set<String> result = new HashSet<String>(); + for (ApplicationConfigProvider provider : providers) { + for (ConfigOptionDef def : provider.getOptions()) { + if (def.isFinal()) { + result.add(def.getKey()); + } + } + } + return result; + } + +} diff --git a/src/main/java/org/nuiton/config/ApplicationConfigProvider.java b/src/main/java/org/nuiton/config/ApplicationConfigProvider.java new file mode 100644 index 0000000..23d0200 --- /dev/null +++ b/src/main/java/org/nuiton/config/ApplicationConfigProvider.java @@ -0,0 +1,79 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +import java.util.Locale; +import java.util.ServiceLoader; + +/** + * Provider of a {@link ApplicationConfig}. + * <p/> + * Each library of application which use {@link ApplicationConfig} should + * implements this and add the provider available via the + * {@link ServiceLoader} mecanism. + * <p/> + * Using such provider offers a nice way to find out what options can be loaded + * in a application. It also offers a simply way to generate application + * config report for documentation. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.4.8 + */ +public interface ApplicationConfigProvider { + + /** + * Returns the name of the provided application config. + * <p/> + * This should be the name of the library or application which offers + * the configuration. + * + * @return the name of the provided application config + */ + String getName(); + + /** + * Returns the localized description of the configuration. + * + * @param locale locale used to render description + * @return the localized description of the configuration + */ + String getDescription(Locale locale); + + /** + * Returns all options offered by the configuration. + * + * @return all options offered by the configuration + * @see ConfigOptionDef + */ + ConfigOptionDef[] getOptions(); + + /** + * Returns all actions offered by the configuration. + * + * @return all actions offered by the configuration. + * @see ConfigActionDef + */ + ConfigActionDef[] getActions(); +} diff --git a/src/main/java/org/nuiton/config/ApplicationConfigSaveException.java b/src/main/java/org/nuiton/config/ApplicationConfigSaveException.java new file mode 100644 index 0000000..035e24c --- /dev/null +++ b/src/main/java/org/nuiton/config/ApplicationConfigSaveException.java @@ -0,0 +1,39 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2013 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +/** + * throw if any error when saving configuration. + * + * @author tchemit <chemit@codelutin.com> + * @since 3.0 + */ +public class ApplicationConfigSaveException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public ApplicationConfigSaveException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/nuiton/config/ArgumentsParserException.java b/src/main/java/org/nuiton/config/ArgumentsParserException.java new file mode 100644 index 0000000..fb3ed45 --- /dev/null +++ b/src/main/java/org/nuiton/config/ArgumentsParserException.java @@ -0,0 +1,46 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +/** + * Argument parsing exception. + * + * @author Benjamin Poussin <poussin@codelutin.com> + * @since 2.7 + */ +public class ArgumentsParserException extends Exception { + + private static final long serialVersionUID = 1L; + + public ArgumentsParserException(String msg) { + super(msg); + } + + public ArgumentsParserException(String msg, Throwable eee) { + super(msg, eee); + } + +} + diff --git a/src/main/java/org/nuiton/config/ConfigActionDef.java b/src/main/java/org/nuiton/config/ConfigActionDef.java new file mode 100644 index 0000000..f734b20 --- /dev/null +++ b/src/main/java/org/nuiton/config/ConfigActionDef.java @@ -0,0 +1,78 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +import java.io.Serializable; + +/** + * Le contrat de marquage des actions, on utilise cette interface pour + * caracteriser une action. + * <p/> + * Ex : + * <p/> + * <pre> + * public enum MyAppConfigAction implements ConfigActionDef { + * HELP(MyAppHelpAction.class.getName() + "#show", "-h", "--help"); + * public String action; + * public String[] aliases; + * + * private WikittyConfigAction(String action, String... aliases) { + * this.action = action; + * this.aliases = aliases; + * } + * + * @Override + * public String getAction() { + * return action; + * } + * + * @Override + * public String[] getAliases() { + * return aliases; + * } + * + * } + * </pre> + * + * @author sletellier + * @author tchemit <chemit@codelutin.com> + * @since 2.6.10 + */ +public interface ConfigActionDef extends Serializable { + + /** + * Must return fully qualified method path : package.Class#method + * + * @return action to run + */ + String getAction(); + + /** + * Return all alias used to execute action. + * + * @return aliases used to execute action + */ + String[] getAliases(); +} diff --git a/src/main/java/org/nuiton/config/ConfigOptionDef.java b/src/main/java/org/nuiton/config/ConfigOptionDef.java new file mode 100644 index 0000000..e6165ae --- /dev/null +++ b/src/main/java/org/nuiton/config/ConfigOptionDef.java @@ -0,0 +1,163 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +import java.io.Serializable; + +/** + * Le contrat de marquage des options, on utilise cette interface pour + * caracteriser une option de configuration. + * <p/> + * <pre> + * public enum MyConfigOption implements ConfigOptionDef { + * + * APP_CONFIG_FILE( + * ApplicationConfig.CONFIG_FILE_NAME, + * "Main configuration app file", + * "myApp-config.properties", + * String.class, true, true), + * + * APP_NAME( + * ApplicationConfig.CONFIG_FILE_NAME, + * Application name, + * "MyApp", + * String.class, true, true); + * + * public String key; + * public String description; + * public String defaultValue; + * public Class<?> type; + * public boolean isTransient; + * public boolean isFinal; + * + * private WikittyConfigOption(String key, String description, + * String defaultValue, Class<?> type, boolean isTransient, boolean isFinal) { + * this.key = key; + * this.description = description; + * this.defaultValue = defaultValue; + * this.type = type; + * this.isTransient = isTransient; + * this.isFinal = isFinal; + * } + * + * @Override + * public boolean isFinal() { + * return isFinal; + * } + * + * @Override + * public boolean isTransient() { + * return isTransient; + * } + * + * @Override + * public String getDefaultValue() { + * return defaultValue; + * } + * + * @Override + * public String getDescription() { + * return description; + * } + * + * @Override + * public String getKey() { + * return key; + * } + * + * @Override + * public Class<?> getType() { + * return type; + * } + * + * @Override + * public void setDefaultValue(String defaultValue) { + * this.defaultValue = defaultValue; + * } + * + * @Override + * public void setTransient(boolean isTransient) { + * this.isTransient = isTransient; + * } + * + * @Override + * public void setFinal(boolean isFinal) { + * this.isFinal = isFinal; + * } + * } + * </pre> + * + * @since 1.0.0-rc-9 + */ +public interface ConfigOptionDef extends Serializable { + + /** @return la clef identifiant l'option */ + String getKey(); + + /** @return le type de l'option */ + Class<?> getType(); + + /** @return la clef i18n de description de l'option */ + String getDescription(); + + /** + * @return la valeur par defaut de l'option sous forme de chaine de + * caracteres + */ + String getDefaultValue(); + + /** + * @return <code>true</code> si l'option ne peut etre sauvegardee sur + * disque (utile par exemple pour les mots de passe, ...) + */ + boolean isTransient(); + + /** + * @return <code>true</code> si l'option n'est pas modifiable (utilise + * par exemple pour la version de l'application, ...) + */ + boolean isFinal(); + + /** + * Changes the default value of the option. + * + * @param defaultValue the new default value of the option + */ + void setDefaultValue(String defaultValue); + + /** + * Changes the transient state of the option. + * + * @param isTransient the new value of the transient state + */ + void setTransient(boolean isTransient); + + /** + * Changes the final state of the option. + * + * @param isFinal the new transient state value + */ + void setFinal(boolean isFinal); +} diff --git a/src/site/apt/index.apt b/src/site/apt/index.apt new file mode 100644 index 0000000..d102904 --- /dev/null +++ b/src/site/apt/index.apt @@ -0,0 +1,395 @@ +~~~ +~~ #%L +~~ Nuiton Config +~~ $Id$ +~~ $HeadURL$ +~~ %% +~~ Copyright (C) 2013 CodeLutin +~~ %% +~~ This program is free software: you can redistribute it and/or modify +~~ it under the terms of the GNU Lesser General Public License as +~~ published by the Free Software Foundation, either version 3 of the +~~ License, or (at your option) any later version. +~~ +~~ This program is distributed in the hope that it will be useful, +~~ but WITHOUT ANY WARRANTY; without even the implied warranty of +~~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +~~ GNU General Lesser Public License for more details. +~~ +~~ You should have received a copy of the GNU General Lesser Public +~~ License along with this program. If not, see +~~ <http://www.gnu.org/licenses/lgpl-3.0.html>. +~~ #L% +~~~ + ---- + Nuiton config + ---- + ---- + 2009-08-23 + ---- + +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 parsage de la ligne de commande ; + + * l'execution des actions ; + + * la sauvegarde de la configuration. + +Note + + <<Nuiton-config>> quitte le projet <nuiton-utils> pour devenir un projet autonome. + + Voici quelques liens sur le nouveau projet: + + * {{{http://svn.nuiton.org/svn/nuiton-config}svn}} + + * {{{http://nuiton.org/projects/nuiton-config}forge}} + + * {{{http://maven-site.nuiton.org/nuiton-config}site}} + + [] + + A noter que le GAV de l'artefact ne change pas (<org.nuiton:nuiton-config>). + + La dernière version stable dans nuiton-utils est la 2.7; vous pouvez dès à + présent utiliser la version 3.0-alpha-1 de nuiton-config. + + Pour plus de détails sur les changements importants entre chaque version, + vous pouvez consulter les {{{./versions.html}Notes de versions}}. + +Lecture/écriture + +* Lecture des fichiers de configuration + + La lecture des fichiers de configuration est effectuée lors de l'appel + à la methode <<<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 methodes disponibles : + + * <<<save>>> : sauvegarde dans un fichier specifique ; + + * <<<saveForSystem>>> : sauvegarde les donnees dans /etc ; + + * <<<saveForUser>>> : sauvegarde les donnees dans $HOME. + + [] + + Seules les options qui ont été modifiées par l'application (par la methode + <<<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 + methode <<<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é defini + 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 methode qui traitera + l'action. Si la methode 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 appelera la methode : + ++------------------------------------------- +public class Test { + public doLogin(String login, String password, boolean dryRun) { + [...] + } +} ++------------------------------------------- + + Les actions ne sont pas executées, mais seulement parsées. Cela signifie + qu'elles seront executées seulement lorsque l'application appelera la méthode + <<<doAction(int)>>>. + Par défaut, toutes les actions sont de niveau 0 et sont executé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 éxecuter 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 recupé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 definir les options et les actions. + Ces alias doivent être renseignés par la methode <<<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 argment de type File. Enfin dans le troisieme 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 mecanisme pour + autre chose : + ++------------------------------------------- +addAlias("cl", "Code Lutin"); ++------------------------------------------- + + +* Conversion de type + + Pour convertir les types des options et arguments de méthodes, + {{{http://commons.apache.org/beanutils/}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\}>>> où <<<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 methode <<<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 initilalisé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 ApplicationConfig.OptionDef[] getOptions() { + return PollenConfigurationOption.values(); + } + + @Override + public ApplicationConfig.ActionDef[] getActions() { + return new ApplicationConfig.ActionDef[0]; + } +} ++------------------------------------------- + + Puis ajouter le fichier <META-INF/services/org.nuiton.util.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. diff --git a/src/site/apt/versions.apt b/src/site/apt/versions.apt new file mode 100644 index 0000000..b58bcdf --- /dev/null +++ b/src/site/apt/versions.apt @@ -0,0 +1,34 @@ +~~~ +~~ #%L +~~ Nuiton Config +~~ $Id$ +~~ $HeadURL$ +~~ %% +~~ Copyright (C) 2013 CodeLutin +~~ %% +~~ This program is free software: you can redistribute it and/or modify +~~ it under the terms of the GNU Lesser General Public License as +~~ published by the Free Software Foundation, either version 3 of the +~~ License, or (at your option) any later version. +~~ +~~ This program is distributed in the hope that it will be useful, +~~ but WITHOUT ANY WARRANTY; without even the implied warranty of +~~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +~~ GNU General Lesser Public License for more details. +~~ +~~ You should have received a copy of the GNU General Lesser Public +~~ License along with this program. If not, see +~~ <http://www.gnu.org/licenses/lgpl-3.0.html>. +~~ #L% +~~~ + ---- + Nuiton config + ---- + ---- + 2013-07-23 + ---- + +Utilisation de la version 3.0 + + * Pour passer sur cette version, il faut changer les packages <org.nuiton.util.config> + en <org.nuiton.config>. \ No newline at end of file diff --git a/src/site/site_fr.xml b/src/site/site_fr.xml new file mode 100644 index 0000000..b477de2 --- /dev/null +++ b/src/site/site_fr.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + #%L + Nuiton Config + $Id$ + $HeadURL$ + %% + Copyright (C) 2013 CodeLutin + %% + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Lesser Public License for more details. + + You should have received a copy of the GNU General Lesser Public + License along with this program. If not, see + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> + + +<project name="${project.name}"> + + <skin> + <groupId>org.apache.maven.skins</groupId> + <artifactId>maven-fluido-skin</artifactId> + <version>1.3.0</version> + </skin> + + <custom> + <fluidoSkin> + <topBarEnabled>false</topBarEnabled> + <googleSearch/> + <sideBarEnabled>true</sideBarEnabled> + <searchEnabled>true</searchEnabled> + <sourceLineNumbersEnabled>true</sourceLineNumbersEnabled> + </fluidoSkin> + </custom> + + <bannerLeft> + <name>${project.name}</name> + <href>index.html</href> + </bannerLeft> + + <bannerRight> + <src>http://www.codelutin.com/images/lutinorange-codelutin.png</src> + <href>http://www.codelutin.com</href> + </bannerRight> + + <publishDate position="right" /> + <version position="right" /> + + <poweredBy> + + <logo href="http://maven.apache.org" name="Maven" + img="http://maven-site.chorem.org/public/images/logos/maven-feather.png"/> + + </poweredBy> + + <body> + + <head> + <script type="text/javascript" + src="http://maven-site.chorem.org/public/js/mavenpom-site.js"> + </script> + + <link rel="stylesheet" type="text/css" + href="http://maven-site.chorem.org/public/css/mavenpom-site.css"/> + </head> + + <links> + <item name="Nuiton.org" href="http://nuiton.org"/> + <item name="Code Lutin" href="http://www.codelutin.com"/> + <item name="Libre entreprise" href="http://www.libre-entreprise.org"/> + </links> + + <breadcrumbs> + <item name="${project.name}" + href="${project.url}/index.html"/> + </breadcrumbs> + + <menu name="Utilisateur"> + <item name="Accueil" href="index.html"/> + <item name="Note de versions" href="versions.html"/> + </menu> + + <menu ref="reports"/> + + <footer> + + <div id='projectMetas' + projectversion='${project.version}' + platform='${project.platform}' + projectid='${project.projectId}' + scm='${project.scm.developerConnection}' + scmwebeditorenabled='${project.scmwebeditorEnabled}' + scmwebeditorurl='${project.scmwebeditorUrl}' + siteSourcesType='${project.siteSourcesType}' + piwikEnabled='${project.piwikEnabled}' + piwikId='${project.piwikId}' locale='fr'> + </div> + </footer> + </body> +</project> diff --git a/src/test/java/org/nuiton/config/ApplicationConfigTest.java b/src/test/java/org/nuiton/config/ApplicationConfigTest.java new file mode 100644 index 0000000..d9fc26b --- /dev/null +++ b/src/test/java/org/nuiton/config/ApplicationConfigTest.java @@ -0,0 +1,660 @@ +package org.nuiton.config; + +/* + * #%L + * Nuiton Config + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2011 - 2013 CodeLutin, Tony Chemit + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.nuiton.util.FileUtil; +import org.nuiton.util.Version; +import org.nuiton.util.VersionUtil; +import org.nuiton.config.ApplicationConfig.Action; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +/** + * @author Benjamin Poussin <poussin@codelutin.com> + * @author tchemit <chemit@codelutin.com> + * @since 2.6.10 + */ +public class ApplicationConfigTest { + + private static final Log log = + LogFactory.getLog(ApplicationConfigTest.class); + + public static final long TIMESTAMP = System.nanoTime(); + + protected static int DUMMY_ACTION_CALL; + + public static class DummyAction { + @Action.Step(1) + public void dummyAction(String s, int step) { + DUMMY_ACTION_CALL++; + log.info(s + ':' + step); + } + } + + @Rule + public final TestName testName = new TestName(); + + protected File testDirectory; + + @Before + public void before() { + testDirectory = FileUtil.getTestSpecificDirectory( + getClass(), + testName.getMethodName(), + null, + TIMESTAMP); + } + + @Test + public void saveForUser() throws IOException { + + // Initiliaze path and filename + String path = testDirectory.getAbsolutePath(); + + String oldHome = SystemUtils.getUserHome().getAbsolutePath(); + + try { + System.setProperty("user.home", path); + + ApplicationConfig config = + new ApplicationConfig(testName.getMethodName()); + + File userFile = config.getUserConfigFile(); + + if (userFile.exists()) { + FileUtils.forceDelete(userFile); + } + + config.setOption("key1", "toto"); + config.setOption("key2", "tata"); + config.setOption("key3", "tutu"); + + // Parent directory will be created + config.saveForUser(); + + // I like to test that file.create works :( + Assert.assertTrue(userFile.exists()); + + Properties p = loadPropertyFile(userFile); + + Assert.assertEquals(3, p.size()); + + String property; + + property = p.getProperty("key1"); + Assert.assertEquals("toto", property); + + property = p.getProperty("key2"); + Assert.assertEquals("tata", property); + + property = p.getProperty("key3"); + Assert.assertEquals("tutu", property); + + } finally { + + if (oldHome != null) { + System.setProperty("user.home", oldHome); + } + } + + } + + @Test + public void cleanUserConfig() throws IOException, ArgumentsParserException { + + // Initiliaze path and filename + String path = testDirectory.getAbsolutePath(); + + String oldHome = SystemUtils.getUserHome().getAbsolutePath(); + + try { + System.setProperty("user.home", path); + + ApplicationConfig config = + new ApplicationConfig(testName.getMethodName()); + + + config.setOption("key1", "toto"); + config.setOption("key2", ""); + config.setOption("key3", "tutu"); + + File userFile = config.getUserConfigFile(); + + Assert.assertTrue(userFile.getAbsolutePath().startsWith(path)); + + if (userFile.exists()) { + FileUtils.forceDelete(userFile); + } + + config.saveForUser(); + + Assert.assertTrue(userFile.exists()); + + Properties p = loadPropertyFile(userFile); + + Assert.assertEquals(3, p.size()); + + String property; + + property = p.getProperty("key1"); + Assert.assertEquals("toto", property); + + property = p.getProperty("key2"); + Assert.assertEquals("", property); + + property = p.getProperty("key3"); + Assert.assertEquals("tutu", property); + + // reload config + config = new ApplicationConfig(testName.getMethodName()); + config.parse(); + + config.cleanUserConfig(); + + Properties p2 = loadPropertyFile(userFile); + + // key2 was removed + Assert.assertEquals(2, p2.size()); + + property = p2.getProperty("key1"); + Assert.assertEquals("toto", property); + + property = p2.getProperty("key2"); + Assert.assertNull(property); + + property = p2.getProperty("key3"); + Assert.assertEquals("tutu", property); + + // now key3 will be removed (even if not blank) + config.cleanUserConfig("key3"); + + // reload config + config = new ApplicationConfig(testName.getMethodName()); + config.parse(); + + Properties p3 = loadPropertyFile(userFile); + + // key3 was removed + Assert.assertEquals(1, p3.size()); + + property = p3.getProperty("key1"); + Assert.assertEquals("toto", property); + } finally { + + if (oldHome != null) { + System.setProperty("user.home", oldHome); + } + } + + } + + @Test + public void getUnparsed() throws Exception { + ApplicationConfig instance = new ApplicationConfig(); + List<String> expResult = new ArrayList<String>(); + List<String> result = instance.getUnparsed(); + Assert.assertEquals(expResult, result); + + expResult.add("toto"); + expResult.add("titi"); + expResult.add("tata"); + + instance.parse("toto", "titi", "tata"); + result = instance.getUnparsed(); + Assert.assertEquals(expResult, result); + } + + @Test + public void addAction() throws Exception { + Action action = null; + ApplicationConfig instance = new ApplicationConfig(); + + // test add null Action + instance.addAction(action); + + action = new Action(1, new DummyAction(), DummyAction.class.getMethod("dummyAction", String.class, Integer.TYPE), "coucou", "12"); + instance.addAction(action); + } + + @Test + public void doAction() throws Exception { + ApplicationConfig instance = new ApplicationConfig(); + + Action action = new Action(1, new DummyAction(), DummyAction.class.getMethod("dummyAction", String.class, Integer.TYPE), "coucou", "12"); + instance.addAction(action); + + DUMMY_ACTION_CALL = 0; + Assert.assertEquals(0, DUMMY_ACTION_CALL); + instance.doAction(0); + Assert.assertEquals(0, DUMMY_ACTION_CALL); + instance.doAction(1); + Assert.assertEquals(1, DUMMY_ACTION_CALL); + instance.doAction(2); + Assert.assertEquals(1, DUMMY_ACTION_CALL); + } + + @Test + public void setUseOnlyAliases() { + ApplicationConfig instance = new ApplicationConfig(); + Assert.assertEquals(false, instance.isUseOnlyAliases()); + instance.setUseOnlyAliases(false); + Assert.assertEquals(false, instance.isUseOnlyAliases()); + instance.setUseOnlyAliases(true); + Assert.assertEquals(true, instance.isUseOnlyAliases()); + } + + @Test + public void addAlias() throws Exception { + ApplicationConfig instance = new ApplicationConfig(); + instance.addAlias("toto", "totochange"); + instance.addAlias("titi", "titichange"); + + List<String> expResult = new ArrayList<String>(); + List<String> result = instance.getUnparsed(); + Assert.assertEquals(expResult, result); + + expResult.add("totochange"); + expResult.add("titichange"); + expResult.add("tata"); + + instance.parse("toto", "titi", "tata"); + result = instance.getUnparsed(); + Assert.assertEquals(expResult, result); + } + + @Test + public void testSetConfigFileName() { + ApplicationConfig instance = new ApplicationConfig(); + instance.setConfigFileName("bidulle"); + Assert.assertEquals("bidulle", instance.getConfigFileName()); + } + + /** Test of setOption method, of class ApplicationConfig. */ + @Test + public void testSetOption() { + ApplicationConfig instance = new ApplicationConfig(); + Assert.assertEquals(null, instance.getOption("truc")); + instance.setOption("truc", "bidulle"); + Assert.assertEquals("bidulle", instance.getOption("truc")); + } + + @Test + public void testSubConfig() { + ApplicationConfig instance = new ApplicationConfig(); + Assert.assertEquals(null, instance.getOption("truc")); + instance.setOption("truc", "bidulle"); + instance.setOption("machin", "chouette"); + instance.setOption("toto.truc", "AutreBidulle"); + instance.setOption("toto.specific", "theOne"); + Assert.assertEquals("bidulle", instance.getOption("truc")); + + ApplicationConfig sub = instance.getSubConfig("toto."); + // test la surcharge du default + Assert.assertEquals("AutreBidulle", sub.getOption("truc")); + // test l'utilisation du default + Assert.assertEquals("chouette", sub.getOption("machin")); + // test une valeur specifique au sub + Assert.assertEquals("theOne", sub.getOption("specific")); + + // TODO ajouter d'autres tests, pour les autres methodes surchargee + // TODO ajouter d'autres tests pour des sub-(sub-(sub)) config + } + + @Test + public void getAsList() { + List<String> asString = new ArrayList<String>(); + asString.add(ApplicationConfig.class.getName()); + asString.add(ApplicationConfigTest.class.getName()); + + List<Class> asClass = new ArrayList<Class>(); + asClass.add(ApplicationConfig.class); + asClass.add(ApplicationConfigTest.class); + + ApplicationConfig instance = new ApplicationConfig(); + Assert.assertEquals(null, instance.getOption("truc")); + Assert.assertEquals(Collections.<String>emptyList(), instance.getOptionAsList("truc").getOption()); + + instance.setOption("truc", ApplicationConfig.class.getName() + + "," + ApplicationConfigTest.class.getName()); + + Assert.assertEquals(asString, instance.getOptionAsList("truc").getOption()); + Assert.assertEquals(asClass, instance.getOptionAsList("truc").getOptionAsClass()); + } + + @Test + public void getMethods() { + ApplicationConfig instance = new ApplicationConfig(); + Map<String, Method> result = instance.getMethods(); + Assert.assertTrue(result.containsKey("option")); + } + + @Test + public void getParams() throws Exception { + Method m = DummyAction.class.getMethod("dummyAction", String.class, Integer.TYPE); + List<String> list = new ArrayList<String>(Arrays.asList("toto", "10", "/tmp", "9")); + ListIterator<String> args = list.listIterator(); + + ApplicationConfig instance = new ApplicationConfig(); + String[] expResult = new String[]{"toto", "10"}; + String[] result = instance.getParams(m, args); + Assert.assertEquals(Arrays.asList(expResult), Arrays.asList(result)); + Assert.assertEquals(2, list.size()); + } + + @Test + public void testCreateAction() throws Exception { + List<String> list = new ArrayList<String>(Arrays.asList("dummy", "toto", "10", "/tmp", "9")); + ListIterator<String> args = list.listIterator(); + args.next(); + ApplicationConfig instance = new ApplicationConfig(); + + Action result = instance.createAction( + DummyAction.class.getName() + "#dummyAction", args); + Assert.assertEquals(1, result.step); + DUMMY_ACTION_CALL = 0; + result.doAction(); + Assert.assertEquals(1, DUMMY_ACTION_CALL); + } + + @Test + public void testParse() throws Exception { + String[] args = "-f file -v -d -o /tmp/file -m coucou 10 others args".split(" "); + ApplicationConfig instance = new ApplicationConfig(); + instance.addAlias("-f", "--option", "file"); + instance.addAlias("-v", "--option", "verbose", "true"); + instance.addAlias("-d", "--option", "debug", "true"); + instance.addAlias("-o", "--option", "output"); + instance.addAlias("-m", "--" + DummyAction.class.getName() + "#dummyAction"); + instance.parse(args); + + DUMMY_ACTION_CALL = 0; + Assert.assertEquals("file", instance.getOption("file")); + Assert.assertEquals("true", instance.getOption("verbose")); + Assert.assertEquals("true", instance.getOption("debug")); + Assert.assertEquals("/tmp/file", instance.getOption("output")); + Assert.assertEquals(Arrays.asList("others", "args"), instance.getUnparsed()); + + instance.doAction(1); + Assert.assertEquals(1, DUMMY_ACTION_CALL); + } + + /** + * Test that system properties such as ${user.home}, ${user.name} are + * replaced. + * + * @throws ArgumentsParserException + */ + @Test + public void testSystemProperties() throws ArgumentsParserException { + ApplicationConfig instance = new ApplicationConfig(); + instance.parse(); + instance.printConfig(); + + instance.setOption("hellomessage", "Hello ${user.name} !"); + + Assert.assertEquals("Hello " + System.getProperty("user.name") + " !", instance.getOption("hellomessage")); + Assert.assertEquals("Hello ${user.name} !", instance.options.getProperty("hellomessage")); + + instance.setOption("tempdir", "${java.io.tmpdir}" + File.separator + "blah"); + File tempDir = instance.getOptionAsFile("tempdir"); + Assert.assertEquals(new File(System.getProperty("java.io.tmpdir"), "blah").getAbsolutePath(), tempDir.getAbsolutePath()); + + instance.setOption("system", "${os.name}"); + instance.setOption("os", "${system}"); + instance.setOption("sysinfo", "I'm running ${os} :)"); + Assert.assertEquals("I'm running " + System.getProperty("os.name") + " :)", instance.getOption("sysinfo")); + + // test not found properties + instance.setOption("notexists", "Attention ${blah.bloh.bluh} :("); + Assert.assertEquals("Attention ${blah.bloh.bluh} :(", instance.getOption("notexists")); + } + + /** + * test if dot is replaced with _ if properties is not found with dot in env + * + * @throws ArgumentsParserException + */ + @Test + public void testEnvProperties() throws ArgumentsParserException { + ApplicationConfig instance = new ApplicationConfig(); + // simulate env variable with _ to replace dot + instance.env.put("test_env", "value"); + + String value = instance.getOption("test.env"); + Assert.assertEquals("value", value); + } + + @Test + public void getUnparsed2() throws Exception { + + String[] args = "test --du i_am_a_test 2 --option file f1 --option verbose false -- --openui false --m coucou 10 others args".split(" "); + ApplicationConfig instance = new ApplicationConfig(); + instance.addActionAlias("--du", DummyAction.class.getName() + "#" + "dummyAction"); + instance.parse(args); + instance.doAction(1); + + log.info(instance.getUnparsed()); + Assert.assertEquals(8, instance.getUnparsed().size()); + Assert.assertEquals("test", instance.getUnparsed().get(0)); + } + + @Test + public void getFlatOptions() throws Exception { + + ApplicationConfig instance = new ApplicationConfig(); + instance.parse(); + instance.setDefaultOption("user.firstname", "toto"); + instance.setDefaultOption("user.lastname", "tutu"); + instance.setOption("user.fullname", "${user.lastname} ${user.firstname}"); + + Assert.assertEquals(1, instance.getOptions().size()); + // il y en a plus de 3 car il y a aussi les variables d'environnement + Assert.assertTrue(instance.getFlatOptions().size() > 3); + + // test replacement and non replacement + Assert.assertEquals("tutu toto", + instance.getFlatOptions().getProperty("user.fullname")); + Assert.assertEquals("tutu toto", + instance.getFlatOptions(true).getProperty("user.fullname")); + Assert.assertEquals("${user.lastname} ${user.firstname}", + instance.getFlatOptions(false).getProperty("user.fullname")); + } + + /** + * Test null options. + * <p/> + * TODO EC20100503 this test throw a huge exception + * + * @throws Exception + */ + @Test + public void getNullOptions() throws Exception { + ApplicationConfig instance = new ApplicationConfig(); + instance.parse(); + + // primitives can not be null + Assert.assertNotNull(instance.getOptionAsBoolean("dfsdfgqsgqfg")); + Assert.assertNotNull(instance.getOptionAsDouble("dfsdfgqsgqfg")); + Assert.assertNotNull(instance.getOptionAsInt("dfsdfgqsgqfg")); + Assert.assertNotNull(instance.getOptionAsLong("dfsdfgqsgqfg")); + // list option can not be null + Assert.assertNotNull(instance.getOptionAsList("dfsdfgqsgqfg")); + + // all other types can be null + Assert.assertNull(instance.getOptionAsClass("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsDate("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsFile("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsLocale("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsTime("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsTimestamp("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsURL("dfsdfgqsgqfg")); + Assert.assertNull(instance.getOptionAsVersion("dfsdfgqsgqfg")); + } + + /** + * Test on printConfig output. + * + * @throws ArgumentsParserException + * @throws UnsupportedEncodingException + */ + public void testxx() throws ArgumentsParserException, UnsupportedEncodingException { + ApplicationConfig instance = new ApplicationConfig(); + instance.parse(); + instance.setOption("toto", "tata"); + + // get content of printConfig + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + try { + instance.printConfig(ps); + } finally { + ps.close(); + } + String content = baos.toString("UTF-8"); + + if (log.isDebugEnabled()) { + log.debug("printConfig = " + content); + } + + Assert.assertTrue(content.indexOf("toto=tata") > 0); + } + + @Test + public void getOptionAsLocale() { + Locale expected; + Locale actual; + + ApplicationConfig instance = new ApplicationConfig(); + + // test null locale + actual = instance.getOptionAsLocale("toto"); + Assert.assertNull(actual); + + // test not null locale + instance.setOption("toto", "fr"); + + expected = Locale.FRENCH; + actual = instance.getOptionAsLocale("toto"); + Assert.assertNotNull(actual); + Assert.assertEquals(expected, actual); + } + + @Test + public void getOptionAsVersion() { + Version expected; + Version actual; + + ApplicationConfig instance = new ApplicationConfig(); + + // test null version + actual = instance.getOptionAsVersion("toto"); + Assert.assertNull(actual); + + // not null version + instance.setOption("toto", "1.2"); + expected = VersionUtil.valueOf("1.2"); + actual = instance.getOptionAsVersion("toto"); + Assert.assertNotNull(actual); + Assert.assertEquals(expected, actual); + } + + @Test + public void getOptionAsLong() { + ApplicationConfig instance = new ApplicationConfig(); + + // test long is not null + long actual = instance.getOptionAsLong("toto"); + Assert.assertNotNull(actual); + Assert.assertEquals(0l, actual); + + // not null version + long expected = System.currentTimeMillis(); + instance.setOption("toto", "" + expected); + actual = instance.getOptionAsLong("toto"); + Assert.assertEquals(expected, actual); + } + + @Test + public void getOsName() throws Exception { + ApplicationConfig config = new ApplicationConfig(); + config.parse(); + String v = config.getOsName(); + Assert.assertTrue(StringUtils.isNotBlank(v)); + if (log.isInfoEnabled()) { + log.info("os.name: " + v); + } + } + + @Test + public void getOsArch() throws Exception { + ApplicationConfig config = new ApplicationConfig(); + config.parse(); + String v = config.getOsArch(); + Assert.assertTrue(StringUtils.isNotBlank(v)); + if (log.isInfoEnabled()) { + log.info("os.arch: " + v); + } + } + + protected Properties loadPropertyFile(File file) throws IOException { + FileInputStream inStream; + + inStream = FileUtils.openInputStream(file); + try { + Properties p = new Properties(); + p.load(inStream); + inStream.close(); + return p; + } finally { + IOUtils.closeQuietly(inStream); + } + } + +} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..c6b513a --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,33 @@ +### +# #%L +# Nuiton Config +# $Id$ +# $HeadURL$ +# %% +# Copyright (C) 2011 - 2013 CodeLutin, Tony Chemit +# %% +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Lesser Public License for more details. +# +# You should have received a copy of the GNU General Lesser Public +# License along with this program. If not, see +# <http://www.gnu.org/licenses/lgpl-3.0.html>. +# #L% +### +# Global logging configuration +log4j.rootLogger=ERROR, stdout + +# Console output... +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) %M - %m%n + +# package level +log4j.logger.org.nuiton.config=INFO -- To stop receiving notification emails like this one, please contact nuiton.org SCM administrator <admin+scm@nuiton.org>.
participants (1)
-
nuiton.org scm