package tc.oc.api.message;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.reflect.TypeToken;
import org.apache.commons.collections.map.HashedMap;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.document.DocumentRegistry;
import tc.oc.api.exceptions.SerializationException;
import tc.oc.api.message.types.ModelMessage;
import tc.oc.api.model.ModelRegistry;
import tc.oc.api.model.NoSuchModelException;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.reflect.InheritablePropertyVisitor;
import tc.oc.commons.core.reflect.Types;
@Singleton
public class MessageRegistry {
protected final Logger logger;
protected final DocumentRegistry documentRegistry;
protected final ModelRegistry modelRegistry;
private final Map<String, MessageMeta<?>> byName = new HashedMap();
private final LoadingCache<Class<? extends Message>, MessageMeta> byType = CacheBuilder.newBuilder().build(
new CacheLoader<Class<? extends Message>, MessageMeta>() {
@Override
public MessageMeta load(Class<? extends Message> type) throws Exception {
// Registered types are explicitly inserted into the cache,
// so a cache miss means the type itself is not a registered
// message, and we need to look for an ancestor that is.
return findAncestorMeta(type);
}
}
);
@Inject MessageRegistry(Loggers loggers, Set<MessageMeta<?>> messages, DocumentRegistry documentRegistry, ModelRegistry modelRegistry) {
this.modelRegistry = modelRegistry;
this.logger = loggers.get(getClass());
this.documentRegistry = documentRegistry;
messages.forEach(meta -> {
if(ModelMessage.class.isAssignableFrom(meta.type())) {
if(meta.type().getTypeParameters().length > 1) {
throw new SerializationException(ModelMessage.class.getSimpleName() + " subtype must have no more than one type parameter");
}
}
if(byName.containsKey(meta.name())) {
throw new SerializationException("Tried to register multiple message types for name " + meta.name());
}
byName.put(meta.name(), meta);
byType.put(meta.type(), meta);
});
}
public TypeToken<? extends Message> resolve(String name) {
return resolve(name, Optional.empty());
}
public TypeToken<? extends Message> resolve(String name, Optional<String> modelName) {
final MessageMeta<?> meta = byName.get(name);
if(meta == null) {
throw new NoSuchMessageException("No registered message type named " + name);
}
TypeToken token = TypeToken.of(meta.type());
if(ModelMessage.class.isAssignableFrom(meta.type()) && modelName.isPresent()) {
try {
token = modelMessageType(token, modelRegistry.resolve(modelName.get()).completeType());
} catch(NoSuchModelException e) {
throw new NoSuchMessageException(e.getMessage());
}
}
return token;
}
private static <M extends Model, N extends ModelMessage<M>> TypeToken<N> modelMessageType(TypeToken<N> messageType, TypeToken<M> modelType) {
if(messageType.getRawType().getTypeParameters().length == 0) {
return messageType;
} else {
return (TypeToken<N>) TypeToken.of(new ParameterizedType() {
@Override
public Type[] getActualTypeArguments() {
return new Type[]{ modelType.getType() };
}
@Override
public Type getRawType() {
return messageType.getRawType();
}
@Override
public Type getOwnerType() {
return messageType.getRawType().getEnclosingClass();
}
});
}
}
public String typeName(Class<? extends Message> type) {
return getMeta(type).name();
}
private <T extends Message> MessageMeta<? super T> getMeta(Class<T> type) {
return byType.getUnchecked(type);
}
private <T extends Message> MessageMeta<? super T> findAncestorMeta(final Class<T> type) {
// We don't want to trigger any cache loads, we just want to search what is
// already in the cache, which is exactly what this map view does.
final Map<Class<? extends Message>, MessageMeta> byTypeMap = byType.asMap();
Map<Class<?>, MessageMeta> metas = Types.walkAncestors(
type,
Types.assignableTo(Message.class),
new InheritablePropertyVisitor<>(new Function<Class<?>, MessageMeta>() {
@Override
public @Nullable MessageMeta apply(Class<?> cls) {
if(type == cls) return null;
return byTypeMap.get(cls);
}
})
).values();
switch(metas.size()) {
case 0: throw new SerializationException("No name found for message type " + type.getName());
case 1: return metas.values().iterator().next();
default: throw new SerializationException("Ambiguous name for message type " + type.getName() + ": could be any of " + Joiner.on(", ").join(metas.values()));
}
}
private boolean isInstantiable(Class<? extends Message> type) {
if(
type.isAnonymousClass() ||
type.isLocalClass() ||
type.isMemberClass() ||
type.isSynthetic() ||
Modifier.isAbstract(type.getModifiers())
) return false;
try {
type.getDeclaredConstructor().setAccessible(true);
} catch(NoSuchMethodException e) {
return false;
}
return true;
}
}