/*
 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.glassfish.pfl.basic.tools.argparser ;

import java.util.Collection ;
import java.util.Collections ;
import java.util.ArrayList ;
import java.util.List ;
import java.util.Map ;
import java.util.Set ;
import java.util.HashMap ;

import java.lang.reflect.Array ;
import java.lang.reflect.Method ;
import java.lang.reflect.Proxy ;
import java.lang.reflect.InvocationHandler ;

import java.net.URL ;

import java.util.Arrays;
import org.glassfish.pfl.basic.contain.Pair;

A general purpose argument parser that uses annotations, reflection, and generics.

This class solves a simple problem: How do I write an argument parser that parses arguments of the form

-xxx value

There are many ways of doing this: the ORB has probably at least 6 different ways of attacking this problem (and I wrote half of them). The approach taken here is to start with an interface, annotate it to indicate some information (like default values) for parsing, and then use reflection to produce an implementation of the interface that uses a dynamic proxy.

The argument parsing is entirely specified in terms of an interface with annotated methods. The name of the method is the name used as a -xxx argument when parsing the data. The type of data determines the valid inputs for the argument. All arguments must be in the -xxx yyy form, where yyy is a valid input for the type of method xxx() in the interface.

The interface is very simply: just create an ArgParser of the appropriate type, and then call parse( String[] ) to get an instance of the interface. There is also a getHelpText() method that returns a String with formatted information about all the valid arguments for this parser.

Not all required information can be derived from the method type and name. Some annotations are defined for additional information:

  1. @DefaultValue. This gives a String that is used to construct a default value for the given method.
  2. @Separator. This is used to override the default "," separator used for lists of values for fields of type List<Type> or Type[].
  3. @Help. This is used to give any help text for the field that is displayed by the getHelpText method.
TODO:
  1. Support I18N for this (required before it could be used for public parts of the ORB).
  2. Support Set<Type> Similar to list, but all elements must be unique.
  3. Support Map<Type,Type>
  4. Do we want to support really complex types, like Map<String,List<Map<String,Foo>>> How would we specify needed separators?
  5. Should we extend along the lines of the ORB arg parser, with an embedded language to describe how to parse a String?
  6. Extend this to support property parsing as well (required for ORB.init parsing). Probably just want to parse Properties as well as String[]. This requires some way to handle the property prefix (as in -ORBInitHost and org.omg.CORBA.ORBInitHost).
  7. Extend to support abstract classes. Only non-private abstract methods that are read-only beans and are annotated with @DefaultValue can be set by the parser. Other methods are allowed, but not used by the parser.
  8. Support an @Complete annotation. All such methods must be void(), and are executed in some order after the parsing is complete.
  9. Add an @Keyword notation to override the default name of the argument (the method name). This allows having different names for the methods and the arguments. Note that this can be used for fully-qualified property names. This supports the ORB dual use of properties and arguments (with perhaps a little fudging for things like ORBInitRef: if there are multiple args, concat them together and use a list separator?)
  10. Add an @ClassName annotation that takes no arguments. This means that the string passed must be the name of a class that has a no-args constructor. The class must be assignment compatible with the return type of the annotated method. At parse time, we just load the class named by the string and create a new instance of it.
  11. Consider using apt for this. This solves two problems:
    1. We can generate a more efficient class than the current version that uses a dynamic proxy and a table lookup. This becomes important when we consider that ORBData may be referenced many times during an invocation.
    2. We can generate much of the required ORBConstants (say as ORBPropertyNames) directly from the annotated interface.
  12. Most of the extensions here are aimed at reducing the code size of the ORB data stuff.
  13. If we don't want to use apt:
    1. Use codegen to generate a class that implements the argument interface.
    2. Instead of generating ORBConstants, look things up from it using static reflection on the name of the constant. This would require some other annotations:
      1. @IndirectKeyword gives the name of the constant in the @ConstantClass to look for. Do we need multiple constant classes?
      2. @ConstantClass would give the class to use for looking up public static final constants for @IndirectKeyword, which is like @Keyword.
Summary of current and proposed annotations:
  1. @DefaultValue: current method-level takes string
  2. @Separator: current method-level takes string
  3. @Help: current method-level takes string
  4. @ConstantClass: proposed class-level takes class constant
  5. @Complete: proposed method-level marker
  6. @Keyword: proposed method-level takes string
  7. @IndirectKeyword: proposed method-level takes string
  8. @ClassName: proposed method-level marker
Author:Ken Cavanaugh
/** A general purpose argument parser that uses annotations, reflection, * and generics. * <p> * This class solves a simple problem: How do I write an argument parser * that parses arguments of the form * <p> * -xxx value * <p> * There are many ways of doing this: the ORB has probably at least 6 * different ways of attacking this problem (and I wrote half of them). * The approach taken here is to start with an interface, annotate it * to indicate some information (like default values) for parsing, and then * use reflection to produce an implementation of the interface that uses a * dynamic proxy. * <p> * The argument parsing is entirely specified in terms of an interface with * annotated methods. The name of the method is the name used as a -xxx * argument when parsing the data. The type of data determines the valid * inputs for the argument. All arguments must be in the -xxx yyy form, * where yyy is a valid input for the type of method xxx() in the interface. * <p> * The interface is very simply: just create an ArgParser of the appropriate type, * and then call parse( String[] ) to get an instance of the interface. There * is also a getHelpText() method that returns a String with formatted information * about all the valid arguments for this parser. * <p> * Not all required information can be derived from the method type and name. * Some annotations are defined for additional information: * <ol> * <li>@DefaultValue. This gives a String that is used to construct a default value * for the given method. * <li>@Separator. This is used to override the default "," separator used for * lists of values for fields of type List&lt;Type&gt; or Type[]. * <li>@Help. This is used to give any help text for the field that is displayed * by the getHelpText method. * </ol> * * TODO: * * <ol> * <li>Support I18N for this (required before it could be used for public * parts of the ORB). * <li>Support Set&lt;Type&gt; Similar to list, but all elements must be unique. * <li>Support Map&lt;Type,Type&gt; * <li>Do we want to support really complex types, like Map&lt;String,List&lt;Map&lt;String,Foo&gt;&gt;&gt; * How would we specify needed separators? * <li>Should we extend along the lines of the ORB arg parser, with an embedded * language to describe how to parse a String? * <li>Extend this to support property parsing as well (required for ORB.init parsing). * Probably just want to parse Properties as well as String[]. This requires some * way to handle the property prefix (as in -ORBInitHost and org.omg.CORBA.ORBInitHost). * <li>Extend to support abstract classes. Only non-private abstract methods * that are read-only * beans and are annotated with @DefaultValue can be set by the parser. Other * methods are allowed, but not used by the parser. * <li>Support an @Complete annotation. All such methods must be void(), and are * executed in some order after the parsing is complete. * <li>Add an @Keyword notation to override the default name of the argument (the * method name). This allows having different names for the methods and the arguments. * Note that this can be used for fully-qualified property names. This supports the * ORB dual use of properties and arguments (with perhaps a little fudging for * things like ORBInitRef: if there are multiple args, concat them together and use * a list separator?) * <li>Add an @ClassName annotation that takes no arguments. This means that the * string passed must be the name of a class that has a no-args constructor. * The class must be assignment compatible with the return type of the annotated * method. At parse time, we just load the class named by the string and create * a new instance of it. * <li>Consider using apt for this. This solves two problems: * <ol> * <li>We can generate a more efficient class than the current version that * uses a dynamic proxy and a table lookup. This becomes important when * we consider that ORBData may be referenced many times during an invocation. * <li>We can generate much of the required ORBConstants (say as ORBPropertyNames) * directly from the annotated interface. * <li> * </ol> * <li>Most of the extensions here are aimed at reducing the code size of the * ORB data stuff. * <li>If we don't want to use apt: * <ol> * <li>Use codegen to generate a class that implements the argument interface. * <li>Instead of generating ORBConstants, look things up from it using static * reflection on the name of the constant. This would require some other annotations: * <ol> * <li>@IndirectKeyword gives the name of the constant in the @ConstantClass to look for. * Do we need multiple constant classes? * <li>@ConstantClass would give the class to use for looking up public static final * constants for @IndirectKeyword, which is like @Keyword. * </ol> * </ol> * </ol> * * Summary of current and proposed annotations: * <ol> * <li>@DefaultValue: current method-level takes string * <li>@Separator: current method-level takes string * <li>@Help: current method-level takes string * <li>@ConstantClass: proposed class-level takes class constant * <li>@Complete: proposed method-level marker * <li>@Keyword: proposed method-level takes string * <li>@IndirectKeyword: proposed method-level takes string * <li>@ClassName: proposed method-level marker * <li> * </ol> * * @author Ken Cavanaugh */
public class ArgParser { private final List<Class<?>> interfaceClasses = new ArrayList<Class<?>>(); private final Map<String,String> helpText = new HashMap<String, String>(); private final Map<String,Object> defaultValues = new HashMap<String, Object>(); private final Map<String,ElementParser> parserData = new HashMap<String, ElementParser>();
Useful utility class for parsing pairs of strings.
/** Useful utility class for parsing pairs of strings. */
public static class StringPair extends Pair<String,String> { public StringPair( String first, String second ) { super( first, second ) ; }
Construct a StringPair from data of the first first:second.
Params:
  • data – The string to parse into a StringPair.
/** Construct a StringPair from data of the first first:second. * @param data The string to parse into a StringPair. */
public StringPair( String data ) { super( null, null ) ; int index = data.indexOf( ':' ) ; if (index < 0) { throw new IllegalArgumentException(data + " does not contain a :"); } _first = data.substring( 0, index ) ; _second = data.substring( index + 1 ) ; } }
Construct an ArgParser that parses an argument string into an instance of the Class argument. cls must be an interface. Each method in this interface must take no arguments. Each method must be annotated with a DefaultValue annotation. Each method must return one of the following types:
  • A primitive type
  • A String type
  • A type that has a public constructor that takes a single String argument
  • An Enum
  • A List parameterized by one of the above types
  • An array of one of the first four types
The name of the method is the name of the keyword.
/** Construct an ArgParser that parses an argument string into an instance of the * Class argument. * cls must be an interface. Each method in this interface must take no arguments. * Each method must be annotated with a DefaultValue annotation. * Each method must return one of the following types: * <ul> * <li>A primitive type * <li>A String type * <li>A type that has a public constructor that takes a single String argument * <li>An Enum * <li>A List parameterized by one of the above types * <li>An array of one of the first four types * </ul> * The name of the method is the name of the keyword. */
public ArgParser( final Class<?> cls ) { init( cls ) ; } public ArgParser( final List<Class<?>> classes ) { init( classes ) ; } private void init( final Class<?> cls ) { final List<Class<?>> classes = new ArrayList<Class<?>>() ; classes.add( cls ) ; init( classes ) ; } private void init( final List<Class<?>> classes ) { final Map<String,String> defaultValueData = new HashMap<String, String>() ; for (Class<?> cls : classes ) { if (!cls.isInterface()) { error( cls.getName() + " is not an interface" ) ; } interfaceClasses.add( cls ) ; // Construct the list of ParserData entries for the methods in cls, and // also a map from keyword to default value data for the @DefaultValue // annotations. // parserData = new HashMap<String,ElementParser>() ; // helpText = new HashMap<String,String>() ; // Map<String,String> defaultValueData = new HashMap<String,String>() ; for (Method m : cls.getMethods()) { final String keyword = checkMethod( m ) ; final ElementParser ep = ElementParser.factory.evaluate( m ) ; parserData.put( keyword, ep ) ; final DefaultValue dv = m.getAnnotation( DefaultValue.class ) ; if (dv == null) { error( "Method " + m.getName() + " does not have a DefaultValue annotation" ) ; } else { defaultValueData.put( keyword, dv.value() ) ; } final Help help = m.getAnnotation( Help.class ) ; if (help != null) { helpText.put(keyword, help.value()); } } internalParse( defaultValueData, defaultValues ) ; } } private String display( final Object obj ) { if (obj.getClass().isArray()) { final StringBuilder sb = new StringBuilder() ; sb.append( "[" ) ; for (int ctr=0; ctr<Array.getLength( obj ); ctr++ ) { final Object element = Array.get( obj, ctr ) ; if (ctr > 0) { sb.append( "," ) ; } sb.append( element.toString() ) ; } sb.append( "]" ) ; return sb.toString() ; } else if (obj instanceof Collection) { final StringBuilder sb = new StringBuilder() ; sb.append( "[" ) ; boolean first = true ; for (Object element : (Collection)obj) { if (first) { first = false ; } else { sb.append( "," ) ; } sb.append( element.toString() ) ; } sb.append( "]" ) ; return sb.toString() ; } else { return obj.toString() ; } }
Returns a formatted text string that describes the expected arguments for this parser.
/** Returns a formatted text string that describes the expected * arguments for this parser. */
public String getHelpText() { final StringBuilder sb = new StringBuilder() ; sb.append( " Legal arguments are:\n" ) ; final Set<String> keys = parserData.keySet() ; final List<String> keyList = new ArrayList<String>( keys ) ; Collections.sort( keyList ) ; for (String keyword : keyList) { final ElementParser ep = parserData.get( keyword ) ; sb.append("\t-").append(keyword).append( " <") ; boolean first = true ; for (String str : ep.describe() ) { if (first) { first = false; } else { sb.append("\n\t "); } sb.append( str ) ; } sb.append( ">\n" ) ; final String defaultValue = display(defaultValues.get(keyword)) ; sb.append("\t " + "(default ").append(defaultValue).append( ")\n") ; final String help = helpText.get( keyword ) ; if (help != null) { sb.append("\t ").append(help).append( "\n") ; } sb.append( "\n" ) ; } return sb.toString() ; }
Parse the argument string into an instance of type T.
/** Parse the argument string into an instance of type T. */
public Object parse( final String[] args ) { final Map<String,String> data = makeMap( args ) ; final Map<String,Object> pdata = new HashMap<String, Object>() ; internalParse( data, pdata ) ; final Object result = makeProxy( pdata ) ; return result ; } public <T> T parse( final String[] args, final Class<T> cls ) { return cls.cast( parse( args )) ; } private void error( final String msg ) { System.out.println( "Error in argument parser: " + msg ) ; System.out.println( getHelpText() ) ; throw new RuntimeException ( msg ) ; } // Check that method has no arguments private String checkMethod( final Method m ) { if (m.getParameterTypes().length == 0) { return m.getName(); } else { error("Method " + m.getName() + " must not have any parameters"); } return null ; } private void internalParse( final Map<String,String> data, Map<String,Object> result) { for (Map.Entry<String,String> entry : data.entrySet()) { final String keyword = entry.getKey() ; final ElementParser ep = parserData.get( keyword ) ; if (ep == null) { error(keyword + " is not a valid keyword"); } final Object val = ep.evaluate( entry.getValue() ) ; result.put( keyword, val ) ; } } private String getKeyword( final String arg ) { if (arg.charAt(0) == '-') { return arg.substring(1); } else { error(arg + " is not a valid keyword"); } return null ; // not reachable } // Data must all be of the form (-keyword value)* private Map<String,String> makeMap( final String[] args ) { final Map<String,String> result = new HashMap<String,String>() ; String keyword = null ; for (String arg : args) { if (keyword == null) { keyword = getKeyword(arg); } else { result.put( keyword, arg ) ; keyword = null ; } } if (keyword != null) { error("No argument supplied for keyword " + keyword); } return result ; } // Make a dynamic proxy of type T for the given data. // The keys in the data must be the same as the method names in // the types. private Object makeProxy( final Map<String,Object> data ) { final InvocationHandler ih = new InvocationHandler() { private Object getValue( final String keyword ) { Object result = data.get( keyword ) ; if (result == null) { result = defaultValues.get(keyword); } return result ; } private String getString( final Object obj ) { final Class cls = obj.getClass() ; if (cls.isArray()) { final StringBuilder sb = new StringBuilder() ; sb.append( "[" ) ; for (int ctr=0; ctr<Array.getLength( obj ); ctr++) { if (ctr>0) { sb.append( "," ) ; } final Object element = Array.get( obj, ctr ) ; sb.append( element.toString() ) ; } sb.append( "]" ) ; return sb.toString() ; } else { return obj.toString() ; } } public Object invoke( final Object proxy, final Method method, final Object[] args ) { final String name = method.getName() ; if (name.equals("toString")) { final StringBuilder sb = new StringBuilder() ; for (String keyword : parserData.keySet()) { sb.append( keyword ) ; sb.append( "=" ) ; sb.append( getString( getValue( keyword ) ) ) ; sb.append( "\n" ) ; } return sb.toString() ; } else { return getValue( name ) ; } } } ; final Class<?>[] interfaces = interfaceClasses.toArray( new Class<?>[ interfaceClasses.size() ] ) ; ClassLoader cl = this.getClass().getClassLoader() ; if (cl == null) { cl = interfaces[0].getClass().getClassLoader() ; } if (cl == null) { ClassLoader.getSystemClassLoader() ; } return Proxy.newProxyInstance( cl, interfaces, ih ) ; } //////////////////////////////////////////////////////////////////////////////////// // // Data for built-in test // //////////////////////////////////////////////////////////////////////////////////// private enum PrimaryColor { RED, GREEN, BLUE } ; private interface TestInterface1{ @DefaultValue( "27" ) @Help( "An integer value" ) int value() ; @DefaultValue( "Michigan" ) @Help( "The name of a lake" ) String lake() ; @DefaultValue( "RED" ) @Help( "Pick a color" ) PrimaryColor color() ; } private interface TestInterface2 { @DefaultValue( "http://www.sun.com" ) @Help( "your favorite URL" ) URL url() ; @DefaultValue( "funny:thing,another:thing,something:else" ) @Help( "A list of pairs of the form xxx:yyy" ) @Separator( "," ) StringPair[] arrayData() ; @DefaultValue( "funny:thing,another:thing,something:else" ) @Help( "A list of pairs of the form xxx:yyy" ) @Separator( "," ) List<StringPair> listData() ; } public static void main( String[] args ) { Class<?>[] interfaces = { TestInterface1.class, TestInterface2.class } ; ArgParser ap = new ArgParser( Arrays.asList( interfaces ) ) ; System.out.println( "Help text for this parser:\n" + ap.getHelpText() ) ; Object result = ap.parse( args ) ; if (!(result instanceof TestInterface1) || !(result instanceof TestInterface2)) { System.out.println( "Error: result not an instance of both test interfaces") ; } System.out.println( "Result is:\n" + result ) ; } }