com.redhat.ceylon.common.tool.ToolFactory Maven / Gradle / Ivy
package com.redhat.ceylon.common.tool;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import com.redhat.ceylon.common.tool.OptionArgumentException.ToolInitializationException;
import com.redhat.ceylon.common.tool.OptionArgumentException.UnknownOptionException;
import com.redhat.ceylon.common.tool.OptionModel.ArgumentType;
import com.redhat.ceylon.common.tools.CeylonTool;
/**
* Responsible for instantiating and configuring a {@link Tool} according to
* some command line arguments and a {@link ToolModel}.
* @author tom
*/
public class ToolFactory {
private static final String SHORT_PREFIX = "-";
private static final char LONG_SEP = '=';
private static final String LONG_PREFIX = "--";
public T newInstance(ToolModel toolModel) {
// Since non-Subtools can't be inner classes, it's OK to pass a null outer.
T result = toolModel.getToolLoader().instance(toolModel, null);
if (result == null) {
throw new ToolException("Couldn't create new instance for tool '" + toolModel.getName() + "'");
}
return result;
}
private void setToolLoaderAndModel(ToolModel toolModel, T tool) {
if (toolModel instanceof AnnotatedToolModel) {
AnnotatedToolModel amodel = (AnnotatedToolModel) toolModel;
try {
amodel.getToolClass().getMethod("setToolLoader", ToolLoader.class).invoke(tool, toolModel.getToolLoader());
} catch (NoSuchMethodException e) {
// Ignore
} catch (ReflectiveOperationException e) {
throw new ToolException("Could not instantitate tool " + amodel.getToolClass(), e);
}
try {
amodel.getToolClass().getMethod("setToolModel", ToolModel.class).invoke(tool, toolModel);
} catch (NoSuchMethodException e) {
// Ignore
} catch (ReflectiveOperationException e) {
throw new ToolException("Could not instantitate tool " + amodel.getToolClass(), e);
}
}
}
private static class Binding {
final String givenOption;
final OptionModel optionModel;
final ArgumentModel argumentModel;
final String unparsedArgumentValue;
A value;
public Binding(String givenOption, OptionModel optionModel,
String unparsedArgumentValue) {
super();
this.givenOption = givenOption;
this.optionModel = optionModel;
this.argumentModel = optionModel.getArgument();
this.unparsedArgumentValue = unparsedArgumentValue;
}
public Binding(ArgumentModel argumentModel,
String unparsedArgumentValue) {
super();
this.givenOption = null;
this.optionModel = null;
this.argumentModel = argumentModel;
this.unparsedArgumentValue = unparsedArgumentValue;
}
private Binding(String givenOption, OptionModel optionModel, ArgumentModel argumentModel, A value) {
this.givenOption = givenOption;
this.optionModel = optionModel;
this.argumentModel = argumentModel;
this.unparsedArgumentValue = null;
this.value = value;
}
static Binding> mv(List> bindings) {
List listValue = new ArrayList(bindings.size()); // size is just a best-guess
String givenOption = null;
OptionModel om = null;
ArgumentModel am = null;
for (Binding binding : bindings) {
if (binding.value instanceof Collection) {
listValue.addAll((Collection extends A>)binding.value);
} else {
listValue.add(binding.value);
}
if (om == null) {
om = binding.optionModel;
am = binding.argumentModel;
givenOption = binding.givenOption;
} else if (om != binding.optionModel
|| am != binding.argumentModel) {
throw new ToolException();
}
}
return new Binding>(givenOption,
(OptionModel)om, (ArgumentModel)am, listValue);
}
public OptionArgumentException invalid(Throwable throwable, ArgumentParser> parser) {
String key;
final Object[] args = new Object[3];
final String badValue = unparsedArgumentValue != null ? unparsedArgumentValue : String.valueOf(value);
if (optionModel != null) {
throw new OptionArgumentException.InvalidOptionValueException(throwable, optionModel, givenOption, badValue);
} else {
throw new OptionArgumentException.InvalidArgumentValueException(throwable, argumentModel, badValue);
}
}
}
/**
* Parses the given arguments binding them to a new instance of the
* tool model.
* @throws OptionArgumentException.InvalidOptionValueException If the value given for an option was not legal
* @throws OptionArgumentException.InvalidArgumentValueException If the value given for an argument was not legal
* @throws OptionArgumentException.OptionWithoutArgumentException If there was an option argument without its argument
* @throws OptionArgumentException.UnexpectedArgumentException If there were additional arguments
* @throws OptionArgumentException.OptionMultiplicityException If there were too many or too few occurances of an option
* @throws OptionArgumentException.ArgumentMultiplicityException If there were too many or too few occurances of an argument
*/
public T bindArguments(ToolModel toolModel, CeylonTool mainTool, Iterable args) {
T tool = newInstance(toolModel);
return bindArguments(toolModel, tool, mainTool, args);
}
class ArgumentProcessor {
final List unrecognised = new ArrayList(1);
final List rest = new ArrayList(1);
final Map, List>> bindings = new HashMap, List>>(1);
final Iterator iter;
final ToolModel toolModel;
final T tool;
private CeylonTool mainTool;
ArgumentProcessor(ToolModel toolModel, T tool, CeylonTool mainTool, Iterator iter) {
this.toolModel = toolModel;
this.tool = tool;
this.iter = iter;
this.mainTool = mainTool;
}
void processArguments() {
boolean eoo = false;
int argumentModelIndex = 0;
int argumentsBoundThisIndex = 0;
argloop: while (iter.hasNext()) {
final String arg = iter.next();
OptionModel> option;
String argument;
if (!eoo && isEoo(arg)) {
eoo = true;
continue;
} else if (!eoo && isLongForm(arg)) {
String longName = getLongFormOption(arg);
option = toolModel.getOption(longName);
if (option == null) {
rest.add(arg);
} else {
switch (option.getArgumentType()) {
case BOOLEAN:
argument = getLongFormArgument(arg, null);
if (argument == null) {
argument = "true";
}
break;
case OPTIONAL:
case REQUIRED:
argument = getLongFormArgument(arg, iter);
if (argument == null) {
if (option.getArgumentType() == ArgumentType.REQUIRED) {
if (iter.hasNext()) {
argument = iter.next();
} else {
throw new OptionArgumentException.OptionWithoutArgumentException(option, arg);
}
} else {
argument = "";
}
}
break;
default:
throw new RuntimeException("Assertion failed");
}
processArgument(new Binding(longName, option, argument));
}
} else if (!eoo && isShortForm(arg)) {
for (int idx = 1; idx < arg.length(); idx++) {
char shortName = arg.charAt(idx);
option = toolModel.getOptionByShort(shortName);
if (option == null) {
unrecognised.add(UnknownOptionException.shortOption(toolModel, shortName, arg));
continue argloop;
}
switch (option.getArgumentType()) {
case BOOLEAN:
argument = "true";
break;
case REQUIRED:
if (idx == arg.length() -1) {// argument is next arg
if (!iter.hasNext()) {
throw new OptionArgumentException.OptionWithoutArgumentException(option, arg);
}
argument = iter.next();
} else {// argument is rest of this arg
argument = arg.substring(idx+1);
idx = arg.length()-1;
}
break;
case OPTIONAL:
// Even though the argument is optional the short form is always considered to be valueless
argument = "";
break;
default:
throw new RuntimeException("Assertion failed");
}
processArgument(new Binding(String.valueOf(shortName), option, argument));
}
} else {// an argument
if (toolModel.getRest() != null) {
eoo = true;
}
option = null;
argument = arg;
if (isArgument(arg)) {
final List> argumentModels = toolModel.getArgumentsAndSubtool();
if (argumentModelIndex >= argumentModels.size()) {
if (toolModel.getRest() != null) {
rest.add(arg);
continue;
} else {
throw new OptionArgumentException.UnexpectedArgumentException(arg, toolModel);
}
}
final ArgumentModel> argumentModel = argumentModels.get(argumentModelIndex);
processArgument(new Binding(argumentModel, argument));
argumentsBoundThisIndex++;
if (argumentsBoundThisIndex >= argumentModel.getMultiplicity().getMax()) {
argumentModelIndex++;
argumentsBoundThisIndex = 0;
}
} else {
rest.add(arg);
}
}
}
try {
checkMultiplicities();
applyBindings();
handleRest();
assertAllRecognised();
syncCwd();
invokeInitialize();
} catch (IllegalAccessException e) {
// Programming error
throw new ToolException(e);
}
}
// For CeylonBaseTools make sure the cwd option is the
// same as for the main CeylonTool
private void syncCwd() {
if (tool instanceof CeylonBaseTool) {
CeylonBaseTool cbt = (CeylonBaseTool)tool;
if (mainTool != null && mainTool.getCwd() != null) {
// If the main tool's `cwd` options is set it
// always overrides the one in the given tool
cbt.setCwd(mainTool.getCwd());
}
}
}
private void processArgument(Binding binding) {
List> values = bindings.get(binding.argumentModel);
if (values == null) {
values = new ArrayList>(1);
bindings.put(binding.argumentModel, values);
}
if (binding.argumentModel.getMultiplicity().isMultivalued()) {
binding.value = parseArgument(binding);
} else {
parseAndSetValue(binding);
}
values.add(binding);
}
private void parseAndSetValue(Binding binding) {
binding.value = parseArgument(binding);
setValue(binding);
}
private A parseArgument(Binding binding) {
final ArgumentParser parser = binding.argumentModel.getParser();
// Note parser won't be null, because the ModelBuilder checked
try {
final A value = parser.parse(binding.unparsedArgumentValue, tool);
if (value instanceof Tool) {
/* Special case for subtools: The ToolArgumentParser can
* instantiate the Tool instance given its name, but it cannot
* configure the Tool because it doesn't have access to the
* remaining arguments, so we have to handle that here.
*
* I did think this could be made more beautiful if parse() took ToolFactory
* I though we could just have a Tool-typed setter and let the parse do the heavy lifting
* But that doesn't work because then the parser also needs access to the Iterator
*/
ToolLoader loader = ((ToolArgumentParser)parser).getToolLoader();
ToolModel model = loader.loadToolModel(binding.unparsedArgumentValue);
model.setParentTool(toolModel);
return (A)bindArguments(model, (T)value, mainTool, new Iterable() {
@Override
public Iterator iterator() {
return iter;
}
});
// TODO Improve error messages to include legal options/argument values
// TODO Can I rewrite the CeylonTool to use @Subtool?
// TODO Help support for subtools?
// TODO doc-tool support for subtools
// TODO Rewrite CeylonHelpTool to use a ToolModel setter.
// TODO Rewrite CeylonDocToolTool to use a ToolModel setter.
// TODO Rewrite BashCompletionTool to use a ToolModel setter.
// TODO BashCompletionSupport for ToolModels and Tools
// TODO Write a proper fucking state machine for this shit.
// i.e. Alternation, Sequence, Repetition on top of/part of the tool model
// could write a visitor of that tree to generate synopses?
// TODO Proper ToolModel support for subtools (getSubtoolModel())
// instantiate, get the remaining arguments and call bindArguments recursively
}
return value;
} catch (OptionArgumentException e) {
throw e;
} catch (ToolException e) {
throw e;
} catch (Exception e) {
throw binding.invalid(e, parser);
}
}
private void setValue(Binding binding) {
try {
Object value;
if (binding.argumentModel.getSetter().getParameterTypes()[0].equals(EnumSet.class)) {
value = EnumSet.copyOf((List)binding.value);
} else {
value = binding.value;
}
binding.argumentModel.getSetter().invoke(tool, value);
} catch (IllegalAccessException e) {
throw new ToolException(e);
} catch (InvocationTargetException e) {
throw binding.invalid(e.getCause(), null);
}
}
private void applyBindings() {
for (Map.Entry, List>> entry : bindings.entrySet()) {
final ArgumentModel> argument = entry.getKey();
List values = (List)entry.getValue();
if (argument.getMultiplicity().isMultivalued()) {
Binding extends List>> mv = Binding.mv(values);
setValue(mv);
}
}
}
private void handleRest() throws IllegalAccessException {
if (toolModel.getRest() != null) {
try {
toolModel.getRest().invoke(tool, rest);
} catch (InvocationTargetException e) {
throw new ToolInitializationException(toolModel, e.getCause());
}
} else {
for (String arg : rest) {
unrecognised.add(UnknownOptionException.longOption(toolModel, arg));
}
}
}
private void invokeInitialize() {
try {
tool.initialize(mainTool);
} catch (ToolError to) {
// those are already tool errors, just let them as-is
throw to;
} catch (Exception e) {
throw new ToolInitializationException(toolModel, e);
}
}
private void checkMultiplicities() {
for (Map.Entry, List>> entry : bindings.entrySet()) {
final ArgumentModel> argument = entry.getKey();
List> values = entry.getValue();
checkMultiplicity(argument, values);
}
for (OptionModel> option : toolModel.getOptions()) {
ArgumentModel> argument = option.getArgument();
checkMultiplicity(argument, bindings.get(argument));
}
for (ArgumentModel> argument : toolModel.getArgumentsAndSubtool()) {
argument.getMultiplicity().getMin();
checkMultiplicity(argument, bindings.get(argument));
}
}
private void assertAllRecognised() {
switch (unrecognised.size()) {
case 0:
break;
case 1:
throw unrecognised.get(0);
default:
throw OptionArgumentException.UnknownOptionException.aggregate(unrecognised);
}
}
}
/**
* Parses the given arguments binding them to an existing instance of the
* the tool model.
* You should probably be using {@link #bindArguments(ToolModel, Iterable)},
* there are few tools which need to call this method directly.
*
* @throws OptionArgumentException.InvalidOptionValueException If the value given for an option was not legal
* @throws OptionArgumentException.InvalidArgumentValueException If the value given for an argument was not legal
* @throws OptionArgumentException.OptionWithoutArgumentException If there was an option argument without its argument
* @throws OptionArgumentException.UnexpectedArgumentException If there were additional arguments
* @throws OptionArgumentException.OptionMultiplicityException If there were too many or too few occurances of an option
* @throws OptionArgumentException.ArgumentMultiplicityException If there were too many or too few occurances of an argument
*/
public T bindArguments(ToolModel toolModel, T tool, CeylonTool mainTool, Iterable args) {
setToolLoaderAndModel(toolModel, tool);
ArgumentProcessor invocation = new ArgumentProcessor<>(toolModel, tool, mainTool, args.iterator());
invocation.processArguments();
return tool;
}
public boolean isEoo(final String arg) {
return arg.equals(LONG_PREFIX);
}
public boolean isLongForm(final String arg) {
return arg.startsWith(LONG_PREFIX);
}
public String getLongFormOption(final String arg) {
final int eq = arg.indexOf(LONG_SEP);
String longName;
if (eq == -1) { // long-form option
longName = arg.substring(LONG_PREFIX.length());
} else {// long-form option argument
longName = arg.substring(LONG_PREFIX.length(), eq);
}
return longName;
}
public String getLongFormArgument(final String arg, Iterator iter) {
final int eq = arg.indexOf(LONG_SEP);
if (eq == -1) {
return null;
}
String argument = arg.substring(eq+1);
return argument;
}
public boolean isShortForm(String arg) {
return arg.startsWith(SHORT_PREFIX) && !arg.equals(SHORT_PREFIX);
}
public boolean isArgument(String arg) {
return true;
}
private void checkMultiplicity(final ArgumentModel> argument, List> values) {
OptionModel> option = argument.getOption();
Multiplicity multiplicity = argument.getMultiplicity();
int size = values != null ? values.size() : 0;
if (size < multiplicity.getMin()) {
if (option != null) {
throw new OptionArgumentException.OptionMultiplicityException(
argument.getOption(), getGivenOptions(values), multiplicity.getMin(),
"option.too.few");
} else {
throw new OptionArgumentException.ArgumentMultiplicityException(
argument, multiplicity.getMin(),
"argument.too.few");
}
}
if (size > multiplicity.getMax()) {
if (option != null) {
throw new OptionArgumentException.OptionMultiplicityException(
argument.getOption(), getGivenOptions(values), multiplicity.getMax(),
"option.too.many");
} else {
throw new OptionArgumentException.ArgumentMultiplicityException(
argument, multiplicity.getMax(),
"argument.too.many");
}
}
}
private String getGivenOptions(List> values) {
TreeSet given = new TreeSet<>();
for (Binding> binding : values) {
if (binding.optionModel.getLongName().equals(binding.givenOption)) {
given.add("--"+binding.givenOption);
}
if (binding.optionModel.getShortName() != null
&& binding.givenOption.equals(binding.optionModel.getShortName().toString())) {
given.add("-"+binding.givenOption);
}
}
StringBuilder sb = new StringBuilder();
for (String s : given) {
sb.append('\'').append(s).append("\'/");
}
return sb.substring(0, sb.length()-1);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy