Package ca.svarb.jyacl

Source Code of ca.svarb.jyacl.CliParser

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;
  }
}
TOP

Related Classes of ca.svarb.jyacl.CliParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.