package org.molgenis.compute.commandline.options; //taken from http://www.javaworld.com/javaworld/jw-08-2004/jw-0816-command.html /** * The central class for option processing. Sets are identified by their name, * but there is also an anonymous default set, which is very convenient if an * application requieres only one set. */ public class Options { private final static String CLASS = "Options"; /** * The name used internally for the default set */ public final static String DEFAULT_SET = "DEFAULT_OPTION_SET"; /** * An enum encapsulating the possible separators between value options and * their actual values. */ public enum Separator { /** * Separate option and value by ":" */ COLON(':'), /** * Separate option and value by "=" */ EQUALS('='), /** * Separate option and value by blank space */ BLANK(' '), // Or, more precisely, whitespace (as allowed by the CLI) /** * This is just a placeholder in case no separator is required (i. e. * for non-value options) */ NONE('D'); // NONE is a placeholder in case no separator is required, // 'D' is just an arbitrary dummy value private char c; private Separator(char c) { this.c = c; } /** * Return the actual separator character * <p> * * @return The actual separator character */ char getName() { return c; } } /** * An enum encapsulating the possible prefixes identifying options (and * separating them from command line data items) */ public enum Prefix { /** * Options start with a "-" (typically on Unix platforms) */ DASH('-'), /** * Options start with a "/" (typically on Windows platforms) */ SLASH('/'); private char c; private Prefix(char c) { this.c = c; } /** * Return the actual prefix character * <p> * * @return The actual prefix character */ char getName() { return c; } } /** * An enum encapsulating the possible multiplicities for options */ public enum Multiplicity { /** * Option needs to occur exactly once */ ONCE, /** * Option needs to occur at least once */ ONCE_OR_MORE, /** * Option needs to occur either once or not at all */ ZERO_OR_ONE, /** * Option can occur any number of times */ ZERO_OR_MORE; } private java.util.HashMap<String, OptionSet> optionSets = new java.util.HashMap<String, OptionSet>(); private Prefix prefix = null; private Multiplicity defaultMultiplicity = null; private String[] arguments = null; private boolean ignoreUnmatched = false; private int defaultMinData = 0; private int defaultMaxData = 0; private StringBuffer checkErrors = null; /** * Constructor * <p> * * @param args * The command line arguments to check * @param prefix * The prefix to use for all command line options. It can only be * set here for all options at the same time * @param defaultMultiplicity * The default multiplicity to use for all options (can be * overridden when adding an option) * @param defMinData * The default minimum number of data items for all sets (can be * overridden when adding a set) * @param defMaxData * The default maximum number of data items for all sets (can be * overridden when adding a set) * <p> * @throws IllegalArgumentException * If either <code>args</code>, <code>prefix</code>, or * <code>defaultMultiplicity</code> is <code>null</code> - or if * the data range values don't make sense */ public Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int defMinData, int defMaxData) { if (args == null) throw new IllegalArgumentException(CLASS + ": args may not be null"); if (prefix == null) throw new IllegalArgumentException(CLASS + ": prefix may not be null"); if (defaultMultiplicity == null) throw new IllegalArgumentException(CLASS + ": defaultMultiplicity may not be null"); if (defMinData < 0) throw new IllegalArgumentException(CLASS + ": defMinData must be >= 0"); if (defMaxData < defMinData) throw new IllegalArgumentException(CLASS + ": defMaxData must be >= defMinData"); arguments = new String[args.length]; int i = 0; for (String s : args) arguments[i++] = s; this.prefix = prefix; this.defaultMultiplicity = defaultMultiplicity; this.defaultMinData = defMinData; this.defaultMaxData = defMaxData; } /** * Constructor * <p> * * @param args * The command line arguments to check * @param prefix * The prefix to use for all command line options. It can only be * set here for all options at the same time * @param defaultMultiplicity * The default multiplicity to use for all options (can be * overridden when adding an option) * @param data * The default minimum and maximum number of data items for all * sets (can be overridden when adding a set) * <p> * @throws IllegalArgumentException * If either <code>args</code>, <code>prefix</code>, or * <code>defaultMultiplicity</code> is <code>null</code> - or if * the data range value doesn't make sense */ public Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity, int data) { this(args, prefix, defaultMultiplicity, data, data); } /** * Constructor. The default number of data items is set to 0. * <p> * * @param args * The command line arguments to check * @param prefix * The prefix to use for all command line options. It can only be * set here for all options at the same time * @param defaultMultiplicity * The default multiplicity to use for all options (can be * overridden when adding an option) * <p> * @throws IllegalArgumentException * If either <code>args</code>, <code>prefix</code>, or * <code>defaultMultiplicity</code> is <code>null</code> */ public Options(String args[], Prefix prefix, Multiplicity defaultMultiplicity) { this(args, prefix, defaultMultiplicity, 0, 0); } /** * Constructor. The prefix is set to * {@link org.molgenis.compute.commandline.options.Options.Prefix#DASH}. * <p> * * @param args * The command line arguments to check * @param defaultMultiplicity * The default multiplicity to use for all options (can be * overridden when adding an option) * @param defMinData * The default minimum number of data items for all sets (can be * overridden when adding a set) * @param defMaxData * The default maximum number of data items for all sets (can be * overridden when adding a set) * <p> * @throws IllegalArgumentException * If either <code>args</code> or * <code>defaultMultiplicity</code> is <code>null</code> - or if * the data range values don't make sense */ public Options(String args[], Multiplicity defaultMultiplicity, int defMinData, int defMaxData) { this(args, Prefix.DASH, defaultMultiplicity, defMinData, defMaxData); } /** * Constructor. The prefix is set to * {@link org.molgenis.compute.commandline.options.Options.Prefix#DASH}. * <p> * * @param args * The command line arguments to check * @param defaultMultiplicity * The default multiplicity to use for all options (can be * overridden when adding an option) * @param data * The default minimum and maximum number of data items for all * sets (can be overridden when adding a set) * <p> * @throws IllegalArgumentException * If either <code>args</code> or * <code>defaultMultiplicity</code> is <code>null</code> - or if * the data range value doesn't make sense */ public Options(String args[], Multiplicity defaultMultiplicity, int data) { this(args, Prefix.DASH, defaultMultiplicity, data, data); } /** * Constructor. The prefix is set to * {@link org.molgenis.compute.commandline.options.Options.Prefix#DASH}, and * the default number of data items is set to 0. * <p> * * @param args * The command line arguments to check * @param defaultMultiplicity * The default multiplicity to use for all options (can be * overridden when adding an option) * <p> * @throws IllegalArgumentException * If either <code>args</code> or * <code>defaultMultiplicity</code> is <code>null</code> */ public Options(String args[], Multiplicity defaultMultiplicity) { this(args, Prefix.DASH, defaultMultiplicity, 0, 0); } /** * Constructor. The prefix is set to * {@link org.molgenis.compute.commandline.options.Options.Prefix#DASH}, the * default number of data items is set to 0, and the multiplicity is set to * {@link org.molgenis.compute.commandline.options.Options.Multiplicity#ONCE} * . * <p> * * @param args * The command line arguments to check * <p> * @throws IllegalArgumentException * If <code>args</code> is <code>null</code> */ public Options(String args[]) { this(args, Prefix.DASH, Multiplicity.ONCE); } /** * Constructor. The prefix is set to * {@link org.molgenis.compute.commandline.options.Options.Prefix#DASH}, and * the multiplicity is set to * {@link org.molgenis.compute.commandline.options.Options.Multiplicity#ONCE} * . * <p> * * @param args * The command line arguments to check * @param data * The default minimum and maximum number of data items for all * sets (can be overridden when adding a set) * <p> * @throws IllegalArgumentException * If <code>args</code> is <code>null</code> - or if the data * range value doesn't make sense */ public Options(String args[], int data) { this(args, Prefix.DASH, Multiplicity.ONCE, data, data); } /** * Constructor. The prefix is set to * {@link org.molgenis.compute.commandline.options.Options.Prefix#DASH}, and * the multiplicity is set to * {@link org.molgenis.compute.commandline.options.Options.Multiplicity#ONCE} * . * <p> * * @param args * The command line arguments to check * @param defMinData * The default minimum number of data items for all sets (can be * overridden when adding a set) * @param defMaxData * The default maximum number of data items for all sets (can be * overridden when adding a set) * <p> * @throws IllegalArgumentException * If <code>args</code> is <code>null</code> - or if the data * range values don't make sense */ public Options(String args[], int defMinData, int defMaxData) { this(args, Prefix.DASH, Multiplicity.ONCE, defMinData, defMaxData); } /** * Constructor. The default number of data items is set to 0, and the * multiplicity is set to * {@link org.molgenis.compute.commandline.options.Options.Multiplicity#ONCE} * . * <p> * * @param args * The command line arguments to check * @param prefix * The prefix to use for all command line options. It can only be * set here for all options at the same time * <p> * @throws IllegalArgumentException * If either <code>args</code> or <code>prefix</code> is * <code>null</code> */ public Options(String args[], Prefix prefix) { this(args, prefix, Multiplicity.ONCE, 0, 0); } /** * Constructor. The multiplicity is set to * {@link org.molgenis.compute.commandline.options.Options.Multiplicity#ONCE} * . * <p> * * @param args * The command line arguments to check * @param prefix * The prefix to use for all command line options. It can only be * set here for all options at * @param data * The default minimum and maximum number of data items for all * sets (can be overridden when adding a set) * <p> * @throws IllegalArgumentException * If either <code>args</code> or <code>prefix</code> is * <code>null</code> - or if the data range value doesn't make * sense */ public Options(String args[], Prefix prefix, int data) { this(args, prefix, Multiplicity.ONCE, data, data); } /** * Constructor. The multiplicity is set to * {@link org.molgenis.compute.commandline.options.Options.Multiplicity#ONCE} * . * <p> * * @param args * The command line arguments to check * @param prefix * The prefix to use for all command line options. It can only be * set here for all options at the same time * @param defMinData * The default minimum number of data items for all sets (can be * overridden when adding a set) * @param defMaxData * The default maximum number of data items for all sets (can be * overridden when adding a set) * <p> * @throws IllegalArgumentException * If either <code>args</code> or <code>prefix</code> is * <code>null</code> - or if the data range values don't make * sense */ public Options(String args[], Prefix prefix, int defMinData, int defMaxData) { this(args, prefix, Multiplicity.ONCE, defMinData, defMaxData); } /** * Return the (first) matching set. This invocation does not ignore * unmatched options and requires that data items are the last ones on the * command line. * <p> * * @return The first set which matches (i. e. the <code>check()</code> * method returns <code>true</code>) - or <code>null</code>, if no * set matches. */ public OptionSet getMatchingSet() { return getMatchingSet(false, true); } /** * Return the (first) matching set. * <p> * * @param ignoreUnmatched * A boolean to select whether unmatched options can be ignored * in the checks or not * @param requireDataLast * A boolean to indicate whether the data items have to be the * last ones on the command line or not * <p> * @return The first set which matches (i. e. the <code>check()</code> * method returns <code>true</code>) - or <code>null</code>, if no * set matches. */ public OptionSet getMatchingSet(boolean ignoreUnmatched, boolean requireDataLast) { for (String setName : optionSets.keySet()) if (check(setName, ignoreUnmatched, requireDataLast)) return optionSets.get(setName); return null; } /** * Add an option set. * <p> * * @param setName * The name for the set. This must be a unique identifier * @param minData * The minimum number of data items for this set * @param maxData * The maximum number of data items for this set * <p> * @return The new <code>Optionset</code> instance created. This is useful * to allow chaining of <code>addOption()</code> calls right after * this method */ public OptionSet addSet(String setName, int minData, int maxData) { if (setName == null) throw new IllegalArgumentException(CLASS + ": setName may not be null"); if (optionSets.containsKey(setName)) throw new IllegalArgumentException(CLASS + ": a set with the name " + setName + " has already been defined"); OptionSet os = new OptionSet(prefix, defaultMultiplicity, setName, minData, maxData); optionSets.put(setName, os); return os; } /** * Add an option set. * <p> * * @param setName * The name for the set. This must be a unique identifier * @param data * The minimum and maximum number of data items for this set * <p> * @return The new <code>Optionset</code> instance created. This is useful * to allow chaining of <code>addOption()</code> calls right after * this method */ public OptionSet addSet(String setName, int data) { return addSet(setName, data, data); } /** * Add an option set. The defaults for the number of data items are used. * <p> * * @param setName * The name for the set. This must be a unique identifier * <p> * @return The new <code>Optionset</code> instance created. This is useful * to allow chaining of <code>addOption()</code> calls right after * this method */ public OptionSet addSet(String setName) { return addSet(setName, defaultMinData, defaultMaxData); } /** * Return an option set - or <code>null</code>, if no set with the given * name exists * <p> * * @param setName * The name for the set to retrieve * <p> * @return The set to retrieve (or <code>null</code>, if no set with the * given name exists) */ public OptionSet getSet(String setName) { return optionSets.get(setName); } /** * This returns the (anonymous) default set * <p> * * @return The default set */ public OptionSet getSet() { if (getSet(DEFAULT_SET) == null) addSet(DEFAULT_SET, defaultMinData, defaultMaxData); return getSet(DEFAULT_SET); } /** * The error messages collected during the last option check (invocation of * any of the <code>check()</code> methods). This is useful to determine * what was wrong with the command line arguments provided * <p> * * @return A string with all collected error messages */ public String getCheckErrors() { return checkErrors.toString(); } /** * Run the checks for the default set. <code>ignoreUnmatched</code> is set * to <code>false</code>, and <code>requireDataLast</code> is set to * <code>true</code>. * <p> * * @return A boolean indicating whether all checks were successful or not */ public boolean check() { return check(DEFAULT_SET, false, true); } /** * Run the checks for the default set. * <p> * * @param ignoreUnmatched * A boolean to select whether unmatched options can be ignored * in the checks or not * @param requireDataLast * A boolean to indicate whether the data items have to be the * last ones on the command line or not * <p> * @return A boolean indicating whether all checks were successful or not */ public boolean check(boolean ignoreUnmatched, boolean requireDataLast) { return check(DEFAULT_SET, ignoreUnmatched, requireDataLast); } /** * Run the checks for the given set. <code>ignoreUnmatched</code> is set to * <code>false</code>, and <code>requireDataLast</code> is set to * <code>true</code>. * <p> * * @param setName * The name for the set to check * <p> * @return A boolean indicating whether all checks were successful or not * <p> * @throws IllegalArgumentException * If either <code>setName</code> is <code>null</code>, or the * set is unknown. */ public boolean check(String setName) { return check(setName, false, true); } /** * Run the checks for the given set. * <p> * * @param setName * The name for the set to check * @param ignoreUnmatched * A boolean to select whether unmatched options can be ignored * in the checks or not * @param requireDataLast * A boolean to indicate whether the data items have to be the * last ones on the command line or not * <p> * @return A boolean indicating whether all checks were successful or not * <p> * @throws IllegalArgumentException * If either <code>setName</code> is <code>null</code>, or the * set is unknown. */ public boolean check(String setName, boolean ignoreUnmatched, boolean requireDataLast) { if (setName == null) throw new IllegalArgumentException(CLASS + ": setName may not be null"); if (optionSets.get(setName) == null) throw new IllegalArgumentException(CLASS + ": Unknown OptionSet: " + setName); checkErrors = new StringBuffer(); checkErrors.append("Checking set "); checkErrors.append(setName); checkErrors.append('\n'); // .... Access the data for the set to use OptionSet set = optionSets.get(setName); java.util.ArrayList<OptionData> options = set.getOptionData(); java.util.ArrayList<String> data = set.getData(); java.util.ArrayList<String> unmatched = set.getUnmatched(); // .... Catch some trivial cases if (options.size() == 0) { // No options have been defined at all if (arguments.length == 0) { // No arguments have been given: in this case, this is a success return true; } else { checkErrors.append("No options have been defined, nothing to check\n"); return false; } } else if (arguments.length == 0) { // Options have been defined, but no arguments given checkErrors.append("Options have been defined, but no arguments have been given; nothing to check\n"); return false; } // .... Parse all the arguments given int ipos = 0; int offset = 0; java.util.regex.Matcher m = null; String value = null; String detail = null; String next = null; String key = null; String pre = Character.toString(prefix.getName()); boolean add = true; boolean[] matched = new boolean[arguments.length]; for (int i = 0; i < matched.length; i++) // Initially, we assume there was no match at all matched[i] = false; while (true) { value = null; detail = null; offset = 0; add = true; key = arguments[ipos]; for (OptionData optionData : options) { // For each argument, we may need to check all defined options m = optionData.getPattern().matcher(key); if (m.lookingAt()) { if (optionData.useValue()) { // The code section for value options if (optionData.useDetail()) { detail = m.group(1); offset = 2; // required for correct Matcher.group // access below } if (optionData.getSeparator() == Separator.BLANK) { // In this case, the next argument must be the value if (ipos + 1 == arguments.length) { // The last argument, thus no value follows it: // Error checkErrors.append("At end of arguments - no value found following argument "); checkErrors.append(key); checkErrors.append('\n'); add = false; } else { next = arguments[ipos + 1]; if (next.startsWith(pre)) { // The next one is an argument, not a value: // Error checkErrors.append("No value found following argument "); checkErrors.append(key); checkErrors.append('\n'); add = false; } else { value = next; matched[ipos++] = true; // Mark the key and // the value matched[ipos] = true; } } } else { // The value follows the separator in this case value = m.group(1 + offset); matched[ipos] = true; } } else { // Simple, non-value options matched[ipos] = true; } if (add) optionData.addResult(value, detail); // Store the // result break; // No need to check more options, we have a match } } ipos++; // Advance to the next argument to check if (ipos >= arguments.length) break; // Terminating condition for // the check loop } // .... Identify unmatched arguments and actual (non-option) data int first = -1; // Required later for requireDataLast for (int i = 0; i < matched.length; i++) { // Assemble the list of unmatched options if (!matched[i]) { if (arguments[i].startsWith(pre)) { // This is an unmatched option unmatched.add(arguments[i]); checkErrors.append("No matching option found for argument "); checkErrors.append(arguments[i]); checkErrors.append('\n'); } else { // This is actual data if (first < 0) first = i; data.add(arguments[i]); } } } // .... Checks to determine overall success; start with multiplicity of // options boolean err = true; for (OptionData optionData : options) { key = optionData.getKey(); err = false; // Local check result for one option switch (optionData.getMultiplicity()) { case ONCE: if (optionData.getResultCount() != 1) err = true; break; case ONCE_OR_MORE: if (optionData.getResultCount() == 0) err = true; break; case ZERO_OR_ONE: if (optionData.getResultCount() > 1) err = true; break; } if (err) { checkErrors.append("Wrong number of occurences found for argument "); checkErrors.append(prefix.getName()); checkErrors.append(key); checkErrors.append('\n'); return false; } } // .... Check range for data if (data.size() < set.getMinData() || data.size() > set.getMaxData()) { checkErrors.append("Invalid number of data arguments: "); checkErrors.append(data.size()); checkErrors.append(" (allowed range: "); checkErrors.append(set.getMinData()); checkErrors.append(" ... "); checkErrors.append(set.getMaxData()); checkErrors.append(")\n"); return false; } // .... Check for location of the data in the list of command line // arguments if (requireDataLast) { if (first + data.size() != arguments.length) { checkErrors .append("Invalid data specification: data arguments are not the last ones on the command line\n"); return false; } } // .... Check for unmatched arguments if (!ignoreUnmatched && unmatched.size() > 0) return false; // Don't // accept // unmatched // arguments // .... If we made it to here, all checks were successful return true; } /** * Add the given non-value option to <i>all</i> known sets. See * {@link OptionSet#addOption(String)} for details. */ public void addOptionAllSets(String key) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, defaultMultiplicity); } /** * Add the given non-value option to <i>all</i> known sets. See * {@link OptionSet#addOption(String, org.molgenis.compute.commandline.options.Options.Multiplicity)} * for details. */ public void addOptionAllSets(String key, Multiplicity multiplicity) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, false, Separator.NONE, false, multiplicity); } /** * Add the given value option to <i>all</i> known sets. See * {@link OptionSet#addOption(String, org.molgenis.compute.commandline.options.Options.Separator)} * for details. */ public void addOptionAllSets(String key, Separator separator) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, false, separator, true, defaultMultiplicity); } /** * Add the given value option to <i>all</i> known sets. See * {@link OptionSet#addOption(String, org.molgenis.compute.commandline.options.Options.Separator, org.molgenis.compute.commandline.options.Options.Multiplicity)} * for details. */ public void addOptionAllSets(String key, Separator separator, Multiplicity multiplicity) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, false, separator, true, multiplicity); } /** * Add the given value option to <i>all</i> known sets. See * {@link OptionSet#addOption(String, boolean, org.molgenis.compute.commandline.options.Options.Separator)} * for details. */ public void addOptionAllSets(String key, boolean details, Separator separator) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, details, separator, true, defaultMultiplicity); } /** * Add the given value option to <i>all</i> known sets. See * {@link OptionSet#addOption(String, boolean, org.molgenis.compute.commandline.options.Options.Separator, org.molgenis.compute.commandline.options.Options.Multiplicity)} * for details. */ public void addOptionAllSets(String key, boolean details, Separator separator, Multiplicity multiplicity) { for (String setName : optionSets.keySet()) optionSets.get(setName).addOption(key, details, separator, true, multiplicity); } /** * This is the overloaded {@link Object#toString()} method, and it is * provided mainly for debugging purposes. * <p> * * @return A string representing the instance */ public String toString() { StringBuffer sb = new StringBuffer(); for (OptionSet set : optionSets.values()) { sb.append("Set: "); sb.append(set.getSetName()); sb.append('\n'); for (OptionData data : set.getOptionData()) { sb.append(data.toString()); sb.append('\n'); } } return sb.toString(); } }