PartialInvocationHandler.java

/*-
 * #%L
 * io.earcam.instrumental.proxy
 * %%
 * Copyright (C) 2018 earcam
 * %%
 * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
 * 
 * You <b>must</b> choose to accept, in full - any individual or combination of 
 * the following licenses:
 * <ul>
 * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
 * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
 * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
 * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
 * </ul>
 * #L%
 */
package io.earcam.instrumental.proxy.handler;

import static io.earcam.unexceptional.Exceptional.throwAsUnchecked;
import static io.earcam.unexceptional.Exceptional.unwrap;
import static java.lang.reflect.Modifier.PUBLIC;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Predicate;

import io.earcam.instrumental.proxy.Proxies;
import io.earcam.instrumental.reflect.Methods;

/**
 * Suffers from http://www.javaspecialists.eu/archive/Issue117.html - so don't try extending this as an anonymous
 * class..
 *
 *
 * Methods not defined on the concrete subclass class of
 * {@link io.earcam.instrumental.proxy.handler.PartialInvocationHandler}
 * will be invoked on the delegate.
 * Enforce type safety through common interfaces honouring ISP.
 *
 */
public abstract class PartialInvocationHandler<T> implements InvocationHandler {

	private final Method[] methods;
	protected final T delegate;


	/**
	 * <p>
	 * Constructor for PartialInvocationHandler.
	 * </p>
	 *
	 * @param delegate a T object.
	 */
	public PartialInvocationHandler(T delegate)
	{
		this.delegate = delegate;
		this.methods = allPublicMethods(getClass());
	}


	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
	{
		return Arrays.stream(methods)
				.filter(m -> matches(m, method.getName(), method.getReturnType(), method.getParameterTypes()))
				.findFirst()
				.map(m -> invoke(m, this, args))
				.orElseGet(() -> invoke(method, delegate, args));
	}

	private static class Pfft extends PartialInvocationHandler<Object> {

		public Pfft(Object delegate)
		{
			super(delegate);
		}
	}


	private Object invoke(Method method, Object target, Object[] args)
	{
		try {
			if(target == delegate && method.isDefault() && !this.getClass().isAssignableFrom(Pfft.class)) {
				return wrapDefaultInPfft(method, args);
			}

			if(this.getClass().isAssignableFrom(Pfft.class)) {
				return invokePfft(method, args);
			} else {
				return method.invoke(target, args);
			}
		} catch(InvocationTargetException e) {
			throw throwAsUnchecked(unwrap(e));
		} catch(Throwable e) {
			throw throwAsUnchecked(e);
		}
	}


	/**
	 * <p>
	 * wrapDefaultInPfft.
	 * </p>
	 *
	 * @param method a {@link java.lang.reflect.Method} object.
	 * @param args an array of {@link java.lang.Object} objects.
	 * @return a {@link java.lang.Object} object.
	 * @throws java.lang.Throwable if any.
	 */
	protected Object wrapDefaultInPfft(Method method, Object[] args) throws Throwable
	{
		MethodHandle methodHandle = Methods.handleFor(method);
		Object implementingProxy = Proxies.proxy(new Pfft(this), method.getDeclaringClass());
		return methodHandle.bindTo(implementingProxy).invokeWithArguments(args);
	}


	@SuppressWarnings("unchecked")
	private Object invokePfft(Method method, Object[] args) throws Throwable
	{
		return ((PartialInvocationHandler<Object>) delegate).invoke(void.class, method, args);
	}


	private static Method[] allPublicMethods(Class<?> type)
	{
		Predicate<? super Method> invocationHandlerInvokeMethod = m -> matches(m, "invoke", Object.class, Object.class, Method.class, Object[].class);

		return Methods.methodsOf(type)
				.filter(Methods.hasModifiers(PUBLIC))
				.filter(invocationHandlerInvokeMethod.negate())
				.toArray(s -> new Method[s]);
	}


	private static boolean matches(Method m, String name, Class<?> returnType, Class<?>... argumentTypes)
	{
		return m.getName().equals(name)
				&& m.getReturnType().equals(returnType)
				&& Arrays.equals(m.getParameterTypes(), argumentTypes);
	}
}