All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redhat.ceylon.common.tool.ToolFactory Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
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)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> 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