This is an automated email from the git hooks/post-receive script. New commit to annotated tag nuitonvconfig-3.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>.