Java tutorial
/* * 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; } } }