NoopInvocationHandler.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 java.util.Collections.unmodifiableMap;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import io.earcam.instrumental.proxy.Proxies;

/**
 * Use the static instance {@link io.earcam.instrumental.proxy.handler.NoopInvocationHandler#NOOP_INVOCATION_HANDLER}.
 * Create instances only where you
 * want/need
 * the {@link java.lang.Object#toString()} to be distinct.
 *
 * If near-NOOP functionality required, then compose
 * {@link io.earcam.instrumental.proxy.handler.NoopInvocationHandler#NOOP_INVOCATION_HANDLER} as
 * the {@code delegate} for {@link io.earcam.instrumental.proxy.handler.PartialInvocationHandler}.
 *
 * Not quite NOOP:
 * 1. honours equals by reference of proxy
 * 2. honours hashCode as system identity of proxy
 * 3. toString outputs system identity of proxy and this instance of
 * {@link io.earcam.instrumental.proxy.handler.NoopInvocationHandler}
 * 4. returns default values for primitives and <code>null</code> for {@link java.lang.Object}s
 * 5. Does NOT honour default methods on interfaces - this is achievable via composition with the
 * {@link io.earcam.instrumental.proxy.handler.PartialInvocationHandler}
 *
 */
public class NoopInvocationHandler implements InvocationHandler, Serializable {

	private static final long serialVersionUID = 3305923532694020975L;

	/** Constant <code>NOOP_INVOCATION_HANDLER</code> */
	public static final NoopInvocationHandler NOOP_INVOCATION_HANDLER = new NoopInvocationHandler();

	private static final Map<Class<?>, Object> PRIMITIVE_TO_DEFAULT_VALUE;
	static {
		Map<Class<?>, Object> tmp = new HashMap<>();
		tmp.put(int.class, 0);
		tmp.put(char.class, '\0');
		tmp.put(short.class, (short) 0);
		tmp.put(long.class, 0L);
		tmp.put(double.class, 0D);
		tmp.put(byte.class, (byte) 0);
		tmp.put(boolean.class, false);

		PRIMITIVE_TO_DEFAULT_VALUE = unmodifiableMap(tmp);
	}


	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
	{
		if(isEquals(method)) {
			return proxy == args[0];
		}
		if(isHashCode(method)) {
			return System.identityHashCode(proxy);
		}
		if(isToString(method)) {
			return proxiedToString(proxy);
		}
		return safeReturn(method.getReturnType());
	}


	private boolean isEquals(Method method)
	{
		return matches(method, "equals", boolean.class, Object.class);
	}


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


	private boolean isHashCode(Method method)
	{
		return matches(method, "hashCode", int.class);
	}


	private boolean isToString(Method method)
	{
		return matches(method, "toString", String.class);
	}


	private String proxiedToString(Object proxy)
	{
		return new StringBuilder()
				.append(Proxy.class.getName())
				.append('(')
				.append(toString())
				.append(")@")
				.append(Proxies.identityToStringOf(proxy))
				.toString();
	}


	private Object safeReturn(Class<?> returnType)
	{
		return PRIMITIVE_TO_DEFAULT_VALUE.getOrDefault(returnType, null);
	}
}