001/*
002 * Copyright (C) 2012 Facebook, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may
005 * not use this file except in compliance with the License. You may obtain
006 * a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations
014 * under the License.
015 */
016package com.facebook.swift.codec.metadata;
017
018import com.facebook.swift.codec.ThriftDocumentation;
019import com.facebook.swift.codec.ThriftOrder;
020import com.facebook.swift.codec.ThriftStruct;
021import com.facebook.swift.codec.ThriftUnion;
022import com.facebook.swift.codec.internal.coercion.DefaultJavaCoercions;
023import com.facebook.swift.codec.internal.coercion.FromThrift;
024import com.facebook.swift.codec.internal.coercion.ToThrift;
025import com.facebook.swift.codec.metadata.MetadataErrors.Monitor;
026import com.google.common.annotations.VisibleForTesting;
027import com.google.common.base.Function;
028import com.google.common.base.Joiner;
029import com.google.common.base.Preconditions;
030import com.google.common.collect.ImmutableList;
031import com.google.common.collect.Sets;
032import com.google.common.reflect.TypeToken;
033import com.google.common.util.concurrent.ListenableFuture;
034
035import javax.annotation.concurrent.ThreadSafe;
036
037import java.lang.reflect.Field;
038import java.lang.reflect.Method;
039import java.lang.reflect.Type;
040import java.nio.ByteBuffer;
041import java.util.ArrayDeque;
042import java.util.Deque;
043import java.util.HashMap;
044import java.util.Map;
045import java.util.Set;
046import java.util.concurrent.ConcurrentHashMap;
047import java.util.concurrent.ConcurrentMap;
048
049import static com.facebook.swift.codec.metadata.ReflectionHelper.getFutureReturnType;
050import static com.facebook.swift.codec.metadata.ReflectionHelper.getIterableType;
051import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapKeyType;
052import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapValueType;
053import static com.facebook.swift.codec.metadata.ThriftType.BINARY;
054import static com.facebook.swift.codec.metadata.ThriftType.BOOL;
055import static com.facebook.swift.codec.metadata.ThriftType.BYTE;
056import static com.facebook.swift.codec.metadata.ThriftType.DOUBLE;
057import static com.facebook.swift.codec.metadata.ThriftType.I16;
058import static com.facebook.swift.codec.metadata.ThriftType.I32;
059import static com.facebook.swift.codec.metadata.ThriftType.I64;
060import static com.facebook.swift.codec.metadata.ThriftType.STRING;
061import static com.facebook.swift.codec.metadata.ThriftType.VOID;
062import static com.facebook.swift.codec.metadata.ThriftType.array;
063import static com.facebook.swift.codec.metadata.ThriftType.enumType;
064import static com.facebook.swift.codec.metadata.ThriftType.list;
065import static com.facebook.swift.codec.metadata.ThriftType.map;
066import static com.facebook.swift.codec.metadata.ThriftType.set;
067import static com.facebook.swift.codec.metadata.ThriftType.struct;
068import static com.google.common.base.Preconditions.checkState;
069import static com.google.common.collect.Iterables.concat;
070import static com.google.common.collect.Iterables.transform;
071
072import static java.lang.reflect.Modifier.isStatic;
073
074/**
075 * ThriftCatalog contains the metadata for all known structs, enums and type coercions.  Since,
076 * metadata extraction can be very expensive, and only single instance of the catalog should be
077 * created.
078 */
079@ThreadSafe
080public class ThriftCatalog
081{
082    private final MetadataErrors.Monitor monitor;
083    private final ConcurrentMap<Type, ThriftStructMetadata> structs = new ConcurrentHashMap<>();
084    private final ConcurrentMap<Class<?>, ThriftEnumMetadata<?>> enums = new ConcurrentHashMap<>();
085    private final ConcurrentMap<Type, TypeCoercion> coercions = new ConcurrentHashMap<>();
086    private final ConcurrentMap<Class<?>, ThriftType> manualTypes = new ConcurrentHashMap<>();
087
088    private final ThreadLocal<Deque<Type>> stack = new ThreadLocal<Deque<Type>>()
089    {
090        @Override
091        protected Deque<Type> initialValue()
092        {
093            return new ArrayDeque<>();
094        }
095    };
096
097    public ThriftCatalog()
098    {
099        this(MetadataErrors.NULL_MONITOR);
100    }
101
102    @VisibleForTesting
103    public ThriftCatalog(Monitor monitor)
104    {
105        this.monitor = monitor;
106        addDefaultCoercions(DefaultJavaCoercions.class);
107    }
108
109    @VisibleForTesting
110    Monitor getMonitor()
111    {
112        return monitor;
113    }
114
115    public void addThriftType(ThriftType thriftType)
116    {
117        manualTypes.put(TypeToken.of(thriftType.getJavaType()).getRawType(), thriftType);
118    }
119
120    /**
121     * Add the @ToThrift and @FromThrift coercions in the specified class to this catalog.  All
122     * coercions must be symmetrical, so ever @ToThrift method must have a corresponding @FromThrift
123     * method.
124     */
125    public void addDefaultCoercions(Class<?> coercionsClass)
126    {
127        Preconditions.checkNotNull(coercionsClass, "coercionsClass is null");
128        Map<ThriftType, Method> toThriftCoercions = new HashMap<>();
129        Map<ThriftType, Method> fromThriftCoercions = new HashMap<>();
130        for (Method method : coercionsClass.getDeclaredMethods()) {
131            if (method.isAnnotationPresent(ToThrift.class)) {
132                verifyCoercionMethod(method);
133                ThriftType thriftType = getThriftType(method.getGenericReturnType());
134                ThriftType coercedType = thriftType.coerceTo(method.getGenericParameterTypes()[0]);
135
136                Method oldValue = toThriftCoercions.put(coercedType, method);
137                Preconditions.checkArgument(
138                        oldValue == null,
139                        "Coercion class two @ToThrift methods (%s and %s) for type %s",
140                        coercionsClass.getName(),
141                        method,
142                        oldValue,
143                        coercedType);
144            }
145            else if (method.isAnnotationPresent(FromThrift.class)) {
146                verifyCoercionMethod(method);
147                ThriftType thriftType = getThriftType(method.getGenericParameterTypes()[0]);
148                ThriftType coercedType = thriftType.coerceTo(method.getGenericReturnType());
149
150                Method oldValue = fromThriftCoercions.put(coercedType, method);
151                Preconditions.checkArgument(
152                        oldValue == null,
153                        "Coercion class two @FromThrift methods (%s and %s) for type %s",
154                        coercionsClass.getName(),
155                        method,
156                        oldValue,
157                        coercedType);
158            }
159        }
160
161        // assure coercions are symmetric
162        Set<ThriftType> difference = Sets.symmetricDifference(toThriftCoercions.keySet(), fromThriftCoercions.keySet());
163        Preconditions.checkArgument(
164                difference.isEmpty(),
165                "Coercion class %s does not have matched @ToThrift and @FromThrift methods for types %s",
166                coercionsClass.getName(),
167                difference);
168
169        // add the coercions
170        Map<Type, TypeCoercion> coercions = new HashMap<>();
171        for (Map.Entry<ThriftType, Method> entry : toThriftCoercions.entrySet()) {
172            ThriftType type = entry.getKey();
173            Method toThriftMethod = entry.getValue();
174            Method fromThriftMethod = fromThriftCoercions.get(type);
175            // this should never happen due to the difference check above, but be careful
176            Preconditions.checkState(
177                    fromThriftMethod != null,
178                    "Coercion class %s does not have matched @ToThrift and @FromThrift methods for type %s",
179                    coercionsClass.getName(),
180                    type);
181            TypeCoercion coercion = new TypeCoercion(type, toThriftMethod, fromThriftMethod);
182            coercions.put(type.getJavaType(), coercion);
183        }
184        this.coercions.putAll(coercions);
185    }
186
187    private void verifyCoercionMethod(Method method)
188    {
189        Preconditions.checkArgument(isStatic(method.getModifiers()), "Method %s is not static", method.toGenericString());
190        Preconditions.checkArgument(method.getParameterTypes().length == 1, "Method %s must have exactly one parameter", method.toGenericString());
191        Preconditions.checkArgument(method.getReturnType() != void.class, "Method %s must have a return value", method.toGenericString());
192    }
193
194    /**
195     * Gets the default TypeCoercion (and associated ThriftType) for the specified Java type.
196     */
197    public TypeCoercion getDefaultCoercion(Type type)
198    {
199        return coercions.get(type);
200    }
201
202    /**
203     * Gets the ThriftType for the specified Java type.  The native Thrift type for the Java type will
204     * be inferred from the Java type, and if necessary type coercions will be applied.
205     *
206     * @return the ThriftType for the specified java type; never null
207     * @throws IllegalArgumentException if the Java Type can not be coerced to a ThriftType
208     */
209    public ThriftType getThriftType(Type javaType)
210            throws IllegalArgumentException
211    {
212        Class<?> rawType = TypeToken.of(javaType).getRawType();
213        ThriftType manualType = manualTypes.get(rawType);
214        if (manualType != null) {
215            return manualType;
216        }
217        if (boolean.class == rawType) {
218            return BOOL;
219        }
220        if (byte.class == rawType) {
221            return BYTE;
222        }
223        if (short.class == rawType) {
224            return I16;
225        }
226        if (int.class == rawType) {
227            return I32;
228        }
229        if (long.class == rawType) {
230            return I64;
231        }
232        if (double.class == rawType) {
233            return DOUBLE;
234        }
235        if (String.class == rawType) {
236            return STRING;
237        }
238        if (ByteBuffer.class.isAssignableFrom(rawType)) {
239            return BINARY;
240        }
241        if (Enum.class.isAssignableFrom(rawType)) {
242            Class<?> enumClass = TypeToken.of(javaType).getRawType();
243            ThriftEnumMetadata<? extends Enum<?>> thriftEnumMetadata = getThriftEnumMetadata(enumClass);
244            return enumType(thriftEnumMetadata);
245        }
246        if (rawType.isArray()) {
247            Class<?> elementType = rawType.getComponentType();
248            if (elementType == byte.class) {
249                // byte[] is encoded as BINARY and requires a coersion
250                return coercions.get(byte[].class).getThriftType();
251            }
252            return array(getThriftType(elementType));
253        }
254        if (Map.class.isAssignableFrom(rawType)) {
255            Type mapKeyType = getMapKeyType(javaType);
256            Type mapValueType = getMapValueType(javaType);
257            return map(getThriftType(mapKeyType), getThriftType(mapValueType));
258        }
259        if (Set.class.isAssignableFrom(rawType)) {
260            Type elementType = getIterableType(javaType);
261            return set(getThriftType(elementType));
262        }
263        if (Iterable.class.isAssignableFrom(rawType)) {
264            Type elementType = getIterableType(javaType);
265            return list(getThriftType(elementType));
266        }
267        // The void type is used by service methods and is encoded as an empty struct
268        if (void.class.isAssignableFrom(rawType) || Void.class.isAssignableFrom(rawType)) {
269            return VOID;
270        }
271        if (rawType.isAnnotationPresent(ThriftStruct.class)) {
272            ThriftStructMetadata structMetadata = getThriftStructMetadata(javaType);
273            return struct(structMetadata);
274        }
275        if (rawType.isAnnotationPresent(ThriftUnion.class)) {
276            ThriftStructMetadata structMetadata = getThriftStructMetadata(javaType);
277            // An union looks like a struct with a single field.
278            return struct(structMetadata);
279        }
280
281        if (ListenableFuture.class.isAssignableFrom(rawType)) {
282            Type returnType = getFutureReturnType(javaType);
283            return getThriftType(returnType);
284        }
285
286        // coerce the type if possible
287        TypeCoercion coercion = coercions.get(javaType);
288        if (coercion != null) {
289            return coercion.getThriftType();
290        }
291        throw new IllegalArgumentException("Type can not be coerced to a Thrift type: " + javaType);
292    }
293
294    public boolean isSupportedStructFieldType(Type javaType)
295    {
296        Class<?> rawType = TypeToken.of(javaType).getRawType();
297        if (boolean.class == rawType) {
298            return true;
299        }
300        if (byte.class == rawType) {
301            return true;
302        }
303        if (short.class == rawType) {
304            return true;
305        }
306        if (int.class == rawType) {
307            return true;
308        }
309        if (long.class == rawType) {
310            return true;
311        }
312        if (double.class == rawType) {
313            return true;
314        }
315        if (String.class == rawType) {
316            return true;
317        }
318        if (ByteBuffer.class.isAssignableFrom(rawType)) {
319            return true;
320        }
321        if (Enum.class.isAssignableFrom(rawType)) {
322            return true;
323        }
324        if (rawType.isArray()) {
325            Class<?> elementType = rawType.getComponentType();
326            return isSupportedArrayComponentType(elementType);
327        }
328        if (Map.class.isAssignableFrom(rawType)) {
329            Type mapKeyType = getMapKeyType(javaType);
330            Type mapValueType = getMapValueType(javaType);
331            return isSupportedStructFieldType(mapKeyType) && isSupportedStructFieldType(mapValueType);
332        }
333        if (Set.class.isAssignableFrom(rawType)) {
334            Type elementType = getIterableType(javaType);
335            return isSupportedStructFieldType(elementType);
336        }
337        if (Iterable.class.isAssignableFrom(rawType)) {
338            Type elementType = getIterableType(javaType);
339            return isSupportedStructFieldType(elementType);
340        }
341        if (rawType.isAnnotationPresent(ThriftStruct.class)) {
342            return true;
343        }
344        if (rawType.isAnnotationPresent(ThriftUnion.class)) {
345            return true;
346        }
347
348        // NOTE: void is not a supported struct type
349
350        // coerce the type if possible
351        TypeCoercion coercion = coercions.get(javaType);
352        if (coercion != null) {
353            return true;
354        }
355        return false;
356    }
357
358    public boolean isSupportedArrayComponentType(Class<?> componentType)
359    {
360        return boolean.class == componentType ||
361                byte.class == componentType ||
362                short.class == componentType ||
363                int.class == componentType ||
364                long.class == componentType ||
365                double.class == componentType;
366    }
367
368    /**
369     * Gets the ThriftEnumMetadata for the specified enum class.  If the enum class contains a method
370     * annotated with @ThriftEnumValue, the value of this method will be used for the encoded thrift
371     * value; otherwise the Enum.ordinal() method will be used.
372     */
373    public <T extends Enum<T>> ThriftEnumMetadata<?> getThriftEnumMetadata(Class<?> enumClass)
374    {
375        ThriftEnumMetadata<?> enumMetadata = enums.get(enumClass);
376        if (enumMetadata == null) {
377            enumMetadata = new ThriftEnumMetadataBuilder<>((Class<T>) enumClass).build();
378
379            ThriftEnumMetadata<?> current = enums.putIfAbsent(enumClass, enumMetadata);
380            if (current != null) {
381                enumMetadata = current;
382            }
383        }
384        return enumMetadata;
385    }
386
387    /**
388     * Gets the ThriftStructMetadata for the specified struct class.  The struct class must be
389     * annotated with @ThriftStruct or @ThriftUnion.
390     */
391    public <T> ThriftStructMetadata getThriftStructMetadata(Type structType)
392    {
393        ThriftStructMetadata structMetadata = structs.get(structType);
394        Class<?> structClass = TypeToken.of(structType).getRawType();
395        if (structMetadata == null) {
396            if (structClass.isAnnotationPresent(ThriftStruct.class)) {
397                structMetadata = extractThriftStructMetadata(structType);
398            }
399            else if (structClass.isAnnotationPresent(ThriftUnion.class)) {
400                structMetadata = extractThriftUnionMetadata(structType);
401            }
402            else {
403                throw new IllegalStateException("getThriftStructMetadata called on a class that has no @ThriftStruct or @ThriftUnion annotation");
404            }
405
406            ThriftStructMetadata current = structs.putIfAbsent(structType, structMetadata);
407            if (current != null) {
408                structMetadata = current;
409            }
410        }
411        return structMetadata;
412    }
413
414
415    private static Class<?> getSwiftMetaClassOf(Class<?> cls) throws ClassNotFoundException
416    {
417        ClassLoader loader = cls.getClassLoader();
418        if (loader == null) {
419            throw new ClassNotFoundException("null class loader");
420        }
421        return loader.loadClass(cls.getName() + "$swift_meta");
422    }
423
424    @SuppressWarnings("PMD.EmptyCatchBlock")
425    public static ImmutableList<String> getThriftDocumentation(Class<?> objectClass)
426    {
427        ThriftDocumentation documentation = objectClass.getAnnotation(ThriftDocumentation.class);
428
429        if (documentation == null) {
430            try {
431                Class<?> swiftDocsClass = getSwiftMetaClassOf(objectClass);
432
433                documentation = swiftDocsClass.getAnnotation(ThriftDocumentation.class);
434            }
435            catch (ClassNotFoundException e) {
436                // ignored
437            }
438        }
439
440        return documentation == null ? ImmutableList.<String>of() : ImmutableList.copyOf(documentation.value());
441    }
442
443    @SuppressWarnings("PMD.EmptyCatchBlock")
444    public static ImmutableList<String> getThriftDocumentation(Method method)
445    {
446        ThriftDocumentation documentation = method.getAnnotation(ThriftDocumentation.class);
447
448        if (documentation == null) {
449            try {
450                Class<?> swiftDocsClass = getSwiftMetaClassOf(method.getDeclaringClass());
451
452                documentation = swiftDocsClass.getDeclaredMethod(method.getName()).getAnnotation(ThriftDocumentation.class);
453            }
454            catch (ReflectiveOperationException e) {
455                // ignored
456            }
457        }
458
459        return documentation == null ? ImmutableList.<String>of() : ImmutableList.copyOf(documentation.value());
460    }
461
462    @SuppressWarnings("PMD.EmptyCatchBlock")
463    public static ImmutableList<String> getThriftDocumentation(Field field)
464    {
465        ThriftDocumentation documentation = field.getAnnotation(ThriftDocumentation.class);
466
467        if (documentation == null) {
468            try {
469                Class<?> swiftDocsClass = getSwiftMetaClassOf(field.getDeclaringClass());
470
471                documentation = swiftDocsClass.getDeclaredField(field.getName()).getAnnotation(ThriftDocumentation.class);
472            }
473            catch (ReflectiveOperationException e) {
474                // ignored
475            }
476        }
477
478        return documentation == null ? ImmutableList.<String>of() : ImmutableList.copyOf(documentation.value());
479    }
480
481    @SuppressWarnings("PMD.EmptyCatchBlock")
482    public static <T extends Enum<T>> ImmutableList<String> getThriftDocumentation(Enum<T> enumConstant)
483    {
484        try {
485            Field f = enumConstant.getDeclaringClass().getField(enumConstant.name());
486            return getThriftDocumentation(f);
487        } catch (ReflectiveOperationException e) {
488            // ignore
489        }
490        return ImmutableList.<String>of();
491    }
492
493    @SuppressWarnings("PMD.EmptyCatchBlock")
494    public static Integer getMethodOrder(Method method)
495    {
496        ThriftOrder order = method.getAnnotation(ThriftOrder.class);
497
498        if (order == null) {
499            try {
500                Class<?> swiftDocsClass = getSwiftMetaClassOf(method.getDeclaringClass());
501
502                order = swiftDocsClass.getDeclaredMethod(method.getName()).getAnnotation(ThriftOrder.class);
503            }
504            catch (ReflectiveOperationException e) {
505                // ignored
506            }
507        }
508
509        return order == null ? null : order.value();
510    }
511
512    private ThriftStructMetadata extractThriftStructMetadata(Type structType)
513    {
514        Preconditions.checkNotNull(structType, "structType is null");
515
516        Deque<Type> stack = this.stack.get();
517        if (stack.contains(structType)) {
518            String path = Joiner.on("->").join(transform(concat(stack, ImmutableList.of(structType)), new Function<Type, Object>()
519            {
520                @Override
521                public Object apply(Type input)
522                {
523                    return TypeToken.of(input).getRawType().getName();
524                }
525            }));
526            throw new IllegalArgumentException("Circular references are not allowed: " + path);
527        }
528
529        stack.push(structType);
530        try {
531            ThriftStructMetadataBuilder builder = new ThriftStructMetadataBuilder(this, structType);
532            ThriftStructMetadata structMetadata = builder.build();
533            return structMetadata;
534        }
535        finally {
536            Type top = stack.pop();
537            checkState(structType.equals(top),
538                    "ThriftCatalog circularity detection stack is corrupt: expected %s, but got %s",
539                    structType,
540                    top);
541        }
542    }
543
544    private ThriftStructMetadata extractThriftUnionMetadata(Type unionType)
545    {
546        Preconditions.checkNotNull(unionType, "unionType is null");
547
548        Deque<Type> stack = this.stack.get();
549        if (stack.contains(unionType)) {
550            String path = Joiner.on("->").join(transform(concat(stack, ImmutableList.of(unionType)), new Function<Type, Object>()
551            {
552                @Override
553                public Object apply(Type input)
554                {
555                    return TypeToken.of(input).getRawType().getName();
556                }
557            }));
558            throw new IllegalArgumentException("Circular references are not allowed: " + path);
559        }
560
561        stack.push(unionType);
562        try {
563            ThriftUnionMetadataBuilder builder = new ThriftUnionMetadataBuilder(this, unionType);
564            ThriftStructMetadata unionMetadata = builder.build();
565            return unionMetadata;
566        }
567        finally {
568            Type top = stack.pop();
569            checkState(unionType.equals(top),
570                    "ThriftCatalog circularity detection stack is corrupt: expected %s, but got %s",
571                    unionType,
572                    top);
573        }
574    }
575
576}