Index: lutinutil/src/java/org/codelutin/util/OptionDefinitionParser.java diff -u /dev/null lutinutil/src/java/org/codelutin/util/OptionDefinitionParser.java:1.1 --- /dev/null Mon Nov 19 19:56:09 2007 +++ lutinutil/src/java/org/codelutin/util/OptionDefinitionParser.java Mon Nov 19 19:56:04 2007 @@ -0,0 +1,731 @@ +/* *##% +* 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. +*##%*/ + +/* * +* OptionParser.java +* +* Created: 22 août 2003 +* +* @author Benjamin Poussin +* @version $Revision: 1.1 $ +* +* Mise a jour: $Date: 2007-11-19 19:56:04 $ +* par : $Author: tchemit $ +*/ + +package org.codelutin.util; + +import static org.codelutin.i18n.I18n._; +import static org.codelutin.i18n.I18n.n_; +import static org.codelutin.util.OptionArgumentValueType._newfile; +import static org.codelutin.util.OptionArgumentValueType._string; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +/** + * Parser de définition d'options de la ligne de commande. + *

+ * Ce parseur génère des class OptionDefinition et OptionArgumentDefinition + *

+ * à partir de définition d'options données via un fichier de propriétés. + *

+ * La classe contient une méthode statique publique : + *

+ * {@link #doParse(File)} pour lancer un parsing sur le fichier de définitions + * donné. + *

+ *

+ *

+ * Dans ce fichier chaque option est représentée par deux lignes : + *

+ * optionName.definition={0,2} --option|-o + * [groupArgumentFacultatif] + *

+ * optionName.description= La description de l'option + * + * @author chemit + */ + +public class OptionDefinitionParser { + + private static final String DEFINITION_KEY_SUFFIX = n_(".definition"); + + private static final String DESCRIPTION_KEY_SUFFIX = n_(".description"); + + private static final int DEFINITION_KEY_PREFIX_LENGTH = + DEFINITION_KEY_SUFFIX.length(); + + private static final Character[] openingChars = {'(', '{', '[', '<'}; + + private static final Character[] closingChars = {')', '}', ']', '>'}; + + protected static final String LUTINUTIL_PARSERDEF_PRINT_DETAIL_OPTION_HEAD = + "lutinutil.parserdef.printDetail.option.head"; + + /** + * Effectue la parsing d'un fichier contenant les définitions des options. + *

+ * Retourne le parseur utilisé pour le parsing avec les définitions d'options + * validées {@link #getDefinitions()} et les non validées + * {@link #getUnvalidDefinitions()} pour les non validées. + * + * @param propertiesFilePath le fichier contenant les définitions des + * options + * @return le parseur après parsing du fichier + * @throws IOException si erreur pendant la lecture du fichier + */ + public static OptionDefinitionParser doParse(File propertiesFilePath) + throws IOException { + + OptionDefinitionParser parser; + + parser = new OptionDefinitionParser(propertiesFilePath); + + parser.parse(); + + return parser; + } + + /** lists of definitions found while parsing */ + protected List definitions; + + /** lists of unvalid definitions found while parsing */ + protected List unvalidDefinitions; + + /** le path du fichier (dans le classpath) qui contient les définitions d'options */ + protected File propertiesFile; + + /** list of exceptions found while parsing */ + protected List errors; + + /** collection of definition names found in properties file */ + protected List definitionNames; + + protected OptionDefinitionFactory factory; + + public OptionDefinition[] getDefinitions() { + return definitions.toArray(new OptionDefinition[definitions.size()]); + } + + public OptionDefinition[] getUnvalidDefinitions() { + return unvalidDefinitions.toArray(new OptionDefinition[unvalidDefinitions.size()]); + } + + public boolean hasFailed() { + return errors != null && !errors.isEmpty(); + } + + public List getErrors() { + return errors; + } + + public void initFactory(Class aClass) + throws IllegalAccessException, InstantiationException { + factory = OptionDefinitionFactory.newFactory(aClass); + } + + public void printErrors(Writer writer) throws IOException { + if (!hasFailed()) { + return; + } + writer.append(_("lutinutil.parserdef.printError.head", errors.size())); + for (int i = 0, j = errors.size(); i < j; i++) { + writer.append(_("lutinutil.parserdef.printError.error", + i + 1, j, errors.get(i))); + } + } + + public void printDetails(Writer writer) throws IOException { + int size = definitions.size(); + writer.append(_("lutinutil.parserdef.printDetail.parser.head", this, size)); + for (int i = 0; i < size; i++) { + OptionDefinition definition = definitions.get(i); + writer.append(_(LUTINUTIL_PARSERDEF_PRINT_DETAIL_OPTION_HEAD, + i + 1, size, definition)); + definition.printDetail(writer); + } + } + + protected OptionDefinitionParser(File propertiesFile) { + this.propertiesFile = propertiesFile; + } + + protected Map init() { + + // load properties file + Properties properties = new Properties(); + InputStream stream = null; + try { + + try { + stream = new BufferedInputStream(new FileInputStream(propertiesFile)); + properties.load(stream); + } finally { + stream.close(); + } + + } catch (IOException e) { + throw new RuntimeException( + _("lutinutil.error.could.not.load.properties.file", + propertiesFile, e.getMessage()), e); + } + + this.errors = new ArrayList(); + this.definitions = new ArrayList(); + this.unvalidDefinitions = new ArrayList(); + + Map result = new TreeMap(); + this.definitionNames = new ArrayList(); + for (Object o : properties.keySet()) { + String key = o.toString(); + if (!key.endsWith(DEFINITION_KEY_SUFFIX)) { + continue; + } + String s = key.substring(0, key.length() - DEFINITION_KEY_PREFIX_LENGTH); + definitionNames.add(s); + result.put(key, properties.getProperty(key)); + String descriptionKey = s + DESCRIPTION_KEY_SUFFIX; + result.put(descriptionKey, properties.getProperty(descriptionKey)); + } + Collections.sort(definitionNames); + return result; + } + + protected void parse() throws IOException { + + Map properties = init(); + + try { + + for (String definitionName : definitionNames) { + + String definitionStr = properties.get(definitionName + DEFINITION_KEY_SUFFIX); + String description = properties.get(definitionName + DESCRIPTION_KEY_SUFFIX); + + if (!preOptionParsingCheck(definitionStr, description)) { + // we don't treate this one, add a SyntaxUnvalidOptionDefinition definition + unvalidDefinitions.add(new SyntaxUnvalidOptionDefinition(description, definitionStr)); + continue; + } + int nbErrors = errors.size(); + + // build option definition + OptionDefinition definition = parseDefinition(definitionName, description, definitionStr); + + if (nbErrors < errors.size() || definition instanceof SyntaxUnvalidOptionDefinition || !postOptionParsingCheck(definition, definitions)) { + // don't add the unvalid option + unvalidDefinitions.add(definition); + } else { + definitions.add(definition); + } + } + } finally { + if (properties != null) { + properties.clear(); + } + } + } + + protected OptionDefinition parseDefinition(String definitionName, String description, String definition) { + + // assume when arrives here, that the gloabl option definition is fine : + // says : definition is not empty nor null + // says : definition has a first valid alias + + List tokens; + + tokens = new ArrayList(Arrays.asList(StringUtil.split(openingChars, closingChars, definition, " "))); + + // by default, option is mandatory + int repetitionMin = 0; + int repetitionMax = 1; + Collection argumentDefinitions = new ArrayList(); + boolean safe = true; + String token; + + token = tokens.get(0).trim(); + + // alias + String[] alias = token.split("\\|"); + tokens.remove(0); + + if (!tokens.isEmpty()) { + + token = tokens.get(0).trim(); + + if (token.startsWith("{") || token.startsWith("+") || token.startsWith("*")) { + + // option cardinalité (non obligatoire, par défaut {0,1}) + + Object[] tmp = parseCardinalite(token, false); + + repetitionMin = (Integer) tmp[1]; + repetitionMax = (Integer) tmp[2]; + + tokens.remove(0); + } + + if (!tokens.isEmpty()) { + // l'option possède des arguments + + //safe = parseOption(tokens.toArray(new String[tokens.size()]), argumentDefinitions); + + // on split tout d'abord les espaces pour obtenir les groupement + // d'arguments + int mandatoryPosition = 0; + // les definitions d"arguments déjàé trouvées dans le groupement + Collection groupArgumentDefinitions; + for (String groupArgument : tokens.toArray(new String[tokens.size()])) { + + groupArgumentDefinitions = new ArrayList(); + + if (groupArgument.startsWith("<")) { + // il s'agit d'un groupe d'arguments obligatoires + if (!parseOptionGroupArguments(groupArgument, mandatoryPosition, groupArgumentDefinitions)) { + safe = false; + break; + } + argumentDefinitions.addAll(groupArgumentDefinitions); + mandatoryPosition++; + continue; + } + if (groupArgument.startsWith("[")) { + // il s'agit d'un groupe d'arguments facultatifs + if (!parseOptionGroupArguments(groupArgument, -1, groupArgumentDefinitions)) { + safe = false; + break; + } + argumentDefinitions.addAll(groupArgumentDefinitions); + continue; + } + registerError(_("lutinutil.parserdef.error.unvalid.syntax.unknown.group.of.arguments", groupArgument)); + } + } + } + + // construction de la définition complête de l'option + return safe ? new OptionDefinition( + definitionName, + description, + definition, + repetitionMin, + repetitionMax, + alias, + argumentDefinitions.toArray(new OptionArgumentDefinition[argumentDefinitions.size()]) + ) : new SyntaxUnvalidOptionDefinition(description, definition); + } + + /** + * Permet de construire un dictionnaire d'OptionArgument utilisables pour + * la définition d'un argument d'une option donné. + *

+ * La définition d'un argument d'une option respecte la syntaxe suivante : + * + *

  • + * <argument_0|argument_1|argument_2|...|argument_n> + * pour un argument obligatoire + *
  • + *
  • + * [argument_0|argument_1|argument_2|...|argument_n] + * pour un argument facultatif. + *
  • + * + * Chaque argument_i peut être de la forme : + * + *

    + * On ne traite pas ici du caractère optional ou obligatoire de l'argument de l'option + * et on suppose de plus que la définition commence et termine bien par un délimitateur accepté + * + * @param groupArgumentDefinition la définition de l'arugment de l'option + * @param position la position du prochain argument obligatoire + * @param argumentDefinitionsFound la liste des arguments detectes dans ce groupe + * @return true si le parsing s'est bien déroulé, false autrement. + */ + protected boolean parseOptionGroupArguments( + String groupArgumentDefinition, int position, + Collection argumentDefinitionsFound) { + + boolean result = true; + + boolean mandatory = position > -1; + + // la liste des type déjà enregistrés + Collection argumentValueTypesFound = new ArrayList(); + + // on supprime tout espace en début ou fin de chaine de définition + groupArgumentDefinition = groupArgumentDefinition.trim(); + + // on supprime les délimitateurs de début et fin de chaine de définition + groupArgumentDefinition = groupArgumentDefinition.substring(1, groupArgumentDefinition.length() - 1); + + // on split pour obtenir la définition de chaque argument + String[] argumentDefinitions = groupArgumentDefinition.split("\\|"); + + for (String argumentDefinition : argumentDefinitions) { + + OptionArgumentType argumentType = OptionArgumentType.findType(argumentDefinition); + + //contient la clef et le type + String[] keyAndType = argumentType.explodeDefinition(argumentDefinition); + + //contient le type (String), min(Integer), repetittionMax(Integer) + Object[] typeAndCardMinAndCardMax; + + if (argumentType == OptionArgumentType.constant) { + // la clef contient la cardinalite + typeAndCardMinAndCardMax = parseCardinalite(keyAndType[0], mandatory); + // on recupère la clef nettoye + keyAndType[0] = (String) typeAndCardMinAndCardMax[0]; + // on pousse le type connu + typeAndCardMinAndCardMax[0] = keyAndType[1]; + } else { + typeAndCardMinAndCardMax = parseCardinalite(keyAndType[1], mandatory); + } + + // recherche de la classe d'implantation + OptionArgumentValueType argumentValueType = + OptionArgumentValueType.findTypeFromDefinition((String) typeAndCardMinAndCardMax[0]); + // check coherence of the new required argument definition + if (!checkOptionArgument( + argumentDefinition, + groupArgumentDefinition, + argumentType, + argumentValueType, + keyAndType[0], + argumentValueTypesFound, + argumentDefinitionsFound, + (Integer) typeAndCardMinAndCardMax[1], + (Integer) typeAndCardMinAndCardMax[2], + mandatory)) { + result = false; + continue; + } + if (argumentType == OptionArgumentType.valued) { + // on sauvegarde le type (pour les conflits possibles) + argumentValueTypesFound.add(argumentValueType); + } + + // instanciate the new argument definition + OptionArgumentDefinition definition = new OptionArgumentDefinition( + argumentType, + argumentValueType, + keyAndType[0], + (Integer) typeAndCardMinAndCardMax[1], + (Integer) typeAndCardMinAndCardMax[2], + position + ); + // register le new argument definition + argumentDefinitionsFound.add(definition); + } + return result; + } + + /** + * @param key la valeur dont on cherche la cardinalité + * @param mandatory flag to say if this is a mandatory element or not. + * @return un tableau contenant 3 object : le texte donné sans les informations + * de cardinalité, la répétitionMin, la répétitionMax. + * @throws OptionDefinitionParserException + * si probleme de parsing ou de syntaxe + */ + protected Object[] parseCardinalite(String key, boolean mandatory) + throws OptionDefinitionParserException { + Object[] result = new Object[3]; + int repetitionMin; + int repetitionMax; + String cleanKey; + + if (key.endsWith("+")) { + // cardinalité + = {1,-1} + repetitionMin = 1; + repetitionMax = -1; + cleanKey = key.substring(0, key.length() - 1); + } else { + if (key.endsWith("*")) { + // cardinalité * = {0,-1} + repetitionMin = 0; + repetitionMax = -1; + cleanKey = key.substring(0, key.length() - 1); + } else { + + if (key.endsWith("}")) { + // cardinalité spécifiée + int index = key.indexOf("{"); + cleanKey = key.substring(0, index); + String tmp = key.substring(index + 1, key.length() - 1); + int comaIndex = tmp.indexOf(','); + if (comaIndex == -1) { + // cardinalité unique + repetitionMax = repetitionMin = Integer.valueOf(tmp); + } else { + repetitionMin = Integer.valueOf(tmp.substring(0, comaIndex)); + repetitionMax = Integer.valueOf(tmp.substring(comaIndex + 1)); + } + } else { + // pas de cardinalité + // rien à faire (on prend les valeurs par defaut) + cleanKey = key; + if (mandatory) { + repetitionMin = 1; + repetitionMax = 1; + } else { + repetitionMin = 0; + repetitionMax = 1; + } + } + } + } + result[0] = cleanKey.trim().toLowerCase(); + result[1] = repetitionMin; + result[2] = repetitionMax; + return result; + } + + /** + * Examine une définition d'option avant parsing. + * + * @param definition la définition de l'option à parser + * @param description la descrption de l'option + * @return true si la définition est syntaxiquement valide + */ + protected boolean preOptionParsingCheck(String definition, String description) { + // check definition is not empty + if (definition == null || definition.isEmpty() || definition.trim().isEmpty()) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.empty.option.definition", definition, description)); + return false; + } + // check description is not empty + if (description == null || description.isEmpty() || description.trim().isEmpty()) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.empty.option.description", definition, description)); + return false; + } + // check defintion contains alias ? + if (definition.trim().indexOf('-') != 0) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.unfound.alias.in.option", definition, description)); + return false; + } + // check same number {[< to >]} in definition + if (!StringUtil.checkEnclosure(definition, '{', '}')) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.underbrace.option", definition, description)); + return false; + } + if (!StringUtil.checkEnclosure(definition, '<', '>')) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.lesser.option", definition, description)); + return false; + } + if (!StringUtil.checkEnclosure(definition, '[', ']')) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.caret.option", definition, description)); + return false; + } + // what else ? + return true; + } + + protected boolean postOptionParsingCheck(OptionDefinition definition, List definitions) { + + if (definition.getAlias().length > 4) { + registerError(_("lutinutil.parserdef.error.too.much.alias.option", Arrays.toString(definition.getAlias()), definition.getDefinition())); + return false; + } + + List alias = new ArrayList(); + // on vérifier que les alias commencent tous par '-' + for (String alia : definition.getAlias()) { + + if (!alia.startsWith("-")) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.alias.option", alia, definition.getDefinition())); + return false; + } + } + + for (OptionDefinition optionDefinition : definitions) { + // on vérifier que le nom n'est pas dupliqué + if (optionDefinition.getName().equals(definition.getName())) { + registerError(_("lutinutil.parserdef.error.duplicated.option.name", definition.getName(), definition.getDefinition(), optionDefinition.getDefinition())); + return false; + } + // on vérifie les conflits sur les alias + alias.addAll(Arrays.asList(optionDefinition.getAlias())); + for (String alia : definition.getAlias()) { + if (alias.contains(alia)) { + registerError(_("lutinutil.parserdef.error.duplicated.option.alias", alia, definition.getDefinition(), optionDefinition.getDefinition())); + return false; + } + } + } + + if (!checkCardinalite(definition.getMin(), definition.getMax(), definition.getDefinition(), definition.getDescription())) { + return false; + } + + // do the rest! + return true; + } + + protected boolean checkCardinalite(int min, int max, String definition, String description) { + if (min < 0) { + registerError(_("lutinutil.parserdef.error.min.can.not.be.negative", min, definition, description)); + return false; + } + if (max == 0) { + registerError(_("lutinutil.parserdef.error.max.can.not.be.zero", max, definition, description)); + return false; + } + // on vérifie que la cardinalité max >-2 + if (max < -1) { + registerError(_("lutinutil.parserdef.error.max.too.low", max, definition, description)); + return false; + } + // on vérifie que la cardinalité max==-1 || max >= min + if (max > -1 && max < min) { + registerError(_("lutinutil.parserdef.error.max.lowest.than.min", max, min, definition, description)); + return false; + } + return true; + } + + protected boolean checkOptionArgument(String argumentDefinition, + String groupementDefinition, + OptionArgumentType type, + OptionArgumentValueType argumentValueType, + String key, + Collection argumentValueTypesUsed, + Collection argumentsFound, + int repetitionMin, + int repetitionMax, + boolean mandatory) { + + if (type == null) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.unknown.argument.type", argumentDefinition, groupementDefinition)); + return false; + } + if (argumentValueType == null) { + registerError(_("lutinutil.parserdef.error.unvalid.syntax.unknown.value.type", argumentDefinition, groupementDefinition)); + return false; + } + if (key == null) { + registerError(_("lutinutil.parserdef.error.unfound.key", argumentDefinition, groupementDefinition)); + return false; + } + if (!checkCardinalite(repetitionMin, repetitionMax, argumentDefinition, groupementDefinition)) { + return false; + } + + // un argument de type constant ne peut pas avoir de cardinalite autre que {1,1} + if (type == OptionArgumentType.constant) { + if (mandatory && !(repetitionMin == 1 && repetitionMax == 1)) { + registerError(_("lutinutil.parserdef.error.const.argument.mandatory.cardinalite", repetitionMin, repetitionMax, argumentDefinition, groupementDefinition)); + return false; + } + if (!mandatory && !(repetitionMin == 0 && repetitionMax == 1)) { + registerError(_("lutinutil.parserdef.error.const.argument.optional.cardinalite", repetitionMin, repetitionMax, argumentDefinition, groupementDefinition)); + return false; + } + } else { + if (mandatory && repetitionMin < 1) { + registerError(_("lutinutil.parserdef.error.argument.mandatory.cardinalite", repetitionMin, repetitionMax, argumentDefinition, groupementDefinition)); + return false; + } + if (!mandatory && repetitionMin > 0) { + registerError(_("lutinutil.parserdef.error.argument.optional.cardinalite", repetitionMin, repetitionMax, argumentDefinition, groupementDefinition)); + return false; + } + } + // on verifie que la clef n'est pas déjà enregistrée + for (OptionArgumentDefinition optionArgumentDefinition : argumentsFound) { + if (optionArgumentDefinition.getKey().equals(key)) { + registerError(_("lutinutil.parserdef.error.duplicated.argument.key", key, argumentDefinition, groupementDefinition)); + return false; + } + } + + if (type == OptionArgumentType.valued) { + if (((argumentValueType == _newfile) && argumentValueTypesUsed.contains(_string)) || + ((argumentValueType == _string) && argumentValueTypesUsed.contains(_newfile))) { + registerError(_("lutinutil.parserdef.error.string.and.newfile.in.same.group", argumentDefinition, groupementDefinition)); + return false; + + } + // on vérifier qu'un type n'est pas dupliqué + if (argumentValueTypesUsed.contains(argumentValueType)) { + registerError(_("lutinutil.parserdef.error.duplicated.argument.valued.type", argumentValueType, argumentDefinition, groupementDefinition)); + return false; + } + } + return true; + } + + protected void registerError(String content) { + registerError(content, null); + } + + protected void registerError(String content, Exception e) { + OptionDefinitionParserException e1; + if (e == null) { + e1 = new OptionDefinitionParserException(content); + } else { + e1 = new OptionDefinitionParserException(content, e); + } + errors.add(e1); + } + + /** + * To contain a syntax unvalid OptionDefinition + * + * @author chemit + */ + protected static class SyntaxUnvalidOptionDefinition extends OptionDefinition { + + public SyntaxUnvalidOptionDefinition(String description, String definition) { + super(null, description, definition, 0, 0, 0, null); + } + } + +}