net.slashies.phpBridge.Dispatch.java Source code

Java tutorial

Introduction

Here is the source code for net.slashies.phpBridge.Dispatch.java

Source

package net.slashies.phpBridge;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import com.google.common.base.Charsets;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Primitives;

/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

@SuppressWarnings({ "rawtypes", "unchecked" })
class Dispatch {

    private final LoadingCache<CallSignature, BiFunction<Object, List<Object>, Object>> methodCache = CacheBuilder
            .newBuilder().expireAfterAccess(60, TimeUnit.MINUTES).build(CacheLoader.from(this::resolveMethod));

    public Dispatch() {
    }

    public Object invoke(Object o, String methodName, List<Object> valueStack) {
        try {
            return methodCache.getUnchecked(makeSig(o, methodName, valueStack)).apply(o, valueStack);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isCorrectArity(int arity, Executable e) {
        if (e.isVarArgs())
            return e.getParameterTypes().length <= arity; // Still need a minimum number of args for varargs
        return e.getParameterTypes().length == arity; // If not varargs then the arity needs to be exactly right
    }

    private BiFunction<Object, List<Object>, Object> wrapMethodHandle(MethodHandle handle, Executable e) {
        if (Modifier.isStatic(e.getModifiers()) || e instanceof Constructor) {
            return (o, args) -> {
                try {
                    return handle.invokeWithArguments(args);
                } catch (Throwable e1) {
                    throw new RuntimeException(e1);
                }
            };
        }
        return (o, args) -> {
            try {
                final Object[] argArr = new Object[args.size() + 1];
                argArr[0] = o;
                int idx = 1;
                for (Object arg : args)
                    argArr[idx++] = arg;
                return handle.invokeWithArguments(argArr);
            } catch (Throwable e1) {
                throw new RuntimeException(e1);
            }
        };
    }

    private BiFunction<Object, List<Object>, Object> wrapConstructor(Executable e) {
        try {
            return wrapMethodHandle(MethodHandles.lookup().unreflectConstructor(((Constructor) e)), e);
        } catch (IllegalAccessException e1) {
            throw new RuntimeException(e1);
        }
    }

    private BiFunction<Object, List<Object>, Object> wrapMethod(Executable e) {
        try {
            return wrapMethodHandle(MethodHandles.lookup().unreflect((Method) e), e);
        } catch (IllegalAccessException e1) {
            throw new RuntimeException(e1);
        }
    }

    private boolean testCastMatch(final CallSignature sig, Executable e) {
        final Class callTypes[] = sig.argTypes;
        final Class argTypes[] = e.getParameterTypes();

        return IntStream.range(0, callTypes.length).allMatch(i -> argTypes[i].isAssignableFrom(callTypes[i]));
    }

    private static int compareClasses(Class a, Class b) {
        if (a.isArray() != b.isArray())
            return 0; // Incomparable
        if (a.isArray())
            return compareClasses(a.getComponentType(), b.getComponentType());
        return ComparisonChain.start().compareFalseFirst(a.isInterface(), b.isInterface())
                // Choose the more specific option, if possible
                .compareTrueFirst(b.isAssignableFrom(a), a.isAssignableFrom(b)).result();
    }

    private static boolean classesAreComparable(Class a, Class b) {
        if (a.equals(b))
            return true;
        if (a.isArray() != b.isArray())
            return false;
        if (a.isArray())
            return classesAreComparable(a.getComponentType(), b.getComponentType());
        return a.isAssignableFrom(b) || b.isAssignableFrom(a);
    }

    private static boolean argumentsAreComparable(Executable aE, Executable bE) {
        if (aE.isVarArgs() != bE.isVarArgs())
            return false; // We don't care if they are or not, but they need to be the same

        final Class[] aArgs = aE.getParameterTypes(), bArgs = bE.getParameterTypes();
        if (aArgs.length != bArgs.length)
            return false;

        return IntStream.range(0, aArgs.length).allMatch(idx -> {
            final Class a = aArgs[idx], b = bArgs[idx];
            return classesAreComparable(a, b);
        });
    }

    private static <E extends Executable> int argumentPartialOrder(E aE, E bE) {
        final Class[] a = aE.getParameterTypes(), b = bE.getParameterTypes();
        if (a.length != b.length)
            return 0; // Incomparable;
        return IntStream.range(0, a.length).map(idx -> compareClasses(a[idx], b[idx]))
                // If either is equal to zero keep the opposite one
                // "Fall" to zero if the signs are NOT the same
                // Pick arbitrarily if the signs are the same
                .reduce(0, (x, y) -> x == 0 ? y : y == 0 ? x : x > 0 != y > 0 ? 0 : x);
    }

    private static boolean hasTotalOrderOverArgs(List<Executable> execs) {
        return execs.size() < 2 || IntStream.range(1, execs.size())
                .allMatch(idx -> argumentsAreComparable(execs.get(idx - 1), execs.get(idx)));
    }

    private static Stream<Method> findPublicDeclarations(Method original, Class target) {
        checkArgument(Modifier.isPublic(original.getModifiers()));
        if (Modifier.isPublic(target.getModifiers()))
            return Stream.of(original);
        return ClassHierarchyCollection.hiararchyStream(target).distinct()
                .filter(c -> Modifier.isPublic(c.getModifiers())).map(Class::getMethods).flatMap(Arrays::stream)
                .filter(m -> original.getName().equals(m.getName()))
                .filter(m -> classesAreComparable(original.getReturnType(), m.getReturnType()))
                .filter(m -> argumentsAreComparable(original, m));
    }

    private BiFunction<Object, List<Object>, Object> resolveMethod(final CallSignature sig) {
        final List<Executable> methods;
        final boolean isConstructor = sig.methodName.equals("new");

        if (isConstructor) {
            methods = Arrays.stream(sig.targetType.getConstructors()).collect(Collectors.toList());
        } else {
            methods = Arrays.stream(sig.targetType.getMethods()).distinct()
                    .flatMap(m -> findPublicDeclarations(m, sig.targetType)).collect(Collectors.toList());
        }

        final Set<Executable> potentials = methods.stream()
                .filter(m -> (isConstructor ? sig.targetType.getName() : sig.methodName).equals(m.getName()))
                .filter(e -> Modifier.isPublic(e.getModifiers()))
                .filter(e -> isCorrectArity(sig.argTypes.length, e)).collect(Collectors.toSet());

        final List<Executable> castMatches = potentials.stream().filter(e -> testCastMatch(sig, e))
                .collect(Collectors.toList());

        if (castMatches.size() >= 1 && hasTotalOrderOverArgs(castMatches)) {
            final Executable min;
            if (isConstructor) {
                min = castMatches.stream()
                        .min(Comparator.comparing(Function.identity(), Dispatch::argumentPartialOrder)).get();
            } else {
                min = ((Collection<Method>) (Collection) castMatches).stream()
                        .min(Comparator.comparing(Function.<Method>identity(), Dispatch::argumentPartialOrder)
                                .thenComparing(Method::getReturnType, Dispatch::compareClasses))
                        .get();
            }

            return isConstructor ? wrapConstructor(min) : wrapMethod(min);
        }

        if (castMatches.size() > 1)
            throw new AmbigiousDispatchException(sig);

        // If there is no exact match then we have to attempt to coerce the types to match
        // If this doesn't produce exactly one target then we give up
        // Better to fail then produce an unpredictable result

        final List<Map.Entry<Executable, Function<List<Object>, List<Object>>>> coercions = potentials
                .parallelStream().map(e -> Maps.immutableEntry(e, Optional.ofNullable(tryCoerceArgs(sig, e))))
                .filter(e -> e.getValue().isPresent()).map(e -> Maps.immutableEntry(e.getKey(), e.getValue().get()))
                .collect(Collectors.toList());

        if (coercions.isEmpty())
            throw new UnknownDispatchException(sig);
        else if (coercions.size() > 1)
            throw new AmbigiousDispatchException(sig);

        final Map.Entry<Executable, Function<List<Object>, List<Object>>> target = coercions.get(0);
        final Function<List<Object>, List<Object>> coercion = target.getValue();
        final BiFunction<Object, List<Object>, Object> mh;
        if (isConstructor)
            mh = wrapConstructor(target.getKey());
        else
            mh = wrapMethod(target.getKey());

        return (o, args) -> mh.apply(o, coercion.apply(args));
    }

    private Function<Object, Object> coerceInterface(Class callType, Class argType) {
        if (Map.class.isAssignableFrom(callType)) {
            if (Iterable.class.equals(argType) || Collection.class.equals(argType) || List.class.equals(argType))
                return m -> ((Map) m).values().stream().collect(Collectors.toList());
            if (Set.class.equals(argType))
                return m -> ((Map) m).values().stream().collect(Collectors.toCollection(Sets::newLinkedHashSet));
            if (SortedSet.class.equals(argType) || NavigableSet.class.equals(argType))
                return m -> ((Map) m).values().stream().collect(Collectors.toCollection(TreeSet::new));
            if (SortedMap.class.equals(argType) || NavigableMap.class.equals(argType))
                return m -> new TreeMap((Map) m);
        }
        return null;
    }

    private Function<Object, Object> coerceType(Class callType, Class argType) {
        if (Map.class.isAssignableFrom(callType)) {
            if (Collection.class.isAssignableFrom(argType)) // Might be a concrete collection type!
                return m -> {
                    try {
                        Collection c = (Collection) argType.newInstance();
                        c.addAll(((Map) m).values());
                        return c;
                    } catch (InstantiationException | IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                };

            if (Map.class.isAssignableFrom(callType)) // Might be a concrete map type!
                return m -> {
                    try {
                        Map c = (Map) argType.newInstance();
                        c.putAll((Map) m);
                        return c;
                    } catch (InstantiationException | IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                };
        }

        if (BigDecimal.class.isAssignableFrom(argType))
            return TypeConversions::asBigDecimal;

        // Desperate
        if (Number.class.isAssignableFrom(argType))
            return ((Function) TypeConversions::asBigDecimal).andThen(PrimitiveUtils.narrower(argType));

        return null;
    }

    private Function<Object, Object> coerce(Class callType, Class argType) {

        final boolean argIsPrimOrBoxed = Primitives.unwrap(argType).isPrimitive();
        argType = Primitives.wrap(argType);
        assert !callType.isPrimitive();
        final boolean callIsBoxed = Primitives.unwrap(callType).isPrimitive();

        if (argType.isAssignableFrom(callType))
            return Function.identity(); // No coercion needed for this arg

        if (Boolean.class == argType) {
            assert Boolean.class != callType : "Should have been caught by identity";

            if (void.class == callType)
                return v -> false;
            if (Number.class.isAssignableFrom(callType))
                return n -> ((Number) n).intValue() != 0;

            return null; // Only accept numbers, nulls, and real booleans for boolean args
        }

        if (void.class == callType) { // Null call argument
            if (!argType.isPrimitive())
                return Function.identity();

            Object narrowedNull = PrimitiveUtils.narrower(callType).apply(new Long(0));
            return v -> narrowedNull;
        }

        // Prims (and boxes) merely need a potentially narrowing conversion
        if (argIsPrimOrBoxed && callIsBoxed)
            return PrimitiveUtils.narrower(callType);

        Function<Object, Object> potential;

        if (argType.isInterface()) { // Try to playcate common interface types
            potential = coerceInterface(callType, argType);
            if (null != potential)
                return potential;
        }

        potential = coerceType(callType, argType);
        if (null != potential)
            return potential;

        // At this point we are desperate
        if (CharSequence.class.isAssignableFrom(callType) && argIsPrimOrBoxed) {
            potential = v -> new BigDecimal(v.toString());
            potential = potential.andThen(PrimitiveUtils.narrower(argType));
            return potential;
        }

        return null;
    }

    private Function<List<Object>, List<Object>> tryCoerceVarArgs(final CallSignature sig, final Executable e) {
        final Class callTypes[] = sig.argTypes;
        final Class argTypes[] = e.getParameterTypes();
        final Class varargType = checkNotNull(callTypes[callTypes.length - 1]).getComponentType();

        // Remember varargs includes an empty array
        assert callTypes.length >= argTypes.length - 1;

        final List<Function<Object, Object>> argCoercions = ImmutableList
                .copyOf(IntStream.range(0, callTypes.length).mapToObj(i -> coerce(callTypes[i], argTypes[i]))
                        .collect(Collectors.toList()));

        final BiConsumer<Iterator<Object>, List<Object>> varargHandler;
        if (callTypes.length < argTypes.length) {
            varargHandler = (itr, l) -> {
                checkState(!itr.hasNext());
                l.add(Array.newInstance(varargType, 0));
            };
        } else if (callTypes.length == argTypes.length && callTypes[callTypes.length].isArray()
                && callTypes[callTypes.length].getComponentType() == varargType) {
            varargHandler = (itr, l) -> l.add(itr.next());
        } else {
            varargHandler = (itr, l) -> l.add(Iterators.toArray(itr, varargType));
        }

        return args -> {
            List<Object> l = Lists.newArrayListWithExpectedSize(callTypes.length);
            Iterator<Object> argItr = args.iterator();
            Iterator<Function<Object, Object>> cItr = argCoercions.iterator();
            while (cItr.hasNext())
                l.add(cItr.next().apply(argItr.next()));

            varargHandler.accept(argItr, l);

            return l;
        };
    }

    private Function<List<Object>, List<Object>> tryCoerceArgs(final CallSignature sig, final Executable e) {
        if (e.isVarArgs())
            return tryCoerceVarArgs(sig, e);

        final Class callTypes[] = sig.argTypes;
        final Class argTypes[] = e.getParameterTypes();
        assert callTypes.length == argTypes.length;

        List<Function<Object, Object>> argCoercions = IntStream.range(0, callTypes.length)
                .mapToObj(i -> coerce(callTypes[i], argTypes[i])).collect(Collectors.toList());

        if (argCoercions.contains(null))
            return null;

        final List<Function<Object, Object>> immutableArgCoercions = ImmutableList.copyOf(argCoercions);

        return args -> {
            Object[] argCopy = args.toArray();
            IntStream.range(0, args.size())
                    .forEach(idx -> argCopy[idx] = immutableArgCoercions.get(idx).apply(argCopy[idx]));
            return Arrays.asList(argCopy);
        };
    }

    private static Class getType(Object o) {
        return null == o ? void.class : o.getClass();
    }

    private CallSignature makeSig(Object o, String methodName, List<Object> valueStack) {
        return new CallSignature(o, methodName, valueStack.stream().map(Dispatch::getType));
    }

    private static final HashFunction sigHashF = Hashing.goodFastHash(32);

    private class CallSignature {
        private final String methodName;
        // private final boolean isStatic;
        private final Class targetType;
        private final Class[] argTypes;

        private final int hash;

        CallSignature(Object target, String name, Stream<Class> argumentTypes) {
            checkNotNull(target);
            if (target instanceof ClassWrapper)
                this.targetType = ((ClassWrapper) target).getClazz();
            else
                this.targetType = target.getClass();

            methodName = checkNotNull(name.trim());
            checkArgument(!methodName.isEmpty());
            this.argTypes = argumentTypes.toArray(Class[]::new);

            hash = sigHashF.newHasher().putString(methodName, Charsets.UTF_8).putInt(Arrays.hashCode(argTypes))
                    .putInt(targetType.hashCode()).hash().asInt();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof CallSignature) {
                CallSignature s = (CallSignature) obj;
                return s == this || (hash == s.hash && targetType.equals(s.targetType)
                        && methodName.equals(s.methodName) && Arrays.equals(argTypes, s.argTypes));
            }
            return false;
        }

        @Override
        public int hashCode() {
            return hash;
        }

        @Override
        public String toString() {
            return String.format("%s::%s(%s)", targetType.getName(), methodName,
                    Arrays.stream(argTypes).map(Class::getName).collect(Collectors.joining(", ")));
        }
    }

    private static class AmbigiousDispatchException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public AmbigiousDispatchException(CallSignature sig) {
            super("Unable to unambigiously determine target for the caller signature " + sig.toString());
        }
    }

    private static class UnknownDispatchException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public UnknownDispatchException(CallSignature sig) {
            super("Unable to find any potential call site for " + sig.toString());
        }
    }

}