package jalse.entities;
import static jalse.entities.Entities.isEntitySubtype;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import jalse.entities.functions.DefaultFunction;
import jalse.entities.functions.EntityFunction;
import jalse.entities.functions.EntityFunctionResolver;
import jalse.entities.functions.EntityMethodFunction;
import jalse.entities.functions.GetAttributeFunction;
import jalse.entities.functions.GetEntitiesFunction;
import jalse.entities.functions.GetEntityFunction;
import jalse.entities.functions.KillEntitiesFunction;
import jalse.entities.functions.KillEntityFunction;
import jalse.entities.functions.MarkAsTypeFunction;
import jalse.entities.functions.NewEntityFunction;
import jalse.entities.functions.ScheduleForActorFunction;
import jalse.entities.functions.SetAttributeFunction;
import jalse.entities.functions.StreamEntitiesFunction;
import jalse.entities.functions.UnmarkAsTypeFunction;
import jalse.entities.methods.EntityMethod;
/**
* This is the default {@link EntityProxyFactory} implementation for JALSE. This proxy factory will
* use {@link EntityFunctionResolver} to resolve an {@link EntityFunction} to translate the proxy
* {@link Method} calls to {@link EntityMethod}s. Override {@link #addResolverFunctions()} to change
* what {@link EntityMethodFunction}s are added to the resolver. <br>
* <br>
* NOTE: Proxies are cached per {@link Entity} using {@link EntityProxyCache}.
*
* @author Elliot Ford
*
* @see #uncacheProxyOfEntity(Entity, Class)
* @see #uncacheProxiesOfEntity(Entity)
* @see #uncacheAllProxies()
*
*/
public class DefaultEntityProxyFactory implements EntityProxyFactory {
/**
* A {@link WeakHashMap} cache for {@link Entity} proxies.
*
* @author Elliot Ford
*
* @see Collections#synchronizedMap(Map)
*
*/
protected class EntityProxyCache {
private final Map<Entity, Map<Class<?>, Object>> proxyMap;
/**
* Creates a new entity proxy cache.
*/
private EntityProxyCache() {
proxyMap = Collections.synchronizedMap(new WeakHashMap<>());
}
/**
* Either retrieves a proxy from the cache or creates (and caches) a new entity proxy.
*
* @param e
* Entity to cache type for.
* @param type
* Entity type to proxy.
* @return Proxy entity of type.
*
* @see EntityProxyHandler
*/
@SuppressWarnings("unchecked")
public <T extends Entity> T getOrNew(final Entity e, final Class<T> type) {
final Map<Class<?>, Object> proxies = proxyMap.computeIfAbsent(e, k -> new ConcurrentHashMap<>());
return (T) proxies.computeIfAbsent(type, k -> newEntityProxy(e, k));
}
/**
* Uncaches all proxies.
*/
public void invalidateAll() {
proxyMap.clear();
}
/**
* Uncaches all proxies for an entity.
*
* @param e
* Entity key.
*/
public void invalidateEntity(final Entity e) {
proxyMap.remove(e);
}
/**
* Uncaches a the proxy of the specified type for the entity.
*
* @param e
* Entity key.
* @param type
* Entity type to remove.
*/
public void invalidateType(final Entity e, final Class<? extends Entity> type) {
final Map<Class<?>, Object> proxies = proxyMap.get(e);
if (proxies != null) {
proxies.remove(type);
proxyMap.computeIfPresent(e, (k, v) -> v.isEmpty() ? null : v);
}
}
private Object newEntityProxy(final Entity e, final Class<?> type) {
logger.fine(String.format("Creating proxy of type %s for entity %s", type, e.getID()));
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] { type },
new EntityProxyHandler(e));
}
}
/**
* The {@link InvocationHandler} for the entity proxies. The {@link Entity} is wrapped in a
* {@link WeakReference}.
*
* @author Elliot Ford
*
*/
protected class EntityProxyHandler implements InvocationHandler {
private final WeakReference<Entity> entityRef;
/**
* Creates a new entity proxy handler.
*
* @param entity
* Entity to proxy events for.
*/
private EntityProxyHandler(final Entity entity) {
entityRef = new WeakReference<Entity>(entity);
}
/**
* Gets the entity this proxy is for.
*
* @return Host entity.
*
* @see WeakReference
*/
public Entity getEntity() {
final Entity entity = entityRef.get();
if (entity == null) {
throw new IllegalStateException("Entity reference lost");
}
return entity;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
/*
* Get Entity reference.
*/
final Entity entity = getEntity();
/*
* Not an Entity subclass.
*/
final Class<?> declaringClazz = method.getDeclaringClass();
if (!isEntitySubtype(declaringClazz)) {
return method.invoke(entity, args);
}
/*
* Invoke resolved method.
*/
@SuppressWarnings("unchecked")
final EntityFunction resolvedType = resolver.resolveType((Class<? extends Entity>) declaringClazz);
final EntityMethod entityMethod = resolvedType.apply(method);
return entityMethod.invoke(proxy, entity, args);
}
}
private static final Logger logger = Logger.getLogger(DefaultEntityProxyFactory.class.getName());
/**
* Resolver for entity types.
*/
protected final EntityFunctionResolver resolver;
/**
* Proxy cache for entities.
*/
protected final EntityProxyCache cache;
/**
* Creates a new DefaultEntityProxyFactory.
*
* @see #addResolverFunctions()
*/
public DefaultEntityProxyFactory() {
resolver = new EntityFunctionResolver();
cache = new EntityProxyCache();
addResolverFunctions();
}
/**
* Adds the {@link EntityMethodFunction}s to the resolver.
*
* @see DefaultFunction
* @see GetAttributeFunction
* @see SetAttributeFunction
* @see NewEntityFunction
* @see GetEntityFunction
* @see StreamEntitiesFunction
* @see GetEntitiesFunction
* @see KillEntityFunction
* @see KillEntitiesFunction
* @see MarkAsTypeFunction
* @see UnmarkAsTypeFunction
* @see ScheduleForActorFunction
*/
protected void addResolverFunctions() {
resolver.addMethodFunction(new DefaultFunction());
resolver.addMethodFunction(new GetAttributeFunction());
resolver.addMethodFunction(new SetAttributeFunction());
resolver.addMethodFunction(new NewEntityFunction());
resolver.addMethodFunction(new GetEntityFunction());
resolver.addMethodFunction(new StreamEntitiesFunction());
resolver.addMethodFunction(new GetEntitiesFunction());
resolver.addMethodFunction(new KillEntityFunction());
resolver.addMethodFunction(new KillEntitiesFunction());
resolver.addMethodFunction(new MarkAsTypeFunction());
resolver.addMethodFunction(new UnmarkAsTypeFunction());
resolver.addMethodFunction(new ScheduleForActorFunction());
}
@Override
public boolean isProxyEntity(final Entity e) {
return Proxy.isProxyClass(e.getClass()) && Proxy.getInvocationHandler(e) instanceof EntityProxyHandler;
}
@Override
public <T extends Entity> T proxyOfEntity(final Entity e, final Class<T> type) {
validateType(type);
return cache.getOrNew(e, type);
}
/**
* Uncaches all proxies.
*/
public void uncacheAllProxies() {
resolver.unresolveAllTypes();
cache.invalidateAll();
}
/**
* Uncaches all proxies for an entity.
*
* @param e
* Entity to uncache for.
*/
public void uncacheProxiesOfEntity(final Entity e) {
cache.invalidateEntity(e);
}
/**
* Uncaches the specific type proxy for an entity.
*
* @param e
* Entity to uncache for.
* @param type
* Proxy type.
*/
public void uncacheProxyOfEntity(final Entity e, final Class<? extends Entity> type) {
cache.invalidateType(e, type);
}
@Override
public void validateType(final Class<? extends Entity> type) {
resolver.resolveType(type);
}
}