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 extends Tool> T newInstance(ToolModel<T> 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 <T extends Tool> void setToolLoaderAndModel(ToolModel<T> toolModel, T tool) { if (toolModel instanceof AnnotatedToolModel) { AnnotatedToolModel<T> amodel = (AnnotatedToolModel<T>) 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<A> { final String givenOption; final OptionModel<A> optionModel; final ArgumentModel<A> argumentModel; final String unparsedArgumentValue; A value; public Binding(String givenOption, OptionModel<A> optionModel, String unparsedArgumentValue) { super(); this.givenOption = givenOption; this.optionModel = optionModel; this.argumentModel = optionModel.getArgument(); this.unparsedArgumentValue = unparsedArgumentValue; } public Binding(ArgumentModel<A> argumentModel, String unparsedArgumentValue) { super(); this.givenOption = null; this.optionModel = null; this.argumentModel = argumentModel; this.unparsedArgumentValue = unparsedArgumentValue; } private Binding(String givenOption, OptionModel<A> optionModel, ArgumentModel<A> argumentModel, A value) { this.givenOption = givenOption; this.optionModel = optionModel; this.argumentModel = argumentModel; this.unparsedArgumentValue = null; this.value = value; } static <A> Binding<List<A>> mv(List<Binding<A>> bindings) { List<A> listValue = new ArrayList<A>(bindings.size()); // size is just a best-guess String givenOption = null; OptionModel<A> om = null; ArgumentModel<A> am = null; for (Binding<A> 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<List<A>>(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 extends Tool> T bindArguments(ToolModel<T> toolModel, CeylonTool mainTool, Iterable<String> args) { T tool = newInstance(toolModel); return bindArguments(toolModel, tool, mainTool, args); } class ArgumentProcessor<T extends Tool> { final List<UnknownOptionException> unrecognised = new ArrayList<UnknownOptionException>(1); final List<String> rest = new ArrayList<String>(1); final Map<ArgumentModel<?>, List<Binding<?>>> bindings = new HashMap<ArgumentModel<?>, List<Binding<?>>>(1); final Iterator<String> iter; final ToolModel<T> toolModel; final T tool; private CeylonTool mainTool; ArgumentProcessor(ToolModel<T> toolModel, T tool, CeylonTool mainTool, Iterator<String> 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 NOT_ALLOWED: 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 NOT_ALLOWED: 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<ArgumentModel<?>> 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(); invokeInitialize(); } catch (IllegalAccessException e) { // Programming error throw new ToolException(e); } } private <A> void processArgument(Binding<A> binding) { List<Binding<?>> values = bindings.get(binding.argumentModel); if (values == null) { values = new ArrayList<Binding<?>>(1); bindings.put(binding.argumentModel, values); } if (binding.argumentModel.getMultiplicity().isMultivalued()) { binding.value = parseArgument(binding); } else { parseAndSetValue(binding); } values.add(binding); } private <A> void parseAndSetValue(Binding<A> binding) { binding.value = parseArgument(binding); setValue(binding); } private <A> A parseArgument(Binding<A> binding) { final ArgumentParser<A> 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<T> model = loader.loadToolModel(binding.unparsedArgumentValue); model.setParentTool(toolModel); return (A)bindArguments(model, (T)value, mainTool, new Iterable<String>() { @Override public Iterator<String> 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 <A> void setValue(Binding<A> 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<ArgumentModel<?>, List<Binding<?>>> 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<ArgumentModel<?>, List<Binding<?>>> entry : bindings.entrySet()) { final ArgumentModel<?> argument = entry.getKey(); List<Binding<?>> 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 extends Tool> T bindArguments(ToolModel<T> toolModel, T tool, CeylonTool mainTool, Iterable<String> args) { setToolLoaderAndModel(toolModel, tool); ArgumentProcessor<T> 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<String> 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<Binding<?>> 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<Binding<?>> values) { TreeSet<String> 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); } }