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

272 lines
10 KiB
Java

package tc.oc.api.document;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.api.client.repackaged.com.google.common.base.Joiner;
import com.google.common.base.Function;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.annotations.SerializedName;
import com.google.inject.Injector;
import tc.oc.api.docs.BasicDeletableModel;
import tc.oc.api.docs.BasicModel;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.SimplePlayerId;
import tc.oc.api.docs.SimpleUserId;
import tc.oc.api.docs.UserId;
import tc.oc.api.docs.virtual.BasicDocument;
import tc.oc.api.docs.virtual.DeletableModel;
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.logging.Loggers;
import tc.oc.commons.core.reflect.Types;
import tc.oc.commons.core.util.C3;
import tc.oc.commons.core.util.MapUtils;
/**
* Cache of {@link DocumentMeta} records, populated on-demand when a {@link Document}
* is serialized or deserialized.
*/
@Singleton
public class DocumentRegistry {
protected final Logger logger;
protected final DocumentGenerator generator;
protected final Injector injector;
private final LoadingCache<Class<? extends Document>, DocumentMeta> cache = CacheBuilder.newBuilder().build(
new CacheLoader<Class<? extends Document>, DocumentMeta>() {
@Override
public DocumentMeta load(Class<? extends Document> type) throws Exception {
return register(type);
}
}
);
@Inject DocumentRegistry(Loggers loggers, DocumentGenerator generator, Injector injector) {
this.generator = generator;
this.injector = injector;
this.logger = loggers.get(getClass());
}
/**
* Is the given document type directly instantiable? This is true only
* if the type is a non-abstract class with a default constructor.
*/
public boolean isInstantiable(Class<? extends Document> type) {
if(type.isInterface() || Modifier.isAbstract(type.getModifiers())) return false;
try {
type.getDeclaredConstructor();
return true;
} catch(NoSuchMethodException e) {
return false;
}
}
public <T extends Document> T instantiate(Class<T> type, Map<String, Object> data) {
return instantiate(getMeta(type), data);
}
public <T extends Document> T instantiate(DocumentMeta<T> meta, Map<String, Object> data) {
if(meta.type().isInterface()) {
// To create an interface document, choose the best base type,
// instantiate that, and then wrap it in a proxy that implements
// the rest of the properties.
return generator.instantiate(meta, instantiate(getMeta(meta.baseType()), data), data);
} else if(isInstantiable(meta.type())) {
// If document type is directly instantiable, get an instance
// from the injector and use setters to initialize it.
final T doc = injector.getInstance(meta.type());
for(Map.Entry<String, Setter> entry : meta.setters().entrySet()) {
if(data.containsKey(entry.getKey())) {
entry.getValue().setUnchecked(doc, data.get(entry.getKey()));
}
}
return doc;
} else {
throw new SerializationException("Document type " + meta.type().getName() + " is not instantiable");
}
}
public <T extends Document> T copy(T original) {
final DocumentMeta<T> meta = getMeta((Class<T>) original.getClass());
return instantiate(meta, ImmutableMap.copyOf(Maps.transformValues(meta.getters(), getter -> getter.get(original))));
}
/**
* Get (or create) the metadata for the given {@link Document} type.
*/
public <T extends Document> DocumentMeta<T> getMeta(Class<T> type) {
return cache.getUnchecked(type);
}
private <T extends Document> DocumentMeta<T> register(final Class<T> type) {
logger.fine("Registering serializable type " + type);
// Find property accessors declared directly on the given document
final Map<String, Getter> getters = new HashMap<>();
final Map<String, Setter> setters = new HashMap<>();
for(Method method : DocumentMeta.serializedMethods(type)) {
registerMethod(getters, setters, method);
}
for(Field field : DocumentMeta.serializedFields(type)) {
registerField(getters, setters, field);
}
// Find the immediate supertypes of the document
final List<DocumentMeta<? super T>> parents = new ArrayList<>();
for(Class<? super T> parent : Types.parents(type)) {
if(Document.class.isAssignableFrom(parent)) {
parents.add((DocumentMeta<? super T>) getMeta(parent.asSubclass(Document.class)));
}
}
// Merge all ancestors into a single list
final List<DocumentMeta<? super T>> ancestors = ImmutableList.copyOf(
C3.merge(
Lists.transform(
parents,
(Function<DocumentMeta<? super T>, Collection<? extends DocumentMeta<? super T>>>) DocumentMeta::ancestors
)
)
);
if(logger.isLoggable(Level.FINE)) {
logger.fine("Linearized ancestors for " + type + ": " + Joiner.on(", ").join(ancestors));
}
// Copy inherited accessors from ancestor documents
for(DocumentMeta<? super T> ancestor : ancestors) {
MapUtils.putAbsent(getters, ancestor.getters());
MapUtils.putAbsent(setters, ancestor.setters());
}
return new DocumentMeta<>(type, ancestors, bestBaseClass(type), getters, setters);
}
private static String serializedName(Member member) {
if(member instanceof AnnotatedElement) {
SerializedName nameAnnot = ((AnnotatedElement) member).getAnnotation(SerializedName.class);
if(nameAnnot != null) return nameAnnot.value();
}
return member.getName();
}
private static @Nullable Type getterType(Method method) {
if(method.getGenericParameterTypes().length == 0 && method.getGenericReturnType() != Void.TYPE) {
return method.getGenericReturnType();
}
return null;
}
private static @Nullable Type setterType(Method method) {
if(method.getGenericParameterTypes().length == 1) {
return method.getGenericParameterTypes()[0];
}
return null;
}
private void registerMethod(Map<String, Getter> getters, Map<String, Setter> setters, Method method) {
if(Modifier.isStatic(method.getModifiers())) return;
final String name = serializedName(method);
boolean accessor = false;
if(getterType(method) != null) {
accessor = true;
if(!getters.containsKey(name)) {
if(logger.isLoggable(Level.FINE)) {
logger.fine(" " + name + " -- get --> " + method);
}
getters.put(name, new GetterMethod(this, method));
}
}
if(setterType(method) != null) {
accessor = true;
if(!setters.containsKey(name)) {
if(logger.isLoggable(Level.FINE)) {
logger.fine(" " + name + " -- set --> " + method);
}
setters.put(name, new SetterMethod(this, method));
}
}
if(!accessor) {
throw new SerializationException("Serialized method " + method + " is not a valid getter or setter");
}
}
private void registerField(Map<String, Getter> getters, Map<String, Setter> setters, Field field) {
if(Modifier.isTransient(field.getModifiers()) ||
Modifier.isStatic(field.getModifiers()) ||
field.isSynthetic() ||
field.isEnumConstant()) return;
final String name = serializedName(field);
final boolean gettable = !getters.containsKey(name);
final boolean settable = !setters.containsKey(name);
if(gettable || settable) {
if(logger.isLoggable(Level.FINE)) {
String access;
if(gettable && settable) {
access = "get/set";
} else if(gettable) {
access = "get";
} else {
access = "set";
}
logger.fine(" " + name + " -- " + access + " --> " + field);
}
if(gettable) {
getters.put(name, new FieldGetter(this, field));
}
if(settable) {
setters.put(name, new FieldSetter(this, field));
}
}
}
// TODO: This could could be done in a more general way i.e. search
// the registry for the best existing implementation to inherit from.
public Class<? extends Document> bestBaseClass(Class<? extends Document> type) {
if(PlayerId.class.isAssignableFrom(type)) {
return SimplePlayerId.class;
} else if(UserId.class.isAssignableFrom(type)) {
return SimpleUserId.class;
} else if(DeletableModel.class.isAssignableFrom(type)) {
return BasicDeletableModel.class;
} else if(Model.class.isAssignableFrom(type)) {
return BasicModel.class;
} else {
return BasicDocument.class;
}
}
}