ThriftCatalog.java
/*
* Copyright (C) 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.swift.codec.metadata;
import com.facebook.swift.codec.ThriftStruct;
import com.facebook.swift.codec.internal.coercion.DefaultJavaCoercions;
import com.facebook.swift.codec.internal.coercion.FromThrift;
import com.facebook.swift.codec.internal.coercion.ToThrift;
import com.facebook.swift.codec.metadata.MetadataErrors.Monitor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.concurrent.ThreadSafe;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getFutureReturnType;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getIterableType;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapKeyType;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapValueType;
import static com.facebook.swift.codec.metadata.ThriftType.BOOL;
import static com.facebook.swift.codec.metadata.ThriftType.BYTE;
import static com.facebook.swift.codec.metadata.ThriftType.DOUBLE;
import static com.facebook.swift.codec.metadata.ThriftType.I16;
import static com.facebook.swift.codec.metadata.ThriftType.I32;
import static com.facebook.swift.codec.metadata.ThriftType.I64;
import static com.facebook.swift.codec.metadata.ThriftType.STRING;
import static com.facebook.swift.codec.metadata.ThriftType.VOID;
import static com.facebook.swift.codec.metadata.ThriftType.enumType;
import static com.facebook.swift.codec.metadata.ThriftType.list;
import static com.facebook.swift.codec.metadata.ThriftType.map;
import static com.facebook.swift.codec.metadata.ThriftType.set;
import static com.facebook.swift.codec.metadata.ThriftType.struct;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.transform;
import static java.lang.reflect.Modifier.isStatic;
/**
* ThriftCatalog contains the metadata for all known structs, enums and type coercions. Since,
* metadata extraction can be very expensive, and only single instance of the catalog should be
* created.
*/
@ThreadSafe
public class ThriftCatalog
{
private final MetadataErrors.Monitor monitor;
private final ConcurrentMap<Class<?>, ThriftStructMetadata<?>> structs = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, ThriftEnumMetadata<?>> enums = new ConcurrentHashMap<>();
private final ConcurrentMap<Type, TypeCoercion> coercions = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, ThriftType> manualTypes = new ConcurrentHashMap<>();
private final ThreadLocal<Deque<Class<?>>> stack = new ThreadLocal<Deque<Class<?>>>()
{
@Override
protected Deque<Class<?>> initialValue()
{
return new ArrayDeque<>();
}
};
public ThriftCatalog()
{
this(MetadataErrors.NULL_MONITOR);
}
@VisibleForTesting
public ThriftCatalog(Monitor monitor)
{
this.monitor = monitor;
addDefaultCoercions(DefaultJavaCoercions.class);
}
@VisibleForTesting
Monitor getMonitor()
{
return monitor;
}
public void addThriftType(ThriftType thriftType)
{
manualTypes.put(TypeToken.of(thriftType.getJavaType()).getRawType(), thriftType);
}
/**
* Add the @ToThrift and @FromThrift coercions in the specified class to this catalog. All
* coercions must be symmetrical, so ever @ToThrift method must have a corresponding @FromThrift
* method.
*/
public void addDefaultCoercions(Class<?> coercionsClass)
{
Preconditions.checkNotNull(coercionsClass, "coercionsClass is null");
Map<ThriftType, Method> toThriftCoercions = new HashMap<>();
Map<ThriftType, Method> fromThriftCoercions = new HashMap<>();
for (Method method : coercionsClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(ToThrift.class)) {
verifyCoercionMethod(method);
ThriftType thriftType = getThriftType(method.getGenericReturnType());
ThriftType coercedType = thriftType.coerceTo(method.getGenericParameterTypes()[0]);
Method oldValue = toThriftCoercions.put(coercedType, method);
Preconditions.checkArgument(
oldValue == null,
"Coercion class two @ToThrift methods (%s and %s) for type %s",
coercionsClass.getName(),
method,
oldValue,
coercedType);
}
else if (method.isAnnotationPresent(FromThrift.class)) {
verifyCoercionMethod(method);
ThriftType thriftType = getThriftType(method.getGenericParameterTypes()[0]);
ThriftType coercedType = thriftType.coerceTo(method.getGenericReturnType());
Method oldValue = fromThriftCoercions.put(coercedType, method);
Preconditions.checkArgument(
oldValue == null,
"Coercion class two @FromThrift methods (%s and %s) for type %s",
coercionsClass.getName(),
method,
oldValue,
coercedType);
}
}
// assure coercions are symmetric
Set<ThriftType> difference = Sets.symmetricDifference(toThriftCoercions.keySet(), fromThriftCoercions.keySet());
Preconditions.checkArgument(
difference.isEmpty(),
"Coercion class %s does not have matched @ToThrift and @FromThrift methods for types %s",
coercionsClass.getName(),
difference);
// add the coercions
Map<Type, TypeCoercion> coercions = new HashMap<>();
for (Map.Entry<ThriftType, Method> entry : toThriftCoercions.entrySet()) {
ThriftType type = entry.getKey();
Method toThriftMethod = entry.getValue();
Method fromThriftMethod = fromThriftCoercions.get(type);
// this should never happen due to the difference check above, but be careful
Preconditions.checkState(
fromThriftMethod != null,
"Coercion class %s does not have matched @ToThrift and @FromThrift methods for type %s",
coercionsClass.getName(),
type);
TypeCoercion coercion = new TypeCoercion(type, toThriftMethod, fromThriftMethod);
coercions.put(type.getJavaType(), coercion);
}
this.coercions.putAll(coercions);
}
private void verifyCoercionMethod(Method method)
{
Preconditions.checkArgument(isStatic(method.getModifiers()), "Method %s is not static", method.toGenericString());
Preconditions.checkArgument(method.getParameterTypes().length == 1, "Method %s must have exactly one parameter", method.toGenericString());
Preconditions.checkArgument(method.getReturnType() != void.class, "Method %s must have a return value", method.toGenericString());
}
/**
* Gets the default TypeCoercion (and associated ThriftType) for the specified Java type.
*/
public TypeCoercion getDefaultCoercion(Type type)
{
return coercions.get(type);
}
/**
* Gets the ThriftType for the specified Java type. The native Thrift type for the Java type will
* be inferred from the Java type, and if necessary type coercions will be applied.
*
* @return the ThriftType for the specified java type; never null
* @throws IllegalArgumentException if the Java Type can not be coerced to a ThriftType
*/
public ThriftType getThriftType(Type javaType)
throws IllegalArgumentException
{
Class<?> rawType = TypeToken.of(javaType).getRawType();
ThriftType manualType = manualTypes.get(rawType);
if (manualType != null) {
return manualType;
}
if (boolean.class == rawType) {
return BOOL;
}
if (byte.class == rawType) {
return BYTE;
}
if (short.class == rawType) {
return I16;
}
if (int.class == rawType) {
return I32;
}
if (long.class == rawType) {
return I64;
}
if (double.class == rawType) {
return DOUBLE;
}
if (ByteBuffer.class.isAssignableFrom(rawType)) {
return STRING;
}
if (Enum.class.isAssignableFrom(rawType)) {
Class<?> enumClass = TypeToken.of(javaType).getRawType();
ThriftEnumMetadata<? extends Enum<?>> thriftEnumMetadata = getThriftEnumMetadata(enumClass);
return enumType(thriftEnumMetadata);
}
if (Map.class.isAssignableFrom(rawType)) {
Type mapKeyType = getMapKeyType(javaType);
Type mapValueType = getMapValueType(javaType);
return map(getThriftType(mapKeyType), getThriftType(mapValueType));
}
if (Set.class.isAssignableFrom(rawType)) {
Type elementType = getIterableType(javaType);
return set(getThriftType(elementType));
}
if (Iterable.class.isAssignableFrom(rawType)) {
Type elementType = getIterableType(javaType);
return list(getThriftType(elementType));
}
// The void type is used by service methods and is encoded as an empty struct
if (void.class.isAssignableFrom(rawType) || Void.class.isAssignableFrom(rawType)) {
return VOID;
}
if (rawType.isAnnotationPresent(ThriftStruct.class)) {
ThriftStructMetadata<?> structMetadata = getThriftStructMetadata(rawType);
return struct(structMetadata);
}
if (ListenableFuture.class.isAssignableFrom(rawType)) {
Type returnType = getFutureReturnType(javaType);
return getThriftType(returnType);
}
// coerce the type if possible
TypeCoercion coercion = coercions.get(javaType);
if (coercion != null) {
return coercion.getThriftType();
}
throw new IllegalArgumentException("Type can not be coerced to a Thrift type: " + javaType);
}
public boolean isSupportedStructFieldType(Type javaType)
{
Class<?> rawType = TypeToken.of(javaType).getRawType();
if (boolean.class == rawType) {
return true;
}
if (byte.class == rawType) {
return true;
}
if (short.class == rawType) {
return true;
}
if (int.class == rawType) {
return true;
}
if (long.class == rawType) {
return true;
}
if (double.class == rawType) {
return true;
}
if (ByteBuffer.class.isAssignableFrom(rawType)) {
return true;
}
if (Enum.class.isAssignableFrom(rawType)) {
return true;
}
if (Map.class.isAssignableFrom(rawType)) {
Type mapKeyType = getMapKeyType(javaType);
Type mapValueType = getMapValueType(javaType);
return isSupportedStructFieldType(mapKeyType) && isSupportedStructFieldType(mapValueType);
}
if (Set.class.isAssignableFrom(rawType)) {
Type elementType = getIterableType(javaType);
return isSupportedStructFieldType(elementType);
}
if (Iterable.class.isAssignableFrom(rawType)) {
Type elementType = getIterableType(javaType);
return isSupportedStructFieldType(elementType);
}
if (rawType.isAnnotationPresent(ThriftStruct.class)) {
return true;
}
// NOTE: void is not a supported struct type
// coerce the type if possible
TypeCoercion coercion = coercions.get(javaType);
if (coercion != null) {
return true;
}
return false;
}
/**
* Gets the ThriftEnumMetadata for the specified enum class. If the enum class contains a method
* annotated with @ThriftEnumValue, the value of this method will be used for the encoded thrift
* value; otherwise the Enum.ordinal() method will be used.
*/
public <T extends Enum<T>> ThriftEnumMetadata<?> getThriftEnumMetadata(Class<?> enumClass)
{
ThriftEnumMetadata<?> enumMetadata = enums.get(enumClass);
if (enumMetadata == null) {
enumMetadata = new ThriftEnumMetadata<>((Class<T>) enumClass);
ThriftEnumMetadata<?> current = enums.putIfAbsent(enumClass, enumMetadata);
if (current != null) {
enumMetadata = current;
}
}
return enumMetadata;
}
/**
* Gets the ThriftStructMetadata for the specified struct class. The struct class must be
* annotated with @ThriftStruct.
*/
public <T> ThriftStructMetadata<T> getThriftStructMetadata(Class<T> structClass)
{
ThriftStructMetadata<?> structMetadata = structs.get(structClass);
if (structMetadata == null) {
structMetadata = extractThriftStructMetadata(structClass);
ThriftStructMetadata<?> current = structs.putIfAbsent(structClass, structMetadata);
if (current != null) {
structMetadata = current;
}
}
return (ThriftStructMetadata<T>) structMetadata;
}
private <T> ThriftStructMetadata<T> extractThriftStructMetadata(Class<T> structClass)
{
Preconditions.checkNotNull(structClass, "structClass is null");
Deque<Class<?>> stack = this.stack.get();
if (stack.contains(structClass)) {
String path = Joiner.on("->").join(transform(concat(stack, ImmutableList.of(structClass)), new Function<Class<?>, Object>()
{
@Override
public Object apply(Class<?> input)
{
return input.getName();
}
}));
throw new IllegalArgumentException("Circular references are not allowed: " + path);
}
stack.push(structClass);
try {
ThriftStructMetadataBuilder<T> builder = new ThriftStructMetadataBuilder<>(this, structClass);
ThriftStructMetadata<T> structMetadata = builder.build();
return structMetadata;
}
finally {
Class<?> top = stack.pop();
checkState(structClass.equals(top),
"ThriftCatalog circularity detection stack is corrupt: expected %s, but got %s",
structClass,
top);
}
}
}