ArgsToStruct.java

/***************************************************************************
   Copyright 2015 Emily Estes

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
***************************************************************************/
package net.metanotion.util;


import java.io.File;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Set;
import javax.inject.Named;

/** This is a reflection based command line option parser. Given a Java class with a field name "x" it looks for a
command line argument of "-x:<i>&lt;value></i>" unless "x" is a boolean, then it interprets the presence of the switch
as true and the absence as false. It only understands strings, {@link java.io.File}, integers, and boolean types. Also
the {@link javax.inject.Named} annotation can be used on a field to alter the name of the command line argument.
	@param <C> The type of the Java object this argument parser instance will generate.
*/
public final class ArgsToStruct<C> {
	private static final Set<String> emptySet = Collections.emptySet();
	private final Class<C> struct;
	private final Set<String> required;
	private final Set<String> ignore;

	/** Create an argument parser to convert command line arguments into a struct.
		@param struct The type of configuration object to produce.
	*/
	public ArgsToStruct(final Class<C> struct) { this(struct, emptySet, emptySet); }

	/** Create an argument parser to convert command line arguments into a struct.
		@param struct The type of configuration object to produce.
		@param required A list of all the field names required to be non-null.
		@param ignore A list of all the field names to ignore in the class.
	*/
	public ArgsToStruct(final Class<C> struct, Set<String> required, Set<String> ignore) {
		this.struct = struct;
		this.required = required;
		this.ignore = ignore;
	}

	/** Create an instance of the configuration object from the arguments.
		@param args The command line arguments to parse.
		@return A coonfiguration object.
		@throws InstantiationException if it cannot create the configuration object instance.
		@throws IllegalAccessException if the fields are inaccessible.
	*/
	public C getInstance(final String[] args) throws InstantiationException, IllegalAccessException {
		return this.getInstance(args, new Object(), true);
	}

	/** Create an instance of the configuration object from the arguments.
		@param args The command line arguments to parse.
		@param defaults The default object instance containing default values in public, non-static fields for missing
			values.
		@param zeroArgsHelpText If true, call printHelpText with System.out and print out the automatically generated
			help text and throw a runtime exception if args.length == 0.
		@return A configuration object.
		@throws InstantiationException if it cannot create the configuration object instance.
		@throws IllegalAccessException if the fields are inaccessible.
	*/
	public C getInstance(final String[] args,
			final Object defaults,
			final boolean zeroArgsHelpText) throws InstantiationException, IllegalAccessException {
		if((args.length == 0) && zeroArgsHelpText) {
			final PrintWriter writer = new PrintWriter(System.out);
			printHelpText(writer);
			writer.flush();
			throw new RuntimeException("No arguments specified");
		}
		final C inst = struct.newInstance();
		final Class defClass = defaults.getClass();
		for(final Field f: struct.getFields()) {
			final String name = getName(f);
			if(ignore.contains(name)) { continue; }
			final String arg = findArg(name, args);
			if((arg == null) && (required.contains(name))) {
				throw new RuntimeException("Required argument -" + name + " is missing.");
			}
			final Class type = f.getType();
			if(isBasic(type)) {
				final Object defValue = getDefault(name, defClass, defaults);
				final Object o = this.parseSimpleType(arg, type, name, defValue);
				if(o != null) {
					f.setAccessible(true);
					f.set(inst, o);
				}
			}
		}
		return inst;
	}

	/** Generate a help text based on the class structure using the {@link net.metanotion.util.Description} annotation
	to generate usage information.
		@param out The writer to print the help text to.
	*/
	public void printHelpText(final PrintWriter out) {
		out.println("The following options are supported:");
		for(final Field f: struct.getFields()) {
			final String name = getName(f);
			if(ignore.contains(name)) { continue; }
			final Class type = f.getType();
			if(!isBasic(type)) { continue; }
			out.println("\nOption" + (required.contains(name) ? " [REQUIRED]" : "") + " -" + name
				+ "   (type: " + type.getName() + ")");
			final Annotation desc = f.getAnnotation(Description.class);
			if(desc != null) {
				out.println("\tUsage: " + ((Description) desc).value());
			}
		}
	}

	private static String findArg(final String arg, final String[] args) {
		for(int i=0; i < args.length; i++) {
			if(args[i].startsWith("-" + arg)) {
				return args[i];
			}
		}
		return null;
	}

	private static boolean isBasic(final Class type) {
		if((type == Integer.class) ||
			(type == Integer.TYPE) ||
			(type == String.class) ||
			(type == File.class) ||
			(type == Boolean.class) ||
			(type == Boolean.TYPE)) { return true; }
		return false;
	}

	private static Object parseSimpleType(final String arg,
			final Class type,
			final String name,
			final Object def) throws IllegalAccessException {
		Object o = null;
		final String value = ((arg !=null) && (arg.length() >= (name.length() + 2))) ? arg.substring(name.length() + 2) : null;
		if((type == Integer.class) || (type == Integer.TYPE)) {
			if(value == null) {
				o = (Integer) def;
			} else {
				try {
					o = Integer.parseInt(value);
				} catch (NumberFormatException nfe) {
					o = (Integer) def;
				}
			}
		} else if (type == File.class) {
			if(value != null) {
				o = new File(value);
			} else {
				o = (File) def;
			}
		} else if ((type == Boolean.class) || (type == Boolean.TYPE)) {
			if(arg != null) {
				o = true;
			} else {
				o = false;
			}
		} else if (type == String.class) {
			o = value;
			if(o == null) {o = (String) def; }
		}
		return o;
	}

	private static Object getDefault(final String name, final Class defClass, final Object defaults)
			throws IllegalAccessException {
		try {
			return defClass.getField(name).get(defaults);
		} catch (final NoSuchFieldException nsfe) {
			return null;
		}
	}

	private static final String getName(final Field f) {
		final Annotation name = f.getAnnotation(Named.class);
		if(name == null) {
			return f.getName();
		} else {
			return ((Named) name).value();
		}
	}
}