135 lines
5.5 KiB
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);
|
|
}
|
|
}
|
|
}
|