/* * 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 com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.auto.value.AutoValue; import com.google.common.base.Defaults; import com.google.common.base.Function; import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.FluentIterable; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.MutableClassToInstanceMap; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import org.apache.beam.sdk.options.PipelineOptionsFactory.AnnotationPredicates; import org.apache.beam.sdk.options.PipelineOptionsFactory.Registration; import org.apache.beam.sdk.options.ValueProvider.RuntimeValueProvider; import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider; import org.apache.beam.sdk.transforms.display.DisplayData; import org.apache.beam.sdk.transforms.display.HasDisplayData; import org.apache.beam.sdk.util.InstanceBuilder; import org.apache.beam.sdk.util.common.ReflectHelpers; /** * Represents and {@link InvocationHandler} for a {@link Proxy}. The invocation handler uses bean * introspection of the proxy class to store and retrieve values based off of the property name. * * <p>Unset properties use the {@code @Default} metadata on the getter to return values. If there * is no {@code @Default} annotation on the getter, then a <a * href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">default</a> as * per the Java Language Specification for the expected return type is returned. * * <p>In addition to the getter/setter pairs, this proxy invocation handler supports * {@link Object#equals(Object)}, {@link Object#hashCode()}, {@link Object#toString()} and * {@link PipelineOptions#as(Class)}. */ @ThreadSafe class ProxyInvocationHandler implements InvocationHandler { /** * No two instances of this class are considered equivalent hence we generate a random hash code. */ private final int hashCode = ThreadLocalRandom.current().nextInt(); private final Set<Class<? extends PipelineOptions>> knownInterfaces; private final ClassToInstanceMap<PipelineOptions> interfaceToProxyCache; private final Map<String, BoundValue> options; private final Map<String, JsonNode> jsonOptions; private final Map<String, String> gettersToPropertyNames; private final Map<String, String> settersToPropertyNames; ProxyInvocationHandler(Map<String, Object> options) { this(bindOptions(options), Maps.<String, JsonNode>newHashMap()); } private static Map<String, BoundValue> bindOptions(Map<String, Object> inputOptions) { HashMap<String, BoundValue> options = Maps.newHashMap(); for (Map.Entry<String, Object> entry : inputOptions.entrySet()) { options.put(entry.getKey(), BoundValue.fromExplicitOption(entry.getValue())); } return options; } private ProxyInvocationHandler( Map<String, BoundValue> options, Map<String, JsonNode> jsonOptions) { this.options = options; this.jsonOptions = jsonOptions; this.knownInterfaces = new HashSet<>(PipelineOptionsFactory.getRegisteredOptions()); gettersToPropertyNames = Maps.newHashMap(); settersToPropertyNames = Maps.newHashMap(); interfaceToProxyCache = MutableClassToInstanceMap.create(); } @Override public Object invoke(Object proxy, Method method, Object[] args) { if (args == null && "toString".equals(method.getName())) { return toString(); } else if (args != null && args.length == 1 && "equals".equals(method.getName())) { return equals(args[0]); } else if (args == null && "hashCode".equals(method.getName())) { return hashCode(); } else if (args == null && "outputRuntimeOptions".equals(method.getName())) { return outputRuntimeOptions((PipelineOptions) proxy); } else if (args != null && "as".equals(method.getName()) && args[0] instanceof Class) { @SuppressWarnings("unchecked") Class<? extends PipelineOptions> clazz = (Class<? extends PipelineOptions>) args[0]; return as(clazz); } else if (args != null && "populateDisplayData".equals(method.getName()) && args[0] instanceof DisplayData.Builder) { @SuppressWarnings("unchecked") DisplayData.Builder builder = (DisplayData.Builder) args[0]; builder.delegate(new PipelineOptionsDisplayData()); return Void.TYPE; } String methodName = method.getName(); synchronized (this) { if (gettersToPropertyNames.containsKey(methodName)) { String propertyName = gettersToPropertyNames.get(methodName); if (!options.containsKey(propertyName)) { // Lazy bind the default to the method. Object value = jsonOptions.containsKey(propertyName) ? getValueFromJson(propertyName, method) : getDefault((PipelineOptions) proxy, method); options.put(propertyName, BoundValue.fromDefault(value)); } return options.get(propertyName).getValue(); } else if (settersToPropertyNames.containsKey(methodName)) { options.put(settersToPropertyNames.get(methodName), BoundValue.fromExplicitOption(args[0])); return Void.TYPE; } } throw new RuntimeException("Unknown method [" + method + "] invoked with args [" + Arrays.toString(args) + "]."); } /** * Track whether options values are explicitly set, or retrieved from defaults. */ @AutoValue abstract static class BoundValue { @Nullable abstract Object getValue(); abstract boolean isDefault(); private static BoundValue of(@Nullable Object value, boolean isDefault) { return new AutoValue_ProxyInvocationHandler_BoundValue(value, isDefault); } /** * Create a {@link BoundValue} representing an explicitly set option. */ static BoundValue fromExplicitOption(@Nullable Object value) { return BoundValue.of(value, false); } /** * Create a {@link BoundValue} representing a default option value. */ static BoundValue fromDefault(@Nullable Object value) { return BoundValue.of(value, true); } } /** * Backing implementation for {@link PipelineOptions#as(Class)}. * * @param iface The interface that the returned object needs to implement. * @return An object that implements the interface {@code <T>}. */ synchronized <T extends PipelineOptions> T as(Class<T> iface) { checkNotNull(iface); checkArgument(iface.isInterface(), "Not an interface: %s", iface); if (!interfaceToProxyCache.containsKey(iface)) { Registration<T> registration = PipelineOptionsFactory.validateWellFormed(iface, knownInterfaces); List<PropertyDescriptor> propertyDescriptors = registration.getPropertyDescriptors(); Class<T> proxyClass = registration.getProxyClass(); gettersToPropertyNames.putAll(generateGettersToPropertyNames(propertyDescriptors)); settersToPropertyNames.putAll(generateSettersToPropertyNames(propertyDescriptors)); knownInterfaces.add(iface); interfaceToProxyCache.putInstance(iface, InstanceBuilder.ofType(proxyClass) .fromClass(proxyClass) .withArg(InvocationHandler.class, this) .build()); } return interfaceToProxyCache.getInstance(iface); } /** * Returns true if the other object is a ProxyInvocationHandler or is a Proxy object and has the * same ProxyInvocationHandler as this. * * @param obj The object to compare against this. * @return true iff the other object is a ProxyInvocationHandler or is a Proxy object and has the * same ProxyInvocationHandler as this. */ @Override public boolean equals(Object obj) { return obj != null && ((obj instanceof ProxyInvocationHandler && this == obj) || (Proxy.isProxyClass(obj.getClass()) && this == Proxy.getInvocationHandler(obj))); } /** * Each instance of this ProxyInvocationHandler is unique and has a random hash code. * * @return A hash code that was generated randomly. */ @Override public int hashCode() { return hashCode; } /** * Returns a map of properties which correspond to {@link RuntimeValueProvider}. */ public Map<String, Map<String, Object>> outputRuntimeOptions(PipelineOptions options) { Set<PipelineOptionSpec> optionSpecs = PipelineOptionsReflector.getOptionSpecs(knownInterfaces); Map<String, Map<String, Object>> properties = Maps.newHashMap(); for (PipelineOptionSpec spec : optionSpecs) { if (spec.getGetterMethod().getReturnType().equals(ValueProvider.class)) { Object vp = invoke(options, spec.getGetterMethod(), null); if (((ValueProvider) vp).isAccessible()) { continue; } Map<String, Object> property = Maps.newHashMap(); property.put("type", ((ParameterizedType) spec.getGetterMethod() .getGenericReturnType()).getActualTypeArguments()[0]); properties.put(spec.getName(), property); } } return properties; } /** * Nested class to handle display data in order to set the display data namespace to something * sensible. */ class PipelineOptionsDisplayData implements HasDisplayData { /** * Populate display data. See {@link HasDisplayData#populateDisplayData}. All explicitly set * pipeline options will be added as display data. */ public void populateDisplayData(DisplayData.Builder builder) { Set<PipelineOptionSpec> optionSpecs = PipelineOptionsReflector.getOptionSpecs(knownInterfaces); Multimap<String, PipelineOptionSpec> optionsMap = buildOptionNameToSpecMap(optionSpecs); for (Map.Entry<String, BoundValue> option : options.entrySet()) { BoundValue boundValue = option.getValue(); if (boundValue.isDefault()) { continue; } DisplayDataValue resolved = DisplayDataValue.resolve(boundValue.getValue()); HashSet<PipelineOptionSpec> specs = new HashSet<>(optionsMap.get(option.getKey())); for (PipelineOptionSpec optionSpec : specs) { if (!optionSpec.shouldSerialize()) { // Options that are excluded for serialization (i.e. those with @JsonIgnore) are also // excluded from display data. These options are generally not useful for display. continue; } builder.add(DisplayData.item(option.getKey(), resolved.getType(), resolved.getValue()) .withNamespace(optionSpec.getDefiningInterface())); } } for (Map.Entry<String, JsonNode> jsonOption : jsonOptions.entrySet()) { if (options.containsKey(jsonOption.getKey())) { // Option overwritten since deserialization; don't re-write continue; } HashSet<PipelineOptionSpec> specs = new HashSet<>(optionsMap.get(jsonOption.getKey())); if (specs.isEmpty()) { // No PipelineOptions interface for this key not currently loaded builder.add(DisplayData.item(jsonOption.getKey(), jsonOption.getValue().toString()) .withNamespace(UnknownPipelineOptions.class)); continue; } for (PipelineOptionSpec spec : specs) { if (!spec.shouldSerialize()) { continue; } Object value = getValueFromJson(jsonOption.getKey(), spec.getGetterMethod()); DisplayDataValue resolved = DisplayDataValue.resolve(value); builder.add(DisplayData.item(jsonOption.getKey(), resolved.getType(), resolved.getValue()) .withNamespace(spec.getDefiningInterface())); } } } } /** * Helper class to resolve a {@link DisplayData} type and value from {@link PipelineOptions}. */ @AutoValue abstract static class DisplayDataValue { /** * The resolved display data value. May differ from the input to {@link #resolve(Object)} */ abstract Object getValue(); /** The resolved display data type. */ abstract DisplayData.Type getType(); /** * Infer the value and {@link DisplayData.Type type} for the given * {@link PipelineOptions} value. */ static DisplayDataValue resolve(@Nullable Object value) { DisplayData.Type type = DisplayData.inferType(value); if (type == null) { value = displayDataString(value); type = DisplayData.Type.STRING; } return new AutoValue_ProxyInvocationHandler_DisplayDataValue(value, type); } /** * Safe {@link Object#toString()} wrapper to extract display data values for various types. */ private static String displayDataString(@Nullable Object value) { if (value == null) { return ""; } if (!value.getClass().isArray()) { return value.toString(); } if (!value.getClass().getComponentType().isPrimitive()) { return Arrays.deepToString((Object[]) value); } // At this point, we have some type of primitive array. Arrays.deepToString(..) requires an // Object array, but will unwrap nested primitive arrays. String wrapped = Arrays.deepToString(new Object[]{value}); return wrapped.substring(1, wrapped.length() - 1); } } /** * Marker interface used when the original {@link PipelineOptions} interface is not known at * runtime. This can occur if {@link PipelineOptions} are deserialized from JSON. * * <p>Pipeline authors can ensure {@link PipelineOptions} type information is available at * runtime by registering their {@link PipelineOptions options} interfaces. See the "Registration" * section of {@link PipelineOptions} documentation. */ interface UnknownPipelineOptions extends PipelineOptions {} /** * Construct a mapping from an option name to its {@link PipelineOptions} interface(s) * declarations. An option may be declared in multiple interfaces. If it is overridden in a * type hierarchy, only the overriding interface will be included. */ private Multimap<String, PipelineOptionSpec> buildOptionNameToSpecMap( Set<PipelineOptionSpec> props) { Multimap<String, PipelineOptionSpec> optionsMap = HashMultimap.create(); for (PipelineOptionSpec prop : props) { optionsMap.put(prop.getName(), prop); } // Filter out overridden options for (Map.Entry<String, Collection<PipelineOptionSpec>> entry : optionsMap.asMap().entrySet()) { /* Compare all interfaces for an option pairwise (iface1, iface2) to look for type hierarchies. If one is the base-class of the other, remove it from the output and continue iterating. This is an N^2 operation per-option, but the number of interfaces defining an option should always be small (usually 1). */ List<PipelineOptionSpec> specs = Lists.newArrayList(entry.getValue()); if (specs.size() < 2) { // Only one known implementing interface, no need to check for inheritance continue; } for (int i = 0; i < specs.size() - 1; i++) { Class<?> iface1 = specs.get(i).getDefiningInterface(); for (int j = i + 1; j < specs.size(); j++) { Class<?> iface2 = specs.get(j).getDefiningInterface(); if (iface1.isAssignableFrom(iface2)) { optionsMap.remove(entry.getKey(), specs.get(i)); specs.remove(i); // Removed element at current "i" index. Set iterators to re-evaluate // new "i" element in outer loop. i--; j = specs.size(); } else if (iface2.isAssignableFrom(iface1)) { optionsMap.remove(entry.getKey(), specs.get(j)); specs.remove(j); // Removed element at current "j" index. Set iterator to re-evaluate // new "j" element in inner-loop. j--; } } } } return optionsMap; } /** * This will output all the currently set values. This is a relatively costly function * as it will call {@code toString()} on each object that has been set and format * the results in a readable format. * * @return A pretty printed string representation of this. */ @Override public synchronized String toString() { SortedMap<String, Object> sortedOptions = new TreeMap<>(); // Add the options that we received from deserialization sortedOptions.putAll(jsonOptions); // Override with any programmatically set options. for (Map.Entry<String, BoundValue> entry : options.entrySet()) { sortedOptions.put(entry.getKey(), entry.getValue().getValue()); } StringBuilder b = new StringBuilder(); b.append("Current Settings:\n"); for (Map.Entry<String, Object> entry : sortedOptions.entrySet()) { b.append(" " + entry.getKey() + ": " + entry.getValue() + "\n"); } return b.toString(); } /** * Uses a Jackson {@link ObjectMapper} to attempt type conversion. * * @param method The method whose return type you would like to return. * @param propertyName The name of the property that is being returned. * @return An object matching the return type of the method passed in. */ private Object getValueFromJson(String propertyName, Method method) { try { JavaType type = PipelineOptionsFactory.MAPPER.getTypeFactory() .constructType(method.getGenericReturnType()); JsonNode jsonNode = jsonOptions.get(propertyName); return PipelineOptionsFactory.MAPPER.readValue(jsonNode.toString(), type); } catch (IOException e) { throw new RuntimeException("Unable to parse representation", e); } } /** * Returns a default value for the method based upon {@code @Default} metadata on the getter * to return values. If there is no {@code @Default} annotation on the getter, then a <a * href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">default</a> as * per the Java Language Specification for the expected return type is returned. * * @param proxy The proxy object for which we are attempting to get the default. * @param method The getter method that was invoked. * @return The default value from an {@link Default} annotation if present, otherwise a default * value as per the Java Language Specification. */ @SuppressWarnings({"unchecked", "rawtypes"}) private Object getDefault(PipelineOptions proxy, Method method) { if (method.getReturnType().equals(RuntimeValueProvider.class)) { throw new RuntimeException(String.format( "Method %s should not have return type " + "RuntimeValueProvider, use ValueProvider instead.", method.getName())); } if (method.getReturnType().equals(StaticValueProvider.class)) { throw new RuntimeException(String.format( "Method %s should not have return type " + "StaticValueProvider, use ValueProvider instead.", method.getName())); } @Nullable Object defaultObject = null; for (Annotation annotation : method.getAnnotations()) { defaultObject = returnDefaultHelper(annotation, proxy, method); if (defaultObject != null) { break; } } if (method.getReturnType().equals(ValueProvider.class)) { String propertyName = gettersToPropertyNames.get(method.getName()); return defaultObject == null ? new RuntimeValueProvider( method.getName(), propertyName, (Class<? extends PipelineOptions>) method.getDeclaringClass(), proxy.getOptionsId()) : new RuntimeValueProvider( method.getName(), propertyName, (Class<? extends PipelineOptions>) method.getDeclaringClass(), defaultObject, proxy.getOptionsId()); } else if (defaultObject != null) { return defaultObject; } /* * We need to make sure that we return something appropriate for the return type. Thus we return * a default value as defined by the JLS. */ return Defaults.defaultValue(method.getReturnType()); } /** * Helper method to return standard Default cases. */ @Nullable private Object returnDefaultHelper( Annotation annotation, PipelineOptions proxy, Method method) { if (annotation instanceof Default.Class) { return ((Default.Class) annotation).value(); } else if (annotation instanceof Default.String) { return ((Default.String) annotation).value(); } else if (annotation instanceof Default.Boolean) { return ((Default.Boolean) annotation).value(); } else if (annotation instanceof Default.Character) { return ((Default.Character) annotation).value(); } else if (annotation instanceof Default.Byte) { return ((Default.Byte) annotation).value(); } else if (annotation instanceof Default.Short) { return ((Default.Short) annotation).value(); } else if (annotation instanceof Default.Integer) { return ((Default.Integer) annotation).value(); } else if (annotation instanceof Default.Long) { return ((Default.Long) annotation).value(); } else if (annotation instanceof Default.Float) { return ((Default.Float) annotation).value(); } else if (annotation instanceof Default.Double) { return ((Default.Double) annotation).value(); } else if (annotation instanceof Default.Enum) { return Enum.valueOf((Class<Enum>) method.getReturnType(), ((Default.Enum) annotation).value()); } else if (annotation instanceof Default.InstanceFactory) { return InstanceBuilder.ofType(((Default.InstanceFactory) annotation).value()) .build() .create(proxy); } return null; } /** * Returns a map from the getters method name to the name of the property based upon the passed in * {@link PropertyDescriptor}s property descriptors. * * @param propertyDescriptors A list of {@link PropertyDescriptor}s to use when generating the * map. * @return A map of getter method name to property name. */ private static Map<String, String> generateGettersToPropertyNames( List<PropertyDescriptor> propertyDescriptors) { ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); for (PropertyDescriptor descriptor : propertyDescriptors) { if (descriptor.getReadMethod() != null) { builder.put(descriptor.getReadMethod().getName(), descriptor.getName()); } } return builder.build(); } /** * Returns a map from the setters method name to its matching getters method name based upon the * passed in {@link PropertyDescriptor}s property descriptors. * * @param propertyDescriptors A list of {@link PropertyDescriptor}s to use when generating the * map. * @return A map of setter method name to getter method name. */ private static Map<String, String> generateSettersToPropertyNames( List<PropertyDescriptor> propertyDescriptors) { ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); for (PropertyDescriptor descriptor : propertyDescriptors) { if (descriptor.getWriteMethod() != null) { builder.put(descriptor.getWriteMethod().getName(), descriptor.getName()); } } return builder.build(); } static class Serializer extends JsonSerializer<PipelineOptions> { @Override public void serialize(PipelineOptions value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { ProxyInvocationHandler handler = (ProxyInvocationHandler) Proxy.getInvocationHandler(value); synchronized (handler) { // We first filter out any properties that have been modified since // the last serialization of this PipelineOptions and then verify that // they are all serializable. Map<String, BoundValue> filteredOptions = Maps.newHashMap(handler.options); removeIgnoredOptions(handler.knownInterfaces, filteredOptions); ensureSerializable(handler.knownInterfaces, filteredOptions); // Now we create the map of serializable options by taking the original // set of serialized options (if any) and updating them with any properties // instances that have been modified since the previous serialization. Map<String, Object> serializableOptions = Maps.<String, Object>newHashMap(handler.jsonOptions); for (Map.Entry<String, BoundValue> entry : filteredOptions.entrySet()) { serializableOptions.put(entry.getKey(), entry.getValue().getValue()); } jgen.writeStartObject(); jgen.writeFieldName("options"); jgen.writeObject(serializableOptions); List<Map<String, Object>> serializedDisplayData = Lists.newArrayList(); DisplayData displayData = DisplayData.from(value); for (DisplayData.Item item : displayData.items()) { @SuppressWarnings("unchecked") Map<String, Object> serializedItem = PipelineOptionsFactory.MAPPER.convertValue(item, Map.class); serializedDisplayData.add(serializedItem); } jgen.writeFieldName("display_data"); jgen.writeObject(serializedDisplayData); jgen.writeEndObject(); } } /** * We remove all properties within the passed in options where there getter is annotated with * {@link JsonIgnore @JsonIgnore} from the passed in options using the passed in interfaces. */ private void removeIgnoredOptions( Set<Class<? extends PipelineOptions>> interfaces, Map<String, ?> options) { // Find all the method names that are annotated with JSON ignore. Set<String> jsonIgnoreMethodNames = FluentIterable.from( ReflectHelpers.getClosureOfMethodsOnInterfaces(interfaces)) .filter(AnnotationPredicates.JSON_IGNORE.forMethod) .transform(new Function<Method, String>() { @Override public String apply(Method input) { return input.getName(); } }).toSet(); // Remove all options that have the same method name as the descriptor. for (PropertyDescriptor descriptor : PipelineOptionsFactory.getPropertyDescriptors(interfaces)) { if (jsonIgnoreMethodNames.contains(descriptor.getReadMethod().getName())) { options.remove(descriptor.getName()); } } } /** * We use an {@link ObjectMapper} to verify that the passed in options are serializable * and deserializable. */ private void ensureSerializable(Set<Class<? extends PipelineOptions>> interfaces, Map<String, BoundValue> options) throws IOException { // Construct a map from property name to the return type of the getter. Map<String, Type> propertyToReturnType = Maps.newHashMap(); for (PropertyDescriptor descriptor : PipelineOptionsFactory.getPropertyDescriptors(interfaces)) { if (descriptor.getReadMethod() != null) { propertyToReturnType.put(descriptor.getName(), descriptor.getReadMethod().getGenericReturnType()); } } // Attempt to serialize and deserialize each property. for (Map.Entry<String, BoundValue> entry : options.entrySet()) { try { String serializedValue = PipelineOptionsFactory.MAPPER.writeValueAsString(entry.getValue().getValue()); JavaType type = PipelineOptionsFactory.MAPPER.getTypeFactory() .constructType(propertyToReturnType.get(entry.getKey())); PipelineOptionsFactory.MAPPER.readValue(serializedValue, type); } catch (Exception e) { throw new IOException(String.format( "Failed to serialize and deserialize property '%s' with value '%s'", entry.getKey(), entry.getValue().getValue()), e); } } } } static class Deserializer extends JsonDeserializer<PipelineOptions> { @Override public PipelineOptions deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { ObjectNode objectNode = (ObjectNode) jp.readValueAsTree(); ObjectNode optionsNode = (ObjectNode) objectNode.get("options"); Map<String, JsonNode> fields = Maps.newHashMap(); for (Iterator<Map.Entry<String, JsonNode>> iterator = optionsNode.fields(); iterator.hasNext(); ) { Map.Entry<String, JsonNode> field = iterator.next(); fields.put(field.getKey(), field.getValue()); } PipelineOptions options = new ProxyInvocationHandler(Maps.<String, BoundValue>newHashMap(), fields) .as(PipelineOptions.class); ValueProvider.RuntimeValueProvider.setRuntimeOptions(options); return options; } } }