/* * Author: tdanford * Date: Nov 6, 2008 */ package org.seqcode.gseutils.models; import java.lang.reflect.*; import java.util.*; /** * Model-based command-line argument parsing! * * The class takes a Model subclass in the constructor. When the parse() method is called, * it attempts to parse the given arguments and put them in an instance of the Model, which * it then returns. * * If it sees a String field "key" in the Model, it looks to fill it from an instance of * "--key value" on the command line. * * If it sees a Boolean field "flag" in the Model, it looks to fill it with 'true' if * the value-less flag "--flag" appears on the command line, or 'false' otherwise. * * Any remaining fields that are either key-less, or argument listed after the separator * "--", it tries to stuff into a field called 'args' in the Model, if one exists and has the * type String[]. (The name of this field can be modified by passing an additional argument * to the constructor.) * * @author tdanford * * @param <ArgModel> */ public class Arguments<ArgModel extends ArgumentModel> { public static void main(String[] args) { Arguments<Test> a = new Arguments<Test>(Test.class); Test t = a.parse(args); if(!t.checkArgs()) { System.err.println(t.getArgErrors()); } else { System.out.println(t.toString()); } } public static class Test extends ArgumentModel { public static String[] required = { "bar" }; public String[] args; public String foo, bar; public Integer x, y; public Boolean flag; public Integer y() { return 2*x; } } private String remainingArgsName; private Class<ArgModel> modelClass; private ModelFieldAnalysis<ArgModel> analysis; public Arguments(Class<ArgModel> c) { analysis = new ModelFieldAnalysis<ArgModel>(c); modelClass = c; remainingArgsName = "args"; } public Arguments(Class<ArgModel> c, String rem) { analysis = new ModelFieldAnalysis<ArgModel>(c); modelClass = c; remainingArgsName = rem; } public ArgModel parse(String[] args) { try { ArgModel model = modelClass.newInstance(); /* * We need to make sure that the Boolean fields get a "false" value by * default. Everything else will (presumably) get a null, unless there is an * argument-class-specific default constructor that sets up alternate default * values. */ for(Field f : analysis.findTypedFields(Boolean.class)) { f.set(model, Boolean.FALSE); } ArrayList<String> remlist = new ArrayList<String>(); argloop: for(int i = 0; i < args.length; i++) { if(args[i].startsWith("--")) { if(args[i].equals("--")) { // We've seen a divider, so we need to parse the rest of the list // and put *all* the values into the 'remainder' list. for(int j = i + 1; j < args.length; j++) { remlist.add(args[j]); } break argloop; // quits the for-loop altogether. } else { String key = args[i].substring(2, args[i].length()); boolean isFlag = (i == args.length-1) || args[i+1].startsWith("--"); if(isFlag) { // This parses the "--flag" without a corresponding "value" entry. Field f = analysis.findField(key); if(Model.isSubclass(f.getType(), Boolean.class)) { f.set(model, (Boolean)true); } } else { // We're parsing a "--key value" pair in this block. String value = args[++i]; // the ++i in that line, makes sure that we don't double-parse the // args[i+1] element (the 'value'). Field f = analysis.findField(key); if(f != null) { Class type = f.getType(); try { if(Model.isSubclass(type, String.class)) { f.set(model, value); } else if (Model.isSubclass(type, Integer.class)) { f.set(model, Integer.parseInt(value)); } else if (Model.isSubclass(type, Double.class)) { f.set(model, Double.parseDouble(value)); } } catch(NumberFormatException e) { throw new IllegalArgumentException(key); } } } } } else { // This is an entry (a) before a "--" divider, but *not* preceded by a // --key element. So we put it in the "remainder" list, to be handled // at the end. remlist.add(args[i]); } } /* * Any non-keyed, or trailing, arguments were put in the 'remlist' variable. * At this point, we package them all up in an array, and put them in the * contents of the remainingArgsName field (assuming that field is a String[] type). */ Field remf = analysis.findField(remainingArgsName); if(remf != null && Model.isSubclass(remf.getType(), String[].class)) { String[] remarray = remlist.toArray(new String[remlist.size()]); remf.set(model, remarray); } /* * This block looks for any non-static methods, that * (a) have the same name as a field * (b) have a return-type that is a subclass of the corresponding field type * (c) take no arguments. * * For each such method that it finds, it calls the method and sets the value * of the field to be the returned value from the method invocation. * * This lets us define argument-specific transformations -- for instance, * loading a genome object from a genome string, etc. */ Class modelClass = analysis.getModelClass(); Method[] methods = modelClass.getMethods(); for(int i = 0; i < methods.length; i++) { if((methods[i].getModifiers() & Modifier.STATIC) == 0) { if(methods[i].getParameterTypes().length == 0) { String methodName = methods[i].getName(); Field field = analysis.findField(methodName); if(field != null && Model.isSubclass(methods[i].getReturnType(), field.getType())) { Object value = methods[i].invoke(model, null); if(value != null) { field.set(model, value); } } } } } return model; } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return null; } }