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

org.embulk.config.TaskInvocationHandler Maven / Gradle / Ivy

package org.embulk.config;

import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

class TaskInvocationHandler implements InvocationHandler {
    @Deprecated  // https://github.com/embulk/embulk/issues/1304
    private final ModelManager model;

    private final Class iface;
    private final Map objects;
    private final Set injectedFields;

    @SuppressWarnings("deprecation")  // https://github.com/embulk/embulk/issues/1304
    public TaskInvocationHandler(ModelManager model, Class iface, Map objects, Set injectedFields) {
        this.model = model;
        this.iface = iface;
        this.objects = objects;
        this.injectedFields = injectedFields;
    }

    /**
     * Returns a Multimap from fieldName Strings to their getter Methods.
     *
     * It expects to be called only from TaskSerDe. Multimap is used inside org.embulk.config.
     */
    static Multimap fieldGetters(Class iface) {
        ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
        for (Method method : iface.getMethods()) {
            String methodName = method.getName();
            String fieldName = getterFieldNameOrNull(methodName);
            if (fieldName != null && hasExpectedArgumentLength(method, 0)
                    && (!method.isDefault() || method.getAnnotation(Config.class) != null)) {
                // If the method has default implementation, and @Config is not annotated there, the method is kept.
                builder.put(fieldName, method);
            }
        }
        return builder.build();
    }

    // visible for ModelManager.AccessorSerializer
    Map getObjects() {
        return objects;
    }

    // visible for ModelManager.AccessorSerializer
    Set getInjectedFields() {
        return injectedFields;
    }

    protected Object invokeGetter(Method method, String fieldName) {
        return objects.get(fieldName);
    }

    protected void invokeSetter(Method method, String fieldName, Object value) {
        if (value == null) {
            objects.remove(fieldName);
        } else {
            objects.put(fieldName, value);
        }
    }

    private Map getSerializableFields() {
        Map data = new HashMap(objects);
        for (String injected : injectedFields) {
            data.remove(injected);
        }
        return data;
    }

    protected TaskSource invokeDump() {
        return new DataSourceImpl(model, model.writeObjectAsObjectNode(getSerializableFields()));
    }

    protected String invokeToString() {
        StringBuilder sb = new StringBuilder();
        sb.append(iface.getName());
        sb.append(getSerializableFields());
        return sb.toString();
    }

    protected int invokeHashCode() {
        return objects.hashCode();
    }

    protected boolean invokeEquals(Object other) {
        return (other instanceof TaskInvocationHandler)
                && objects.equals(((TaskInvocationHandler) other).objects);
    }

    public Object invoke(Object proxy, Method method, Object[] args) {
        String methodName = method.getName();

        switch (methodName) {
            case "validate":
                checkArgumentLength(method, 0, methodName);
                model.validate(proxy);
                return proxy;

            case "dump":
                checkArgumentLength(method, 0, methodName);
                return invokeDump();

            case "toString":
                checkArgumentLength(method, 0, methodName);
                return invokeToString();

            case "hashCode":
                checkArgumentLength(method, 0, methodName);
                return invokeHashCode();

            case "equals":
                checkArgumentLength(method, 1, methodName);
                if (args[0] instanceof Proxy) {
                    Object otherHandler = Proxy.getInvocationHandler(args[0]);
                    return invokeEquals(otherHandler);
                }
                return false;

            default: {
                String fieldName;
                fieldName = getterFieldNameOrNull(methodName);
                if (fieldName != null) {
                    if (method.isDefault() && !this.objects.containsKey(fieldName)) {
                        // If and only if the method has default implementation, and @Config is not annotated there,
                        // it is tried to call the default implementation directly without proxying.
                        //
                        // methodWithDefaultImpl.invoke(proxy) without this hack would cause infinite recursive calls.
                        //
                        // See hints:
                        // https://rmannibucau.wordpress.com/2014/03/27/java-8-default-interface-methods-and-jdk-dynamic-proxies/
                        // https://stackoverflow.com/questions/22614746/how-do-i-invoke-java-8-default-methods-reflectively
                        //
                        // This hack is required to support `org.joda.time.DateTimeZone` in some Tasks, for example
                        // TimestampParser.Task and TimestampParser.TimestampColumnOption.
                        //
                        // TODO: Remove the hack once a cleaner way is found, or Joda-Time is finally removed.
                        // https://github.com/embulk/embulk/issues/890
                        if (CONSTRUCTOR_MethodHandles_Lookup != null) {
                            synchronized (CONSTRUCTOR_MethodHandles_Lookup) {
                                boolean hasSetAccessible = false;
                                try {
                                    CONSTRUCTOR_MethodHandles_Lookup.setAccessible(true);
                                    hasSetAccessible = true;
                                } catch (SecurityException ex) {
                                    // Skip handling default implementation in case of errors.
                                }

                                if (hasSetAccessible) {
                                    try {
                                        return CONSTRUCTOR_MethodHandles_Lookup
                                                .newInstance(
                                                        method.getDeclaringClass(),
                                                        MethodHandles.Lookup.PUBLIC
                                                                | MethodHandles.Lookup.PRIVATE
                                                                | MethodHandles.Lookup.PROTECTED
                                                                | MethodHandles.Lookup.PACKAGE)
                                                .unreflectSpecial(method, method.getDeclaringClass())
                                                .bindTo(proxy)
                                                .invokeWithArguments();
                                    } catch (Throwable ex) {
                                        // Skip handling default implementation in case of errors.
                                    } finally {
                                        CONSTRUCTOR_MethodHandles_Lookup.setAccessible(false);
                                    }
                                }
                            }
                        }
                    }
                    checkArgumentLength(method, 0, methodName);
                    return invokeGetter(method, fieldName);
                }
                fieldName = setterFieldNameOrNull(methodName);
                if (fieldName != null) {
                    checkArgumentLength(method, 1, methodName);
                    invokeSetter(method, fieldName, args[0]);
                    return this;
                }
            }
        }

        throw new IllegalArgumentException(String.format("Undefined method '%s'", methodName));
    }

    private static String getterFieldNameOrNull(String methodName) {
        if (methodName.startsWith("get")) {
            return methodName.substring(3);
        }
        return null;
    }

    private static String setterFieldNameOrNull(String methodName) {
        if (methodName.startsWith("set")) {
            return methodName.substring(3);
        }
        return null;
    }

    protected static boolean hasExpectedArgumentLength(Method method, int expected) {
        return method.getParameterTypes().length == expected;
    }

    protected static void checkArgumentLength(Method method, int expected, String methodName) {
        if (!hasExpectedArgumentLength(method, expected)) {
            throw new IllegalArgumentException(
                    String.format("Method '%s' expected %d argument but got %d arguments", methodName, expected, method.getParameterTypes().length));
        }
    }

    static {
        Constructor constructorMethodHandlesLookupTemporary = null;
        try {
            constructorMethodHandlesLookupTemporary =
                    MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
        } catch (NoSuchMethodException | SecurityException ex) {
            constructorMethodHandlesLookupTemporary = null;
        } finally {
            CONSTRUCTOR_MethodHandles_Lookup = constructorMethodHandlesLookupTemporary;
        }
    }

    private static final Constructor CONSTRUCTOR_MethodHandles_Lookup;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy