com.planet57.gshell.util.cli2.CliProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.planet57.gshell.util.cli2.CliProcessor.java

Source

/*
 * Copyright (c) 2009-present 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.planet57.gshell.util.cli2;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.planet57.gossip.Log;
import com.planet57.gshell.util.IllegalAnnotationError;
import com.planet57.gshell.util.cli2.handler.Handler;
import com.planet57.gshell.util.cli2.handler.Handlers;
import com.planet57.gshell.util.i18n.MessageSource;
import com.planet57.gshell.util.setter.SetterFactory;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.MissingArgumentException;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.slf4j.Logger;
import com.planet57.gshell.util.i18n.I18N;
import com.planet57.gshell.util.i18n.MessageBundle;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Processes an object for cli annotations.
 *
 * @since 2.3
 */
public class CliProcessor {
    private interface Messages extends MessageBundle {
        @DefaultMessage("Option '%s' takes an operand: %s")
        String MISSING_OPERAND(String option, String operand);

        @DefaultMessage("'%s' is not a valid option")
        String UNDEFINED_OPTION(String option);

        @DefaultMessage("No argument is allowed: %s")
        String NO_ARGUMENT_ALLOWED(String value);

        @DefaultMessage("Option '%s' is required")
        String REQUIRED_OPTION_MISSING(List<?> option);

        @DefaultMessage("Argument '%s' is required")
        String REQUIRED_ARGUMENT_MISSING(String argument);

        @DefaultMessage("Too many arguments: %s")
        String TOO_MANY_ARGUMENTS(String argument);
    }

    private static final Messages messages = I18N.create(Messages.class);

    private static final Logger log = Log.getLogger(CliProcessor.class);

    private final List<OptionDescriptor> optionDescriptors = new ArrayList<>();

    private final List<ArgumentDescriptor> argumentDescriptors = new ArrayList<>();

    private boolean stopAtNonOption;

    private MessageSource userMessages;

    private CliParser.Flavor flavor = CliParser.Flavor.DEFAULT;

    public CliProcessor() {
        // empty
    }

    public boolean isStopAtNonOption() {
        return stopAtNonOption;
    }

    public void setStopAtNonOption(boolean flag) {
        this.stopAtNonOption = flag;
    }

    public MessageSource getMessages() {
        return userMessages;
    }

    public void setMessages(final MessageSource messages) {
        this.userMessages = messages;
    }

    public CliParser.Flavor getFlavor() {
        return flavor;
    }

    public void setFlavor(final CliParser.Flavor flavor) {
        this.flavor = checkNotNull(flavor);
    }

    public List<OptionDescriptor> getOptionDescriptors() {
        return optionDescriptors;
    }

    public List<ArgumentDescriptor> getArgumentDescriptors() {
        return argumentDescriptors;
    }

    public void addBean(final Object bean) {
        discoverDescriptors(bean);

        if (bean instanceof CliProcessorAware) {
            ((CliProcessorAware) bean).setProcessor(this);
        }
    }

    //
    // Discovery
    //

    private void discoverDescriptors(final Object bean) {
        assert bean != null;

        // Recursively process all the methods/fields (@Inherited won't work here)
        for (Class<?> type = bean.getClass(); type != null; type = type.getSuperclass()) {
            for (Method method : type.getDeclaredMethods()) {
                discoverDescriptor(bean, method);
            }
            for (Field field : type.getDeclaredFields()) {
                discoverDescriptor(bean, field);
            }
        }

        // Sanity check the argument indexes
        for (int i = 0; i < argumentDescriptors.size(); i++) {
            if (argumentDescriptors.get(i) == null) {
                throw new IllegalAnnotationError(String.format("No @Argument for index: %d", i));
            }
        }
    }

    private void discoverDescriptor(final Object bean, final AnnotatedElement element) {
        assert bean != null;
        assert element != null;

        Option opt = element.getAnnotation(Option.class);
        Argument arg = element.getAnnotation(Argument.class);

        if (opt != null && arg != null) {
            throw new IllegalAnnotationError(
                    String.format("Element can only implement @Option or @Argument, not both: %s", element));
        }

        if (opt != null) {
            log.trace("Discovered @Option for: {} -> {}", element, opt);

            OptionDescriptor desc = new OptionDescriptor(opt, SetterFactory.create(element, bean));

            // Make sure we have unique names
            for (OptionDescriptor tmp : optionDescriptors) {
                if (desc.getName() != null && desc.getName().equals(tmp.getName())) {
                    throw new IllegalAnnotationError(
                            String.format("Duplicate @Option name: %s, on: %s", desc.getName(), element));
                }
                if (desc.getLongName() != null && desc.getLongName().equals(tmp.getLongName())) {
                    throw new IllegalAnnotationError(
                            String.format("Duplicate @Option longName: %s, on: %s", desc.getLongName(), element));
                }
            }

            optionDescriptors.add(desc);
        } else if (arg != null) {
            log.trace("Discovered @Argument for: {} -> {}", element, arg);

            ArgumentDescriptor desc = new ArgumentDescriptor(arg, SetterFactory.create(element, bean));
            int index = arg.index();

            // Make sure the argument will fit in the list
            while (index >= argumentDescriptors.size()) {
                argumentDescriptors.add(null);
            }

            if (argumentDescriptors.get(index) != null) {
                throw new IllegalAnnotationError(
                        String.format("Duplicate @Argument index: %s, on: %s", index, element));
            }

            argumentDescriptors.set(index, desc);
        }
    }

    //
    // Processing
    //

    public void process(final List<?> args) throws Exception {
        checkNotNull(args);
        process(toStringArray(args));
    }

    private static String[] toStringArray(final List<?> args) {
        String[] strings = new String[args.size()];
        for (int i = 0; i < args.size(); i++) {
            strings[i] = String.valueOf(args.get(i));
        }
        return strings;
    }

    public void process(final String... args) throws Exception {
        checkNotNull(args);
        if (log.isTraceEnabled()) {
            log.trace("Processing: {}", Arrays.toString(args));
        }

        CliParser parser = flavor.create();
        log.trace("Parser: {}", parser);

        CommandLine cl;
        try {
            cl = parser.parse(createOptions(), args, stopAtNonOption);
        } catch (UnrecognizedOptionException e) {
            throw new ProcessingException(messages.UNDEFINED_OPTION(e.getOption()));
        } catch (MissingArgumentException e) {
            OptionDescriptor desc = ((Opt) e.getOption()).getDescriptor();
            throw new ProcessingException(messages.MISSING_OPERAND(desc.getSyntax(), desc.getToken()));
        } catch (ParseException e) {
            throw new ProcessingException(e);
        }

        Set<CliDescriptor> present = new HashSet<>();
        boolean override = false;

        if (log.isTraceEnabled()) {
            log.trace("Parsed options: {}", Arrays.toString(cl.getOptions()));
        }

        for (Object tmp : cl.getOptions()) {
            Opt opt = (Opt) tmp;
            log.trace("Processing option: {}", opt);

            OptionDescriptor desc = opt.getDescriptor();
            present.add(desc);

            // Track the override, this is used to handle when --help present, but a required arg/opt is missing
            if (!override) {
                override = desc.getOverride();
            }

            Handler handler = Handlers.create(desc);
            String[] values = opt.getValues();

            if (values == null || values.length == 0) {
                // Set the value
                handler.handle(opt.getValue());
            } else {
                // Set the values
                for (String value : values) {
                    handler.handle(value);
                }
            }
        }

        log.trace("Remaining arguments: {}", cl.getArgList());

        int i = 0;
        for (final String arg : cl.getArgs()) {
            log.trace("Processing argument: {}", arg);

            // Check if we allow an argument or we have overflowed
            if (i >= argumentDescriptors.size()) {
                throw new ProcessingException(argumentDescriptors.size() == 0 ? messages.NO_ARGUMENT_ALLOWED(arg)
                        : messages.TOO_MANY_ARGUMENTS(arg));
            }

            ArgumentDescriptor desc = argumentDescriptors.get(i);
            present.add(desc);

            // For single-valued args, increment the argument index, else let the multivalued handler consume it
            if (!desc.isMultiValued()) {
                i++;
            }

            // Set the value
            Handler handler = Handlers.create(desc);
            handler.handle(arg);
        }

        // Check for any required arguments which were not present
        if (!override) {
            try {
                parser.ensureRequiredOptionsPresent();
            } catch (MissingOptionException e) {
                throw new ProcessingException(messages.REQUIRED_OPTION_MISSING(e.getMissingOptions()));
            }

            for (ArgumentDescriptor arg : argumentDescriptors) {
                if (arg.isRequired() && !present.contains(arg)) {
                    throw new ProcessingException(messages.REQUIRED_ARGUMENT_MISSING(arg.getToken()));
                }
            }
        }

        // TODO: Handle setting defaults
    }

    private Options createOptions() {
        Options opts = new Options();

        for (OptionDescriptor opt : optionDescriptors) {
            opts.addOption(new Opt(opt));
        }

        return opts;
    }

    private static class Opt extends org.apache.commons.cli.Option {
        private final OptionDescriptor desc;

        private Opt(final OptionDescriptor opt) throws IllegalArgumentException {
            super(opt.getName(), opt.getDescription());
            this.desc = opt;

            setLongOpt(opt.getLongName());
            setArgName(opt.getToken());
            setRequired(opt.isRequired());
            setValueSeparator(opt.getSeparator());
            setArgs(opt.getArgs());
            setOptionalArg(opt.isArgumentOptional());
        }

        public OptionDescriptor getDescriptor() {
            return desc;
        }
    }
}