All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.dataflow.sdk.options.ProxyInvocationHandler 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.PipelineOptionsFactory.JsonIgnorePredicate;
import com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory.Registration;
import com.google.cloud.dataflow.sdk.util.InstanceBuilder;
import com.google.cloud.dataflow.sdk.util.common.ReflectHelpers;
import com.google.common.base.Defaults;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.MutableClassToInstanceMap;

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 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.Proxy;
import java.lang.reflect.Type;
import java.util.Arrays;
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 javax.annotation.concurrent.ThreadSafe;

/**
 * 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.
 * 

* 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 default as * per the Java Language Specification for the expected return type is returned. *

* 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 { private static final ObjectMapper MAPPER = new ObjectMapper(); /** * No two instances of this class are considered equivalent hence we generate a random hash code * between 0 and {@link Integer#MAX_VALUE}. */ private final int hashCode = (int) (Math.random() * Integer.MAX_VALUE); private final Set> knownInterfaces; private final ClassToInstanceMap interfaceToProxyCache; private final Map options; private final Map jsonOptions; private final Map gettersToPropertyNames; private final Map settersToPropertyNames; ProxyInvocationHandler(Map options) { this(options, Maps.newHashMap()); } private ProxyInvocationHandler(Map options, Map 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 && "as".equals(method.getName()) && args[0] instanceof Class) { @SuppressWarnings("unchecked") Class clazz = (Class) args[0]; return as(clazz); } else if (args != null && "cloneAs".equals(method.getName()) && args[0] instanceof Class) { @SuppressWarnings("unchecked") Class clazz = (Class) args[0]; return cloneAs(proxy, clazz); } String methodName = method.getName(); synchronized (this) { if (gettersToPropertyNames.keySet().contains(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, value); } return options.get(propertyName); } else if (settersToPropertyNames.containsKey(methodName)) { options.put(settersToPropertyNames.get(methodName), args[0]); return Void.TYPE; } } throw new RuntimeException("Unknown method [" + method + "] invoked with args [" + Arrays.toString(args) + "]."); } /** * 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 . */ synchronized T as(Class iface) { Preconditions.checkNotNull(iface); Preconditions.checkArgument(iface.isInterface()); if (!interfaceToProxyCache.containsKey(iface)) { Registration registration = PipelineOptionsFactory.validateWellFormed(iface, knownInterfaces); List propertyDescriptors = registration.getPropertyDescriptors(); Class 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); } /** * Backing implementation for {@link PipelineOptions#cloneAs(Class)}. * * @return A copy of the PipelineOptions. */ synchronized T cloneAs(Object proxy, Class iface) { PipelineOptions clonedOptions; try { clonedOptions = MAPPER.readValue(MAPPER.writeValueAsBytes(proxy), PipelineOptions.class); } catch (IOException e) { throw new IllegalStateException("Failed to serialize the pipeline options to JSON.", e); } for (Class knownIface : knownInterfaces) { clonedOptions.as(knownIface); } return clonedOptions.as(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; } /** * 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 sortedOptions = new TreeMap<>(); // Add the options that we received from deserialization sortedOptions.putAll(jsonOptions); // Override with any programmatically set options. sortedOptions.putAll(options); StringBuilder b = new StringBuilder(); b.append("Current Settings:\n"); for (Map.Entry 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 = MAPPER.getTypeFactory().constructType(method.getGenericReturnType()); JsonNode jsonNode = jsonOptions.get(propertyName); return 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 default 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) { for (Annotation annotation : method.getAnnotations()) { 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) method.getReturnType(), ((Default.Enum) annotation).value()); } else if (annotation instanceof Default.InstanceFactory) { return InstanceBuilder.ofType(((Default.InstanceFactory) annotation).value()) .build() .create(proxy); } } /* * 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()); } /** * 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 generateGettersToPropertyNames( List propertyDescriptors) { ImmutableMap.Builder 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 generateSettersToPropertyNames( List propertyDescriptors) { ImmutableMap.Builder 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 { @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 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 serializableOptions = Maps.newHashMap(handler.jsonOptions); serializableOptions.putAll(filteredOptions); jgen.writeStartObject(); jgen.writeFieldName("options"); jgen.writeObject(serializableOptions); 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> interfaces, Map options) { // Find all the method names that are annotated with JSON ignore. Set jsonIgnoreMethodNames = FluentIterable.from( ReflectHelpers.getClosureOfMethodsOnInterfaces(interfaces)) .filter(JsonIgnorePredicate.INSTANCE).transform(new Function() { @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> interfaces, Map options) throws IOException { // Construct a map from property name to the return type of the getter. Map 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 entry : options.entrySet()) { try { String serializedValue = MAPPER.writeValueAsString(entry.getValue()); JavaType type = MAPPER.getTypeFactory() .constructType(propertyToReturnType.get(entry.getKey())); 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()), e); } } } } static class Deserializer extends JsonDeserializer { @Override public PipelineOptions deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { ObjectNode objectNode = (ObjectNode) jp.readValueAsTree(); ObjectNode optionsNode = (ObjectNode) objectNode.get("options"); Map fields = Maps.newHashMap(); for (Iterator> iterator = optionsNode.fields(); iterator.hasNext(); ) { Map.Entry field = iterator.next(); fields.put(field.getKey(), field.getValue()); } PipelineOptions options = new ProxyInvocationHandler(Maps.newHashMap(), fields) .as(PipelineOptions.class); return options; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy