package jalse.entities.functions;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;
import jalse.entities.Entity;
import jalse.entities.methods.EntityMethod;
/**
* This is a resolver for {@link Entity} types. It can be given a number of
* {@link EntityMethodFunction} that can be used to process {@link Method}s from an {@link Entity}
* type and then map them to an {@link EntityMethod}. Once all methods are resolved a
* {@link EntityFunction} can be obtained for the original {@link Entity} type (so an
* {@link InvocationHandler} can use the mapped methods). <br>
* <br>
* The resolving process can throw exceptions if it does not resolve correctly. Types resolved (
* {@link #resolveType(Class)}) are cached so they do not have to be resolved again (unless
* {@link #unresolveType(Class)} or {@link #unresolveAllTypes()} are called). <br>
* <br>
* NOTE: this is a thread-safe implementation.
*
* @author Elliot Ford
*
*/
public class EntityFunctionResolver {
private static final Logger logger = Logger.getLogger(EntityFunctionResolver.class.getName());
private final Map<Class<? extends Entity>, EntityFunction> resolved;
private final Set<EntityMethodFunction> functions;
/**
* Creates a new type resolver (no method functions).
*/
public EntityFunctionResolver() {
resolved = new ConcurrentHashMap<>();
functions = new CopyOnWriteArraySet<>();
}
/**
* Creates a new type resolver with the supplied method functions.
*
* @param methodFunctions
* Method functions to add.
*/
public EntityFunctionResolver(final Set<EntityMethodFunction> methodFunctions) {
this();
addMethodFunctions(methodFunctions);
}
/**
* Adds a method function.
*
* @param methodFunction
* Function to add.
* @return Whether the function was not already added.
*/
public boolean addMethodFunction(final EntityMethodFunction methodFunction) {
return functions.add(methodFunction);
}
/**
* Adds a number of method functions.
*
* @param methodFunctions
* Functions to add.
*/
public void addMethodFunctions(final Set<? extends EntityMethodFunction> methodFunctions) {
functions.addAll(methodFunctions);
}
/**
* Gets all of the resolved {@link Entity} types.
*
* @return All resolved types.
*/
public Set<Class<? extends Entity>> getResolvedTypes() {
return new HashSet<>(resolved.keySet());
}
/**
* Removes all method functions.
*/
public void removeAllMethodFunctions() {
functions.clear();
}
/**
* Removes a method function.
*
* @param methodFunction
* function to remove.
* @return Whether the function was there to remove.
*/
public boolean removeMethodResolver(final EntityMethodFunction methodFunction) {
return functions.remove(methodFunction);
}
/**
* Removes a number of method functions.
*
* @param methodFunctions
* Functions to remove.
*/
public void removeMethodResolvers(final Set<? extends EntityMethodFunction> methodFunctions) {
functions.removeAll(methodFunctions);
}
/**
* Resolves an {@link Entity} type to a new resolved {@link EntityFunction}.
*
* @param type
* Type to resolve.
* @return New (or cached) resolved mapped type.
*/
@SuppressWarnings("unchecked")
public EntityFunction resolveType(final Class<? extends Entity> type) {
// Check already resolved
EntityFunction resolvedType = resolved.get(Objects.requireNonNull(type));
if (resolvedType != null) {
return resolvedType;
}
// Log type
logger.info(String.format("Resolving type %s", type));
// Check not entity
if (Entity.class.equals(type)) {
throw new IllegalArgumentException("Entity is not a valid Entity subclass");
}
// Check is interface
if (!type.isInterface()) {
throw new IllegalArgumentException(String.format("Type %s is not an interface", type));
}
// Check type parameters
if (type.getTypeParameters().length > 0) {
throw new IllegalArgumentException(String.format("Type %s cannot take type parameters", type));
}
// Build method map
final Map<Method, EntityMethod> methodMap = new HashMap<>();
final Set<Class<? extends Entity>> totalDependencies = new HashSet<>();
for (final Method m : type.getDeclaredMethods()) {
// Check static
if (Modifier.isStatic(m.getModifiers())) {
// Ignore static methods as not invoked on instance
continue;
}
// Process functions
for (final EntityMethodFunction methodFunction : functions) {
// Log method
logger.fine(String.format("Resolving method %s with function %s", m, methodFunction.getClass()));
// Resolve method
final EntityMethod em = methodFunction.apply(m);
if (em != null) {
// Resolved method
if (methodMap.put(m, em) != null) {
throw new IllegalArgumentException(
String.format("Method %s of type %s had multiple function hits", m, type));
}
// Add dependencies to be resolved
totalDependencies.addAll(em.getDependencies());
}
}
// Check method could be resolved
if (!methodMap.containsKey(m)) {
throw new IllegalArgumentException(String.format("Method %s of type %s had no resolver hits", m, type));
}
}
// Created resolved type
resolvedType = new EntityFunction(type, methodMap);
resolved.put(type, resolvedType);
// Resolve dependencies
totalDependencies.forEach(this::resolveType);
// Resolve parents
for (final Class<?> parent : type.getInterfaces()) {
// Ignore if entity
if (Entity.class.equals(parent)) {
continue;
}
// Check if entity subclass
if (!Entity.class.isAssignableFrom(parent)) {
throw new IllegalArgumentException(
String.format("Parent type %s of type %s is not an Entity", parent, type));
}
// Resolve dependency
resolveType((Class<? extends Entity>) parent);
}
return resolvedType;
}
/**
* Uncaches all resolved types.
*/
public void unresolveAllTypes() {
resolved.clear();
}
/**
* Uncaches a resolved type.
*
* @param type
* Type to uncache.
*
* @return Whether the type was present in the resolver.
*/
public boolean unresolveType(final Class<? extends Entity> type) {
return resolved.remove(Objects.requireNonNull(type)) != null;
}
}