272 lines
10 KiB
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;
|
|
}
|
|
}
|
|
}
|