/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.runtime.cli; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import gobblin.runtime.api.JobTemplate; import gobblin.runtime.embedded.EmbeddedGobblin; import lombok.Getter; /** * A helper class for automatically inferring {@link Option}s from the public methods in a class. * * For each public method in the class to infer with exactly zero or one String parameter, the helper will create * an optional {@link Option}. Using the annotation {@link EmbeddedGobblinCliOption} the helper can automatically * add a description to the {@link Option}. Annotating a method with {@link NotOnCli} will prevent the helper from * creating an {@link Option} from it. * * For an example usage see {@link EmbeddedGobblin.CliFactory} */ public abstract class PublicMethodsGobblinCliFactory implements EmbeddedGobblinCliFactory { private static final List<String> BLACKLISTED_FROM_CLI = ImmutableList.of( "getClass", "hashCode", "notify", "notifyAll", "toString", "wait" ); protected final Class<? extends EmbeddedGobblin> klazz; @Getter private final Options options; private final Map<String, Method> methodsMap; public PublicMethodsGobblinCliFactory(Class<? extends EmbeddedGobblin> klazz) { this.klazz = klazz; this.methodsMap = Maps.newHashMap(); this.options = inferOptionsFromMethods(); } @Override public EmbeddedGobblin buildEmbeddedGobblin(CommandLine cli) { try { EmbeddedGobblin embeddedGobblin = constructEmbeddedGobblin(cli); applyCommandLineOptions(cli, embeddedGobblin); return embeddedGobblin; } catch (IOException | JobTemplate.TemplateException exc) { throw new RuntimeException("Could not instantiate " + this.klazz.getSimpleName(), exc); } } public abstract EmbeddedGobblin constructEmbeddedGobblin(CommandLine cli) throws JobTemplate.TemplateException, IOException; @Override public String getUsageString() { return "[OPTIONS]"; } /** * For each method for which the helper created an {@link Option} and for which the input {@link CommandLine} contains * that option, this method will automatically call the method on the input {@link EmbeddedGobblin} with the correct * arguments. */ public void applyCommandLineOptions(CommandLine cli, EmbeddedGobblin embeddedGobblin) { try { for (Option option : cli.getOptions()) { if (!this.methodsMap.containsKey(option.getOpt())) { // Option added by cli driver itself. continue; } if (option.hasArg()) { this.methodsMap.get(option.getOpt()).invoke(embeddedGobblin, option.getValue()); } else { this.methodsMap.get(option.getOpt()).invoke(embeddedGobblin); } } } catch (IllegalAccessException | InvocationTargetException exc) { throw new RuntimeException("Could not apply options to " + embeddedGobblin.getClass().getName(), exc); } } private Options inferOptionsFromMethods() { Options options = new Options(); for (Method method : klazz.getMethods()) { if (canUseMethod(method)) { EmbeddedGobblinCliOption annotation = method.isAnnotationPresent(EmbeddedGobblinCliOption.class) ? method.getAnnotation(EmbeddedGobblinCliOption.class) : null; String optionName = annotation == null || Strings.isNullOrEmpty(annotation.name()) ? method.getName() : annotation.name(); String description = annotation == null ? "" : annotation.description(); Option.Builder builder = Option.builder(optionName).desc(description); boolean hasArg = method.getParameterTypes().length > 0; if (hasArg) { builder.hasArg(); } Option option = builder.build(); options.addOption(option); this.methodsMap.put(option.getOpt(), method); } } return options; } private boolean canUseMethod(Method method) { if (!Modifier.isPublic(method.getModifiers())) { return false; } if (BLACKLISTED_FROM_CLI.contains(method.getName())) { return false; } if (method.isAnnotationPresent(NotOnCli.class)) { return false; } Class<?>[] parameters = method.getParameterTypes(); if (parameters.length > 2) { return false; } if (parameters.length == 1 && parameters[0] != String.class) { return false; } return true; } }