
com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory Maven / Gradle / Ivy
/*
* Copyright (C) 2015 Google Inc.
*
* 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.google.cloud.dataflow.sdk.options;
import com.google.cloud.dataflow.sdk.options.Validation.Required;
import com.google.cloud.dataflow.sdk.runners.PipelineRunner;
import com.google.cloud.dataflow.sdk.runners.PipelineRunnerRegistrar;
import com.google.cloud.dataflow.sdk.runners.worker.DataflowWorkerHarness;
import com.google.cloud.dataflow.sdk.util.StringUtils;
import com.google.cloud.dataflow.sdk.util.common.ReflectHelpers;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nullable;
/**
* 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:
*
* - Any property with the same name must have the same return type for all derived interfaces
* of {@link PipelineOptions}.
*
- Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
*
- Every method must conform to being a getter or setter for a JavaBean.
*
- The derived interface of {@link PipelineOptions} must be composable with every interface
* registered with this factory.
*
*
* See the JavaBeans
* specification 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 }.
* This sets the {@link ApplicationNameOptions#getAppName() "appName"} to the calling
* {@link Class#getSimpleName() classes simple name}.
*
* Note that {@code } must be composable with every registered interface with this factory.
* See {@link PipelineOptionsFactory#validateWellFormed(Class, Set)} for more details.
*
* @return An object that implements {@code }.
*/
public static T as(Class klass) {
return new Builder().as(klass);
}
/**
* Sets the command line arguments to parse when constructing the {@link PipelineOptions}.
*
* Example GNU style command line arguments:
*
* --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 property, will set the "x" property to [1, 2, 3])
* --x=1,2,3 (shorthand list style property, will set the "x" property to [1, 2, 3])
* --complexObject='{"key1":"value1",...} (JSON format for all other complex types)
*
*
* 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.
*
* 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}.
*
* JSON format is required for all other types.
*
* 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.
*
* 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 {@link PipelineOptions} conforms to all the
* validation criteria from {@code }. 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;
// Do not allow direct instantiation
private Builder() {
this(null, false, true);
}
private Builder(String[] args, boolean validation,
boolean strictParsing) {
this.defaultAppName = findCallersClassName();
this.args = args;
this.validation = validation;
this.strictParsing = strictParsing;
}
/**
* Sets the command line arguments to parse when constructing the {@link PipelineOptions}.
*
* Example GNU style command line arguments:
*
* --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 property, will set the "x" property to [1, 2, 3])
* --x=1,2,3 (shorthand list style property, will set the "x" property to [1, 2, 3])
* --complexObject='{"key1":"value1",...} (JSON format for all other complex types)
*
*
* 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.
*
* 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}.
*
* JSON format is required for all other types.
*
* 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.
*
* 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) {
Preconditions.checkNotNull(args, "Arguments should not be null.");
return new Builder(args, validation, strictParsing);
}
/**
* After creation we will validate that {@link PipelineOptions} conforms to all the
* validation criteria from {@code }. See
* {@link PipelineOptionsValidator#validate(Class, PipelineOptions)} for more details about
* validation.
*/
public Builder withValidation() {
return new Builder(args, true, strictParsing);
}
/**
* During parsing of the arguments, we will skip over improperly formatted and unknown
* arguments.
*/
public Builder withoutStrictParsing() {
return new Builder(args, validation, false);
}
/**
* 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 } using the values configured on
* this builder during construction.
*
* Note that {@code } must be composable with every registered interface with this factory.
* See {@link PipelineOptionsFactory#validateWellFormed(Class, Set)} for more details.
*
* @return An object that implements {@code }.
*/
public T as(Class klass) {
Map initialOptions = Maps.newHashMap();
// Attempt to parse the arguments into the set of initial options to use
if (args != null) {
ListMultimap 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);
}
if (validation) {
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.
*
* Visible for testing.
* {@code printStream} and {@code exit} used for testing.
*/
@SuppressWarnings("unchecked")
static boolean printHelpUsageAndExitIfNeeded(ListMultimap 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);
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> matches = Iterables.filter(
getRegisteredOptions(),
new Predicate>() {
@Override
public boolean apply(Class extends PipelineOptions> input) {
if (helpOption.contains(".")) {
return input.getName().endsWith(helpOption);
} else {
return input.getSimpleName().equals(helpOption);
}
}
});
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,
Iterables.transform(matches, ReflectHelpers.CLASS_NAME));
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 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()).getSimpleName();
} catch (ClassNotFoundException e) {
break;
}
}
}
return "unknown";
}
/**
* Stores the generated proxyClass and its respective {@link BeanInfo} object.
*
* @param The type of the proxyClass.
*/
static class Registration {
private final Class proxyClass;
private final List propertyDescriptors;
public Registration(Class proxyClass, List beanInfo) {
this.proxyClass = proxyClass;
this.propertyDescriptors = beanInfo;
}
List getPropertyDescriptors() {
return propertyDescriptors;
}
Class getProxyClass() {
return proxyClass;
}
}
private static final Set> SIMPLE_TYPES = ImmutableSet.>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];
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Map>> SUPPORTED_PIPELINE_RUNNERS;
/** Classes that are used as the boundary in the stack trace to find the callers class name. */
private static final Set 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 IGNORED_METHODS;
/** The set of options that have been registered and visible to the user. */
private static final Set> REGISTERED_OPTIONS =
Sets.newConcurrentHashSet();
/** A cache storing a mapping from a given interface to its registration record. */
private static final Map, Registration>> INTERFACE_CACHE =
Maps.newConcurrentMap();
/** A cache storing a mapping from a set of interfaces to its registration record. */
private static final Map>, Registration>> COMBINED_CACHE =
Maps.newConcurrentMap();
/** The width at which options should be output. */
private static final int TERMINAL_WIDTH = 80;
/**
* Finds the appropriate {@code ClassLoader} to be used by the
* {@link ServiceLoader#load} call, which by default would use the context
* {@code ClassLoader}, which can be null. The fallback is as follow: context
* ClassLoader, class ClassLoader and finaly the system ClassLoader.
*/
static ClassLoader findClassLoader() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = PipelineOptionsFactory.class.getClassLoader();
}
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
return classLoader;
}
static {
try {
IGNORED_METHODS = ImmutableSet.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);
}
ClassLoader classLoader = findClassLoader();
// Store the list of all available pipeline runners.
ImmutableMap.Builder>> builder =
ImmutableMap.builder();
Set pipelineRunnerRegistrars =
Sets.newTreeSet(ObjectsClassComparator.INSTANCE);
pipelineRunnerRegistrars.addAll(
Lists.newArrayList(ServiceLoader.load(PipelineRunnerRegistrar.class, classLoader)));
for (PipelineRunnerRegistrar registrar : pipelineRunnerRegistrars) {
for (Class extends PipelineRunner>> klass : registrar.getPipelineRunners()) {
builder.put(klass.getSimpleName(), klass);
}
}
SUPPORTED_PIPELINE_RUNNERS = builder.build();
// Load and register the list of all classes that extend PipelineOptions.
register(PipelineOptions.class);
Set pipelineOptionsRegistrars =
Sets.newTreeSet(ObjectsClassComparator.INSTANCE);
pipelineOptionsRegistrars.addAll(
Lists.newArrayList(ServiceLoader.load(PipelineOptionsRegistrar.class, classLoader)));
for (PipelineOptionsRegistrar registrar : pipelineOptionsRegistrars) {
for (Class extends PipelineOptions> klass : registrar.getPipelineOptions()) {
register(klass);
}
}
}
/**
* This registers the interface with this factory. This interface must conform to the following
* restrictions:
*
* - Any property with the same name must have the same return type for all derived
* interfaces of {@link PipelineOptions}.
*
- Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
*
- Every method must conform to being a getter or setter for a JavaBean.
*
- The derived interface of {@link PipelineOptions} must be composable with every interface
* registered with this factory.
*
*
* @param iface The interface object to manually register.
*/
public static synchronized void register(Class extends PipelineOptions> iface) {
Preconditions.checkNotNull(iface);
Preconditions.checkArgument(iface.isInterface(), "Only interface types are supported.");
if (REGISTERED_OPTIONS.contains(iface)) {
return;
}
validateWellFormed(iface, REGISTERED_OPTIONS);
REGISTERED_OPTIONS.add(iface);
}
/**
* Validates that the interface conforms to the following:
*
* - Any property with the same name must have the same return type for all derived
* interfaces of {@link PipelineOptions}.
*
- Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
*
- Every method must conform to being a getter or setter for a JavaBean.
*
- The derived interface of {@link PipelineOptions} must be composable with every interface
* part of allPipelineOptionsClasses.
*
- Only getters may be annotated with {@link JsonIgnore @JsonIgnore}.
*
- If any getter is annotated with {@link JsonIgnore @JsonIgnore}, then all getters for
* this property must be annotated with {@link JsonIgnore @JsonIgnore}.
*
*
* @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.
*/
static synchronized Registration validateWellFormed(
Class iface, Set> validatedPipelineOptionsInterfaces) {
Preconditions.checkArgument(iface.isInterface(), "Only interface types are supported.");
@SuppressWarnings("unchecked")
Set> combinedPipelineOptionsInterfaces =
FluentIterable.from(validatedPipelineOptionsInterfaces).append(iface).toSet();
// Validate that the view of all currently passed in options classes is well formed.
if (!COMBINED_CACHE.containsKey(combinedPipelineOptionsInterfaces)) {
@SuppressWarnings("unchecked")
Class allProxyClass =
(Class) Proxy.getProxyClass(PipelineOptionsFactory.class.getClassLoader(),
combinedPipelineOptionsInterfaces.toArray(EMPTY_CLASS_ARRAY));
try {
List propertyDescriptors =
validateClass(iface, validatedPipelineOptionsInterfaces, allProxyClass);
COMBINED_CACHE.put(combinedPipelineOptionsInterfaces,
new Registration(allProxyClass, propertyDescriptors));
} catch (IntrospectionException e) {
throw Throwables.propagate(e);
}
}
// Validate that the local view of the class is well formed.
if (!INTERFACE_CACHE.containsKey(iface)) {
@SuppressWarnings({"rawtypes", "unchecked"})
Class proxyClass = (Class) Proxy.getProxyClass(
PipelineOptionsFactory.class.getClassLoader(), new Class[] {iface});
try {
List propertyDescriptors =
validateClass(iface, validatedPipelineOptionsInterfaces, proxyClass);
INTERFACE_CACHE.put(iface,
new Registration(proxyClass, propertyDescriptors));
} catch (IntrospectionException e) {
throw Throwables.propagate(e);
}
}
@SuppressWarnings("unchecked")
Registration result = (Registration) INTERFACE_CACHE.get(iface);
return result;
}
public static Set> getRegisteredOptions() {
return Collections.unmodifiableSet(REGISTERED_OPTIONS);
}
/**
* 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) {
Preconditions.checkNotNull(out);
out.println("The set of registered options are:");
Set> sortedOptions =
new TreeSet<>(ClassNameComparator.INSTANCE);
sortedOptions.addAll(REGISTERED_OPTIONS);
for (Class extends PipelineOptions> kls : sortedOptions) {
out.format(" %s%n", kls.getName());
}
out.format("%nUse --help= for detailed help. For example:%n"
+ " --help=DataflowPipelineOptions %n"
+ " --help=com.google.cloud.dataflow.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:
*
* OptionGroup:
* ... option group description ...
*
* --option1={@code } or list of valid enum choices
* Default: value (if available, see {@link Default})
* ... option description ... (if available, see {@link Description})
* --option2={@code } or list of valid enum choices
* Default: value (if available, see {@link Default})
* ... option description ... (if available, see {@link Description})
*
* 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) {
Preconditions.checkNotNull(out);
Preconditions.checkNotNull(iface);
validateWellFormed(iface, REGISTERED_OPTIONS);
Iterable methods = ReflectHelpers.getClosureOfMethodsOnInterface(iface);
ListMultimap, Method> ifaceToMethods = ArrayListMultimap.create();
for (Method method : methods) {
// Process only methods that are not marked as hidden.
if (method.getAnnotation(Hidden.class) == null) {
ifaceToMethods.put(method.getDeclaringClass(), method);
}
}
SortedSet> ifaces = new TreeSet<>(ClassNameComparator.INSTANCE);
// Keep interfaces that are not marked as hidden.
ifaces.addAll(Collections2.filter(ifaceToMethods.keySet(), new Predicate>() {
@Override
public boolean apply(Class> input) {
return input.getAnnotation(Hidden.class) == null;
}
}));
for (Class> currentIface : ifaces) {
Map propertyNamesToGetters =
getPropertyNamesToGetters(ifaceToMethods.get(currentIface));
// Don't output anything if there are no defined options
if (propertyNamesToGetters.isEmpty()) {
continue;
}
SortedSetMultimap requiredGroupNameToProperties =
getRequiredGroupNamesToProperties(propertyNamesToGetters);
out.format("%s:%n", currentIface.getName());
prettyPrintDescription(out, currentIface.getAnnotation(Description.class));
out.println();
List lists = Lists.newArrayList(propertyNamesToGetters.keySet());
Collections.sort(lists, 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 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();
}
}
/**
* 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 requiredGroupNameToProperties) {
if (annotation == null || annotation.groups() == null) {
return;
}
for (String group : annotation.groups()) {
SortedSet 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 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>> getRegisteredRunners() {
return SUPPORTED_PIPELINE_RUNNERS;
}
static List getPropertyDescriptors(
Set> interfaces) {
return COMBINED_CACHE.get(interfaces).getPropertyDescriptors();
}
/**
* Creates a set of {@link DataflowWorkerHarnessOptions} based of a set of known system
* properties. This is meant to only be used from the {@link DataflowWorkerHarness} as a method to
* bootstrap the worker harness.
*
* For internal use only.
*
* @return A {@link DataflowWorkerHarnessOptions} object configured for the
* {@link DataflowWorkerHarness}.
*/
public static DataflowWorkerHarnessOptions createFromSystemPropertiesInternal()
throws IOException {
return createFromSystemProperties();
}
/**
* Creates a set of {@link DataflowWorkerHarnessOptions} based of a set of known system
* properties. This is meant to only be used from the {@link DataflowWorkerHarness} as a method to
* bootstrap the worker harness.
*
* @return A {@link DataflowWorkerHarnessOptions} object configured for the
* {@link DataflowWorkerHarness}.
* @deprecated for internal use only
*/
@Deprecated
public static DataflowWorkerHarnessOptions createFromSystemProperties() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
DataflowWorkerHarnessOptions options;
if (System.getProperties().containsKey("sdk_pipeline_options")) {
String serializedOptions = System.getProperty("sdk_pipeline_options");
LOG.info("Worker harness starting with: " + serializedOptions);
options = objectMapper.readValue(serializedOptions, PipelineOptions.class)
.as(DataflowWorkerHarnessOptions.class);
} else {
options = PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class);
}
// These values will not be known at job submission time and must be provided.
if (System.getProperties().containsKey("worker_id")) {
options.setWorkerId(System.getProperty("worker_id"));
}
if (System.getProperties().containsKey("job_id")) {
options.setJobId(System.getProperty("job_id"));
}
return options;
}
/**
* This method is meant to emulate the behavior of {@link Introspector#getBeanInfo(Class, int)}
* to construct the list of {@link PropertyDescriptor}.
*
* TODO: Swap back to using Introspector once the proxy class issue with AppEngine is resolved.
*/
private static List getPropertyDescriptors(Class> beanClass)
throws IntrospectionException {
// The sorting is important to make this method stable.
SortedSet methods = Sets.newTreeSet(MethodComparator.INSTANCE);
methods.addAll(Arrays.asList(beanClass.getMethods()));
SortedMap propertyNamesToGetters = getPropertyNamesToGetters(methods);
List descriptors = Lists.newArrayList();
/*
* 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) {
Class> getterPropertyType = getterMethod.getReturnType();
Class> setterPropertyType = method.getParameterTypes()[0];
Preconditions.checkArgument(getterPropertyType == setterPropertyType,
"Type mismatch between getter and setter methods for property [%s]. "
+ "Getter is of type [%s] whereas setter is of type [%s].",
propertyName, getterPropertyType.getName(), setterPropertyType.getName());
}
descriptors.add(new PropertyDescriptor(
propertyName, getterMethod, method));
}
// Add the remaining getters with missing setters.
for (Map.Entry getterToMethod : propertyNamesToGetters.entrySet()) {
descriptors.add(new PropertyDescriptor(
getterToMethod.getKey(), getterToMethod.getValue(), null));
}
return descriptors;
}
/**
* Returns a map of the property name to the getter method it represents.
* If there are duplicate methods with the same bean name, then it is indeterminate
* as to which method will be returned.
*/
private static SortedMap getPropertyNamesToGetters(Iterable methods) {
SortedMap propertyNamesToGetters = Maps.newTreeMap();
for (Method method : methods) {
String methodName = method.getName();
if ((!methodName.startsWith("get")
&& !methodName.startsWith("is"))
|| method.getParameterTypes().length != 0
|| method.getReturnType() == void.class) {
continue;
}
String propertyName = Introspector.decapitalize(
methodName.startsWith("is") ? methodName.substring(2) : methodName.substring(3));
propertyNamesToGetters.put(propertyName, method);
}
return propertyNamesToGetters;
}
/**
* Returns a map of required groups of arguments to the properties that satisfy the requirement.
*/
private static SortedSetMultimap getRequiredGroupNamesToProperties(
Map propertyNamesToGetters) {
SortedSetMultimap result = TreeMultimap.create();
for (Map.Entry 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:
*
* - Any property with the same name must have the same return type for all derived
* interfaces of {@link PipelineOptions}.
*
- Every bean property of any interface derived from {@link PipelineOptions} must have a
* getter and setter method.
*
- Every method must conform to being a getter or setter for a JavaBean.
*
- Only getters may be annotated with {@link JsonIgnore @JsonIgnore}.
*
- If any getter is annotated with {@link JsonIgnore @JsonIgnore}, then all getters for
* this property must be annotated with {@link JsonIgnore @JsonIgnore}.
*
*
* @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 validateClass(Class extends PipelineOptions> iface,
Set> validatedPipelineOptionsInterfaces,
Class> klass) throws IntrospectionException {
Set methods = Sets.newHashSet(IGNORED_METHODS);
// Ignore static methods, "equals", "hashCode", "toString" and "as" on the generated class.
for (Method method : klass.getMethods()) {
if (Modifier.isStatic(method.getModifiers())) {
methods.add(method);
}
}
try {
methods.add(klass.getMethod("equals", Object.class));
methods.add(klass.getMethod("hashCode"));
methods.add(klass.getMethod("toString"));
methods.add(klass.getMethod("as", Class.class));
methods.add(klass.getMethod("cloneAs", Class.class));
} catch (NoSuchMethodException | SecurityException e) {
throw Throwables.propagate(e);
}
// Verify that there are no methods with the same name with two different return types.
Iterable interfaceMethods = FluentIterable
.from(ReflectHelpers.getClosureOfMethodsOnInterface(iface))
.toSortedSet(MethodComparator.INSTANCE);
SortedSetMultimap methodNameToMethodMap =
TreeMultimap.create(MethodNameComparator.INSTANCE, MethodComparator.INSTANCE);
for (Method method : interfaceMethods) {
methodNameToMethodMap.put(method, method);
}
for (Map.Entry> entry
: methodNameToMethodMap.asMap().entrySet()) {
Set> returnTypes = FluentIterable.from(entry.getValue())
.transform(ReturnTypeFetchingFunction.INSTANCE).toSet();
SortedSet collidingMethods = FluentIterable.from(entry.getValue())
.toSortedSet(MethodComparator.INSTANCE);
Preconditions.checkArgument(returnTypes.size() == 1,
"Method [%s] has multiple definitions %s with different return types for [%s].",
entry.getKey().getName(),
collidingMethods,
iface.getName());
}
// Verify that there is no getter with a mixed @JsonIgnore annotation and verify
// that no setter has @JsonIgnore.
Iterable allInterfaceMethods = FluentIterable
.from(ReflectHelpers.getClosureOfMethodsOnInterfaces(validatedPipelineOptionsInterfaces))
.append(ReflectHelpers.getClosureOfMethodsOnInterface(iface))
.toSortedSet(MethodComparator.INSTANCE);
SortedSetMultimap methodNameToAllMethodMap =
TreeMultimap.create(MethodNameComparator.INSTANCE, MethodComparator.INSTANCE);
for (Method method : allInterfaceMethods) {
methodNameToAllMethodMap.put(method, method);
}
List descriptors = getPropertyDescriptors(klass);
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getReadMethod() == null
|| descriptor.getWriteMethod() == null
|| IGNORED_METHODS.contains(descriptor.getReadMethod())
|| IGNORED_METHODS.contains(descriptor.getWriteMethod())) {
continue;
}
SortedSet getters = methodNameToAllMethodMap.get(descriptor.getReadMethod());
SortedSet gettersWithJsonIgnore = Sets.filter(getters, JsonIgnorePredicate.INSTANCE);
Iterable getterClassNames = FluentIterable.from(getters)
.transform(MethodToDeclaringClassFunction.INSTANCE)
.transform(ReflectHelpers.CLASS_NAME);
Iterable gettersWithJsonIgnoreClassNames = FluentIterable.from(gettersWithJsonIgnore)
.transform(MethodToDeclaringClassFunction.INSTANCE)
.transform(ReflectHelpers.CLASS_NAME);
Preconditions.checkArgument(gettersWithJsonIgnore.isEmpty()
|| getters.size() == gettersWithJsonIgnore.size(),
"Expected getter for property [%s] to be marked with @JsonIgnore on all %s, "
+ "found only on %s",
descriptor.getName(), getterClassNames, gettersWithJsonIgnoreClassNames);
SortedSet settersWithJsonIgnore =
Sets.filter(methodNameToAllMethodMap.get(descriptor.getWriteMethod()),
JsonIgnorePredicate.INSTANCE);
Iterable settersWithJsonIgnoreClassNames = FluentIterable.from(settersWithJsonIgnore)
.transform(MethodToDeclaringClassFunction.INSTANCE)
.transform(ReflectHelpers.CLASS_NAME);
Preconditions.checkArgument(settersWithJsonIgnore.isEmpty(),
"Expected setter for property [%s] to not be marked with @JsonIgnore on %s",
descriptor.getName(), settersWithJsonIgnoreClassNames);
}
// Verify that each property has a matching read and write method.
for (PropertyDescriptor propertyDescriptor : descriptors) {
Preconditions.checkArgument(
IGNORED_METHODS.contains(propertyDescriptor.getWriteMethod())
|| propertyDescriptor.getReadMethod() != null,
"Expected getter for property [%s] of type [%s] on [%s].",
propertyDescriptor.getName(),
propertyDescriptor.getPropertyType().getName(),
iface.getName());
Preconditions.checkArgument(
IGNORED_METHODS.contains(propertyDescriptor.getReadMethod())
|| propertyDescriptor.getWriteMethod() != null,
"Expected setter for property [%s] of type [%s] on [%s].",
propertyDescriptor.getName(),
propertyDescriptor.getPropertyType().getName(),
iface.getName());
methods.add(propertyDescriptor.getReadMethod());
methods.add(propertyDescriptor.getWriteMethod());
}
// Verify that no additional methods are on an interface that aren't a bean property.
SortedSet unknownMethods = new TreeSet<>(MethodComparator.INSTANCE);
unknownMethods.addAll(Sets.difference(Sets.newHashSet(klass.getMethods()), methods));
Preconditions.checkArgument(unknownMethods.isEmpty(),
"Methods %s on [%s] do not conform to being bean properties.",
FluentIterable.from(unknownMethods).transform(ReflectHelpers.METHOD_FORMATTER),
iface.getName());
return descriptors;
}
/** A {@link Comparator} that uses the classes name to compare them. */
private static class ClassNameComparator implements Comparator> {
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 object's classes canonical name to compare them. */
private static class ObjectsClassComparator implements Comparator