com.facebook.swift.codec.metadata.ThriftStructMetadataBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.swift.codec.metadata.ThriftStructMetadataBuilder.java

Source

/*
 * 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.ThriftConstructor;
import com.facebook.swift.codec.ThriftField;
import com.facebook.swift.codec.ThriftStruct;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.reflect.TypeToken;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import static com.facebook.swift.codec.metadata.ReflectionHelper.extractParameterNames;
import static com.facebook.swift.codec.metadata.ReflectionHelper.findAnnotatedMethods;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getAllDeclaredFields;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getAllDeclaredMethods;
import static com.facebook.swift.codec.metadata.ThriftStructMetadataBuilder.FieldMetadata.extractThriftFieldName;
import static com.facebook.swift.codec.metadata.ThriftStructMetadataBuilder.FieldMetadata.getOrExtractThriftFieldName;
import static com.facebook.swift.codec.metadata.ThriftStructMetadataBuilder.FieldMetadata.getThriftFieldId;
import static com.facebook.swift.codec.metadata.ThriftStructMetadataBuilder.FieldMetadata.getThriftFieldName;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static com.google.common.collect.Sets.newTreeSet;
import static java.util.Arrays.asList;

@NotThreadSafe
public class ThriftStructMetadataBuilder<T> {
    private final String structName;
    private final Class<T> structClass;
    private final Class<?> builderClass;

    private final List<FieldMetadata> fields = newArrayList();

    // readers
    private final List<Extractor> extractors = newArrayList();

    // writers
    private final List<MethodInjection> builderMethodInjections = newArrayList();
    private final List<ConstructorInjection> constructorInjections = newArrayList();
    private final List<FieldInjection> fieldInjections = newArrayList();
    private final List<MethodInjection> methodInjections = newArrayList();

    private final ThriftCatalog catalog;
    private final MetadataErrors metadataErrors;

    public ThriftStructMetadataBuilder(ThriftCatalog catalog, Class<T> structClass) {
        this.catalog = checkNotNull(catalog, "catalog is null");
        this.structClass = checkNotNull(structClass, "structClass is null");
        this.metadataErrors = new MetadataErrors(catalog.getMonitor());

        // verify the class is public and has the correct annotations
        verifyStructClass();

        // assign the struct name from the annotation or from the Java class
        structName = extractStructName();
        // get the builder class from the annotation or from the Java class
        builderClass = extractBuilderClass();
        // extract all of the annotated constructor and report an error if
        // there is more than one or none
        // also extract thrift fields from the annotated parameters and verify
        extractFromConstructors();
        // extract thrift fields from the annotated fields and verify
        extractFromFields();
        // extract thrift fields from the annotated methods (and parameters) and verify
        extractFromMethods();

        // finally normalize the field metadata using things like
        normalizeThriftFields(catalog);
    }

    public MetadataErrors getMetadataErrors() {
        return metadataErrors;
    }

    private void verifyStructClass() {
        // Verify struct class is public and not abstract
        if (Modifier.isAbstract(structClass.getModifiers())) {
            metadataErrors.addError("Struct class [%s] is abstract", structClass.getName());
        }
        if (!Modifier.isPublic(structClass.getModifiers())) {
            metadataErrors.addError("Struct class [%s] is not public", structClass.getName());
        }

        if (!structClass.isAnnotationPresent(ThriftStruct.class)) {
            metadataErrors.addError("Struct class [%s] does not have a @ThriftStruct annotation",
                    structClass.getName());
        }
    }

    private String extractStructName() {
        ThriftStruct annotation = structClass.getAnnotation(ThriftStruct.class);
        if (annotation == null) {
            return structClass.getSimpleName();
        } else if (!annotation.value().isEmpty()) {
            return annotation.value();
        } else {
            return structClass.getSimpleName();
        }
    }

    private Class<?> extractBuilderClass() {
        ThriftStruct annotation = structClass.getAnnotation(ThriftStruct.class);
        if (annotation != null && !annotation.builder().equals(void.class)) {
            return annotation.builder();
        } else {
            return null;
        }
    }

    private void extractFromConstructors() {
        if (builderClass == null) {
            // struct class must have a valid constructor
            addConstructors(structClass);
        } else {
            // builder class must have a valid constructor
            addConstructors(builderClass);

            // builder class must have a build method annotated with @ThriftConstructor
            addBuilderMethods();

            // verify struct class does not have @ThriftConstructors
            for (Constructor<?> constructor : structClass.getConstructors()) {
                if (constructor.isAnnotationPresent(ThriftConstructor.class)) {
                    metadataErrors.addWarning(
                            "Struct class [%s] has a builder class, but constructor %s annotated with @ThriftConstructor",
                            structClass.getName(), constructor);
                }
            }
        }
    }

    private void addConstructors(Class<?> clazz) {
        for (Constructor<?> constructor : clazz.getConstructors()) {
            if (constructor.isSynthetic()) {
                continue;
            }
            if (!constructor.isAnnotationPresent(ThriftConstructor.class)) {
                continue;
            }

            if (!Modifier.isPublic(constructor.getModifiers())) {
                metadataErrors.addError("@ThriftConstructor [%s] is not public", constructor.toGenericString());
                continue;
            }

            List<ParameterInjection> parameters = getParameterInjections(constructor.getParameterAnnotations(),
                    constructor.getGenericParameterTypes(), extractParameterNames(constructor));
            if (parameters != null) {
                fields.addAll(parameters);
                constructorInjections.add(new ConstructorInjection(constructor, parameters));
            }
        }

        // add the default constructor
        if (constructorInjections.isEmpty()) {
            try {
                Constructor<?> constructor = clazz.getDeclaredConstructor();
                if (!Modifier.isPublic(constructor.getModifiers())) {
                    metadataErrors.addError("Default constructor [%s] is not public",
                            constructor.toGenericString());
                }
                constructorInjections.add(new ConstructorInjection(constructor));
            } catch (NoSuchMethodException e) {
                metadataErrors.addError("Struct class [%s] does not have a public no-arg constructor",
                        clazz.getName());
            }
        }

        if (constructorInjections.size() > 1) {
            metadataErrors.addError("Multiple constructors are annotated with @ThriftConstructor ",
                    constructorInjections);
        }
    }

    private void addBuilderMethods() {
        for (Method method : findAnnotatedMethods(builderClass, ThriftConstructor.class)) {
            List<ParameterInjection> parameters = getParameterInjections(method.getParameterAnnotations(),
                    method.getGenericParameterTypes(), extractParameterNames(method));

            // parameters are null if the method is misconfigured
            if (parameters != null) {
                fields.addAll(parameters);
                builderMethodInjections.add(new MethodInjection(method, parameters));
            }
        }

        // find invalid methods not skipped by findAnnotatedMethods()
        for (Method method : getAllDeclaredMethods(builderClass)) {
            if (method.isAnnotationPresent(ThriftConstructor.class) || hasThriftFieldAnnotation(method)) {
                if (!Modifier.isPublic(method.getModifiers())) {
                    metadataErrors.addError("@ThriftConstructor method [%s] is not public",
                            method.toGenericString());
                }
                if (Modifier.isStatic(method.getModifiers())) {
                    metadataErrors.addError("@ThriftConstructor method [%s] is static", method.toGenericString());
                }
            }
        }

        if (builderMethodInjections.isEmpty()) {
            metadataErrors.addError(
                    "Struct builder class [%s] does not have a public builder method annotated with @ThriftConstructor",
                    builderClass.getName());
        }
        if (builderMethodInjections.size() > 1) {
            metadataErrors.addError("Multiple builder methods are annotated with @ThriftConstructor ",
                    builderMethodInjections);
        }
    }

    private void extractFromFields() {
        if (builderClass == null) {
            // struct fields are readable and writable
            addFields(structClass, true, true);
        } else {
            // builder fields are writable
            addFields(builderClass, false, true);
            // struct fields are readable
            addFields(structClass, true, false);
        }
    }

    private void addFields(Class<?> clazz, boolean allowReaders, boolean allowWriters) {
        for (Field fieldField : ReflectionHelper.findAnnotatedFields(clazz, ThriftField.class)) {
            addField(fieldField, allowReaders, allowWriters);
        }

        // find invalid fields not skipped by findAnnotatedFields()
        for (Field field : getAllDeclaredFields(clazz)) {
            if (field.isAnnotationPresent(ThriftField.class)) {
                if (!Modifier.isPublic(field.getModifiers())) {
                    metadataErrors.addError("@ThriftField field [%s] is not public", field.toGenericString());
                }
                if (Modifier.isStatic(field.getModifiers())) {
                    metadataErrors.addError("@ThriftField field [%s] is static", field.toGenericString());
                }
            }
        }
    }

    private void addField(Field fieldField, boolean allowReaders, boolean allowWriters) {
        checkArgument(fieldField.isAnnotationPresent(ThriftField.class));

        ThriftField annotation = fieldField.getAnnotation(ThriftField.class);
        if (allowReaders) {
            FieldExtractor fieldExtractor = new FieldExtractor(fieldField, annotation);
            fields.add(fieldExtractor);
            extractors.add(fieldExtractor);
        }
        if (allowWriters) {
            FieldInjection fieldInjection = new FieldInjection(fieldField, annotation);
            fields.add(fieldInjection);
            fieldInjections.add(fieldInjection);
        }
    }

    private void extractFromMethods() {
        if (builderClass != null) {
            // builder methods are writable
            addMethods(builderClass, false, true);
            // struct methods are readable
            addMethods(structClass, true, false);
        } else {
            // struct methods are readable and writable
            addMethods(structClass, true, true);
        }
    }

    private void addMethods(Class<?> clazz, boolean allowReaders, boolean allowWriters) {
        for (Method fieldMethod : findAnnotatedMethods(clazz, ThriftField.class)) {
            addMethod(clazz, fieldMethod, allowReaders, allowWriters);
        }

        // find invalid methods not skipped by findAnnotatedMethods()
        for (Method method : getAllDeclaredMethods(clazz)) {
            if (method.isAnnotationPresent(ThriftField.class) || hasThriftFieldAnnotation(method)) {
                if (!Modifier.isPublic(method.getModifiers())) {
                    metadataErrors.addError("@ThriftField method [%s] is not public", method.toGenericString());
                }
                if (Modifier.isStatic(method.getModifiers())) {
                    metadataErrors.addError("@ThriftField method [%s] is static", method.toGenericString());
                }
            }
        }
    }

    private void addMethod(Class<?> clazz, Method method, boolean allowReaders, boolean allowWriters) {
        checkArgument(method.isAnnotationPresent(ThriftField.class));

        ThriftField annotation = method.getAnnotation(ThriftField.class);

        // verify parameters
        if (isValidateGetter(method)) {
            if (allowReaders) {
                MethodExtractor methodExtractor = new MethodExtractor(method, annotation);
                fields.add(methodExtractor);
                extractors.add(methodExtractor);
            } else {
                metadataErrors.addError("Reader method %s.%s is not allowed on a builder class", clazz.getName(),
                        method.getName());
            }
        } else if (isValidateSetter(method)) {
            if (allowWriters) {
                List<ParameterInjection> parameters;
                if (method.getParameterTypes().length > 1 || Iterables.any(
                        asList(method.getParameterAnnotations()[0]), Predicates.instanceOf(ThriftField.class))) {
                    parameters = getParameterInjections(method.getParameterAnnotations(),
                            method.getGenericParameterTypes(), extractParameterNames(method));
                    if (annotation.value() != Short.MIN_VALUE) {
                        metadataErrors.addError(
                                "A method with annotated parameters can not have a field id specified: %s.%s ",
                                clazz.getName(), method.getName());
                    }
                    if (!annotation.name().isEmpty()) {
                        metadataErrors.addError(
                                "A method with annotated parameters can not have a field name specified: %s.%s ",
                                clazz.getName(), method.getName());
                    }
                    if (annotation.required()) {
                        metadataErrors.addError(
                                "A method with annotated parameters can not be marked as required: %s.%s ",
                                clazz.getName(), method.getName());
                    }
                } else {
                    parameters = ImmutableList.of(new ParameterInjection(0, annotation, extractFieldName(method),
                            method.getGenericParameterTypes()[0]));
                }
                fields.addAll(parameters);
                methodInjections.add(new MethodInjection(method, parameters));
            } else {
                metadataErrors.addError(
                        "Inject method %s.%s is not allowed on struct class, since struct has a builder",
                        clazz.getName(), method.getName());
            }
        } else {
            metadataErrors.addError("Method %s.%s is not a supported getter or setter", clazz.getName(),
                    method.getName());
        }
    }

    private boolean hasThriftFieldAnnotation(Method method) {
        for (Annotation[] parameterAnnotations : method.getParameterAnnotations()) {
            for (Annotation parameterAnnotation : parameterAnnotations) {
                if (parameterAnnotation instanceof ThriftField) {
                    return true;
                }
            }
        }
        return false;
    }

    private static String extractFieldName(Method method) {
        checkNotNull(method, "method is null");
        String methodName = method.getName();
        if ((methodName.startsWith("get") || methodName.startsWith("set")) && methodName.length() > 3) {
            String name = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
            return name;
        } else if (methodName.startsWith("is") && methodName.length() > 2) {
            String name = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
            return name;
        } else {
            return methodName;
        }

    }

    private boolean isValidateGetter(Method method) {
        return method.getParameterTypes().length == 0 && method.getReturnType() != void.class;
    }

    private boolean isValidateSetter(Method method) {
        return method.getParameterTypes().length >= 1;
    }

    private List<ParameterInjection> getParameterInjections(Annotation[][] parameterAnnotations,
            Type[] parameterTypes, String[] parameterNames) {
        List<ParameterInjection> parameters = newArrayListWithCapacity(parameterAnnotations.length);
        for (int parameterIndex = 0; parameterIndex < parameterAnnotations.length; parameterIndex++) {
            Annotation[] annotations = parameterAnnotations[parameterIndex];
            Type parameterType = parameterTypes[parameterIndex];

            ThriftField thriftField = null;
            for (Annotation annotation : annotations) {
                if (annotation instanceof ThriftField) {
                    thriftField = (ThriftField) annotation;
                }
            }

            ParameterInjection parameterInjection = new ParameterInjection(parameterIndex, thriftField,
                    parameterNames[parameterIndex], parameterType);

            parameters.add(parameterInjection);
        }
        return parameters;
    }

    private void normalizeThriftFields(ThriftCatalog catalog) {
        // assign all fields an id (if possible)
        Set<String> fieldsWithConflictingIds = inferThriftFieldIds();

        // group fields by id
        Multimap<Optional<Short>, FieldMetadata> fieldsById = Multimaps.index(fields, getThriftFieldId());
        for (Entry<Optional<Short>, Collection<FieldMetadata>> entry : fieldsById.asMap().entrySet()) {
            Collection<FieldMetadata> fields = entry.getValue();

            // fields must have an id
            if (!entry.getKey().isPresent()) {
                for (String fieldName : newTreeSet(transform(fields, getOrExtractThriftFieldName()))) {
                    // only report errors for fields that don't have conflicting ids
                    if (!fieldsWithConflictingIds.contains(fieldName)) {
                        metadataErrors.addError("ThriftStruct %s fields %s do not have an id", structName,
                                newTreeSet(transform(fields, getOrExtractThriftFieldName())));
                    }
                }
                continue;
            }
            short fieldId = entry.getKey().get();

            // assure all fields for this ID have the same name
            String fieldName = extractFieldName(fieldId, fields);
            for (FieldMetadata field : fields) {
                field.setName(fieldName);
            }

            // verify fields have a supported java type and all fields
            // for this ID have the same thrift type
            verifyFieldType(fieldId, fieldName, fields, catalog);
        }
    }

    /**
     * Assigns all fields an id if possible.  Fields are grouped by name and for each group, if there
     * is a single id, all fields in the group are assigned this id.  If the group has multiple ids,
     * an error is reported.
     */
    private Set<String> inferThriftFieldIds() {
        Set<String> fieldsWithConflictingIds = new HashSet<>();

        // group fields by explicit name or by name extracted from field, method or property
        Multimap<String, FieldMetadata> fieldsByExplicitOrExtractedName = Multimaps.index(fields,
                getOrExtractThriftFieldName());
        inferThriftFieldIds(fieldsByExplicitOrExtractedName, fieldsWithConflictingIds);

        // group fields by name extracted from field, method or property
        // this allows thrift name to be set explicitly without having to duplicate the name on getters and setters
        // todo should this be the only way this works?
        Multimap<String, FieldMetadata> fieldsByExtractedName = Multimaps.index(fields, extractThriftFieldName());
        inferThriftFieldIds(fieldsByExtractedName, fieldsWithConflictingIds);

        return fieldsWithConflictingIds;
    }

    private void inferThriftFieldIds(Multimap<String, FieldMetadata> fieldsByName,
            Set<String> fieldsWithConflictingIds) {
        // for each name group, set the ids on the fields without ids
        for (Entry<String, Collection<FieldMetadata>> entry : fieldsByName.asMap().entrySet()) {
            Collection<FieldMetadata> fields = entry.getValue();

            // skip all entries without a name or singleton groups... we'll deal with these later
            if (fields.size() <= 1) {
                continue;
            }

            // all ids used by this named field
            Set<Short> ids = ImmutableSet.copyOf(Optional.presentInstances(transform(fields, getThriftFieldId())));

            // multiple conflicting ids
            if (ids.size() > 1) {
                String fieldName = entry.getKey();
                if (!fieldsWithConflictingIds.contains(fieldName)) {
                    metadataErrors.addError("ThriftStruct '%s' field '%s' has multiple ids: %s", structName,
                            fieldName, ids);
                    fieldsWithConflictingIds.add(fieldName);
                }
                continue;
            }

            // single id, so set on all fields in this group (groups with no id are handled later)
            if (ids.size() == 1) {
                // propagate the id to all fields in this group
                short id = Iterables.getOnlyElement(ids);
                for (FieldMetadata field : fields) {
                    field.setId(id);
                }
            }
        }
    }

    private String extractFieldName(short id, Collection<FieldMetadata> fields) {
        // get the names used by these fields
        Set<String> names = ImmutableSet.copyOf(filter(transform(fields, getThriftFieldName()), notNull()));

        String name;
        if (!names.isEmpty()) {
            if (names.size() > 1) {
                metadataErrors.addWarning("ThriftStruct %s field %s has multiple names %s", structName, id, names);
            }
            name = names.iterator().next();
        } else {
            // pick a name for this field
            name = Iterables.find(transform(fields, extractThriftFieldName()), notNull());
        }
        return name;
    }

    /**
     * Verifies that the the fields all have a supported Java type and that all fields map to the
     * exact same ThriftType.
     */
    private void verifyFieldType(short id, String name, Collection<FieldMetadata> fields, ThriftCatalog catalog) {
        boolean isSupportedType = true;
        for (FieldMetadata field : fields) {
            if (!catalog.isSupportedStructFieldType(field.getJavaType())) {
                metadataErrors.addError("ThriftStruct %s field %s(%s) type %s is not a supported Java type",
                        structName, name, id, TypeToken.of(field.getJavaType()));
                isSupportedType = false;
                // only report the error once
                break;
            }
        }

        // fields must have the same type
        if (isSupportedType) {
            Set<ThriftType> types = new HashSet<>();
            for (FieldMetadata field : fields) {
                types.add(catalog.getThriftType(field.getJavaType()));
            }
            if (types.size() > 1) {
                metadataErrors.addWarning("ThriftStruct %s field %s(%s) has multiple types %s", structName, name,
                        id, types);
            }
        }
    }

    //
    // Build final metadata
    //

    public ThriftStructMetadata<T> build() {
        // this code assumes that metadata is clean
        metadataErrors.throwIfHasErrors();

        // builder constructor injection
        ThriftMethodInjection builderMethodInjection = buildBuilderConstructorInjections();

        // constructor injection (or factory method for builder)
        ThriftConstructorInjection constructorInjection = buildConstructorInjections();

        // fields injections
        Iterable<ThriftFieldMetadata> fieldsMetadata = buildFieldInjections();

        // methods injections
        List<ThriftMethodInjection> methodInjections = buildMethodInjections();

        return new ThriftStructMetadata<>(structName, structClass, builderClass, builderMethodInjection,
                ImmutableList.copyOf(fieldsMetadata), constructorInjection, methodInjections);
    }

    private ThriftMethodInjection buildBuilderConstructorInjections() {
        ThriftMethodInjection builderMethodInjection = null;
        if (builderClass != null) {
            MethodInjection builderMethod = builderMethodInjections.get(0);
            builderMethodInjection = new ThriftMethodInjection(builderMethod.getMethod(),
                    buildParameterInjections(builderMethod.getParameters()));
        }
        return builderMethodInjection;
    }

    private ThriftConstructorInjection buildConstructorInjections() {
        ConstructorInjection constructor = constructorInjections.get(0);
        return new ThriftConstructorInjection(constructor.getConstructor(),
                buildParameterInjections(constructor.getParameters()));
    }

    private Iterable<ThriftFieldMetadata> buildFieldInjections() {
        Multimap<Optional<Short>, FieldMetadata> fieldsById = Multimaps.index(fields, getThriftFieldId());
        return Iterables.transform(fieldsById.asMap().values(),
                new Function<Collection<FieldMetadata>, ThriftFieldMetadata>() {
                    @Override
                    public ThriftFieldMetadata apply(Collection<FieldMetadata> input) {
                        checkArgument(!input.isEmpty(), "input is empty");
                        return buildField(input);
                    }
                });
    }

    private ThriftFieldMetadata buildField(Collection<FieldMetadata> input) {
        short id = -1;
        String name = null;
        ThriftType type = null;

        // process field injections and extractions
        ImmutableList.Builder<ThriftInjection> injections = ImmutableList.builder();
        ThriftExtraction extraction = null;
        for (FieldMetadata fieldMetadata : input) {
            id = fieldMetadata.getId();
            name = fieldMetadata.getName();
            type = catalog.getThriftType(fieldMetadata.getJavaType());

            if (fieldMetadata instanceof FieldInjection) {
                FieldInjection fieldInjection = (FieldInjection) fieldMetadata;
                injections.add(new ThriftFieldInjection(fieldInjection.getId(), fieldInjection.getName(),
                        fieldInjection.getField()));
            } else if (fieldMetadata instanceof ParameterInjection) {
                ParameterInjection parameterInjection = (ParameterInjection) fieldMetadata;
                injections
                        .add(new ThriftParameterInjection(parameterInjection.getId(), parameterInjection.getName(),
                                parameterInjection.getParameterIndex(), fieldMetadata.getJavaType()));
            } else if (fieldMetadata instanceof FieldExtractor) {
                FieldExtractor fieldExtractor = (FieldExtractor) fieldMetadata;
                extraction = new ThriftFieldExtractor(fieldExtractor.getId(), fieldExtractor.getName(),
                        fieldExtractor.getField());
            } else if (fieldMetadata instanceof MethodExtractor) {
                MethodExtractor methodExtractor = (MethodExtractor) fieldMetadata;
                extraction = new ThriftMethodExtractor(methodExtractor.getId(), methodExtractor.getName(),
                        methodExtractor.getMethod());
            }
        }

        // add type coercion
        TypeCoercion coercion = null;
        if (type.isCoerced()) {
            coercion = catalog.getDefaultCoercion(type.getJavaType());
        }

        ThriftFieldMetadata thriftFieldMetadata = new ThriftFieldMetadata(id, type, name, injections.build(),
                extraction, coercion);
        return thriftFieldMetadata;
    }

    private List<ThriftMethodInjection> buildMethodInjections() {
        return Lists.transform(methodInjections, new Function<MethodInjection, ThriftMethodInjection>() {
            @Override
            public ThriftMethodInjection apply(MethodInjection injection) {
                return new ThriftMethodInjection(injection.getMethod(),
                        buildParameterInjections(injection.getParameters()));
            }
        });
    }

    private List<ThriftParameterInjection> buildParameterInjections(List<ParameterInjection> parameters) {
        return Lists.transform(parameters, new Function<ParameterInjection, ThriftParameterInjection>() {
            @Override
            public ThriftParameterInjection apply(ParameterInjection injection) {
                return new ThriftParameterInjection(injection.getId(), injection.getName(),
                        injection.getParameterIndex(), injection.getJavaType());
            }
        });
    }

    static abstract class FieldMetadata {
        private Short id;
        private String name;

        private FieldMetadata(ThriftField annotation) {
            if (annotation != null) {
                if (annotation.value() != Short.MIN_VALUE) {
                    id = annotation.value();
                }
                if (!annotation.name().isEmpty()) {
                    name = annotation.name();
                }
            }
        }

        public Short getId() {
            return id;
        }

        public void setId(short id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public abstract Type getJavaType();

        public abstract String extractName();

        static <T extends FieldMetadata> Function<T, Optional<Short>> getThriftFieldId() {
            return new Function<T, Optional<Short>>() {
                @Override
                public Optional<Short> apply(@Nullable T input) {
                    if (input == null) {
                        return Optional.absent();
                    }
                    Short value = input.getId();
                    return Optional.fromNullable(value);
                }
            };
        }

        static <T extends FieldMetadata> Function<T, String> getThriftFieldName() {
            return new Function<T, String>() {
                @Override
                public String apply(@Nullable T input) {
                    if (input == null) {
                        return null;
                    }
                    return input.getName();
                }
            };
        }

        static <T extends FieldMetadata> Function<T, String> getOrExtractThriftFieldName() {
            return new Function<T, String>() {
                @Override
                public String apply(@Nullable T input) {
                    if (input == null) {
                        return null;
                    }
                    String name = input.getName();
                    if (name == null) {
                        name = input.extractName();
                    }
                    if (name == null) {
                        throw new NullPointerException(String.valueOf("name is null"));
                    }
                    return name;
                }
            };
        }

        static <T extends FieldMetadata> Function<T, String> extractThriftFieldName() {
            return new Function<T, String>() {
                @Override
                public String apply(@Nullable T input) {
                    if (input == null) {
                        return null;
                    }
                    return input.extractName();
                }
            };
        }
    }

    private static abstract class Extractor extends FieldMetadata {
        protected Extractor(ThriftField annotation) {
            super(annotation);
        }
    }

    private static class FieldExtractor extends Extractor {
        private final Field field;

        private FieldExtractor(Field field, ThriftField annotation) {
            super(annotation);
            this.field = field;
        }

        public Field getField() {
            return field;
        }

        @Override
        public String extractName() {
            return field.getName();
        }

        @Override
        public Type getJavaType() {
            return field.getGenericType();
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("FieldExtractor");
            sb.append("{field=").append(field);
            sb.append('}');
            return sb.toString();
        }
    }

    public static class MethodExtractor extends Extractor {
        private final Method method;

        public MethodExtractor(Method method, ThriftField annotation) {
            super(annotation);
            this.method = method;
        }

        public Method getMethod() {
            return method;
        }

        @Override
        public String extractName() {
            return extractFieldName(method);
        }

        @Override
        public Type getJavaType() {
            return method.getGenericReturnType();
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("MethodExtractor");
            sb.append("{method=").append(method);
            sb.append('}');
            return sb.toString();
        }
    }

    private static abstract class Injection extends FieldMetadata {
        protected Injection(ThriftField annotation) {
            super(annotation);
        }
    }

    private static class FieldInjection extends Injection {
        private final Field field;

        private FieldInjection(Field field, ThriftField annotation) {
            super(annotation);
            this.field = field;
        }

        public Field getField() {
            return field;
        }

        @Override
        public String extractName() {
            return field.getName();
        }

        @Override
        public Type getJavaType() {
            return field.getGenericType();
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("FieldInjection");
            sb.append("{field=").append(field);
            sb.append('}');
            return sb.toString();
        }
    }

    public class ConstructorInjection {
        private final Constructor<?> constructor;
        private final List<ParameterInjection> parameters;

        public ConstructorInjection(Constructor<?> constructor, List<ParameterInjection> parameters) {
            this.constructor = constructor;
            this.parameters = ImmutableList.copyOf(parameters);
        }

        public ConstructorInjection(Constructor<?> constructor, ParameterInjection... parameters) {
            this.constructor = constructor;
            this.parameters = ImmutableList.copyOf(parameters);
        }

        public Constructor<?> getConstructor() {
            return constructor;
        }

        public List<ParameterInjection> getParameters() {
            return parameters;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("ConstructorInjection");
            sb.append("{constructor=").append(constructor);
            sb.append(", parameters=").append(parameters);
            sb.append('}');
            return sb.toString();
        }
    }

    public class MethodInjection {
        private final Method method;
        private final List<ParameterInjection> parameters;

        public MethodInjection(Method method, List<ParameterInjection> parameters) {
            this.method = method;
            this.parameters = ImmutableList.copyOf(parameters);
        }

        public Method getMethod() {
            return method;
        }

        public List<ParameterInjection> getParameters() {
            return parameters;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("MethodInjection");
            sb.append("{method=").append(method);
            sb.append(", parameters=").append(parameters);
            sb.append('}');
            return sb.toString();
        }
    }

    private static class ParameterInjection extends Injection {
        private final int parameterIndex;
        private final String extractedName;
        private final Type parameterJavaType;

        private ParameterInjection(int parameterIndex, ThriftField annotation, String extractedName,
                Type parameterJavaType) {
            super(annotation);
            checkNotNull(parameterJavaType, "parameterJavaType is null");

            this.parameterIndex = parameterIndex;
            this.extractedName = extractedName;
            this.parameterJavaType = parameterJavaType;
            if (void.class.equals(parameterJavaType)) {
                throw new AssertionError();
            }
            checkArgument(getName() != null || extractedName != null,
                    "Parameter must have an explicit name or an extractedName");
        }

        public int getParameterIndex() {
            return parameterIndex;
        }

        @Override
        public String extractName() {
            return extractedName;
        }

        @Override
        public Type getJavaType() {
            return parameterJavaType;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("ParameterInjection");
            sb.append("{parameterIndex=").append(parameterIndex);
            sb.append(", extractedName='").append(extractedName).append('\'');
            sb.append(", parameterJavaType=").append(parameterJavaType);
            sb.append('}');
            return sb.toString();
        }
    }
}