MethodReferences.java

package org.codefilarete.reflection;

import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Function;

import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.Strings;
import org.codefilarete.tool.exception.Exceptions;
import org.codefilarete.tool.function.SerializableTriConsumer;
import org.codefilarete.tool.function.SerializableTriFunction;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableBiFunction;
import org.danekja.java.util.function.serializable.SerializableConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;
import org.danekja.java.util.function.serializable.SerializableSupplier;

/**
 * Helper methods for method reference
 * 
 * @author Guillaume Mary
 */
public class MethodReferences {
	
	private static final MethodReferenceCapturer SINGLETON  = new MethodReferenceCapturer();
	
	public static <A, B> String toMethodReferenceString(SerializableFunction<A, B>  methodReference) {
		Method method = SINGLETON.findMethod(methodReference);
		return toMethodReferenceString(method);
	}
	
	public static <A, B, C> String toMethodReferenceString(SerializableBiFunction<A, B, C> methodReference) {
		Method method = SINGLETON.findMethod(methodReference);
		return toMethodReferenceString(method);
	}
	
	public static <A, B, C, D> String toMethodReferenceString(SerializableTriFunction<A, B, C, D> methodReference) {
		Method method = SINGLETON.findMethod(methodReference);
		return toMethodReferenceString(method);
	}
	
	public static <A> String toMethodReferenceString(SerializableConsumer<A> methodReference) {
		Method method = SINGLETON.findMethod(methodReference);
		return toMethodReferenceString(method);
	}
	
	public static <A, B> String toMethodReferenceString(SerializableBiConsumer<A, B> methodReference) {
		Method method = SINGLETON.findMethod(methodReference);
		return toMethodReferenceString(method);
	}
	
	public static <A, B, C> String toMethodReferenceString(SerializableTriConsumer<A, B, C> methodReference) {
		Method method = SINGLETON.findMethod(methodReference);
		return toMethodReferenceString(method);
	}
	
	public static String toMethodReferenceString(Method method) {
		return method.getDeclaringClass().getSimpleName() + "::" + method.getName();
	}
	
	/**
	 * Gives a signature of a serializable method reference ({@link Function}, {@link java.util.function.Consumer}, etc)
	 * THIS METHOD WILL ONLY WORK WITH A METHOD REFERENCE, NOT WITH AN ANONYMOUS LAMBDA FUNCTION.
	 * This can't be enforced by signature, hence this warning.
	 * Argument must be {@link Serializable} due to the algorithm used to compute hashCode.
	 * 
	 * @param methodReference the method reference to hash
	 * @return a hashcode for the method reference
	 */
	public static int hashCodeMethodReference(Serializable methodReference) {
		SerializedLambda serializedLambda = buildSerializedLambda(methodReference);
		// Inspired by SerializedLambda#toString()
		String lambdaSignature = getTargetMethodRawSignature(serializedLambda);
		return lambdaSignature.hashCode();
	}
	
	/**
	 * Gives a raw version of the method targeted by the given {@link SerializedLambda}
	 * THIS METHOD WILL ONLY WORK WITH A METHOD REFERENCE, NOT WITH AN ANONYMOUS LAMBDA FUNCTION.
	 * @param serializedLambda a method reference
	 * @return a concatenation of method class, method name, method arguments, method return type
	 */
	public static String getTargetMethodRawSignature(SerializedLambda serializedLambda) {
		return serializedLambda.getImplClass()
				.concat(serializedLambda.getImplMethodName())
				.concat(serializedLambda.getImplMethodSignature());	// contains method arguments and return type
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a no-arg constructor 
	 * 
	 * @param methodReference a reference to a no-arg constructor
	 * @param <O> returned value type of the supplier
	 * @return a {@link SerializedLambda}
	 */
	public static <O> SerializedLambda buildSerializedLambda(SerializableSupplier<O> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a getter (method returning value without argument) or a one-arg constructor 
	 * 
	 * @param methodReference a getter
	 * @param <T> the target instance type of the getter
	 * @param <O> returned value type of the getter
	 * @return a {@link SerializedLambda}
	 */
	public static <T, O> SerializedLambda buildSerializedLambda(SerializableFunction<T, O> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a 2-args constructor (or getter with 2 arguments)
	 *
	 * @param methodReference a 2-args constructor
	 * @param <O> constructor type, or returned value type of the getter
	 * @param <I1> first argument type
	 * @param <I2> second argument type
	 * @return a {@link SerializedLambda}
	 */
	public static <I1, I2, O> SerializedLambda buildSerializedLambda(SerializableBiFunction<I1, I2, O> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a 3-args constructor (or getter with one argument)
	 *
	 * @param methodReference a 3-args constructor
	 * @param <O> constructor type, or returned value type of the getter
	 * @param <I1> first argument type
	 * @param <I2> second argument type
	 * @param <I3> second argument type
	 * @return a {@link SerializedLambda}
	 */
	public static <I1, I2, I3, O> SerializedLambda buildSerializedLambda(SerializableTriFunction<I1, I2, I3, O> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a setter (method without return value but with one argument)
	 * 
	 * @param methodReference a setter
	 * @param <T> target instance type of the setter
	 * @return a {@link SerializedLambda}
	 */
	public static <T> SerializedLambda buildSerializedLambda(SerializableConsumer<T> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a 2-args setter (method without return value but with 2 arguments)
	 * 
	 * @param methodReference a 2-args setter
	 * @param <T> target instance type of the setter
	 * @param <U> argument type
	 * @return a {@link SerializedLambda}
	 */
	public static <T, U> SerializedLambda buildSerializedLambda(SerializableBiConsumer<T, U> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the {@link SerializedLambda} of a 3-args setter (method without return value but with 3 arguments)
	 * 
	 * @param methodReference a setter
	 * @param <T> target instance type of the setter
	 * @param <U> first argument type
	 * @param <V> second argument type
	 * @return a {@link SerializedLambda}
	 */
	public static <T, U, V> SerializedLambda buildSerializedLambda(SerializableTriConsumer<T, U, V> methodReference) {
		return buildSerializedLambda((Serializable) methodReference);
	}
	
	/**
	 * Gives the SerializedLambda of a serializable method reference ({@link Function}, {@link java.util.function.Consumer}, etc)
	 * THIS METHOD WILL ONLY WORK WITH A METHOD REFERENCE, NOT WITH AN ANONYMOUS LAMBDA FUNCTION.
	 * This can't be enforced by signature, hence this warning.
	 * 
	 * Left public for cases not taking into account by other buildSerializedLambda(..) methods.
	 * 
	 * @param methodReference the method reference to hash
	 * @return a SerializedLambda, not null
	 */
	public static SerializedLambda buildSerializedLambda(Serializable methodReference) {
		// algorithm made possible thanks to https://stackoverflow.com/a/25625761
		// (https://stackoverflow.com/questions/21887358/reflection-type-inference-on-java-8-lambdas)
		Method writeReplace = Reflections.getMethod(methodReference.getClass(), "writeReplace");
		writeReplace.setAccessible(true);
		Object serializedForm;
		try {
			serializedForm = writeReplace.invoke(methodReference);
		} catch (IllegalAccessException | InvocationTargetException e) {
			// Considered as will never happen
			throw Exceptions.asRuntimeException(e);
		}
		return (SerializedLambda) serializedForm;
	}
	
	/**
	 * Gives the class that implements the method referenced by the given lambda.
	 * Example : if String::length is given, then CharSequence.class is returned because CharSequence defines length()
	 *
	 * @param serializedLambda a lambda representing a method reference, not any anonymous lambda
	 * @return the class that implements method referenced by the given lambda
	 * @see #giveInstantiatedClass(SerializedLambda)
	 */
	public static Class giveImplementingClass(SerializedLambda serializedLambda) {
		String implementationClass = serializedLambda.getImplClass().replace('/', '.');
		return Reflections.forName(implementationClass);
	}
	
	/**
	 * Gives the class that is directly referenced by the given method reference lambda.
	 * Example : if String::length is given, then String.class is returned, not CharSequence.class whereas
	 * CharSequence defines length()
	 *
	 * @param serializedLambda a lambda representing a method reference, not any anonymous lambda
	 * @return the class that is directly referenced by the given method reference lambda.
	 * @see #giveImplementingClass(SerializedLambda)
	 */
	public static Class giveInstantiatedClass(SerializedLambda serializedLambda) {
		// We don't use getImplClass() because it less accurate when lambda is a method reference pointing to a method
		// subclass while method is defined in an upper class : getImplClass points to class defining method (the parent one)
		// while getInstantiatedMethodType gives user-pointed method
		String instantiatedMethodType = serializedLambda.getInstantiatedMethodType();
		// it has the following structure:
		// parenthesis implementing_class arguments_types parenthesis return_type
		int closingParenthesisIndex = instantiatedMethodType.indexOf(')');
		String implementingClassAndArgumentsTypes = instantiatedMethodType.substring(1, closingParenthesisIndex);
		
		// NB : we need ";" to be kept because it's in String representing type : see Class.getName()
		List<String> types = Strings.split(implementingClassAndArgumentsTypes, ';', true);
		return Reflections.forName(types.get(0).replace("/", "."));
	}
}