package ca.svarb.jyacl;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ca.svarb.utils.ArgumentChecker;
import ca.svarb.utils.ClassMaker;
import ca.svarb.utils.ClassReader;
import ca.svarb.utils.TextUtils;
/**
* Main class for this package.
* Create a CliParser object for a given Interface. The CliParser object can
* then be used to parse String[] args and return an instance of the Interface.<p>
*
* For example, define an Interface as follows:<p>
*
* <pre>
* public interface TestInterface {
* String getName();
* Integer getId();
* }
* </pre>
*
* Create a parser object:<p>
*
* <pre>
* CliParser parser = new CliParser(TestInterface.class);
* </pre>
*
* Then the object can be used as follows:<p>
*
* <pre>
* String[] args = new String[] { "--name=abc", "--id=23" };
* TestInterface options = (TestInterface)parser.processArguments(args);
* </pre>
*
* The options object returned will return "abc" from it's getName() method
* and 23 as an Integer object from it's getId() method.<p>
*
* The CliParser supports Interface methods that return a String, Integer
* or Enum object or methods that return another Interface. The name of
* the command line option will be the portion
* of the method after the "get" converted to lowercase.
* e.g. getName() will result in a command line option --name.<p>
*
* String methods will allow any value to be specified, Integer objects
* will be validated to be an integer value and Enum types will be
* validated to be one of the Enum values. For example given:<p>
* <pre>
* public enum RoleType {
* USER, MANAGER, HR, ADMIN;
* }
*
* RoleType getRole();
* </pre>
*
* the getRole() method will result in a commandline option --role which
* must be one of "user", "manager", "hr" or "admin".<p>
*
* Returning another Interface results in alternate usages of the command
* line options. For example with the following 3 interfaces: <p>
*
* <pre>
* public interface CreateInterface {
* String getName();
* Integer getId();
* }
*
* public interface ListInterface {
* Integer getId();
* }
*
* public interface TestInterface {
* CreateInterface getCreate();
* ListInterface getList();
* }
* </pre>
*
* Constructing a CliParser object with the TestInerface results in a
* command line with a usage of:
*
* <pre>
* --create --name=VALUE --id=INTEGER
* OR --list --id=INTEGER
* </pre>
*
* CliParser also recognizes the following annotations on the
* interface methods:<p>
*
* <pre>
* {@literal @}Mandatory<p>
* {@literal @}Help(String[] lines)<p>
* {@literal @}Unique<p>
* </pre>
*
* @author bmodi
*
*/
public class CliParser {
public static final String DEFAULT_FIRST_LINE_PREFIX = "Usage: ";
public static final String DEFAULT_OTHER_LINES_PREFIX = " OR: ";
public static final String DEFAULT_OPTIONS_TITLE = "Options:";
public static String NEWLINE = System.getProperty("line.separator");
private Class<? extends Object> optionsInterface;
private ClassMaker classMaker;
private UsageParser usageParser;
private ClassReader classReader;
private String firstLinePrefix;
private String otherPrefix;
private String optionsTitle;
/**
* Create parser for the given interface
* @param optionsInterface
*/
public CliParser(Class<? extends Object> optionsInterface) {
this(optionsInterface, DEFAULT_FIRST_LINE_PREFIX, DEFAULT_OTHER_LINES_PREFIX, DEFAULT_OPTIONS_TITLE);
}
/**
* Create parser for the given interface using the specified prefixes
* @param optionsInterface
* @param firstLinePrefix
* @param otherLinesPrefix
*/
public CliParser(Class<? extends Object> optionsInterface, String firstLinePrefix, String otherLinesPrefix, String optionsTitle) {
ArgumentChecker.checkNulls("optionsInterface", optionsInterface);
ArgumentChecker.checkNulls("firstLinePrefix", firstLinePrefix);
ArgumentChecker.checkNulls("firstLinePrefix", otherLinesPrefix);
ArgumentChecker.checkNulls("optionsTitle", optionsTitle);
this.optionsInterface = optionsInterface;
this.firstLinePrefix=firstLinePrefix;
this.otherPrefix=otherLinesPrefix;
this.optionsTitle=optionsTitle;
classReader = new ClassReader(optionsInterface);
usageParser = new UsageParser(classReader);
classMaker = new ClassMaker();
}
public Object processArguments(String[] args) throws CliException {
ArgumentChecker.checkNulls("args", args);
Object instance=null;
// Convert the args into a list of "key=value" pairs
List<Argument> arguments = Argument.createList(args);
// Figure out the usage specified - will be
// unnamed for single usage, otherwise
// it is multiple usage statements
Usage usage = usageParser.identifyUsage(arguments);
Collection<CliOption> cliOptions = usage.getCliOptions();
Map<String, Object> getterMap = createGetterMap(arguments, cliOptions);
// At this point all arguments have been validated against
// the interface. Create an instance of the class that
// returns the specified values from it's methods.
if ( usageParser.isUnnamedUsage() ) {
instance=classMaker.makeInstance(optionsInterface, getterMap);
} else {
// If the usage is a named usage, the options instance
// needs to be returned by the usage interface (and null for the rest)
instance=classMaker.makeInstance(usage.getReturnType(), getterMap);
Map<String, Object> usageGetterMap = createGetterMap(usageParser, usage.getName(), instance);
instance=classMaker.makeInstance(optionsInterface, usageGetterMap);
}
return instance;
}
/**
* Generates a helpful usage statement for the interface
* this parser object parses and prints it to the given
* stream.
* @param output
* @throws IOException
*/
public void showUsage(OutputStream output) throws IOException {
String usageString=firstLinePrefix;
if ( usageParser.isUnnamedUsage() ) {
List<CliOption> cliOptions = this.classReader.getCliOptions();
usageString += TextUtils.buildDelimitedString(cliOptions, " ");
} else {
// If multiple usages show all of them
Collection<Usage> usages = usageParser.getUsages();
String lineDelim="";
for (Usage usage : usages) {
usageString += lineDelim + usage + " " +
TextUtils.buildDelimitedString(usage.getCliOptions(), " ");
lineDelim = NEWLINE+otherPrefix;
}
}
output.write(usageString.getBytes());
output.write(NEWLINE.getBytes());
}
/**
* Generate full help for the interface including usage statement
* and help text for all options.
* @param output
* @throws IOException
*/
public void showHelp(OutputStream output) throws IOException {
showUsage(output);
String fullHelpString=NEWLINE+optionsTitle+NEWLINE;
output.write(fullHelpString.getBytes());
List<CliOption> cliOptions;
if (this.usageParser.isUnnamedUsage()) {
cliOptions = this.classReader.getCliOptions();
} else {
cliOptions = this.classReader.getAllCliOptions();
}
List<String> optionNames = getOptionNames(cliOptions);
String helpLine;
int maxLength=TextUtils.findLongestString(optionNames);
for (CliOption cliOption : cliOptions) {
String helpText[] = cliOption.getHelpText();
helpLine=String.format(" %-"+maxLength+"s %s"+NEWLINE, cliOption.asOptionString(), helpText[0]);
output.write(helpLine.getBytes());
for (int i=1; i<helpText.length; i++) {
String helpTextLine=String.format(" %-"+maxLength+"s %s"+NEWLINE, "", helpText[1]);
output.write(helpTextLine.getBytes());
}
}
}
private List<String> getOptionNames(List<CliOption> cliOptions) {
List<String> names=new ArrayList<String>();
for (CliOption cliOption : cliOptions) {
names.add(cliOption.asOptionString());
}
return names;
}
/**
* Generate a getter map that will return the object "instance" when the getter
* is equal to "selectedUsage", and for all other usages defined by usageParser
* the getter map returns <code>null</code>
*/
private Map<String, Object> createGetterMap(UsageParser usageParser, String selectedUsage, Object instance) {
Map<String, Object> getterMap = new HashMap<String, Object>();
Collection<Usage> usages = usageParser.getUsages();
// Loop through all usages and by default set them all to return null
for (Usage usage : usages) {
getterMap.put(usage.getName(), null);
}
getterMap.put(selectedUsage, instance);
return getterMap;
}
private Map<String, Object> createGetterMap(List<Argument> arguments, Collection<CliOption> cliOptions)
throws CliException {
Map<String, Object> getterMap = new HashMap<String, Object>();
// Build the getterMap by going through each argument specified and
// finding the matching option.
// Throw an exception if:
// - any of the arguments specifies an unknown option.
// - an option is repeated twice
boolean unique=false;
for (Argument argument : arguments) {
CliOption option = findOption(argument, cliOptions);
if ( option==null ) {
throw new CliException("Unknown option --"+argument.getName());
}
Object value = getValue(argument, option);
if ( option.isUnique() ) {
unique = true;
getterMap = new HashMap<String, Object>();
getterMap.put(option.getName(), value);
break;
}
if ( getterMap.containsKey(option.getName())) {
throw new CliException("Multiple values found for option --"+option.getName());
}
getterMap.put(option.getName(), value);
}
// Go through all options required by the given interface.
// If no value was provided for a non-mandatory option in the
// input arguments then return <code>Boolean.TRUE</code> for
// boolean options and <code>null</code> for any other
// option type. If the option was mandatory and no value was
// provided then throw an exception.
for(CliOption option : cliOptions) {
if ( !getterMap.containsKey(option.getName()) ) {
if ( !unique && option.isMandatory() ) {
throw new CliException("--"+option.getName()+" option is required");
} else {
getterMap.put(option.getName(),
option.getReturnType().equals(Boolean.class) ?
Boolean.FALSE : null);
}
}
}
return getterMap;
}
private Object getValue(Argument argument, CliOption option) {
Object returnValue=null;
Class<?> returnType = option.getReturnType();
String argumentValue = argument.getValue();
if ( returnType==Integer.class) {
returnValue=new Integer(argumentValue);
} else if ( returnType==Boolean.class) {
returnValue=argumentValue==null ? Boolean.TRUE : new Boolean(argumentValue);
} else if ( returnType.isEnum() ) {
Object[] enums = returnType.getEnumConstants();
for (Object currEnum : enums) {
if ( argumentValue.toUpperCase().equals(currEnum.toString()) ) {
returnValue=currEnum;
}
}
} else {
returnValue=argumentValue;
}
return returnValue;
}
private CliOption findOption(Argument argument, Collection<CliOption> cliOptions) {
for (CliOption option : cliOptions) {
if ( option.getName().equals(argument.getName() ) ) {
return option;
}
}
return null;
}
}