com.github.rvesse.airline.model.MetadataLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.github.rvesse.airline.model.MetadataLoader.java

Source

/**
 * Copyright (C) 2010-16 the original author or authors.
 *
 * 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 com.github.rvesse.airline.model;

import com.github.rvesse.airline.*;
import com.github.rvesse.airline.annotations.Alias;
import com.github.rvesse.airline.annotations.Arguments;
import com.github.rvesse.airline.annotations.Command;
import com.github.rvesse.airline.annotations.DefaultOption;
import com.github.rvesse.airline.annotations.Group;
import com.github.rvesse.airline.annotations.Groups;
import com.github.rvesse.airline.annotations.Option;
import com.github.rvesse.airline.annotations.OptionType;
import com.github.rvesse.airline.annotations.Parser;
import com.github.rvesse.airline.annotations.restrictions.Partial;
import com.github.rvesse.airline.annotations.restrictions.Partials;
import com.github.rvesse.airline.builder.ParserBuilder;
import com.github.rvesse.airline.help.sections.HelpSection;
import com.github.rvesse.airline.help.sections.factories.HelpSectionRegistry;
import com.github.rvesse.airline.help.suggester.Suggester;
import com.github.rvesse.airline.parser.ParserUtil;
import com.github.rvesse.airline.parser.options.OptionParser;
import com.github.rvesse.airline.restrictions.ArgumentsRestriction;
import com.github.rvesse.airline.restrictions.GlobalRestriction;
import com.github.rvesse.airline.restrictions.OptionRestriction;
import com.github.rvesse.airline.restrictions.common.PartialRestriction;
import com.github.rvesse.airline.restrictions.factories.RestrictionRegistry;
import com.github.rvesse.airline.utils.AirlineUtils;
import com.github.rvesse.airline.utils.comparators.StringHierarchyComparator;
import com.github.rvesse.airline.utils.predicates.parser.CommandTypeFinder;
import com.github.rvesse.airline.utils.predicates.parser.GroupFinder;

import javax.inject.Inject;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IteratorUtils;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;

/**
 * Helper for loading meta-data
 *
 */
public class MetadataLoader {

    public static <C> ParserMetadata<C> loadParser(Class<?> cliClass) {
        if (cliClass == null)
            return ParserBuilder.<C>defaultConfiguration();

        Annotation annotation = cliClass.getAnnotation(Parser.class);
        if (annotation == null)
            return ParserBuilder.<C>defaultConfiguration();

        return loadParser((Parser) annotation);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static <C> ParserMetadata<C> loadParser(Parser parserConfig) {
        ParserBuilder<C> builder = new ParserBuilder<C>();

        // Factory and converter options
        if (!parserConfig.typeConverter().equals(DefaultTypeConverter.class)) {
            builder = builder.withTypeConverter(ParserUtil.createInstance(parserConfig.typeConverter()));
        } else {
            builder = builder.withDefaultTypeConverter();
        }
        if (!parserConfig.commandFactory().equals(DefaultCommandFactory.class)) {
            builder = builder.withCommandFactory(ParserUtil.createInstance(parserConfig.commandFactory()));
        } else {
            builder = builder.withDefaultCommandFactory();
        }

        // Abbreviation options
        if (parserConfig.allowCommandAbbreviation()) {
            builder = builder.withCommandAbbreviation();
        }
        if (parserConfig.allowOptionAbbreviation()) {
            builder = builder.withOptionAbbreviation();
        }

        // Alias options
        if (parserConfig.aliasesOverrideBuiltIns()) {
            builder = builder.withAliasesOverridingBuiltIns();
        }
        if (parserConfig.aliasesMayChain()) {
            builder = builder.withAliasesChaining();
        }
        for (Alias alias : parserConfig.aliases()) {
            builder.withAlias(alias.name()).withArguments(alias.arguments());
        }
        if (!StringUtils.isEmpty(parserConfig.userAliasesFile())) {
            if (parserConfig.userAliasesSearchLocation().length > 0) {
                builder = builder.withUserAliases(parserConfig.userAliasesFile(), parserConfig.userAliasesPrefix(),
                        parserConfig.userAliasesSearchLocation());
            } else {
                builder = builder.withUserAliases(parserConfig.userAliasesFile(), parserConfig.userAliasesPrefix(),
                        new String[] { new File(".").getAbsolutePath() });
            }
        }

        // Parsing options
        builder.withArgumentsSeparator(parserConfig.argumentsSeparator());
        if (parserConfig.defaultParsersFirst() && parserConfig.useDefaultOptionParsers()) {
            builder = builder.withDefaultOptionParsers();
        }
        for (Class<? extends OptionParser> optionParserClass : parserConfig.optionParsers()) {
            OptionParser<C> optionParser = ParserUtil.createInstance(optionParserClass);
            builder = builder.withOptionParser(optionParser);
        }
        if (!parserConfig.defaultParsersFirst() && parserConfig.useDefaultOptionParsers()) {
            builder = builder.withDefaultOptionParsers();
        }

        return builder.build();
    }

    public static <C> GlobalMetadata<C> loadGlobal(Class<?> cliClass) {
        Annotation annotation = cliClass.getAnnotation(com.github.rvesse.airline.annotations.Cli.class);
        if (annotation == null)
            throw new IllegalArgumentException(
                    String.format("Class %s does not have the @Cli annotation", cliClass));

        com.github.rvesse.airline.annotations.Cli cliConfig = (com.github.rvesse.airline.annotations.Cli) annotation;

        // Prepare commands
        CommandMetadata defaultCommand = null;
        if (!cliConfig.defaultCommand().equals(com.github.rvesse.airline.annotations.Cli.NO_DEFAULT.class)) {
            defaultCommand = loadCommand(cliConfig.defaultCommand());
        }
        List<CommandMetadata> defaultGroupCommands = new ArrayList<CommandMetadata>();
        for (Class<?> cls : cliConfig.commands()) {
            defaultGroupCommands.add(loadCommand(cls));
        }

        // Prepare parser configuration
        ParserMetadata<C> parserConfig = cliConfig.parserConfiguration() != null
                ? MetadataLoader.<C>loadParser(cliConfig.parserConfiguration())
                : MetadataLoader.<C>loadParser(cliClass);

        // Prepare restrictions
        // We find restrictions in the following order:
        // 1 - Those declared via annotations
        // 2 - Those declared via the restrictions field of the @Cli annotation
        // 3 - Standard restrictions if the includeDefaultRestrctions field of
        // the @Cli annotation is true
        List<GlobalRestriction> restrictions = new ArrayList<GlobalRestriction>();
        for (Class<? extends Annotation> annotationClass : RestrictionRegistry
                .getGlobalRestrictionAnnotationClasses()) {
            annotation = cliClass.getAnnotation(annotationClass);
            if (annotation == null)
                continue;
            GlobalRestriction restriction = RestrictionRegistry.getGlobalRestriction(annotationClass, annotation);
            if (restriction != null)
                restrictions.add(restriction);
        }
        for (Class<? extends GlobalRestriction> cls : cliConfig.restrictions()) {
            restrictions.add(ParserUtil.createInstance(cls));
        }
        if (cliConfig.includeDefaultRestrictions()) {
            restrictions.addAll(AirlineUtils.arrayToList(GlobalRestriction.DEFAULTS));
        }

        // Prepare groups
        // We sort sub-groups by name length then lexically
        // This means that when we build the groups hierarchy we'll ensure we
        // build the parent groups first wherever possible
        Map<String, CommandGroupMetadata> subGroups = new TreeMap<String, CommandGroupMetadata>(
                new StringHierarchyComparator());
        List<CommandGroupMetadata> groups = new ArrayList<CommandGroupMetadata>();
        for (Group groupAnno : cliConfig.groups()) {
            String groupName = groupAnno.name();
            String subGroupPath = null;
            if (StringUtils.containsWhitespace(groupName)) {
                // Normalize the path
                subGroupPath = StringUtils.join(StringUtils.split(groupAnno.name()), ' ');
            }

            // Maybe a top level group we've already seen
            CommandGroupMetadata group = CollectionUtils.find(groups, new GroupFinder(groupName));
            if (group == null) {
                // Maybe a sub-group we've already seen
                group = subGroups.get(subGroupPath);
            }

            List<CommandMetadata> groupCommands = new ArrayList<CommandMetadata>();
            for (Class<?> cls : groupAnno.commands()) {
                groupCommands.add(loadCommand(cls));
            }

            if (group == null) {
                // Newly discovered group
                //@formatter:off
                group = loadCommandGroup(subGroupPath != null ? subGroupPath : groupName, groupAnno.description(),
                        groupAnno.hidden(), Collections.<CommandGroupMetadata>emptyList(),
                        !groupAnno.defaultCommand().equals(Group.NO_DEFAULT.class)
                                ? loadCommand(groupAnno.defaultCommand())
                                : null,
                        groupCommands);
                //@formatter:on
                if (subGroupPath == null) {
                    groups.add(group);
                } else {
                    // Remember sub-groups for later
                    subGroups.put(subGroupPath, group);
                }
            } else {
                for (CommandMetadata cmd : groupCommands) {
                    group.addCommand(cmd);
                }
            }
        }
        // Build sub-group hierarchy
        buildGroupsHierarchy(groups, subGroups);

        // Find all commands
        List<CommandMetadata> allCommands = new ArrayList<CommandMetadata>();
        allCommands.addAll(defaultGroupCommands);
        if (defaultCommand != null && !defaultGroupCommands.contains(defaultCommand)) {
            allCommands.add(defaultCommand);
        }
        for (CommandGroupMetadata group : groups) {
            allCommands.addAll(group.getCommands());
            if (group.getDefaultCommand() != null) {
                allCommands.add(group.getDefaultCommand());
            }

            Queue<CommandGroupMetadata> subGroupsQueue = new LinkedList<CommandGroupMetadata>();
            subGroupsQueue.addAll(group.getSubGroups());
            while (subGroupsQueue.size() > 0) {
                CommandGroupMetadata subGroup = subGroupsQueue.poll();
                allCommands.addAll(subGroup.getCommands());
                if (subGroup.getDefaultCommand() != null)
                    allCommands.add(subGroup.getDefaultCommand());
                subGroupsQueue.addAll(subGroup.getSubGroups());
            }
        }

        // Post-process to find possible further group assignments
        loadCommandsIntoGroupsByAnnotation(allCommands, groups, defaultGroupCommands);

        return loadGlobal(cliConfig.name(), cliConfig.description(), defaultCommand, defaultGroupCommands, groups,
                restrictions, parserConfig);
    }

    /**
     * Loads global meta-data
     * 
     * @param name
     *            CLI name
     * @param description
     *            CLI description
     * @param defaultCommand
     *            Default Command
     * @param defaultGroupCommands
     *            Default Group Commands
     * @param groups
     *            Command Groups
     * @param parserConfig
     *            Parser Configuration
     * @param restrictions
     *            Restrictions
     * @return Global meta-data
     */
    public static <C> GlobalMetadata<C> loadGlobal(String name, String description, CommandMetadata defaultCommand,
            Iterable<CommandMetadata> defaultGroupCommands, Iterable<CommandGroupMetadata> groups,
            Iterable<GlobalRestriction> restrictions, ParserMetadata<C> parserConfig) {
        List<OptionMetadata> globalOptions = new ArrayList<>();
        if (defaultCommand != null) {
            globalOptions.addAll(defaultCommand.getGlobalOptions());
        }
        for (CommandMetadata command : defaultGroupCommands) {
            globalOptions.addAll(command.getGlobalOptions());
        }
        for (CommandGroupMetadata group : groups) {
            for (CommandMetadata command : group.getCommands()) {
                globalOptions.addAll(command.getGlobalOptions());
            }

            // Remember to also search sub-groups for global options
            Queue<CommandGroupMetadata> subGroups = new LinkedList<CommandGroupMetadata>();
            subGroups.addAll(group.getSubGroups());
            while (subGroups.size() > 0) {
                CommandGroupMetadata subGroup = subGroups.poll();
                for (CommandMetadata command : subGroup.getCommands()) {
                    globalOptions.addAll(command.getGlobalOptions());
                }
                subGroups.addAll(subGroup.getSubGroups());
            }
        }
        globalOptions = ListUtils.unmodifiableList(mergeOptionSet(globalOptions));
        return new GlobalMetadata<C>(name, description, globalOptions, defaultCommand, defaultGroupCommands, groups,
                restrictions, parserConfig);
    }

    /**
     * Loads command group meta-data
     * 
     * @param name
     *            Group name
     * @param description
     *            Group description
     * @param hidden
     *            Whether the group is hidden
     * @param defaultCommand
     *            Default command for the group
     * @param commands
     *            Commands for the group
     * @return Command group meta-data
     */
    public static CommandGroupMetadata loadCommandGroup(String name, String description, boolean hidden,
            Iterable<CommandGroupMetadata> subGroups, CommandMetadata defaultCommand,
            Iterable<CommandMetadata> commands) {
        // Process the name
        if (StringUtils.containsWhitespace(name)) {
            String[] names = StringUtils.split(name);
            name = names[names.length - 1];
        }

        List<OptionMetadata> groupOptions = new ArrayList<OptionMetadata>();
        if (defaultCommand != null) {
            groupOptions.addAll(defaultCommand.getGroupOptions());
        }
        for (CommandMetadata command : commands) {
            groupOptions.addAll(command.getGroupOptions());
        }
        groupOptions = ListUtils.unmodifiableList(mergeOptionSet(groupOptions));
        return new CommandGroupMetadata(name, description, hidden, groupOptions, subGroups, defaultCommand,
                commands);
    }

    /**
     * Loads command meta-data
     * 
     * @param defaultCommands
     *            Default command classes
     * @return Command meta-data
     */
    public static <T> List<CommandMetadata> loadCommands(Iterable<Class<? extends T>> defaultCommands) {
        List<CommandMetadata> commandMetadata = new ArrayList<CommandMetadata>();
        Iterator<Class<? extends T>> iter = defaultCommands.iterator();
        while (iter.hasNext()) {
            commandMetadata.add(loadCommand(iter.next()));
        }
        return commandMetadata;
    }

    /**
     * Loads command meta-data
     * 
     * @param commandType
     *            Command class
     * @return Command meta-data
     */
    public static CommandMetadata loadCommand(Class<?> commandType) {
        if (commandType == null) {
            return null;
        }
        Command command = null;
        List<Group> groups = new ArrayList<>();
        Map<String, HelpSection> helpSections = new HashMap<>();

        for (Class<?> cls = commandType; command == null && !Object.class.equals(cls); cls = cls.getSuperclass()) {
            command = cls.getAnnotation(Command.class);

            if (cls.isAnnotationPresent(Groups.class)) {
                groups.addAll(Arrays.asList(cls.getAnnotation(Groups.class).value()));
            }
            if (cls.isAnnotationPresent(Group.class)) {
                groups.add(cls.getAnnotation(Group.class));
            }
        }

        if (command == null)
            throw new IllegalArgumentException(
                    String.format("Command %s is not annotated with @Command", commandType.getName()));

        // Find help sections
        for (Class<?> cls = commandType; !Object.class.equals(cls); cls = cls.getSuperclass()) {
            for (Class<? extends Annotation> helpAnnotationClass : HelpSectionRegistry.getAnnotationClasses()) {
                Annotation annotation = cls.getAnnotation(helpAnnotationClass);
                if (annotation == null)
                    continue;
                HelpSection section = HelpSectionRegistry.getHelpSection(helpAnnotationClass, annotation);
                if (section == null)
                    continue;

                // Because we're going up the class hierarchy the titled section
                // lowest down the hierarchy should win so if we've already seen
                // a section with this title ignore it
                if (helpSections.containsKey(section.getTitle().toLowerCase(Locale.ENGLISH)))
                    continue;

                helpSections.put(section.getTitle().toLowerCase(Locale.ENGLISH), section);
            }
        }

        String name = command.name();
        String description = command.description().isEmpty() ? null : command.description();
        List<String> groupNames = Arrays.asList(command.groupNames());
        boolean hidden = command.hidden();

        InjectionMetadata injectionMetadata = loadInjectionMetadata(commandType);

        //@formatter:off
        CommandMetadata commandMetadata = new CommandMetadata(name, description, hidden,
                injectionMetadata.globalOptions, injectionMetadata.groupOptions, injectionMetadata.commandOptions,
                injectionMetadata.defaultOption, AirlineUtils.first(injectionMetadata.arguments, null),
                injectionMetadata.metadataInjections, commandType, groupNames, groups,
                AirlineUtils.listCopy(helpSections.values()));
        //@formatter:on

        return commandMetadata;
    }

    /**
     * Loads suggester meta-data
     * 
     * @param suggesterClass
     *            Suggester class
     * @return Suggester meta-data
     */
    public static SuggesterMetadata loadSuggester(Class<? extends Suggester> suggesterClass) {
        InjectionMetadata injectionMetadata = loadInjectionMetadata(suggesterClass);
        return new SuggesterMetadata(suggesterClass, injectionMetadata.metadataInjections);
    }

    /**
     * Loads injection meta-data
     * 
     * @param type
     *            Class
     * @return Injection meta-data
     */
    public static InjectionMetadata loadInjectionMetadata(Class<?> type) {
        InjectionMetadata injectionMetadata = new InjectionMetadata();
        loadInjectionMetadata(type, injectionMetadata, Collections.<Field>emptyList());
        injectionMetadata.compact();
        return injectionMetadata;
    }

    /**
     * Loads injection meta-data
     * 
     * @param type
     *            Class
     * @param injectionMetadata
     *            Injection meta-data
     * @param fields
     *            Fields
     */
    public static void loadInjectionMetadata(Class<?> type, InjectionMetadata injectionMetadata,
            List<Field> fields) {
        if (type.isInterface()) {
            return;
        }
        for (Class<?> cls = type; !Object.class.equals(cls); cls = cls.getSuperclass()) {
            for (Field field : cls.getDeclaredFields()) {
                field.setAccessible(true);
                List<Field> path = new ArrayList<>(fields);
                path.add(field);

                Inject injectAnnotation = field.getAnnotation(Inject.class);
                if (injectAnnotation != null) {
                    if (field.getType().equals(GlobalMetadata.class)
                            || field.getType().equals(CommandGroupMetadata.class)
                            || field.getType().equals(CommandMetadata.class)) {
                        injectionMetadata.metadataInjections.add(new Accessor(path));
                    } else {
                        loadInjectionMetadata(field.getType(), injectionMetadata, path);
                    }
                }

                try {
                    @SuppressWarnings("unchecked")
                    Annotation aGuiceInject = field
                            .getAnnotation((Class<? extends Annotation>) Class.forName("com.google.inject.Inject"));
                    if (aGuiceInject != null) {
                        if (field.getType().equals(GlobalMetadata.class)
                                || field.getType().equals(CommandGroupMetadata.class)
                                || field.getType().equals(CommandMetadata.class)) {
                            injectionMetadata.metadataInjections.add(new Accessor(path));
                        } else {
                            loadInjectionMetadata(field.getType(), injectionMetadata, path);
                        }
                    }
                } catch (ClassNotFoundException e) {
                    // this is ok, means Guice is not on the class path, so
                    // probably not being used
                    // and thus, ok that this did not work.
                } catch (ClassCastException e) {
                    // ignore this too, we're doing some funky cross your
                    // fingers type reflect stuff to play
                    // nicely with Guice
                }

                Option optionAnnotation = field.getAnnotation(Option.class);
                DefaultOption defaultOptionAnnotation = field.getAnnotation(DefaultOption.class);
                if (optionAnnotation != null) {
                    OptionType optionType = optionAnnotation.type();
                    String name;
                    if (!optionAnnotation.title().isEmpty()) {
                        name = optionAnnotation.title();
                    } else {
                        name = field.getName();
                    }

                    List<String> options = AirlineUtils.arrayToList(optionAnnotation.name());
                    String description = optionAnnotation.description();

                    int arity = optionAnnotation.arity();
                    if (arity < 0 && arity != Integer.MIN_VALUE)
                        throw new IllegalArgumentException(String.format("Invalid arity for option %s", name));

                    if (optionAnnotation.arity() >= 0) {
                        arity = optionAnnotation.arity();
                    } else {
                        Class<?> fieldType = field.getType();
                        if (Boolean.class.isAssignableFrom(fieldType)
                                || boolean.class.isAssignableFrom(fieldType)) {
                            arity = 0;
                        } else {
                            arity = 1;
                        }
                    }

                    boolean hidden = optionAnnotation.hidden();
                    boolean override = optionAnnotation.override();
                    boolean sealed = optionAnnotation.sealed();

                    // Find and create restrictions
                    Map<Class<? extends Annotation>, Set<Integer>> partials = loadPartials(field);
                    List<OptionRestriction> restrictions = new ArrayList<OptionRestriction>();
                    for (Class<? extends Annotation> annotationClass : RestrictionRegistry
                            .getOptionRestrictionAnnotationClasses()) {
                        Annotation annotation = field.getAnnotation(annotationClass);
                        if (annotation == null)
                            continue;
                        OptionRestriction restriction = RestrictionRegistry.getOptionRestriction(annotationClass,
                                annotation);
                        if (restriction != null) {
                            // Adjust for partial if necessary
                            if (partials.containsKey(annotationClass))
                                restriction = new PartialRestriction(partials.get(annotationClass), restriction);

                            restrictions.add(restriction);
                        }
                    }

                    //@formatter:off
                    OptionMetadata optionMetadata = new OptionMetadata(optionType, options, name, description,
                            arity, hidden, override, sealed, restrictions, path);
                    //@formatter:on
                    switch (optionType) {
                    case GLOBAL:
                        if (defaultOptionAnnotation != null)
                            throw new IllegalArgumentException(String.format(
                                    "Field %s which defines a global option cannot be annotated with @DefaultOption as this may only be applied to command options",
                                    field));
                        injectionMetadata.globalOptions.add(optionMetadata);
                        break;
                    case GROUP:
                        if (defaultOptionAnnotation != null)
                            throw new IllegalArgumentException(String.format(
                                    "Field %s which defines a global option cannot be annotated with @DefaultOption as this may only be applied to command options",
                                    field));
                        injectionMetadata.groupOptions.add(optionMetadata);
                        break;
                    case COMMAND:
                        // Do we also have a @DefaultOption annotation

                        if (defaultOptionAnnotation != null) {
                            // Can't have both @DefaultOption and @Arguments
                            if (injectionMetadata.arguments.size() > 0)
                                throw new IllegalArgumentException(String.format(
                                        "Field %s cannot be annotated with @DefaultOption because there are fields with @Arguments annotations present",
                                        field));
                            // Can't have more than one @DefaultOption
                            if (injectionMetadata.defaultOption != null)
                                throw new IllegalArgumentException(String.format(
                                        "Command type %s has more than one field with @DefaultOption declared upon it",
                                        type));
                            // Arity of associated @Option must be 1
                            if (optionMetadata.getArity() != 1)
                                throw new IllegalArgumentException(String.format(
                                        "Field %s annotated with @DefaultOption must also have an @Option annotation with an arity of 1",
                                        field));
                            injectionMetadata.defaultOption = optionMetadata;
                        }
                        injectionMetadata.commandOptions.add(optionMetadata);
                        break;
                    }
                }

                if (optionAnnotation == null && defaultOptionAnnotation != null) {
                    // Can't have @DefaultOption on a field without also @Option
                    throw new IllegalArgumentException(String.format(
                            "Field %s annotated with @DefaultOption must also have an @Option annotation", field));
                }

                Arguments argumentsAnnotation = field.getAnnotation(Arguments.class);
                if (field.isAnnotationPresent(Arguments.class)) {
                    // Can't have both @DefaultOption and @Arguments
                    if (injectionMetadata.defaultOption != null)
                        throw new IllegalArgumentException(String.format(
                                "Field %s cannot be annotated with @Arguments because there is a field with @DefaultOption present",
                                field));

                    List<String> titles = new ArrayList<>();

                    if (!(argumentsAnnotation.title().length == 1 && argumentsAnnotation.title()[0].equals(""))) {
                        titles.addAll(AirlineUtils.arrayToList(argumentsAnnotation.title()));
                    } else {
                        titles.add(field.getName());
                    }

                    String description = argumentsAnnotation.description();

                    Map<Class<? extends Annotation>, Set<Integer>> partials = loadPartials(field);
                    List<ArgumentsRestriction> restrictions = new ArrayList<>();
                    for (Class<? extends Annotation> annotationClass : RestrictionRegistry
                            .getArgumentsRestrictionAnnotationClasses()) {
                        Annotation annotation = field.getAnnotation(annotationClass);
                        if (annotation == null)
                            continue;
                        ArgumentsRestriction restriction = RestrictionRegistry
                                .getArgumentsRestriction(annotationClass, annotation);
                        if (restriction != null) {
                            // Adjust for partial if necessary
                            if (partials.containsKey(annotationClass))
                                restriction = new PartialRestriction(partials.get(annotationClass), restriction);

                            restrictions.add(restriction);
                        }
                    }

                    //@formatter:off
                    injectionMetadata.arguments.add(new ArgumentsMetadata(titles, description, restrictions, path));
                    //@formatter:on
                }
            }
        }
    }

    private static Map<Class<? extends Annotation>, Set<Integer>> loadPartials(Field field) {
        Map<Class<? extends Annotation>, Set<Integer>> partials = new HashMap<>();

        Annotation partialsAnnotation = field.getAnnotation(Partials.class);
        if (partialsAnnotation != null) {
            for (Partial partial : ((Partials) partialsAnnotation).value()) {
                collectPartial(partials, partial);
            }
        }
        Annotation partialAnnotation = field.getAnnotation(Partial.class);
        if (partialAnnotation != null) {
            collectPartial(partials, (Partial) partialAnnotation);
        }

        return partials;
    }

    private static void collectPartial(Map<Class<? extends Annotation>, Set<Integer>> partials, Partial partial) {
        Set<Integer> indices = partials.get(partial.restriction());
        if (indices == null) {
            indices = new HashSet<>();
            partials.put(partial.restriction(), indices);
        }
        indices.addAll(AirlineUtils.arrayToList(ArrayUtils.toObject(partial.appliesTo())));
    }

    private static List<OptionMetadata> mergeOptionSet(List<OptionMetadata> options) {
        Map<OptionMetadata, List<OptionMetadata>> metadataIndex = new HashMap<>();
        for (OptionMetadata option : options) {
            List<OptionMetadata> list = metadataIndex.get(option);
            if (list == null) {
                list = new ArrayList<OptionMetadata>();
                metadataIndex.put(option, list);
            }
            list.add(option);
        }

        options = new ArrayList<OptionMetadata>();
        for (List<OptionMetadata> ops : metadataIndex.values()) {
            options.add(new OptionMetadata(ops));
        }
        options = ListUtils.unmodifiableList(options);

        Map<String, OptionMetadata> optionIndex = new LinkedHashMap<>();
        for (OptionMetadata option : options) {
            for (String optionName : option.getOptions()) {
                if (optionIndex.containsKey(optionName)) {
                    throw new IllegalArgumentException(
                            String.format("Fields %s and %s have conflicting definitions of option %s",
                                    optionIndex.get(optionName).getAccessors().iterator().next(),
                                    option.getAccessors().iterator().next(), optionName));
                }
                optionIndex.put(optionName, option);
            }
        }

        return options;
    }

    private static List<OptionMetadata> overrideOptionSet(List<OptionMetadata> options) {
        options = ListUtils.unmodifiableList(options);

        Map<Set<String>, OptionMetadata> optionIndex = new HashMap<>();
        for (OptionMetadata option : options) {
            Set<String> names = option.getOptions();
            if (optionIndex.containsKey(names)) {
                // Multiple classes in the hierarchy define this option
                // Determine if we can successfully override this option
                tryOverrideOptions(optionIndex, names, option);
            } else {
                // Need to check there isn't another option with partial overlap
                // of names, this is considered an illegal override
                for (Set<String> existingNames : optionIndex.keySet()) {
                    Set<String> intersection = AirlineUtils.intersection(names, existingNames);
                    if (intersection.size() > 0) {
                        throw new IllegalArgumentException(String.format(
                                "Fields %s and %s have overlapping definitions of option %s, options can only be overridden if they have precisely the same set of option names",
                                option.getAccessors().iterator().next(),
                                optionIndex.get(existingNames).getAccessors().iterator().next(), intersection));
                    }
                }

                optionIndex.put(names, option);
            }
        }

        return ListUtils.unmodifiableList(IteratorUtils.toList(optionIndex.values().iterator()));
    }

    private static void tryOverrideOptions(Map<Set<String>, OptionMetadata> optionIndex, Set<String> names,
            OptionMetadata parent) {

        // As the metadata is extracted from the deepest class in the hierarchy
        // going upwards we need to treat the passed option as the parent and
        // the pre-existing option definition as the child
        OptionMetadata child = optionIndex.get(names);

        Accessor parentField = parent.getAccessors().iterator().next();
        Accessor childField = child.getAccessors().iterator().next();

        // Check for duplicates
        boolean isDuplicate = parent == child || parent.equals(child);

        // Parent must not state it is sealed UNLESS it is a duplicate which can
        // happen when using @Inject to inject options via delegates
        if (parent.isSealed() && !isDuplicate)
            throw new IllegalArgumentException(String.format(
                    "Fields %s and %s have conflicting definitions of option %s - parent field %s declares itself as sealed and cannot be overridden",
                    parentField, childField, names, parentField));

        // Child must explicitly state that it overrides otherwise we cannot
        // override UNLESS it is the case that this is a duplicate which
        // can happen when using @Inject to inject options via delegates
        if (!child.isOverride() && !isDuplicate)
            throw new IllegalArgumentException(String.format(
                    "Fields %s and %s have conflicting definitions of option %s - if you wanted to override this option you must explicitly specify override = true in your child field annotation",
                    parentField, childField, names));

        // Attempt overriding, this will error if the overriding is not possible
        OptionMetadata merged = OptionMetadata.override(names, parent, child);
        optionIndex.put(names, merged);
    }

    public static void loadCommandsIntoGroupsByAnnotation(List<CommandMetadata> allCommands,
            List<CommandGroupMetadata> commandGroups, List<CommandMetadata> defaultCommandGroup) {
        List<CommandMetadata> newCommands = new ArrayList<CommandMetadata>();

        // first, create any groups explicitly annotated
        createGroupsFromAnnotations(allCommands, newCommands, commandGroups, defaultCommandGroup);

        for (CommandMetadata command : allCommands) {
            boolean addedToGroup = false;

            // now add the command to any groupNames specified in the Command
            // annotation
            for (String groupName : command.getGroupNames()) {
                CommandGroupMetadata group = CollectionUtils.find(commandGroups, new GroupFinder(groupName));
                if (group != null) {
                    // Add to existing top level group
                    group.addCommand(command);
                    addedToGroup = true;
                } else {
                    if (StringUtils.containsWhitespace(groupName)) {
                        // Add to sub-group
                        String[] groups = StringUtils.split(groupName);
                        CommandGroupMetadata subGroup = null;
                        for (int i = 0; i < groups.length; i++) {
                            if (i == 0) {
                                // Find/create the necessary top level group
                                subGroup = CollectionUtils.find(commandGroups, new GroupFinder(groups[i]));
                                if (subGroup == null) {
                                    subGroup = new CommandGroupMetadata(groups[i], "", false,
                                            Collections.<OptionMetadata>emptyList(),
                                            Collections.<CommandGroupMetadata>emptyList(), null,
                                            Collections.<CommandMetadata>emptyList());
                                    commandGroups.add(subGroup);
                                }
                            } else {
                                // Find/create the next sub-group
                                CommandGroupMetadata nextSubGroup = CollectionUtils.find(subGroup.getSubGroups(),
                                        new GroupFinder(groups[i]));
                                if (nextSubGroup == null) {
                                    nextSubGroup = new CommandGroupMetadata(groups[i], "", false,
                                            Collections.<OptionMetadata>emptyList(),
                                            Collections.<CommandGroupMetadata>emptyList(), null,
                                            Collections.<CommandMetadata>emptyList());
                                }
                                subGroup.addSubGroup(nextSubGroup);
                                subGroup = nextSubGroup;
                            }
                        }
                        if (subGroup == null)
                            throw new IllegalStateException("Failed to resolve sub-group path");
                        subGroup.addCommand(command);
                        addedToGroup = true;
                    } else {
                        // Add to newly created top level group
                        CommandGroupMetadata newGroup = loadCommandGroup(groupName, "", false,
                                Collections.<CommandGroupMetadata>emptyList(), null,
                                Collections.singletonList(command));
                        commandGroups.add(newGroup);
                        addedToGroup = true;
                    }
                }
            }

            if (addedToGroup && defaultCommandGroup.contains(command)) {
                defaultCommandGroup.remove(command);
            }
        }

        allCommands.addAll(newCommands);
    }

    @SuppressWarnings("rawtypes")
    private static void createGroupsFromAnnotations(List<CommandMetadata> allCommands,
            List<CommandMetadata> newCommands, List<CommandGroupMetadata> commandGroups,
            List<CommandMetadata> defaultCommandGroup) {

        // We sort sub-groups by name length then lexically
        // This means that when we build the groups hierarchy we'll ensure we
        // build the parent groups first wherever possible
        Map<String, CommandGroupMetadata> subGroups = new TreeMap<String, CommandGroupMetadata>(
                new StringHierarchyComparator());
        for (CommandMetadata command : allCommands) {
            boolean addedToGroup = false;

            // first, create any groups explicitly annotated
            for (Group groupAnno : command.getGroups()) {
                Class defaultCommandClass = null;
                CommandMetadata defaultCommand = null;

                // load default command if needed
                if (!groupAnno.defaultCommand().equals(Group.NO_DEFAULT.class)) {
                    defaultCommandClass = groupAnno.defaultCommand();
                    defaultCommand = CollectionUtils.find(allCommands, new CommandTypeFinder(defaultCommandClass));
                    if (null == defaultCommand) {
                        defaultCommand = loadCommand(defaultCommandClass);
                        newCommands.add(defaultCommand);
                    }
                }

                // load other commands if needed
                List<CommandMetadata> groupCommands = new ArrayList<CommandMetadata>(groupAnno.commands().length);
                CommandMetadata groupCommand = null;
                for (Class commandClass : groupAnno.commands()) {
                    groupCommand = CollectionUtils.find(allCommands, new CommandTypeFinder(commandClass));
                    if (null == groupCommand) {
                        groupCommand = loadCommand(commandClass);
                        newCommands.add(groupCommand);
                        groupCommands.add(groupCommand);
                    }
                }

                // Find the group metadata
                // May already exist as a top level group
                CommandGroupMetadata groupMetadata = CollectionUtils.find(commandGroups,
                        new GroupFinder(groupAnno.name()));
                if (groupMetadata == null) {
                    // Not a top level group

                    String subGroupPath = null;
                    if (StringUtils.containsWhitespace(groupAnno.name())) {
                        // Is this a sub-group we've already seen?
                        // Make sure to normalize white space in the path
                        subGroupPath = StringUtils.join(StringUtils.split(groupAnno.name()), " ");
                        groupMetadata = subGroups.get(subGroupPath);
                    }

                    if (groupMetadata == null) {
                        // Newly discovered group
                        groupMetadata = loadCommandGroup(groupAnno.name(), groupAnno.description(),
                                groupAnno.hidden(), Collections.<CommandGroupMetadata>emptyList(), defaultCommand,
                                groupCommands);
                        if (!StringUtils.containsWhitespace(groupAnno.name())) {
                            // Add as top level group
                            commandGroups.add(groupMetadata);
                        } else {
                            // This is a new sub-group, put aside for now and
                            // we'll build the sub-group tree later
                            subGroups.put(subGroupPath, groupMetadata);
                        }
                    }
                }

                groupMetadata.addCommand(command);
                addedToGroup = true;
            }

            if (addedToGroup && defaultCommandGroup.contains(command)) {
                defaultCommandGroup.remove(command);
            }
        }

        buildGroupsHierarchy(commandGroups, subGroups);
    }

    protected static void buildGroupsHierarchy(List<CommandGroupMetadata> commandGroups,
            Map<String, CommandGroupMetadata> subGroups) {
        // Add sub-groups into hierarchy as appropriate
        for (String subGroupPath : subGroups.keySet()) {
            CommandGroupMetadata subGroup = subGroups.get(subGroupPath);
            String[] groups = StringUtils.split(subGroupPath);
            CommandGroupMetadata parentGroup = null;
            for (int i = 0; i < groups.length - 1; i++) {
                if (i == 0) {
                    // Should be a top level group
                    parentGroup = CollectionUtils.find(commandGroups, new GroupFinder(groups[i]));
                    if (parentGroup == null) {
                        // Top level parent group does not exist so create empty
                        // top level group
                        parentGroup = new CommandGroupMetadata(groups[i], "", false,
                                Collections.<OptionMetadata>emptyList(),
                                Collections.<CommandGroupMetadata>emptyList(), null,
                                Collections.<CommandMetadata>emptyList());
                        commandGroups.add(parentGroup);
                    }
                } else {
                    // Should be a sub-group of the current parent
                    CommandGroupMetadata nextParent = CollectionUtils.find(parentGroup.getSubGroups(),
                            new GroupFinder(groups[i]));
                    if (nextParent == null) {
                        // Next parent group does not exist so create empty
                        // group
                        nextParent = new CommandGroupMetadata(groups[i], "", false,
                                Collections.<OptionMetadata>emptyList(),
                                Collections.<CommandGroupMetadata>emptyList(), null,
                                Collections.<CommandMetadata>emptyList());
                    }
                    parentGroup.addSubGroup(nextParent);
                    nextParent.setParent(parentGroup);
                    parentGroup = nextParent;
                }
            }
            if (parentGroup == null)
                throw new IllegalStateException("Failed to resolve sub-group path");
            parentGroup.addSubGroup(subGroup);
            subGroup.setParent(parentGroup);
        }
    }

    private static class InjectionMetadata {
        private List<OptionMetadata> globalOptions = new ArrayList<>();
        private List<OptionMetadata> groupOptions = new ArrayList<>();
        private List<OptionMetadata> commandOptions = new ArrayList<>();
        private OptionMetadata defaultOption = null;
        private List<ArgumentsMetadata> arguments = new ArrayList<>();
        private List<Accessor> metadataInjections = new ArrayList<>();

        private void compact() {
            globalOptions = overrideOptionSet(globalOptions);
            groupOptions = overrideOptionSet(groupOptions);
            commandOptions = overrideOptionSet(commandOptions);
            if (defaultOption != null) {
                for (OptionMetadata option : commandOptions) {
                    boolean found = false;
                    for (String opt : defaultOption.getOptions()) {
                        if (option.getOptions().contains(opt)) {
                            defaultOption = option;
                            found = true;
                            break;
                        }
                    }
                    if (found)
                        break;
                }
            }

            if (arguments.size() > 1) {
                arguments = ListUtils
                        .unmodifiableList(AirlineUtils.singletonList(new ArgumentsMetadata(arguments)));
            }
        }
    }
}