ProjectAres/API/api/src/main/java/tc/oc/api/document/ProxyDocumentGenerator.java

135 lines
5.5 KiB
Java

package tc.oc.api.document;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import com.google.common.cache.LoadingCache;
import tc.oc.api.docs.virtual.Document;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.exceptions.SerializationException;
import tc.oc.commons.core.inspect.Inspectable;
import tc.oc.commons.core.inspect.InspectableProperty;
import tc.oc.commons.core.reflect.MethodHandleUtils;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.stream.BiStream;
import tc.oc.commons.core.util.CacheUtils;
public class ProxyDocumentGenerator implements DocumentGenerator {
@Override
public <T extends Document> T instantiate(DocumentMeta<T> meta, Document base, Map<String, Object> data) {
// Validate data before creating the document
for(Map.Entry<String, Getter> entry : meta.getters().entrySet()) {
final String name = entry.getKey();
final Getter getter = entry.getValue();
if(data.containsKey(name)) {
getter.validate(data.get(name));
} else if(!getter.hasDefault()) {
throw new IllegalArgumentException("Missing value for required property " + name);
}
}
return new Invoker<>(meta, base, data).proxy;
}
private static final Method Object_toString = Methods.declaredMethod(Object.class, "toString");
private static final Method Inspectable_inspect = Methods.declaredMethod(Inspectable.class, "inspect");
private class Invoker<T extends Document> implements InvocationHandler, Inspectable {
final DocumentMeta<T> meta;
final Map<String, Object> data;
final LoadingCache<Method, MethodHandle> handles;
final T proxy;
Invoker(DocumentMeta<T> meta, Document base, Map<String, Object> data) {
this.meta = meta;
this.data = data;
this.proxy = (T) Proxy.newProxyInstance(meta.type().getClassLoader(), new Class[]{meta.type(), Inspectable.class}, this);
this.handles = CacheUtils.newCache(method -> {
if(method.getDeclaringClass().isAssignableFrom(Inspectable.class) &&
!method.getDeclaringClass().isAssignableFrom(Object.class)) {
// Send Inspectable methods to this
return MethodHandles.lookup()
.unreflect(method)
.bindTo(this);
} else if(method.equals(Object_toString)) {
// Send toString to this.inspect()
return MethodHandles.lookup()
.unreflect(Inspectable_inspect)
.bindTo(this);
}
final String name = method.getName();
final Getter getter;
// If method is a property getter, and we have a value for the property,
// return a constant method handle that just returns the value.
if(method.getParameterTypes().length == 0) {
getter = meta.getters().get(name);
if(getter != null && data.containsKey(name)) {
return MethodHandles.constant(method.getReturnType(), data.get(name));
}
} else {
getter = null;
}
// If the base class implements the method, call that one.
if(method.getDeclaringClass().isInstance(base)) {
return MethodHandles.lookup()
.unreflect(method)
.bindTo(base);
}
// If method has a default implementation in the document interface,
// return a method handle that calls that implementation directly,
// with the proxy as target object. We can't just invoke the method
// the normal way, because the proxy would intercept it again.
if(method.isDefault()) {
return MethodHandleUtils.defaultMethodHandle(method)
.bindTo(proxy);
}
// If the method is a nullable property, we can return null as a default value.
if(getter != null && getter.isNullable()) {
return MethodHandles.constant(method.getReturnType(), null);
}
throw new SerializationException("No implementation for method '" + name + "'");
});
}
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return handles.getUnchecked(method).invokeWithArguments(args);
}
@Override
public String inspectType() {
return meta.type().getSimpleName();
}
@Override
public Optional<String> inspectIdentity() {
if(proxy instanceof Model) {
return Optional.of(((Model) proxy)._id());
}
return Optional.empty();
}
@Override
public Stream<? extends InspectableProperty> inspectableProperties() {
return BiStream.from(data)
.merge(InspectableProperty::of);
}
}
}