AccessorDefinition.java

package org.codefilarete.reflection;

import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Collection;

import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.Reflections.MemberNotFoundException;
import org.codefilarete.tool.StringAppender;
import org.codefilarete.tool.collection.Iterables;

/**
 * A common representation of "class member", in the meaning of property accessor. So it means Fields, Methods and MethodReferences.
 * Main goal is to make a majority of {@link ValueAccessPoint} comparable between each other even if they are not of same type :
 * a {@link MutatorByField} would have the same {@link AccessorDefinition} than an {@link AccessorByMethod} for the same property.
 * 
 * @author Guillaume Mary
 * @see #giveDefinition(ValueAccessPoint)
 */
public class AccessorDefinition {
	
	private static final MethodReferenceCapturer METHOD_REFERENCE_CAPTURER = new MethodReferenceCapturer();
	
	/**
	 * Gives an {@link AccessorDefinition} defining given {@link ValueAccessPoint}.
	 * The mechanism is to create an {@link AccessorDefinition} for known classes ({@link AccessorChain},
	 * {@link PropertyAccessor}, {@link AbstractReflector}) which may requires that given point sticks to Java Bean
	 * Naming Convention (else it will throw an exception).
	 * To come over this mechanism, given point must implement {@link AccessorDefinitionDefiner} because it is taken
	 * priority over the other.
	 *
	 * @param accessPoint any {@link ValueAccessPoint}
	 * @return a common representation of given input
	 * @throws UnsupportedOperationException when member can't be found because given {@link ValueAccessPoint} is not a known concrete type
	 * @throws MemberNotFoundException when member can't be found because it doesn't meet Java Bean Naming Convention
	 */
	public static AccessorDefinition giveDefinition(ValueAccessPoint<?> accessPoint) {
		AccessorDefinition result;
		// we take AccessorDefinitionDefiner in priority over concrete classes, because they can also implement it
		// through a subclass
		if (accessPoint instanceof AccessorDefinitionDefiner) {
			result = ((AccessorDefinitionDefiner) accessPoint).asAccessorDefinition();
		} else if (accessPoint instanceof AccessorChain) {
			result = giveDefinition((AccessorChain) accessPoint);
		} else if (accessPoint instanceof PropertyAccessor) {
			result = giveDefinition(((PropertyAccessor) accessPoint).getAccessor());
		} else if (accessPoint instanceof AbstractReflector) {
			result = giveReflectorDefinition((AbstractReflector) accessPoint);
		} else {
			throw new UnsupportedOperationException("Accessor type is unsupported to compute its definition : " + (accessPoint == null ? "null" : Reflections.toString(accessPoint.getClass())));
		}
		return result;
	}
	
	/**
	 * Gives an {@link AccessorDefinition} defining given {@link Method}.
	 *
	 * @param method any {@link Method}
	 * @return a common representation of given input
	 */
	public static AccessorDefinition giveDefinition(Method method) {
		return new AccessorDefinition(method.getDeclaringClass(), Reflections.propertyName(method), method.getReturnType());
	}
	
	/**
	 * Gives an {@link AccessorDefinition} defining given {@link Field}.
	 *
	 * @param field any {@link Field}
	 * @return a common representation of given input
	 */
	public static AccessorDefinition giveDefinition(Field field) {
		return new AccessorDefinition(field.getDeclaringClass(), field.getName(), field.getType());
	}
	
	/**
	 * Dedicated to accessor / mutator by field, method and method reference
	 * @param o any accessor / mutator by field, method and method reference
	 * @return a {@link AccessorDefinition} describing input
	 */
	private static AccessorDefinition giveReflectorDefinition(AbstractReflector o) {
		String memberName = null;
		Class declarator = null;
		Class memberType = null;
		if (o instanceof ValueAccessPointByField) {
			Field member = ((ValueAccessPointByField) o).getField();
			memberName = member.getName();
			declarator = member.getDeclaringClass();
			memberType = member.getType();
		} else if (o instanceof ValueAccessPointByMethod) {
			Method member = ((ValueAccessPointByMethod<?>) o).getMethod();
			memberName = Reflections.propertyName(member.getName());
			declarator = member.getDeclaringClass();
			if (o instanceof Accessor) {
				memberType = member.getReturnType();
			} else {
				memberType = member.getParameterTypes()[0];
			}
		} else if (o instanceof ValueAccessPointByMethodReference) {
			memberName = Reflections.propertyName(((ValueAccessPointByMethodReference<?>) o).getMethodName());
			declarator = ((ValueAccessPointByMethodReference<?>) o).getDeclaringClass();
			Method method = METHOD_REFERENCE_CAPTURER.findMethod(((ValueAccessPointByMethodReference<?>) o).getSerializedLambda());
			if (o instanceof Accessor) {
				memberType = method.getReturnType();
			} else {
				memberType = method.getParameterTypes()[0];
			}
		}
		
		return new AccessorDefinition(declarator, memberName, memberType);
	}
	
	/**
	 * Dedicated to {@link AccessorChain}: returns an {@link AccessorDefinition} made of the dotted properties of the "path" of the given {@link AccessorChain}.
	 * @param o an {@link AccessorChain}
	 * @return a {@link AccessorDefinition} describing input
	 */
	private static AccessorDefinition giveDefinition(AccessorChain<?, ?> o) {
		StringAppender stringAppender = new StringAppender() {
			@Override
			public StringAppender cat(Object s) {
				if (s instanceof Accessor) {
					return super.cat(giveDefinition((Accessor) s).getName());
				} else {
					return super.cat(s);
				}
			}
		};
		stringAppender.ccat(o.getAccessors(), ".");
		Accessor firstAccessor = Iterables.first(o.getAccessors());
		Accessor lastAccessor = Iterables.last(o.getAccessors());
		return new AccessorDefinition(
				giveDefinition(firstAccessor).getDeclaringClass(),
				stringAppender.toString(),
				giveDefinition(lastAccessor).getMemberType()
		);
	}
	
	/**
	 * @param o any {@link ValueAccessPoint}
	 * @return a short representation of the given {@link ValueAccessPoint} : owner + name (spearator depends on accessor kind)
	 */
	public static String toString(@Nullable ValueAccessPoint<?> o) {
		String result;
		if (o == null) {
			result = "null";
		} else if (o instanceof AccessorByMember) {
			result = toString(((AccessorByMember) o).getGetter());
		} else if (o instanceof AccessorByMethodReference) {
			result = MethodReferences.toMethodReferenceString(((AccessorByMethodReference) o).getMethodReference());
		} else if (o instanceof MutatorByMember) {
			result = toString(((MutatorByMember) o).getSetter());
		} else if (o instanceof MutatorByMethodReference) {
			result = MethodReferences.toMethodReferenceString(((MutatorByMethodReference) o).getMethodReference());
		} else if (o instanceof PropertyAccessor) {
			result = toString(((PropertyAccessor) o).getAccessor());
		} else if (o instanceof AccessorChain) {
			StringAppender chainPrint = new StringAppender();
			((AccessorChain) o).getAccessors().forEach(accessor -> chainPrint.cat(toString((Accessor) accessor)).cat(" > "));
			result = chainPrint.cutTail(3).toString();
		} else {
			throw new UnsupportedOperationException("Don't know how find out member definition for " + Reflections.toString(o.getClass()));
		}
		return result;
	}
	
	private static String toString(Member member) {
		String result;
		if (member instanceof Method) {
			result = Reflections.toString((Method) member);
		} else {
			result = Reflections.toString((Field) member);
		}
		return result;
	}
	
	/**
	 * @param accessPoints several {@link ValueAccessPoint}s
	 * @return the concatenation of each call to {@link #toString(ValueAccessPoint)} for every element of the collection, separated by ">"
	 */
	public static String toString(Collection<ValueAccessPoint<?>> accessPoints) {
		StringAppender chainPrint = new StringAppender();
		accessPoints.forEach(accessor -> chainPrint.cat(toString(accessor)).cat(" > "));
		return chainPrint.cutTail(3).toString();
	}
	
	private final Class declaringClass;
	private final String name;
	private final Class memberType;
	
	/**
	 * Constructor with mandatory attributes
	 * 
	 * @param declaringClass the owning class of the member
	 * @param name name of the member
	 * @param memberType member type (input for setter, return type for getter, type for field)
	 */
	public AccessorDefinition(Class declaringClass, String name, Class memberType) {
		this.declaringClass = declaringClass;
		this.name = name;
		this.memberType = memberType;
	}
	
	public <T> Class<T> getDeclaringClass() {
		return declaringClass;
	}
	
	public String getName() {
		return name;
	}
	
	/**
	 * Implementation based on strict equality of {@link #getDeclaringClass()}, {@link #getName()} and {@link #getMemberType()}
	 * 
	 * @param obj another object
	 * @return true if {@link #getDeclaringClass()}, {@link #getName()} and {@link #getMemberType()} of current instance
	 * and given one are equal
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		} else if (obj instanceof AccessorDefinition) {
			AccessorDefinition other = (AccessorDefinition) obj;
			
			return other.getDeclaringClass().equals(this.getDeclaringClass())
					&& other.getName().equals(this.getName())
					&& other.getMemberType().equals(this.getMemberType());
		} else {
			return false;
		}
	}
	
	@Override
	public int hashCode() {
		int result = declaringClass.hashCode();
		result = 31 * result + name.hashCode();
		result = 31 * result + memberType.hashCode();
		return result;
	}
	
	public Class getMemberType() {
		return memberType;
	}
	
	@Override
	public String toString() {
		return Reflections.toString(getDeclaringClass()) + '.' + getName();
	}
}