/**
* Copyright 2014 J. Patrick Meyer
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itemanalysis.jmetrik.commandbuilder;
import java.util.*;
/**
* A flexible class for defining an option and storing values. It can take named arguments of not.
* The option value may be an individual element or a list of values. This class can handle options
* in various forms such as the following:
*
* 1. myOptionName(someValue)
* 2. myOptionName(someValue1, someValue2)
* 3. myOptionName(argName = argValue)
* 4. myOptionName(argName1 = argValue1, argName2 = argValue2 )
* 5. myOptionName(argName1 = (argValue11, argValue12, argValue13), argName2 = (argValue21, argValue22) )
*
* When combined with {@link ArgumentValueChecker} the values can be validated. If validation fails, no
* value is added.
*
* All values are stored as Strings, but there are methods that will convert the value to double or int
* before returning them.
*
* This class is called MegaCommand because it uses one class to accomplish the same work that once took many classes.
*
*/
public class MegaOption{
/**
* A unique option name
*/
private String optionName = "";
/**
* An option description for help files.
*/
private String optionDescription = "";
/**
* A flag that indicates whether the option has named arguments. If false, the option is a list of values.
*/
private boolean argumentValuePair = false;
/**
* A flag that indicates whether the option is required or not.
*/
private boolean required = false;
/**
* A flag to indicate whether an argument may have multiple values
*/
private boolean allowMultiple = false;
/**
* Arguments are not required. All possible arguments are included in this list.
*/
private HashSet<String> argument = null;
/**
* An argument may have a description. Descriptions are stored here.
*/
private HashMap<String, String> argumentDescription = null;
private HashMap<String, Boolean> argumentRequired = null;
/**
* An argument may have a single value or a list of values. Values are stored here.
*/
private HashMap<String, ArrayList<String>> argumentValue = null;
/**
* A value checker
*/
private ArgumentValueChecker valueChecker = null;
/**
* A static name for the no argument case.
*/
private static String NULL_ARGUMENT = "NOARG";
public boolean hasAnyValues = false;
public MegaOption(String optionName, String optionDescription, OptionType type){
this(optionName, optionDescription, type, false);
}
public MegaOption(String optionName, OptionType type){
this(optionName, "", type, false);
}
/**
* This constructor is preferred as several of the options are predefined according to the type of option.
* Two types of options require additional configuration. For {@link OptionType#SELECT_ALL_OPTION} and
* {@link OptionType#SELECT_ONE_OPTION} you must also add a {@link SelectFromListValueChecker} object
* using {@link this#setValueChecker(ArgumentValueChecker)}. Those value checker will ensure that
* the value is obtained from a predefined list of possible values.
*
* @param optionName name of option that is unique to a command.
* @param optionDescription a short description of the option for help text.
* @param type the type of option.
*/
public MegaOption(String optionName, String optionDescription, OptionType type, boolean required){
this.optionName = optionName;
this.optionDescription = optionDescription;
this.required = required;
this.argumentRequired = new HashMap<String, Boolean>();
if(type == OptionType.FREE_LIST_OPTION){
this.argumentValuePair = false;
this.allowMultiple = true;
}else if(type == OptionType.ARGUMENT_VALUE_OPTION_LIST){
this.argumentValuePair = true;
this.allowMultiple = true;
}else if(type == OptionType.SELECT_ALL_OPTION){
//For a SelectAll option to be complete, you must also
//use the SelectFromListValueChecker to define the list
//of options from which to choose.
this.argumentValuePair = false;
this.allowMultiple = true;
}else{
//This could either be a FreeOption or a SelectOne option.
//They are stored in the same way. The difference is that
//a SelectOne option will use the SelectFromListValueChecker to
// define the list of options from which to choose. A FReeOption
//does not use a SelectFromListValueChecker.
this.argumentValuePair = false;
this.allowMultiple = false;
}
initialize();
}
private void initialize(){
argumentValue = new HashMap<String, ArrayList<String>>();
argumentDescription = new HashMap<String, String>();
if(argumentValuePair){
argument = new HashSet<String>();
}else{
argumentValue.put(NULL_ARGUMENT, new ArrayList<String>());
}
}
public void isRequired(boolean required){
this.required = required;
}
/**
* Check the values using an instance of {@link ArgumentValueChecker}. You can define your own
* value checker or use one of the predefined class. {@link SelectFromListValueChecker} is designed
* for options that have values that must be selected from a predefined list. {@link NumericValueChecker}
* is designed for checking that String can be parsed into a double and optionally that the numeric
* value is within a ranges such as [min <= value <= max].
*
* @param checker
*/
public void setValueChecker(ArgumentValueChecker checker){
this.valueChecker = checker;
}
public String getOptionName(){
return optionName;
}
public String getOptionDescription(){
return optionDescription;
}
/**
* Indicates whether the option is required
*
* @return true if option is required and false otherwise
*/
public boolean isRequired(){
return required;
}
/**
* Indicates whether the option uses argument value pairs or just one or more values.
* @return true if argument uses option value pairs. Return false if uses one ore more values
* without arguments.
*/
public boolean hasArguments(){
return argumentValuePair;
}
public boolean includesArgument(String argName){
return argument.contains(argName);
}
public HashSet<String> getArguments(){
return argument;
}
/**
* Adds an argument with no description. The description will default to an empty String.
*
* @param argName an argument name that is unique to the option.
*/
public void addArgument(String argName){
this.addArgument(argName, "", false);
}
/**
* If {@link #required} is true then you must add arguments with this method to define them.
* This method is called during the definition stage.
*
* @param argName an argument name unique to the option that is 50 characters or less . If a name is
* repeated only the first one is kept in the list
* @param description a description of the argument that is 2000 characters or less for help text.
* @throws IllegalArgumentException
*/
public void addArgument(String argName, String description, boolean required)throws IllegalArgumentException{
String name = "";
if(argumentValuePair){
name = argName.substring(0, Math.min(50, argName.length()));//Names must be 50 characters or less
boolean added = argument.add(name);
if(added){
int length = description.length();
this.argumentDescription.put(name, description.substring(0, Math.min(length, 2000)));
this.argumentRequired.put(name, required);
ArrayList<String> av = argumentValue.get(name);
if(av==null){
argumentValue.put(name, new ArrayList<String>());
}
}
}else{
throw new IllegalArgumentException(optionName + " does not take named arguments.");
}
}
/**
* Adds an argument value to this option. This is usually called from {@link MegaOptionParser} when
* text is split into option values.
*
* @param argName name of argument to which the value belongs.
* @param argValue value for the argument.
* @throws IllegalArgumentException
*/
public void addValueAt(String argName, String argValue)throws IllegalArgumentException{
if(valueChecker !=null){
if(!valueChecker.checkValueAt(argName, argValue)){
throw new IllegalArgumentException("<" + argValue + "> is an invalid value for option [" + optionName + "].");
}
}
if(argumentValuePair){
if(allowMultiple){
argumentValue.get(argName).add(argValue);
}else{
argumentValue.get(argName).add(0, argValue);
}
hasAnyValues = true;
}else{
throw new IllegalArgumentException(optionName + " does not take named arguments.");
}
}
/**
* Adds a value to a unnamed list. This is usually called from {@link MegaOptionParser} when
* text is split into option values.
*
* @param value a value for the option.
* @throws IllegalArgumentException
*/
public void addValue(String value)throws IllegalArgumentException{
if(valueChecker !=null){
if(!valueChecker.checkValue(value)){
throw new IllegalArgumentException("<" + value + "> is an invalid value for the option [" + optionName + "].");
}
}
if(argumentValuePair){
throw new IllegalArgumentException(optionName + " requires named argument value pairs.");
}else{
if(allowMultiple){
argumentValue.get(NULL_ARGUMENT).add(value);
}else{
ArrayList<String> argumentValueList = argumentValue.get(NULL_ARGUMENT);
if(!argumentValueList.isEmpty()) argumentValueList.remove(0);
argumentValueList.add(value);
}
hasAnyValues = true;
}
}
/**
* This method provides a way to check if a value is included in a list. It is designed for
* options of type OptionType.SELECT_ALL_OPTION or OptionType.SELECT_ONE_OPTION because
* only selected values wil be included in the list.
*
* @param value value that might be included in the list
* @return true if value is included, false otherwise
*/
public boolean containsValue(String value){
ArrayList<String> valueList = argumentValue.get(NULL_ARGUMENT);
return valueList.contains(value.trim());
}
/**
* This method provides a way to check if a value is included in a list. It is designed for
* options of type OptionType.SELECT_ALL_OPTION or OptionType.SELECT_ONE_OPTION because
* only selected values wil be included in the list.
*
* @param argument argument of interest
* @param value value that might be included in the list
* @return true if value is included, false otherwise
*/
public boolean containsValueAt(String argument, String value){
ArrayList<String> valueList = argumentValue.get(argument);
return valueList.contains(value.trim());
}
public int getNumberOfValues(){
return argumentValue.get(NULL_ARGUMENT).size();
}
public int getNumberOfValuesAt(String argument){
return argumentValue.get(argument).size();
}
/**
* Gets the first value in the list and returns it. If the list is empty it will
* return the default value.
*
* List values and default values will be checked. If they do not pass the test
* an exception will be thrown.
*
* @return value of the option.
*/
public String getValue(String defaultValue)throws IllegalArgumentException{
//check the default value
if(valueChecker !=null){
if(!valueChecker.checkValue(defaultValue)){
throw new IllegalArgumentException("<" + defaultValue + "> is an invalid value for option [" + optionName + "].");
}
}
if(argumentValuePair){
throw new IllegalArgumentException(optionName + " requires named argument value pairs.");
}else{
if(argumentValue.isEmpty())return defaultValue;
return argumentValue.get(NULL_ARGUMENT).get(0);
}
}
/**
* Converts value to a double before returning it.
*
* @param defaultValue
* @return
* @throws IllegalArgumentException
*/
public double getValueAsDouble(double defaultValue)throws IllegalArgumentException{
String value = getValue(Double.valueOf(defaultValue).toString());
return Double.parseDouble(value);
}
/**
* Converts value to an int before returning it.
* @param defaultValue
* @return
* @throws IllegalArgumentException
*/
public int getValueAsInteger(int defaultValue)throws IllegalArgumentException{
String value = getValue(Integer.valueOf(defaultValue).toString());
return Integer.parseInt(value);
}
/**
* Gets the first value in the list for an argument and returns it.
*
* @param argName name of argument for which the value is sought.
* @return default value for the argument. If argument is required but not value is found an
* IllegalArgumentException will be thrown. The default value is ignored for required arguments.
*/
public String getValueAt(String argName, String defaultValue)throws IllegalArgumentException{
//check the default value
if(valueChecker !=null){
if(!valueChecker.checkValueAt(argName, defaultValue)){
throw new IllegalArgumentException("<" + defaultValue + "> is an invalid value for option [" + optionName + "].");
}
}
if(argumentValuePair){
if(argument.contains(argName) && argumentValue.containsKey(argName)){
String value = defaultValue;
if(argumentValue.get(argName).size()>0) value = argumentValue.get(argName).get(0);
return value;
}else{
if(argumentRequired.get(argName)==Boolean.TRUE) throw new IllegalArgumentException("Value not found for required argument: " + argName);
return defaultValue;
}
}else{
throw new IllegalArgumentException(optionName + " does not take named arguments.");
}
}
public double getValueAtAsDouble(String argName, double defaultValue)throws IllegalArgumentException{
String value = getValueAt(argName, Double.valueOf(defaultValue).toString());
return Double.parseDouble(value);
}
public int getValueAtAsInteger(String argName, int defaultValue)throws IllegalArgumentException{
String value = getValueAt(argName, Integer.valueOf(defaultValue).toString());
return Integer.parseInt(value);
}
/**
* Gets all the values in the list for this option.
*
* @param defaultValues if no values exist in the list, the default values will be returned.
* @return a list values as an array of strings.
* @throws IllegalArgumentException
*/
public String[] getValues(String[] defaultValues)throws IllegalArgumentException{
//check the default values
if(valueChecker !=null){
for(String dflt : defaultValues){
if(!valueChecker.checkValue(dflt)){
throw new IllegalArgumentException("<" + dflt + "> is an invalid value for option [" + optionName + "].");
}
}
}
if(argumentValuePair){
throw new IllegalArgumentException(optionName + " requires named argument value pairs.");
}else{
ArrayList<String> valueList = argumentValue.get(NULL_ARGUMENT);
if(valueList.isEmpty()) return defaultValues;
String[] values = new String[valueList.size()];
for(int i=0;i<valueList.size();i++){
values[i] = valueList.get(i);
}
return values;
}
}
public double[] getValuesAsDouble(double[] defaultValues)throws IllegalArgumentException{
int n = defaultValues.length;
String[] dv = new String[n];
for(int i=0;i<n;i++){
dv[i] = Double.valueOf(defaultValues[i]).toString();
}
String[] s = getValues(dv);
double[] d = new double[n];
for(int i=0;i<n;i++){
d[i] = Double.parseDouble(s[i]);
}
return d;
}
public int[] getValuesAsInteger(int[] defaultValues)throws IllegalArgumentException{
int n = defaultValues.length;
String[] dv = new String[n];
for(int i=0;i<n;i++){
dv[i] = Integer.valueOf(defaultValues[i]).toString();
}
String[] s = getValues(dv);
int[] d = new int[n];
for(int i=0;i<n;i++){
d[i] = Integer.parseInt(s[i]);
}
return d;
}
/**
* Gets all the values in a list ofr a particular argument.
*
* @param argName name of argument
* @param defaultValues if no values exist in the list, the default values will be returned.
* @return a list values for the argument as an array of strings.
* @throws IllegalArgumentException
*/
public String[] getValuesAt(String argName, String[] defaultValues)throws IllegalArgumentException{
//check the default values
if(valueChecker !=null){
for(String dflt : defaultValues){
if(!valueChecker.checkValue(dflt)){
throw new IllegalArgumentException("<" + dflt + "> is an invalid value for option [" + optionName + "].");
}
}
}
if(argumentValuePair){
if(argument.contains(argName) && argumentValue.containsKey(argName)){
ArrayList<String> valueList = argumentValue.get(argName);
if(valueList.size()==0) return defaultValues;
String[] values = new String[valueList.size()];
for(int i=0;i<valueList.size();i++){
values[i] = valueList.get(i);
}
return values;
}else{
return defaultValues;
}
}else{
throw new IllegalArgumentException(optionName + " does not use named argument value pairs.");
}
}
public double[] getValuesAtAsDouble(String argName, double[] defaultValues)throws IllegalArgumentException{
int n = defaultValues.length;
String[] dv = new String[n];
for(int i=0;i<n;i++){
dv[i] = Double.valueOf(defaultValues[i]).toString();
}
String[] s = getValuesAt(argName, dv);
n = s.length;
double[] d = new double[n];
for(int i=0;i<n;i++){
d[i] = Double.parseDouble(s[i]);
}
return d;
}
public int[] getValuesAtAsInteger(String argName, int[] defaultValues)throws IllegalArgumentException{
int n = defaultValues.length;
String[] dv = new String[n];
for(int i=0;i<n;i++){
dv[i] = Integer.valueOf(defaultValues[i]).toString();
}
String[] s = getValuesAt(argName, dv);
n = s.length;
int[] d = new int[n];
for(int i=0;i<n;i++){
d[i] = Integer.parseInt(s[i]);
}
return d;
}
public boolean hasAnyValues(){
return hasAnyValues;
}
public MegaOption split(String input, MegaOption option){
if(option==null) throw new NullPointerException("Check your option names");
String REGEX = ",(?![^()]*+\\))";//split only on commas not contained in parentheses
int nameEnd = input.indexOf("(");
int optionEnd = input.lastIndexOf(")");
String optionName = input.substring(0, nameEnd).trim();
String optionValue = input.substring(nameEnd+1, optionEnd).trim();
if(!optionName.equals(option.getOptionName())){
return null;
}
if(option.hasArguments()){
if(optionValue.contains("=")){
String[] argValuePairs = optionValue.split(REGEX);
String REGEX2 = "=(?![^()]*+\\))";//split only on equal signs not contained in parentheses
for(int i=0;i<argValuePairs.length;i++){
//pair[0] is the argument name
//pair[1] is the argument value, which could be a comma delimited list
String[] pair = argValuePairs[i].split(REGEX2);
String arg = pair[0].trim();
if(option.includesArgument(arg)){
String value = pair[1].trim();
int start = value.indexOf("(");
int end = value.lastIndexOf(")");
if(start==-1 && end == -1){
//A single value, just add it
option.addValueAt(arg, value);
}else{
//A list of values contained in parentheses. Split the list and add each element.
value = value.substring(start+1, end);
String[] valueList = value.split(",");
for(String v : valueList){
option.addValueAt(arg, v.trim());
}
}
}else{
throw new IllegalArgumentException(option.getOptionName() + " does not include the argument " + arg);
}
}
}else{
throw new IllegalArgumentException("Argument/value pairs must be separated by an = sign in " + option.getOptionName() + ".");
}
}else{
String[] s = optionValue.split(REGEX);
for(int i=0;i<s.length;i++){
option.addValue(s[i].trim());
}
}
return option;
}
/**
* Formatted output for the display of the option. A question mark is used for options or arguments
* that have no current values. The option is displayed in a way that it can be split by
* {@link MegaOptionParser}.
*
* @return a string representation of the option.
*/
public String paste(){
String output = getOptionName() + "(";
if(argumentValuePair){
for(String arg : argument){
// output += arg + " = ";
ArrayList<String> values = argumentValue.get(arg);
if(!values.isEmpty()){
if(values.size()==1){
output += arg + " = " + values.get(0);
}else{
output += arg + " = (";
for(String s : values){
output += s + ",";
}
output = output.trim();
output = output.substring(0, output.length()-1);//remove last comma
output += ")";
}
output += ", ";
}
// if(values.isEmpty()){
//// output += arg + " = ?";
// }else
}
}else{
ArrayList<String> values = argumentValue.get(NULL_ARGUMENT);
if(values.isEmpty()){
output += "?";
}else if(values.size()==1){
output += values.get(0);
}else{
for(String s : values){
output += s + ",";
}
output = output.trim();
output = output.substring(0, output.length()-1);//remove last comma
}
}
output = output.trim();
if(output.endsWith(",")) output = output.substring(0,output.length()-1);
output += ")";
return output;
}
public String getHelpText(){
StringBuilder sb = new StringBuilder();
Formatter f = new Formatter(sb);
f.format("%2s", "");
f.format("%-20s", optionName.substring(0, Math.min(20, optionName.length())));
//display parameters
String output = "";
if(required){
output += "(Required, ";
}else{
output += "(Optional, ";
}
if(argumentValuePair){
output += "Named arguments, ";
}else{
output += "Value only, ";
}
if(allowMultiple){
output += "Multiple values allowed)";
}else{
output += "One value only)";
}
f.format("%-45s", output);
// f.format("%n");
if(!"".equals(optionDescription)){
f.format("%1s", " ");
formatDescription(optionDescription, f, "");
f.format("%n");
}else{
f.format("%n");
}
//display arguments and description if included
if(argumentValuePair){
String text = "";
for(String arg : argument){
f.format("%3s", "");
f.format("%-22s", "["+arg+"]");
if(argumentRequired.get(arg)==Boolean.TRUE){
text = "(Required) ";
}else{
text = "(Optional) ";
}
String descr = argumentDescription.get(arg);
if(descr!=null || !"".equals(descr)){
formatDescription(descr, f, text);
f.format("%n");
}else{
f.format("%n");
}
}
}
return f.toString();
}
private void formatDescription(String description, Formatter f, String text){
String[] textArray = description.split("\\s+");//split on white space
String line = text;
int lineCount = 0;
for(String s : textArray){
if((line+s).trim().length()<=100){
//concatenate text to line
line += s + " ";
}else{
//print line
if(lineCount>0) f.format("%25s", "");
f.format("%-100s", line);
f.format("%n");
line = s + " ";
lineCount++;
}
}
//last line
if(lineCount>0) f.format("%25s", "");
f.format("%-100s", line);
}
@Override
public String toString(){
return optionName;
}
@Override
public boolean equals(Object o){
if(!(o instanceof MegaOption)) return false;
if(o==this) return true;
Option opt = (Option)o;
if(opt.toString().equals(this.toString())) return true;
return false;
}
@Override
public int hashCode(){
return optionName.hashCode();
}
}