blob: b887394e8fe33c1a724ab795eef72b14c2e21427 [file] [log] [blame]
/*
* 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 org.apache.beam.sdk.options;
import static java.util.Locale.ROOT;
import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument;
import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkNotNull;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.annotation.Nonnull;
import org.apache.beam.model.jobmanagement.v1.JobApi.PipelineOptionDescriptor;
import org.apache.beam.model.jobmanagement.v1.JobApi.PipelineOptionType;
import org.apache.beam.sdk.PipelineRunner;
import org.apache.beam.sdk.annotations.Experimental;
import org.apache.beam.sdk.options.Validation.Required;
import org.apache.beam.sdk.runners.PipelineRunnerRegistrar;
import org.apache.beam.sdk.transforms.display.DisplayData;
import org.apache.beam.sdk.util.StringUtils;
import org.apache.beam.sdk.util.common.ReflectHelpers;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.annotations.VisibleForTesting;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.CaseFormat;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Function;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Joiner;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Optional;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Predicate;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Predicates;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Strings;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.FluentIterable;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableListMultimap;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableSet;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableSortedSet;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Iterables;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Iterators;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ListMultimap;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Maps;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Ordering;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.RowSortedTable;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Sets;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.SortedSetMultimap;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.TreeBasedTable;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.TreeMultimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Constructs a {@link PipelineOptions} or any derived interface that is composable to any other
* derived interface of {@link PipelineOptions} via the {@link PipelineOptions#as} method. Being
* able to compose one derived interface of {@link PipelineOptions} to another has the following
* restrictions:
*
* <ul>
* <li>Any property with the same name must have the same return type for all derived interfaces
* of {@link PipelineOptions}.
* <li>Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
* <li>Every method must conform to being a getter or setter for a JavaBean.
* <li>The derived interface of {@link PipelineOptions} must be composable with every interface
* registered with this factory.
* </ul>
*
* <p>See the <a
* href="http://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html">JavaBeans
* specification</a> for more details as to what constitutes a property.
*/
public class PipelineOptionsFactory {
/**
* Creates and returns an object that implements {@link PipelineOptions}. This sets the {@link
* ApplicationNameOptions#getAppName() "appName"} to the calling {@link Class#getSimpleName()
* classes simple name}.
*
* @return An object that implements {@link PipelineOptions}.
*/
public static PipelineOptions create() {
return new Builder().as(PipelineOptions.class);
}
/**
* Creates and returns an object that implements {@code <T>}. This sets the {@link
* ApplicationNameOptions#getAppName() "appName"} to the calling {@link Class#getSimpleName()
* classes simple name}.
*
* <p>Note that {@code <T>} must be composable with every registered interface with this factory.
* See {@link PipelineOptionsFactory.Cache#validateWellFormed(Class, Set)} for more details.
*
* @return An object that implements {@code <T>}.
*/
public static <T extends PipelineOptions> T as(Class<T> klass) {
return new Builder().as(klass);
}
/**
* Sets the command line arguments to parse when constructing the {@link PipelineOptions}.
*
* <p>Example GNU style command line arguments:
*
* <pre>
* --project=MyProject (simple property, will set the "project" property to "MyProject")
* --readOnly=true (for boolean properties, will set the "readOnly" property to "true")
* --readOnly (shorthand for boolean properties, will set the "readOnly" property to "true")
* --x=1 --x=2 --x=3 (list style simple property, will set the "x" property to [1, 2, 3])
* --x=1,2,3 (shorthand list style simple property, will set the "x" property to [1, 2, 3])
* --complexObject='{"key1":"value1",...} (JSON format for all other complex types)
* </pre>
*
* <p>Simple properties are able to bound to {@link String}, {@link Class}, enums and Java
* primitives {@code boolean}, {@code byte}, {@code short}, {@code int}, {@code long}, {@code
* float}, {@code double} and their primitive wrapper classes.
*
* <p>Simple list style properties are able to be bound to {@code boolean[]}, {@code char[]},
* {@code short[]}, {@code int[]}, {@code long[]}, {@code float[]}, {@code double[]}, {@code
* Class[]}, enum arrays, {@code String[]}, and {@code List<String>}.
*
* <p>JSON format is required for all other types.
*
* <p>By default, strict parsing is enabled and arguments must conform to be either {@code
* --booleanArgName} or {@code --argName=argValue}. Strict parsing can be disabled with {@link
* Builder#withoutStrictParsing()}. Empty or null arguments will be ignored whether or not strict
* parsing is enabled.
*
* <p>Help information can be output to {@link System#out} by specifying {@code --help} as an
* argument. After help is printed, the application will exit. Specifying only {@code --help} will
* print out the list of {@link PipelineOptionsFactory#getRegisteredOptions() registered options}
* by invoking {@link PipelineOptionsFactory#printHelp(PrintStream)}. Specifying {@code
* --help=PipelineOptionsClassName} will print out detailed usage information about the
* specifically requested PipelineOptions by invoking {@link
* PipelineOptionsFactory#printHelp(PrintStream, Class)}.
*/
public static Builder fromArgs(String... args) {
return new Builder().fromArgs(args);
}
/**
* After creation we will validate that {@code <T>} conforms to all the validation criteria. See
* {@link PipelineOptionsValidator#validate(Class, PipelineOptions)} for more details about
* validation.
*/
public Builder withValidation() {
return new Builder().withValidation();
}
/** A fluent {@link PipelineOptions} builder. */
public static class Builder {
private final String defaultAppName;
private final String[] args;
private final boolean validation;
private final boolean strictParsing;
private final boolean isCli;
// Do not allow direct instantiation
private Builder() {
this(null, false, true, false);
}
private Builder(String[] args, boolean validation, boolean strictParsing, boolean isCli) {
this.defaultAppName = findCallersClassName();
this.args = args;
this.validation = validation;
this.strictParsing = strictParsing;
this.isCli = isCli;
}
/**
* Sets the command line arguments to parse when constructing the {@link PipelineOptions}.
*
* <p>Example GNU style command line arguments:
*
* <pre>
* --project=MyProject (simple property, will set the "project" property to "MyProject")
* --readOnly=true (for boolean properties, will set the "readOnly" property to "true")
* --readOnly (shorthand for boolean properties, will set the "readOnly" property to "true")
* --x=1 --x=2 --x=3 (list style simple property, will set the "x" property to [1, 2, 3])
* --x=1,2,3 (shorthand list style simple property, will set the "x" property to [1, 2, 3])
* --complexObject='{"key1":"value1",...} (JSON format for all other complex types)
* </pre>
*
* <p>Simple properties are able to bound to {@link String}, {@link Class}, enums and Java
* primitives {@code boolean}, {@code byte}, {@code short}, {@code int}, {@code long}, {@code
* float}, {@code double} and their primitive wrapper classes.
*
* <p>Simple list style properties are able to be bound to {@code boolean[]}, {@code char[]},
* {@code short[]}, {@code int[]}, {@code long[]}, {@code float[]}, {@code double[]}, {@code
* Class[]}, enum arrays, {@code String[]}, and {@code List<String>}.
*
* <p>JSON format is required for all other types.
*
* <p>By default, strict parsing is enabled and arguments must conform to be either {@code
* --booleanArgName} or {@code --argName=argValue}. Strict parsing can be disabled with {@link
* Builder#withoutStrictParsing()}. Empty or null arguments will be ignored whether or not
* strict parsing is enabled.
*
* <p>Help information can be output to {@link System#out} by specifying {@code --help} as an
* argument. After help is printed, the application will exit. Specifying only {@code --help}
* will print out the list of {@link PipelineOptionsFactory#getRegisteredOptions() registered
* options} by invoking {@link PipelineOptionsFactory#printHelp(PrintStream)}. Specifying {@code
* --help=PipelineOptionsClassName} will print out detailed usage information about the
* specifically requested PipelineOptions by invoking {@link
* PipelineOptionsFactory#printHelp(PrintStream, Class)}.
*/
public Builder fromArgs(String... args) {
checkNotNull(args, "Arguments should not be null.");
return new Builder(args, validation, strictParsing, true);
}
/**
* After creation we will validate that {@link PipelineOptions} conforms to all the validation
* criteria from {@code <T>}. See {@link PipelineOptionsValidator#validate(Class,
* PipelineOptions)} for more details about validation.
*/
public Builder withValidation() {
return new Builder(args, true, strictParsing, isCli);
}
/**
* During parsing of the arguments, we will skip over improperly formatted and unknown
* arguments.
*/
public Builder withoutStrictParsing() {
return new Builder(args, validation, false, isCli);
}
/**
* Creates and returns an object that implements {@link PipelineOptions} using the values
* configured on this builder during construction.
*
* @return An object that implements {@link PipelineOptions}.
*/
public PipelineOptions create() {
return as(PipelineOptions.class);
}
/**
* Creates and returns an object that implements {@code <T>} using the values configured on this
* builder during construction.
*
* <p>Note that {@code <T>} must be composable with every registered interface with this
* factory. See {@link PipelineOptionsFactory.Cache#validateWellFormed(Class)} for more details.
*
* @return An object that implements {@code <T>}.
*/
public <T extends PipelineOptions> T as(Class<T> klass) {
Map<String, Object> initialOptions = Maps.newHashMap();
// Attempt to parse the arguments into the set of initial options to use
if (args != null) {
ListMultimap<String, String> options = parseCommandLine(args, strictParsing);
LOG.debug("Provided Arguments: {}", options);
printHelpUsageAndExitIfNeeded(options, System.out, true /* exit */);
initialOptions = parseObjects(klass, options, strictParsing);
}
// Create our proxy
ProxyInvocationHandler handler = new ProxyInvocationHandler(initialOptions);
T t = handler.as(klass);
// Set the application name to the default if none was set.
ApplicationNameOptions appNameOptions = t.as(ApplicationNameOptions.class);
if (appNameOptions.getAppName() == null) {
appNameOptions.setAppName(defaultAppName);
}
// Ensure the options id has been populated either by the user using the command line
// or by the default value factory.
t.getOptionsId();
if (validation) {
if (isCli) {
PipelineOptionsValidator.validateCli(klass, t);
} else {
PipelineOptionsValidator.validate(klass, t);
}
}
return t;
}
}
/**
* Determines whether the generic {@code --help} was requested or help was requested for a
* specific class and invokes the appropriate {@link
* PipelineOptionsFactory#printHelp(PrintStream)} and {@link
* PipelineOptionsFactory#printHelp(PrintStream, Class)} variant. Prints to the specified {@link
* PrintStream}, and exits if requested.
*
* <p>Visible for testing. {@code printStream} and {@code exit} used for testing.
*/
@SuppressWarnings("unchecked")
static boolean printHelpUsageAndExitIfNeeded(
ListMultimap<String, String> options, PrintStream printStream, boolean exit) {
if (options.containsKey("help")) {
final String helpOption = Iterables.getOnlyElement(options.get("help"));
// Print the generic help if only --help was specified.
if (Boolean.TRUE.toString().equals(helpOption)) {
printHelp(printStream);
if (exit) {
System.exit(0);
} else {
return true;
}
}
// Otherwise attempt to print the specific help option.
try {
Class<?> klass = Class.forName(helpOption, true, ReflectHelpers.findClassLoader());
if (!PipelineOptions.class.isAssignableFrom(klass)) {
throw new ClassNotFoundException("PipelineOptions of type " + klass + " not found.");
}
printHelp(printStream, (Class<? extends PipelineOptions>) klass);
} catch (ClassNotFoundException e) {
// If we didn't find an exact match, look for any that match the class name.
Iterable<Class<? extends PipelineOptions>> matches =
getRegisteredOptions().stream()
.filter(
input -> {
if (helpOption.contains(".")) {
return input.getName().endsWith(helpOption);
} else {
return input.getSimpleName().equals(helpOption);
}
})
.collect(Collectors.toList());
try {
printHelp(printStream, Iterables.getOnlyElement(matches));
} catch (NoSuchElementException exception) {
printStream.format("Unable to find option %s.%n", helpOption);
printHelp(printStream);
} catch (IllegalArgumentException exception) {
printStream.format(
"Multiple matches found for %s: %s.%n",
helpOption,
StreamSupport.stream(matches.spliterator(), false)
.map(ReflectHelpers.CLASS_NAME::apply)
.collect(Collectors.toList()));
printHelp(printStream);
}
}
if (exit) {
System.exit(0);
} else {
return true;
}
}
return false;
}
/** Returns the simple name of the calling class using the current threads stack. */
private static String findCallersClassName() {
Iterator<StackTraceElement> elements =
Iterators.forArray(Thread.currentThread().getStackTrace());
// First find the PipelineOptionsFactory/Builder class in the stack trace.
while (elements.hasNext()) {
StackTraceElement next = elements.next();
if (PIPELINE_OPTIONS_FACTORY_CLASSES.contains(next.getClassName())) {
break;
}
}
// Then find the first instance after that is not the PipelineOptionsFactory/Builder class.
while (elements.hasNext()) {
StackTraceElement next = elements.next();
if (!PIPELINE_OPTIONS_FACTORY_CLASSES.contains(next.getClassName())) {
try {
return Class.forName(next.getClassName(), true, ReflectHelpers.findClassLoader())
.getSimpleName();
} catch (ClassNotFoundException e) {
break;
}
}
}
return "unknown";
}
/**
* Stores the generated proxyClass and its respective {@link BeanInfo} object.
*
* @param <T> The type of the proxyClass.
*/
static class Registration<T extends PipelineOptions> {
private final Class<T> proxyClass;
private final List<PropertyDescriptor> propertyDescriptors;
public Registration(Class<T> proxyClass, List<PropertyDescriptor> beanInfo) {
this.proxyClass = proxyClass;
this.propertyDescriptors = beanInfo;
}
List<PropertyDescriptor> getPropertyDescriptors() {
return propertyDescriptors;
}
Class<T> getProxyClass() {
return proxyClass;
}
}
private static final ImmutableSet<Class<?>> SIMPLE_TYPES =
ImmutableSet.<Class<?>>builder()
.add(boolean.class)
.add(Boolean.class)
.add(char.class)
.add(Character.class)
.add(short.class)
.add(Short.class)
.add(int.class)
.add(Integer.class)
.add(long.class)
.add(Long.class)
.add(float.class)
.add(Float.class)
.add(double.class)
.add(Double.class)
.add(String.class)
.add(Class.class)
.build();
private static final Logger LOG = LoggerFactory.getLogger(PipelineOptionsFactory.class);
@SuppressWarnings("rawtypes")
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[0];
static final ObjectMapper MAPPER =
new ObjectMapper()
.registerModules(ObjectMapper.findModules(ReflectHelpers.findClassLoader()));
/** Classes that are used as the boundary in the stack trace to find the callers class name. */
private static final ImmutableSet<String> PIPELINE_OPTIONS_FACTORY_CLASSES =
ImmutableSet.of(PipelineOptionsFactory.class.getName(), Builder.class.getName());
/** Methods that are ignored when validating the proxy class. */
private static final Set<Method> IGNORED_METHODS;
/** A predicate that checks if a method is synthetic via {@link Method#isSynthetic()}. */
private static final Predicate<Method> NOT_SYNTHETIC_PREDICATE = input -> !input.isSynthetic();
private static final Predicate<Method> NOT_STATIC_PREDICATE =
input -> !Modifier.isStatic(input.getModifiers());
/** Ensure all classloader or volatile data are contained in a single reference. */
static final AtomicReference<Cache> CACHE = new AtomicReference<>();
/** The width at which options should be output. */
private static final int TERMINAL_WIDTH = 80;
static {
try {
IGNORED_METHODS =
ImmutableSet.<Method>builder()
.add(Object.class.getMethod("getClass"))
.add(Object.class.getMethod("wait"))
.add(Object.class.getMethod("wait", long.class))
.add(Object.class.getMethod("wait", long.class, int.class))
.add(Object.class.getMethod("notify"))
.add(Object.class.getMethod("notifyAll"))
.add(Proxy.class.getMethod("getInvocationHandler", Object.class))
.build();
} catch (NoSuchMethodException | SecurityException e) {
LOG.error("Unable to find expected method", e);
throw new ExceptionInInitializerError(e);
}
resetCache();
}
/**
* This registers the interface with this factory. This interface must conform to the following
* restrictions:
*
* <ul>
* <li>Any property with the same name must have the same return type for all derived interfaces
* of {@link PipelineOptions}.
* <li>Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
* <li>Every method must conform to being a getter or setter for a JavaBean.
* <li>The derived interface of {@link PipelineOptions} must be composable with every interface
* registered with this factory.
* </ul>
*
* @param iface The interface object to manually register.
*/
public static synchronized void register(Class<? extends PipelineOptions> iface) {
CACHE.get().register(iface);
}
/**
* Resets the set of interfaces registered with this factory to the default state.
*
* <p>IMPORTANT: this is marked as experimental because the correct usage of this method requires
* appropriate synchronization beyond the scope of this method.
*
* @see PipelineOptionsFactory#register(Class)
* @see Cache#Cache()
*/
@Experimental(Experimental.Kind.UNSPECIFIED)
public static synchronized void resetCache() {
CACHE.set(new Cache());
}
public static Set<Class<? extends PipelineOptions>> getRegisteredOptions() {
return Collections.unmodifiableSet(CACHE.get().registeredOptions);
}
/**
* Outputs the set of registered options with the PipelineOptionsFactory with a description for
* each one if available to the output stream. This output is pretty printed and meant to be human
* readable. This method will attempt to format its output to be compatible with a terminal
* window.
*/
public static void printHelp(PrintStream out) {
checkNotNull(out);
out.println("The set of registered options are:");
Set<Class<? extends PipelineOptions>> sortedOptions =
new TreeSet<>(ClassNameComparator.INSTANCE);
sortedOptions.addAll(CACHE.get().registeredOptions);
for (Class<? extends PipelineOptions> kls : sortedOptions) {
out.format(" %s%n", kls.getName());
}
out.format(
"%nUse --help=<OptionsName> for detailed help. For example:%n"
+ " --help=DataflowPipelineOptions <short names valid for registered options>%n"
+ " --help=org.apache.beam.sdk.options.DataflowPipelineOptions%n");
}
/**
* Outputs the set of options available to be set for the passed in {@link PipelineOptions}
* interface. The output is in a human readable format. The format is:
*
* <pre>
* OptionGroup:
* ... option group description ...
*
* --option1={@code <type>} or list of valid enum choices
* Default: value (if available, see {@link Default})
* ... option description ... (if available, see {@link Description})
* Required groups (if available, see {@link Required})
* --option2={@code <type>} or list of valid enum choices
* Default: value (if available, see {@link Default})
* ... option description ... (if available, see {@link Description})
* Required groups (if available, see {@link Required})
* </pre>
*
* This method will attempt to format its output to be compatible with a terminal window.
*/
public static void printHelp(PrintStream out, Class<? extends PipelineOptions> iface) {
checkNotNull(out);
checkNotNull(iface);
CACHE.get().validateWellFormed(iface);
Set<PipelineOptionSpec> properties = PipelineOptionsReflector.getOptionSpecs(iface, true);
RowSortedTable<Class<?>, String, Method> ifacePropGetterTable =
TreeBasedTable.create(ClassNameComparator.INSTANCE, Ordering.natural());
for (PipelineOptionSpec prop : properties) {
ifacePropGetterTable.put(prop.getDefiningInterface(), prop.getName(), prop.getGetterMethod());
}
for (Map.Entry<Class<?>, Map<String, Method>> ifaceToPropertyMap :
ifacePropGetterTable.rowMap().entrySet()) {
Class<?> currentIface = ifaceToPropertyMap.getKey();
Map<String, Method> propertyNamesToGetters = ifaceToPropertyMap.getValue();
SortedSetMultimap<String, String> requiredGroupNameToProperties =
getRequiredGroupNamesToProperties(propertyNamesToGetters);
out.format("%s:%n", currentIface.getName());
prettyPrintDescription(out, currentIface.getAnnotation(Description.class));
out.println();
List<String> lists = Lists.newArrayList(propertyNamesToGetters.keySet());
lists.sort(String.CASE_INSENSITIVE_ORDER);
for (String propertyName : lists) {
Method method = propertyNamesToGetters.get(propertyName);
String printableType = method.getReturnType().getSimpleName();
if (method.getReturnType().isEnum()) {
printableType = Joiner.on(" | ").join(method.getReturnType().getEnumConstants());
}
out.format(" --%s=<%s>%n", propertyName, printableType);
Optional<String> defaultValue = getDefaultValueFromAnnotation(method);
if (defaultValue.isPresent()) {
out.format(" Default: %s%n", defaultValue.get());
}
prettyPrintDescription(out, method.getAnnotation(Description.class));
prettyPrintRequiredGroups(
out, method.getAnnotation(Validation.Required.class), requiredGroupNameToProperties);
}
out.println();
}
}
private static final Set<Class<?>> JSON_INTEGER_TYPES =
Sets.newHashSet(
short.class,
Short.class,
int.class,
Integer.class,
long.class,
Long.class,
BigInteger.class);
private static final Set<Class<?>> JSON_NUMBER_TYPES =
Sets.newHashSet(
float.class, Float.class, double.class, Double.class, java.math.BigDecimal.class);
/**
* Outputs the set of options available to be set for the passed in {@link PipelineOptions}
* interfaces. The output for consumption of the job service client.
*/
public static List<PipelineOptionDescriptor> describe(
Set<Class<? extends PipelineOptions>> ifaces) {
checkNotNull(ifaces);
List<PipelineOptionDescriptor> result = new ArrayList<>();
Set<Method> seenMethods = Sets.newHashSet();
for (Class<? extends PipelineOptions> iface : ifaces) {
CACHE.get().validateWellFormed(iface);
Set<PipelineOptionSpec> properties = PipelineOptionsReflector.getOptionSpecs(iface, false);
RowSortedTable<Class<?>, String, Method> ifacePropGetterTable =
TreeBasedTable.create(ClassNameComparator.INSTANCE, Ordering.natural());
for (PipelineOptionSpec prop : properties) {
ifacePropGetterTable.put(
prop.getDefiningInterface(), prop.getName(), prop.getGetterMethod());
}
for (Map.Entry<Class<?>, Map<String, Method>> ifaceToPropertyMap :
ifacePropGetterTable.rowMap().entrySet()) {
Class<?> currentIface = ifaceToPropertyMap.getKey();
Map<String, Method> propertyNamesToGetters = ifaceToPropertyMap.getValue();
List<String> lists = Lists.newArrayList(propertyNamesToGetters.keySet());
lists.sort(String.CASE_INSENSITIVE_ORDER);
for (String propertyName : lists) {
Method method = propertyNamesToGetters.get(propertyName);
if (!seenMethods.add(method)) {
continue;
}
Class<?> returnType = method.getReturnType();
PipelineOptionType.Enum optionType = PipelineOptionType.Enum.STRING;
if (JSON_INTEGER_TYPES.contains(returnType)) {
optionType = PipelineOptionType.Enum.INTEGER;
} else if (JSON_NUMBER_TYPES.contains(returnType)) {
optionType = PipelineOptionType.Enum.NUMBER;
} else if (returnType == boolean.class || returnType == Boolean.class) {
optionType = PipelineOptionType.Enum.BOOLEAN;
} else if (List.class.isAssignableFrom(returnType)) {
optionType = PipelineOptionType.Enum.ARRAY;
}
String optionName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, propertyName);
Description description = method.getAnnotation(Description.class);
PipelineOptionDescriptor.Builder builder =
PipelineOptionDescriptor.newBuilder()
.setName(optionName)
.setType(optionType)
.setGroup(currentIface.getName());
Optional<String> defaultValue = getDefaultValueFromAnnotation(method);
if (defaultValue.isPresent()) {
builder.setDefaultValue(defaultValue.get());
}
if (description != null) {
builder.setDescription(description.value());
}
result.add(builder.build());
}
}
}
return result;
}
/**
* Output the requirement groups that the property is a member of, including all properties that
* satisfy the group requirement, breaking up long lines on white space characters and attempting
* to honor a line limit of {@code TERMINAL_WIDTH}.
*/
private static void prettyPrintRequiredGroups(
PrintStream out,
Required annotation,
SortedSetMultimap<String, String> requiredGroupNameToProperties) {
if (annotation == null || annotation.groups() == null) {
return;
}
for (String group : annotation.groups()) {
SortedSet<String> groupMembers = requiredGroupNameToProperties.get(group);
String requirement;
if (groupMembers.size() == 1) {
requirement = Iterables.getOnlyElement(groupMembers) + " is required.";
} else {
requirement = "At least one of " + groupMembers + " is required";
}
terminalPrettyPrint(out, requirement.split("\\s+"));
}
}
/**
* Outputs the value of the description, breaking up long lines on white space characters and
* attempting to honor a line limit of {@code TERMINAL_WIDTH}.
*/
private static void prettyPrintDescription(PrintStream out, Description description) {
if (description == null || description.value() == null) {
return;
}
String[] words = description.value().split("\\s+");
terminalPrettyPrint(out, words);
}
private static void terminalPrettyPrint(PrintStream out, String[] words) {
final String spacing = " ";
if (words.length == 0) {
return;
}
out.print(spacing);
int lineLength = spacing.length();
for (int i = 0; i < words.length; ++i) {
out.print(" ");
out.print(words[i]);
lineLength += 1 + words[i].length();
// If the next word takes us over the terminal width, then goto the next line.
if (i + 1 != words.length && words[i + 1].length() + lineLength + 1 > TERMINAL_WIDTH) {
out.println();
out.print(spacing);
lineLength = spacing.length();
}
}
out.println();
}
/** Returns a string representation of the {@link Default} value on the passed in method. */
private static Optional<String> getDefaultValueFromAnnotation(Method method) {
for (Annotation annotation : method.getAnnotations()) {
if (annotation instanceof Default.Class) {
return Optional.of(((Default.Class) annotation).value().getSimpleName());
} else if (annotation instanceof Default.String) {
return Optional.of(((Default.String) annotation).value());
} else if (annotation instanceof Default.Boolean) {
return Optional.of(Boolean.toString(((Default.Boolean) annotation).value()));
} else if (annotation instanceof Default.Character) {
return Optional.of(Character.toString(((Default.Character) annotation).value()));
} else if (annotation instanceof Default.Byte) {
return Optional.of(Byte.toString(((Default.Byte) annotation).value()));
} else if (annotation instanceof Default.Short) {
return Optional.of(Short.toString(((Default.Short) annotation).value()));
} else if (annotation instanceof Default.Integer) {
return Optional.of(Integer.toString(((Default.Integer) annotation).value()));
} else if (annotation instanceof Default.Long) {
return Optional.of(Long.toString(((Default.Long) annotation).value()));
} else if (annotation instanceof Default.Float) {
return Optional.of(Float.toString(((Default.Float) annotation).value()));
} else if (annotation instanceof Default.Double) {
return Optional.of(Double.toString(((Default.Double) annotation).value()));
} else if (annotation instanceof Default.Enum) {
return Optional.of(((Default.Enum) annotation).value());
} else if (annotation instanceof Default.InstanceFactory) {
return Optional.of(((Default.InstanceFactory) annotation).value().getSimpleName());
}
}
return Optional.absent();
}
static Map<String, Class<? extends PipelineRunner<?>>> getRegisteredRunners() {
return CACHE.get().supportedPipelineRunners;
}
/**
* This method is meant to emulate the behavior of {@link Introspector#getBeanInfo(Class, int)} to
* construct the list of {@link PropertyDescriptor}.
*
* <p>TODO: Swap back to using Introspector once the proxy class issue with AppEngine is resolved.
*/
private static List<PropertyDescriptor> getPropertyDescriptors(
Set<Method> methods, Class<? extends PipelineOptions> beanClass)
throws IntrospectionException {
SortedMap<String, Method> propertyNamesToGetters = new TreeMap<>();
for (Map.Entry<String, Method> entry :
PipelineOptionsReflector.getPropertyNamesToGetters(methods).entries()) {
propertyNamesToGetters.put(entry.getKey(), entry.getValue());
}
List<PropertyDescriptor> descriptors = Lists.newArrayList();
List<TypeMismatch> mismatches = new ArrayList<>();
Set<String> usedDescriptors = Sets.newHashSet();
/*
* Add all the getter/setter pairs to the list of descriptors removing the getter once
* it has been paired up.
*/
for (Method method : methods) {
String methodName = method.getName();
if (!methodName.startsWith("set")
|| method.getParameterTypes().length != 1
|| method.getReturnType() != void.class) {
continue;
}
String propertyName = Introspector.decapitalize(methodName.substring(3));
Method getterMethod = propertyNamesToGetters.remove(propertyName);
// Validate that the getter and setter property types are the same.
if (getterMethod != null) {
Type getterPropertyType = getterMethod.getGenericReturnType();
Type setterPropertyType = method.getGenericParameterTypes()[0];
if (!getterPropertyType.equals(setterPropertyType)) {
TypeMismatch mismatch = new TypeMismatch();
mismatch.propertyName = propertyName;
mismatch.getterPropertyType = getterPropertyType;
mismatch.setterPropertyType = setterPropertyType;
mismatches.add(mismatch);
continue;
}
}
// Properties can appear multiple times with subclasses, and we don't
// want to add a bad entry if we have already added a good one (with both
// getter and setter).
if (!usedDescriptors.contains(propertyName)) {
descriptors.add(new PropertyDescriptor(propertyName, getterMethod, method));
usedDescriptors.add(propertyName);
}
}
throwForTypeMismatches(mismatches);
// Add the remaining getters with missing setters.
for (Map.Entry<String, Method> getterToMethod : propertyNamesToGetters.entrySet()) {
descriptors.add(
new PropertyDescriptor(getterToMethod.getKey(), getterToMethod.getValue(), null));
}
return descriptors;
}
private static class TypeMismatch {
private String propertyName;
private Type getterPropertyType;
private Type setterPropertyType;
}
private static void throwForTypeMismatches(List<TypeMismatch> mismatches) {
if (mismatches.size() == 1) {
TypeMismatch mismatch = mismatches.get(0);
throw new IllegalArgumentException(
String.format(
"Type mismatch between getter and setter methods for property [%s]. "
+ "Getter is of type [%s] whereas setter is of type [%s].",
mismatch.propertyName, mismatch.getterPropertyType, mismatch.setterPropertyType));
} else if (mismatches.size() > 1) {
StringBuilder builder =
new StringBuilder("Type mismatches between getters and setters detected:");
for (TypeMismatch mismatch : mismatches) {
builder.append(
String.format(
"%n - Property [%s]: Getter is of type [%s] whereas setter is of type [%s].",
mismatch.propertyName,
mismatch.getterPropertyType.toString(),
mismatch.setterPropertyType.toString()));
}
throw new IllegalArgumentException(builder.toString());
}
}
/**
* Returns a map of required groups of arguments to the properties that satisfy the requirement.
*/
private static SortedSetMultimap<String, String> getRequiredGroupNamesToProperties(
Map<String, Method> propertyNamesToGetters) {
SortedSetMultimap<String, String> result = TreeMultimap.create();
for (Map.Entry<String, Method> propertyEntry : propertyNamesToGetters.entrySet()) {
Required requiredAnnotation =
propertyEntry.getValue().getAnnotation(Validation.Required.class);
if (requiredAnnotation != null) {
for (String groupName : requiredAnnotation.groups()) {
result.put(groupName, propertyEntry.getKey());
}
}
}
return result;
}
/**
* Validates that a given class conforms to the following properties:
*
* <ul>
* <li>Any method with the same name must have the same return type for all derived interfaces
* of {@link PipelineOptions}.
* <li>Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
* <li>Every method must conform to being a getter or setter for a JavaBean.
* <li>Only getters may be annotated with {@link JsonIgnore @JsonIgnore}.
* <li>If any getter is annotated with {@link JsonIgnore @JsonIgnore}, then all getters for this
* property must be annotated with {@link JsonIgnore @JsonIgnore}.
* </ul>
*
* @param iface The interface to validate.
* @param validatedPipelineOptionsInterfaces The set of validated pipeline options interfaces to
* validate against.
* @param klass The proxy class representing the interface.
* @return A list of {@link PropertyDescriptor}s representing all valid bean properties of {@code
* iface}.
* @throws IntrospectionException if invalid property descriptors.
*/
private static List<PropertyDescriptor> validateClass(
Class<? extends PipelineOptions> iface,
Set<Class<? extends PipelineOptions>> validatedPipelineOptionsInterfaces,
Class<? extends PipelineOptions> klass)
throws IntrospectionException {
checkArgument(
Modifier.isPublic(iface.getModifiers()),
"Please mark non-public interface %s as public. The JVM requires that "
+ "all non-public interfaces to be in the same package which will prevent the "
+ "PipelineOptions proxy class to implement all of the interfaces.",
iface.getName());
// Verify that there are no methods with the same name with two different return types.
validateReturnType(iface);
SortedSet<Method> allInterfaceMethods =
FluentIterable.from(
ReflectHelpers.getClosureOfMethodsOnInterfaces(validatedPipelineOptionsInterfaces))
.append(ReflectHelpers.getClosureOfMethodsOnInterface(iface))
.filter(NOT_SYNTHETIC_PREDICATE)
.filter(NOT_STATIC_PREDICATE)
.toSortedSet(MethodComparator.INSTANCE);
List<PropertyDescriptor> descriptors = getPropertyDescriptors(allInterfaceMethods, iface);
// Verify that all method annotations are valid.
validateMethodAnnotations(allInterfaceMethods, descriptors);
// Verify that each property has a matching read and write method.
validateGettersSetters(iface, descriptors);
// Verify all methods are bean methods or known methods.
validateMethodsAreEitherBeanMethodOrKnownMethod(iface, klass, descriptors);
return descriptors;
}
/**
* Validates that any method with the same name must have the same return type for all derived
* interfaces of {@link PipelineOptions}.
*
* @param iface The interface to validate.
*/
private static void validateReturnType(Class<? extends PipelineOptions> iface) {
Iterable<Method> interfaceMethods =
FluentIterable.from(ReflectHelpers.getClosureOfMethodsOnInterface(iface))
.filter(NOT_SYNTHETIC_PREDICATE)
.toSortedSet(MethodComparator.INSTANCE);
SortedSetMultimap<Method, Method> methodNameToMethodMap =
TreeMultimap.create(MethodNameComparator.INSTANCE, MethodComparator.INSTANCE);
for (Method method : interfaceMethods) {
methodNameToMethodMap.put(method, method);
}
List<MultipleDefinitions> multipleDefinitions = Lists.newArrayList();
for (Map.Entry<Method, Collection<Method>> entry : methodNameToMethodMap.asMap().entrySet()) {
Set<Class<?>> returnTypes =
FluentIterable.from(entry.getValue())
.transform(ReturnTypeFetchingFunction.INSTANCE)
.toSet();
SortedSet<Method> collidingMethods =
FluentIterable.from(entry.getValue()).toSortedSet(MethodComparator.INSTANCE);
if (returnTypes.size() > 1) {
MultipleDefinitions defs = new MultipleDefinitions();
defs.method = entry.getKey();
defs.collidingMethods = collidingMethods;
multipleDefinitions.add(defs);
}
}
throwForMultipleDefinitions(iface, multipleDefinitions);
}
/**
* Validates that a given class conforms to the following properties:
*
* <ul>
* <li>Only getters may be annotated with {@link JsonIgnore @JsonIgnore}.
* <li>If any getter is annotated with {@link JsonIgnore @JsonIgnore}, then all getters for this
* property must be annotated with {@link JsonIgnore @JsonIgnore}.
* </ul>
*
* @param allInterfaceMethods All interface methods that derive from {@link PipelineOptions}.
* @param descriptors The list of {@link PropertyDescriptor}s representing all valid bean
* properties of {@code iface}.
*/
private static void validateMethodAnnotations(
SortedSet<Method> allInterfaceMethods, List<PropertyDescriptor> descriptors) {
SortedSetMultimap<Method, Method> methodNameToAllMethodMap =
TreeMultimap.create(MethodNameComparator.INSTANCE, MethodComparator.INSTANCE);
for (Method method : allInterfaceMethods) {
methodNameToAllMethodMap.put(method, method);
}
// Verify that there is no getter with a mixed @JsonIgnore annotation.
validateGettersHaveConsistentAnnotation(
methodNameToAllMethodMap, descriptors, AnnotationPredicates.JSON_IGNORE);
// Verify that there is no getter with a mixed @Default annotation.
validateGettersHaveConsistentAnnotation(
methodNameToAllMethodMap, descriptors, AnnotationPredicates.DEFAULT_VALUE);
// Verify that no setter has @JsonIgnore.
validateSettersDoNotHaveAnnotation(
methodNameToAllMethodMap, descriptors, AnnotationPredicates.JSON_IGNORE);
// Verify that no setter has @Default.
validateSettersDoNotHaveAnnotation(
methodNameToAllMethodMap, descriptors, AnnotationPredicates.DEFAULT_VALUE);
}
/** Validates that getters don't have mixed annotation. */
private static void validateGettersHaveConsistentAnnotation(
SortedSetMultimap<Method, Method> methodNameToAllMethodMap,
List<PropertyDescriptor> descriptors,
final AnnotationPredicates annotationPredicates) {
List<InconsistentlyAnnotatedGetters> inconsistentlyAnnotatedGetters = new ArrayList<>();
for (final PropertyDescriptor descriptor : descriptors) {
if (descriptor.getReadMethod() == null
|| IGNORED_METHODS.contains(descriptor.getReadMethod())) {
continue;
}
SortedSet<Method> getters = methodNameToAllMethodMap.get(descriptor.getReadMethod());
SortedSet<Method> gettersWithTheAnnotation =
Sets.filter(getters, annotationPredicates.forMethod);
Set<Annotation> distinctAnnotations =
Sets.newLinkedHashSet(
FluentIterable.from(gettersWithTheAnnotation)
.transformAndConcat(
new Function<Method, Iterable<? extends Annotation>>() {
@SuppressFBWarnings(
value = "NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION",
justification = "https://github.com/google/guava/issues/920")
@Nonnull
@Override
public Iterable<? extends Annotation> apply(@Nonnull Method method) {
return FluentIterable.from(method.getAnnotations());
}
})
.filter(annotationPredicates.forAnnotation));
if (distinctAnnotations.size() > 1) {
throw new IllegalArgumentException(
String.format(
"Property [%s] is marked with contradictory annotations. Found [%s].",
descriptor.getName(),
FluentIterable.from(gettersWithTheAnnotation)
.transformAndConcat(
new Function<Method, Iterable<String>>() {
@SuppressFBWarnings(
value = "NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION",
justification = "https://github.com/google/guava/issues/920")
@Nonnull
@Override
public Iterable<String> apply(final @Nonnull Method method) {
return FluentIterable.from(method.getAnnotations())
.filter(annotationPredicates.forAnnotation)
.transform(
new Function<Annotation, String>() {
@SuppressFBWarnings(
value = "NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION",
justification =
"https://github.com/google/guava/issues/920")
@Nonnull
@Override
public String apply(@Nonnull Annotation annotation) {
return String.format(
"[%s on %s]",
ReflectHelpers.ANNOTATION_FORMATTER.apply(annotation),
ReflectHelpers.CLASS_AND_METHOD_FORMATTER.apply(
method));
}
});
}
})
.join(Joiner.on(", "))));
}
Iterable<String> getterClassNames =
FluentIterable.from(getters)
.transform(MethodToDeclaringClassFunction.INSTANCE)
.transform(ReflectHelpers.CLASS_NAME);
Iterable<String> gettersWithTheAnnotationClassNames =
FluentIterable.from(gettersWithTheAnnotation)
.transform(MethodToDeclaringClassFunction.INSTANCE)
.transform(ReflectHelpers.CLASS_NAME);
if (!(gettersWithTheAnnotation.isEmpty()
|| getters.size() == gettersWithTheAnnotation.size())) {
InconsistentlyAnnotatedGetters err = new InconsistentlyAnnotatedGetters();
err.descriptor = descriptor;
err.getterClassNames = getterClassNames;
err.gettersWithTheAnnotationClassNames = gettersWithTheAnnotationClassNames;
inconsistentlyAnnotatedGetters.add(err);
}
}
throwForGettersWithInconsistentAnnotation(
inconsistentlyAnnotatedGetters, annotationPredicates.annotationClass);
}
/** Validates that setters don't have the given annotation. */
private static void validateSettersDoNotHaveAnnotation(
SortedSetMultimap<Method, Method> methodNameToAllMethodMap,
List<PropertyDescriptor> descriptors,
AnnotationPredicates annotationPredicates) {
List<AnnotatedSetter> annotatedSetters = new ArrayList<>();
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getWriteMethod() == null
|| IGNORED_METHODS.contains(descriptor.getWriteMethod())) {
continue;
}
SortedSet<Method> settersWithTheAnnotation =
Sets.filter(
methodNameToAllMethodMap.get(descriptor.getWriteMethod()),
annotationPredicates.forMethod);
Iterable<String> settersWithTheAnnotationClassNames =
FluentIterable.from(settersWithTheAnnotation)
.transform(MethodToDeclaringClassFunction.INSTANCE)
.transform(ReflectHelpers.CLASS_NAME);
if (!settersWithTheAnnotation.isEmpty()) {
AnnotatedSetter annotated = new AnnotatedSetter();
annotated.descriptor = descriptor;
annotated.settersWithTheAnnotationClassNames = settersWithTheAnnotationClassNames;
annotatedSetters.add(annotated);
}
}
throwForSettersWithTheAnnotation(annotatedSetters, annotationPredicates.annotationClass);
}
/**
* Validates that every bean property of the given interface must have both a getter and setter.
*
* @param iface The interface to validate.
* @param descriptors The list of {@link PropertyDescriptor}s representing all valid bean
* properties of {@code iface}.
*/
private static void validateGettersSetters(
Class<? extends PipelineOptions> iface, List<PropertyDescriptor> descriptors) {
List<MissingBeanMethod> missingBeanMethods = new ArrayList<>();
for (PropertyDescriptor propertyDescriptor : descriptors) {
if (!(IGNORED_METHODS.contains(propertyDescriptor.getWriteMethod())
|| propertyDescriptor.getReadMethod() != null)) {
MissingBeanMethod method = new MissingBeanMethod();
method.property = propertyDescriptor;
method.methodType = "getter";
missingBeanMethods.add(method);
continue;
}
if (!(IGNORED_METHODS.contains(propertyDescriptor.getReadMethod())
|| propertyDescriptor.getWriteMethod() != null)) {
MissingBeanMethod method = new MissingBeanMethod();
method.property = propertyDescriptor;
method.methodType = "setter";
missingBeanMethods.add(method);
}
}
throwForMissingBeanMethod(iface, missingBeanMethods);
}
/**
* Validates that every non-static or synthetic method is either a known method such as {@link
* PipelineOptions#as} or a bean property.
*
* @param iface The interface to validate.
* @param klass The proxy class representing the interface.
*/
private static void validateMethodsAreEitherBeanMethodOrKnownMethod(
Class<? extends PipelineOptions> iface,
Class<? extends PipelineOptions> klass,
List<PropertyDescriptor> descriptors) {
Set<Method> knownMethods = Sets.newHashSet(IGNORED_METHODS);
// Ignore synthetic methods
for (Method method : klass.getMethods()) {
if (Modifier.isStatic(method.getModifiers()) || method.isSynthetic()) {
knownMethods.add(method);
}
}
// Ignore methods on the base PipelineOptions interface.
try {
knownMethods.add(iface.getMethod("as", Class.class));
knownMethods.add(iface.getMethod("outputRuntimeOptions"));
knownMethods.add(iface.getMethod("populateDisplayData", DisplayData.Builder.class));
} catch (NoSuchMethodException | SecurityException e) {
throw new RuntimeException(e);
}
for (PropertyDescriptor descriptor : descriptors) {
knownMethods.add(descriptor.getReadMethod());
knownMethods.add(descriptor.getWriteMethod());
}
final Set<String> knownMethodsNames = Sets.newHashSet();
for (Method method : knownMethods) {
knownMethodsNames.add(method.getName());
}
// Verify that no additional methods are on an interface that aren't a bean property.
// Because methods can have multiple declarations, we do a name-based comparison
// here to prevent false positives.
SortedSet<Method> unknownMethods = new TreeSet<>(MethodComparator.INSTANCE);
unknownMethods.addAll(
Sets.filter(
Sets.difference(Sets.newHashSet(iface.getMethods()), knownMethods),
Predicates.and(
NOT_SYNTHETIC_PREDICATE,
input -> !knownMethodsNames.contains(input.getName()),
NOT_STATIC_PREDICATE)));
checkArgument(
unknownMethods.isEmpty(),
"Methods %s on [%s] do not conform to being bean properties.",
FluentIterable.from(unknownMethods).transform(ReflectHelpers.METHOD_FORMATTER),
iface.getName());
}
private static void checkInheritedFrom(
Class<?> checkClass, Class fromClass, Set<Class<?>> nonPipelineOptions) {
if (checkClass.equals(fromClass)) {
return;
}
if (checkClass.getInterfaces().length == 0) {
nonPipelineOptions.add(checkClass);
return;
}
for (Class<?> klass : checkClass.getInterfaces()) {
checkInheritedFrom(klass, fromClass, nonPipelineOptions);
}
}
private static void throwNonPipelineOptions(
Class<?> klass, Set<Class<?>> nonPipelineOptionsClasses) {
StringBuilder errorBuilder =
new StringBuilder(
String.format(
"All inherited interfaces of [%s] should inherit from the PipelineOptions interface. "
+ "The following inherited interfaces do not:",
klass.getName()));
for (Class<?> invalidKlass : nonPipelineOptionsClasses) {
errorBuilder.append(String.format("%n - %s", invalidKlass.getName()));
}
throw new IllegalArgumentException(errorBuilder.toString());
}
private static void validateInheritedInterfacesExtendPipelineOptions(Class<?> klass) {
Set<Class<?>> nonPipelineOptionsClasses = new LinkedHashSet<>();
checkInheritedFrom(klass, PipelineOptions.class, nonPipelineOptionsClasses);
if (!nonPipelineOptionsClasses.isEmpty()) {
throwNonPipelineOptions(klass, nonPipelineOptionsClasses);
}
}
private static class MultipleDefinitions {
private Method method;
private SortedSet<Method> collidingMethods;
}
private static void throwForMultipleDefinitions(
Class<? extends PipelineOptions> iface, List<MultipleDefinitions> definitions) {
if (definitions.size() == 1) {
MultipleDefinitions errDef = definitions.get(0);
throw new IllegalArgumentException(
String.format(
"Method [%s] has multiple definitions %s with different return types for [%s].",
errDef.method.getName(), errDef.collidingMethods, iface.getName()));
} else if (definitions.size() > 1) {
StringBuilder errorBuilder =
new StringBuilder(
String.format(
"Interface [%s] has Methods with multiple definitions with different return types:",
iface.getName()));
for (MultipleDefinitions errDef : definitions) {
errorBuilder.append(
String.format(
"%n - Method [%s] has multiple definitions %s",
errDef.method.getName(), errDef.collidingMethods));
}
throw new IllegalArgumentException(errorBuilder.toString());
}
}
private static class InconsistentlyAnnotatedGetters {
PropertyDescriptor descriptor;
Iterable<String> getterClassNames;
Iterable<String> gettersWithTheAnnotationClassNames;
}
private static void throwForGettersWithInconsistentAnnotation(
List<InconsistentlyAnnotatedGetters> getters, Class<? extends Annotation> annotationClass) {
if (getters.size() == 1) {
InconsistentlyAnnotatedGetters getter = getters.get(0);
throw new IllegalArgumentException(
String.format(
"Expected getter for property [%s] to be marked with @%s on all %s, "
+ "found only on %s",
getter.descriptor.getName(),
annotationClass.getSimpleName(),
getter.getterClassNames,
getter.gettersWithTheAnnotationClassNames));
} else if (getters.size() > 1) {
StringBuilder errorBuilder =
new StringBuilder(
String.format(
"Property getters are inconsistently marked with @%s:",
annotationClass.getSimpleName()));
for (InconsistentlyAnnotatedGetters getter : getters) {
errorBuilder.append(
String.format(
"%n - Expected for property [%s] to be marked on all %s, " + "found only on %s",
getter.descriptor.getName(),
getter.getterClassNames,
getter.gettersWithTheAnnotationClassNames));
}
throw new IllegalArgumentException(errorBuilder.toString());
}
}
private static class AnnotatedSetter {
PropertyDescriptor descriptor;
Iterable<String> settersWithTheAnnotationClassNames;
}
private static void throwForSettersWithTheAnnotation(
List<AnnotatedSetter> setters, Class<? extends Annotation> annotationClass) {
if (setters.size() == 1) {
AnnotatedSetter setter = setters.get(0);
throw new IllegalArgumentException(
String.format(
"Expected setter for property [%s] to not be marked with @%s on %s",
setter.descriptor.getName(),
annotationClass.getSimpleName(),
setter.settersWithTheAnnotationClassNames));
} else if (setters.size() > 1) {
StringBuilder builder =
new StringBuilder(
String.format("Found setters marked with @%s:", annotationClass.getSimpleName()));
for (AnnotatedSetter setter : setters) {
builder.append(
String.format(
"%n - Setter for property [%s] should not be marked with @%s on %s",
setter.descriptor.getName(),
annotationClass.getSimpleName(),
setter.settersWithTheAnnotationClassNames));
}
throw new IllegalArgumentException(builder.toString());
}
}
private static class MissingBeanMethod {
String methodType;
PropertyDescriptor property;
}
private static void throwForMissingBeanMethod(
Class<? extends PipelineOptions> iface, List<MissingBeanMethod> missingBeanMethods) {
if (missingBeanMethods.size() == 1) {
MissingBeanMethod missingBeanMethod = missingBeanMethods.get(0);
throw new IllegalArgumentException(
String.format(
"Expected %s for property [%s] of type [%s] on [%s].",
missingBeanMethod.methodType,
missingBeanMethod.property.getName(),
missingBeanMethod.property.getPropertyType().getName(),
iface.getName()));
} else if (missingBeanMethods.size() > 1) {
StringBuilder builder =
new StringBuilder(
String.format("Found missing property methods on [%s]:", iface.getName()));
for (MissingBeanMethod method : missingBeanMethods) {
builder.append(
String.format(
"%n - Expected %s for property [%s] of type [%s]",
method.methodType,
method.property.getName(),
method.property.getPropertyType().getName()));
}
throw new IllegalArgumentException(builder.toString());
}
}
/** A {@link Comparator} that uses the classes name to compare them. */
private static class ClassNameComparator implements Comparator<Class<?>> {
static final ClassNameComparator INSTANCE = new ClassNameComparator();
@Override
public int compare(Class<?> o1, Class<?> o2) {
return o1.getName().compareTo(o2.getName());
}
}
/** A {@link Comparator} that uses the generic method signature to sort them. */
private static class MethodComparator implements Comparator<Method> {
static final MethodComparator INSTANCE = new MethodComparator();
@Override
public int compare(Method o1, Method o2) {
return o1.toGenericString().compareTo(o2.toGenericString());
}
}
/** A {@link Comparator} that uses the methods name to compare them. */
static class MethodNameComparator implements Comparator<Method> {
static final MethodNameComparator INSTANCE = new MethodNameComparator();
@Override
public int compare(Method o1, Method o2) {
return o1.getName().compareTo(o2.getName());
}
}
/** A {@link Function} that gets the method's return type. */
private static class ReturnTypeFetchingFunction implements Function<Method, Class<?>> {
static final ReturnTypeFetchingFunction INSTANCE = new ReturnTypeFetchingFunction();
@SuppressFBWarnings(
value = "NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION",
justification = "https://github.com/google/guava/issues/920")
@Override
public Class<?> apply(@Nonnull Method input) {
return input.getReturnType();
}
}
/** A {@link Function} with returns the declaring class for the method. */
private static class MethodToDeclaringClassFunction implements Function<Method, Class<?>> {
static final MethodToDeclaringClassFunction INSTANCE = new MethodToDeclaringClassFunction();
@SuppressFBWarnings(
value = "NP_METHOD_PARAMETER_TIGHTENS_ANNOTATION",
justification = "https://github.com/google/guava/issues/920")
@Override
public Class<?> apply(@Nonnull Method input) {
return input.getDeclaringClass();
}
}
/**
* A {@link Predicate} that returns true if the method is annotated with {@code annotationClass}.
*/
static class AnnotationPredicates {
static final AnnotationPredicates JSON_IGNORE =
new AnnotationPredicates(
JsonIgnore.class,
input -> JsonIgnore.class.equals(input.annotationType()),
input -> input.isAnnotationPresent(JsonIgnore.class));
private static final Set<Class<?>> DEFAULT_ANNOTATION_CLASSES =
Sets.newHashSet(
FluentIterable.from(Default.class.getDeclaredClasses()).filter(Class::isAnnotation));
static final AnnotationPredicates DEFAULT_VALUE =
new AnnotationPredicates(
Default.class,
input -> DEFAULT_ANNOTATION_CLASSES.contains(input.annotationType()),
input -> {
for (Annotation annotation : input.getAnnotations()) {
if (DEFAULT_ANNOTATION_CLASSES.contains(annotation.annotationType())) {
return true;
}
}
return false;
});
final Class<? extends Annotation> annotationClass;
final Predicate<Annotation> forAnnotation;
final Predicate<Method> forMethod;
AnnotationPredicates(
Class<? extends Annotation> annotationClass,
Predicate<Annotation> forAnnotation,
Predicate<Method> forMethod) {
this.annotationClass = annotationClass;
this.forAnnotation = forAnnotation;
this.forMethod = forMethod;
}
}
/**
* Splits string arguments based upon expected pattern of --argName=value.
*
* <p>Example GNU style command line arguments:
*
* <pre>
* --project=MyProject (simple property, will set the "project" property to "MyProject")
* --readOnly=true (for boolean properties, will set the "readOnly" property to "true")
* --readOnly (shorthand for boolean properties, will set the "readOnly" property to "true")
* --x=1 --x=2 --x=3 (list style simple property, will set the "x" property to [1, 2, 3])
* --x=1,2,3 (shorthand list style simple property, will set the "x" property to [1, 2, 3])
* --complexObject='{"key1":"value1",...} (JSON format for all other complex types)
* </pre>
*
* <p>Simple properties are able to bound to {@link String}, {@link Class}, enums and Java
* primitives {@code boolean}, {@code byte}, {@code short}, {@code int}, {@code long}, {@code
* float}, {@code double} and their primitive wrapper classes.
*
* <p>Simple list style properties are able to be bound to {@code boolean[]}, {@code char[]},
* {@code short[]}, {@code int[]}, {@code long[]}, {@code float[]}, {@code double[]}, {@code
* Class[]}, enum arrays, {@code String[]}, and {@code List<String>}.
*
* <p>JSON format is required for all other types.
*
* <p>If strict parsing is enabled, options must start with '--', and not have an empty argument
* name or value based upon the positioning of the '='. Empty or null arguments will be ignored
* whether or not strict parsing is enabled.
*/
private static ListMultimap<String, String> parseCommandLine(
String[] args, boolean strictParsing) {
ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
for (String arg : args) {
if (Strings.isNullOrEmpty(arg)) {
continue;
}
try {
checkArgument(arg.startsWith("--"), "Argument '%s' does not begin with '--'", arg);
int index = arg.indexOf('=');
// Make sure that '=' isn't the first character after '--' or the last character
checkArgument(
index != 2, "Argument '%s' starts with '--=', empty argument name not allowed", arg);
if (index > 0) {
builder.put(arg.substring(2, index), arg.substring(index + 1, arg.length()));
} else {
builder.put(arg.substring(2), "true");
}
} catch (IllegalArgumentException e) {
if (strictParsing) {
throw e;
} else {
LOG.warn(
"Strict parsing is disabled, ignoring option '{}' because {}", arg, e.getMessage());
}
}
}
return builder.build();
}
/**
* Using the parsed string arguments, we convert the strings to the expected return type of the
* methods that are found on the passed-in class.
*
* <p>For any return type that is expected to be an array or a collection, we further split up
* each string on ','.
*
* <p>We special case the "runner" option. It is mapped to the class of the {@link PipelineRunner}
* based off of the {@link PipelineRunner PipelineRunners} simple class name. If the provided
* runner name is not registered via a {@link PipelineRunnerRegistrar}, we attempt to obtain the
* class that the name represents using {@link Class#forName(String)} and use the result class if
* it subclasses {@link PipelineRunner}.
*
* <p>If strict parsing is enabled, unknown options or options that cannot be converted to the
* expected java type using an {@link ObjectMapper} will be ignored.
*/
private static <T extends PipelineOptions> Map<String, Object> parseObjects(
Class<T> klass, ListMultimap<String, String> options, boolean strictParsing) {
Map<String, Method> propertyNamesToGetters = Maps.newHashMap();
Cache cache = CACHE.get();
cache.validateWellFormed(klass);
@SuppressWarnings("unchecked")
Iterable<PropertyDescriptor> propertyDescriptors =
cache.getPropertyDescriptors(
FluentIterable.from(getRegisteredOptions()).append(klass).toSet());
for (PropertyDescriptor descriptor : propertyDescriptors) {
propertyNamesToGetters.put(descriptor.getName(), descriptor.getReadMethod());
}
Map<String, Object> convertedOptions = Maps.newHashMap();
for (final Map.Entry<String, Collection<String>> entry : options.asMap().entrySet()) {
try {
// Search for close matches for missing properties.
// Either off by one or off by two character errors.
if (!propertyNamesToGetters.containsKey(entry.getKey())) {
SortedSet<String> closestMatches =
new TreeSet<>(
Sets.filter(
propertyNamesToGetters.keySet(),
input -> StringUtils.getLevenshteinDistance(entry.getKey(), input) <= 2));
switch (closestMatches.size()) {
case 0:
throw new IllegalArgumentException(
String.format("Class %s missing a property named '%s'.", klass, entry.getKey()));
case 1:
throw new IllegalArgumentException(
String.format(
"Class %s missing a property named '%s'. Did you mean '%s'?",
klass, entry.getKey(), Iterables.getOnlyElement(closestMatches)));
default:
throw new IllegalArgumentException(
String.format(
"Class %s missing a property named '%s'. Did you mean one of %s?",
klass, entry.getKey(), closestMatches));
}
}
Method method = propertyNamesToGetters.get(entry.getKey());
// Only allow empty argument values for String, String Array, and Collection<String>.
Class<?> returnType = method.getReturnType();
JavaType type = MAPPER.getTypeFactory().constructType(method.getGenericReturnType());
if ("runner".equals(entry.getKey())) {
String runner = Iterables.getOnlyElement(entry.getValue());
final Map<String, Class<? extends PipelineRunner<?>>> pipelineRunners =
cache.supportedPipelineRunners;
if (pipelineRunners.containsKey(runner.toLowerCase())) {
convertedOptions.put("runner", pipelineRunners.get(runner.toLowerCase(ROOT)));
} else {
try {
Class<?> runnerClass = Class.forName(runner, true, ReflectHelpers.findClassLoader());
if (!PipelineRunner.class.isAssignableFrom(runnerClass)) {
throw new IllegalArgumentException(
String.format(
"Class '%s' does not implement PipelineRunner. "
+ "Supported pipeline runners %s",
runner, cache.getSupportedRunners()));
}
convertedOptions.put("runner", runnerClass);
} catch (ClassNotFoundException e) {
String msg =
String.format(
"Unknown 'runner' specified '%s', supported pipeline runners %s",
runner, cache.getSupportedRunners());
throw new IllegalArgumentException(msg, e);
}
}
} else if (isCollectionOrArrayOfAllowedTypes(returnType, type)) {
// Split any strings with ","
List<String> values =
FluentIterable.from(entry.getValue())
.transformAndConcat(input -> Arrays.asList(input.split(",")))
.toList();
if (values.contains("")) {
checkEmptyStringAllowed(returnType, type, method.getGenericReturnType().toString());
}
convertedOptions.put(entry.getKey(), MAPPER.convertValue(values, type));
} else if (isSimpleType(returnType, type)) {
String value = Iterables.getOnlyElement(entry.getValue());
if (value.isEmpty()) {
checkEmptyStringAllowed(returnType, type, method.getGenericReturnType().toString());
}
convertedOptions.put(entry.getKey(), MAPPER.convertValue(value, type));
} else {
String value = Iterables.getOnlyElement(entry.getValue());
if (value.isEmpty()) {
checkEmptyStringAllowed(returnType, type, method.getGenericReturnType().toString());
}
try {
convertedOptions.put(entry.getKey(), MAPPER.readValue(value, type));
} catch (IOException e) {
throw new IllegalArgumentException("Unable to parse JSON value " + value, e);
}
}
} catch (IllegalArgumentException e) {
if (strictParsing) {
throw e;
} else {
LOG.warn(
"Strict parsing is disabled, ignoring option '{}' with value '{}' because {}",
entry.getKey(),
entry.getValue(),
e.getMessage());
}
}
}
return convertedOptions;
}
/**
* Returns true if the given type is one of {@code SIMPLE_TYPES} or an enum, or if the given type
* is a {@link ValueProvider ValueProvider&lt;T&gt;} and {@code T} is one of {@code SIMPLE_TYPES}
* or an enum.
*/
private static boolean isSimpleType(Class<?> type, JavaType genericType) {
Class<?> unwrappedType =
type.equals(ValueProvider.class) ? genericType.containedType(0).getRawClass() : type;
return SIMPLE_TYPES.contains(unwrappedType) || unwrappedType.isEnum();
}
/**
* Returns true if the given type is an array or {@link Collection} of {@code SIMPLE_TYPES} or
* enums, or if the given type is a {@link ValueProvider ValueProvider&lt;T&gt;} and {@code T} is
* an array or {@link Collection} of {@code SIMPLE_TYPES} or enums.
*/
private static boolean isCollectionOrArrayOfAllowedTypes(Class<?> type, JavaType genericType) {
JavaType containerType =
type.equals(ValueProvider.class) ? genericType.containedType(0) : genericType;
// Check if it is an array of simple types or enum.
if (containerType.getRawClass().isArray()
&& (SIMPLE_TYPES.contains(containerType.getRawClass().getComponentType())
|| containerType.getRawClass().getComponentType().isEnum())) {
return true;
}
// Check if it is Collection of simple types or enum.
if (Collection.class.isAssignableFrom(containerType.getRawClass())) {
JavaType innerType = containerType.containedType(0);
// Note that raw types are allowed, hence the null check.
if (innerType == null
|| SIMPLE_TYPES.contains(innerType.getRawClass())
|| innerType.getRawClass().isEnum()) {
return true;
}
}
return false;
}
/**
* Ensures that empty string value is allowed for a given type.
*
* <p>Empty strings are only allowed for {@link String}, {@link String String[]}, {@link
* Collection Collection&lt;String&gt;}, or {@link ValueProvider ValueProvider&lt;T&gt;} and
* {@code T} is of type {@link String}, {@link String String[]}, {@link Collection
* Collection&lt;String&gt;}.
*
* @param type class object for the type under check.
* @param genericType complete type information for the type under check.
* @param genericTypeName a string representation of the complete type information.
*/
private static void checkEmptyStringAllowed(
Class<?> type, JavaType genericType, String genericTypeName) {
JavaType unwrappedType =
type.equals(ValueProvider.class) ? genericType.containedType(0) : genericType;
Class<?> containedType = unwrappedType.getRawClass();
if (unwrappedType.getRawClass().isArray()) {
containedType = unwrappedType.getRawClass().getComponentType();
} else if (Collection.class.isAssignableFrom(unwrappedType.getRawClass())) {
JavaType innerType = unwrappedType.containedType(0);
// Note that raw types are allowed, hence the null check.
containedType = innerType == null ? String.class : innerType.getRawClass();
}
if (!containedType.equals(String.class)) {
String msg =
String.format(
"Empty argument value is only allowed for String, String Array, "
+ "Collections of Strings or any of these types in a parameterized ValueProvider, "
+ "but received: %s",
genericTypeName);
throw new IllegalArgumentException(msg);
}
}
/** Hold all data which can change after a classloader change. */
static final class Cache {
private final Map<String, Class<? extends PipelineRunner<?>>> supportedPipelineRunners;
/** The set of options that have been registered and visible to the user. */
private final Set<Class<? extends PipelineOptions>> registeredOptions =
Sets.newConcurrentHashSet();
/** A cache storing a mapping from a given interface to its registration record. */
private final Map<Class<? extends PipelineOptions>, Registration<?>> interfaceCache =
Maps.newConcurrentMap();
/** A cache storing a mapping from a set of interfaces to its registration record. */
private final Map<Set<Class<? extends PipelineOptions>>, Registration<?>> combinedCache =
Maps.newConcurrentMap();
private Cache() {
final ClassLoader loader = ReflectHelpers.findClassLoader();
// Store the list of all available pipeline runners.
ImmutableMap.Builder<String, Class<? extends PipelineRunner<?>>> builder =
ImmutableMap.builder();
for (PipelineRunnerRegistrar registrar :
ReflectHelpers.loadServicesOrdered(PipelineRunnerRegistrar.class, loader)) {
for (Class<? extends PipelineRunner<?>> klass : registrar.getPipelineRunners()) {
String runnerName = klass.getSimpleName().toLowerCase();
builder.put(runnerName, klass);
if (runnerName.endsWith("runner")) {
builder.put(runnerName.substring(0, runnerName.length() - "Runner".length()), klass);
}
}
}
supportedPipelineRunners = builder.build();
initializeRegistry(loader);
}
/** Load and register the list of all classes that extend PipelineOptions. */
private void initializeRegistry(final ClassLoader loader) {
for (PipelineOptionsRegistrar registrar :
ReflectHelpers.loadServicesOrdered(PipelineOptionsRegistrar.class, loader)) {
for (Class<? extends PipelineOptions> klass : registrar.getPipelineOptions()) {
register(klass);
}
}
}
private synchronized void register(Class<? extends PipelineOptions> iface) {
checkNotNull(iface);
checkArgument(iface.isInterface(), "Only interface types are supported.");
if (registeredOptions.contains(iface)) {
return;
}
validateWellFormed(iface);
registeredOptions.add(iface);
}
private <T extends PipelineOptions> Registration<T> validateWellFormed(Class<T> iface) {
return validateWellFormed(iface, registeredOptions);
}
@VisibleForTesting
Set<String> getSupportedRunners() {
ImmutableSortedSet.Builder<String> supportedRunners = ImmutableSortedSet.naturalOrder();
for (Class<? extends PipelineRunner<?>> runner : supportedPipelineRunners.values()) {
supportedRunners.add(runner.getSimpleName());
}
return supportedRunners.build();
}
@VisibleForTesting
Map<String, Class<? extends PipelineRunner<?>>> getSupportedPipelineRunners() {
return supportedPipelineRunners;
}
/**
* Validates that the interface conforms to the following:
*
* <ul>
* <li>Every inherited interface of {@code iface} must extend PipelineOptions except for
* PipelineOptions itself.
* <li>Any property with the same name must have the same return type for all derived
* interfaces of {@link PipelineOptions}.
* <li>Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
* <li>Every method must conform to being a getter or setter for a JavaBean.
* <li>The derived interface of {@link PipelineOptions} must be composable with every
* interface part of allPipelineOptionsClasses.
* <li>Only getters may be annotated with {@link JsonIgnore @JsonIgnore}.
* <li>If any getter is annotated with {@link JsonIgnore @JsonIgnore}, then all getters for
* this property must be annotated with {@link JsonIgnore @JsonIgnore}.
* </ul>
*
* @param iface The interface to validate.
* @param validatedPipelineOptionsInterfaces The set of validated pipeline options interfaces to
* validate against.
* @return A registration record containing the proxy class and bean info for iface.
*/
synchronized <T extends PipelineOptions> Registration<T> validateWellFormed(
Class<T> iface, Set<Class<? extends PipelineOptions>> validatedPipelineOptionsInterfaces) {
checkArgument(iface.isInterface(), "Only interface types are supported.");
// Validate that every inherited interface must extend PipelineOptions except for
// PipelineOptions itself.
validateInheritedInterfacesExtendPipelineOptions(iface);
@SuppressWarnings("unchecked")
Set<Class<? extends PipelineOptions>> combinedPipelineOptionsInterfaces =
FluentIterable.from(validatedPipelineOptionsInterfaces).append(iface).toSet();
// Validate that the view of all currently passed in options classes is well formed.
if (!combinedCache.containsKey(combinedPipelineOptionsInterfaces)) {
final Class<?>[] interfaces = combinedPipelineOptionsInterfaces.toArray(EMPTY_CLASS_ARRAY);
@SuppressWarnings("unchecked")
Class<T> allProxyClass =
(Class<T>) Proxy.getProxyClass(ReflectHelpers.findClassLoader(interfaces), interfaces);
try {
List<PropertyDescriptor> propertyDescriptors =
validateClass(iface, validatedPipelineOptionsInterfaces, allProxyClass);
combinedCache.put(
combinedPipelineOptionsInterfaces,
new Registration<>(allProxyClass, propertyDescriptors));
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
}
// Validate that the local view of the class is well formed.
if (!interfaceCache.containsKey(iface)) {
@SuppressWarnings({"rawtypes", "unchecked"})
Class<T> proxyClass =
(Class<T>)
Proxy.getProxyClass(ReflectHelpers.findClassLoader(iface), new Class[] {iface});
try {
List<PropertyDescriptor> propertyDescriptors =
validateClass(iface, validatedPipelineOptionsInterfaces, proxyClass);
interfaceCache.put(iface, new Registration<>(proxyClass, propertyDescriptors));
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
Registration<T> result = (Registration<T>) interfaceCache.get(iface);
return result;
}
List<PropertyDescriptor> getPropertyDescriptors(
Set<Class<? extends PipelineOptions>> interfaces) {
return combinedCache.get(interfaces).getPropertyDescriptors();
}
}
}