package arkref.ext.fig.basic; import static arkref.ext.fig.basic.LogInfo.*; import java.io.*; import java.util.*; import java.lang.annotation.*; import java.lang.reflect.*; class OptInfo { public String group, name, gloss; public String condReq; public boolean required; public boolean specified; public Object obj; // For serialization: sometimes the obj doesn't have enough information, // so we need to use the string that was used to construct the object public String stringRepn; // Used when obj is Random or BufferedReader (hard to get string) // One of the following two are set public Field field; public Method setMethod, getMethod; public String fullName() { return group+"."+name; } // Return "" if field is not an enum type public String getEnumStr() { return getEnumStr(field != null ? field.getType() : getMethod.getReturnType()); } public static String getEnumStr(Class c) { return StrUtils.join(c.getEnumConstants(), "|"); } public Object getValue() { try { return field != null ? field.get(obj) : getMethod.invoke(obj); } catch(InvocationTargetException e) { stderr.println("Can't access method: " + e); return null; } catch(IllegalAccessException e) { stderr.println("Can't access field: " + e); return null; } } // Important to format properly in a way that we can read it and parse it again. public String getValueString() { if(stringRepn != null) return stringRepn; Object o = getValue(); //System.out.println("GOT " + fullName() + " " + o); if(o == null) return ""; if(o instanceof ArrayList) return StrUtils.join((ArrayList)o); if(o instanceof Pair) return ((Pair)o).getFirst() + "," + ((Pair)o).getSecond(); // Array if(objIsArray(o)) { StringBuilder buf = new StringBuilder(); for(int i = 0; i < Array.getLength(o); i++) { if(i > 0) buf.append(' '); buf.append(Array.get(o, i)); } return buf.toString(); } if(o instanceof Random) // Argh, can't get the seed, just assume it's 1 return "1"; return o.toString(); } public String toString() { String valueStr = getValueString(); String s = String.format("%-30s <%5s> : %s [%s]", fullName(), typeStr(), gloss, valueStr); String t = getEnumStr(); if(!t.equals("")) s += " " + t; return s; } public void print() { stdout.println(" " + toString()); } private Type getGenericType() { return field != null ? field.getGenericType() : getMethod.getGenericReturnType(); } private String typeStr() { return typeStr(getGenericType()); } private static boolean isEnum(Type type) { return type instanceof Class && ((Class)type).isEnum(); } // Array detectors static boolean objIsArray(Object o) { return typeIsArray(o.getClass()); } static boolean typeIsArray(Type t) { return t instanceof Class && ((Class)t).getComponentType() != null; } static Class arrayTypeOfObj(Object o) { return arrayTypeOfType(o.getClass()); } static Class arrayTypeOfType(Type t) { return (Class)((Class)t).getComponentType(); } private static boolean isBool(Type type) { return type.equals(boolean.class) || type.equals(Boolean.class); } private static String typeStr(Type type) { if(type.equals(boolean.class) || type.equals(Boolean.class)) return "bool"; if(type.equals(int.class) || type.equals(Integer.class)) return "int"; if(type.equals(short.class) || type.equals(Short.class)) return "shrt"; if(type.equals(double.class) || type.equals(Double.class)) return "dbl"; if(type.equals(String.class)) return "str"; if(type.equals(BufferedReader.class)) return "read"; if(type.equals(Random.class)) return "rand"; if(isEnum(type)) return "enum"; if(typeIsArray(type)) return typeStr(arrayTypeOfType(type)) + "*"; if(type instanceof ParameterizedType) { ParameterizedType ptype = (ParameterizedType)type; type = ptype.getRawType(); Type[] childTypes = ptype.getActualTypeArguments(); if(type.equals(ArrayList.class)) return typeStr(childTypes[0]) + "*"; if(type.equals(Pair.class)) return typeStr(childTypes[0]) + "2"; } return "unk"; } private static boolean checkNumArgs(int want, int have, String fullName) { if(have != want) { stderr.printf(want + " arguments required for " + fullName + ", but got " + have + "\n"); return false; } return true; } // Return errorValue if there's an error (null is a valid value). // type: the data type of the variable // l: the command line arguments to interpret private static String errorValue = "ERROR"; private static Object interpretValue(Type type, List<String> l, String fullName) { int n = l.size(); String firstArg = n > 0 ? l.get(0) : null; if(type.equals(boolean.class) || type.equals(Boolean.class)) { boolean x = (n == 0 ? true : Boolean.parseBoolean(firstArg)); return x; } if(type.equals(int.class) || type.equals(Integer.class)) { if(!checkNumArgs(1, n, fullName)) return errorValue; int x; if(firstArg.equals("MAX")) x = Integer.MAX_VALUE; else if(firstArg.equals("MIN")) x = Integer.MIN_VALUE; else x = Integer.parseInt(firstArg); return x; } if(type.equals(short.class) || type.equals(Short.class)) { if(!checkNumArgs(1, n, fullName)) return errorValue; short x; if(firstArg.equals("MAX")) x = Short.MAX_VALUE; else if(firstArg.equals("MIN")) x = Short.MIN_VALUE; else x = Short.parseShort(firstArg); return x; } if(type.equals(double.class) || type.equals(Double.class)) { if(!checkNumArgs(1, n, fullName)) return errorValue; double x; if(firstArg.equals("MAX")) x = Double.POSITIVE_INFINITY; else if(firstArg.equals("MIN")) x = Double.NEGATIVE_INFINITY; else x = Double.parseDouble(firstArg); return x; } if(type.equals(double[].class)) { double[] x = new double[l.size()]; for(int i = 0; i < l.size(); i++) x[i] = Double.parseDouble(l.get(i)); return x; } if(type.equals(String[].class)) { String[] x = new String[l.size()]; for(int i = 0; i < l.size(); i++) x[i] = l.get(i); return x; } if(type.equals(String.class)) { // Join many arguments using spaces String x = StrUtils.join(l); return x; } if(type.equals(BufferedReader.class)) { if(!checkNumArgs(1, n, fullName)) return errorValue; BufferedReader x = "-".equals(firstArg) ? LogInfo.stdin : IOUtils.openInHard(firstArg); return x; } if(type.equals(Random.class)) { if(!checkNumArgs(1, n, fullName)) return errorValue; // seed 0 means use the time int seed = Integer.parseInt(firstArg); Random x = seed == 0 ? new Random() : new Random(seed); return x; } if(type instanceof Class && ((Class)type).isEnum()) { if(n == 0) return null; if(!checkNumArgs(1, n, fullName)) return errorValue; Object x = Utils.parseEnum((Class)type, firstArg); if(x == null) { stderr.println("Invalid enum: '" + firstArg + "'; valid choices: " + getEnumStr((Class)type)); return errorValue; } return x; } // Foo[], where Foo is any class if(typeIsArray(type)) { // Put the elements in the array Class childType = arrayTypeOfType(type); Object x = Array.newInstance(childType, l.size()); int i = 0; for(String a : l) { Object o = interpretValue(childType, ListUtils.newList(a), fullName); if(o == errorValue) return errorValue; Array.set(x, i++, o); } return x; } // Pair or ArrayList if(type instanceof ParameterizedType) { // Types involving generics: pair, arraylist ParameterizedType ptype = (ParameterizedType)type; type = ptype.getRawType(); Type[] childTypes = ptype.getActualTypeArguments(); if(type.equals(Pair.class)) { // Delimited by comma if(!checkNumArgs(1, n, fullName)) return errorValue; // Put the elements in the array String[] tokens = firstArg.split(",", 2); if(tokens.length != 2) { stderr.println("Invalid pair: '" + firstArg + "'"); return errorValue; } Object o1 = interpretValue(childTypes[0], ListUtils.newList(tokens[0]), fullName); if(o1 == errorValue) return errorValue; Object o2 = interpretValue(childTypes[1], ListUtils.newList(tokens[1]), fullName); if(o2 == errorValue) return errorValue; return new Pair(o1, o2); } else if(type.equals(List.class) || type.equals(ArrayList.class)) { ArrayList x = new ArrayList(); // Put the elements in the array for(String a : l) { Object o = interpretValue(childTypes[0], ListUtils.newList(a), fullName); if(o == errorValue) return errorValue; x.add(o); } return x; } } // Try to construct the weird type using the constructor // that takes one string argument. if(type instanceof Class) { try { Constructor con = ((Class)type).getConstructor(String.class); return con.newInstance(new Object[] { StrUtils.join(l) }); } catch(Exception e) { stderr.println("Failed to construct " + type + ": " + e); e.printStackTrace(); return errorValue; } } stderr.println("Can't handle weird field type: " + type); return errorValue; } private void setField(Object v) throws IllegalAccessException, InvocationTargetException { if (!tryToUseSetters(v)) { if(field != null) field.set(obj, v); else setMethod.invoke(obj, v); } } private boolean tryToUseSetters(Object v) { if(field == null) return false; String targetMethodName = "set" + field.getName(); Method[] methods = obj.getClass().getMethods(); for (Method m: methods) { String methodName = m.getName().toLowerCase(); if (methodName.equalsIgnoreCase(targetMethodName)) { try { m.invoke(obj, v); } catch (Exception e) { return false; } return true; } } return false; } public boolean set(List<String> l, boolean append) { try { Object v = interpretValue(getGenericType(), l, fullName()); if(v == errorValue) return false; //System.out.println(name + " " + stringRepn + " " + v); if(!append) { // Treat boolean case specially because -flag means true, and l is empty if(isBool(getGenericType())) stringRepn = v.toString(); else stringRepn = StrUtils.join(l); // field.set(obj, v); setField(v); } else { Object oldv = field.get(obj); //System.out.println("append " + l); //System.out.println((oldv == null ? "" : (String)oldv + " ") + v); stringRepn = (stringRepn == null ? "" : stringRepn + " ") + StrUtils.join(l); if(oldv instanceof ArrayList) ((ArrayList)oldv).addAll((ArrayList)v); else if(oldv instanceof String) // field.set(obj, (oldv == null ? "" : (String)oldv + " ") + v); setField((oldv == null ? "" : (String)oldv + " ") + v); } } catch(InvocationTargetException e) { stderr.println("Can't set method: " + e); return false; } catch(IllegalAccessException e) { stderr.println("Can't set field: " + e); return false; } specified = true; return true; } } /** * Due to historical reasons, all the member functions are prefixed with do, * and all the static functions (apply to the global theParser instance) * are not. * * 3/1/2007: static methods register and parse have been deprecated. * Please create an instance and use the doRegister and doParse counterparts. */ public class OptionsParser { public OptionsParser() { } public OptionsParser(Object... objects) { doRegisterAll(objects); } public OptionsParser doRegister(String group, Object o) { if(objects.containsKey(group)) throw Exceptions.bad("Group name already exists: " + group); objects.put(group, o); // Recursively register its option sets for(Field field : classOf(o).getFields()) { OptionSet ann = (OptionSet)field.getAnnotation(OptionSet.class); if(ann == null) continue; try { doRegister(group+"."+ann.name(), field.get(o)); } catch(IllegalAccessException e) { throw Exceptions.bad("Can't access field: " + e); } } for(Method method : classOf(o).getMethods()) { OptionSet ann = (OptionSet)method.getAnnotation(OptionSet.class); if(ann == null) continue; try { doRegister(group+"."+ann.name(), method.invoke(o)); } catch(InvocationTargetException e) { throw Exceptions.bad("Can't access method: " + e); } catch(IllegalAccessException e) { throw Exceptions.bad("Can't access method: " + e); } } return this; } public OptionsParser doRegisterAll(Object[] objects) { // Strings are interpreted as the key name for the next object. String name = null; for(Object o : objects) { if(o == null) continue; if(o instanceof String) name = (String)o; else { if(name == null) { if(o instanceof Class) name = ((Class)o).getSimpleName(); else name = o.getClass().getSimpleName(); } doRegister(name, o); name = null; } } return this; } @Deprecated // Don't use the static methods public static void register(String group, Object o) { theParser.doRegister(group, o); } @Deprecated // Don't use the static methods public static void registerAll(Object[] objects) { theParser.doRegisterAll(objects); } private static Class classOf(Object o) { return (o instanceof Class) ? (Class)o : o.getClass(); } private List<OptInfo> matchOpt(ArrayList<OptInfo> options, String s, boolean allowMultipleMatches) { s = s.toLowerCase(); ArrayList<OptInfo> completeMatches = new ArrayList<OptInfo>(); ArrayList<OptInfo> partialMatches = new ArrayList<OptInfo>(); for(OptInfo opt : options) { String t; // First try to match full name t = opt.fullName().toLowerCase(); if(t.equals(s)) completeMatches.add(opt); if(t.startsWith(s)) partialMatches.add(opt); // Otherwise, match name (without the group) if(!mustMatchFullName) { t = opt.name.toLowerCase(); if(t.equals(s)) completeMatches.add(opt); if(t.startsWith(s)) partialMatches.add(opt); } } if(completeMatches.size()+partialMatches.size() == 0) { if(!ignoreUnknownOpts) stderr.println("Unknown option: '" + s + "'; -help for usage"); return ListUtils.newList(); } if(allowMultipleMatches) return partialMatches; else { // Enforce one match if(completeMatches.size() == 1) return ListUtils.newList(completeMatches.get(0)); if(completeMatches.size() == 0 && partialMatches.size() == 1) return ListUtils.newList(partialMatches.get(0)); stderr.println("Ambiguous option: '" + s + "'; possible matches:"); for(OptInfo opt : partialMatches) opt.print(); return ListUtils.newList(); } } private static void printHelp(List<OptInfo> options) { stdout.println("Usage:"); for(OptInfo opt : options) opt.print(); } public void printHelp() { printHelp(options); } private ArrayList<OptInfo> getOptInfos() { ArrayList<OptInfo> options = new ArrayList<OptInfo>(); // For each group... for(String group : objects.keySet()) { Object obj = objects.get(group); // For each field that has an option annotation... //for(Field field : classOf(obj).getDeclaredFields()) { for(Field field : classOf(obj).getFields()) { Option ann = (Option)field.getAnnotation(Option.class); if(ann == null) continue; // Get the option OptInfo opt = new OptInfo(); opt.group = group; opt.name = ann.name().equals("") ? field.getName() : ann.name(); opt.gloss = ann.gloss(); opt.condReq = ann.condReq(); opt.required = ann.required(); opt.obj = obj; opt.field = field; options.add(opt); //System.out.println("OPT " + opt.name); } // In Scala, "@Option var x" generates two methods // a setter and a getter // public int Options.x() // public void Options.x_$eq(int) // Map getter method name to the option HashMap<String,OptInfo> optMap = new HashMap(); for(Method method : classOf(obj).getMethods()) { Option ann = (Option)method.getAnnotation(Option.class); if(ann == null) continue; //System.out.println("OPT " + method); String getterName = method.getName().replace("_$eq", ""); OptInfo opt = optMap.get(getterName); if(opt == null) { opt = new OptInfo(); opt.group = group; opt.name = ann.name().equals("") ? method.getName() : ann.name(); opt.gloss = ann.gloss(); opt.condReq = ann.condReq(); opt.required = ann.required(); opt.obj = obj; options.add(opt); optMap.put(getterName, opt); } // Get the option if(method.getName().endsWith("_$eq")) // setter opt.setMethod = method; else // getter opt.getMethod = method; } } for(OptInfo opt : options) { if(!(opt.field != null || (opt.getMethod != null && opt.setMethod != null))) System.err.printf("%s must have either field or a getter/setter pair (probably missing setter; use var instead of val in Scala)\n", opt.fullName()); } return options; } // Options file: one option per line // Key and value separated by tab (or spaces). private boolean readOptionsFile(ArrayList<OptInfo> options, String file) { if(new File(file).isDirectory()) file = new File(file, defaultDirFileName).toString(); boolean ignoreOpts = new File(file).getName().equals(ignoreOptsFileName); try { //OrderedStringMap map = OrderedStringMap.fromFile(file); // {12/06/08}: Allow spaces BufferedReader in = IOUtils.openIn(file); String line; while((line = in.readLine()) != null) { line = line.trim(); if(line.length() == 0 || line.startsWith("#")) continue; String[] tokens = line.split("\\s+", 2); String key = tokens[0]; String val = (tokens.length > 1 ? tokens[1] : ""); boolean append = false; if(key.startsWith("+")) { append = true; key = key.substring(1); } if(key.equals("!include")) { // Include other file if(!readOptionsFile(options, val)) return false; } else { for(OptInfo opt : matchOpt(options, key, false)) { if(ignoreOpts && ignoreFileNameOpts.contains(opt.fullName())) continue; if(!opt.set(Arrays.asList(StrUtils.split(val)), append)) return false; } } } } catch(IOException e) { stderr.println(e); return false; } return true; } public boolean parseOptionsFile(String path) { ArrayList<OptInfo> options = getOptInfos(); return readOptionsFile(options, path); } // Return true iff x is a strict prefix of private static boolean isStrictPrefixOf(String x, String... ys) { for(String y : ys) if(x.startsWith(y) && x.length() > y.length()) return true; return false; } private static String stripDashes(String s) { int i = 0; while(i < s.length() && (s.charAt(i) == '-' || s.charAt(i) == '+')) i++; return s.substring(i); } @Deprecated public static boolean parse(String[] args) { return theParser.doParse(args); } public void doParseHard(String[] args) { if(!doParse(args)) throw new RuntimeException("Parsing '" + StrUtils.join(args) + "' failed"); } public boolean doParse(String[] args) { if(this.options == null) this.options = getOptInfos(); // For each command-line argument... for(int i = 0; i < args.length;) { if(args[i].equals("-help")) { // Get usage help printHelp(options); i++; return false; //if(!ignoreUnknownOpts) continue; //else return false; } else if(isStrictPrefixOf(args[i], "++")) { if(!readOptionsFile(options, args[i++].substring(2))) { if(ignoreUnknownOpts) continue; else return false; } } else if(isStrictPrefixOf(args[i], "-", "+", "--")) { boolean append = args[i].startsWith("+"); boolean allowMultipleMatches = args[i].startsWith("--"); //System.err.println(allowMultipleMatches + " " + args[i]); List<OptInfo> opts = matchOpt(options, stripDashes(args[i++]), allowMultipleMatches); // Get the data values of this parameter ArrayList<String> l = new ArrayList<String>(); boolean nextIsVerbatim = false; boolean allIsVerbatim = false; while(i < args.length) { if(args[i].equals("--")) nextIsVerbatim = true; else if(args[i].equals("---")) allIsVerbatim = !allIsVerbatim; else { if(!allIsVerbatim && !nextIsVerbatim && (isStrictPrefixOf(args[i], "+", "-", "++"))) break; l.add(args[i]); nextIsVerbatim = false; } i++; } if(opts.size() == 0 && !ignoreUnknownOpts) return false; for(OptInfo opt : opts) { if(!opt.set(l, append)) { if(ignoreUnknownOpts) continue; else return false; } } } else { stderr.println("Argument not part of an option: " + args[i]); if(!ignoreUnknownOpts) return false; } } // Check that all required options are specified if(!relaxRequired) { List<String> missingOptMsgs = new ArrayList<String>(); for(OptInfo o : options) { String msg = isMissing(o, options); if(msg != null) missingOptMsgs.add(msg); } if(missingOptMsgs.size() > 0) { stderr.println("Missing required option(s):"); for(String msg : missingOptMsgs) stderr.println(msg); return false; } } return true; } // Return the option info with the given name (which could be full or not). // If not, then prepend the given group. private OptInfo findOptInfo(List<OptInfo> optInfos, String name, String group) { for(OptInfo info : optInfos) if(info.fullName().equals(name)) return info; name = group + "." + name; for(OptInfo info : optInfos) if(info.fullName().equals(name)) return info; return null; } // If the option is missing, return the message (to be printed out) of why // Otherwise, return null private String isMissing(OptInfo o, List<OptInfo> optInfos) { if(o.specified) return null; // Specified, we're fine if(o.required) return o.toString(); // This option is required if(!StrUtils.isEmpty(o.condReq)) { // This option is conditionally required String[] tokens = o.condReq.split("=", 2); String name = tokens[0], value = tokens.length == 2 ? tokens[1] : null; OptInfo info = findOptInfo(optInfos, name, o.group); boolean missing; if(info == null) // Shouldn't happen, but if it does, the user will be notified return o.toString() + ", " + name + " not found"; else if(value == null) { // Just need to be specified if(info.specified) return o.toString() + ", " + name + " specified"; } else { if(info.getValue() instanceof ArrayList) { // For an array, suffices if just one element matches for(Object x : (ArrayList)info.getValue()) if(x.toString().matches(value)) return o.toString() + ", " + o.condReq + " holds"; } else { if(info.getValueString().matches(value)) return o.toString() + ", " + o.condReq + " holds"; } } } return null; } // Return a list of options (verbose - human-readable) @Deprecated public static OrderedStringMap getOptionStrings() { return theParser.doGetOptionStrings(); } public OrderedStringMap doGetOptionStrings() { if(this.options == null) this.options = getOptInfos(); OrderedStringMap map = new OrderedStringMap(); for(OptInfo opt : options) map.put(opt.toString()); return map; } // Return a list of option pairs (mapping name to value) @Deprecated public static OrderedStringMap getOptionPairs() { return theParser.doGetOptionPairs(); } public OrderedStringMap doGetOptionPairs() { if(this.options == null) this.options = getOptInfos(); OrderedStringMap map = new OrderedStringMap(); for(OptInfo opt : options) map.put(opt.fullName(), opt.getValueString()); return map; } public boolean writeEasy(String path) { return doGetOptionPairs().printEasy(path); } public OptionsParser setDefaultDirFileName(String defaultDirFileName) { this.defaultDirFileName = defaultDirFileName; return this; } public OptionsParser setIgnoreOptsFromFileName(String ignoreOptsFileName, List<String> ignoreFileNameOpts) { this.ignoreOptsFileName = ignoreOptsFileName; this.ignoreFileNameOpts = ignoreFileNameOpts; return this; } public OptionsParser relaxRequired() { this.relaxRequired = true; return this; } public OptionsParser ignoreUnknownOpts() { this.ignoreUnknownOpts = true; return this; } public OptionsParser mustMatchFullName() { this.mustMatchFullName = true; return this; } //public String getHotSpec() { return hotSpec; } // Each object could either be a class or an object. private HashMap<String, Object> objects = new HashMap<String, Object>(); private ArrayList<OptInfo> options; //private String hotSpec; // Settings for parsing private String defaultDirFileName; // If ++<dir> is specified, read from <dir>/<defaultDirFileName> private String ignoreOptsFileName; // If reading a file with this file name... private List<String> ignoreFileNameOpts; // ignore these options private boolean relaxRequired; // Forget about having to have all options private boolean ignoreUnknownOpts; // Don't stop parsing if have error private boolean mustMatchFullName; // Must include group and name @Deprecated public static final OptionsParser theParser = new OptionsParser(); }