diff --git a/src/main/java/tc/oc/minecraft/api/command/CommandSender.java b/src/main/java/tc/oc/minecraft/api/command/CommandSender.java index 27b4f47..43afe35 100644 --- a/src/main/java/tc/oc/minecraft/api/command/CommandSender.java +++ b/src/main/java/tc/oc/minecraft/api/command/CommandSender.java @@ -2,8 +2,13 @@ import net.md_5.bungee.api.chat.BaseComponent; import tc.oc.minecraft.api.permissions.Permissible; +import tc.oc.reference.Handle; +import tc.oc.reference.Handleable; -public interface CommandSender extends Permissible { +public interface CommandSender extends Permissible, Handleable { + + @Override + Handle handle(); /** * Get the unique name of this command sender. diff --git a/src/main/java/tc/oc/minecraft/api/entity/Player.java b/src/main/java/tc/oc/minecraft/api/entity/Player.java index 56d4aa3..a9880c7 100644 --- a/src/main/java/tc/oc/minecraft/api/entity/Player.java +++ b/src/main/java/tc/oc/minecraft/api/entity/Player.java @@ -6,9 +6,13 @@ import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import tc.oc.minecraft.api.command.CommandSender; +import tc.oc.reference.Handle; public interface Player extends CommandSender { + @Override + Handle handle(); + /** * Gets this player's display name. * @@ -56,4 +60,6 @@ public interface Player extends CommandSender { * Get the Minecraft protocol version in use by this player's connection */ int getProtocolVersion(); + + } diff --git a/src/main/java/tc/oc/minecraft/api/plugin/Plugin.java b/src/main/java/tc/oc/minecraft/api/plugin/Plugin.java index f08c13e..1578718 100644 --- a/src/main/java/tc/oc/minecraft/api/plugin/Plugin.java +++ b/src/main/java/tc/oc/minecraft/api/plugin/Plugin.java @@ -9,8 +9,14 @@ import tc.oc.exception.ExceptionHandler; import tc.oc.minecraft.api.server.Server; import tc.oc.minecraft.api.logging.Loggable; +import tc.oc.reference.Handle; +import tc.oc.reference.Handleable; + +public interface Plugin extends Loggable, Handleable { + + @Override + Handle handle(); -public interface Plugin extends Loggable { /** * Return metadata about this plugin */ diff --git a/src/main/java/tc/oc/minecraft/api/server/LocalServer.java b/src/main/java/tc/oc/minecraft/api/server/LocalServer.java index 8273d07..9cc3da8 100644 --- a/src/main/java/tc/oc/minecraft/api/server/LocalServer.java +++ b/src/main/java/tc/oc/minecraft/api/server/LocalServer.java @@ -5,12 +5,16 @@ import tc.oc.minecraft.api.command.ConsoleCommandSender; import tc.oc.minecraft.api.logging.Loggable; import tc.oc.minecraft.api.plugin.PluginFinder; +import tc.oc.reference.Handle; /** * The local server i.e. the one hosting plugins */ public interface LocalServer extends Loggable, Server { + @Override + Handle handle(); + PluginFinder getPluginFinder(); ConsoleCommandSender getConsoleSender(); diff --git a/src/main/java/tc/oc/minecraft/api/server/Server.java b/src/main/java/tc/oc/minecraft/api/server/Server.java index 63c4f32..e4bee27 100644 --- a/src/main/java/tc/oc/minecraft/api/server/Server.java +++ b/src/main/java/tc/oc/minecraft/api/server/Server.java @@ -4,11 +4,16 @@ import java.util.UUID; import tc.oc.minecraft.api.entity.Player; +import tc.oc.reference.Handle; +import tc.oc.reference.Handleable; /** * A Minecraft server or proxy, local or remote */ -public interface Server { +public interface Server extends Handleable { + + @Override + Handle handle(); /** * Return all players currently connected. diff --git a/src/main/java/tc/oc/proxy/Proxies.java b/src/main/java/tc/oc/proxy/Proxies.java new file mode 100644 index 0000000..c6363fa --- /dev/null +++ b/src/main/java/tc/oc/proxy/Proxies.java @@ -0,0 +1,42 @@ +package tc.oc.proxy; + +import java.lang.reflect.Proxy; +import java.util.Map; +import java.util.function.Supplier; + +public class Proxies { + + public static T forwarding(Class type, Supplier supplier) { + return forwarding(type.getClassLoader(), type, supplier); + } + + public static T forwarding(ClassLoader loader, Class type, Supplier supplier) { + return (T) Proxy.newProxyInstance( + loader, + new Class[]{type}, + (proxy, method, args) -> method.invoke(supplier.get(), args) + ); + } + + public static T forwarding(ClassLoader loader, Class type, Supplier supplier, Map, ?> extensions) { + final Class[] inters = new Class[extensions.size() + 1]; + int i = 0; + for(Class inter : extensions.keySet()) { + inters[i++] = inter; + } + inters[i] = type; + + return (T) Proxy.newProxyInstance( + loader, + inters, + (proxy, method, args) -> { + for(Class inter : extensions.keySet()) { + if(method.getDeclaringClass().isAssignableFrom(inter)) { + return method.invoke(extensions.get(inter), args); + } + } + return method.invoke(supplier.get(), args); + } + ); + } +} diff --git a/src/main/java/tc/oc/proxy/ProxyBuilder.java b/src/main/java/tc/oc/proxy/ProxyBuilder.java new file mode 100644 index 0000000..35955cc --- /dev/null +++ b/src/main/java/tc/oc/proxy/ProxyBuilder.java @@ -0,0 +1,150 @@ +package tc.oc.proxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +public class ProxyBuilder { + + private static final List IDENTITY_METHODS; + + static { + try { + IDENTITY_METHODS = Arrays.asList( + Object.class.getDeclaredMethod("equals", Object.class), + Object.class.getDeclaredMethod("hashCode") + ); + + } catch(NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + + private ClassLoader loader = null; + private final Map methodHandlers = new LinkedHashMap<>(); + private final Map, InvocationHandler> typeHandlers = new LinkedHashMap<>(); + + public ProxyBuilder loadFrom(ClassLoader loader) { + this.loader = loader; + return this; + } + + public HandlerBuilder delegate(Method method) { + return new HandlerBuilder<>(to -> methodHandlers.put(method, to)); + } + + public HandlerBuilder delegate(Iterable methods) { + return new HandlerBuilder<>(to -> { + for(Method method : methods) { + methodHandlers.put(method, to); + } + }); + } + + public HandlerBuilder delegate(Class type) { + return new HandlerBuilder<>(to -> typeHandlers.put(type, to)); + } + + public HandlerBuilder delegateIdentity() { + return delegate(IDENTITY_METHODS); + } + + private void validate() { + for(Class type : typeHandlers.keySet()) { + if(!type.isInterface() && !type.equals(Object.class)) { + throw new IllegalArgumentException("Cannot proxy non-interface type " + type.getName()); + } + } + for(Method method : methodHandlers.keySet()) { + if(typeHandlers.keySet().stream().noneMatch(method.getDeclaringClass()::isAssignableFrom)) { + throw new IllegalArgumentException("Method " + method.getName() + " is not present in any implemented interfaces"); + } + } + } + + public Object newProxyInstance() { + validate(); + return Proxy.newProxyInstance( + loader != null ? loader : Thread.currentThread().getContextClassLoader(), + typeHandlers.keySet().toArray(new Class[typeHandlers.size()]), + new Invoker(methodHandlers, typeHandlers) + ); + } + + public class HandlerBuilder { + private Consumer consumer; + + private HandlerBuilder(Consumer consumer) { + this.consumer = consumer; + } + + public ProxyBuilder to(InvocationHandler handler) { + if(consumer == null) { + throw new IllegalStateException(); + } + consumer.accept(handler); + consumer = null; + return ProxyBuilder.this; + } + + public ProxyBuilder toInstance(T to) { + return to((proxy, m, args) -> m.invoke(to, args)); + } + + public ProxyBuilder toSupplier(Supplier to) { + return to((proxy, method, args) -> method.invoke(to.get(), args)); + } + } + + private static class Invoker implements InvocationHandler { + + private final LoadingCache handlers; + + private Invoker(Map methods, Map, InvocationHandler> types) { + this.handlers = CacheBuilder.newBuilder().build(new CacheLoader() { + @Override + public InvocationHandler load(Method method) throws Exception { + // Look for an exact method + InvocationHandler handler = methods.get(method); + if(handler != null) return handler; + + // Look for the exact declaring interface of the method + // If Object is delegated, that will be caught here + final Class decl = method.getDeclaringClass(); + handler = types.get(decl); + if(handler != null) return handler; + + // Any Object methods not handled above are delegated to the Invoker itself + if(decl.equals(Object.class)) { + return (proxy, method1, args) -> method1.invoke(Invoker.this, args); + } + + // If the method is not from Object, look for an interface that inherits it + for(Map.Entry, InvocationHandler> entry : types.entrySet()) { + if(decl.isAssignableFrom(entry.getKey())) { + return entry.getValue(); + } + } + + // Give up ¯\_(ツ)_/¯ + throw new NoSuchMethodError(method.getName()); + } + }); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return handlers.get(method).invoke(proxy, method, args); + } + } +} diff --git a/src/main/java/tc/oc/reference/Handle.java b/src/main/java/tc/oc/reference/Handle.java new file mode 100644 index 0000000..72906bb --- /dev/null +++ b/src/main/java/tc/oc/reference/Handle.java @@ -0,0 +1,332 @@ +package tc.oc.reference; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import javax.inject.Provider; + +import tc.oc.proxy.ProxyBuilder; +import tc.oc.util.Lazy; + +/** + * A lightweight object that can quickly retrieve some other object of type {@link T}, + * without necessarily holding a strong reference to it. This is similar to a {@link WeakReference}, + * but more flexible, because the mechanism for (de)referencing can be implemented + * by the creator of the handle. Various useful bells and whistles are also provided. + * + * There are several ways to retrieve the referent object: + * + * {@link #getIfPresent()} is self-explanatory + * + * {@link #get()} returns the referent if available, otherwise it throws a + * {@link HandleUnavailableException}. Any exception thrown by the dereferencing code is wrapped + * in a {@link HandleDereferenceException}, so that it can be reliably distinguished. + * + * {@link #proxy()} tries to return a proxy for the actual referent that routes all + * method calls through the handle. This only works if the handle was constructed with + * an explicit interface type. Otherwise, {@link #proxy()} just calls {@link #get()}. + */ +public abstract class Handle implements Supplier, Provider { + + private final @Nullable Class type; + private final @Nullable Object key; + private final Lazy proxy; + + private Handle(@Nullable Class type, @Nullable Object key) { + if(type != null && !type.isInterface()) { + throw new IllegalArgumentException("Handle type " + type.getName() + " is not an interface"); + } + + this.type = type; + this.key = key; + + proxy = Lazy.from(() -> (T) new ProxyBuilder() + .loadFrom(type.getClassLoader()) + .delegateIdentity().toSupplier(this::key) + .delegate(Handleable.class).toInstance(new Handleable() { + @Override public Handle handle() { return Handle.this; } + }) + .delegate(type).toSupplier(this) + .newProxyInstance() + ); + } + + public boolean hasKey() { + return key != null; + } + + public Object key() throws HandleUnavailableException { + return key != null ? key : get(); + } + + public boolean hasProxy() { + return type != null; + } + + public T proxy() { + return type != null ? proxy.get() : get(); + } + + @Override + public int hashCode() { + return key().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this == obj || ( + obj instanceof Handle && + this.key().equals(((Handle) obj).key()) + ); + } + + public boolean isPresent() { + return getIfPresent().isPresent(); + } + + public abstract Optional getIfPresent(); + + @Override + public T get() throws HandleUnavailableException, HandleDereferenceException { + final Optional opt; + try { opt = getIfPresent(); } + catch(Throwable ex) { throw HandleDereferenceException.causedBy(ex); } + return opt.orElseThrow(HandleUnavailableException::new); + } + + public Handle ifPresent(Consumer consumer) { + if(isPresent()) { + consumer.accept(get()); + } + return this; + } + + public Handle ifAbsent(Runnable runnable) { + if(!isPresent()) { + runnable.run(); + } + return this; + } + + public T orElse(T t) { + return isPresent() ? get() : t; + } + + public T orElseGet(Supplier supplier) { + return isPresent() ? get() : supplier.get(); + } + + public T orElseThrow(Supplier exceptionSupplier) throws X { + if(isPresent()) return get(); + throw exceptionSupplier.get(); + } + + public Handle filter(Predicate predicate) { + return new Handle(type, key) { + @Override + public Optional getIfPresent() { + return Handle.this.getIfPresent().filter(predicate); + } + }; + } + + public Handle map(Function mapper) { + return map(null, mapper); + } + + public Handle map(@Nullable Object key, Function mapper) { + return map(null, key, mapper); + } + + public Handle map(@Nullable Class type, @Nullable Object key, Function mapper) { + return new Handle(type, key) { + @Override + public Optional getIfPresent() { + return Handle.this.getIfPresent().flatMap(u -> { + try { + return Optional.of(mapper.apply(u)); + } catch(HandleUnavailableException ex) { + return Optional.empty(); + } + }); + } + + @Override + public U get() throws HandleUnavailableException { + return mapper.apply(Handle.this.get()); + } + }; + } + + public Handle flatMap(Function> mapper) { + return flatMap(null, mapper); + } + + public Handle flatMap(@Nullable Object key, Function> mapper) { + return flatMap(null, key, mapper); + } + + public Handle flatMap(@Nullable Class type, @Nullable Object key, Function> mapper) { + return new Handle(type, key) { + @Override + public Optional getIfPresent() { + return Handle.this.getIfPresent().flatMap(mapper); + } + }; + } + + public static Handle empty() { + return empty(null); + } + + public static Handle empty(@Nullable Object key) { + return empty(null, key); + } + + public static Handle empty(@Nullable Class type, @Nullable Object key) { + return new Handle(type, key) { + @Override + public boolean isPresent() { + return false; + } + + @Override + public Optional getIfPresent() { + return Optional.empty(); + } + + @Override + public T get() { + throw new HandleUnavailableException(); + } + }; + } + + public static Handle ofInstance(T instance) { + return ofInstance(null, instance); + } + + public static Handle ofInstance(@Nullable Class type, T instance) { + final Optional opt = Optional.of(instance); + return new Handle(type, instance) { + @Override + public boolean isPresent() { + return true; + } + + @Override + public Optional getIfPresent() { + return opt; + } + + @Override + public T get() { + return instance; + } + }; + } + + public static Handle ofWeakInstance(T instance) { + return ofWeakInstance(null, instance); + } + + public static Handle ofWeakInstance(@Nullable Object key, T instance) { + return ofWeakInstance(null, key, instance); + } + + public static Handle ofWeakInstance(@Nullable Class type, @Nullable Object key, T instance) { + return ofReference(type, key, new WeakReference<>(instance)); + } + + public static Handle ofReference(Reference reference) { + return ofReference(null, reference); + } + + public static Handle ofReference(@Nullable Object key, Reference reference) { + return ofReference(null, key, reference); + } + + public static Handle ofReference(@Nullable Class type, @Nullable Object key, Reference reference) { + return ofNullableSupplier(type, key, reference::get); + } + + public static Handle ofSupplier(Supplier supplier) { + return ofSupplier(null, supplier); + } + + public static Handle ofSupplier(@Nullable Object key, Supplier supplier) { + return ofSupplier(null, key, supplier); + } + + public static Handle ofSupplier(@Nullable Class type, @Nullable Object key, Supplier supplier) { + return new Handle(type, key) { + @Override + public Optional getIfPresent() { + try { + return Optional.of(get()); + } catch(HandleUnavailableException ex) { + return Optional.empty(); + } + } + + @Override + public T get() throws HandleUnavailableException { + return supplier.get(); + } + }; + } + + public static Handle ofOptionalSupplier(Supplier> supplier) { + return ofOptionalSupplier(null, supplier); + } + + public static Handle ofOptionalSupplier(@Nullable Object key, Supplier> supplier) { + return ofOptionalSupplier(null, key, supplier); + } + + public static Handle ofOptionalSupplier(@Nullable Class type, @Nullable Object key, Supplier> supplier) { + return new Handle(type, key) { + @Override + public Optional getIfPresent() { + return supplier.get(); + } + }; + } + + public static Handle ofNullableSupplier(Supplier supplier) { + return ofNullableSupplier(null, supplier); + } + + public static Handle ofNullableSupplier(@Nullable Object key, Supplier supplier) { + return ofNullableSupplier(null, key, supplier); + } + + public static Handle ofNullableSupplier(@Nullable Class type, @Nullable Object key, Supplier supplier) { + return new Handle(type, key) { + @Override + public boolean isPresent() { + return null != supplier.get(); + } + + @Override + public Optional getIfPresent() { + return Optional.ofNullable(supplier.get()); + } + + @Override + public T get() { + final T instance; + try { instance = supplier.get(); } + catch(Throwable ex) { throw HandleDereferenceException.causedBy(ex); } + if(instance == null) throw new HandleUnavailableException(); + return instance; + } + }; + } +} + diff --git a/src/main/java/tc/oc/reference/HandleDereferenceException.java b/src/main/java/tc/oc/reference/HandleDereferenceException.java new file mode 100644 index 0000000..7d7ec8e --- /dev/null +++ b/src/main/java/tc/oc/reference/HandleDereferenceException.java @@ -0,0 +1,13 @@ +package tc.oc.reference; + +public class HandleDereferenceException extends RuntimeException { + + public HandleDereferenceException(Throwable cause) { + super("Exception while dereferencing", cause); + } + + public static HandleDereferenceException causedBy(Throwable cause) { + return cause instanceof HandleDereferenceException ? (HandleDereferenceException) cause + : new HandleDereferenceException(cause); + } +} diff --git a/src/main/java/tc/oc/reference/HandleUnavailableException.java b/src/main/java/tc/oc/reference/HandleUnavailableException.java new file mode 100644 index 0000000..19099b5 --- /dev/null +++ b/src/main/java/tc/oc/reference/HandleUnavailableException.java @@ -0,0 +1,12 @@ +package tc.oc.reference; + +public class HandleUnavailableException extends RuntimeException { + + public HandleUnavailableException() { + this("Handle is not available"); + } + + public HandleUnavailableException(String message) { + super(message); + } +} diff --git a/src/main/java/tc/oc/reference/Handleable.java b/src/main/java/tc/oc/reference/Handleable.java new file mode 100644 index 0000000..9120ecd --- /dev/null +++ b/src/main/java/tc/oc/reference/Handleable.java @@ -0,0 +1,42 @@ +package tc.oc.reference; + +/** + * An object that can supply a {@link Handle} for itself. + * + * When creating a handle that uses an anonymous class or lambda to retrieve the referent, + * be careful not to capture the referent itself. For example, this is bad: + * + * + * class Thing implements Handleable { + * int id; + * + * @Override + * public Handle handle() { + * return Handle.ofSupplier(() -> Thing.find(this.id)) + * } + * } + * + * + * The lambda captures a reference to the Thing by accessing an instance field, + * and so the Handle effectively leaks the instance. To ensure this doesn't happen, + * you can create the reference in a static method that doesn't have any access + * to the referent: + * + * + * private static Handle makeHandle(int id) { + * return Handle.ofSupplier(() -> Thing.find(id)); + * } + * + * @Override + * public Handle handle() { + * return makeHandle(this.id); + * } + * + * + */ +public interface Handleable { + + default Handle handle() { + return Handle.ofWeakInstance(this); + } +} diff --git a/src/main/java/tc/oc/util/Lazy.java b/src/main/java/tc/oc/util/Lazy.java new file mode 100644 index 0000000..47337cb --- /dev/null +++ b/src/main/java/tc/oc/util/Lazy.java @@ -0,0 +1,43 @@ +package tc.oc.util; + +import java.util.function.Supplier; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Provider; + +public class Lazy implements Supplier, Provider { + + public static Lazy from(Supplier supplier) { + return new Lazy<>(supplier); + } + + private @Nullable Supplier supplier; + private @Nullable T instance; + + @Inject Lazy(Provider provider) { + this.supplier = provider::get; + } + + Lazy(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public T get() { + if(instance != null) return instance; + synchronized(this) { + if(instance != null) return instance; + if(supplier == null) { + throw new IllegalStateException("Circular instantiation of lazy object"); + } + final Supplier temp = supplier; + supplier = null; + instance = temp.get(); + if(instance == null) { + throw new NullPointerException("Lazy object cannot have a null value"); + } + return instance; + } + } +} +