Index: lutinutil/src/java/org/codelutin/option/OptionAction.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/OptionAction.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/OptionAction.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,69 @@ +package org.codelutin.option; + +/** + * A simple empty action for an option. + *

+ * Use method {@link #doRun(Option[])} to launch action for the given options. + *

+ * This method offers some hooks to use for controling running : + *

+ * {@link #beforeAll(Option[])} : no {@link #option} is set, before any doRun. + *

+ * {@link #beforeRun()} : an {@link #option} has been set in action, just before {@link #run()} + *

+ * {@link #afterRun()} : just after {@link #run()}, the current {@link #option} is still available. + *

+ * {@link #afterAll(Option[])} : no more {@link #option} is set, after all doRun. + * + * @author chemit + */ + +public abstract class OptionAction { + /** + * Method to implement logic of the action for the current option. + *

+ * This method will never by directly called, you have to use + * {@link #doRun(Option[])} method instead which offers some hooks to control actions. + * + * @throws Exception if any problem while action + */ + protected abstract void run() throws Exception; + + /** the parser used to obtain options */ + protected P parser; + + /** the current option to be used for running. */ + protected O option; + + protected OptionAction(P parser) { + this.parser = parser; + } + + public void doRun(O... options) throws Exception { + beforeAll(options); + for (O option : options) { + this.option = option; + beforeRun(); + run(); + afterRun(); + this.option = null; + } + afterAll(options); + } + + protected void beforeAll(O... options) { + } + + protected void beforeRun() { + } + + protected void afterRun() { + } + + protected void afterAll(O... options) { + } + + protected P getParser() { + return parser; + } +} Index: lutinutil/src/java/org/codelutin/option/OptionParserContexts.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/OptionParserContexts.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/OptionParserContexts.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,626 @@ +/* +* ##% Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.option; + +import org.apache.commons.beanutils.ConvertUtils; +import org.codelutin.option.def.ArgumentDefinition; +import org.codelutin.option.def.ArgumentType; +import org.codelutin.option.def.ArgumentValueType; +import org.codelutin.option.def.GroupDefinition; +import org.codelutin.option.def.OptionDefinition; +import org.codelutin.util.CardinalityHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * La classe contenant tous les contextes de parsing pour le parseur {@link OptionParser} + * + * @author tony + */ +public class OptionParserContexts { + + public static ArgumentDefinition findArgumentByTypeAndKey(ArgumentType type, String key, ArgumentDefinition... defs) { + for (ArgumentDefinition def : defs) { + if (def.getType() == type && def.getKey().equals(key)) { + return def; + } + } + return null; + } + + public static ArgumentDefinition findArgumentByTypeAndValueType(ArgumentType type, ArgumentValueType valueType, ArgumentDefinition... defs) { + for (ArgumentDefinition def : defs) { + if (def.getType() == type && def.getValueType() == valueType) { + return def; + } + } + return null; + } + + public static List getOptionalGroups(List groups) { + List result = new ArrayList(); + for (GroupDefinition group : groups) { + if (!group.isMandatory()) { + result.add(group); + } + } + return result; + } + + public static GroupDefinition getNextMandatoryGroup(List groups) { + for (GroupDefinition group : groups) { + if (group.isMandatory()) { + return group; + } + } + return null; + } + + /** + * le context de base à utiliser dans le parseur d'options {@link OptionParser} + * + * @author chemit + */ + abstract static class AbstractOptionParserContext

, S extends ParserUtil.AbstractParserContext> extends ParserUtil.AbstractParserContext { + protected AbstractOptionParserContext(P parent, boolean withSons) { + super(parent, withSons); + } + + protected abstract Object instanciate(); + + protected ParserException newError(String content, Exception e) { + ParserException e1; + if (e == null) { + e1 = new ParserException(content, this); + } else if (e instanceof ParserException) { + // propage exception + e1 = (ParserException) e; + } else { + e1 = new ParserException(content, e, this); + } + return e1; + } + } + + /** + * Cette classe contient les contextes d'options détectés pendand le parsing. + *

+ * Elle est de visibilité package, car elle n'a pas à être utilisée ailleurs. + * + * @author chemit + */ + static class ParserContext extends AbstractOptionParserContext { + + /** le dictionnaire des definitions d'options indexées par leurs alias */ + protected final Map indexAlias; + + /** order on options by their names */ + protected final List definitionsIndex; + + /** les occurences des options trouvées (est mis à jour dans la méthode */ + final int[] optionFounds; + + /** les positions des args non utilisés */ + final List unusedArguments; + + /** les options disponibles pendant la détection des args */ + final List availabledOptions; + + int pos = 0; + private OptionParser parser; + + protected ParserContext(OptionParser parser) { + super(null, true); + this.parser = parser; + optionFounds = new int[parser.getOptionKeys().size()]; + unusedArguments = new ArrayList(); + availabledOptions = new ArrayList(); + this.definitionsIndex = new ArrayList(); + this.indexAlias = new TreeMap(); + for (OptionKey optionKey : parser.getOptionKeys()) { + OptionDefinition definition = optionKey.getDefinition(); + availabledOptions.add(definition); + definitionsIndex.add(definition.getKey()); + // keep all alias of the option + for (String alias : definition.getAlias()) { + indexAlias.put(alias, definition); + } + } + } + + @Override + protected void clear() { + super.clear(); + unusedArguments.clear(); + availabledOptions.clear(); + definitionsIndex.clear(); + indexAlias.clear(); + pos = 0; + } + + /** + * Detecte les options et leurs arguments, à partir de la suite des + * arguments donnés et construit la liste des parser d'options. + *

+ * Détecte les contextes d'options à analyser. + * Une fois un contexte d'option détecté (un alias et les arguments + * jusqu'au prochain alias), on instancie un {@link OptionContext}, + * et on lance la méthode + * {@link #addSon(org.codelutin.option.ParserUtil.AbstractParserContext)}. + *

+ * Cette méthode suit l'algorthime générique, on surcharge + * {@link #preAddSonHook(OptionContext)} , pour lancer l'analyse de + * l'option, et la méthode {@link #postAddSonHook(OptionContext)} + * pour mettre à jour la table d'occurrences des options trouvées si + * l'option est valide. + * + * @param args la liste des arguments à scanner + */ + protected void detectOptions(String... args) { + int length = args.length; + + List argsForOption = new ArrayList(); + // detect all arguments before first alias + fillArgumentsForOption(length, argsForOption, true, args); + + while (pos < length) { + // detect a new option with his arguments + // coming here means pos is exactly an index of an option's alias + int start = pos; + String alias = args[pos++]; + // detect all arguments before next alias + argsForOption.clear(); + fillArgumentsForOption(length, argsForOption, false, args); + + // a fully option was detected + log.info(" (pos " + start + ") " + alias + " " + argsForOption); + // find definition from alias + OptionDefinition definition = indexAlias.get(alias); + if (definition == null) { + //fatal error : unknown option + addError(org.codelutin.i18n.I18n._("lutinutil.parser.unknown.option", alias, start)); + continue; + } + if (!availabledOptions.contains(definition)) { + // fatal error : the option is not available + addError(org.codelutin.i18n.I18n._("lutinutil.parser.unavailable.option", alias, start)); + continue; + } + // create a OptionContext + OptionContext optionContext = new OptionContext(parser, this, definition, argsForOption.toArray(new String[argsForOption.size()]), start, alias); + // try to add the parser + addSon(optionContext); + } + // check if we did not miss a mandatory option + for (OptionDefinition definition : availabledOptions) { + if (definition.isMandatory()) { + int optionPos = definitionsIndex.indexOf(definition.getKey()); + if (optionFounds[optionPos] == 0) { + addError(org.codelutin.i18n.I18n._("lutinutil.error.parser.unfound.mandatory.option", definition)); + } + } + } + // check the min property on each found option + for (OptionContext context : contexts) { + int optionPos = definitionsIndex.indexOf(context.definition.getKey()); + int nbOccur = optionFounds[optionPos]; + int min = context.definition.getMin(); + if (nbOccur < min) { + addError(org.codelutin.i18n.I18n._("lutinutil.error.parser.too.less.option.found", context.definition, nbOccur, min)); + } + } + } + + protected Map> instanciate() { + Map> acceptedOptions = new LinkedHashMap>(); + for (OptionContext context : contexts) { + Option option = context.instanciate(); + OptionKey key = parser.getOptionKey(context.definition.getKey()); + List

+ * If a {@link #lastArgument} exisits, it means we previously matched + * a repeatable argument, so we will try to match it before. + *

+ * If mandatory and not found will add an error to the Option parser. + * + * @param argument the argument from command line + * @param pos the position of argument in option's arguments + * @return the parser of argument found or null if not found + */ + protected ArgumentContext detectArgument(String argument, int pos) { + ArgumentContext result = null; + if (lastArgument != null) { + // the lastArgument is still available + // try to find a match argument with this definition + result = detectArgument(lastGroup, lastArgument, argument, pos); + + if (CardinalityHelper.isMandatory(lastArgument.getMin()) && result == null) { + addError(org.codelutin.i18n.I18n._("lutinutil.error.parser.unfound.mandatory.argument", lastArgument, lastGroup, argument)); + return null; + } + // the lastGroup is no more available + availabledGroups.remove(lastGroup); + } + if (availabledGroups.isEmpty()) { + // no more group to match + return null; + } + // always try to match a mandatory + GroupDefinition group = getNextMandatoryGroup(availabledGroups); + if (group != null) { + // must match the argument in this group + result = detectArgument(group, argument, pos); + if (result == null) { + addError(org.codelutin.i18n.I18n._("lutinutil.error.parser.unfound.mandatory.group", group, argument)); + } + return result; + } + // try to match in one of the optional groups + List optionalGroups = getOptionalGroups(availabledGroups); + while (!optionalGroups.isEmpty()) { + // try next optional group + group = optionalGroups.get(0); + result = detectArgument(group, argument, pos); + if (result != null) { + break; + } + } + return result; + } + + /** + * Detect a argument in a group definition. + * + * @param group the definition of group to use + * @param argument the argument from command line + * @param pos the position of argument inf option's arguments + * @return ther matched argument parser or null if not found + */ + protected ArgumentContext detectArgument(GroupDefinition group, String argument, int pos) { + ArgumentDefinition def = detectArgumentDefinition(argument, group); + if (def == null) { + return null; + } + ArgumentContext result; + result = detectArgument(group, def, argument, pos); + return result; + } + + /** + * Detect a argument in a argument definition. + * + * @param group the definition of group to use + * @param definition the definition of argument to use + * @param argument the argument from command line + * @param pos the position of argument inf option's arguments + * @return ther matched argument parser or null if not found + */ + protected ArgumentContext detectArgument(GroupDefinition group, ArgumentDefinition definition, String argument, int pos) { + String valueStr = argument; + ArgumentValueType valueType = definition.getValueType(); + switch (definition.getType()) { + case constant: + if (argument.trim().equalsIgnoreCase(definition.getKey())) { + // we found a matching argument + return new ArgumentContext(this, definition, group, argument, valueStr, commandLinePosition + 1 + pos, pos); + } + break; + case namedAndValued: + int index = argument.trim().indexOf('='); + if (index == -1 || !argument.substring(0, index).equals(definition.getKey())) { + break; + } + valueStr = argument.substring(index + 1); + case valued: + boolean matchType = valueType.matchType(argument); + if (matchType) { + // convert to matching type from argument definition value type + Object value = ConvertUtils.convert(valueStr, valueType.getClazz()); + return new ArgumentContext(this, definition, group, argument, value, commandLinePosition + 1 + pos, pos); + } + break; + } + return null; + } + + protected ArgumentDefinition detectArgumentDefinition(String argument, GroupDefinition currentGroup) { + boolean named = argument.indexOf('=') > -1; + ArgumentDefinition result; + ArgumentType type; + ArgumentValueType valueType; + ArgumentDefinition[] defs = currentGroup.getArguments(); + if (named) { + String key = argument.substring(0, argument.indexOf('=')); + type = ArgumentType.namedAndValued; + // find a named and valued + result = findArgumentByTypeAndKey(type, key, defs); + log.info(argument + " : " + result); + if (result == null) { + //fatal error : unknown namedAndValued argument + addError(org.codelutin.i18n.I18n._("lutinutil.error.parser.valued.argument.unfound", key, Arrays.asList(currentGroup))); + return null; + } + return result; + } + // try a constant + type = ArgumentType.constant; + result = findArgumentByTypeAndKey(type, argument, defs); + if (result != null) { + log.info(argument + " : " + result); + return result; + } + + // try a valued + type = ArgumentType.valued; + valueType = ArgumentValueType.findTypeFromArgument(argument); + result = findArgumentByTypeAndValueType(type, valueType, defs); + + if (result == null && valueType == ArgumentValueType.FILE) { + // special case : wants a newFile type, but file already + // exists (so it was detected as a file argument) + // since we arrive here, it means no file argument + // try with a _newFile + result = findArgumentByTypeAndValueType(type, ArgumentValueType.NEWFILE, defs); + } + if (result == null && valueType == ArgumentValueType.NEWFILE) { + // we didn't find a _newFile, it could be also a string + result = findArgumentByTypeAndValueType(type, ArgumentValueType.STRING, defs); + } + log.info(argument + " : " + result); + return result; + } + + @Override + public String toString() { + return super.toString() + " argument:" + Arrays.toString(parser.arguments) + ", definition:" + definition + ", commandlinepos:" + commandLinePosition; + } + } + + /** @author tony */ + static class ArgumentContext extends AbstractOptionParserContext { + + /** la définition de l'argument */ + final ArgumentDefinition definition; + /** la définition du group de l'argument */ + final GroupDefinition groupDefinition; + /** la position dans la ligne de commande */ + final int commandLinePosition; + /** la position dans les arguments de l'option */ + final int pos; + /** l'argument correspondant dans la ligne de commande */ + final String argument; + /** la valeur typée de l'argument */ + final Object value; + + protected ArgumentContext(OptionContext parent, + ArgumentDefinition definition, + GroupDefinition groupDefinition, + String argument, Object value, + int commandLinePosition, int pos) { + super(parent, false); + this.argument = argument; + this.definition = definition; + this.commandLinePosition = commandLinePosition; + this.value = value; + this.pos = pos; + this.groupDefinition = groupDefinition; + log.info(this); + } + + public Argument instanciate() { + Argument argument; + argument = new Argument( + definition.getKey(), + definition.getType(), + definition.getValueType(), + pos, + this.argument, + value + ); + return argument; + } + + @Override + public String toString() { + return super.toString() + " argument:" + argument + ", definition:" + definition + ", pos:" + pos + ", commandlinepos:" + commandLinePosition; + } + } + +} Index: lutinutil/src/java/org/codelutin/option/package.html diff -u /dev/null lutinutil/src/java/org/codelutin/option/package.html:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/package.html Sun Dec 30 22:50:46 2007 @@ -0,0 +1,35 @@ + + +

Lutin util option

+Ce paquetage contient l'ensemble des classes nécessaires à la gestion des +options et des configurations au démarrage d'une application. +

+On dispose d'un parseur OptionParser qui permet à partir de +définitions +d'options et de configs connues, de parser les arguments de démarrage d'une +application java et d'instancier les Option et Config. +

+Le paquetage def contient les classes de définitions des options et +des configs, ainsi qu'une parseur de définition et d'un constructeur de +définitions par programmation. +

+Le modèle d'une option est le suivant : +

    +
  • Option : représente une option détectée par le parsuer + avec ses arguments +
  • +
  • OptionKey : représente une clef d'option
  • +
  • Argument : représente un argument d'une option
  • +
+Le modèle d'une config est le suivant : +
    +
  • Config : représente une configuration d'application
  • +
  • ConfigKey : représente une clef de configuration
  • +
  • ConfigPropertyKey : représente une clef de propriété de + configuration +
  • +
+ +

+ + \ No newline at end of file Index: lutinutil/src/java/org/codelutin/option/OptionKey.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/OptionKey.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/OptionKey.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,163 @@ +/* +* ##% Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.option; + +import org.codelutin.option.def.OptionDefinition; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.List; + +/** + * Une classe pour représenter une clef d'option typée. + *

+ * Utiliser dans le parseur d'option {@link OptionParser} pour + *

+ * pouvoir accéder de manière typée à une option ou à une action d'option. + *

+ * A l'initialisation d'une clef d'option typée, on ne connait pas la classe + * concrete d'implantation de l'OptionAction, celle-ci devra être positionné après + * l'initialisation générique du parseur (utiliser la méthode {@link #setActionClass(Class)} + * pour positionner la classe contrête d'OptionAction). + *

+ * L'action liée est instanciée une seule fois, pour lancer une action utilisez + * la méthode {@link #doAction(OptionParser , Option[])}. + * + * @author chemit + */ +public class OptionKey> { + + /** la clef non typée de l'option */ + final protected String optionKey; + + /** la description traduite de l'option */ + final protected String description; + + /** la définition de l'option */ + final protected OptionDefinition definition; + + /** la classe d'implantation de l'optin liée */ + final protected Class optionClass; + + /** la classe abstraite de l'action de l'option liée */ + final protected Class abstractActionClass; + + /** le constructeur d'option à utilisé */ + protected Constructor optionConstructor; + + /** la classe concrete de l'action de l'option liée (calculé au runtime) */ + protected Class actionClass; + + /** l'unique instance de l'action liée */ + protected A action; + + // constructeur protégé, pour empécher les instanciations multiples + protected OptionKey(String key, Class optionClass, Class abstractActionClass, String description, OptionDefinition definition) { + this.optionKey = key; + this.optionClass = optionClass; + this.abstractActionClass = abstractActionClass; + this.description = description; + this.definition = definition; + if (!Modifier.isAbstract(abstractActionClass.getModifiers())) { + // we have a concrete action class + actionClass = abstractActionClass; + } + } + + public String getOptionKey() { + return optionKey; + } + + public Class getOptionClass() { + return optionClass; + } + + public Class getAbstractActionClass() { + return abstractActionClass; + } + + public Class getActionClass() { + return actionClass; + } + + public void setActionClass(Class actionClass) { + this.actionClass = actionClass; + } + + public String getDescription() { + return description; + } + + public OptionDefinition getDefinition() { + return definition; + } + + /** + * Instanciate a new option + * + * @param alias the option alias + * @param args the arguments of the option + * @return the instanciated option + */ + public O newOption(String alias, List args) { + try { + if (optionConstructor == null) { + optionConstructor = optionClass.getConstructor(String.class, Argument[].class); + } + O result; + result = optionConstructor.newInstance(alias, args.toArray(new Argument[args.size()])); + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Launch action for the given parser and options + * + * @param parser the used parser + * @param options the options to fire + * @throws Exception if any problem + */ + public void doAction(P parser, O... options) throws Exception { + if (options.length > 0) { + getAction(parser).doRun(options); + } + } + + /** + * Get the shared action linked with this key. + * + * @param parser the parser used + * @return the share action's instance + */ + public synchronized A getAction(P parser) { + if (action == null) { + if (actionClass == null) { + throw new IllegalStateException("no concrete action found for " + abstractActionClass); + } + try { + action = actionClass.getConstructor(parser.getClass()).newInstance(parser); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return action; + } +} Index: lutinutil/src/java/org/codelutin/option/OptionParser.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/OptionParser.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/OptionParser.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,424 @@ +/* *##% +* Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Cédric Pineau, Benjamin Poussin, +* +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*##%*/ + +package org.codelutin.option; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import static org.codelutin.i18n.I18n._; +import org.codelutin.option.OptionParserContexts.ParserContext; +import org.codelutin.option.def.OptionDefinition; +import org.codelutin.option.def.OptionDefinitionBuilder; +import org.codelutin.util.ArrayUtil; +import org.codelutin.util.ReflectUtil; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Parser abstrait d'options de la ligne de commande. + *

+ * TODO a finir. + *

+ * Pour utiliser le parseur , on invoque la méthode {@link #doParse(String[])} + * pour effectuer le parsing des arguments passés. + *

+ * Si on veut utiliser les actions des options (OptionAction), il faut avant + * tout parsing, enregistrer les classes concretes d'implantation de ces actions. + *

+ * Cela se fait en utilisant la méthode {@link #registerActions(Class[])}, les + * implantations doivent être présentes en tant qu'inner class d'une ou plusieurs + * classes container. + */ + +public abstract class OptionParser { + + /** logger non statique pour épouser la catégorie de l'implantation */ + protected final Log log = LogFactory.getLog(getClass()); + + /** + * Cette méthode encapsule l'appel à la méthode statique buildDefinition + * générée dans le parseur de classe targetClass et retourne + * les définitions d'options obtenues par le builder utilisé. + * + * @param targetClass la classe d'implantation du parseur + * @return les définitions d'options construites + * @see OptionDefinitionBuilder + */ + protected static OptionDefinition[] buildDefinitions(Class targetClass) { + OptionDefinitionBuilder builder = new OptionDefinitionBuilder(); + try { + Method method = targetClass.getDeclaredMethod("buildDefinitions", OptionDefinitionBuilder.class); + method.setAccessible(true); + method.invoke(targetClass, builder); + OptionDefinition[] result = builder.getDefinition(); + builder.clear(); + return result; + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e); + } + } + } + + /** les clefs d'options connues par le parseur */ + protected final List optionKeys; + + /** les clefs d'options connues par le parseur */ + protected final List configKeys; + + /** les options trouvées et valides, indexées par leur clef typée dans le dernier parsing */ + protected Map> options; + + protected Map configs; + + /** les arguments non utilisés, indexés par leur position dans du dernier parsing */ + protected Map unusedArguments; + + /** les erreurs rencontrées dans du dernier parsing */ + protected ParserException[] errors; + + /** les arguments utilisés dans le dernier parsing */ + protected String[] arguments; + + public OptionParser() throws IllegalArgumentException { + optionKeys = Collections.unmodifiableList(ReflectUtil.getConstants(getClass(), OptionKey.class)); + configKeys = Collections.unmodifiableList(ReflectUtil.getConstants(getClass(), ConfigKey.class)); + } + + /** + * Enregistre les implantations d'action à partir de plusieurs classes + * contenant des classes imbriquées d'implantation d'OptionAction. + *

+ * Cette méthode doit être invoquée avant toute demande d'OptionAction + * après parsing. + * + * @param clazz la liste des classes contenant des implantations d'OptionAction + */ + public void registerActions(Class... clazz) { + for (Class aClass : clazz) { + registerActions(aClass); + } + } + + /** + * Enregistre les implantations d'action à partir de plusieurs classes + * contenant des classes imbriquées d'implantation d'OptionAction. + *

+ * Cette méthode doit être invoquée avant toute demande d'OptionAction + * après parsing. + * + * @param clazz la liste des classes contenant des implantations d'OptionAction + */ + public void registerConfigs(Class... clazz) { + for (Class aClass : clazz) { + registerConfig(aClass); + } + } + + public List getConfigKeys() { + return configKeys; + } + + public List getOptionKeys() { + return optionKeys; + } + + /** + * Launch parsing of some arguments. + * + * @param args arguments to parse + * @throws ParserException if any problem while parsing + * @throws java.io.IOException if io problems with writers + */ + public void doParse(String... args) throws ParserException, IOException { + cleanResult(); + arguments = args; + unusedArguments = new HashMap(); + ParserContext context = new ParserContext(this); + if (args.length == 0) { + errors = new ParserException[0]; + return; + } + // detect options + context.detectOptions(args); + + // instanciate real options from safe contexts + options = context.instanciate(); + + // transfert unused arguments + for (Integer unusedArgument : context.unusedArguments) { + unusedArguments.put(unusedArgument, args[unusedArgument]); + } + + // transfert errors from parser parser + errors = context.getErrors().toArray(new ParserException[context.getNbErrors()]); + + // clean parser + context.clear(); + } + + /** + * @param key la clef typée des options à rechercher + * @return true s'il existe des options trouvées lors du dernier + * parsing correspondant à la clef donnée. + */ + public boolean isOptionEnabled(OptionKey key) { + List list = options.get(key); + return list != null && !list.isEmpty(); + } + + /** @return le nombre d'options valides trouvées lors du dernier parsing */ + public int getNbOptions() { + int result = 0; + if (options != null) { + for (List

+ * L'ordre d'exécution est celui des clefs données. + * + * @param keys les clefs des options à utiliser + * @throws Exception si problème dans l'exécution des actions + */ + @SuppressWarnings({"unchecked"}) + public void doActions(OptionKey... keys) throws Exception { + for (OptionKey key : keys) { + key.doAction(this, getOptions(key)); + } + } + + /** + * Obtenir les options trouvées lors du dernier parsing pour la clef donnée. + * + * @param key la clef typée des options à rechercher + * @return une table des options trouvées ou un tableau vide + */ + public O[] getOptions(OptionKey key) { + return ArrayUtil.toArray(options.get(key), key.getOptionClass()); + } + + /** + * Obtenir la première des option de cette clef trouvée lors du dernier parsing + * + * @param key la clef typée de l'option à rechercher + * @return la premier option trouvée ou null + */ + public O getOption(OptionKey key) { + return !isOptionEnabled(key) ? null : getOptions(key)[0]; + } + + public C getConfig(ConfigKey key) { + return key.getConfig(); + } + + /** @return true 0; + } + + /** @return les erreurs rencontrées lors du dernier parsing */ + public ParserException[] getErrors() { + return errors; + } + + /** @return la liste des arguments non utilisés lors du dernier parsing */ + public String[] getUnusedArguments() { + return unusedArguments.values().toArray(new String[unusedArguments.size()]); + } + + /** @return les arguments en entrée du dernier parsing */ + public String[] getArguments() { + return arguments; + } + + public void printUsage(Writer w, String name) throws IOException { + String head = _("lutinutil.parserdef.printUsage.head", name); + String prefixOption = _("lutinutil.parserdef.printUsage.options.head", name); + String prefixConfig = _("lutinutil.parserdef.printUsage.configs.head", name); + String prefix = "\n"; + ParserUtil.toString(getClass(), w, head, prefix, prefixOption, prefixConfig); + } + + /** + * Imprime dans un writer les erreurs rencontrées pendant l'analyse. + * + * @param w le writer à utiliser + * @throws IOException si problèmes d'écriture dans le writer + */ + public void printErrors(Writer w) throws IOException { + if (!hasFailed() && (unusedArguments == null || unusedArguments.isEmpty())) { + return; + } + w.append(_("lutinutil.parserdef.printError", Arrays.toString(arguments))).append('\n'); + if (!unusedArguments.isEmpty()) { + Set unsedPos = unusedArguments.keySet(); + w.append(_("lutinutil.parserdef.printError.unused.head", unsedPos.size())); + Iterator itr = unsedPos.iterator(); + for (int i = 0, j = unsedPos.size(); i < j; i++) { + int pos = itr.next(); + w.append(_("lutinutil.parserdef.printError.unused", i + 1, j, pos)); + w.append(unusedArguments.get(i)); + } + } + if (hasFailed()) { + w.append(_("lutinutil.parserdef.printError.head", errors.length)); + for (int i = 0, j = errors.length; i < j; i++) { + w.append(_("lutinutil.parserdef.printError.error", i + 1, j)); + w.append(errors[i].toString()); + } + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + cleanResult(); + } + + /** + * Obtient une clef typée d'option à partir de sa clef non typée. + * + * @param key la clef non typée + * @return la clef typée ou null si non trouvée. + */ + @SuppressWarnings({"unchecked"}) + protected OptionKey getOptionKey(String key) { + for (OptionKey optionKey : optionKeys) { + if (optionKey.getOptionKey().equals(key)) { + return optionKey; + } + } + return null; + } + + /** + * Obtient une clef typée d'une config à partir de sa clef non typée. + * + * @param key la clef non typée + * @return la clef typée ou null si non trouvée. + */ + @SuppressWarnings({"unchecked"}) + protected ConfigKey getConfigKey(String key) { + for (ConfigKey optionKey : configKeys) { + if (optionKey.getCategory().equals(key)) { + return optionKey; + } + } + return null; + } + + /** nettoye les résultat du parseur (utilisé avant tout parsing) */ + protected void cleanResult() { + if (options != null) { + options.clear(); + options = null; + } + if (unusedArguments != null) { + unusedArguments.clear(); + unusedArguments = null; + } + arguments = null; + errors = null; + } + + /** + * Enregistre les implantations d'action à partir d'une classe contenant + * des classes imbriquées d'implantation d'OptionAction. + * + * @param clazz la classe contenant des implantations d'OptionAction + */ + protected void registerActions(Class clazz) { + // recherche des inner classes + Class[] classes = clazz.getClasses(); + for (Class aClass : classes) { + if (OptionAction.class.isAssignableFrom(aClass)) { + registerAction(aClass); + } + } + } + + @SuppressWarnings({"unchecked"}) + protected void registerAction(Class aClass) { + for (OptionKey key : optionKeys) { + if (key.getAbstractActionClass().isAssignableFrom(aClass)) { + // found a matching key + key.setActionClass(aClass); + } + } + } + + /** + * Enregistre les implantations de config à partir d'une classe contenant + * des classes imbriquées d'implantation Config. + * + * @param clazz la classe contenant des implantations de Config + */ + protected void registerConfigs(Class clazz) { + // recherche des inner classes + Class[] classes = clazz.getClasses(); + for (Class aClass : classes) { + if (Config.class.isAssignableFrom(aClass)) { + registerConfig(aClass); + } + } + } + + @SuppressWarnings({"unchecked"}) + protected void registerConfig(Class aClass) { + for (ConfigKey key : configKeys) { + if (key.getAbstractConfigClass().isAssignableFrom(aClass)) { + // found a matching key + key.setConfigClass(aClass); + } + } + } + +} + + + + Index: lutinutil/src/java/org/codelutin/option/Option.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/Option.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/Option.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,140 @@ +/** + * ##% Copyright (C) 2002, 2003 Code Lutin This program is free software; you + * can redistribute it and/or modify it under the terms of the GNU General + * Public License as published by the Free Software Foundation; either version 2 + * 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 Public License for more details. You + * should have received a copy of the GNU General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 59 Temple Place + * - Suite 330, Boston, MA 02111-1307, USA. ##%* + */ + +/** + * Option.java Created: 22 août 2003 + * + *@author Benjamin Poussin + * + * Copyright Code Lutin + *@version $Revision: 1.1 $ Mise a jour: $Date: 2007-12-30 22:50:46 $ par : $Author: tchemit $ + */ + +package org.codelutin.option; + +import org.codelutin.option.def.ArgumentType; +import static org.codelutin.option.def.ArgumentType.constant; +import static org.codelutin.option.def.ArgumentType.namedAndValued; +import static org.codelutin.option.def.ArgumentType.valued; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Classe représentant une option trouvée lors du parsing de + * la ligne de commande par le parseur d'options {@link org.codelutin.option.OptionParser}. + * + * @author chemit + * @created 15 nov 2007 + */ +public class Option { // Option + + /** L'alias utilisé sur la ligne de commande pour cette option */ + protected final String usedAlias; + + /** la liste des arguments consommés pour cette option */ + protected final Argument[] arguments; + + /** + * @param usedAlias used alias + * @param arguments arguments of the option + */ + public Option(String usedAlias, Argument... arguments) { + this.usedAlias = usedAlias; + this.arguments = arguments; + } + + /** @return the alias used to obtain this option */ + public String getUsedAlias() { + return usedAlias; + } + + /** + * Obtain the argument of type constant for a a given group of arguments. + * + * @param position position of group of argument to scan + * @return the key of the constant argument found, or null + */ + public String getConstantArgumentValue(int position) { + return filterValue(position, constant, String.class, null); + } + + /** + * Obtain the first value of the argument of type valued or namedAndValued for a a given + * group of arguments and a given value type. + * + * @param position position of group of argument to scan + * @param clazz type of argument's values + * @param key the key (only if named) + * @param named flag to say if named or not + * @return the first value of the valued argument found, or null + */ + protected T getValuedArgumentValue(int position, Class clazz, String key, boolean named) { + return named ? + filterValue(position, namedAndValued, clazz, key) : + filterValue(position, valued, clazz, null); + } + + /** + * Obtain all values of the argument of type valued or namedAndValued for a a given group of + * arguments and a given value type. + * + * @param position position of group of argument to scan + * @param clazz type of argument's values + * @param key the key (only if named) + * @param named flag to say if named or not + * @return all values of the valued argument found, or null + */ + protected T[] getValuedArgumentValues(int position, Class clazz, String key, boolean named) { + T[] result; + result = named ? + filterValues(position, valued, clazz, null) : + filterValues(position, namedAndValued, clazz, key); + return result; + } + + @SuppressWarnings({"unchecked"}) + protected T filterValue(int position, ArgumentType type, Class clazz, String key) { + for (Argument argument : arguments) { + if (argument.getPos() == position + && argument.getType() == type + && argument.getValueType().getClazz() == clazz + && (key == null || argument.getKey().equals(key))) { + return (T) argument.getValue(); + } + } + return null; + } + + @SuppressWarnings({"unchecked"}) + protected T[] filterValues(int position, ArgumentType type, Class clazz, String key) { + List result = new ArrayList(); + for (Argument argument : arguments) { + if (argument.getPos() == position + && argument.getType() == type + && argument.getValueType().getClazz() == clazz + && (key == null || argument.getKey().equals(key))) { + result.add((T) argument.getValue()); + } + } + return (T[]) Array.newInstance(clazz, result.size()); + } + + @Override + public String toString() { + return super.toString() + " (" + usedAlias + ")" + Arrays.toString(arguments); + } + +}// Option Index: lutinutil/src/java/org/codelutin/option/ParserUtil.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/ParserUtil.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/ParserUtil.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,194 @@ +/* +* ##% Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.option; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.codelutin.option.def.OptionDefinition; +import org.codelutin.util.ReflectUtil; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +/** + * Usefull methods for Parser + * + * @author chemit + */ + +public class ParserUtil { + + protected static String addTitle(String txt, char c, boolean headAndTail) { + StringBuilder writer = new StringBuilder(); + if (headAndTail) { + for (int i = 0, j = txt.length(); i < j; i++) { + writer.append(c); + } + writer.append('\n'); + } + writer.append(txt).append('\n'); + for (int i = 0, j = txt.length(); i < j; i++) { + writer.append(c); + } + return writer.toString(); + } + + public static void toString(Class parserClass, Writer w, String head, String prefix, String prefixOption, String prefixConfig) throws IOException { + try { + w.append(addTitle(head, '=', true)).append("\n\n"); + w.append(addTitle(prefixOption, '-', false)).append("\n"); + for (OptionKey key : ReflectUtil.getConstants(parserClass, OptionKey.class)) { + OptionDefinition definition = key.getDefinition(); + w.append(prefix).append(definition.toString()); + w.append("\n ").append(key.getDescription()).append("\n\n"); + } + List configKeys = ReflectUtil.getConstants(parserClass, ConfigKey.class); + if (configKeys.isEmpty()) { + return; + } + w.append(addTitle(prefixConfig, '-', false)).append("\n"); + for (ConfigKey key : configKeys) { + w.append("\n").append(addTitle(key.getDescription() + " (" + key.getCategory() + ")", '~', false)).append("\n"); + for (ConfigPropertyKey propertyKey : ReflectUtil.getConstants(key.getAbstractConfigClass(), ConfigPropertyKey.class)) { + w.append(prefix).append(propertyKey.getKey()); + w.append("\n ").append(propertyKey.getDescription()).append("\n\n"); + } + } + } finally { + if (w != null) { + w.flush(); + } + } + } + + /** + * Un contexte abstrait pour parseur (qui peut contenir un contexte parent + * et des contextes enfants) et qui gère les erreurs. + * + * @author chemit + */ + + public abstract static class AbstractParserContext

, S extends AbstractParserContext, E extends Exception> { + + protected abstract E newError(String content, Exception e); + + /** logger non statique pour être dans la bonne catégorie */ + protected final Log log = LogFactory.getLog(getClass()); + + /** le contexte parent (peut être null) */ + protected P parent; + + /** la liste des contextes enfants (peut être null) */ + protected final List contexts; + + /** flag pour marquer l'état du parser */ + protected boolean valid = true; + + /** list of errors found while parsing */ + protected List errors; + + protected AbstractParserContext(P parent, boolean withSons) { + this.parent = parent; + this.contexts = withSons ? new ArrayList() : null; + } + + public boolean isRoot() { + return parent == null; + } + + boolean hasChilds() { + return contexts != null; + } + + public void addSon(S context) { + preAddSonHook(context); + //TODO See if the test is ok... + if (context != null && context.valid) { + log.debug("[" + getClass().getSimpleName() + " : (" + context.valid + ")" + context + "]"); + contexts.add(context); + postAddSonHook(context); + } + } + + protected void preAddSonHook(S context) { + if (!hasChilds()) { + throw new RuntimeException("could not add a son to a leaf context"); + } + if (context == null || !context.valid) { + unvalidate(); + } + } + + public P getParent() { + return parent; + } + + protected void postAddSonHook(S context) { + } + + protected void clear() { + if (hasChilds()) { + for (S context : contexts) { + context.clear(); + } + } + //TODO on doit vider aussi les contexts ? + //contexts.clear(); + } + + public List getContexts() { + return contexts; + } + + public List getErrors() { + if (errors == null) { + errors = new ArrayList(); + } + return errors; + } + + public void unvalidate() { + this.valid = false; + if (!isRoot()) { + parent.unvalidate(); + } + } + + public int getNbErrors() { + return errors == null ? 0 : errors.size(); + } + + protected E addError(String content) { + return addError(content, null); + } + + protected E addError(String content, Exception e) { + E e1 = newError(content, e); + getErrors().add(e1); + if (!isRoot()) { + parent.addError(content, e1); + } + unvalidate(); + return e1; + } + } + +} Index: lutinutil/src/java/org/codelutin/option/Argument.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/Argument.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/Argument.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,105 @@ +/** + * ##% Copyright (C) 2002, 2007 Code Lutin This program is free software; you + * can redistribute it and/or modify it under the terms of the GNU General + * Public License as published by the Free Software Foundation; either version 2 + * 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 Public License for more details. You + * should have received a copy of the GNU General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 59 Temple Place + * - Suite 330, Boston, MA 02111-1307, USA. ##%* + */ + +package org.codelutin.option; + +import org.codelutin.option.def.ArgumentType; +import org.codelutin.option.def.ArgumentValueType; + +/** + * Cette classe abstraite définit le contrat à respecter pour un argument d'une + * option dans le parser de ligne de commande. + *

+ * La classe est typée par le type de retour de l'argument attendu {@link #value} + * + * @author chemit + */ +public class Argument { + + /** la definition de l'argument */ + //protected ArgumentDefinition definition; + /** l'argument de la ligne de commande qui a été consommé par cet argument */ + protected String arg; + + /** la valeur typée de l'argument de la ligne de commande utilisé */ + protected T value; + + /** + * la position de l'argument dans l'option si c'est un argument + * d'un groupe obligatoire, sinon -1 + */ + protected int pos; + + protected String key; + protected ArgumentValueType valueType; + protected ArgumentType type; + + public Argument(String key, ArgumentType type, ArgumentValueType valueType, int pos, String arg, T value) { + this.type = type; + this.valueType = valueType; + this.arg = arg; + this.value = value; + this.pos = pos; + this.key = key; + //this.definition = definition; + } + + /** @return l'argument consommé de la ligne de commande */ + public String getArg() { + return arg; + } + + /** + * @return la valeur typée correspondant à l'argument de la ligne de + * commande + */ + public T getValue() { + return value; + } + + /** + * @return la position de l'argument dans l'option si c'est un argument + * d'un groupe obligatiore, sinon -1. + */ + public int getPos() { + return pos; + } + + public String getKey() { + return key; + } + + @Override + public String toString() { + StringBuilder sb; + if (pos == -1) { + sb = new StringBuilder("["); + sb.append("key:").append(key).append(", arg:").append(arg).append(", value:").append(value); + sb.append(']'); + } else { + sb = new StringBuilder("<"); + sb.append("key:").append(key).append(", arg:").append(arg).append(", value:").append(value); + sb.append(", pos:").append(pos); + sb.append('>'); + } + return sb.toString(); + } + + public ArgumentType getType() { + return type; + } + + public ArgumentValueType getValueType() { + return valueType; + } +} Index: lutinutil/src/java/org/codelutin/option/ConfigPropertyKey.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/ConfigPropertyKey.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/ConfigPropertyKey.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,140 @@ +/* +* ##% Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.option; + +import org.apache.commons.beanutils.Converter; +import org.codelutin.option.def.ConfigPropertyModifier; +import static org.codelutin.option.def.ConfigPropertyModifier.*; +import org.codelutin.util.ConverterUtil; + +import java.lang.reflect.Modifier; + +/** + * Cette classe définie la clef d'une propriété de configuration, i.e la clef + * associée à une propriété typée dans la configuration. + *

+ * On retrouve ici toutes les caractéristiques de la propriété : + *

+ * son type {@link #type}. + *

+ * sa clef non typée {@link #key}. + *

+ * ses modifiers {@link #modifiers} (on utilise le mécanisme des modifiers + * de la reflection java {@link Modifier}). + *

+ * sa valeur par défaut {@link #defaultValue}. + *

+ * On définit ici aussi des méthodes isXXX pour connaitre le comportement + * de la propriété (static, final, transient...) + *

+ * Comme pour les autres types de clefs ({@link ConfigKey}, {@link OptionKey}), + * on dispose de méthodes d'instanciation par factory : + *

+ * {@link #newConfigPropertyKey(String, Class, int,String)} pour une propriété sans valeur par défaut + *

+ * {@link #newConfigPropertyKey(String, Class, int,String,String)} pour une propriété avec valeur par défaut + * + * @author chemit + */ +public class ConfigPropertyKey { + + public static ConfigPropertyKey newConfigPropertyKey(String key, Class type, int modifiers, String description) { + return newConfigPropertyKey(key, type, modifiers, description, null); + } + + @SuppressWarnings({"unchecked"}) + public static ConfigPropertyKey newConfigPropertyKey(String key, Class type, int modifiers, String description, String defaultValueStr) { + T defaultValue = null; + if (defaultValueStr != null) { + Converter converter = ConverterUtil.getConverter(type); + defaultValue = (T) converter.convert(type, defaultValueStr); + } + return new ConfigPropertyKey(key, type, modifiers, description, defaultValue); + } + + /** la clef non typée de la propriété */ + final String key; + + /** Le type de la valeur de la propriété */ + final Class type; + + /** les modifiers de la propriété */ + final int modifiers; + + /** la description de la propriété */ + protected final String description; + + /** La valeur par défaut (si elle existe) pour cette propriété */ + final T defaultValue; + + /** @return le type de la valeur de la propriété */ + public Class getType() { + return type; + } + + /** @return la clef non typé de la propriété */ + public String getKey() { + return key; + } + + /** @return la description de la propriété */ + public String getDescription() { + return description; + } + + /** @return la valeur par défaut de la propriété (ou null si non définie) */ + public T getDefaultValue() { + return defaultValue; + } + + /** + * @return true si la propriété est final (i.e ne + * peut être modifiée) + */ + public boolean isFinal() { + return FINAL.isUsed(modifiers); + } + + /** + * @return true si la propriété est transient + * (i.e ne doit pas être enregistrée) + */ + public boolean isTransient() { + return TRANSIENT.isUsed(modifiers); + } + + /** @return true si la propriété est static */ + public boolean isStatic() { + return STATIC.isUsed(modifiers); + } + + /** @return true si la propriété est obligatoire */ + public boolean isMandatory() { + return MANDATORY.isUsed(modifiers); + } + + + private ConfigPropertyKey(String key, Class type, int modifiers, String description, T defaultValue) { + this.key = key; + this.type = type; + this.defaultValue = defaultValue; + this.modifiers = modifiers; + this.description = description; + } +} Index: lutinutil/src/java/org/codelutin/option/Config.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/Config.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/Config.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,605 @@ +/* +* ##% Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.option; + +import org.apache.commons.beanutils.ConvertUtils; +import static org.codelutin.i18n.I18n._; +import org.codelutin.util.ReflectUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Cette classe définie une configuration générique, composée d'un certain nombre + * de propriétés typées (clef=valeur). + *

+ * Chaque propriété de la configuration est représentée par une clef typé unique + * {@link ConfigPropertyKey}. + *

+ * On distingue deux types de propriétés : celles dite "sûres", qui correspondent + * à des propriétés dont la clef est reconnue par la configuration et dont la + * valeur est en adéquation avec son type; + *

+ * Celles dite "non sûre" qui sont soit des propriétés non reconnues par + * la configuration, ou dont la valeur n'est pas en adéquation avec le type de + * propriété. + *

+ * Chaque configuration possède une catégorie {@link #category}, afin de permettre + * l'utilisation dans une application de plusieurs configurations. + *

+ * Pour connaitre toutes les propriétés acceptables pour la configuration, + * utiliser la méthode {@link #getUniverse()} . + *

+ * Pour connaitre à un moment précis, la liste des propriétés valides de la + * configuration, utiliser la méthode {@link #getSafeKeys()}, pour savoir si une + * propriété est utilisée dans la configuration, utiliser {@link #containsSafeKey(ConfigPropertyKey)} + *

+ * Pour connaitre à un moment précis, la liste des propriétés non sûres, utiliser + * la méthode {@link #getUnsafeKeys()} et {@link #containsUnsafeKey(Object)} + *

+ * Les méthodes classiques de sauvegarde et d'acquisition de données sont celles + * de l'objet {@link Properties} (load(XXX) et store). + * A noter que lors d'une sauvegarde, seule les propriétés sûres sont prise + * en compte afin d'assurer d'avoir toujours une configuration cohérente. + *

+ * Pour obtenir la valeur d'une propriété, on utilise la méthode {@link #getProperty(ConfigPropertyKey)}, + * qui checher dans un premier temps la valeur de la propriété dans la configuration, + * et si la propriété n'est pas définie, retourne la valeur par défaut (si elle + * est définie) pour la propriété. + *

+ * Pour positionner la valeur d'une propriété, on utilise la méthode {@link #setProperty(ConfigPropertyKey , Object)}. + * A noter que cette fonction est de visibilité protected, + * les propriétés modifiables seront exposées en écriture dans les implantation + * de cette classe par leur setter. + *

+ * Un certain nombre de méthodes sont définies pour connaitre l'état de la + * configuration, à savoir : {@link #isEmpty()}, {@link #isFull()}, {@link #isSafe()}. + * + * @author chemit + */ +public abstract class Config { + + /** + * la méthode à implanter pour initialiser la config (positionnement la source + * par exemple) + */ + protected abstract void init(); + + /** la catégorie de la config (clef unique non typée) */ + protected final String category; + + /** l'univers des clefs possibles dans la configuration */ + protected final List universe; + + /** la liste des clefs actuellement utilisées dans la configuration */ + protected final List safeKeys; + + /** les valeurs des propriétés de la configuration, indexées par leur clef typée */ + protected Map data; + + /** the internal map of rejected data indexed by their key found */ + protected SortedMap unsafeData; + + /** the internal Properties object used for io operations */ + protected Properties tmp; + + /** la source utilisée pour les opérations IO de la config */ + protected URI source; + + protected Config(String category) { + this.category = category; + universe = Collections.unmodifiableList(ReflectUtil.getConstants(getClass(), ConfigPropertyKey.class)); + safeKeys = new ArrayList(); + init(); + } + + /** + * @param key la clef typée de la propriété recherchée + * @return la valeur de la propriété, ou la valeur par défaut si non trouvée + */ + @SuppressWarnings({"unchecked"}) + public T getProperty(ConfigPropertyKey key) { + if (safeKeys.contains(key)) { + return (T) getData().get(key); + } + if (universe.contains(key)) { + // try default value + return key.getDefaultValue(); + } + return null; + } + + /** @return l'univers de clefs de propriétés connues dans cette config. */ + public List getUniverse() { + return universe; + } + + /** @return la liste des clefs de propriétés dit sûres */ + public List getSafeKeys() { + return safeKeys; + } + + /** @return true si toutes les propriétés sont rengeignées. */ + public boolean isFull() { + return universe.size() == safeKeys.size(); + } + + /** @return true si aucune propriété n'est enregistrée. */ + public boolean isEmpty() { + return safeKeys.isEmpty(); + } + + /** @return true si aucune propriété non sûre n'est présente. */ + public boolean isSafe() { + return unsafeData == null || unsafeData.isEmpty(); + } + + /** @return le nombre de propriétés actuellement enregistrées */ + public int size() { + return safeKeys.size(); + } + + /** + * @param key the given key + * @return true if key is legal and used, + * false otherwise. + */ + public boolean containsSafeKey(ConfigPropertyKey key) { + return universe.contains(key) && safeKeys.contains(key); + } + + /** + * @param key the given key + * @return true if key is known as unsafe, + * false otherwise. + */ + public boolean containsUnsafeKey(Object key) { + return !isSafe() && unsafeData.containsKey(key); + } + + /** + * @param key the given unsafe key, this is the original key of unsafe + * property (if a load was done), this is not connected with MyPropertyKey. + * @return the value of the given unsafe value, or null if there is not a + * such unsafe property. + */ + public Object getUnsafeData(Object key) { + if (isSafe()) { + return null; + } + Object result; + result = getUnsafeData().get(key); + return result; + } + + public List getUnsafeKeys() { + if (isSafe()) { + return Collections.emptyList(); + } + List result = new ArrayList(getUnsafeData().size()); + for (Object o : unsafeData.keySet()) { + result.add(o); + } + return result; + } + + /** + * Remove a unsafe property using a given unsage key, if there is a such + * unsafe property. + * + * @param key the key to use + */ + public void removeUnsafeData(Object key) { + if (isSafe()) { + return; + } + getUnsafeData().remove(key); + } + + /** + * clear the instance (data, tmp,unsafeData and keys). + *

+ * After the method invocation, the {@link #isEmpty()}'s method's return is + * always true. + */ + public void clear() { + if (data != null) { + data.clear(); + } + if (tmp != null) { + tmp.clear(); + } + clearUnsafeData(); + safeKeys.clear(); + } + + /** + * clear the instance unsafeData. + *

+ * After the method invocation, the {@link #isSafe()}'s method's return is + * always true. + */ + public void clearUnsafeData() { + if (unsafeData != null) { + unsafeData.clear(); + } + } + + /** + * Fill a {@link Properties} object with all safe properties convert to String + * (should be used to store the config with Properties.store method) + * + * @return a {@link Properties} object filled with safe properties of config + */ + public Properties toProperties() { + Properties result = new Properties(); + if (!isEmpty()) { + for (ConfigPropertyKey key : getSafeKeys()) { + Object val = getData().get(key); + // always convert the value to String + result.put(key.getKey(), val == null ? "" : val.toString()); + } + } + return result; + } + + /** + * Load some properties from a given {@link Properties} object. + * + * @param data the properties to inject + * @return the current instance + */ + public Config load(Properties data) { + if (data == null || data.isEmpty()) { + return this; + } + getTmp().putAll(data); + updateUniverse(); + return this; + } + + /** + * Load some properties from a given Properties files via a {@link Reader} object. + * + * @param reader the data to inject + * @return the current instance + * @throws IOException if problem while grabbing data. + */ + public Config load(Reader reader) throws IOException { + if (reader == null) { + return this; + } + getTmp().load(reader); + updateUniverse(); + return this; + } + + /** + * Load some properties from a given properties files via a {@link InputStream} object. + * + * @param inputStream the data to inject + * @return the current instance + * @throws IOException if problem while grabbing data. + */ + public Config load(InputStream inputStream) throws IOException { + if (inputStream == null) { + return this; + } + getTmp().load(inputStream); + updateUniverse(); + return this; + } + + /** + * Load some properties from a given xml properties file via a {@link InputStream} object. + * + * @param inputStream the data to inject + * @return the current instance + * @throws IOException if problem while grabbing data. + */ + public Config loadFromXML(InputStream inputStream) throws IOException { + if (inputStream == null) { + return this; + } + getTmp().loadFromXML(inputStream); + updateUniverse(); + return this; + } + + /** + * Try to fill the curent instance missing properties using the given + * otherProperties object. + * + * @param otherProperties the other properties object to use + * @return the current instance + */ + public Config fillMissingKeys(Config otherProperties) { + for (ConfigPropertyKey key : universe) { + if (!containsSafeKey(key)) { + Object value = otherProperties.getProperty(key); + setProperty(key, value); + } + } + return this; + } + + /** + * Save safe current configuration in a simple properties file. + *

+ *

+ * Each valid property is saved using the his key given by the method + * {@link org.codelutin.util.MyPropertiesUtil.MyPropertyKey#getKey()} for his entry. + *

+ * Unsafe data are not stored! + * + * @param out where to store + * @param comments comment to add in file + * @throws IOException if any problem while saving + */ + public void store(OutputStream out, String comments) throws IOException { + getTmp().clear(); + try { + if (!isEmpty()) { + for (ConfigPropertyKey key : getSafeKeys()) { + Object val = data.get(key); + // a shame that Properties.store can only deal with String value + getTmp().put(key.getKey(), val == null ? "" : val.toString()); + } + } + getTmp().store(out, comments); + } finally { + getTmp().clear(); + } + } + + + @Override + public String toString() { + StringBuilder s = new StringBuilder(super.toString()).append("'); + if (!isEmpty()) { + for (ConfigPropertyKey key : safeKeys) { + s.append('\n').append(key.getKey()).append(" = ").append(getData().get(key)); + } + } + if (!isSafe()) { + s.append("\nunsafe:"); + for (Map.Entry entry : unsafeData.entrySet()) { + s.append('\n').append(entry.getKey()).append(" = ").append(entry.getValue()); + } + } + return s.toString(); + } + + protected URI getSource() { + return source; + } + + protected void setSource(URI source) { + this.source = source; + } + + /** + * Set a value for a given property using his def and update + * also the list of keys used. + * + * @param def the def to use + * @param value the value to set + */ + protected void addProperty(ConfigPropertyKey def, Object value) { + getData().put(def, value); + if (!safeKeys.contains(def)) { + safeKeys.add(def); + } + } + + /** + * Remove a given property using his def and update + * also the list of keys used. + * + * @param def the def to use + */ + protected void removeProperty(ConfigPropertyKey def) { + getData().remove(def); + if (safeKeys.contains(def)) { + safeKeys.remove(def); + } + } + + /** + * Set a unsafe property. + * + * @param key unsafe key + * @param value the unsafe property value to set + */ + protected void addUnsafeData(Object key, Object value) { + getUnsafeData().put(key, value); + } + + /** + * Set the value for a given property using his def. + * + * @param def the given key to use + * @param value the given valuer to set + */ + protected void setProperty(ConfigPropertyKey def, Object value) { + checkAuthorizedKey(def); + if (value == null) { + removeProperty(def); + return; + } + + Class klass = def.getType(); + String key = def.getKey(); + + if (klass == String.class) { + addProperty(def, String.valueOf(value)); + return; + } + + if (value instanceof String) { + // make a conversion from String + Object typeValue = convert(klass, value); + if (typeValue == null) { + addUnsafeData(key, value); + } else { + addProperty(def, typeValue); + } + return; + } + + if (value.getClass() == klass) { + // add a matching typed value + addProperty(def, value); + return; + } + + Object value2 = value; + if (klass.isEnum()) { + // try an Enum conversion from ordinal + value2 = ConvertUtils.lookup(klass).convert(klass, value); + if (value2 != null && value2.getClass() == klass) { + // add a matching typed value after conversion + addProperty(def, value2); + } + } + if (value2 == null) { + addUnsafeData(key, value); + } + } + + /** + * Convert the given o object to the type klass + * + * @param klass the class of value to extract + * @param o the original value + * @return the converted value + */ + protected Object convert(Class klass, Object o) { + if (o == null) { + return null; + } + Object result; + if (klass == String.class) { + result = o; + } else { + if (o.getClass() == String.class) { + result = ConvertUtils.convert(String.valueOf(o), klass); + } else { + result = o; + } + } + if (result != null && !klass.isAssignableFrom(result.getClass())) { + result = null; + } + return result; + } + + /** + * Update unvierse after a load(XXX) method. + *

+ * The data to inject are stored in the {@link #tmp}. + */ + protected void updateUniverse() { + if (tmp == null || tmp.isEmpty()) { + return; + } + try { + // tmp contains new data to digest + for (Object o : tmp.keySet()) { + String kStr = o + ""; + ConfigPropertyKey key = getKey(kStr); + Object value = tmp.get(o); + if (key != null && universe.contains(key)) { + setProperty(key, value); + } else { + addUnsafeData(kStr, value); + } + } + } finally { + tmp.clear(); + } + } + + /** + * Pour obtenir la clef typée d'une propriété de config à partir de sa clef non typée + * + * @param key la clef non typée de la propriété + * @return la clef typée ou null si n'existe pas + */ + protected ConfigPropertyKey getKey(String key) { + for (ConfigPropertyKey configKey : universe) { + if (configKey.getKey().equals(key)) { + return configKey; + } + } + return null; + } + + + /** @return the lazy tmp instance */ + protected Properties getTmp() { + if (tmp == null) { + tmp = new Properties(); + } + return tmp; + } + + /** @return the lazy data instance */ + protected Map getData() { + if (data == null) { + data = new TreeMap(); + } + return data; + } + + /** @return the lazy unsafeData instance */ + protected Map getUnsafeData() { + if (unsafeData == null) { + unsafeData = new TreeMap(); + } + return unsafeData; + } + + /** + * Test if a def is authorized in the current{@link #universe}. + *

+ * If not throw an {@link IllegalArgumentException}. + * + * @param def the given def to test + */ + protected void checkAuthorizedKey(ConfigPropertyKey def) { + if (def == null || !universe.contains(def)) { + throw new IllegalArgumentException(_("lutinutil.error.config.unauthorized.key", def, universe.toString())); + } + } +} Index: lutinutil/src/java/org/codelutin/option/ParserException.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/ParserException.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/ParserException.java Sun Dec 30 22:50:46 2007 @@ -0,0 +1,62 @@ +/**##% + * Copyright (C) 2002, 2003 Code Lutin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * 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 Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *##%**/ + +package org.codelutin.option; + +import java.util.ArrayList; +import java.util.List; + +/** + * Exception soulevable uniquement par le parser d'option. + * + * @author chemit + * @see OptionParser + */ +public class ParserException extends Exception { // ParserException + + final OptionParserContexts.AbstractOptionParserContext context; + private static final long serialVersionUID = -4760568663653584477L; + + public ParserException(String msg, OptionParserContexts.AbstractOptionParserContext context) { + super(msg); + this.context = context; + } + + public ParserException(String msg, Throwable eee, OptionParserContexts.AbstractOptionParserContext context) { + super(msg, eee); + this.context = context; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder().append(super.toString()).append("\nfrom contexts :"); + OptionParserContexts.AbstractOptionParserContext cont = context; + List l = new ArrayList(); + String prefix = ""; + while (!cont.isRoot()) { + l.add(cont); + cont = (OptionParserContexts.AbstractOptionParserContext) cont.parent; + } + l.add(cont); + for (OptionParserContexts.AbstractOptionParserContext context : l) { + builder.append('\n').append(prefix).append(context); + prefix += " "; + } + return builder.toString(); + } +} // ParserException \ No newline at end of file Index: lutinutil/src/java/org/codelutin/option/ConfigKey.java diff -u /dev/null lutinutil/src/java/org/codelutin/option/ConfigKey.java:1.1 --- /dev/null Sun Dec 30 22:50:52 2007 +++ lutinutil/src/java/org/codelutin/option/ConfigKey.java Sun Dec 30 22:50:47 2007 @@ -0,0 +1,143 @@ +/* +* ##% Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Code Lutin, +* Tony Chemit +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* 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 Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* ##% */ +package org.codelutin.option; + +import java.lang.reflect.Modifier; + +/** + * Une classe pour représenter une clef de config typée. + *

+ * Dans le cadre des configs, on parlera plutôt de category de config, + * plutôt que de clefs. + *

+ * Chauqe Config est définie par les caractéristiques suivantes : + *

+ * une catégorie unique {@link #category} + *

+ * une classe abstraite d'implantation {@link #abstractConfigClass} : il s'agit + * de la classe abstraite générée implantant toutes les propriétés de la config, + * mais qui n'implante pas la logique de chargement et de sauvegarde de cette config + * précise (cela est de la responsabilité du dev). + *

+ * une classe concrete d'implantation {@link #configClass} : il s'agit d'une classe + * fille de {@link #abstractConfigClass} qui implante la logique de chargement + * et de sauvegarde de la config. Cette classe n'est pas connue au moment de l'instancation + * de cette classe, le dev doit enregistrer l'implantation finale à utiliser avec + * la méthode {@link #setConfigClass(Class)}. (Cela sera fait de manière transparent + * par le parseur. + *

+ *

+ * Le but de cette classe pivot, est de pouvoir accéder de manière typée à une config. + * Elle sera utilisée dans le parseur {@link OptionParser}, grâce à la méthode + * {@link OptionParser#getConfig(ConfigKey)} pour + *

+ * pouvoir accéder de manière typée à une config. + *

+ *

+ * De plus, cette clef sert aussi pour contenir l'unique instance partagée de + * l'implantation de la config {@link #config}. + *

+ * Pour obtenir l'instance, on utilise la méthode {@link #getConfig()} qui retourne + * l'instance typée de la config. + *

+ * Comme pour les autres types de clefs ({@link ConfigPropertyKey}, {@link OptionKey}), + * on dispose de méthodes d'instanciation par factory : + *

+ * {@link #newConfigKey(String, Class,String)} + * + * @author chemit + */ +public class ConfigKey { + + /** + * Public factory of ConfigKey + * + * @param key the non typed category of the config + * @param configClass the abstract class of config + * @param description the description of the config (says the category) + * @return the new instance + */ + public static ConfigKey newConfigKey(String key, Class configClass, String description) { + return new ConfigKey(key, configClass, description); + } + + /** la clef non typée de l'option */ + final protected String category; + + /** la classe d'implantation de l'optin liée */ + final protected Class abstractConfigClass; + + /** la description de la propriété */ + final protected String description; + + /** la classe concrete de la config (calculé au runtime) */ + protected Class configClass; + + /** l'unique instance de la config liée */ + protected C config; + + // constructeur privé, pour empécher les instanciations multiples + private ConfigKey(String key, Class abstractConfigClass, String description) { + this.category = key; + this.abstractConfigClass = abstractConfigClass; + this.description = description; + if (!Modifier.isAbstract(abstractConfigClass.getModifiers())) { + configClass = abstractConfigClass; + } + } + + public String getCategory() { + return category; + } + + public Class getAbstractConfigClass() { + return abstractConfigClass; + } + + public String getDescription() { + return description; + } + + public Class getConfigClass() { + return configClass; + } + + public void setConfigClass(Class configClass) { + this.configClass = configClass; + } + + /** + * Get the shared config linked with this key. + * + * @return the share config's instance + */ + public synchronized C getConfig() { + if (config == null) { + if (configClass == null) { + throw new IllegalStateException("no concrete config found for " + abstractConfigClass); + } + try { + config = configClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return config; + } +} \ No newline at end of file